20230523 5. 继承

继 承

继承 inheritance

类、超类和子类

is-a 关系是继承的明显特征

定义子类

关键字 extends 表示继承

已存在的类称为 超类 ( superclass )、 基类( base class ) 或 父类( parent class); 新类称为 子类(subclass/child class) 或 派生类 ( derived class )

超类 这个名称可能带来误解,实际上,子类比超类拥有的功能更加丰富。

前缀 超 和 子 来源于计算机科学和数学理论中的集合语言的术语。

声明为私有的类成员不会被子类继承,这很让人困惑。子类不能访问父类的私有字段,只是拥有这些字段。

在通过扩展超类定义子类的时候, 仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中, 而将具有特殊用途的方法放在子类中,这种将通用的功能放到超类的做法,在面向对象程序设计中十分普遍。

记录 Record 不能继承

覆盖方法 override

关键字 super 指示调用父类方法

有些人认为 super 与 this 引用是类似的概念, 实际上,这样比较并不太恰当。这是因为 super 不是一个对象的引用, 不能将 super 赋给另一个对象变量, 它只是一个指示编译器调用超类方法的特殊关键字。 this 是一个对象的引用,可以被赋给另一个对象变量。

子类可以增加字段,增加方法或覆盖超类的方法,继承绝不会删除任何字段或方法

子类构造器

我们可以通过 super 实现对超类构造器的调用。使用 super 调用构造器的语句必须是子类构造器的第一条语句。

关键字 this 有两个用途:一是引用隐式参数,二是调用该类其他的构造器 , 同样,super 关键字也有两个用途:一是调用超类的方法,二是调用超类的构造器。在调用构造器的时候, 这两个关键字的使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现。 构造参数既可以传递给本类 (this) 的其他构造器, 也可以传递给超类(super) 的构造器。

如果构造子类对象时没有显式地调用超类的构造器,那么超类必须有一个无参构造器,无参构造器在子类构造前使用。

静态代码块、代码块,构造函数,执行顺序:

Employee static{}   // 父类静态代码块
Manager static{}    // 子类静态代码块
Employee block{}    // 父类代码块
Employee()          // 父类构造函数
Manager block{}     // 子类代码块
Manager()           // 子类构造函数

一个对象变量(例如, 变量 e ) 可以指示多种实际类型的现象被称为 多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为 动态绑定( dynamic binding )

在 Java 中, 不需要将方法声明为虚拟方法。动态绑定是默认的处理方式。如果不希望让一个方法具有虚拟特征, 可以将它标记为 final

继承层次结构

继承并不仅限于一个层次。 由一个公共超类派生出来的所有类的集合被称为 继承层次结构( inheritance hierarchy )。在继承层次中, 从某个特定的类到其祖先的路径被称为该类的 继承链 ( inheritance chain)

Java 不支持多继承。

多 态

有一个用来判断是否应该设计为继承关系的简单规则, 这就是 is-a 规则, 它表明子类的每个对象也是超类的对象。

is-a 规则的另一种表述法是 替换原则(substitution principle) 。 它表明程序中出现超类对象的任何地方都可以用子类对象置换。

在 Java 程序设计语言中, 对象变量是 多态 的。

不能将一个超类的引用赋给子类变量。

警告: 在 Java 中, 子类数组的引用可以转换成超类数组的引用, 而不需要采用强制类型转换。

Child[] childArr = new Child[5];
Parent[] parentArr = childArr;
parentArr[0] = new Parent();    // 运行时异常:Exception in thread "main" java.lang.ArrayStoreException
childArr[0].childSay();

所有数组都要牢记创建它们的元素类型, 并负责监督仅将类型兼容的引用存储到数组中。

理解方法调用

假设要调用 x.f(args) ,隐式参数 x 声明为类 C 的一个对象。下面是调用过程的详细描述:

  1. 编译器査看对象的声明类型和方法名,编译器将会一一列举所有 C 类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)

  2. 编译器将査看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配, 就选择这个方法。这个过程被称为 重载解析(overloading resolution)

    方法的名字和参数列表称为方法的签名。

    如果在子类中定义了一个与超类签名相同的方法, 那么子类中的这个方法就覆盖了超类中的这个相同签名的方法。

    不过, 返回类型不是签名的一部分, 因此, 在覆盖方法时, 一定要保证返回类型的兼容性。 允许子类将覆盖方法的返回类型定义为原返回类型的子类型。 协变(covariant)的返回类型

  3. 如果是 private 方法、 static 方法、 final 方法或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为 静态绑定( static binding )

  4. 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法

    每次调用方法都要进行搜索,时间开销相当大。因此, 虚拟机预先为每个类创建了一个 方法表( method table) ,其中列出了所有方法的签名和实际调用的方法。

