5.1 类、超类和子类
每个经理Manager都是一名雇员Employee,is-a关系是继承的一个明显特征。
关键字extends表示继承。extends表明正在构造的新类(子类、派生类)派生于一个已存在的类(超类、基类或父类)。
子类比超类拥有更丰富的功能,而且自动地继承了超类的方法和域。
在设计类的时候,需要将通用的方法放在超类中,将具有特殊用途的方法放在子类中。子类可以提供新的方法来覆盖override超类中的方法。通过super关键字,调用超类Employee中的getSalary()方法,而不是子类Manager中的getSalary。
super与this不一样,super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特有关键字。
可以通过super()调用超类的构造器。使用super调用构造器的语句必须是子类构造器的第一条语句。
如果子类的构造器没有显式地调用超类的构造器,则将自动调用超类的默认构造器;如果超类没有默认构造器,且子类的构造器中又没有显式调用超类的其他构造器,则Java编译器将报错。
一个对象变量可以引用多种实际类型的现象被成为多态(polymorphism),在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。
5.1.1 继承层次
继承并不仅限于一个层次。如可以由Manager类派生Executive类。由一个公共超类派生出来的所有类的集合被称为继承层次。在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链。
Java不支持多继承。
5.1.2 多态
子类的每个对象也是超类的对象,程序中出现超类对象的任何地方都可以用子类对象置换。例如将一个子类的对象赋给超类变量。Java中,对象变量是多态的。一个Employee变量既可以引用一个Employee对象,也可以引用一个Employee的子类的对象。反之,不能将一个超类的引用赋给子类变量。
5.1.3 动态绑定
对象方法的调用过程:
1. 编译器查看对象的声明类型和方法名,假设调用x.f(param),编译器会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法。至此,编译器获得所有可能被调用的候选方法。
2. 编译器查看调用方法时提供的参数类型。如果在所有名为否的方法中存在一个与参数类型完全匹配,就选择这个方法。这个过程称为重载解析(overloading resolution)。允许类型转换。至此,编译器获得需要调用的方法名字和参数类型。
3. 如果是private、static、final方法或者构造器,编译器将准确知道应该调用哪个方法,是为静态绑定(static binding)。对应的,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
4. 当程序运行,并且采用动态绑定调用方法时,虚拟机调用与x所引用对象的实际类型最合适的那个类的方法。首先在实际类型中找,否则就去超类中寻找。
动态绑定的特性之一:无需对现存的代码进行修改,就可以对程序进行扩展。
在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。如果超类方法是public,子类方法一定要声明为public。
5.1.4 阻止继承:final类和方法
确保它们不会在子类中改变语义。
不允许扩展的类被称为final类。
final方法不能被子类覆盖。final类中的所有方法自动成为final方法。
5.1.5 强制类型转换
只能在继承层次内进行类型转换。在将超类转化成子类之前,应该使用instanceof进行检查。
5.1.6 抽象类
抽象类可以包含抽象方法、也可以不含抽象方法,抽象类还可以包含具体数据和具体方法。
包含一个或多个抽象方法的类本身必须被声明为抽象的。
抽象类不能被实例化,不能创建这个类的对象。可以定义抽象类的对象变量,但是它子女引用非抽象子类的对象。
抽象方法充当着占位的角色,它们的具体实现在子类中。扩展抽象类有两种选择:1. 在子类中仍是抽象方法,此时子类也必须为抽象类;2. 定义全部的抽象方法,子类不再是抽象类。
5.1.7 受保护访问
protected,允许超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。
1. 仅对本类可见——private
2. 对所有类可见——public
3. 对本包和所有子类可见——protected
4. 对本包可见——默认。
5.2 Object:所有类的超类
5.2.1 Equals方法
Object类中的equals方法用于检测一个对象是否等于另一个对象,判断两个对象是否具有相同的引用。
在子类中定义equals方法时,首先调用超类的equals,如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。
5.2.2 相等测试与继承
Java语言规范要求equals方法具有如下特性:
1. 自反性:对于任何非空引用x,x.equals(x)应该返回true
2. 对称性:对于任何引用x和y,当且仅当y.equals(x)=true,x.equals(y)也=true
3. 传递性:对于任何引用x、y和z,如果x.equals(y)=true,y.equals(z)=true,则x.equal(z)也=true
4. 一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
5. 对于任意非空引用x,x.equals(y)应该返回false
完美equals方法的建议:
1. 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。
2. 检测this与otherObject是否引用同一个对象:if(this==otherObject)return true;
3. 检测otherObject是否为null,如果为null,返回false
4. 比较this与otherObject是否属于同一个类
5. 将otherObject转换为相应的类型变量
6. 对所有需要比较的域进行比较,使用==比较基本类型域,使用equals比较对象域,所有域都匹配,返回true。
5.2.3 HashCode方法
散列码(hash code)是由对象导出的一个整型值,散列码是没有规律的。
hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,值为对象的存储地址。
如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。
5.2.4 ToString方法
用于返回表示对象值的字符串,大多数遵循的格式:类名,随后是域值。
只要对象与一个字符串通过操作符“+”连接起来,编译器就会自动调用toString方法,以便获得这个对象的字符串描述。
Object类定义了toString方法,用来输出对象所属的类名和散列码。
5.3 泛型数组列表
一旦确定了数组的大小,改变它就不太容易了。可以使用ArrayList——一个采用类型参数的泛型类。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,如ArrayList<Employee>。
ArrayList内部使用一个数组进行管理,如果调用add且内部数组已经满了,ArrayList就自动创建一个更大的数组,并拷贝所有对象。size方法返回包含的实际元素数目。如果确认ArrayList的大小不再变化,就可以调用trimToSize方法将存储区域的大小调整为当前元素数量所需要的存储空间,多余的将被垃圾回收。
5.3.1 访问ArrayList元素
get和set方法访问和改变元素。使用add添加元素。使用泛型可以进行类型检测。
使用带索引参数的add方法,add(n, e);位于n之后的所有元素都要向后移动一个位置。
remove(e);位于这个元素之后的所有元素都向前移动一个位置,并且数组大小减1。
5.3.2 类型化与原始数组列表的兼容性
5.4 对象包装器与自动打包
包装器类型:Byte,Character,Short,Integer,Long,Float,Double,Boolean。对象包装器类是final,没有子类。
定义一个整型数组列表,尖括号中的类型参数不允许是基本类型,即不允许ArrayList<int>,只能写成ArrayList<Integer>。由于每个值分别包装在对象中,效率远远低于int[]数组。
list.add(3)将自动变换成list.add(new Integer(3));,这个变换称为自动打包。
当将一个Integer对象赋给一个int值时,将会自动拆包,编译器将int n=list.get(i);翻译成int n=list.get(i).intValue();
5.5 参数数量可变的方法
method(Object...) ...是Java代码的一部分,表明这个方法可以接收任意数量的对象。Object...参数与Object[]完全一样。
5.6 枚举类
enum Size{ SMALL, MEDIUM, LARGE, EXTRA_LARGE};这个声明定义的类型是一个类,刚好有4个实例。
可以在枚举类型中添加一些构造器、方法和域。所有的枚举类型都是Enum类的子类,继承了许多方法。toString()返回枚举常量名,如Size.SMALL.toString()返回字符串“SMALL”。toString的逆方法是valueOf,Size s=Enum.valueOf(Size.class, "SMALL");将s设置成Size.SMALL。
values方法返回包含全部枚举值的数组。ordinal方法返回enumeration声明中枚举常量的位置,从0开始。
5.7 反射
可以运用反射:1. 在运行中分析类的能力。2. 在运行中查看对象。3. 实现数组的操作代码。4. 利用Method对象。
使用反射的主要对象是工具构造者,而不是应用程序猿。
5.7.1 Class类
在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息保存着每个对象所属的类足迹。虚拟机利用运行时信息选择相应的方法执行。保存这些信息的类被称为Class,一个Class对象表示一个特定类的属性。
Object类中的getClass()方法返回一个Class类型的实例。
Class的getName方法返回类的名字,如果类在一个包里,类名包括包名。
静态方法forName返回类名对应的Class对象。
如果T是任意的java类型,T.class代表匹配的类对象。一个Class对象表示的是一个类型,而这个类型未必一定是一种类。例如,int不是类,但int.class是一个Class类型的对象。
newInstance方法可以快速创建一个类的实例,e.getClass().newInstance()创建一个与额具有相同类型的实例。newInstance方法调用默认构造器初始化新创建的对象。
5.7.2 捕获异常
当程序运行过程中发生错误时,就会抛出异常,抛出异常比终止程序灵活的多,可以提供一个捕获异常的处理器对异常进行处理,没有异常处理器,程序就会终止。
将可能抛出已检查异常的方法调用代码放在try块中,然后在catch子句中提供处理器代码。如果try块中没有抛出任何异常,则跳过catch子句的处理器代码。
5.7.3 利用反射分析类的能力
在java.lang.reflect包中有三个类:Field,Method,Constructor,分别描述类的域、方法和构造器。都有一个getName方法,用来返回项目的名称。getModifiers方法返回一个整型值,用不同的位开关描述public、static这样的修饰符使用情况。使用Modifier类的静态方法分析getModifiers返回的整型数值。
Field类的getType方法,发货描述域所属类型的Class对象。
Method和Constructor类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。
Class类的getFields、getMethods和getConstructors方法将分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。Class类的getDeclareFields、getDeclareMethods、getDeclaredConstructors方法将分别返回类中声明的全部域、方法和构造器,包括私有和受保护成员,但不包括超类的成员。
5.7.4 在运行时使用反射分析对象
查看任意对象的数据域名称和类型:1.获得对应的Class对象。2. 通过Class对象调用getDeclaredFields。
查看对象域的关键方法是Field类中的get方法。假设field是Field类型的对象,obj是包含f域的类的对象,field.get(obj)返回一个对象,其值为obj域的当前值。field.set(obj, value)可以将obj对象的field域设置成新值。
反射机制的默认行为受限于Java的访问控制,可以调用Field、Method或Constructor对象的setAccessible方法。
5.7.5 使用反射编写泛型数组代码
5.7.6 方法指针
5.8 继承设计的技巧
1. 将公共操作和域放在超类。
2. 不要使用受保护的域:protected机制不能带来更好的保护,原因1:子类集合是无限制的,任何人都能够由某个类派生一个子类,并编写代码直接访问protected域,从而破坏封装性,2:同一个包中的所有类都可以访问protected域,不管它是否是这个类的子类。
3. 使用继承实现is-a关系。
4. 除非所有继承的方法都有意义,否则不要使用继承。
5. 在覆盖方法时,不要改变预期的行为。
6. 使用多态,而非类型信息:使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更易于维护和扩展。
7. 不要过多地使用反射:编译器很难帮助人们发现程序中的错误,任何错误只能在运行时被发现,并导致异常。