继承的基本思想是:可以基于已有的类创建新的类。
反射是指在程序运行期间更多地了解类以及其属性的能力。
关键字extends表明正在构造的新类派生于一个已存在的类。这个已存在的类称为超类(super class)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。子类其实比超类拥有的功能更多。Java与C++定义继承的方式十分相似。Java用关键字extends代替了C++中的冒号(:)。在Java中,所有的继承都是公共继承,而没有C++中的私有继承和保护继承。
super是一个指示编译器调用超类方法的特殊关键字,它不是一个对象的引用。在Java中使用关键字super调用超类的方法,而在C++中则采用超类名加::操作符的形式。可以在子类中增加字段、增加方法或覆盖超类的方法,不过,继续绝不会删除任何字段或方法。
使用super调用构造器的语句必须是子类构造器的第一条语句。如果子类构造器没有显式地调用超类的构造器,将自动调用超类的无参构造器。如果超类没有无参构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,Java编译器就会报告一个错误。
与this一样,super关键字也有两个含义:一是调用超类的方法,二是调用超类的构造器。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造器参数可以传递给当前类(this)的另一个构造器,也可以传递给超类(super)的构造器。
一个对象可以指示多种实际类型的现象称为多态(polymorphism)。在运行时能够自动地选择适当的方法,称为动态绑定(dynamic binding)。在C++中,如果希望实现动态绑定,需要将成员函数声明为virtual。在Java中,动态绑定是默认的行为。如果不希望让一个方法是虚拟的,可以将它标记为final。
继承并仅限于一个层次。由一个公共超类派生出来的所有类的集合称为继承层次(inheritance hierarchy)。在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链(inheritance chain)。在C++中,一个类可以有多个超类。Java不支持多重继续。
有一个简单规则可以用来判断是否应该将数据设计为继承关系,这就是”is-a”规则,它指出子类的每个对象也是超类的对象。“is-a”规则的另一种表述是原则替换。它指出程序中出现超类对象的任何地方都可以使用子类对象替换。。在Java中,对象变量是多态的。不过,不能将超类的引用赋给子类变量。在Java中,子类引用的数据可以转换成超类引用的数组,而不需要使用强制类型转换。
如果在子类中定义了一个与超类签名相同的方法,那么子类中的这个方法就会覆盖超类中这个相同前面的方法。允许子类将覆盖方法的返回类型改为原返回类型的子类型。
如果是private方法,static方法,final方法或构造器,那么编译器将可以准确地知道应该调用哪个方法。这称为静态绑定(static bindign)。与此对应的是,如果要调用的方法依赖于隐式参数的实际类型,那么必须在运行时使用动态绑定。动态绑定有一个非常重要的特性:无需对现有的代码进行修改就可以对程序进行扩展。
Warning:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。
不允许扩展的类被称为final类。类中的某个特定的方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法。注意,不包括字段)。如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程称为内联。
对象引用的强制类型转换语法与数值表达式的强制类型转换类似,仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。进行强制类型转换的唯一原因是:要在暂时忽视对象的实际类型之后使用对象的全部功能。总结:1)只能在继承层次内进行强制类型转换 2)在将超类转换成子类之前,应该使用instanceof进行检查。注意:如果对一个null的对象进行instanceof检查,不会产生异常,只是返回false。
使用abstract关键字修饰的方法称为抽象方法。包含一个或多个抽象方法的类本身必须被声明为抽象的,除了抽象方法,抽象类还可以包含字段和具体方法。即使不含抽象方法,也可以将类声明为抽象类。抽象类不能实例化。需要注意,可以定义一个抽象类的变量,但是这样的变量只能引用非抽象子类的对象。在C++中,有一种抽象方法称为纯虚函数(pure virtual function),要在末尾用=0标记。如果至少有一个纯虚函数,这个C++类就是抽象类。在C++中,没有提供用于表示抽象类的特殊关键字。
在Java中,保护字段(由protected修饰符修饰的字段)只能由同一个包中的类访问。事实上,Java中的受保护部分对所有子类及同一个包中的所有其他类都可见。Java中的4个访问控制修饰符小结:
1)仅对本类可见——private
2)对外部可见——public
3)对本包和所有子类可见——protected
4)对本包可见——默认,不需要修饰符。
Object类是Java中的所有类的始祖,在Java中每个类都扩展Object。如果没有明确地指出超类,Object就被认为是这个类的超类。可以使用Object类型的变量引用任何类型的对象。在Java中,只有基本类型不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
Object类中实现的equals方法将确定两个对象引用是否相等。在子类中定义equals方法时,首先调用超类的equals方法。如果检测失败,对象就不可能相等。如果超类中的字段都相等,就需要比较子类中的实例字段。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.equals(z)也应该返回true。
4)一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
5)对于任意非空引用x,x.equals(null)应该返回false。
equals相关API如下:
散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。如果是两个不同的对象,散列码基本上不会相同。由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值由对象的存储地址得出。注意,字符串的散列码是由内容导出的。hashCode方法应该返回一个整数(也可以是负数)。equals与hashCode的定义必须相容:即如果两个对象相等,那么它们的散列码也应该返回相同的值。如果存在数组类型的字段,那么可以使用静态的Arrays.hashCode方法计算一个散列码,这个散列码由数组元素的散列码组成。
hashCode方法的相关API如下:
toString方法,它会返回表示对象值的一个字符串。绝大多数(但不是全部)的toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的字段值。随处可见toString方法的主要原因是:只要对象与一个字符串通过操作符“+”连接起来,Java编译器就会自动地调用toString方法来获得这个对象的字符串描述。打印数组可以使用Arrays.toString。要想打印多维数组,则需要调用Arrays.deepToString方法。
Object类部分API如下:
ArrayList类类似于数组,但在添加和删除元素时,它能够自动地调整数组容量,而不需要为此编写任何代码。ArrayList是一个有类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象的类型,需要用一对尖括号将类名括起来追加到ArrayList后面。在Java10中,最后使用var关键字以避免重复写类名。Java 5 以前的版本没有提供泛型类,而是有一个保存Object类型的元素的ArrayList类,它是一个”自适应大小”的集合。仍然可以使用没有后缀<……>的ArrayList,这将被认为是删去了类型参数的一个”原始”类型。
可以使用add方法将元素添加到数组列表中。数组列表管理着一个内部的对象引用数组。数组列表的容量与数组的大小有一个非常重要的区别。如果分配一个有100个元素的数组,数组就有100个空位置(槽)可以使用。而容量为100个元素的数组列表只是可能保存100个元素(实际上也可以超过100,不过要以重新分配空间为代价),但是在最初,甚至完成初始化构造之后,数组列表不包含任何元素。size方法将返回数组列表中包含的实际元素个数。一旦数组列表的大小被确定,不再改变时可以调用trimToSize方法将存储块的大小调整为保存当前元素数量所需要的存储空间。垃圾回收器将回收多余的存储空间。
ArrayList相关API如下:
将一个原始ArrayList赋值给一个类型化ArrayList会得到一个警告。
所有的基本类型都有一个与之对应的类。通常,这些类称为包装器(wrapper)。这些包装器类有显而易见的名字:Integer,Long,Float,Double,Short,Byte,Character和Boolean(前6个类派生于公共的超类Number)。包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,包装器类还是final,因此不能派生它们的子类。将一个int类型的元素添加到Integer对象的过程称为自动装箱,相反的,当将一个Integer对象赋给一个int值时,将会自动拆箱。自动装箱和自动拆箱甚至也适用于算术表达式。基本类型与它们对应的对象包装器有一个很大不同:同一性。自动装箱的规范要求boolean,byte,char<=127,介于-128和127之间的short和int被包装到固定的对象中。包装器类引用可以是null,但会抛出一个NullPointerException异常。最后要注意,装箱和拆箱是编译器要做的工作,而不是虚拟机。编译器在生产类的字节码时会插入必要的方法调用。虚拟机只是执行这些字节码。
Integer相关API如下:
可以提供参数数量可变的方法(有时这些方法被称为“变参”(varags)方法),如:public PrintStream printf(String fmt ,Object …… args){return format(fmt,args);} 这里的省略号……是Java代码的一部分,它表明这个方法可以接受任意数量的对象(除了fmt参数之外)。允许将数组作为最后一个参数传递给有可变参数的方法。
在比较两个枚举类型值时,并不需要调用equals,可以直接使用”==”即可。枚举的构造器总是私有的。所有的枚举类型都是Enum类的子类。每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组。ordinal方法返回enum声明中枚举常量的位置,位置从0开始计数。
与枚举类型相关的API 如下:
反射库(reflection library)提供了一个丰富且精巧的工具集,可以用来编写能够动态操纵Java代码的程序。能够分析类能力的程序称为反射(reflective)。反射可以用来:
1)在运行时分析类的能力。
2)在运行时检查对象,例如,编写一个适用于所有类的toString方法。
3)实现范型数组操作代码。
4)利用Method对象,这个对象很像C++中的函数指针。
程序运行期间,Java运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。保存这些信息的类名为Class。Object类中的getClass方法将会返回一个Class类型的实例。Class对象会描述一个特定类的属性。可能最常用的Class方法就是getName,该方法返回类的名字。需要注意,一个Class对象实际上表示的是一个类型,这可能是类,也可能不是类。Class类实际上是一个泛型类。虚拟机为每个类型管理一个唯一的Class对象,因此,可以利用==运算符实现两个对象的比较。
与反射相关的部分API 如下:
异常有两种类型:非检查型异常和检查型异常。对于检查型异常,编译器将会检测程序员是否知道这个异常并做好准备来处理后果。如果一个方法包含一条可能抛出检查型异常的语句,则在方法名上增加一个throws子句。调用这个方法的任何方法也都需要一个throws声明,包括main方法。
在Java中,类通常有一些关联的数据文件,如:图像和声音文件;包含消息字符串和按钮标签的文本文件。这些文件被称为资源(resource)。与之相关的API 如下:
利用反射分析类的能力,检查类的结构。与之相关的API如下:
使用反射在运行时分析对象,与之相关的API如下:
为反射编写泛型数组代码,与之相关的API如下:
调用任意方法和构造器,与之相关的API如下:
继承的设计技巧:
1)将公共的操作和字段放在超类中。
2)不要使用受保护的字段。
3)使用继承实现“is-a”关系。
4)除非所有继承的方法都有意义,否则不要使用继承。
5)在覆盖方法时,不要改变预期的行为。
6)使用多态,而不用使用类型信息。
7)不要滥用反射。