vlambda博客
学习文章列表

JDK17 |java17学习 第 1 章 Java 17 入门

Chapter 2: Java Object-Oriented Programming (OOP)

面向对象编程(OOP)的诞生是为了更好地控制共享数据的并发修改,这是预 OOP 编程的诅咒。这个想法的核心是不允许直接访问数据,而是只通过专用的代码层来实现。由于需要在过程中传递和修改数据,因此构想了对象的概念。在最一般的意义上,object 是一组数据,只能通过传递的方法集传递和访问。据说这些数据构成了对象状态,而方法构成了对象行为。对象状态被隐藏(封装),无法直接访问。

每个对象都是基于称为 class 的特定模板构建的。换句话说,一个类定义了一类对象。每个对象都有一个特定的接口,这是对其他对象与其交互方式的正式定义。最初,一个对象会通过调用其方法向另一个对象发送消息。但是这个术语并不成立,尤其是在引入了实际的基于消息的协议和系统之后。

为了避免代码重复,引入了对象之间的父子关系——一个类可以从另一个类继承行为。在这种关系中,第一个类称为子类,或子类,而第二个类称为父类、基类或超类。

在类和接口之间定义了另一种形式的关系——一个类可以实现一个接口。由于接口描述了您如何与对象交互,而不是对象如何响应交互,因此不同的对象在实现相同的接口时可以表现不同。

在 Java 中,一个类只能有一个直接父级,但可以实现多个接口。

表现得像它的任何祖先并遵守多个接口的能力称为多态性。

在本章中,我们将了解这些 OOP 概念以及它们是如何在 Java 中实现的。讨论的主题包括:

  • 面向对象的概念
  • 班级
  • 界面
  • 重载、覆盖和隐藏
  • final 变量、方法和类
  • 记录和密封类
  • 多态性在行动

Technical requirements

为了能够执行本章提供的代码示例,您将需要以下内容:

  • 装有 Microsoft Windows、Apple macOS 或 Linux 操作系统的计算机
  • Java SE 版本 17 或更高版本
  • 您喜欢的 IDE 或代码编辑器

本书第 1 章Java 17 入门。本章的代码示例文件可在 GitHub 存储库中获得,地址为 https:// /github.com/PacktPublishing/Learn-Java-17-Programming.gitexamples/src/main/java/com/packt/learnjava/ch02_oop 文件夹中.

OOP concepts

正如我们在介绍中已经说过的,主要的 OOP 概念如下:

  • 类:定义对象的属性和行为(方法)是基于这个类创建的。
  • 对象:这将状态(数据)定义为其属性的值,添加所采取的行为(方法) 来自一个类,并将它们保持在一起。
  • 继承:这会将行为沿通过父级连接的类的链传播孩子的关系。
  • 接口:这描述了如何 访问对象数据和行为。它将对象的外观 与其实现(行为)隔离(抽象)。
  • 封装:隐藏状态实现的细节。
  • 多态性:这允许对象呈现 已实现 接口的外观和表现得像任何祖先类。

Object/class

原则上,您可以使用最少的类和对象创建一个非常强大的应用程序。在函数式编程 被添加到 Java 8 和 JDK 之后,它变得 变得更容易了,它允许您四处传递行为 作为函数。然而传递数据(状态)仍然需要类/对象。这意味着 Java 作为 OOP 语言的地位保持不变。

一个类定义了保持对象状态的所有内部对象属性的类型。一个类还定义了由方法代码表达的对象行为。可以有一个没有状态或行为的类/对象。 Java 还提供了一种使行为可静态访问的规定——无需创建对象。但是这些可能性只不过是对对象/类概念的补充,该概念是为了将状态和行为保持在一起而引入的。

为了说明这一概念,例如,Vehicle 类在原则上定义了车辆的属性和行为。让我们简化模型并假设车辆只有两个属性——重量和一定功率的发动机。它也可以有一定的行为——它可以在一定的时间内达到一定的速度,这取决于它的两个属性的值。这种行为可以用一种计算车辆在特定时间段内可以达到的速度的方法来表达。 Vehicle 类的每个对象都会有一个特定的状态(其属性的值),并且速度计算将导致同一时间段内的不同速度。

所有 Java 代码 都包含在方法中。 方法是一组语句,具有(可选)输入参数并返回一个值(也是可选的)。此外,每个 方法都可能有副作用——例如,它可以显示 一条消息或将数据写入数据库。类/对象行为在方法中实现。

例如,按照我们的示例,速度计算可以驻留在 double calculateSpeed(float seconds) 方法中。你可以猜到,这个方法的名字是calculateSpeed。它接受秒数(带有小数部分)作为参数,并以 double 形式返回速度值。

Inheritance

正如我们已经提到的,对象可以建立父子关系并共享属性 和这种方式的行为。例如,我们可以创建一个 Car 类,该类继承 Vehicle 类。此外,child 类可以有自己的属性(例如乘客数量)和特定于汽车的行为(例如软减震)。但是,如果我们创建一个 Truck 类作为车辆的子类,它的附加卡车特定属性(例如有效载荷)和行为(硬减震)将会有所不同。

据说 CarTruck 类的每个对象都有一个 的父对象车辆 类。但是 CarTruck 类的对象不共享特定的 Vehicle对象(每次创建子对象时,都会先创建一个新的父对象)。他们只分享父母的行为。这就是为什么所有子对象可以具有相同的行为但不同的状态。这是实现代码可重用性的一种方法,但当对象行为必须动态更改时,它可能不够灵活。在这种情况下,对象组合(从其他类引入行为)或函数式编程更合适(参见 第 13 章函数式编程)。

有可能使孩子的行为与继承的行为不同。为了实现它,可以在 child 类中重新实现捕获行为的方法。据说孩子可以覆盖继承的行为。我们将很快解释如何做到这一点(参见重载、覆盖和隐藏部分)。例如,如果Car类有自己的速度计算方法,则Vehicle父类的对应方法 没有被继承,新的速度计算,在child类中实现,改为使用。

父类的属性也可以被继承(但不能被覆盖)。然而,类属性通常被声明为私有的;它们不能被继承——这就是封装的重点。查看各种访问级别的描述 - publicprotecteddefault、和 private - 在 访问修饰符 部分Programming/9781803241432/4" linkend="ch04lvl1sec24">第 3 章Java 基础知识

如果父类从另一个类继承了某些行为,那么 child 类也会获取(继承)这种行为,当然,除非父类覆盖它。继承链的长度没有限制。

Java 中的父子关系使用 extends 关键字表示:

class A { }
class B extends A { }
class C extends B { }
class D extends C { }

在此代码中,ABCD 类有如下关系:

  • D 类继承自 ABC 类。
  • C 类继承自 AB 类。
  • B 类继承自 A 类。

A 类的所有非私有方法都由 BCD 类。

B 类的所有非私有方法都由 CD 类。

C 类的所有非私有方法都由 D 类继承(如果没有被覆盖)。

Abstraction/interface

方法的名称 及其参数类型列表称为方法签名。它描述了一个对象(CarTruck 的行为,在我们的示例中)可以访问。这样的描述与 return 类型一起作为接口呈现。它没有说明执行计算的代码 - 仅说明方法名称、参数类型、它们在参数列表中的位置以及结果类型。所有实现细节都隐藏(封装)在实现此接口的类中。

正如我们已经提到的,一个类可以实现许多不同的接口。但是两个不同的类(和它们的对象)即使实现了相同的接口也可以表现不同。

与类类似,接口也可以使用 extends 关键字建立父子关系:

interface A { }
interface B extends A {}
interface C extends B {}
interface D extends C {}

