继承

继承(inheritance)是面向对象编程的另一个基本概念。继承背后的理念是可以在已有类的基础上创建新类。本章包含了反射内容(看来反射在应用级别并不属于高级话题啊),帮助在运行程序的过程中来找到其它的类(技术复杂,对于制作工具的开发人员很重要,但是应用开发人员不会常用)

类、超类、子类

Employee类为例子(上一章类和面向对象中的例子),如果现在多出了有关经理的管理,这时,经理的大部分特性和普通职员是一样的,但是在计算薪水的过程中,职员完成自己的工作拿到工资,但是经理如果做得很好可以得到额外的奖金。这种情况适用于继承的场景。首先,定义一个新类Manager,然后增加一些功能,但是可以保留一些Employee类中已经写过的功能,包括类中所有的属性。抽象一些,这就是典型的is-a关系。is-a关系是继承最显著的特点

定义子类

public class Manager extends Employee
{
    added methods and fields
}

已有的类叫做超类(superclass)、基类(baseclass)或父类(parent class)。新类叫做子类(subclass、derived class、child class)。super和sub是来自于理论计算机科学和数学的术语,即可以理解为Employee的全集包含Manager的全集或者Manager的全集是Employee的子集

Manager类包含新属性和新方法来设置属性

public class Manager extends Employee
{
    private double bonus;
    . . .
    public void setBonus(double bonus)
    {
        this.bonus = bonus;
    }
}

Manager类自动从Employee类中继承了它们的方法和属性。在定义子类时,只需要关心它们的区别。设计类时,应把最通用的方法放在超类,而更特别的方法放在对应子类中

覆写方法

一些超类的方法可能不适合Manager子类。即getSalary方法应该返回基本工资和奖金的总和,所以应该在子类中新写一个方法

public class Manager extends Employee
{
    . . .
    public double getSalary()
    {
        . . .
    }
    . . .
}

在方法体中,如果直接使用return salary + bonus;是不行的,因为只有Employee中的方法才能直接访问这个类中的属性,所以Manager类不能直接访问salary属性,如果要访问,那么也应该使用公有接口,即

public double getSalary()
{
    double baseSalary = getSalary(); // still won't work
    return baseSalary + bonus;
}

现在的问题是getSalary调用的是这个覆写的方法,结果就是无限循环。要表示我们想调用父类的方法,应该使用关键字super

public double getSalary()
{
    double baseSalary = super.getSalary();
    return baseSalary + bonus;
}

有些人认为super和this的含义类似。其实这并不准确,因为super不是一个对象的引用,即你不能将super的值赋给任何一个对象变量。super是一个特殊的关键字,用来告诉编译器调用父类中的方法

子类构造器

public Manager(String name, double salary, int year, int month, int day)
{
    super(name, salary, year, month, day);
    bonus = 0;
}

这里的super含义与上一节不同,这是调用父类构造器的简写。因为子类不能访问超类的私有属性,所以它必须通过构造器来初始化它们。使用super调用构造器必须在子类构造器的第一行。如果子类构造器没有显式调用父类构造器,那么父类的无参构造器会被自动调用。如果父类没有无参构造器并且子类构造器没有显式调用父类的其它构造器,Java编译器会报错

Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
var staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);

for (Employee e : staff)
    System.out.println(e.getName() + " " + e.getSalary());

e.getSalary方法可以调用e存储的实际对象的对应方法。虚拟机知道每个对象的实际类型。一个对象变量可以引用不同的真实类型叫做多态

继承层级(hierarchies)

继承不需要在某一层停止。继承自一个共同超类的所有类的叫做一个继承层级(inheritance hierarchy)。一个祖先类经常会有不止一条继承链(chain)。C++中,一个类可以有多个父类,而Java不支持这个特性

多态(polymorphism)

