Java-4-继承与反射

本章将介绍面向对象编程的另一重要特性- 类的继承,也就是在一个类的基础上新建一个有着更多特性的类。在本章中将会重点介绍 Object 类 - 一个所有类的父类。此外,反射(reflecion)也在本章进行介绍,它不直接在 coding 时指明所使用的类,而是在运行时再获得相关的类的信息。

A. 继承

我们直接通过一个例子来说明 类的继承包括哪些要点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 编写一个 Manager 类,继承自 Employee
*/
public class Manager extends Employee{
public Manager(String name, double salary){
super(name, salary); //调用超类的构造函数
bonus = 0;
}

private double bonus;
public void setBonus(double bonus){
this.bonus = bonus;
}
@override
public double getSalary(){
return super.getSalary() + bonus;
}
}
/**
* 调用
*/
Manager boss = new Manager();
boss.setBonus(100); //子类中额外定义的方法
boss.raiseSalary(5); //可以调用继承自 Employee 的 nonprivate 方法
Employee empl = boss; //将子类的实例赋值给超类类型的变量
double salary = empl.getSalary(); //实际上是调用的 超类的 getSalary() 方法
//cast 的使用
if(empl instanceof Manager){
Manager mgr = (Manager)empl;
mgr.setBonus(1000); //强转之后,不会再报 compiler-error
}

a. 子类构造要点

超类的特点

超类与子类: 在上述例子中,Manager 为 子类(subclass), Employee 类为 超类(superclass)。

必须要被继承的类 抽象类通过 abstract 关键字进行修饰,一个抽象类中要包含抽象方法,也使用 abstract 关键字进行修饰(抽象类中也可以有非抽象方法)。一个抽象类无法通过 new 创建一个实例,必须被子类重写 抽象方法 之后才能 创建实例。虽然不能创建实例,但是非抽象子类的实例可以赋值给抽象类类型的变量。参考例子如下:

1
2
3
4
5
6
7
8
9
10
11
public abstract class Person{
private String name;
public Person(String name){this.name = name};
public abstract int getId(); //抽象方法
}
public class Student extends Person{
private int id;
public Student(String name, int Id){super(name); this.id = id;}
public int getId(){return id}; //override 抽象方法
}
Person p = new Student("Fred", 1111); //可以将子类赋值给抽象超类

无法被继承的类:如果一个类 不想再通过子类 进行拓展,可以将 类 声明为 final, 从而没有子类可以继承该类。例子有 String, LocalTimeURL 类。

方法的继承与重写

子类的构造函数: 如果超类中没有不带参数的构造函数,则子类的构造函数中必须调用 超类的构造函数,并且调用需要放在子类构造函数的第一句。操作时,使用 super 关键字调用 超类的构造函数。

方法继承: 在子类中可以继承超类中的所有非私有方法,且可以拥有自己额外的变量和方法,例如上面 Manager 类的 bonus 变量和 setBonus 方法。(ref)

子类无法获得超类的 private 方法,但 对于超类中的 protected 方法,无论子类在哪个包里,子类总是可以获取 超类中 使用 protected 修饰的变量和方法。

类继承 与 接口实现 的冲突:如果一个子类,所继承的超类和 所实现的接口 中有相同的方法(超类中方法为非abstract,接口中方法为 default,即都有实际实现,在子类中无需重写,从而形成一个冲突),默认的,子类的方法为超类中的方法,我们称这一特性为 “classes win”。 (出于兼容考虑)

超类方法的重写: 在子类中可以重写 超类中的 非私有方法。需要注意的是,在子类中,无法调用(invoke) 超类的 private 变量/方法。只能调用超类的 public 方法,上面 super.getSalary() ,就是通过 super 关键字,调用超类的 public 方法。需要注意的是,这里的 super 并不是超类对象的引用,而是一个动态的方法传递。

方法重写的时候需要注意它的参数和返回值:子类的重写方法的参数 必须和超类的参数 相同;子类的重写方法的返回值需要与超类方法的返回值 相同 或者是 超类方法返回值 类型 的 子类

重写方法的权限至少要与超类方法的权限相同,e.g. 超类中方法的权限为 public, 则子类中方法的权限必须为 public

final方法 如果一个方法声明为 final 类型,则子类无法重写该方法。例如 Object 类的 getClass 方法,就为 final 类型,任何子类无法复写。

b. 匿名子类

