示例中定义了一个基类(父类、超类)Employee
,然后class Manager extends Employee
:
1 | class Employee { |
类、超类和子类
super关键字
这里我们希望调用超类中的getSalary()方法,而不是自调用,需要使用特定的关键字super
:
1 | //使用超类中带有n,s,year,month,day参数的构造器 |
注意:super
不是对象的引用,和this
不同,它只是一个指示编译器调用超类方法的特殊关键字
- 不能删除继承的任何域和方法
- 如果父类没有不带参数的构造器,子类又没有显示调用其他超类带构造器,系统会报错。
多态
一个对象变量可以指示多种实际类型的现象被称为多态(Polymorphism).在运行时能够自动地选择调用哪个方法的现象称为动态绑定。
一个判断是否应该设计为继承关系的规则:”is-a”规则。
- 动态绑定和静态绑定
- 动态绑定是指在类的继承关系中,编译器在调用函数时可以根据声明类型、方法名、参数去动态调用与之完全匹配的方法。优先调用当前声明的子类的方法
一个类中,方法名字和参数列表称为方法的签名,而返回类型不是方法的签名。子类中可以覆盖超类中相同签名的方法,因此在覆盖方法是们一定要保证返回类型的兼容性。1
2
3
4//假设在超类Employee中有:
public Employee getBuddy() {...}
//子类Manager要覆盖这个方法:
public Manager getBuddy() {...}//It's ok to change the return type - 如果是private、static、final方法或着构造器,那么编译器可以准确地知道调用什么方法,这种调用方式称为静态绑定。
注意:再覆盖方法时,子类方法不能低于超类方法的可见性(public>private)
final关键字:阻止继承
在定义类的时候使用final修饰符阻止人们定义该类的子类:
1 | final class Executive extends Manager {...} |
类中的特定方法也可以被声明为final
,这样做子类就不能再覆盖这个方法:
1 | class Employee { |
域也可以被声明为final
。对于fianl域
,构造对象之后就不允许改变它们的值了。不过如果将一个类声明为final,只有其中的方法自动地成为final
,而不包括域。
强制类型转换
将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋值给一个子类变量,必须进行类型转换。为避免ClassCastException
异常,应该养成一个良好的程序设计习惯:在进行类型转换之前,先查一下能否成功地转换,使用instanceof
运算符:
1 | if (staff[1] instanceof Manager) { |
- 只能在继承层次上进行类型转换
- 在将超类转换成子类之前,应该使用
instanceof
进行检查
在一般情况下,应该尽量少用类型转换和instanceof
运算符
抽象类
使用abstract
关键字的方法,不需要实现:
1 | public abstract String getDescription(); |
包含一个或多个抽象方法的类本身必须被声明为抽象的:
1 | abstract class Person { |
抽象类的子类如果没有实现所有的抽象方法,则必须将子类也标为抽象类,因为这种情况下子类中还是有抽象方法。
抽象类不能被实例化,但可以创建一个具体子类的对象。
1 | new Person("Vincent");//error |
Object:所有类的超类
Object类是Java中所有类的超类,即Java中每个类都是由它拓展而来。只有基本类型(primitive types)不是对象,例如数值、字符、布尔类型。所有的数组类,不管是对象数组还是基本类型的数组都拓展自Object类。
Object类有一些共同的方法:
1. equals方法
Object类中的equals方法用于检测两个对象是否相等。Object类中的这个方法返回两个对象是否具有相同的引用,在很多情况下这是没有意义的。所以对于自定义类我们会重写equals
方法。
相等测试与继承
Java语言规范要求equals方法具有:自反性,对称性,传递性,一致性,对于非空x有x.equals(null) = false.
书中用了getClass()方法来检测,许多程序元也喜欢用instanceof
进行检测,关于instanceof的效果参考langya2007。
但是用instanceof
检测违背了对称性。例如 e.equals(m)
返回true,而m.equals(e)
返回false(这里m(manager)是e(employee)的子类)。
- 如果子类能够拥有自己相等的概念,,则对称性需求将强制采用getClass进行检测
- 如果由超类决定相等的概念,那么就可以用
instanceof
进行检测,这样可以在不同的子类对象之间进行相等比较。
2. hashCode方法
散列码是由对象导出的一个整型值,散列码是没有规律的,不同对象的散列码基本不同。hashCode方法定义在Object类中,每个对象都有一个默认的散列码,其值为对象的存储地址。
字符串String的散列码是由内容导出的,that is内容相同的字符串散列码相同。但是,StringBuffer类没有定义hashCode方法,它的散列码是由Object类的默认hashCode方法到处的对象存储地址。
1 | //String使用下列方法计算散列码: |
如果重新定义equals方法,就必须重新定义hashCode方法,以便用户将对象插入到散列表中
1 | //使用null安全的方法Objects.hashCode()方法获取散列码 |
Equals和hashCode定义必须一直,如果x.equals(y)返回true,那么两个对象的散列码必须相同,例如如果定义Employee.equals比较雇员ID,那么hashCode方法就需要散列ID,而不是雇员的姓名或存储地址。
另外,数组类型可以使用Arrays.hashCode()方法计算一个散列码。
3. toString方法
返回对象值的字符串,且绝大多数toString方法都是返回:类型名[域值]
只要对象与一个字符串通过操作符+
连接起来,Java编译器就会自动地调用 toString()
Object类定义了toString方法,打印输出对象所属的类名和散列码:
1 | System.out.println(System.out); |
! 数组继承了object类的toString方法,所以在打印数组时我们不用.toString()
,而是使用静态方法Arrays.toString(...)
或者Arrays.deepToString(...)
(多维数组)。
泛型数组列表
Example: ArrayList<>
对象包装器与自动装箱
Integer
,Long
,Float
,Double
,Short
,Byte
,Character
,Void
,Boolean
(前6个类派生自公共的超类Number
)。对象包装器类是不可变的,一旦够着了包装器就不允许更改包装在其中的值,且包装器类都是final,不能定义子类。
自动装箱拆箱是非常普遍的~
如何编写一个修改数值参数值的方法:需要使用在org.omg.CORBA中定义的holder
类型。
1 | public static void triple(IntHolder x) { |
参数数量可变的方法
我们看printf
的方法定义:
1 | public class PrintStream { |
...
表明这个方法可以接受任意数量的对象(除fmt参数外)
枚举类
ClassName.java
1 | public enum ClassName { |
EnumTest.java
1 | public class EnumTest { |
在比较两个枚举类型的值时,不需要用equals,而直接使用”==“。所有枚举类型都是Enum类的子类,他们继承了Enum的许多方法,例如toString()
, Enum.valueOf()
。
1 | ClassName.STRING.toString();//返回字符串"STRING" |
反射
反射库提供了一个非常丰富且精心设计的工具集,以便编写能够移动操作Java代码的程序。
能够分析类能力的程序称为反射(reflective),反射机制功能:
- 在运行中分析类的能力
- 在运行中查看对象,例如编写一个toString方法供所有类使用
- 实现通用的数组操作代码
- 利用Method对象,这个对象很像C++中的函数指针
Class类
在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。
通过专门的Java类Class
可以访问这些信息
Object类中的
getClass()
方法返回一个Class类型的实例1
2
3Employee e;
...
Class c1 = e.getClass();一个Class对象表示一个特定类的属性。
最常用的Class方法是getName()
1
2
3
4
5
6
7System.out.println(e.getClass().getName);
//输出: Employee
//如果类在一个包里,包的名字也作为类名的一部分
Date d = new Date();
Class c2 = d.getClass();
String name = c2.getName();//name is set to "java.util.Date"调用静态方法
forName()
获得类名对应的Class1
2String className = "java.util.Date";
Class c1 = Class.forName(className);//注意检查异常获取Class对象最简单的方法就是在类名后面加
.class
getName()
方法应用在数组类型时会返回比较奇怪的名字,历史原因
虚拟机为每个类型管理一个Class对象。因此可以利用”==”比较两个类对象:
1 | if (e.getClass() == Employee.class) { |
- 快速创建一个类的实例:
1
Object ee = e.getClass().newInstance();
捕获异常
在可能抛出已检查异常的一个或多个方法调用代码放在try块中,然后再catch子句中提供处理器代码。
继承设计的技巧
- 将公共操作和域放在超类
- 不要使用受保护的域
protected机制并不能带来很好的保护,一个是因为子类集合是无限的,二是一个包中的所有类都可以访问protected域。 - 使用继承实现”is-a”关系
- 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期行为
- 使用多态,而非类型信息。
- 不要过多的使用反射