可以帮你判断是否使用继承的简单法则是对数据正确的设计。is-a规则描述了子类每个对象都是一个父类的对象。例如Manager对象都是Employee对象,但是反之则是不成立的。另一个系统表达is-a规则的是替代法则,即无论何时程序需要一个父类对象,都可以把一个子类对象赋值给超类对象

Employee e;
e = new Employee(. . .); // Employee object expected
e = new Manager(. . .); // OK, Manager can be used as well

Java中,对象变量是多态的。一个Employee类型的变量可以引用Employee类型的对象,也可以引用所有它的子类

Java中,子类引用的数组可以自动转换为父类引用数组,不需要通过强制转换

Manager[] managers = new Manager[10];
Employee[] staff = managers; // OK

这本身是符合继承规则的,但是可能出现一个问题,即managersstaff现在引用的同一个数组,如果执行staff[0] = new Employee("Harry Hacker", . . .);,那么编译器不会报错,但是现在如果调用managers[0].setBonus(1000),编译器就会报错。为了规避这种情况,所有的数组都会记得它们创建时保存的数据类型,之后它们只会存储兼容的类型对象(本例中即Manager及其子类对象),否则会报ArrayStoreException异常

理解方法调用

一个对象是如何进行方法调用的?

x.f(args),隐式参数x是类C的一个对象,这个语句实际发生了:

  1. 编译器会查看对象的声明类型和方法名(可能会有重载方法,编译器会遍历类C中的所有f方法和它的所有父类的f方法(父类的私有方法不能被访问))。现在编译器知道了可以调用的所有f方法
  2. 编译器根据调用方法提供的参数来决定参数类型。如果在所有的f中找到一个参数类型最符合提供的参数类型,那么这个方法被调用(这个过程叫做overloading resolution)。由于类型转换的存在,这个过程可能会很复杂。如果编译器找不到任何方法参数符合给定的参数,那么编译器会报错。现在编译器知道了要被调用的方法的名字和参数类型

如果在子类中定义了方法签名和父类某个方法相同的方法,即覆写(overriding)了父类方法。返回类型不是方法签名的一部分。然而在覆写方法时,应该保持返回值类型的兼容性。子类覆写方法可以将返回值改成父类原方法返回值类型的子类型

  1. 如果方法时私有的、静态的、 final的,或是一个构造器,那么编译器也知道要调用的是哪个方法。这叫静态绑定(static binding)。此外,方法调用依赖隐式参数的真实类型,动态绑定只能在运行时使用
  2. 当程序运行并且使用动态绑定来调用方法。虚拟机必须调用适合x引用的对象的方法版本。如果真实类型是D,是C的子类,如果D定义了一个方法f(String),这个方法会被调用,否则会去父类寻找方法f(String)。这个过程可能会花一些时间,然而,虚拟机为每个类提前计算了方法表(method table),它列出了所有的方法签名和会被调用的方法。当一个方法别调用时,虚拟机只是查看方法表
// Employee method table:
getName() -> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()

// Manager method table:
getName() -> Employee.getName()
getSalary() -> Manager.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
setBonus(double) -> Manager.setBonus(double)

运行时,e.getSalary()(e是Employee类型的变量)的处理过程:

  1. 虚拟机将e的真实类型的方法表取出(可能是Employee Manager或其它Employee的子类)
  2. 虚拟机在类中查找getSalary()方法,此时它知道该调用哪个方法
  3. 虚拟机调用方法

动态绑定让程序不需要修改已有代码就能扩展。新类扩展已有的类,那么已有的类(通用功能)不需要再次编译

在覆写方法是,子类的方法的可见性至少和超类相同,尤其是超类方法是public,那么子类也必须是public

阻止继承: Final类和方法

有时,你想阻止别人继承自己的类。final类是不能被扩展的,在类的定义中使用final修饰符就能表达这个含义

public final class Executive extends Manager
{
    . . .
}

也可以让类中的某个方法是final的。如果这样做,子类不能覆写这个方法(final类中所有方法都是final的)