当我们只是一次性使用一个类的时候,类似之前实现接口的匿名类,我们也可以使用匿名类继承超类,一个例子如下:

1
2
3
4
5
6
ArrayList<String> names = new ArrayList<String>(100){
public void add(int index, String element){
super.add(index, element);
system.out.printf("Adding %s at %d \n", element, index);
}
}

可以看到,在上述代码中,首先 小括号中的 100 被传递给 超类的 构造方法,构造了一个 长度为 100 的一个字符串型数组列表。 接着我们构造了一个匿名子类重写了 ArrayList<String> 类的 add 方法。

上面的例子可以看到,这样使用匿名子类的好处首先是不需要新建一个文件做一个新类并给该子类起名字,此外,可以直接通过超类的构造方法进行类的构造,从而省去了重写超类构造方法的过程。

c. 子类对象的使用

将子类实例赋值给超类类型的变量:Java 允许我们将子类的实例对象 声明/赋值 给超类类型的对象,并且如果调用 子类 重写了的超类方法,最终会调用子类的该方法(而不是超类的该方法,例如例子中, getSalary(), 最终调用的是 Manager 类的这一方法)。Java 中的这一机制叫做:动态方法查找(dynamic method lookup)。

但需要注意的是,如果将子类对象赋值给超类类型,无法调用子类中有而超类中没有的方法(例如上面如果写 empl.setBonus();,会发生编译错误(但实际上不会发生 runtime 错误))。

Casts:可以通过配合使用 cast 和 instanceof 将声明为 超类的 子类对象 强转为子类类型。

B. Object类-所有类的父类

在 Java 中,每一个类都 直接或者间接 继承 Object 类。如果一个类不显式的声明继承一个类,那么它 默认的继承 Object 类。下面的部分,我们对 Object 类中的的重点方法进行介绍,首先是3个被重写最多的方法,toString(), equals(), hashCode() 方法, 最后介绍了较为复杂的 clone() 方法。

a. toString 方法

每一个类都会有一个 toString 方法,返回一个与该类内容相关的,描述该类的字符串。

Object 类默认的 toString 方法 输出 “该类的 类名 + 该类的 hashcode”。

一般的,我们遵循下述格式 重写一个类的 toString 方法,输出格式为: “类名 + 使用方括号包裹的实例变量”。 对于之前的 Employee 类,我们可以重写 toString 方法如下:

1
2
3
public String toString(){
return getClass().getName() + "[name = " + name + ", Salary = " + salary + "]";
}

需要注意的是,对于 数组 Arrays,默认的 toString 方法是返回该数组的地址。 如果想获得数组中的内容,需要调用 Arrays.toString() 方法进行转换,例子如下:

1
2
3
int[] primes = {2, 3, 4, 5, 7};
primes.toString(); //结果为 "[I@xxxxx", 其中 "[I" 表示该数组为整形数组,后面为该数组的地址
Arrays.toString(primes); //结果为 "[2, 3, 4, 5, 7]".

b. equals 方法

用于判断两个对象是否相等的方法。在 Object 中,该方法默认比较两个对象的引用是否相等。

而对于内容依赖型的对象,例如 String 类,我们通常需要重写 它的 equals 方法。(需要注意的是,如果重写 equals 方法,必须同时重写或者保证 hashcode 方法与 equals 方法兼容)我们以下面的代码为例进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Item{
private String description;
private double price;
public boolean equals (Object otherObject){
if(this == otherObject) return true;

if(otherObject == null) return false;
if(getClass() != otherObject.getClass()) return false;
Item other = (Item)otherObject;
return Objects.equals(description, other.description)
&& price == other.price;
}
}