动态绑定有一个非常重要的特性: 无需对现存的代码进行修改,就可以对程序进行扩展。

警告: 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。 特别是, 如果超类方法是 public , 子类方法一定要声明为 public 。经常会发生这类错误:在声明子类方法的时候, 遗漏了 public 修饰符。此时,编译器将会把它解释为试图提供更严格的访问权限。

阻止继承:final 类和方法

不允许扩展的类被称为 final 类。如果在定义类的时候使用了 final 修饰符就表明这个类是 final 类。

类中的特定方法也可以被声明为 final。 如果这样做,子类就不能覆盖这个方法(final 类中的所有方法自动地成为 final 方法) 。

字段也可以被声明为 final。对于 final 字段来说,构造对象之后就不允许改变它们的值了。 不过, 如果将一个类声明为 final, 只有其中的方法自动地成为 final ,而不包括字段。

将方法或类声明为 final 只有一个原因: 确保它们不会在子类中改变语义。 String 类也是 final 类,这意味着不允许任何人定义 String 的子类。换言之, 如果有一个 String 的引用, 它引用的一定是一个 String 对象, 而不可能是其他类的对象。

在早期的 Java 中,有些程序员为了避免动态绑定带来的系统开销而使用 final 关键字。如果一个方法没有被覆盖并且很短, 编译器就能够对它进行优化处理, 这个过程为称为 内联 ( inlining )。 例如,内联调用 e.getName() 将被替换为访问 e.name 字段。

幸运的是, 虚拟机中的即时编译器比传统编译器的处理能力强得多。这种编译器可以准确地知道类之间的继承关系, 并能够检测出类中是否真正地存在覆盖给定的方法。

枚举和记录总是 final ,它们不允许扩展

强制类型转换

// 将表达式 x 的值转换成整数类型, 舍弃了小数部分。
double x = 3.405;
int nx = (int) x;

Employee[] staff = new Employee[10];
Manager boss = (Manager) staff[0];

进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后, 使用对象的全部功能。

将一个值存入变量时, 编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量, 编译器是允许的。但将一个超类的引用赋给一个子类变量, 必须进行类型转换, 这样才能够通过运行时的检査。

只能在继承层次内进行类型转换。

在将超类转换成子类之前,应该使用 instanceof 进行检查。否则可能出现 ClassCastException 异常

如果 x 为 null , 进行下列测试

x instanceof C

不会产生异常, 只是返回 false。之所以这样处理是因为 null 没有引用任何对象, 当然也不会引用 C 类型的对象。

在一般情况下,应该尽量少用类型转换和 instanceof 运算符。

在 Java 中, 需要将 instanceof 运算符和类型转换组合起来使用。

if (staff instanceof Manager) {
    Manager boss = (Manager) staff;
}

instanceof 模式匹配

声明变量的 instanceof 形式称为 模式匹配

if (staff instanceof Manager boss) {
    boss.setBonus(5000);
}

不支持

Manager boss = new Manager();
if (boss instanceof Employee employee) {    // 报错,模式类型 'Employee' 是表达式类型 'Manager' 的父类型
    
}

支持

Manager boss = new Manager();
if (boss instanceof Employee) { // 向后兼容
    
}

当 instanceof 模式引入一个变量时,可以立即在同一个表达式中使用

Employee e = new Manager();
if (e instanceof Manager manager && manager.getBonus() > 5000) {    // 只能是 && ,不能是 || ,因为判断的短路机制
    
}

配合三元表达式使用

double bonus = e instanceof Manager manager ? manager.getBonus() : 0;

受保护访问

最好将类中的字段标记为 private , 而方法标记为 public。任何声明为 private 的内容对其他类都是不可见的。这对于子类来说也完全适用, 即子类也不能访问超类的私有字段

如果希望超类中的某些方法允许被子类访问, 或允许子类的方法访问超类的某个字段,需要将这些方法或字段声明为 protected

受保护字段只能由同一个包中的类访问,其他包的子类不能访问受保护字段。受保护方法没有这个限制。