public class Employee
{
    . . .
    public final String getName()
    {
        return name;
    }
    . . .
}

属性也可以被声明为final,一个final属性在对象被创建后是不能被更改的,然而,类如果被声明为final,只是它的类自动变为final,而属性不是

让一个方法或类为final只有一个好理由这样做,即确保它的语义在子类中不能被改变,例如Calendar类中的getTime setTime方法。这表明Calendar类的设计者已经完成了Date类和Calendar状态(state)的转换,所有子类不能打乱这个设定。类似的,String类是final的,这代表当你引用一个String时,它只能是一个String而不是别的

类型转换

// staff 为 Employee[]
Manager boss = (Manager) staff[0];

只有一个理由来做对象之间的类型转换,即暂时忘记了对象的真实类型且要使用这个对象真实类型的全部特性

将子类对象赋值给父类变量的引用时,相当于你给编译器很少的promise,但是反过来这个过程,相当于给了编译器更多的promise,在运行时编译器会做类型转换检查

// staff[1] 的真实类型是Employee
Manager boss = (Manager) staff[1]; // ERROR

当程序运行时,Java的运行时系统会通知这是一个broken promise并且生成一个ClassCastException。因此,在转换前先判断转换是否能成功是一个好的编程实践

if (staff[1] instanceof Manager)
{
    boss = (Manager) staff[1];
    . . .
}

如果转换不可能成功,编译器不会允许这个转换,String c = (String)staff[1];是一个编译器错误,因为String不是一个Employee的子类

x instanceof C如果x是null也不会报错,只是返回false。事实上,通过强制转换来改变对象的类型不是个好办法。

用强制转换的唯一理由是要使用Manager类独有的方法。如果出现要在Employee对象上调用setBonus的情况,先查看是否在程序设计上出现了问题。通常情况下,要尽量少使用强制转换和instanceof

抽象类

在继承层级上看,类会变得更通用且更抽象。有时,祖先类变得非常通用以至于它只是其他类的基础类而不是一个会使用它的实例对象的类

例如,Employee类,一个职员是一个人(Person)一个学生(Student)也是一个人。那么Person类中有很多对于人来说重要的属性,例如namegetName()getDescription()方法来介绍某个人。对于Employee Student类,很容易实现这几个方法,那么Person类中能提供什么信息?这个类除了名字以外对其它的信息一无所知,getDescription()方法如果要实现,可能返回一个空字符串。如果使用abstract关键字,就不需要实现这个方法了

public abstract class Person
{
    . . .
    public abstract String getDescription();
}

除了抽象方法,抽象类中还能又属性和具体方法

public abstract class Person
{
    private String name;
    public Person(String name)
    {
        this.name = name;
    }
    public abstract String getDescription();
    public String getName()
    {
        return name;
    }
}

总应该将通用的属性和方法(无论是否抽象方法)都放在超类(无论是否抽象类)。抽象方法就像是方法的占位符。当继承抽象类时,有两个选项。继续让抽象方法保持抽象,此时需要保留方法的abstract修饰符。或者在子类实现所有方法,让这个子类不再是抽象类

一个没有抽象方法的类也可以被定义为抽象类。其意义是,一旦类被声明为抽象类,那么不能实例化它,即不能创建这个类的对象。但是可以创建具体子类的对象,并且可以使用抽象类来创建对象变量,但是这个变量只能引用具体对象

Protected Access

一般来说,类中的属性最好定义为private 方法最好标记为public。当你想限制一个方法(少数情况下,一个属性)能被其子类访问,那么将类的某个特性声明为protectedEmployee类将hireDay属性设为protected,即它的子类可以直接访问这个属性

Java中,一个protected属性可以在同一个包中的任何类访问,现在考虑不同包中的Administrator子类。它的方法可以查看它自己的hireDay属性,而不能访问其它Employee对象的这个属性