针对上述代码进行的比较,我们可以按照步骤进行解释:

  1. 直接判断引用是否相等,如果相等,两个对象一定相等,从而减少后面进行判断的 efforts;
  2. 如果传入的对象是 null, 直接返回 false
  3. 由于我们重写的 是 Object 类的 equals 方法,所以传入的参数一定为 Object 类型,所以接下来需要先判断类型是否相等,此处使用 getClass() 方法进行判断。
    1. 此外,还可以使用 instanceof 方法进行判断, 但需要注意二者的区别, 如上面代码 使用 getClass() 进行判断,两个对象的类型必须完全一致。但是如果使用 otherObject instanceof Item 进行判断,otheroObjectitem 类或者 item 的子类,判断语句都为真。
    2. 因此,如果使用 instanceof 进行判断,可能会导致 equals 判断的不对称,也就是说 x.equals(y)y.equals(x) 的结果可能不相等。
    3. instanceof 方法进行判断对于一种情况是安全的,就是我们只要比较超类中的某个变量,而不理会子类中的任何差异。这是,这个超类中的 equals 方法可以声明为 final
  4. 如果类型相符后,需要通过强制转换将 otherObject 强制转换为当前的类的类型。
  5. 比较内容的时候需要注意:
    1. 对于基本数据类型,直接使用 == 进行比较(也可以利用包装类的 equals 方法,例如 Double.equals 防止因为 某个数字为 $\infty$ 或者 NaN 导致判断失败);
    2. 对于对象,我们可以使用 Objects.equals, Arrays.equals,以及对象所属类的 equals 方法。

此外,对于子类重写 equals 方法,一般先引用超类的 equals 方法,if(!super.equals()) return false;

c. hashCode 方法

hash code 是对象对应的一个整数。如果两个对象不同,那么它的 hash code 必须不同,反之,如果两个对象判定为相同,则 hash code 必须相同。String 类的 hashCode 方法如下:

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

从上面的描述中,我们知,hashCode 方法必须和 equals 方法 兼容。

对于 Object 类,它的 hashCode 方法根据实现有下面几类方法,1- 根据数据存储的地址推导 hash code;2- 使用 pseudorandom (伪随机数)or sequential 数字作为对象的 hash code。总之,它保证相同的对象拥有相同的 hash code。

如果我们重写了 Object 类的 equals 方法,就必须重写 hashCode 方法使之与 equals 方法兼容。(否则,在将 对象放入 hash set 或者 hash map 的时候会出现问题)。针对上面重写了 equals 方法的 Item 类,我们重写它的 hashCode 方法如下;

1
2
3
public int hashCode(){
return Objects.hash(description, price);
}

Objects.hash() 方法根据输入的参数计算 hash code 并将它们组合起来。此外,对于 arrays, 使用 Arrays.hashCode() 方法进行判断。

d. clone 方法

不同于上述几个被重写频率很高的方法, clone 方法相对复杂,且被重写的频率也较低。

对于 Object 中的 clone 方法,他提供了对原对象的 浅复制(shallow copy, 也有叫做浅拷贝的),即只复制基本数据类型的元素和不变化(immutable)的元素(具体包括数字元素以及 String),而对于引用类型,则只拷贝地址,也就是说拷贝的对象和原对象中的引用类型元素指向同一块数据实体。

在这里,我们总结对于 clone 方法的几种情况:

  1. 不需要提供 clone 方法,例如 Scanner 对象。
    1. 处理方法:不用做任何处理,由于 Objectclone 方法为 protected 的,因此,如果不在子类中进行重写将其变为 public,使用是就无法调用该方法。
  2. 需要提供 clone 方法,但只需要浅复制。
    1. 处理方法:子类需要实现 Cloneable 接口(这个接口实际上没有任何方法,我们称之为 tagging or marker 接口),Object.clone 方法会首先检查子类是否实现 了 cloneable 接口,如果没有,会抛出 CloneNotSupportedException。此外,我们需要将 重写 clone 方法, 主要是修改 获取权限 和 返回值。具体参考下方的代码例子。需要注意的是,调用 Objectclone 方法后,返回的默认的是 Object 类的对象,需要进行 数据类型的 强制转换。
  3. clone 方法需要实现 深复制。
    1. 处理方法:需要重写 Object 类的 clone 方法,具体操作参考下方例子代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 上方情况2 例子代码
public class Employee implements Cloneable{
public Employee clone() throws CloneNotSupportedException{
return (Employee) super.clone(); //默认是 Object 类的对象
}
}

//上方情况3例子代码
public class Message{
private String sender; //浅复制复制该变量的值
private ArrayList<String> recipients; //浅复制只复制引用
...
public void addRecipient(String recipient){...}

// 重写 clone 方法1:
public Message clone(){
Message cloned = new Message(sender);
cloned.recipients = new ArrayList<> (recipients); //做深复制,
//因为在类的内部,这里可以直接用 cloned.recipients
return cloned;
}
//重写 clone 方法2:(实际中,二种实现只能留一个)
public Message clone(){
try{
Message cloned = new Message(sender);
@SuppressWarnings("unchecked") ArrayList<String> clonedRecipients
= (ArrayList<String>) recipients.clone(); //recipients.clone()返回 Object 类型对象
cloned.recipients = clonedRecipients;
return cloned;
} catch (CloneNotSupportedException ex){
return null;
}
}
}