在实际应用中,要谨慎使用 protected 属性。假设需要将设计的类提供给其他程序员使用,而在这个类中设置了一些受保护字段, 由于其他程序员可以由这个类再派生出新类,并访问其中的受保护字段。在这种情况下,如果需要对这个类的实现进行修改, 就必须通知所有使用这个类的程序员。这违背了 OOP 提倡的数据封装原则。

受保护的方法更具有实际意义。 如果需要限制某个方法的使用, 就可以将它声明为 protected 。这表明子类(可能很熟悉祖先类)得到信任, 可以正确地使用这个方法, 而其他类则不行。

Object.clone 方法就是一个很好的示例

事实上,Java 中的受保护部分(字段和方法)对所有子类及同一个包中的所有其他类都可见。同一个包中不是子类也可以访问

Java 用于控制可见性的 4 个访问修饰符:

  • 仅本类可见 —— private
  • 对所有类可见 —— public
  • 对本包和所有子类可见 —— protected
  • 对本包可见 —— 默认 ,不需要修饰符

protected 可见性强于默认的包私有,public 可见性最强

覆盖(override)一个父类方法时:

  • 更强的可见性
  • 更具体地返回类型
  • 抛出更具体的异常或者不抛出异常

Object: 所有类的超类

Object 类是 Java 中所有类的始祖, 在 Java 中每个类都是由它扩展而来的。但是并不需要这样写:

public class Employee extends Object

如果没有明确地指出超类,Object 就被认为是这个类的超类。

在 Java 中, 只有基本类型 ( primitive types) 不是对象, 例如,数值、 字符和布尔类型的值都不是对象。

所有的数组类型,不管是对象数组还是基本类型的数组都扩展了 Object 类。

equals 方法

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象,通过检测两个对象引用是否相同

public boolean equals(Object obj) {
    return (this == obj);
}

工具方法 Objects.equals ,可以防止 NPE

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

null == null 为 true

比较推荐一种做法:在子类中定义 equals 方法时, 首先调用超类的 equals。 如果检测失败, 对象就不可能相等。 如果超类中的字段都相等, 就需要比较子类中的实例字段。

public boolean equals(Object otherObject) {
    if (!super.equals(otherObject)) return false;
    Manager other = (Manager) otherObject;
    // super.equals checked that this and other belong to the same class
    return bonus == other.bonus;
}

记录 Record 是一种特殊形式的不可变类,会自动定义一个比较字段的 equals 方法,两个记录实例中相应字段值相等,这两个记录实例就相等

相等测试与继承

Employee 类的 equals 方法:

public boolean equals(Object otherObject) {
    // a quick test to see if the objects are identical
    if (this == otherObject) return true;

    // must return false if the explicit parameter is null
    if (otherObject == null) return false;

    // if the classes don't match, they can't be equal
    if (getClass() != otherObject.getClass()) return false;

    // now we know otherObject is a non-null Employee
    Employee other = (Employee) otherObject;

    // test whether the fields have identical values
    return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
}

Manager 继承自 Employee 类,Manager 的 equals 方法:

public boolean equals(Object otherObject) {
    if (!super.equals(otherObject)) return false;
    Manager other = (Manager) otherObject;
    // super.equals checked that this and other belong to the same class
    return bonus == other.bonus;
}

在前面的例子中, 如果发现类不匹配, equals 方法就返冋 false : 但是, 许多程序员却喜欢使用 instanceof 进行检测:

if (!(otherObject instanceof Employee)) return false;  

这样做不但没有解决 otherObject 是子类的情况,并且还有可能会招致一些麻烦。 这就是建议不要使用这种处理方式的原因所在。Java 语言规范要求 equals 方法具有下面的特性:

  • 自反性 : 对于任何非空引用 x , x.equals(x) 应该返回 true
  • 对称性 : 对于任何引用 x 和 y , 当且仅当 y.equals(x) 返回 true , x.equals(y) 也应该返回 true
  • 传递性 : 对于任何引用 x、 y 和 z , 如果 x.equals(y) 返回 true, y.equals(z) 返回 true , x.equals(z) 也应该返回 true
  • 一致性 : 如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果
  • 对于任意非空引用 x , x.equals(null) 应该返回 false

用 instanceof 检测对象类型,不能保证 对称性

某些书的作者认为不应该利用 getClass 检测, 因为这样不符合 置换原则