使用protected修饰符要注意,如果这个类被其它用户使用,那么他们可以通过继承这个类来直接访问你的类中protected的属性

protected修饰的方法,通常是将它作为一个tricky来使用,即表示子类可以被信任能正确使用这个方法,而其它的类不行

Java中的修饰符

  1. private:类中访问
  2. public:全世界都可以访问
  3. protected:包内任何地方和所有子类
  4. default:包内任何地方可以访问(不写修饰符)

Object:整个宇宙的超类

Object类是终极祖先,Java中的每个类都继承Object,如果一个类没有显式声明超类,那么它自动继承Object

Object类型的变量

可以使用Object类型的变量来引用任何类型的对象。Java中,只有基本数据类型不是对象。所有的数组类型(不管数组元素是什么类型)都是继承于Object的类的类型

equals方法

Object类中的equals方法用来测试一个对象是否和另一个对象相等,在Object类中这个方法的实现是两个对象引用是否相等。这个默认实现符合逻辑,如果两个对象是同一个对象,那么它们一定相等。当你想实现基于状态的相等比较,应该重写这个方法

public class Employee
{
    . . .
    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 name.equals(other.name)
            && salary == other.salary
            && hireDay.equals(other.hireDay);
    }
}

getClass方法返回一个对象的类

为了保证namehireDay是null时的比较行为,可以使用Objects.equals方法,如果两个参数都是null那么返回true,其中一个为null则返回false,否则会调用a.equals(b),上述方法可以改写为

return Objects.equals(name, other.name)
    && salary == other.salary
    && Objects.equals(hireDay, other.hireDay);

如果在子类中定义此方法,首先调用超类的equals方法,如果父类的属性都相等,那么可以比较子类的实例属性

Equality Testing and Inheritance

如果隐式和显式参数不属于同一个类,那么equals会有什么行为?在前面的例子中,如果两个对象不都属于Employee,那么会返回false,有些人喜欢使用instanceof,这样可以包含子类,但是这样会有一些问题

Java规范对equals方法有一些要求

  1. 自反(反身)的:对任何非null引用x,x.equals(x)应该返回true
  2. 对称的:x.equals(y) y.equals(x)的结果应该相同
  3. 可传递的:x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也得返回true
  4. 一致的:如果x和y引用的对象没有变,那么每次调用x.equals(y)的结果都相同
  5. 对任何非null引用x,x.equals(null)应该返回false

然而,对于对称性来说,如果x y属于不同的类,可能会产生微妙的结果。如果使用instanceof来判断是否可以比较,那么sub.equals(super)没有办法执行(对称性要求返回相同的结果而不是不同的结果或抛出异常)

一些书的作者认为getClass方法破坏了替换原则,例子是AbstractSet类的equals()方法,它有两个实现子类,TreeSet HashSet,它们使用不同的算法来储存元素,当你要比较两个Set的时候,并不关心它们是怎么实现的。然而,Set的例子很特殊,所以将Abstract.equals设置为final,因为不能让人重新定义Set的相等性(实际上它并不是final,它允许子类实现更高效的比较算法)

现在有两种情况:

  1. 如果子类有自己的比较原则,那么对称性要求使用getClass测试
  2. 如果超类固定了比较原则,那么可以使用instanceof测试,并且允许不同类型的子类可以相等(如果状态相同)(应该将超类的equals方法定义为final)

JDK中有超过150个equals方法的实现,什么形式都有,例如instanceof getClass() 捕获异常 什么都不做