对于第三种情况额外说明:从上面的代码可以看出,重写 Objectclone 方法主要是针对 引用类型的对象,需要将它们从浅复制 改为 深复制。 在上述的重写方法2中,调用了 ArrayListclone 方法,需要注意的是,实际上这个 clone 方法也是浅复制,只是是复制其中每一个元素的地址,对于上述的例子来说,是可以接受的。具体详解参考:巧妙利用 ArrayList 的 clone 方法

C. 枚举类型(Enumerations)

枚举类型(enumerated type) 是 java 5 中添加的,是一种特殊的数据类型。通过例子看它的写法:public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};

枚举类的构造方法: 一般不会为枚举类做构造函数,如果要写,枚举类的构造方法 必须为 private 类型。

Enumerated Type 的常用方法

valueOf(xx) 方法,返回 Enumeration 中的某个值,如果输入的参数在枚举值中没有,则抛出异常。

values() 方法,以数组形式返回枚举类中的所有元素,Size[] allValues = Size.values()

我们可以给每个 enum 实例 添加方法,添加的方法必须是重写的 enum 类中定义的方法。例子如下:

1
2
3
4
5
6
7
8
9
10
public enum Operation{
ADD{
public int eval(int arg1, int arg2){return arg1 + arg2;
},
SUBTRACT{
public int eval(int arg1, int arg2){return arg1 - arg2};
};
public abstract int eval(int arg1, int arg2);
}
}

可以发现,enum 类中的 元素实际上都可以看作是 Operation 类的匿名子类。

静态成员变量:enum 类中允许有静态成员变量。但是需要注意的是,枚举类的 enumerated constants 在静态变量 构建之前就被 构建,所以,在 枚举类 的构造函数 中不允许调用静态成员变量(因为他们还没生成,这一点与一般的类不同)。解决办法不在 构造函数 中调用 静态变量进行初始化,而是使用 静态代码块 进行初始化。

枚举类型 for switching 可以使用 switch 函数来处理枚举类型,例如上面的 Operation, 还可以通过下面形式实现加减运算。

1
2
3
4
5
6
7
8
9
enum Operation{ADD, SUBTRACT};
public static int eval(Operation op, int arg1, int arg2){
int result = 0;
switch(op){
case ADD: result = arg1 + arg2; break;
case SUBSTRACT: result = arg1 - arg2; break;
}
return result;
}

具体进一步 ref 廖雪峰官方网站-枚举类

D. 运行时类型信息与资源

Java 在运行过程(runtime)过程可以判断对象所属的类,本节将对这一功能进行讲解。此外,还会介绍 Java 加载资源的过程。

a. Class类

类的获取

Class 类对象的 获取:对于对象,我们可以调用 getClass() 获得它对应的类,返回值的类型 即为 Class 类型,具体代码如下:

1
2
Object obj = ...;
Class<?> cl = obj.getClass();

此外,还可以通过 .class 后缀获得对象 的 class 类型对象。实际上,.class 后缀 还可以获取 类、接口、基本数据类型、void 等的类型信息,因此,从这个角度讲,可能这里的 Class 理解为 type 更加合适。

常用方法