下面可以从两个截然不同的情况看一下这个问题:

  • 如果子类能够拥有自己的相等概念, 则对称性需求将强制采用 getClass 进行检测
  • 如果由超类决定相等的概念,那么就可以使用 imtanceof 进行检测, 这样可以在不同子类的对象之间进行相等的比较

在雇员和经理的例子中, 只要对应的字段相等, 就认为两个对象相等。如果两个 Manager 对象所对应的姓名、 薪水和雇佣日期均相等, 而奖金不相等, 就认为它们是不相同的, 因此,可以使用 getClass 检测。

但是, 假设使用雇员的 ID 作为相等的检测标准, 并且这个相等的概念适用于所有的子类, 就可以使用 instanceof 进行检测, 并应该将 Employee.equals 声明为 final 。

下面给出编写一个完美的 equals 方法的建议:

  1. 显式参数命名为 otherObject , 稍后需要将它转换成另一个叫做 other 的变量

  2. 检测 this 与 otherObject 是否引用同一个对象:

    if (this == otherObject) return true;
    

    这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的字段所付出的代价小得多

  3. 检测 otherObject 是否为 null , 如 果 为 null , 返 回 false 。这项检测是很必要的

    if (otherObject == null) return false;
    
  4. 比较 this 与 otherObject 是否属于同一个类。如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测:

    if (getClass() != otherObject.getClass()) return false;
    ClassName other = (ClassName) otherObject
    

    如果所有的子类都拥有统一的语义,就使用 instanceof 检测:

    if (!(otherObject instanceof ClassName other)) return false;
    
  5. 现在开始对所有需要比较的字段进行比较了。使用 == 比较基本类型字段,使用 equals 比较对象字段。如果所有的字段都匹配, 就返回 true ; 否则返回 false

    return field1 == other.field1 && Objects.equals(fie1d2, other.field2) && ... ;
    

    如果在子类中重新定义 equals , 就要在其中包含调用 super.equals(other)

对于数组类型的字段, 可以使用静态的 Arrays.equals 方法检测相应的数组元素是否相等。对于多维数组,可以使用 Arrays.deepEquals

下面是实现 equals 方法的一种常见的错误:

public class Employee {
    public boolean equals(Employee other) {
		// ...
    }
}

这个方法声明的显式参数类型是 Employee 。其结果并没有覆盖 Object 类的 equals 方法, 而是定义了一个重载方法。

为了避免发生类型错误, 可以使用 @Override 对覆盖超类的方法进行标记,如果出现了错误, 并且正在定义一个新方法, 编译器就会给出错误报告。

hashCode 方法

散列码( hash code ) 是由对象导出的一个整型值。 散列码是没有规律的。如果 x 和 y 是两个不同的对象, x.hashCode() 与 y.hashCode() 基本上不会相同。

由于 hashCode 方法定义在 Object 类中, 因此每个对象都有一个默认的散列码,其值为对象的存储地址。

Object 类中的 hashCode 方法:

public native int hashCode();

hashCode 方法应该返回一个整型数值(也可以是负数), 并合理地组合实例字段的散列码,以便能够让各个不同的对象产生的散列码更加均匀。

使用 null 安全的方法 Objects.hashCode

public static int hashCode(Object o) {
    return o != null ? o.hashCode() : 0;
}

需要组合多个散列值时, 可以调用 Objects.hash 并提供多个参数:

public static int hash(Object... values) {
    return Arrays.hashCode(values);
}

如果存在数组类型的字段, 那么可以使用静态的 Arrays.hashCode 方法计算一个散列码, 这个散列码由数组元素的散列码组成。

Arrays.hashCode

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

使用静态方法 Double.hashCode 来避免创建 Double 对象,其他基本类型的包装类也提供了相同静态方法:

public static int hashCode(double value) {
    long bits = doubleToLongBits(value);
    return (int)(bits ^ (bits >>> 32));
}

equals 与 hashCode 的定义必须相容:如果 x.equals(y) 返回 true ,那么 x.hashCode() 就必须返回与 y.hashCode() 相同的值

Record 自动提供一个 hashCode 方法,它会由字段值的散列码得出一个散列码

如果实例变量的取值范围很小,那么你需要得到尽可能不同的散列码

toString 方法

Object 类中的 toString 方法

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

绝大多数(但不是全部)的 toString 方法都遵循这样的格式:类的名字,随后是一对方括号括起来的字段值,例如 java.awt.Point[x=10,y=20]。