写一个完美equals()方法的原则

  1. 将显式参数命名为otherObject,之后需要将它转换成你应该调用的对象,将这个变量命名为other
  2. 测试隐式参数是否和显式参数引用同一个对象if (this == otherObject) return true;
  3. 测试otherObject是否为null,如果是则返回false if (otherObject == null) return false;
  4. 比较this otherObject的类,如果equals的语义在在子类会改变,那么使用getClass测试(if (getClass() != otherObject.getClass()) return false;),如果所有的子类的比较语义和超类相同,那么使用instanceof测试(if (!(otherObject instanceof ClassName)) return false;
  5. otherObject转换为你的类(比较)的类型ClassName other = (ClassName) otherObject
  6. 比较属性,基本类型使用==,对象属性使用Objects.equals(a, b)。如果在子类中重新定义了equals,那么要调用super.equals(other)

如果要比较数组是否相同,可以使用Arrays.equals方法

hashCode方法

一个hash码是从一个对象来的整数值。hash码应该是争抢的(scrambled,即不同的对象应该是不同的hash码)

String类实现的计算hash码的方法

for (int i = 0; i < length(); i++)
    hash = 31 * hash + charAt(i);

hashCode方法定义在Object类中,因此,每个对象都有一个默认的hash码。这个hash码是通过对象的内存地址计算出来的

// 2556
var s = "Ok";
// 20526976
var sb = new StringBuilder(s);
// 2556
var t = new String("Ok");
// 20527144
var tb = new StringBuilder(t);

s和t的hash码相同,是因为String的hash码是根据它们的内容计算出来的。因为StringBuilderhashCode方法使用的是从Object继承下来的方法,所以是根据内存地址计算出来的

如果你重新定义了equals方法,那也需要重新定义hashCode方法(针对于用户可能会把它插入到hash表之类的数据结构中),hashCode方法应该返回一个整数值,可以为负数。将实例属性的hash码组合起来,那么不同对象的hash码更可能分散

使用Objects.hashCode,如果参数是null则返回0,否则使用这个参数的hashCode方法

public int hashCode()
{
    return 7 * Objects.hashCode(name)
        + 11 * Double.hashCode(salary)
        + 13 * Objects.hashCode(hireDay);
}

// 组合多个hash值
return Objects.hash(name, salary, hireDay);

equals hashCode方法必须兼容,如果x.equals(y)返回true,那么x.hashCode() y.hashCode()的返回值必须相等

如果有数组类型的属性,可以使用Arrays.hashCode来计算数组元素组合起来的hash码

toString方法

返回一个字符串,表示这个对象的值

java.awt.Point[x=10,y=20]

大多数toString()方法都是以下格式:类名、方括号包起来的属性名和属性值

public String toString()
{
    return "Employee[name=" + name
        + ",salary=" + salary
        + ",hireDay=" + hireDay
        + "]";
}

public String toString()
{
    // 使用getClass().getName()来替代手写类名
    return getClass().getName()
        + "[name=" + name
        + ",salary=" + salary
        + ",hireDay=" + hireDay
        + "]";
}

toString方法是很常见的,任何一个对象和一个字符串通过+操作符拼接,Java编译器都会自动调用这个对象的toString方法来获取这个对象的描述字符串

ObjecttoString方法是类名+对象hash码

使用Arrays.toString方法可以查看字符串形式的数组的内容。要正确打印多维数组,可以使用Arrays.deepToString

强烈建议给自己写的每个类加一个toString方法,使用一些人类友好的信息描述这个类

Generic Array Lists

使用动态泛型数组ArrayList

声明 ArrayList

ArrayList<Employee> staff = new ArrayList<Employee>();
// Java10,避免重复类名
var staff = new ArrayList<Employee>();
// 不使用var,右边可以不写类型名称(钻石语法diamond syntax)
ArrayList<Employee> staff = new ArrayList<>();
staff.add(new Employee("Harry Hacker", . . .));
staff.add(new Employee("Tony Tester", . . .));

在ArrayList内部存储(数组)满了的时候,调用add方法,ArrayList会自动创建一个更大的数组,并且将当前小数组的元素复制进大数组。如果已经知道要存储的内容数量,那么在填充数组之前调用staff.ensureCapacity(100);,这个调用会给数组分配100个对象存储空间。那么前100个元素的添加操作不会有扩容操作

// capacity is 100
new ArrayList<>(100)
// 动态数组当前持有的对象数量
staff.size()

如果确认数组中的元素数量不会再改变,可以调用trimToSize方法,GC会回收所有超出存储元素的内存,再一次添加新元素会移除限制(花时间),所以只有当确定不会改变数组中元素数量(增加)再使用此方法

访问ArrayList中的元素

// a[i] = harry;
staff.set(i, harry);

在ArrayList中的元素数量大于i之前,不要调用list.set(i, x)

在泛型之前的raw ArrayList,get set接收的都是Object类型的对象,这种行为是比较危险的。在插入值时不会报错,只有在取出值并且进行类型转换时才会报错

在指定位置插入元素使用staff.add(n, e);,位置n之后的元素会给这个新插入的元素让出位置

移除指定位置的元素(并返回被移除的元素)Employee e = staff.remove(n);

// 遍历
for (Employee e : staff)
    do something with e

对象Wrapper和自动装箱

所有的基本数据类型都有对应的包装类,包装类的名称和基本数据类型一一对应。这些类都是immutable的,即在创建值后不能更改。且这些类都是final的,不能继承。泛型类型参数不能为基本数据类型,所以应该使用包装类类型

ArrayList<Integer>效率比int[]低很多,所以只有在便捷性比效率更重要的时候使用前者

// auto box
// 自动装箱
list.add(3);
// auto unbox
int n = list.get(i);
// 编译器会先拆箱,然后增加值,然后再装箱
Integer n = 3;
n++;

包装类对象和基本数据类型的不同点只有一点:相等性。==应用于包装类对象时,比较的是两个对象的内存地址,实际上,应该调用equals方法来比较

自动装箱规范规定:

  • boolean,byte,char <= 127
  • short, int -128 <= xx <= 127
    这些数值范围不论类型都会包装到一个固定对象中,所以它们的比较会成功

其它的一些自动装箱的话题:

  1. 包装类的引用可以为null,所以自动拆箱可能会报NPE
  2. 如果将Integer和Double类型放在三目表达式,那么Integer会拆箱提升到double值再装箱成为Double
  3. 装箱和拆箱的是编译器的工作,而不是虚拟机

org.omg.CORBApackage: IntHolder, BooleanHolder等等类,它们是mutable的

public static void triple(IntHolder x)
{
    x.value = 3 * x.value;
}

可变参数的方法

varargs方法,即定义一个可以通过不同数量参数调用的方法

// ... 也是Java代码的一部分,它表示方法可以接收任意数量的对象
// 这个方法如果传入的是基本数据类型,那么会自动装箱
public class PrintStream
{
    public PrintStream printf(String fmt, Object... args) { 
        return format(fmt, args); 
    }
}

Object...参数类型其实和Object[]的类型是一样的

枚举类

// 典型枚举类型
public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }

这种声明代表Size是一个类,这个类只有四个实例,且不能创建新的对象。所以不需要使用equals()方法,可以直接使用==

可以对枚举添加构造器、方法和属性。构造器只有当枚举常量被构造是会调用

public enum Size
{
    SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
    
    private String abbreviation;
    private Size(String abbreviation) { 
        this.abbreviation = abbreviation; 
    }
    public String getAbbreviation() { return abbreviation; }
}

枚举的构造器总是private,可以忽略private修饰符

所有的枚举类都是Enum类的子类,它们会继承一些方法,最常用的是toString()

Size s = Enum.valueOf(Size.class, "SMALL");
Size[] values = Size.values();
// 返回某个枚举常量的声明位置(从0开始)
Size.MEDIUM.ordinal() 

枚举类(Enum)是有泛型参数的,即Size继承的是Enum<Size>,类型参数用在compareTo方法中

反射

反射库提供了非常丰富且强大的工具箱来写动态处理Java代码的程序。一个可以分析类的能力的程序被称作可反射的(reflective)

Class

程序运行时,Java运行系统总是在所有的对象上维护了一套运行时类型验证(runtime type identification)系统。这个系统保持对每个对象所属类的追踪。运行时类型信息被虚拟机用来选择正确的方法去调用。这个系统信息可以通过Class类来操作