在此代码中,ABCD 接口有如下关系:

  • D 接口继承自 ABC 接口。
  • C 接口继承自 AB 接口。
  • B 接口继承自 A 接口。

A接口的所有非私有方法都被BC D 接口。

B 接口的所有非私有方法都被 CD 继承代码>接口。

C 接口的所有非私有方法都由 D 接口继承。

抽象/接口还减少了代码不同部分之间的依赖关系,从而提高了它的可维护性。只要接口保持不变,每个类都可以更改,而无需与其客户协调。

Encapsulation

封装通常被定义为数据隐藏或一组可公开访问的方法和私有可访问的数据。从广义上讲,封装是控制 对对象属性的访问。

对象 属性值的快照称为对象状态。这是被封装的数据。因此,封装解决了推动创建面向对象编程的主要问题——更好地管理对共享数据的并发访问,例如:

class A {
  private String prop = "init value";
  public void setProp(String value){
     prop = value;
  }
  public String getProp(){
     return prop;
  }
}

如您所见,要读取或修改 prop 属性的值,我们无法直接访问它,因为 private 访问修饰符。相反,我们只能通过 setProp(String value)getProp() 方法来完成。

Polymorphism

多态性是 对象作为不同 类的对象或作为不同接口的实现的能力。它的存在归功于前面提到的所有概念——继承、接口和封装。没有它们,多态性是不可能的。

继承允许对象获取或覆盖其所有祖先的行为。接口对客户端代码隐藏了实现它的类的名称。封装防止暴露对象状态。

在接下来的部分中,我们将演示所有这些概念的实际应用,并查看 的具体用法="italic">作用中的多态性部分。

Class

Java 程序 是表示可执行操作的语句序列。语句按方法组织,方法按类组织。一个或多个类存储在 .java 文件中。它们可以由 javac Java 编译器编译(从 Java 语言转换为字节码)并存储在 .class 文件中。每个.class文件只包含一个编译好的类,可以被JVM执行。

java 命令启动 JVM 并告诉它哪个类是 main 类,具有称为 main()main 方法有一个特定的声明——它必须是 public static,必须返回 void ,名称为 main,并接受 String 类型数组的单个参数。

JVM 将主类加载到内存中,找到 main() 方法,并开始逐句执行它。 java 命令还可以传递 main() 方法作为 字符串值。如果 JVM 遇到需要执行另一个类的方法的语句,该类(它的 .class 文件)也会被加载到内存中并执行相应的方法。因此,Java 程序流程就是加载类并执行它们的方法。

以下是 main 类的示例:

public class MyApp {
  public static void main(String[] args){
     AnotherClass an = new AnotherClass();
     for(String s: args){
        an.display(s);
     }
   }
}

它表示一个非常简单的 应用程序,它接收任意数量的参数并将它们一个一个地传递到 display() 方法中AnotherClass 类。当 JVM 启动时,它首先从 MyApp.class 文件加载 MyApp 类。然后,它从 AnotherClass.class 文件中加载 AnotherClass 类,使用 new 运算符(我们稍后会谈到),并在其上调用 display() 方法。

这是 AnotherClass 类:

public class AnotherClass {
   private int result;
   public void display(String s){
      System.out.println(s);
   }
   public int process(int i){
      result = i *2;
      return result;
   }
   public int getResult(){
      return result;
   }
} 

如您所见,display() 方法仅用于其副作用——它打印出传入的值并且不返回任何内容(无效)。 AnotherClass 类还有另外两种方法:

  • process() 方法将输入整数加倍,将其存储在其 result 属性中,并将值返回给调用者。
  • getResult() 方法允许您在以后的任何时间从对象中获取结果。

这两个方法 在我们的演示应用程序中没有使用。我们展示它们只是为了表明一个类可以具有属性(在本例中为 result)和许多其他方法。

private 关键字使值只能从类内部,从其方法访问。 public 关键字使任何其他类都可以访问属性或方法。

Method

正如我们已经 所述,Java 语句 被组织为方法:

<return type> <method name>(<list of parameter types>){
     <method body that is a sequence of statements>
}

我们已经看到了一些例子。方法具有名称、一组输入参数或根本没有参数、{} 括号内的主体和返回类型或 void 关键字,表示该方法不返回任何值。

方法名称和参数列表 类型一起称为方法签名。输入 参数的数量称为arity。

重要的提示

如果两个方法在输入参数列表中具有相同的名称、相同的数量和相同的类型序列,则它们具有相同的 签名

以下两个方法具有相同的签名:

double doSomething(String s, int i){
    //some code goes here
}
double doSomething(String i, int s){
    //some code other code goes here
}

即使签名 相同,方法 中的代码也可能不同。

以下两种方法具有不同的签名:

double doSomething(String s, int i){
    //some code goes here
}
double doSomething(int s, String i){
    //some code other code goes here
}

即使方法名称保持不变,只是参数序列的变化会使签名不同。

Varargs

一种特定类型的参数 需要提及,因为它与所有其他参数完全不同。它被声明为 一个后跟三个点的类型。它被称为varargs,代表可变参数。但是,首先,让我们简要定义一下 Java 中的数组是什么。

数组是一种数据结构,其中包含 相同类型的元素。元素由数字索引引用。这就是我们现在需要知道的。我们将在 第 6 章中更详细地讨论数组 数据结构、泛型和流行实用程序

让我们从一个例子开始。让我们使用 varargs 声明方法参数:

String someMethod(String s, int i, double... arr){
 //statements that compose method body
}

当调用 someMethod 方法时,Java 编译器会将参数从左 匹配到右。一旦它到达最后一个 varargs 参数,它就会创建一个由剩余的 参数组成的数组并将其传递给方法。这是一个演示代码:

public static void main(String... args){
    someMethod("str", 42, 10, 17.23, 4);
}
private static String someMethod(
        String s, int i, double... arr){
    System.out.println(arr[0] + ", " + arr[1] + ", " + arr[2]); 
                                     //prints: 10.0, 17.23, 4.0
    return s;
}

如您所见,varargs 参数的作用类似于指定类型的数组。它可以列为方法的最后一个或唯一的参数。这就是为什么有时您可以看到声明的 main 方法,如前面的示例所示。

Constructor

创建对象 时,JVM 使用构造函数。构造函数的目的是初始化 object 状态以将值分配给所有声明的属性。如果类中没有声明构造函数,JVM 只是为属性分配默认值。我们已经讨论过原始类型的默认值——整数类型是 0,浮点类型是 0.0,以及false 用于布尔类型。对于其他 Java 引用类型(请参阅 第 3 章 , Java Fundamentals),默认值为null,表示引用类型的属性不是分配任何值。

重要的提示

当一个类中没有声明构造函数时,就说这个类有一个JVM提供的不带参数的默认构造函数。

如有必要,可以显式声明任意数量的构造函数,每个构造函数采用一组不同的参数来设置初始状态.这是一个例子:

class SomeClass {
     private int prop1;
     private String prop2;
     public SomeClass(int prop1){
         this.prop1 = prop1;
     }
     public SomeClass(String prop2){
         this.prop2 = prop2;
     }
     public SomeClass(int prop1, String prop2){
         this.prop1 = prop1;
         this.prop2 = prop2;
     }   
     // methods follow 
}

如果构造函数未设置属性,则将自动为其分配相应类型的默认值。

当多个类 沿同一条继承线相关时,首先创建父对象。如果父对象需要为其属性设置非默认初始值,则必须使用 super 关键字将其构造函数作为子构造函数的第一行调用,如下所示:

class TheParentClass {
    private int prop;
    public TheParentClass(int prop){
        this.prop = prop;
    }
    // methods follow
}
class TheChildClass extends TheParentClass{
    private int x;
    private String prop;
    private String anotherProp = "abc";
    public TheChildClass(String prop){
       super(42);
       this.prop = prop;
    }
    public TheChildClass(int arg1, String arg2){
       super(arg1);
       this.prop = arg2;
    }
    // methods follow
}

在前面的代码 示例中,我们向 TheChildClass 添加了两个构造函数——一个总是通过 42 TheParentClass 的构造函数,另一个接受两个参数的。请注意,x 属性已声明但未显式初始化。 0 类型的默认值 int "literal">TheChildClass 被创建。另外,请注意 anotherProp 属性被显式初始化为 "abc" 的值。否则,它将被初始化为 null 值,即任何引用类型的默认值,包括 String

从逻辑上讲,有三种情况不需要在类中显式定义构造函数:

  • 当对象及其任何父对象都没有需要初始化的属性时
  • 当每个属性与类型声明一起初始化时(例如,int x = 42
  • 当属性初始化的默认值足够好时

尽管如此,即使满足所有三个条件(在列表中提到),仍然可能实现构造函数。例如,您可能希望执行一些语句来初始化一些外部资源(文件或另一个数据库),一旦创建对象就会需要这些资源。

一旦添加了显式构造函数,就不会提供默认构造函数,并且以下代码会生成错误:

class TheParentClass {
    private int prop;
    public TheParentClass(int prop){
        this.prop = prop;
    }
    // methods follow
}
class TheChildClass extends TheParentClass{
    private String prop;
    public TheChildClass(String prop){
        //super(42);  //No call to the parent's constructor
        this.prop = prop;
    }
    // methods follow
}

为避免该错误,请向 TheParentClass 添加不带参数的构造函数 或调用显式< /a> 父类的构造函数作为子类构造函数的第一条语句。以下代码不会产生错误:

class TheParentClass {
    private int prop;
    public TheParentClass() {}
    public TheParentClass(int prop){
        this.prop = prop;
    }
    // methods follow
}
class TheChildClass extends TheParentClass{
    private String prop;
    public TheChildClass(String prop){
        this.prop = prop;
    }
    // methods follow
}

需要注意的一个重要方面是构造函数,虽然它们看起来像方法,但不是方法,甚至不是类的成员。构造函数没有返回类型,并且总是与类同名。它的唯一目的是在创建类的新实例时调用。

The new operator

new 操作符创建一个类的对象(也可以说它实例化一个类或创建一个类的实例),通过为新对象的属性分配内存并返回对该记忆的引用。此内存引用分配给与用于创建对象的类或其父对象的类型相同类型的变量:

TheChildClass ref1 = new TheChildClass("something"); 
TheParentClass ref2 = new TheChildClass("something");

这是一个有趣的观察。在代码中,ref1ref2 对象引用都提供了对 TheChildClass 方法的访问TheParentClass。例如,我们可以为这些类添加方法,如下所示:

class TheParentClass {
    private int prop;
    public TheParentClass(int prop){
        this.prop = prop;
    }
    public void someParentMethod(){}
}
class TheChildClass extends TheParentClass{
    private String prop;
    public TheChildClass(int arg1, String arg2){
        super(arg1);
        this.prop = arg2;
    }
    public void someChildMethod(){}
}

然后,我们可以使用以下任何 引用来调用它们

TheChildClass ref1 = new TheChildClass("something");
TheParentClass ref2 = new TheChildClass("something");
ref1.someChildMethod();
ref1.someParentMethod();
((TheChildClass) ref2).someChildMethod();
ref2.someParentMethod();

请注意,要使用父类的类型引用访问子类的方法,我们必须将其强制转换为子类的类型。否则,编译器会产生错误。这是可能的,因为我们已经将子对象的引用分配给了父对象的类型引用。这就是多态的力量。我们将在 Polymorphism in action 部分详细讨论它。

自然,如果我们将父对象分配给父对象类型的变量,即使进行强制转换,我们也无法访问子对象的方法,如下例所示:

TheParentClass ref2 = new TheParentClass(42);
((TheChildClass) ref2).someChildMethod();  //compiler's error
ref2.someParentMethod();

为新的 对象分配内存的区域称为heap。 JVM 有一个名为 garbage collection 的进程,它会监视该区域的使用情况 并在有对象时释放内存以供使用不再需要。例如,看下面的方法:

void someMethod(){
   SomeClass ref = new SomeClass();
   ref.someClassMethod();
   //other statements follow
}

someMethod() 方法执行完成后,SomeClass 的对象不可访问 了。这就是垃圾收集器注意到的, 它释放了这个对象占用的内存。我们将在 Chapter 9 JVM 结构和垃圾收集

Class java.lang.Object

在 Java 中,所有的类 默认都是 Object 类的子类,即使你< /a> 不要隐式指定它。 Object 类在标准 JDK 库的 java.lang 包中声明。我们将在 Packages, importing, and access 部分定义 package 是什么,并在 第 7 章 Java 标准和外部库

让我们回顾一下我们在 Inheritance 部分提供的示例:

class A { }
class B extends A {}
class C extends B {}
class D extends C {}

所有类,ABCDObject 类的子类,每个类都继承了 10 个方法:

  • public String toString()
  • public int hashCode()
  • public boolean equals (Object obj)
  • public Class getClass()
  • 受保护对象 clone()
  • public void notify()
  • public void notifyAll()
  • public void wait()
  • public void wait(long timeout)
  • public void wait(long timeout, int nanos)

前三个,toString()hashCode()equals() 是最常用的方法,并且经常被重新实现(覆盖)。 toString() 方法通常 用于打印状态目的。它在 JDK 中的默认实现如下所示:

public String toString() {
   return getClass().getName()+"@"+
                     Integer.toHexString(hashCode());
}

如果我们在 TheChildClass 类的对象上使用它,结果会如下:

TheChildClass ref1 = new TheChildClass("something");
System.out.println(ref1.toString());  
//prints: com.packt.learnjava.ch02_oop.
//Constructor$TheChildClass@72ea2f77

顺便说一句,在将对象传递给 System.out.println()toString() > 方法和类似的输出方法,因为无论如何它们都是在方法内部执行的,并且在我们的例子中,System.out.println(ref1) 会产生相同的结果。

因此,如您所见,这样的输出对人类不友好,因此最好重写 toString() 方法。最简单的 方法是使用 IDE。例如,在 IntelliJ IDEA 中,在 TheChildClass 代码内单击鼠标右键,如下图所示:

JDK17 |java17学习 第 1 章 Java 17 入门

选择并点击Generate...,然后选择并点击toString(),如下图所示:

JDK17 |java17学习 第 1 章 Java 17 入门

新的弹出窗口将使您能够选择您希望包含在 toString() 方法中的属性。只选择TheChildClass的属性,如下:

JDK17 |java17学习 第 1 章 Java 17 入门

点击OK按钮后,会生成如下代码:

@Override
public String toString() {
    return "TheChildClass{" +
            "prop='" + prop + '\'' +
            '}';
}

如果类中有更多属性并且您已选择它们,则方法输出中将包含更多属性及其值。如果我们现在打印对象,结果将是这样的:

TheChildClass ref1 = new TheChildClass("something");
System.out.println(ref1.toString());  
                      //prints: TheChildClass{prop='something'}

这就是为什么 toString() 方法经常被覆盖甚至包含在 IDE 的服务中的原因。

hashCode()equals() 方法将在 第 6 章数据结构,泛型和流行的实用程序

getClass()clone() 方法不常用。 getClass() 方法返回 Class 类的对象,该对象具有许多提供各种系统信息的方法。最常用的方法是返回当前对象的类名的方法。 clone() 方法可用于复制当前的 对象。只要当前对象 的所有属性都是原始类型,它就可以正常工作。但是,如果存在引用类型属性,则必须重新实现 clone() 方法,以便正确完成引用类型的复制。否则,只会复制引用,而不是对象本身。这样的副本称为浅副本,在某些情况下可能已经足够了。 protected 关键字表示只有类的子级可以访问它。请参阅包、导入和访问部分。

Object 类的最后五个方法用于线程之间的通信——用于并发处理的轻量级进程。它们通常不会重新实现。

Instance and static properties and methods

到目前为止,我们已经见过大部分方法 只能在 对象上调用 (实例) 一个类。此类方法称为实例方法。它们通常使用对象属性(对象状态)的值。否则,如果它们不使用对象状态,则可以将它们设置为 static 并在不创建对象的情况下调用它们。这种方法的一个例子是 main() 方法。这是另一个例子:

class SomeClass{
    public static void someMethod(int i){
        //do something
    }
}

该方法可以如下调用:

SomeClass.someMethod(42);

重要的提示

静态方法也可以在对象上调用,但它被认为是不好的做法,因为它隐藏了方法的静态特性,不让人们试图理解代码。此外,它会引发编译器警告,并且根据编译器的实现,甚至可能会产生编译器错误。

同样,属性 可以声明为静态 并因此 可访问 没有 创建 对象,例如 作为 如下:

class SomeClass{
    public static String SOME_PROPERTY = "abc";
}

该属性也可以通过类直接访问,如下:

System.out.println(SomeClass.SOME_PROPERTY);  //prints: abc

拥有这样一个静态属性违背了状态封装的思想,并且可能导致并发数据修改的所有问题,因为它作为一个副本存在于 JVM 内存中,并且所有使用它的方法共享相同的值。这就是为什么静态属性通常用于两个目的:

  • 存储一个常量——一个可以读取但不能修改的值(也称为只读值)
  • 存储创建成本高或保留只读值的无状态对象

常量的一个典型示例是资源的名称:

class SomeClass{
    public static final String INPUT_FILE_NAME = "myFile.csv";
}

注意静态属性前面的 final 关键字。它告诉编译器和 JVM,这个值一旦分配就不能改变。尝试这样做会产生错误。它有助于保护该值并清楚地表达将该值作为常数的意图。当人们试图理解代码是如何工作的时,这些看似很小的细节会让代码更容易理解。

也就是说,考虑 将接口用于这样的 目的。自 Java 1.8 起,接口 中声明的所有字段 都是隐式静态的 和 final,因此您不太可能 忘记将值声明为 final。稍后我们将讨论 接口。

当一个对象被声明为静态最终类属性时,并不意味着它的所有属性都自动成为最终的。它只保护属性不分配另一个相同类型的对象。我们将在 第 8 章多线程和并发处理。然而,程序员经常使用静态最终对象来存储只读值,这些值只是在应用程序中使用的方式。一个典型的例子是应用程序配置信息。从磁盘读取后创建后,即使可以更改,也不会更改。此外,数据的缓存是从外部资源获得的。

同样,在为此目的使用此类属性之前,请考虑使用提供更多支持只读功能的默认行为的接口。

与静态属性类似,可以在不创建类实例的情况下调用静态方法。例如,考虑以下类:

class SomeClass{
    public static String someMethod() {
        return "abc";
    }
}

我们可以调用前面的< /a> 方法 使用 只是 一个类 名称:

System.out.println(SomeClass.someMethod()); //prints: abc

Interface

Abstraction/interface 部分,我们用一般的 术语讨论了接口。在本节中,我们将描述一种表达它的 Java 语言结构。

接口呈现了对象的期望。它隐藏了实现并且只公开了带有返回值的方法签名。例如,这是一个声明两个抽象方法的接口:

interface SomeInterface {
    void method1();
    String method2(int i);
}

这是一个实现它的类:

class SomeClass implements SomeInterface{
    public void method1(){
        //method body
    }
    public String method2(int i) {
        //method body
        return "abc";
    }
}

无法实例化接口。只能通过创建实现此接口的类的对象来创建接口类型的对象:

SomeInterface si = new SomeClass(); 

如果没有实现接口的所有抽象方法,则该类必须声明为抽象的,不能实例化。请参阅接口与抽象类部分。

接口不描述如何创建类的对象。要发现这一点,您必须查看该类并查看它具有哪些构造函数。接口也没有描述静态类方法。因此,接口只是类实例(对象)的公共面。

在 Java 8 中,接口获得了不仅具有抽象方法(没有主体)而且具有真正实现的能力的能力。根据 Java 语言规范,“接口的主体可以声明接口的成员,即字段、方法、类和接口。”如此宽泛的陈述提出了一个问题,接口和类之间有什么区别?我们已经指出的一个主要区别是——接口不能被实例化;只能实例化一个类。

另一个区别是在接口中实现的非静态方法被声明为 defaultprivate。相比之下,default 声明不适用于类方法。

此外,接口中的字段隐含地是公共的、静态的和最终的。相比之下,类属性和方法默认不是静态的或最终的。类本身的隐式(默认)访问修饰符、其字段、方法和构造函数是包私有的,这意味着它仅在其自己的包中可见。

Default methods

要了解接口中默认方法 的功能,让我们看一下 接口和类的示例实现它,如下:

interface SomeInterface {
    void method1();
    String method2(int i);
    default int method3(){
        return 42;
    }
}
class SomeClass implements SomeInterface{
    public void method1(){
        //method body
    }
    public String method2(int i) {
        //method body
        return "abc";
    }
}

我们现在可以创建 SomeClass 类的对象并进行以下调用:

SomeClass sc = new SomeClass();
sc.method1();
sc.method2(22);  //returns: "abc"
System.out.println(sc.method2(22)); //prints: abc
sc.method3();    //returns: 42
System.out.println(sc.method3());   //prints: 42

如您所见, method3() 没有在 SomeClass 类中实现,但看起来该类有它。这是一种 向现有类添加新方法而不 更改它的方法 - 通过将默认方法添加到接口类实现。

现在让我们将 method3() 实现也添加到类中,如下所示:

class SomeClass implements SomeInterface{
    public void method1(){
        //method body
    }
    public String method2(int i) {
        //method body
        return "abc";
    }
    public int method3(){
        return 15;
    }
}

现在,method3() 的接口实现将被忽略:

SomeClass sc = new SomeClass();
sc.method1();
sc.method2(22);  //returns: "abc"
sc.method3();    //returns: 15
System.out.println(sc.method3());      //prints: 15

重要的提示

接口中默认方法 的目的是为类(实现此接口的)提供一个新方法,而无需更改他们。但是一旦类实现了新方法,接口实现就会被忽略。

Private methods

如果一个接口中有多个默认方法,则可以创建私有方法只能被接口的默认方法访问.它们可用于包含通用功能,而不是在每个默认方法中重复它:

interface SomeInterface {
    void method1();
    String method2(int i);
    default int method3(){
        return getNumber();
    }
    default int method4(){
        return getNumber() + 22;
    }
    private int getNumber(){
        return 42;
    }
}

私有方法的概念与类中的私有方法没有什么不同(请参阅包、导入和访问部分)。无法从接口外部访问私有 方法

Static fields and methods

从 Java 8 开始,接口中声明的所有字段 都是隐式的公共、静态和最终 常量。这就是为什么接口 是常量的首选位置。您不需要将 public static final 添加到他们的声明中。

至于静态方法,它们在接口中的作用与在类中的作用相同:

interface SomeInterface{
   static String someMethod() {
      return "abc";
   }
}

请注意,无需将接口方法标记为public。默认情况下,所有非私有接口方法都是公共的。

我们可以只使用一个接口名称来调用前面的方法:

System.out.println(SomeInetrface.someMethod()); //prints: abc

Interface versus abstract class

我们已经提到一个类可以被声明为abstract。它可能是一个常规类, 我们不想被实例化,或者它可能 是一个包含(或继承) 抽象方法。在最后一种情况下,我们必须将这样的类声明为 abstract 以避免编译错误。

在许多方面,抽象类与接口非常相似。它强制每个扩展它的 child 类来实现抽象方法。否则,子不能被实例化并且必须被声明为抽象本身。

然而,接口和抽象类之间的一些主要区别使得它们在不同的情况下都很有用:

  • abstract 类可以有构造函数,而接口 不能。
  • 抽象类可以有状态,而接口不能。
  • 抽象类的字段可以是 publicprivateprotected , static 与否,以及 final 与否,而在接口中,字段始终是 publicstaticfinal
  • 抽象类中的方法可以是 publicprivateprotected ,而接口方法只能是 publicprivate
  • 如果您要修改的类已经扩展了另一个类,则不能使用抽象类,但可以实现接口,因为一个类只能扩展另一个类,但可以实现多个接口。

您将在 Polymorphism in action 部分中看到抽象用法的示例。

Overloading, overriding, and hiding

我们已经在 InheritanceAbstraction/interface 部分提到了覆盖。它是在父类 中实现的非静态方法 替换为 子 类。接口的默认 方法也可以在扩展它的接口中被覆盖。隐藏类似于覆盖,但仅适用于静态方法和静态,以及实例的属性。

重载是在同一个类或接口中创建多个具有相同名称和不同参数(因此,不同的签名)的方法。

在本节中,我们将讨论所有这些概念并演示它们如何用于类和接口。

Overloading

在同一个接口或具有相同签名的类中不可能有两个方法。要具有不同的签名,新方法必须具有新名称或不同的参数类型列表(并且类型的顺序确实很重要)。拥有两个名称相同但参数类型列表不同的方法构成重载。以下是在接口中重载的合法方法的几个示例:

interface A {
    int m(String s);
    int m(String s, double d);
    default int m(String s, int i) { return 1; }
    static int m(String s, int i, double d) { return 1; }
}

请注意,前面的任何两个方法都没有相同的签名,包括默认方法和静态方法。否则,会产生编译器错误。指定为默认值和静态都不会在重载中起任何作用。返回类型也不影响重载。我们在任何地方都使用 int 作为返回类型,只是为了让示例不那么混乱。

方法重载在类中以类似方式完成:

    class C {
        int m(String s){ return 42; }
        int m(String s, double d){ return 42; }
        static int m(String s, double d, int i) { return 1; }
    }

并且在哪里声明具有相同名称的方法并不重要。下面的方法重载和前面的例子没有区别,如下:

interface A {
    int m(String s);
    int m(String s, double d);
}
interface B extends A {
    default int m(String s, int i) { return 1; }
    static int m(String s, int i, double d) { return 1; }
}
class C {
     int m(String s){ return 42; }
}
class D extends C {
     int m(String s, double d){ return 42; }
     static int m(String s, double d, int i) { return 1; }
}

私有非静态方法 只能由同一类的非静态方法重载。

重要的提示

当方法具有相同的名称但参数类型列表不同并且属于相同的接口(或类)或不同的接口(或类)时,就会发生重载,其中一个是另一个的祖先。私有方法只能由同一类中的方法重载。

Overriding

静态和非静态方法发生的重载相比,方法覆盖仅发生在非静态方法中并且仅在它们具有 时发生完全相同的签名属于不同的接口(或类),其中一个是另一个的祖先。

重要的提示

重写方法驻留在子接口(或类)中,而重写方法具有相同的签名并属于祖先接口(或类)之一。不能覆盖私有方法。

以下是重写接口的方法的示例

interface A {
    default void method(){
        System.out.println("interface A");
    }
}
interface B extends A{
    @Override
    default void method(){
        System.out.println("interface B");
    }
}
class C implements B { }

如果我们使用 C 类实例调用 method(),结果如下:

C c = new C();
c.method();      //prints: interface B

请注意 @Override 注释的用法。它告诉编译器程序员认为带注释的方法覆盖了祖先接口之一的方法。这样,编译器可以确保覆盖确实发生,如果没有发生则生成错误。例如,程序员可能会拼错方法的名称,如下所示:

interface B extends A{
    @Override
    default void method(){
        System.out.println("interface B");
    }
}

如果发生这种情况,编译器 会生成错误,因为没有要覆盖的 metod() 方法。如果没有 @Overrride 注释,程序员可能不会注意到这个错误,结果会完全不同:

C c = new C();
c.method();      //prints: interface A

相同的覆盖规则适用于类实例方法。在以下示例中,C2 类覆盖了 C1 类的方法:

class C1{
    public void method(){
        System.out.println("class C1");
    }
}
class C2 extends C1{
    @Override
    public void method(){
        System.out.println("class C2");
    }
}

结果如下:

C2 c2 = new C2();
c2.method();      //prints: class C2

在具有覆盖方法的类或接口与具有覆盖方法的类 或接口之间有多少祖先并不重要:

class C1{
    public void method(){
        System.out.println("class C1");
    }
}
class C3 extends C1{
    public void someOtherMethod(){
        System.out.println("class C3");
    }
}
class C2 extends C3{
    @Override
    public void method(){
        System.out.println("class C2");
    }
}

上述方法覆盖的结果仍然是相同的。

Hiding

隐藏被许多认为是一个复杂的话题,但它不应该是,我们会尽量让它看起来简单.

hiding 这个名字来源于类和接口的静态属性和方法的行为。每个静态属性或方法在 JVM 内存中作为单个副本存在,因为它们与接口或类相关联,而不是与对象相关联。接口或类作为单个副本存在。这就是为什么我们不能说子类的静态属性或方法会覆盖父类的同名静态属性或方法。所有静态属性 和方法仅在加载类或接口时加载到内存中并保留在那里,不会被复制到任何地方。让我们看一个例子。

让我们创建两个具有父子关系的接口以及具有相同名称的静态字段和方法:

interface A {
    String NAME = "interface A";
    static void method() {
        System.out.println("interface A");
    }
}
interface B extends A {
    String NAME = "interface B";
    static void method() {
        System.out.println("interface B");
    }
}

请注意接口字段标识符的大写。这是常用于表示常量的约定,无论它是在接口还是类中声明。提醒您一下,Java 中的常量是一个变量,一旦初始化,就不能重新分配另一个值。默认情况下,接口字段是常量,因为接口中的任何字段都是 final(请参阅 Final 属性、方法和类部分)。

如果我们从 B 接口打印 NAME 并执行它的 method() ,我们得到以下结果:

System.out.println(B.NAME); //prints: interface B
B.method();                 //prints: interface B

它看起来很像覆盖,但实际上,我们只是调用了一个特定的属性或与这个特定接口相关联的 方法。

同样,考虑以下类:

public class C {
    public static String NAME = "class C";
    public static void method(){
        System.out.println("class C"); 
    }
    public String name1 = "class C";
}
public class D extends C {
    public static String NAME = "class D";
    public static void method(){
        System.out.println("class D"); 
    }
    public String name1 = "class D";
}

如果我们尝试使用类本身访问 D 类的静态成员,我们将得到我们所要求的:

System.out.println(D.NAME);  //prints: class D
D.method();                  //prints: class D

只有在使用对象访问属性或静态方法时才会出现混淆:

C obj = new D();
System.out.println(obj.NAME);        //prints: class C
System.out.println(((D) obj).NAME);  //prints: class D
obj.method();                        //prints: class C
((D)obj).method();                   //prints: class D
System.out.println(obj.name1);       //prints: class C
System.out.println(((D) obj).name1); //prints: class D

obj 变量引用 D 类的对象,铸造证明了这一点,正如您在前面的示例中看到的那样。但是,即使我们使用对象,尝试访问静态属性或方法也会为我们带来 我们用作声明变量类型的类的成员。至于例子最后两行的实例属性,Java中的属性不符合多态行为,我们得到父name1属性"literal">C 类,而不是子 D 类的预期属性。

重要的提示

为避免与类的静态成员混淆,请始终使用类而不是对象来访问它们。为避免与实例属性混淆,请始终将它们声明为私有并通过方法访问它们。

为了说明最后一个技巧,请考虑以下类:

class X {
    private String name = "class X";
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
class Y extends X {
    private String name = "class Y";
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

如果我们对 instance 属性运行与 CD 相同的测试 类,结果将是这样的:

X x = new Y();
System.out.println(x.getName());      //prints: class Y
System.out.println(((Y)x).getName()); //prints: class Y

现在,我们使用方法访问实例属性,这些方法是覆盖效果的主体,不再有意外结果。

为了结束对 Java 中隐藏的讨论,我们想提一下另一种隐藏类型,即当局部变量隐藏同名的实例或静态属性时。这是一个这样做的类:

public class HidingProperty {
   private static String name1 = "static property";
   private String name2 = "instance property";
   public void method() {
      var name1 = "local variable";
      System.out.println(name1);     //prints: local variable
      var name2 = "local variable";  //prints: local variable
      System.out.println(name2);
      System.out.println(HidingProperty.name1); 
                                     //prints: static property
      System.out.println(this.name2);
                                   //prints: instance property
   }
}

可以看到,name1局部变量隐藏了同名的静态属性,而name2局部变量隐藏了实例财产。仍然可以使用类名访问静态属性(参见HidingProperty.name1)。请注意,尽管被声明为 private,它仍可从类内部访问。

始终可以使用 this 关键字 访问实例属性,其中表示当前对象。

The final variable, method, and classes

关于 Java 中常量的概念,我们已经多次提到 final 属性,但 只是一种情况使用 final 关键字。它通常可以应用于任何变量。此外,可以将类似的约束 应用于方法甚至类,从而防止方法被覆盖和类被扩展。

The final variable

final 关键字放在变量声明前面 使这个变量在初始化后不可变,例如:

final String s = "abc";

初始化甚至可以延迟:

final String s;
s = "abc";

对于 object 属性,这种延迟只能持续到对象被创建。这意味着可以在构造函数中初始化属性,例如:

class A {
    private final String s1 = "abc";
    private final String s2;
    private final String s3;   //error
    private final int x;       //error
    public A() {
        this.s1 = "xyz";      //error
        this.s2 = "xyz";     
    }
}

请注意,即使在对象构造期间,也不可能两次初始化属性——在声明期间和在构造函数中。值得注意的是,最终属性必须显式初始化。从前面的示例可以看出,编译器不允许将 final 属性初始化为默认值。

也可以在初始化块中初始化 final 属性:

class B {
    private final String s1 = "abc";
    private final String s2;
    {
        s1 = "xyz"; //error
        s2 = "abc";
    }
}

对于 static 属性,不可能 在构造函数中对其进行初始化,因此必须对其进行初始化在其声明期间或在静态初始化块中:

class C {
    private final static String s1 = "abc";
    private final static String s2;
    static {
        s1 = "xyz"; //error
        s2 = "abc";
    }
}

在接口中,所有字段始终是最终字段,即使它们没有被声明为最终字段。由于接口中既不允许构造函数也不允许初始化块,因此初始化接口字段的唯一方法是在声明期间。不这样做会导致编译错误:

interface I {
    String s1;  //error
    String s2 = "abc";
}

Final method

声明为 final 的方法不能在 child 类中被覆盖或在 static 方法。例如,java.lang.Object 类,它是 Java 中所有类的祖先,它的一些方法声明为 final

public final Class getClass()x
public final void notify()
public final void notifyAll()
public final void wait() throws InterruptedException
public final void wait(long timeout) 
                         throws InterruptedException
public final void wait(long timeout, int nanos)
                         throws InterruptedException

final 类的所有私有方法和未继承方法实际上都是最终方法,因为您无法覆盖它们。

Final class

final 类不能扩展。它不能有孩子,这使得类的所有方法 也有效地final。此功能用于安全性或当程序员希望确保类功能不会因为其他一些设计考虑而被覆盖、重载或隐藏时。

The record class

record 在 Java 16 中添加到 SDK。这是一个期待已久的 Java 功能。它允许您在需要不可变类(仅使用 getter)时避免编写样板代码,该类类似于以下 Person 类(参见 ch02_oop 文件夹中的 "literal">Record 类):

final class Person {
    private int age;
    private String name;
    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }
    public int age() { return age; }
    public String name() { return name; }
    @Override
    public boolean equals(Object o) {
        //implementation not shown for brevity
    }
    @Override
    public int hashCode() {
        //implementation not shown for brevity
    }
    @Override
    public String toString() {
        //implementation not shown for brevity
    }

请注意,上面的 getter 没有 get 前缀。这样做是故意的,因为在不可变类的情况下,没有必要区分 getter 和 setter,因为如果我们想让类真正不可变,setter 不存在也不应该存在。这就是此类和 JavaBeans 之间的主要区别,JavaBeans 是可变的并且同时具有 setter 和 getter。

record 类只允许您将前面的实现替换为以下一行:

record Person(int age, String name){}

我们可以用以下代码演示

record PersonR(int age, String name){} //We added suffix "R" 
                 //to distinguish this class from class Person
Person person = new Person(25, "Bill");
System.out.println(person);  
                          //prints: Person{age=25, name='Bill'}
System.out.println(person.name());            //prints: Bill
 
Person person1 = new Person(25, "Bill");
System.out.println(person.equals(person1));   //prints: true
 
PersonR personR = new PersonR(25, "Bill");
System.out.println(personR);   
                         //prints: PersonR{age=25, name='Bill'}
System.out.println(personR.name());           //prints: Bill
 
PersonR personR1 = new PersonR(25, "Bill");
System.out.println(personR.equals(personR1)); //prints: true
 
System.out.println(personR.equals(person));   //prints: false

除了 final(不可扩展)和不可变之外,record 不能扩展另一个类,因为 它已经扩展了 java.lang.Record,但它可以实现另一个接口,如下例所示:

interface Student{
    String getSchoolName();
}
record StudentImpl(String name, String school) implements Student{
    @Override
    public String getSchoolName() { return school(); }
}

可以在 record 中添加 static 方法,如以下代码片段所示:

record StudentImpl(String name, String school) implements Student{
    public static String getSchoolName(Student student) {
         return student.getSchoolName();
    }
}

static 方法不会也不能访问实例属性,并且只能使用作为参数传递给它的值。

record 可以有另一个构造函数,可以添加,例如如下:

record StudentImpl(String name, String school) implements Student{
    public StudentImpl(String name) {
        this(name, "Unknown");
    } 
}

您可能已经注意到,不可能向 record 添加另一个属性或 setter,而所有额外的 getter 都必须仅使用 record 已经提供的 getter。

Sealed classes and interfaces

final 类不能扩展,而非公共类或接口的访问权限有限。然而,有时 需要从任何地方访问类或接口 但只能由某个类或接口扩展,或者,在接口的情况下,仅由某些类实现。这就是在 Java 17 中将 sealed 类和接口添加到 SDK 的动机。

sealed 类或接口与 final 的区别在于 sealed 类或接口总是有一个 permits 关键字,后面是允许扩展 sealed 类或接口,或者,在接口的情况下,实现它。请注意现有这个词。 permits 关键字之后列出的子类型必须在编译时与密封类存在于同一模块中,如果在默认(未命名)模块中,则必须存在于同一包中。

sealed 类的子类型必须标记为 sealedfinal、或 非密封sealed 接口的子类型必须标记为 sealed非密封,因为接口不能是final

我们先来看一个sealed接口的例子:

sealed interface Engine permits EngineBrand {
    int getHorsePower();
}
sealed interface EngineBrand extends Engine permits Vehicle {
    String getBrand();
} 
non-sealed class Vehicle implements EngineBrand {
    private final String make, model, brand;
    private final int horsePower;
    public Vehicle(String make, String model, 
                   String brand, int horsePower) {
        this.make = make;
        this.model = model;
        this.brand = brand;
        this.horsePower = horsePower;
    }
    public String getMake() { return make; }
    public String getModel() { return model; }
    public String getBrand() { return brand; }
    public int getHorsePower() { return horsePower; }
}

如您所见,EngineBrand 接口 扩展了 Engine 接口并允许(允许)Vehicle 实现。或者,我们可以让 Vehicle 直接实现 Engine 接口,如下例所示:

sealed interface Engine permits EngineBrand, Vehicle {
    int getHorsePower();
}
sealed interface EngineBrand extends Engine permits Vehicle {
    String getBrand();
} 
non-sealed class Vehicle implements Engine, EngineBrand {...}

现在,让我们看看 sealed 类的示例

sealed class Vehicle permits Car, Truck {
    private final String make, model;
    private final int horsePower;
    public Vehicle(String make, String model, int horsePower) {
        this.make = make;
        this.model = model;
        this.horsePower = horsePower;
    }
    public String getMake() { return make; }
    public String getModel() { return model; }
    public int getHorsePower() { return horsePower; }
}

以下是 CarTruck 允许的 Vehicle 密封类:

final class Car extends Vehicle {
    private final int passengerCount;
    public Car(String make, String model, int horsePower, 
      int passengerCount) {
        super(make, model, horsePower);
        this.passengerCount = passengerCount;
    }
    public int getPassengerCount() { return passengerCount; }
}
 
non-sealed class Truck extends Vehicle {
    private final int payloadPounds;
    public Truck(String make, String model, int horsePower, 
      int payloadPounds) {
        super(make, model, horsePower);
        this.payloadPounds = payloadPounds;
    }
    public int getPayloadPounds() { return payloadPounds; }
}

在支持 sealed 类中,Java 17 中的 Java Reflections API 有两个新的 方法,isSealed()getPermittedSubclasses()。以下是它们的用法示例:

Vehicle vehicle = new Vehicle("Ford", "Taurus", 300);
System.out.println(vehicle.getClass().isSealed());  
                                                 //prints: true
System.out.println(Arrays.stream(vehicle.getClass()
                .getPermittedSubclasses())
                .map(Objects::toString).toList());
                             //prints list of permitted classes
 
Car car = new Car("Ford", "Taurus", 300, 4);
System.out.println(car.getClass().isSealed());  //prints: false
System.out.println(car.getClass().getPermittedSubclasses());
                                                 //prints: null

sealed 接口与 record 集成得很好,因为 recordfinal 并且可以被列为 作为允许的 实现。

Polymorphism in action

多态性是 OOP 最强大的 和有用的特性。它使用了我们迄今为止介绍的所有其他 OOP 概念和特性。这是掌握 Java 编程的最高概念点。讨论完之后,本书的其余部分将主要介绍 Java 语言语法和 JVM 功能。

正如我们在OOP 概念部分所述,多态性是对象作为不同类的对象或作为不同接口的实现的能力。如果你在网上搜索多态性这个词,你会发现它是以多种不同形式出现的条件。变形是通过自然或超自然的方式将事物或人的形式或性质转变为完全不同的形式或性质。所以,Java polymorphism 是一个对象 表现得好像经历了变态并且在不同的情况下表现出完全不同的行为的能力。条件。

我们将使用对象工厂——具体的编程 工厂的实现,这是一个返回不同原型或类的对象的方法 (https://en.wikipedia.org/wiki/Factory_(object-oriented_programming)。

The object factory

对象工厂 背后的想法是创建一个在特定条件下返回特定类型的新对象 的方法。例如,查看 CalcUsingAlg1CalcUsingAlg2 类:

interface CalcSomething{ double calculate(); }
class CalcUsingAlg1 implements CalcSomething{
    public double calculate(){ return 42.1; }
}
class CalcUsingAlg2 implements CalcSomething{
    private int prop1;
    private double prop2;
    public CalcUsingAlg2(int prop1, double prop2) {
        this.prop1 = prop1;
        this.prop2 = prop2;
    }
    public double calculate(){ return prop1 * prop2; }
}

如您所见,它们都实现了相同的接口,CalcSomething,但使用不同的算法。现在,假设我们决定在 property 文件中选择所使用的算法。然后,我们可以创建以下对象工厂:

class CalcFactory{
    public static CalcSomething getCalculator(){
        String alg = getAlgValueFromPropertyFile();
        switch(alg){
            case "1":
                return new CalcUsingAlg1();
            case "2":
                int p1 = getAlg2Prop1FromPropertyFile();
                double p2 = getAlg2Prop2FromPropertyFile();
                return new CalcUsingAlg2(p1, p2);
            default:
                System.out.println("Unknown value " + alg);
                return new CalcUsingAlg1();
        }
    }
}

工厂根据 getAlgValueFromPropertyFile() 方法返回的值选择要使用的算法。在第二种算法的情况下,它也使用了getAlg2Prop1FromPropertyFile()方法和getAlg2Prop2FromPropertyFile () 获取算法的输入参数。但是这种复杂性对客户是隐藏的:

CalcSomething calc = CalcFactory.getCalculator();
double result = calc.calculate();

我们可以添加新的算法变体,更改算法参数的来源或算法选择的过程,但客户端不需要更改代码。这就是多态性的力量。

或者,我们可以使用继承来实现多态行为。考虑以下类:

class CalcSomething{
    public double calculate(){ return 42.1; }
}
class CalcUsingAlg2 extends CalcSomething{
    private int prop1;
    private double prop2;
    public CalcUsingAlg2(int prop1, double prop2) {
        this.prop1 = prop1;
        this.prop2 = prop2;
    }
    public double calculate(){ return prop1 * prop2; }
}

那么,我们的工厂可能看起来如下:

class CalcFactory{
    public static CalcSomething getCalculator(){
        String alg = getAlgValueFromPropertyFile();
        switch(alg){
            case "1":
                return new CalcSomething();
            case "2":
                int p1 = getAlg2Prop1FromPropertyFile();
                double p2 = getAlg2Prop2FromPropertyFile();
                return new CalcUsingAlg2(p1, p2);
            default:
                System.out.println("Unknown value " + alg);
                return new CalcSomething();
        }
    }
}

但是客户端代码没有改变:

CalcSomething calc = CalcFactory.getCalculator();
double result = calc.calculate();

如果有选择,经验丰富的 程序员会使用一个通用接口来实现。它允许更灵活的设计,因为 Java 中的类可以实现多个接口,但可以扩展(继承)一个类。

The instanceof operator

不幸的是,生活并不总是那么容易,有时,程序员不得不处理由不相关的类组装而成的代码,甚至是 来自不同的框架。在这种情况下,使用多态可能不是一种选择。但是,您可以隐藏算法选择的复杂性,甚至可以使用 instanceof 运算符来模拟多态行为,该运算符返回 true对象是某个类的实例。

假设我们有两个不相关的类:

class CalcUsingAlg1 {
    public double calculate(CalcInput1 input){
        return 42. * input.getProp1();
    }
}
class CalcUsingAlg2{
    public double calculate(CalcInput2 input){
        return input.getProp2() * input.getProp1();
    }
}

每个类都期望某种类型的对象作为输入:

class CalcInput1{
    private int prop1;
    public CalcInput1(int prop1) { this.prop1 = prop1; }
    public int getProp1() { return prop1; }
}
class CalcInput2{
    private int prop1;
    private double prop2;
    public CalcInput2(int prop1, double prop2) {
        this.prop1 = prop1;
        this.prop2 = prop2;
    }
    public int getProp1() { return prop1; }
    public double getProp2() { return prop2; }
}

假设我们实现的方法接收到这样的对象:

void calculate(Object input) {
    double result = Calculator.calculate(input);
    //other code follows
}

我们在这里仍然使用多态性,因为我们将输入描述为 Object 类型。我们可以这样做,因为 Object 类是所有 Java 类的基类。

现在,让我们看看 Calculator 类是如何实现的:

class Calculator{
    public static double calculate(Object input){
        if(input instanceof CalcInput1 calcInput1){
            return new CalcUsingAlg1().calculate(calcInput1);
        } else if (input instanceof CalcInput2 calcInput2){
            return new CalcUsingAlg2().calculate(calcInput2);
        } else {
            throw new RuntimeException("Unknown input type " + 
                          input.getClass().getCanonicalName());
        }
    }
}

如您所见,它使用 instanceof 运算符来选择合适的算法。通过使用 Object 类作为输入类型,Calculator 类利用了多态性< /a> 也是如此,但它的大部分实现与 无关。然而,从外部看,它看起来是多态的,而且确实如此,但只是在一定程度上。

Summary

本章向您介绍了 OOP 的概念以及它们是如何在 Java 中实现的。它提供了对每个概念的解释,并演示了如何在特定的代码示例中使用它。详细讨论了classinterface的Java语言结构。您还了解了什么是重载、覆盖和隐藏,以及如何使用 final 关键字来保护方法不被覆盖。

实战中的多态部分,您了解了强大的 Java 多态特性。本节将所有介绍的材料放在一起,并展示了多态性如何保持在 OOP 的中心。

在下一章中,您将熟悉 Java 语言语法,包括包、导入、访问修饰符、保留和限制关键字,以及 Java 引用类型的某些方面。您还将学习如何使用 thissuper 关键字,原始类型的扩展和收缩转换,装箱和拆箱、原始类型和引用类型赋值,以及引用类型的 equals() 方法如何工作。

Quiz

  1. 从以下列表中选择所有正确的 OOP 概念:
    1. 封装
    2. 隔离
    3. 授粉
    4. 继承
  2. 从以下列表中选择所有正确的陈述:
    1. Java 对象具有状态。
    2. Java 对象具有行为。
    3. Java 对象具有状态。
    4. Java 对象具有方法。
  3. 从以下列表中选择所有正确的陈述:
    1. 可以继承 Java 对象的行为。
    2. 可以覆盖 Java 对象的行为。
    3. 可以重载 Java 对象行为。
    4. Java 对象的行为可能会不堪重负。
  4. 从以下列表中选择所有正确的陈述:
    1. 不同类的 Java 对象可以具有相同的行为。
    2. 不同类的 Java 对象共享一个父对象状态。
    3. 不同类的 Java 对象有一个相同类的对象作为父对象。
    4. 不同类的 Java 对象可以共享行为。
  5. 从以下列表中选择所有正确的陈述:
    1. 方法签名包含返回类型。
    2. 如果返回类型不同,则方法签名不同。
    3. 如果两个相同类型的参数切换位置,方法签名会发生变化。
    4. 如果两个不同类型的参数切换位置,方法签名会发生变化。
  6. 从以下列表中选择所有正确的陈述:
    1. 封装隐藏了类名。
    2. 封装隐藏了行为。
    3. 封装只允许通过方法访问数据。
    4. 封装不允许直接访问状态。
  7. 从以下列表中选择所有正确的陈述:
    1. 该类在 .java 文件中声明。
    2. 类字节码存储在 .class 文件中。
    3. 父类存储在.base文件中。
    4. child 类存储在 .sub 文件中。
  8. 从以下列表中选择所有正确的陈述:
    1. 方法定义对象状态。
    2. 方法定义对象行为。
    3. 没有参数的方法被标记为void
    4. 一个方法可以有很多 return 语句。
  9. 从以下列表中选择所有正确的陈述:
    1. Varargs 被声明为 var 类型。
    2. Varargs 代表各种参数
    3. Varargs 是一个 String 数组。
    4. Varargs 可以作为指定类型的数组。
  10. 从以下列表中选择所有正确的陈述:
    1. 构造函数是一种创建状态的方法。
    2. 构造函数的主要职责是初始化状态。
    3. JVM 总是提供一个默认的构造函数。
    4. 可以使用parent关键字调用父类构造函数。
  11. 从以下列表中选择所有正确的陈述:
    1. new 运算符为对象分配内存。
    2. new 运算符为对象属性分配默认值。
    3. new 操作符首先创建一个父对象。
    4. new 运算符首先创建一个子对象。
  12. 从以下列表中选择所有正确的陈述:
    1. Object 类属于 java.base 包。
    2. Object 类属于 java.lang 包。
    3. Object 类属于 Java 类库的一个包。
    4. 会自动导入一个 Object 类。
  13. 从以下列表中选择所有正确的陈述:
    1. 使用对象调用实例方法。
    2. 使用类调用静态方法。
    3. 使用类调用实例方法。
    4. 使用对象调用静态方法。
  14. 从以下列表中选择所有正确的陈述:
    1. 接口中的方法隐式是 publicstaticfinal.
    2. 接口可以具有无需在类中实现即可调用的方法。
    3. 接口可以包含可以在没有任何类的情况下使用的字段。
    4. 可以实例化接口。
  15. 从以下列表中选择所有正确的陈述:
    1. 接口的默认方法总是被默认调用。
    2. 接口的私有方法只能被默认方法调用。
    3. 无需在类中实现即可调用接口静态方法。
    4. 默认方法可以增强实现接口的类。
  16. 从以下列表中选择所有正确的陈述:
    1. Abstract 类可以有默认方法。
    2. 可以在没有 abstract 方法的情况下声明 Abstract 类。
    3. 任何类都可以声明为抽象类。
    4. 接口是没有构造函数的抽象类。
  17. 从以下列表中选择所有正确的陈述:
    1. 只能在界面中进行重载。
    2. 只有在一个类扩展另一个类时才能进行重载。
    3. 重载可以在任何类中完成。
    4. 重载的方法必须具有相同的签名。
  18. 从以下列表中选择所有正确的陈述:
    1. 只能在 child 类中进行覆盖。
    2. 可以在界面中进行覆盖。
    3. 被覆盖的方法必须具有相同的名称。
    4. Object 类的任何方法都不能被覆盖。
  19. 从以下列表中选择所有正确的陈述:
    1. 任何方法都可以隐藏。
    2. 变量可以隐藏属性。
    3. 可以隐藏静态方法。
    4. 可以隐藏公共实例属性。
  20. 从以下列表中选择所有正确的陈述:
    1. 任何变量都可以声明为final。
    2. 不能将公共方法声明为 final。
    3. 可以将受保护的方法声明为 final。
    4. 可以将类声明为受保护的。
  21. 从以下列表中选择所有正确的陈述:
    1. 多态行为可以基于继承。
    2. 多态行为可以基于重载。
    3. 多态行为可以基于覆盖。
    4. 多态行为可以基于接口。