只要对象与一个字符串通过操作符 + 连接起来,Java 编译就会自动地调用 toString 方法,以便获得这个对象的字符串描述。在调用 x.toString() 的地方可以用 ""+x 替代。 这条语句将一个空串与 x 的字符串表示相连接。这里的 x 就是 x.toString() 。与 toString 不同的是, 如果 x 是基本类型, 这条语句照样能够执行。

数组继承了 Object 类的 toString 方法,[I@la46e30 ,前缀 [I 表明是一个整型数组,修正的方式是调用静态方法 Arrays.toString 。 要想打印多维数组(即, 数组的数组)则需要调用 Arrays.deepToString 方法。

Objects 类提供的工具方法:

public static String toString(Object o) {
    return String.valueOf(o);
}

public static String toString(Object o, String nullDefault) {
    return (o != null) ? o.toString() : nullDefault;
}

String.valueOf

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

泛型数组列表 ArrayList

在 Java 中,允许在运行时确定数组的大小。

int actualSize = ... ;
Employee[] staff = new Employee[actualSize];  

一旦确定了数组的大小,就无法再改变了。如果需要自动调整容量,使用 ArrayList

ArrayList 是一个采用类型参数( type parameter ) 的泛型类( generic class )。

下面声明和构造一个保存 Employee 对象的数组列表:

ArrayList<Employee> staff = new ArrayList<Employee>();

// 使用 var
var staff = new ArrayList<Employee>();

// 菱形 语法,不同时使用 var
ArrayList<Employee> staff = new ArrayList<>(); 

同时使用 var 和菱形语法,会生成 ArrayList<Object>

在 Java 的老版本中, 程序员使用 Vector 类实现动态数组。 不过, ArrayList 类更加有效, 没有任何理由一定要使用 Vector 类

方法名称 方法声明 描述
ensureCapacity void ensureCapacity(int minCapacity) 确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素
trimToSize void trimToSize() 将数组列表的存储容量削减到当前尺寸,在确定数组列表的大小不会再改变后调用

访问数组列表元素

下面这段代码是错误的:

ArrayList<String> list = new ArrayList(100); // capacity 100,size 0
list.set(0, "x"); // no element 0 yet
// java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

使用 add 方法为数组添加新元素, 而不要使用 set 方法, 它只能替换数组中已经存在的元素内容。

类型化与原始数组列表的兼容性

public void test2() {
    @SuppressWarnings("unchecked")
    ArrayList<String> list = (ArrayList<String>) arr();
}

private ArrayList arr() {
    return new ArrayList();
}

Java 的泛型类型是有限制的不完善的,鉴于兼容性的考虑, 编译器在对类型转换进行检査之后, 如果没有发现违反规则的现象, 就将所有的类型化数组列表转换成原始 ArrayList 对象。 在程序运行时, 所有的数组列表都是一样的, 即没有虚拟机中的类型参数。 因此, 类型转换 ( ArrayList ) 和 ( ArrayList<String> ) 将执行相同的运行时检查 。

在这种情形下, 你并不做什么。只要在与遗留的代码进行交叉操作时,研究一下编泽器的警告性提示,并确保这些警告不会造成太严重的后果就行了。

一旦能确保不会造成严重的后果, 可以用 @SuppressWarnings("unchecked") 标注来标记这个变量能够接受类型转换

对象包装器与自动装箱

有时, 需要将 int 这样的基本类型转换为对象。 所有的基本类型都有一个与之对应的类。例如,Integer 类对应基本类型 int 。通常, 这些类称为 包装器 ( wrapper ) 。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character 和 Boolean (前6 个类派生于公共的超类 Number )。对象包装器类是不可变的,即一旦构造了包装器,就不
允许更改包装在其中的值。同时, 对象包装器类还是 final , 因此不能定义它们的子类。

假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型,也就是说,不允许写成 ArrayList<int> 。这里就用到了 Integer 对象包装器类。 我们可以声明一个 Integer 对象的数组列表。

ArrayList<Integer> list = new ArrayList<>();

由于每个值分别包装在对象中, 所以 ArrayList<Integer> 的效率远远低于 int[] 数组。 因此, 应该用它构造小型集合,其原因是操作的方便性要比执行效率更加重要。

list.add(3); 将自动地变换成 list.add(Integer.value0f(3)); ,这种变换被称为 自动装箱(autoboxing ) 。

大家可能认为自动打包 (autowrapping) 更加合适, 而“ 装箱(boxing)” 这个词源自于 C#。

当将一个 Integer 对象赋给一个 int 值时, 将会自动地拆箱(unboxed)。也就是说, 编译器将下列语句:int n = list.get(i); 翻译成 int n = list.get(i).intValue();

在算术表达式中也能够自动地装箱和拆箱:

Integer n = 3;
n++;

编译器将自动地插入一条对象拆箱的指令, 然后进行自增计算, 最后再将结果装箱。

注意 == 运算符

自动装箱规范要求 boolean、byte、char <= 127,介于 -128 ~ 127 之间的 short 和 int 被包装到固定的对象中。因此,在此范围内的基本数据类型对象 == 成立。比较包装器对象时,应该使用 equals 方法。

不要用 == 比较包装器对象,不要将包装器对象作为锁,不要使用包装器类构造器,使用 valueOf 或自动装箱机制

由于包装器类引用可以为 null , 所以自动装箱有可能会抛出一个 NullPointerException 异常

另外, 如果在一个条件表达式中混合使用 Integer 和 Double 类型, Integer 值就会拆箱,提升为 double , 再装箱为 Double :

Integer n = 1;
Double x = 2.0;
System.out.println(true ? n : x); // Prints 1.0

装箱和拆箱是编译器认可的, 而不是虚拟机。编译器在生成类的字节码时, 插入必要的方法调用。虚拟机只是执行这些字节码。

使用包装器类可以将某些基本方法放在包装器中,例如 Integer.parseInt

参数个数可变的方法

变参方法 varargs

省略号 ... 表明这个方法可以接收任意数量的对象。

用户自己可以定义可变参数的方法, 并将参数指定为任意类型, 甚至是基本类型。

允许将一个数组传递给可变参数方法的最后一个参数。 因此,可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法,而不会破坏任何已经存在的代码。

@Test
public void test4() {
    System.out.println(max(1, 2, 3));
    System.out.println(max(new double[]{1, 2, 3}));
}

public double max(double... values) {
    double largest = Double.NEGATIVE_INFINITY;
    for (double v : values) if (v > largest) largest = v;
    return largest;
}

变参参数 double... 等同于 double[]

抽象类

如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看, 祖先类更加通用, 人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。

方法上使用 abstract 关键字,这样就完全不需要实现这个方法了

抽象方法充当着占位的角色, 它们的具体实现在子类中。扩展抽象类可以有两种选择。一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。

类即使不含抽象方法,也可以将类声明为抽象类。

包含一个或多个抽象方法的类本身必须是抽象的

抽象类可以有字段和具体方法,可以没有抽象方法

抽象类不能被实例化。也就是说, 如果将一个类声明为 abstract , 就不能创建这个类的对象。

可以定义一个抽象类的对象变量, 但是它只能引用非抽象子类的对象。

枚举类

enum Size {
    SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

    private Size(String abbreviation) {
        this.abbreviation = abbreviation;
    }

    public String getAbbreviation() {
        return abbreviation;
    }

    private String abbreviation;
}

枚举是特殊的类

上面的 Size 有4个实例,不可能构造新的对象

枚举的构造器是私有的

比较两个枚举类型的值时, 不需要调用 equals , 而直接使用 == 就可以了

如果需要的话, 可以在枚举类型中添加一些构造器、 方法和字段。 当然, 构造器只是在构造枚举常量的时候被调用。

所有的枚举类型都是 Enum 类的子类。

toString 、name 方法能够返回枚举常量名。 例如, Size.SMALL.toString() 将返回字符串 SMALL 。toString 的逆方法是静态方法 valueOf :

Size s = Enum.valueOf(Size,class, "SMALL");
Size s2 = Size.valueOf("SMALL");

每个枚举类型都有一个静态的 values 方法, 它将返回一个包含全部枚举值的数组。

ordinal 方法返回 enum 声明中枚举常量的位置,位置从 0 开始计数。

密封类

密封类(sealed class)控制哪些类可以继承它

sealed interface JSONValue permits JSONArray, JSONObject, JSONPrimitive {
}

permits 声明的是直接子类

密封类允许的子类必须是可访问的,不能是嵌套在另一个类中的私有类,也不能是另一个包中的包可见的类

密封类的公共子类必须与密封类在同一个包中,如果使用模块,必须在同一个模块中

声明密封类可以不加 permits 子句,这样它的所有直接子类必须在同一个文件中声明

使用密封类的一个重要原因是编译时检查,可以配合 switch 表达式使用,不需要 default

密封类的子类必须指定 sealed, final, non-sealed (允许派生子类)

密封接口与密封类相同,会控制直接子类型

反射

反射库(reflection library) 提供了一个非常丰富且精心设计的工具集, 以便编写能够动态操纵 Java 代码的程序。这项功能被大量地应用于 JavaBeans 中, 它是 Java 组件的体系结构 。

能够分析类能力的程序称为反射(reflective )。

Class 类

在程序运行期间,Java 运行时系统始终为所有对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。然而, 可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class , 这个名字很容易让人混淆。Object 类中的 getClass() 方法将会返回一个 Class 类型的实例。

Employee e;
...
Class cl = e.getClass();

还可以调用静态方法 forName 获得类名对应的 Class 对象:

String className = "java.util .Random";
Class cl = Class.forName(dassName) ;  

如果 T 是任意的 Java 类型(或 void 关键字),T.class 将代表匹配的类对象。例如:

Class cl1 = Random.class; // if you import java.util
Class cl2 = int.class;
Class cl3 = Doublet[].class;  

注意, 一个 Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类。 例如,int 不是类, 但 int.class 是一个 Class 类型的对象。

Class 类实际上是一个泛型类。例如, Employee.class 的类型是 Class<Employee> 。没有说明这个问题的原因是: 它将已经抽象的概念更加复杂化了。 在大多数实际问题中, 可以忽略类型参数, 而使用原始的 Class 类。

鉴于历史原因, getName 方法在应用于数组类型的时候会返回一个很奇怪的名字:

  • Double[].class.getName() 返回 [Ljava.lang.Double;
  • int[].class.getName() 返回 [I
System.out.println(boolean[].class.getName());  // [Z
System.out.println(char[].class.getName());     // [C
System.out.println(byte[].class.getName());     // [B
System.out.println(short[].class.getName());    // [S
System.out.println(int[].class.getName());      // [I
System.out.println(long[].class.getName());     // [J
System.out.println(float[].class.getName());    // [F
System.out.println(double[].class.getName());   // [D

System.out.println(Boolean[].class.getName());      // [Ljava.lang.Boolean;
System.out.println(Character[].class.getName());    // [Ljava.lang.Character;
System.out.println(Byte[].class.getName());         // [Ljava.lang.Byte;
System.out.println(Short[].class.getName());        // [Ljava.lang.Short;
System.out.println(Integer[].class.getName());      // [Ljava.lang.Integer;
System.out.println(Long[].class.getName());         // [Ljava.lang.Long;
System.out.println(Float[].class.getName());        // [Ljava.lang.Float;
System.out.println(Double[].class.getName());       // [Ljava.lang.Double;

System.out.println(Void[].class.getName());         // [Ljava.lang.Void;
System.out.println(String[].class.getName());       // [Ljava.lang.String;

虚拟机为每个类型管理一个 Class 对象。 因此, 可以利用 == 运算符实现两个类对象比较的操作。

Class.newInstance 已废弃,使用 Class.getConstructor 拿到 Constructor 后调用 newInstance

Constructor.newInstance 会把所有构造器异常包装到一个 InvocationTargetException

获得 Class 对象的方法:

  • Object.getClass
  • Class.forName
  • T.class

声明异常入门

当程序运行过程中发生错误时, 就会抛出异常,抛出异常比终止程序要灵活得多 ,这是因为可以提供一个捕获异常的处理器 (handler) 对异常情况进行处理

异常有两种类型: 非检查型(unchecked)异常 和 检查型(checked)异常 。 对于检查型异常, 编译器将会检查是否提供了处理器。 对于非检查型异常,应该避免这些异常产生。

方法名上使用 throws 子句声明抛出异常

资源

类通常有一些关联的数据文件,被称为资源(resource)

有些方法接收描述资源位置的 URL ,可以调用 getResource 方法

使用 getResourceAsStream 得到一个输入流来读取文件数据

国际化 API(internationalization API)经常使用资源文件,每种语言对应一个文件

利用反射分析类的能力

在 java.lang.reflect 包中有三个类 Field、 Method 和 Constructor 分别用于描述类的字段、 方法和构造器。

这三个类都有一个叫做 getName 的方法, 用来返回名称。

Field 类有一个 getType 方法, 用来返回描述字段所属类型的 Class 对象。Method 和 Constructor 类有能够报告参数类型的方法,Method 类还有一个可以报告返回类型的方法。

这三个类还有一个叫做 getModifiers 的方法, 它将返回一个整型数值, 用不同的 0/1 位描述 public 和 static 这样的修饰符使用状况。另外, 还可以利用 java.lang.reflect 包中的 Modifieir 类的静态方法分析 getModifiers 返回的整型数值。 例如, 可以使用 Modifier 类中的 isPublic、 isPrivate 或 isFinal 判断方法或构造器是否是 public、 private 或 final。 我们需要做的全部工作就是调用 Modifier 类的相应方法, 并对返回的整型数值进行分析, 另外,还可以利用 Modifier.toString 方法将修饰符打印出来。

Class 类中的 getFields、 getMethods 和 getConstructors 方法将分别返回类提供的 public 字段、 方法和构造器数组, 其中包括超类的公有成员。Class 类的 getDeclareFields 、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部字段、 方法和构造器, 其中包括私有和受保护成员,但不包括超类的成员。

使用反射在运行时分析对象

查看对象字段的关键方法是 Field 类中的 get 方法。 如果 f 是一个 Field 类型的对象(例如,通过 getDeclaredFields 得到的对象) ,obj 是某个包含 f 字段的类的对象,f.get(obj) 将返回一个对象,其值为 obj 字段的当前值。

如果 f 是私有字段, get 方法将会抛出一个 IllegalAccessException 。除非拥有访问权限,否则 Java 安全机制只允许査看任意对象有哪些字段, 而不允许读取它们的值。

反射机制的默认行为受限于 Java 的访问控制。然而, 如果一个 Java 程序没有受到安全管理器的控制, 就可以覆盖访问控制。 为了达到这个目的, 需要调用 Field、 Method 或 Constructor 对象的 setAccessible 方法。

f.setAtcessible(true);

setAccessible 方法是 AccessibleObject 类中的一个方法, 它是 Field 、 Method 和 Constructor 类的公共超类。

AccessibleObject 类中还有批量设置权限的静态方法 setAccessible

Field[] fields = cl.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);

使用反射编写泛型数组代码

java.lang.reflect 包中的 Array 类允许动态地创建数组。

将一个 Employee[] 临时地转换成 Object[] 数组, 然后再把它转换回来是可以的,但一个从开始就是 Object[] 的数组却永远不能转换成 Employe[] 数组。

以下方法可以用来扩展任意类型的数组, 而不仅是对象数组:

public static Object goodCopyOf(Object a, int newLength) {
    Class cl = a.getClass();
    if (!cl.isArray()) {
        return null;
    }
    Class componentType = cl.getComponentType();
    int length = Array.getLength(a);
    Object newArray = Array.newInstance(componentType, newLength);
    System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
    return newArray;
}

调用任意方法和构造器

反射机制允许你调用任意方法

在 Method 类中有一个 invoke 方法, 它允许调用包装在当前 Method 对象中的方法。

public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException

第一个参数是隐式参数, 其余的对象提供了显式参数(在 Java SE 5.0 以前的版本中, 必须传递一个对象数组, 如果没有显式参数就传递一个 null )。对于静态方法,第一个参数可以被忽略, 即可以将它设置为 null 。

如果返回类型是基本类型, invoke 方法会返回其包装器类型。

invoke 的参数和返回值必须是 Object 类型的。这就意味着必须进行多次的类型转换。

使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些

有鉴于此, 建议仅在必要的时候才使用 Method 对象,而最好使用接口以及 Java SE 8中的 lambda 表达式。 特别要重申: 建议 Java 开发者不要使用 Method 对象的回调功能。使用接口进行回调会使得代码的执行速度更快, 更易于维护。

getMethods 和 getDeclaredMethods 区别:

  • getMethods 返回父类和自身的 public 方法,不返回 private 方法
  • getDeclaredMethods 返回自身的 public 和 private 方法

构造器和方法使用反射调用时同理

继承的设计技巧

  1. 将公共操作和字段放在超类
  2. 不要使用受保护的字段
  3. 使用继承实现 “is-a” 关系
  4. 除非所有继承的方法都有意义, 否则不要使用继承
  5. 在覆盖方法时, 不要改变预期的行为
  6. 使用多态,而非类型信息
  7. 不要滥用反射
posted @ 2025-01-13 12:18  流星<。)#)))≦  阅读(43)  评论(0)    收藏  举报