Object中的getClass方法返回Class类型的实例

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

Class对象描述了某个特定类的(各种)属性,最常用的方法可能是getName(),它返回的是类名

// print Employee Harry Hacker 此时e是一个Employee
// Manager Harry Hacker 此时e是一个Manager
System.out.println(e.getClass().getName() + " " + e.getName());

如果类在一个包中,那么包名也是类名的一个部分

Class cl = generator.getClass();
String name = cl.getName(); // name is set to "java.util.Random"

可以通过Class的静态方法forName方法来获取一个Class对象

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

如果className是一个类或这接口的名字,那么这样是有效的

首先,包含main方法的类会被加载,它(main方法内)加载所有需要的类,每个加载类再加载它们需要的类。这个过程会花费很长时间,你可以通过一些技巧来给用户一个启动程序很快的幻觉

保证main方法所在的类不显式引用其它的类,而通过Class.forName来手动强制加载其它的类

第三种获得Class对象的方法是T.class

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

Class对象只是描述了类型,那么可能是类类型也可能不是,例如int不是对象类型,但是int.class也会返回一个Class类型的对象

Class是一个泛型类

由于历史原因,getName方法对于数组类型会返回奇怪的名字

Double[].class.getName() returns "[Ljava.lang.Double;".
int[].class.getName() returns "[I".

虚拟机对每个类型管理着独一无二的Class对象,如果e是一个Employee的实例,那么if (e.getClass() == Employee.class) . . .会通过,和instanceof的结果不同,如果e是Employee的子类的实例,那么前面的判断不会通过

通过Class的对象,可以构造这个类型的对象,

var className = "java.util.Random"; // or any other name of a class with
// a no-arg constructor
Class cl = Class.forName(className);
Object obj = cl.getConstructor().newInstance();

A Primer on Declaring Exceptions

运行时发生的错误,程序可以抛出一个异常,抛出异常的机制比终止程序更灵活,应为可以提供一个handler来处理异常。如果不处理异常,那么程序会终止并且打印错误信息到控制台,包括异常的类型

异常有两种

  1. 非受检异常(unchecked exceptions):大部分异常(运行时)
  2. 受检异常(checked exceptions):编译器检查,程序员会解决这些问题

这里只是简单提了一下异常,使用的策略就是直接在方法签名抛出受检异常,异常主题宜再开一篇

Resources

类经常会和一些数据文件有关联。例如Image和声音文件,Text和信息字符串和按钮标签。Java中,这种关联文件叫做资源(resource)

假设有一个对话框,它显示书名和版权年份,那么每本书的信息都不一样,所以应该把信息放在文本文件而不是使用字符串硬编码

比较方便的做法是将关联文件和程序文件放在一个JAR包

Class类提供了一个有用的服务来定位资源文件

  1. 获取一个需要资源的类的Class对象,例如ResourceTest.class
  2. 一些方法,例如ImageIcongetImage,接收描述资源路径的URL当参数。然后调用URL url = cl.getResource("about.gif");
  3. 或者使用getResourceAsStream来获取读取文件数据的输入流

这里的关键点是Java虚拟机知道怎么定位一个类,所以它可以在同样的地方搜索关联的资源。如果ResourceTest类在resources包中,那么ResourceTest.class文件会在resources文件夹中,然后可以把图标文件放在同一个目录

除了把资源文件放在类文件相同目录下,还可以提供一个相对或绝对路径。自动加载资源文件是资源加载特性做的全部事情,没有标准的方法来描述所有资源文件,所以每个程序必须定义自己的处理资源文件的方法

另一个常见的资源文件的应用是程序国际化,每种语言会放在一个资源文件中,Java的国际化API支持组织和访问这些文件

