继承
继承已存在的类就是复用这些类的方法,而且可以增加一些新的方法和字段,使新类能够适应新的情况
类、超类和子类
比如公司中的经理和员工的关系,经理 is-a 员工,即经理是个员工,is-a 关系是继承的一个明显特征
定义子类
通过关键字 extends
表示继承
在 Java 中,所有继承都是公共继承,而没有 C++ 中的保护继承和私有继承
关键字 extends
表明正在构造的新类派生于一个已经存在的类。这个已经存在的类称为超类 superclass
、基类 base class
或父类 parent class
;新类称为子类 subclass
、派生类 derived class
或孩子类 child class
。超类和子类是 Java 程序员最常用的两个术语
将通用功能抽取到基类的做法在面向对象程序设计中十分普遍
覆盖方法
超类中的有些方法对子类并不适用,此时可以提供一个新的方法来覆盖 override
超类中的这个方法。而在子类中是不能直接访问到父类中的私有字段,此时需要想起他方法一样使用公共接口。而如果想要调用父类的方法,可以使用 super
关键字,比如:
super.getSalary()
有些人认为
super
和this
引用是类似的概念,但实际上,super
不是一个对象的引用,只是一个编译器调用超类方法的特殊关键字
在 C++ 中使用超类名加
::
操作符的形式
子类构造器
public Manager(String name, double salary, int year, int month, int day) {
super(name, salary, year, month, day);
bonus = 0;
}
这里的 super
是调用超类中带有 name
、 salary
、 year
、 month
和 day
参数的构造器的简写形式。由于子类的构造器不能访问父类的私有字段,所以可以利用特殊的 super
语法调用这个构造器。使用 super
调用构造器的语句必须是子类构造器的第一条语句
如果子类的构造器没有显式的调用超类的构造器,将自动地调用超类的无参数构造器,如果超类没有无参数构造器,则会报一个错误。
当使用比如 e.getSalary()
调用方法时, e
类型可能为父类,也可为子类,此时 getSalary
方法会自动引用对应父类和子类的对象
一个对象变量可以指示多种实际类型的现象称为多态 polymorphism
,在运行时能够自动选择适当的方法,称为动态绑定 dynamic binding
.
在 C++ 中,如果希望实现动态绑定,则需要将成员函数声明为 virtual。在 Java 中,动态绑定是默认的行为
多态
前面提到的 is-a 规则的另一个表述是替换原则。它指出程序中出现超类对象的任何地方都可以使用子类对象替换。
在 java 程序设计语言中,对象变量是多态的 polymorphic
。一个 Employee
类型的对象既可以引用一个 Employee
类型的对象,也可以引用 Employee
类的任何一个子类的对象。
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss; // staff[0] 和 boss 引用同一个对象。但编译器只将 staff[0] 看成一个 Employee 对象
可以这样调用
boss.setBonus(5000);
但不能这样调用
staff[0].setBonus(5000);
这是因为 staff[0]
声明的类型是 Employee
,而 setBonus
不是 Employee
类的方法
而且不能将超类的引用赋给子类变量。比如:
Manager m = staff[i];
在 Java 中,子类引用的数组可以转换成超类引用的数组,而不需要使用强制类型转换。比如
Manager[] managers = new Manager[10];
Employee[] staff = managers;
理解方法调用
准确地理解如何在对象上应用方法调用非常重要。下面假设要调用 x.f(args)
,隐式参数 x
声明为类 c
的一个对象。下面是调用过程的详细描述:
-
编译器查看对象的声明类型和方法名。需要注意的是:可能存在多个名为
f
但参数类型不一样的方法。比如f(int)
和f(String)
。编译器会一一列举所有名为f
的方法和超类中名为f
且可以访问的方法。至此,编译器已知道所有可能被调用的备选方法。 -
接下来,编译器要确定方法调用中提供的参数类型。如果在所有名为
f
的方法中存在一个与所提供参数类型完全匹配的方法,就选择这个方法。这个过程称为重载解析方法的名字和参数称为方法的签名。如果在子类中定义了一个与曹磊签名相同的方法,那么子类中的这个方法就会覆盖子类中这个相同签名的方法。
返回类型不是签名的一部分,但在覆盖一个方法时,需要保证返回类型的兼容性。允许子类将副高方法的返回类型改为原返回类型的子类型
-
如果是
private
方法、static
方法、final
方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法。这称为静态绑定。于此对应,如果要调用的方法依赖于隐式参数的实际类型,那么必须在允许时使用动态绑定。
-
程序运行且柴永动态绑定调用方法时,虚拟机必须调用与
x
所引用对象的实际类型对应的那个方法。
每次调用方法都需要完成这个搜索,时间开销很大。因此,虚拟机预先为每个类计算了一个方法表。在真正调用方法时,虚拟机查找这个表就行了。
在覆盖一个方法时,子类方法不能低于超类方法的可见性。如果超类方法是
public
,子类方法必须也要声明为public
。
阻止继承:final 类和方法
有时,我们可能希望阻止人们利用某个类定义子类。不允许扩展的类称为 final
类。如果在定义类的时候时候用 final
修饰符就表明这个类是 final
类。声明格式如下
public final class Executive extends Manager {
...
}
类中某个方法也能被声明为 final
,但是这样做,子类就嫩覆盖这个方法
强制类型转换
Java 程序设计语言为强制类型转换提供了一个特殊的表示法:
// 将表达式 x 的值转换成整数类型,舍弃了小数部分
double x = 3.405;
int nx = (int)x;
有时候可能需要将某个类的对象引用转换成另外一个类的对象引用。此时仅需要用一对圆括号将目标类名括起来,并放置在需要转换到对象引用之前就可以了。
Manager boss = (Manager) staff[0];
进行强制类型转换的唯一原因是:要在暂时忽视对象的实际类型之后使用对象的全部功能。
在 Java 中,每一个对象变量都会有一个类型。类型描述了这变量所引用的以及能够引用的对象类型。例如, staff[i]
引用一个 Employee
对象(因此它还可以引用 Manager
对象)。
如果将一个子类对象的引用赋给一个超类对象,编译器是允许的。但将一个超类的引用赋给一个子类变量时,就承诺过多了,必须进行强制类型转换。这样才能够通过运行时检查。
Manager boss = (Manager) staff[1]; // ERROR
所以在进行强制类型转换时,先查看是否能够成功地进行转换。此时只需要使用 instanceof
操作符就可以实现
if (staff[1] instanceof Manager) {
boss = (Manager) staff[1];
...
}
综上所述:
- 只能在继承层次内进行强制类型转换
- 在将超类强制类型转换成子类之前,应该使用
instanceof
进行检查
如果 x 为 null,则 x instanceof C 不会产生异常,只是返回 false
一般情况下,尽量少使用强制类型转换和 instanceof
运算符
抽象类
如果自下而上在类的 1 继承层次结构中上移,位于上层的类更具有一般性,可能更加抽象。从某种角度看,祖先类更具有一般性,人们只将它作为派生其他类的基类,而不是用来构造你想使用的特定的实例。
包含一个或多个抽象方法的类本身必须被声明为抽象的
public abstract class Person {
...;
public abstract String getDescription();
}
除了抽象方法之外,抽象类还包含字段和具体方法。
抽象方法充当着占位方法的角色,它们在子类中具体实现。扩展抽象类可以有两种选择。一种是在子类中保留抽象类中的部分或所有抽象方法仍未定义,这样就必须将子类也标记为抽象类;另一种做法是定义全部方法,这样一来,子类就不是抽象的了。
抽象类不能实例化。
假如定义了抽象超类 Person
和两个具体子类 Employee
和 Student
。下面将员工和学生对象填充到一个 Person
引用数组
var people = new Person[2];
people[0] = new Employee(...);
people[1] = new Student(...);
之后,输出这些对象的姓名和对象描述
for (Person p : people)
System.out.print(p.getName() + ", " + p.getDescription());
上面的 p.getDescription()
调用的不是抽象类 Person
对象,而是引用 Employee
或 Student
这样的具体子类的对象。而如果在抽象类中省略抽象方法,那么比如 p.getDescription()
是不能调用抽象方法的。
受保护访问
在有些时候,你可能希望限制超类中的某个方法只允许子类访问,或者更少地,可能希望允许子类的方法访问某个超类的字段。如果将超类中的字段设为 protected
,子类方法就能直接访问这个字段。
在 Java 中,保护字段只能由同一个包中的类访问。
在实际应用中,要谨慎使用受保护字段。如果其他程序员由这个类派生出的新类并访问你的受保护字段,此时你修改你的类,必然影响那些程序员。
事实上,Java 中的受保护部分对所有子类和同一个包中的其他类都可见
Object: 所有类的超类
Object
是 Java 中所有类的始祖,在 Java 中每个类都扩展了 Object
,所以熟悉这个类提供的服务非常重要。
Object 类型的变量
可以使用 Object
类型的变量引用任何类型的对象:
Object obj = new Employee("Harry Hacker", 3500)
当然, Object
类型的变量只能用于作为各种值的一个泛型容器。要想对其中的内容进行具体的操作,还需要弄清楚对象的原始类型,并进行相应的强制类型转换
Employee e = (Employee) obj;
在 Java 中,只有基本类型不是对象,比如,数值、字符和布尔类型的值都不是对象
所有数组类型,不管是对象数组还是基本类型的数组都扩展了 Object
类
Employee[] staff = new Employee[10];
obj = staff;
obj = new int[10];
equals 方法
Object
类中的 equals
方法用于检测一个对象是否等于另一个对象。 Object
类中实现的 equals
方法将确定两个对象引用是否相等。不过,经常需要基于状态检测对象的相等性,如果两个对象有相同的状态,才认为这两个对象是相等的。
只有在两个对象属于同一个类时,才有可能相等。下面这个示例展示 equals
方法的实现机制,我们推荐这样做
public class Employee {
...
public boolean equals(Object otherObject) {
// 检测引用
if (this == otherObject) return true;
// 必须返回 false 如果另一个参数为 null
if (otherObject == null) return false;
// 如果类不能匹配,它们不相等
if (getClass() !== otherObject.getClass()) return false;
// 现在我们知道了 otherObject 是一个非空的 Employee
Employee other = (Employee) otherObject;
// 看字段是否相等
return name.equals(other.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay);
}
}
在子类中定义 equals
方法时,首先调用超类的 equals
。如果检测失败,对象就不可能相等。如果超类中的字段都相等,就需要比较子类中的实例字段。
相等测试与继承
使用 instanceof
进行检测时,会允许检测的类属于子类,所以最好不要采取这种方式。
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
对于数组类型的字段,可以使用静态的
Arrays.equals
方法检测相应的数组元素是否相等
hashCode 方法
散列码 hash code
是由对象导出的一个整型值。散列码是没有规律的。如果 x
和 y
是两个不同的对象, x.hashCode()
与 y.hashCode()
基本上不会相同。
String 类使用以下算法计算散列码:
int hash = 0;
for (int i = 0; i < length(); i++)
hash = 31 * hash + charAt(i);
由于 hashCode
方法定义在 Object
类中,因此每个对象都有一个默认的散列码,其值由对象的存储地址得出
如果重新定义了 equals
方法,就必须为用户可能插入散列表的对象重新定义 hashCode
方法
hashCode
方法返回一个整数(也可以是负数)。要合理地组合实例字段的散列码,以便能够让不同对象产生的散列码分布更加均匀。比如,下面是 Employee
类的 hashCode
方法
public class Employee {
public int hashCode() {
return 7 * name.hashCode()
+ 11 * new Double(salary).hashCode()
+ 13 * hireDay.hashCode();
}
}
不过,还可以使用 null
安全的方法 Objects.hashCode
。如果其参数为 null
,这个方法会返回 0
,否则返回对参数调用 hashCode
的结果。另外,使用 Double.hashCode
来避免创建 Double
对象:
public int hashCode() {
return 7 * Objects.hashCode(name)
+ 11 * Double.hashCode(salary)
+ 13 * Objects.hashCode(hireDay);
}
还有更好的方法,需要组组合多个散列值时,可以调用 Object.hash
并提供所有这些参数。这个方法会对各个参数调用 Objects.hashCode
,并组合这些散列值
public int hashCode() {
return Objects.hash(name, salary, hireDay);
}
如果存在数组类型的字段,那么使用静态的
Arrays.hashCode
方法计算一个散列码,这个散列码由数组元素的散列码组成
toString 方法
在 Object
中还有一个重要的方法,就是 toString
方法,它会返回表示对象值的一个字符串。比如 Point
类的 toString
方法将返回下面这样的字符串:
java.awt.Point[x=10,y=20]
绝大多数(但不是全部)的 toString
方法都遵循这样的格式:类的名字,随后是以对方括号括起来的字段值。下面是 Employee
类中 toString
方法的实现:
public String toString() {
return "Employee[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
实际上,还可以设计得更好一些。最好通过调用 getClass()
, getName()
获得类名的字符串,而不要将类名硬编码写到 toString()
方法中
public String toString() {
return getClass().getName()
+ "[name" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
这样 toString
方法也可以由子类调用。
当然,设计子类的程序员应该定义自己的 toString
方法,并加入子类的字段,如果超类使用了 getClass().getName()
,那么子类只要调用 super.toString()
就可以了
随处可见 toString
方法的主要原因是:只要对象与一个字符串通过操作符 +
连接起来,Java 编译器就会自动调用 toString
方法来获得这个对象的字符串描述
可以不写为
x.toString()
,而写作""+x
。这条语句与toString
不同的是,即使x
是基本类型,这条语句照样都能够执行
数组继承了
object
类的toString
方法。打印出来的结果不尽如人意,此时可以使用Arrays.toString
方法,想要打印多维数组,需要调用Arrays.deepToString
方法
toString
方法是一个非常有用的调试工具,所有使用这个类的程序员也会从这个日志记录中支持中受益匪浅
泛型数组列表
在许多程序设计语言,特别是在 C/C++ 语言中,必须在编译时就必须确定整个数组的大小。在 Java 中,就可以在运行时确定数组的大小。
int actualSize = ...;
var staff = new Employee[actualSize];
这段代码并没有完全解决运行时动态改变数组的问题,此时解决这个问题最简单的方法是使用 ArrayList
, ArrayList
类似于数组,但在添加或删除元素时,它能够自动地调整数据容量。
ArrayList
是一个有类型参数 type parameter
的泛型类。为了指定数组列表保存对象的类型,需要用一对尖括号将类名括起来追加到 ArrayList
的后面
声明数组列表
声明和构造一个保存 Employee
对象的数组
ArrayList<Employee> staff = new ArrayList<Emloyee>();
在 Java 10 中,最好使用 var
关键字以避免重复写类名
var staff = new ArrayList<Employee>()
如果没用使用 var
关键字,可以省去右边的类型参数
ArrayList<Employee> staff = new ArrayList<>()
使用 add
方法添加元素
staff.add(new Employee("Harry Hacker", ...));
数组列表管理着一个内部的对象引用数组。最终,这个数组的空间可能全部用尽,而如果调用 add
则会在内部数组满了的时候自动创建一个更大的数组,并将所有对象从较小的数组拷贝到较大的数组中
如果已经知道或能够估计出数组可能存储的元素树龄,就可以在填充数组之前调用 ensureCapacity
方法:
staff.ensureCapacity(100)
这样前 100 次 add
调用不会带来开销很大的重新分配空间
还可以把初始容量传递给 ArrayList
构造器
ArrayList<Employee> staff = new ArrayList<>(100)
new Array<>(100) 和 new Employee[100] 不同,数组是有一百个空位置可以使用,而数组列表是可能保存 100 个元素,不过要以重新分配内存为代价
size
方法返回数组列表中包含的实际元素个数
staff.size()
一旦确认了数组列表大小将保持恒定,不再发生变化,就可以调用 trimToSize
方法,这个方法将存储块的大小调整为保存当前元素数量所需要的存储空间。垃圾回收期将回收多余的存储空间。
访问数组列表元素
数组列表自动扩展的便利增加了访问元素语法的复杂程度。其原因是 ArrayList
并不是 Java 程序设计语言的一部分:它只是某个人编写并在标准库中提供的一个使用工具类。
不能使用我们喜爱的 []
语法访问改变数组的元素,而要使用 get
和 set
方法
例如,要设置第 i 个元素,可以使用:
staff.set(i, harry);
它等价于数组 a
的元素赋值
a[i] = harry;
只有当数组列表的大小大于 i 时,才能够调用
list.set(i, x)
要得到一个数组列表的元素,可以使用:
Employee e = staff.get(i);
等价于
Employee e = a[i]
没有泛型类时,
get
方法只能返回Object
,因此,get
方法的调用者必须对返回值进行强制类型转换Employee e = (Employee) staff.get(i);
可以使用 staff.add(n, e)
来插入元素
可以使用 staff.remove(n)
来删除元素
插入和删除元素的操作效率很低,对于较小的数组可以这样做,对于大的数组可以考虑使用链表。
对象包装器与自动装箱
有时需要将 int
这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。比如, Integer
类对应基本类型 int
。通常,这些类称为包装器 wrapper
。这些类有显而易见的名字: Integer
、 Long
、 Float
、 Double
、 Short
、 Byte
、 Character
和 Boolean
(前六个派生于公共的超类 Number
)。包装器类是不可变的,一旦构造了包装器,就不允许更改包装在其中的值。同时,包装器类还是 final
,因此不能派生它们的子类。
想要定义一个整型数组列表,尖括号中的类型参数不允许是基本类型,比如 ArrayList<int>
,此时可以用到 Integer
包装器类
var list = new ArrayList<Integer>();
由于每个值分别包装在对象中,所以
ArrayList<Integer>
的效率远远低于int[]
数组,因此只有当程序员操作的方便性比执行效率更重要的时候,才会考虑对较小集合使用这种构造
有一种很有用的特性,可以很容易地向 ArrayList<Integer>
添加 int
类型的元素,下面这个调用
list.add(3);
将自动地变换为
list.add(Integer.valueOf(3));
这种变换称为自动装箱 autoboxing
当将一个 Integer
对象赋给一个 int
值时,将会自动拆箱。也就是说,编译器讲一下语句:
int n = list.get(i);
转换为
int n = list.get(i).intValue();
自动装箱和拆箱也适用于算术表达式。例如,可以将自增运算符应用于一个包装器引用:
Integer n = 3;
n++;
==
运算符可以应用于包装器对象,但是检测的是对选哪个是否有相同的内存位置
Integer a = 1000;
Integer b = 1000;
if (a == b) ...
解决这个问题的方法是在比较两个包装器对象时调用 equals
方法
自动装箱要求
boolean
、byte
、char<=127
、,介于-128
和127
之间的short
和int
被包装到固定的对象中。比如将a
和b
初始化为 100,那么它们的比较结果一定相同。
由于包装器类引用可以为 null,所以自动装箱可能会抛出一个 NullPointerException
异常:
Integer n = null;
System.out.println(2 * n); // throws NullPointerException
另外,如果在一个条件表达式中混合使用 Integer
和 Double
类型, Integer
值就会拆箱,提升为 double
,再装箱为 Double
Integer n = 1;
Double x = 2.0;
System.out.println(true ? n : x); // prints 1.0
装箱和拆箱是编译器要做的工作,而不是虚拟机编译器在生成类的字节码时会插入必要的方法调用。虚拟机只是执行这些字节码
参数数量可变的方法
可以提供参数数量可变的方法(有时这些方法被称为”变参“方法)
比如 printf
:
System.out.printf("%d", n)
和 System.out.printf("%d %s", n, "widgets");
printf
方法是这样定义的
public class PrintStream {
public PrintStream printf(String fmt, Object... args) { return format(fmt, args); }
}
这里的省略号是 Java 代码的一部分,它表明这个方法可以接受任意数量的对象(除 fmt
参数以外)
实际上, printf
方法接受两个参数,一个是格式字符串,一个是 Object[]
数组,其中保存着所有其他参数(如果调用者提供的是整数或是其他基本类型的值,会把它们自动装箱成对象)。
你也可以自己定义可变参数的类型
public static double max(double... values) {
double largest = Double.NEGATIVE_INFINITY;
for (double v : values) if (v > largest) largest = v;
return largest;
}
可以像下面一样调用方法
double m = max(3.1, 40.4, -5);
枚举类
我们可以定义枚举类型
public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }
实际上,这个声明定义的类型是一个类,它刚好有 4 个实例,不可能构造新的对象。因此,在比较两个枚举类型的值时,不需要调用 equals
,直接调用 ==
就好了。如果需要的话,可以为枚举类型增加构造器、方法和字段。构造器只在构造枚举常量的时候调用。
public enum Size {
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private String abbreviation;
public String getAbbreviation() { return abbreviation; }
}
枚举的构造器总是私有的,可以像前例一样省略 private
修饰符。
所有枚举类型都是 Enum
类的子类。它继承了这个类的许多方法。其中最有用的一个是 toString
,这个方法会返回枚举常量名。比如 Size.SMALL.toString()
将返回字符串 SMALL
toString
的逆方法是静态方法 valueOf
。比如
Size s = Enum.valueOf(Size.class, "SMALL");
将 s
设置成 Size.SMALL
每个枚举类型都有一个静态的 values
方法,它将返回一个包含全部枚举值的数组,比如
Size[] values = Size.values();
ordinal
方法返回 enum
声明中枚举常量的位置,位置从 0
开始计数
Enum
类有一个类型参数,不过为简单起见我们省略了这个类型参数
反射
反射库 reflection library
提供了一个丰富且精巧的工具集,可以用来编写能够动态操纵 Java 代码的程序。使用反射,Java 可以支持用户界面生成器、对象关系映射器以及很多需要动态查询类能力的开发工具
能够分析类能力的程序称为反射 reflective
。反射机制可以用来
- 在运行时分析类的能力
- 在运行时监察对象,例如,编写一个适用于所有类的
toString
方法 - 实现泛型数组操作代码
- 利用
Method
对象,这个对象很像 C++ 中的函数指针
反射是一种强大且复杂的机制,主要是开发工具的程序员对它感兴趣,一般的程序员并不需要考虑反射机制。
Class 类
在程序运行期间,Java 运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择要执行的正确的方法。
不过,可以使用一个特殊的 Java 类访问这些信息。保存这些信息的类名为 Class
。 Object
类中的 getClass()
方法将返回一个 Class
类型的实例。
Class
对象会描述一个特定类的属性。最常用的 Class
方法就是 getName
,它会返回类的名字
可以使用 forName
获得类名对应的 Class
对象
String className = "java.util.Random";
Class cl = Class.forName(className);
如果类名保存在一个字符串中,这个字符串会在运行时变化,就可以使用这个方法。入股 className
是一个类名或接口名,这个方法能够正常运行,否则会抛出一个错误,所以无论何时都应该提供一个异常处理器。
在启动时,包含
main
方法的类被加载,它会加载所有需要的类,这些被加载的类又会加载它们需要的类,以此类推,但这样加载时间很长。此时,可以先显示加载启动画面,之后调用Class.forName
手工强制加载其他类,不过,要确保包含main
方法的类没有显式地引用其他类
获得类的第三种方法是一个很方便的快捷方式。如果 T
是任意的 Java 类型(或 void
关键字), T.class
将代表匹配的类对象。
Class cl1 = Random.Class;
Class cl2 = int.class;
Class cl3 = Double[].class;
一个 Class
对象实际上表示的是一个类型,这可能是类,也可能不是类。比如 int
不是类,但是 int.class
是一个 Class
类型的对象
Class 类实际上是一个泛型类,比如,
Employee.class
的类型是Class<Employee>
,大多数情况下,可以忽略实际参数而是用原始的Class
类。
虚拟机为每个类型管理一个唯一的 Class
对象。因此,可以利用 ==
运算符实现两个类对象的比较
if (e.getClass() == Employee.class)...
如果有一个 Class
类型的对象,可以用它构造类的实例。调用 getConstructor
将得到一个 Constructor
类型的对象,然后使用 newInstance
方法来构造一个实例。如果这个类没有无参数的构造器, getConstructor
会抛出一个异常
声明异常入门
当运行发生错误时,抛出异常比终止程序要灵活得多,这是因为可以提供一个处理器 handler
捕获这个异常并进行处理。
异常有两种类型:非检查型 unchecked
异常和检查型 checked
异常。对于检查型异常,编译器将会检查你是否知道这个异常并做好准备来处理后果。不过有很多常见的异常比如越界错误或访问 null
引用,都属于非检查型异常。编译器并不期望你为这些异常提供处理器。
这里有一个最简单的策略,如果一个方法包含一条可能抛出检查型异常的语句,则在方法名上增加一个 throws
子句
public static void doSomethingWithClass(String name)
throws ReflectiveOperationException
{
Class cl = Class.forName(name);
// do something with cl
}
资源
类通常有一些关联的数据文件,比如
- 图像和声音文件
- 包含消息字符串和按钮标签的文本文件
在 Java 中,这些关联的文件被称为资源 resource
。Class 类提供了一个很有用的服务可以查找资源文件。下面给出必要的步骤
- 获得拥有资源的类的 Class 对象,例如,
ResourceTest.class
- 有些方法,如
ImageIcon
类的getImage
方法,接受描述资源位置的 URL。则要调用URL url = cl.getResource("about.gif");
- 否则,使用
getResourceAsStream
方法得到一个输入流来读取文件中的数据
这里的重点在于 Java 虚拟机知道如何查找一个类,所以它能搜索到相同位置上的相同资源。例如,假设类 ResouceTest
在一个 resources
包中。 ResourceTest.class
文件就位于 resources
目录中,可以把一个图标文件放在同一目录下
除了可以将资源文件与类文件放在同一个目录中,还可以提供一个相对或绝对路径,比如 data/about.txt
和 /core/java/title.txt
文件的自动装载是利用资源加载特性完成的。没有标准的方法来解释资源文件的内容,每个程序必须有自己的方法来解释它的资源文件。
另一个经常使用资源的地方是程序的国际化。与语言相关的字符串,如消息和用户界面标签都放在资源文件中,每种语言对应一个文件。
利用反射分析类的能力
在 java.lang.reflect
包中有三个类 Field
、 Method
和 Constructor
分别用于描述类的字段、方法和构造器。这三个类都有一个叫做 getName
的方法,用来返回字段、方法或构造器的名称。
Field
有一个 getType
方法,用来返回描述字段类型的一个对象这个对象的类型同样是 Class
。 Method
和 Constructor
类有报告参数类型的方法,它返回一个整数,用不同的 0/1
位描述所使用的修饰符,比如 public
和 static
。另外,还可以利用 java.lang.reflect
包中 Modifier
类的静态方法分析 getModifiers
返回的这个整数。比如可以使用 Modifier
类中的 isPublic
、 isPrivate
或 isFinal
判断方法或构造器是 public
、 private
还是 final
。还可以利用 Mdifier.toString
方法将修饰符打印出来。
使用反射在运行时分析对象
我们已经知道如何查看人以对象数据字段的名字和类型:
- 获得对应的
Class
对象 - 在这个
Class
对象上调用getDeclaredFields
利用反射机制可以查看在编译时还不知道的对象字段。要做到这一点,关键方法是 Field
类中的 get
方法。如果 f
是一个 Field
类型的对象(比如,通过 getDeclaredFields
得到的对象), obj
是某个包含 f
字段的类的对象, f.get(obj)
将返回一个对象,其值未 obj
的当前字段值。
不仅可以获得值,也可以设置值。但是不能访问私有字段,会抛出一个 IllegalAccessException
错误。不过,可以调用 Field
、 Method
或 Contructor
对象的 setAccessible
方法覆盖 Java
的访问控制。比如:
f.setAccessible(true);
setAccessible
方法是 AccessibleObject
类中的一个方法,它是 Field
、 Method
和 Contructor
类的公共超类。如果不允许访问, setAccessible
调用会抛出一个异常。当你使用反射访问一个模块中的非公共特性时,Java 9 和 10 只会提供一个警告。
调用任意方法和构造器
反射机制允许你调用任意方法
Method
类有一个 invoke
方法,允许你调用包装在当前的当前 Method
对象中的方法。 invoke
方法的签名是:
Object invoke(Object obj, Object... args)
第一个参数是隐式参数,其余的对象提供了显式参数。对于静态方法,第一个参数可以忽略,即可以将它设置为 null
。例如,假设用 ml
表示 Employee
类的 getName
方法,下面语句显示了如何调用
String n = (String) ml.invoke(harry);
如果返回类型是基本类型, invoke
方法会返回其包装器类型。
可以通过调用 getDeclareMethods
方法,然后搜索返回的 Method
对象数组,直到发现想要的方法为止。也可以调用 Class
类的 getMethod
方法得到想要的方法。
继承的设计技巧
-
将公共操作盒字段放在超类中
-
不要使用受保护的字段,
protected
机制并不能带来更多的保护,第一,子类集合是无限的,任何一个人都能直接由你的类派生一个子类,然后编写代码直接访问protected
实例字段,第二,在 Java 中,同一个包中的所有类欧能访问protected
字段,而不管它们是否为这个类的子类 -
使用继承实现
is-a
关系 -
除非所有的继承的方法都有意义,否则不要使用继承
-
在覆盖方法时,不要改变预期的行为。替换原则不仅应用于语法,它也适用于行为。在覆盖一个方法的时候,不应该毫无缘由地改变它的行为。
-
使用多态,而不要使用类型信息。只要看到类似下面的代码,就应该考虑使用多态
if (x is of type 1)
action1(x);
else if (x is of type 2)
action2(x);
如果 action1
和 action2
是相同的概念,就应该为这个概念定义一个方法,并将其放置在两个类型的超类或接口中,然后,就可以调用
x.action();
使用多态性固有的动态分派机制执行正确的动作。使用多态方法或接口实现的代码比使用多个类型检查的代码更易于维护和扩展。
- 不要滥用反射。反射机制对于编写系统程序时极其有用,但通常不适合编写应用程序。反射是很脆弱的,如果使用反射、编译器将无法帮助你查找编程错误。