getName() 方法,返回得到类的名字。需要注意的是,对于 array,例如 String[].class.getName(), 返回 [Ljava.lang.String, int[].class.getName(), 返回 “[I”. 两个返回值的前缀部分分别表示 字符串数组 和 整型数组。

forName() 方法,是 Class 类的静态方法,通过 Class.forName , 可以通过名字 在运行时构造 类的对象(不同于一般的在编译时就确定类的对象的情况)。例子如下:

1
2
String className = "java.util.Scanner";
Class<?> cl = Class.forName(className);

Class 类提供的一个重要服务是可以定位程序运行所需要的资源,例子如下:

1
2
InputStream stream = MyClass.class.getResourceAsStream("config.txt");   //加载配置文件
Scanner in = new Scanner(stream);

b. 类加载器(Class loaders)

类加载器负责向 虚拟机中加载类的内容,供虚拟机运行,java 的加载机制可以分为3步:

  1. bootstrap class loader: Java 库最基础的加载部分,是虚拟机的一部分;
  2. platform class loader:加载其他 java 库文件,允许进行一定的 configure;
  3. system class loader:加载应用类。这一过程首先加载 main 方法所在的部分,然后自动加载 main 方法中涉及到的部分。

下面的例子,展示了我们通过 文件地址,配合类加载器,手动向虚拟机加载相关类文件的过程:

1
2
3
4
5
6
7
8
9
URL[] urls = {
new URL("file://path/to/directory/"),
new URL("file://path/to/jarfile.jar")
};
String className = "com.mycompany.plugins.Entry";
try(URLClassLoader loader = new URLClassLoader(urls)){
Class<?> cl = Class.forName(className, true, loader);
//通过 Class 类的 forName 方法配合 类加载器 loader 生成了一个 类的实例 cl
}

上下文类加载器

在进行类加载的时候,我们可能遇到下述问题:

  1. 我们提供了一个 Util class,用于利用类名加载类,该 Util 类由 系统类加载器(system class loader)进行加载。(代码在下方)
  2. 我们利用另一个类加载器,从 plugin JAR 包加载一个 plugin。然后该 plugin 调用 Util.createInstance("com.mycompany.plugins.MyClass") 来实例化 plugin 包中的类。
1
2
3
4
5
6
public Class Util{
Object createInstance(String className){
Class<?> cl = Class.forName(className);
...
}
}

但实际过程中,上述代码由于 java 类加载器的反向机制(? classloader inversion),无法通过 Util 类加载器所加载的 createInstance() 方法来实例化 plugin 包中的类, 因为实际运行中,Util.createInstance 使用它自己的类加载器来执行 Class.forName, 而这个 class loader 不会去 plugin 所在的 JAR 包中查找对应的类。

解决策略一个是直接向 createInstance(String className, ClassLoader loader) 方法中传入 plugin 包的 class loader. 另一个则是利用上下文类加载器,通过新建一个 Thread 来传入该类加载器。

1
2
3
4
5
6
7
8
9
10
Thread t = Thread.currentThread();
t.setContextClasslaoder(loader);
//update Util 类
public Class Util{
Object createInstance(String className){
ClassLoader loader = t.getContextLoader();
Class<?> cl = Class.forName(className,true,loader);
...
}
}

新线程的类加载器默认是 系统类加载器,但也可以通过 setContextClasslaoder 方法指定特定的加载器。再在函数中通过getContextLoader 来获取。

Service Loader:在程序装配(assemble)和 部署(deploy)我们需要提供不同的服务来帮助其运行。ServiceLoader 可以通过加载适合接口的类型服务,具体的操作方法是在 META-INF/services 文件中添加文件,加入所需要的服务名。然后,通过 ServiceLoader.load() 方法进行调用即可。

E. 反射

反射(Reflection)多用于构建工具,在应用编写中相对少用,故在这里,先对其特性有个大致的了解。(ref)。

反射机制 用于在运行时检视对象的内容并且调用任意的方法,该特性可用于实现 object-relational mappers(ORM) 以及 GUI builders 的工具的构建。(ORM- 对象关系映射,是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的资料之间的转换。)

提供枚举类中所有元素的类及方法: 在 java.lang.reflect 中,Field, Method, Constructor 三个类,描述了类的所有组成部分。这3个类都有 getName() 方法返回其中的成员名; getType 返回 Class 类的对象;getModifiers 返回一个对应的修饰符状态(以一个整数表达),可以结合使用 Modifier.isPublicgetModifiers 的返回值判断修饰语。

Field:通过 Field 类 的 get() 方法 (field 实际上就是类中的 变量- ref field,method,constructor) 还可以获得对象中的元素值,通过 setDouble 等 set 方法设定值。(在设定值之前需要先 setAccessible(true)).

Method: 可以通过 Method 类调用方法。

Constructor: 可以通过 Constructor 类的对象的 newInstance() 方法构建新的实例。

Working with Arraysjava.lang.reflect 包中的 Array 类可用于新建 Array 类型对象。

Proxy Proxy 类可用于在运行时 生成新的实现特定接口的类(1个或多个接口)。

JavaBeans: 是 Java 中一种能够方便地管理 属性、方法、和事件的类(主要用于 GUI 编程)。它通过 public Type getProperty() 读取,public void setProperty(Type newValue) 进行属性(property)的操作。