使用反射来分析类的能力

Field Method Constructor三个类描述了类中的属性(成员变量),方法和构造器。这三个类都有getName方法来返回各自的名称

Field类有getType来返回一个Class对象,描述的是这个属性的类
Method Constructor有方法来得到参数的类型
Method有方法得到返回值的类型

这三个类都有getModifiers来返回一个整数(不同(二进制)位(开关)决定),描述的是它们的修饰符

可以使用Modifier类中的静态方法 isPublic isPrivate isFinal等方法接收表示修饰符的整数来做判断

Modifier类中的toString方法可以打印修饰符

一个Class类中的getFields getMethods getConstructors返回的公有属性,方法和构造器,同时包括了超类的(对应)公有成员。getDeclaredFields getDeclaredMethods getDeclaredConstructors返回的是所有的属性、方法和构造器,它们包括私有成员、包级别访问、保护成员,但是不包括超类的成员

使用反射来分析运行时的对象

如果f是一个类型为Field的对象,objf所在对象,那么f.get(obj)会返回一个对象,它的值就是obj的属性的当前值

var harry = new Employee("Harry Hacker", 50000, 10, 1, 1989);
Class cl = harry.getClass();
// the class object representing Employee
Field f = cl.getDeclaredField("name");
// the name field of the Employee class
Object v = f.get(harry);
// the value of the name field of the harry object, i.e.,
// the String object "Harry Hacker"

使用f.set(obj,value)可以设置f所在的这个对象的属性的值。在例子中,name属性是私有的,所有调用get或set方法会抛出IllegalAccessException异常。Java的安全机制使得可以找到对象有哪些属性而不能读、写,除非获得许可。反射机制的默认行为会顾及Java的访问控制。但是可以通过调用setAccessible方法(Field Method Constructor对象都有)覆盖访问控制。这个方法属于AccessibleObject类,即这几个对象共同的超类。这个特性用于debugger、持久化存储和类似的应用

访问可以被模块系统或一个安全管理器(security manager)

Java9和Java10,在使用反射访问非公有特性时只会有警告信息

这里省略的信息(使用反射写的一个通用的toString方法)以后再来理解吧

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

java.lang.reflect包中的Array类允许你动态创建数组。在Arrays.copyOf方法中使用了这种方式

var a = new Employee[100];
. . .
// array is full
a = Arrays.copyOf(a, 2 * a.length);
public static Object[] badCopyOf(Object[] a, int newLength) // not useful
{
    var newArray = new Object[newLength];
    System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
    return newArray;
}

上述代码中,返回的数组类型是Object[],所以这个对象数组没办法直接转为Employee[]

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类中的方法
// 第一个参数是隐式参数,后面是显式参数
// 对于静态方法,可以将隐式参数设为null
// 如果返回值是基本数据类型,这个方法会返回装箱后的类型
Object invoke(Object obj, Object... args)

使用getDeclaredMethods getMethod来获取Method对象

使用相同的过程来调用任何构造器

对于继承的设计建议

  1. 将通用(共有)的操作和属性放在超类
  2. 不要使用protected属性,这个修饰符并没有提供足够的保护
    1. 子类是没办法限制的,任何人都可以继承这个类然后访问修改protected的属性,此时已经破坏了封装原则
    2. Java中,同包内所有类都能访问protected属性,无论是不是子类
    3. protected方法在表示这个方法不通用且需要在子类重写时很有用
  3. 使用继承来给“is-a”关系建模
  4. 除非所有的继承方法(对子类都)有意义,否则不要使用继承
  5. 当你覆写方法时,不要改变预期行为(替换原则)
  6. 使用多态,而不使用类型信息(即不要各种类型转换)
  7. 不要过度使用反射(反射是脆弱的, 即编译器无法帮助你找到错误)
posted on 2020-12-08 14:46  老鼠不上树  阅读(251)  评论(0)    收藏  举报