继承
类、超类和子类
- “is-a”关系是继承的一个明显特征
5.1.1 定义子类
- 使用关键字extends表示继承,所有的继承都是公共继承。
- 已存在的类成为超类、基类或父类,新类称为子类、派生类或孩子类
- 子类将会继承超类中的方法和字段,应将最一般的方法放在子类中,特殊的方法放在子类中
5.1.2 覆盖方法
- 在子类中,使用super语句调用超类中的方法。
- super与this不同,this是对象的引用,而super只是一个指示编译器调用超类方法的特殊关键字
5.1.3 子类构造器
- 由于子类的构造器不能访问超类的私有字段,所以必须使用super关键字来访问超类的构造器。该语句必须是子类构造器的第一条语句
- 若子类的构造器没有显式的调用超类的构造器,则自动调用超类的无参数构造器,当超类没有无参数构造器时无法编译成功
- 一个对象变量可以指示多种实际类型的现象称为多态,运行时自动选择适当的方法称为动态绑定
5.1.4 继承层次
- 由一个公共超类派生出来的所有类的集合称为继承层次
- 继承层次中从某个特定的类到其祖先的路径称为继承链
5.1.5 多态
-
替换原则:程序中出现超类对象的任何地方都可以使用子类对象替换
-
一个超类类型的变量可以引用超类或任意一个子类的对象,但子类变量不能引用超类的对象
5.1.6 理解方法调用
- 方法调用的过程
- 编译器查看对象的声明类型和方法名:一一列举该类和该类超类中所有同名且可访问的方法
- 确定方法调用中提供的参数类型,若在所有同名方法中存在一个与所提供的参数类型完全匹配的方法则选择它(重载解析)。
- 重载方法时允许子类将返回类型改为原返回类型的子类型
- 当方法为private、static、final时,编译器无需考虑是否为超类的方法(以上的三个修饰符修饰的方法均为类方法),称为静态绑定
- 当方法不为以上的三种时,则采用动态绑定,虚拟机将会调用与引用对象的实际类型对应的那个方法
- 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性
5.1.7 阻止继承:final类和方法
- 声明为final的方法无法被重载
- 将类声明为final则类的所有方法都为final
5.1.8 强制类型转换
-
可以将子类的引用赋给一个超类变量(上转型),但是不能将一个超类的引用赋给一个子类变量
staff[0] = boss;// 上转型 Manager1 manager1 = (Manager1) new Employee1("Harry Hacker", 50000, 1989, 10, 1);// ClassCastException
- 只能在继承层次内进行强制类型转换
- 在将超类强制转化成子类之前,应该使用instanceof进行检查
5.1.9 抽象类
- 包含一个或多个抽象方法的类必须被声明为抽象的
- 抽象方法:占位方法,在子类中具体实现
- 抽象类不能被实例化(可以定义抽象类的对象变量,但此变量只能引用非抽象子类的对象)
- 抽象类也可以包含字段和具体方法
5.1.10 受保护访问
- protected:限制超类中的某个方法或某个字段只能被子类或子类的方法访问。
- 保护字段只能由同一个包中的类访问
- 访问控制修饰符
- 对本类可见:private
- 对外部可见:public
- 对本包和所有子类可见:protected
- 对本包可见:默认
Object:所有类的超类
Object类是所有类的始祖,当没有明确指出超类时,Object被默认认为是该类的超类
5.2.1 Object类型的变量
- 可以使用Object类型的变量引用任何类型的对象
- 只有基本类型不是对象(数组类型也是对象)
5.2.2 equals方法 5.2.3 相等测试与继承
- 实现一个equals方法
- 显式参数命名为otherObject
- 首先调用超类的equals方法
- 如果this与otherObject相等则返回true(==)
- 如果otherObject为null则返回false
- 比较this与otherObject的类
- 若相等性由子类定义(equals的语义可以在子类中改变),使用getClass检测
- 若相等性由超类定义(所有子类的equals语义一致),使用instanceof检测
- otherObject强转为相应类类型变量
- 根据相等性概念比较字段。检查传入类和调用equals方法的类中的实例字段是否相同(为了避免某个字段为null,使用Object.equals())。如果两个参数都为null,Objects.equals(a, b)调用将返回true;如果其中一个参数为null,则返回false;否则,如果两个参数都不为null,则调用a.equals(b)。
5.2.4 hashcode方法
- 如果重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义hashcode方法
- 返回一个整数
5.2.5 toString方法
- 返回表示对象值的一个字符串
泛型数组列表
5.3.1 声明数组列表
- 使用var关键字以避免重复写类名(Java10)
- 使用var时不要省略右侧的类型参数
- 当提前知道数组列表可能存储的元素数量,调用ensureCapacity方法,这样可以减少add重新分配空间的开销;也可以在构造数组时就将初始容量作为参数
- 当数组列表的大小不在不再发生变化后,使用trimToSize方法将存储块的大小调整为保存当前元素数量所需要的存储空间
5.3.2 访问数组列表元素
- 使用get()和set()访问或改变数组元素
- 使用add()方法添加元素:第一个参数是要添加的元素,第二个参数是要插入的位置;使用remove方法删除一个元素
- 插入删除元素的效率很低
对象包装器与自动装箱
- 基本类型对应的对象成为包装器
- 无法更改包装在其中的值,也无法派生它们的子类
- 定义数组列表时可以使用包装器
- 由于每个值分别包装在对象中,包装器数组列表的效率远低于直接使用基本类型的数组
参数数量可变的方法
- 可以提供参数数量可变的方法(变参方法)
- 在声明形参时使用…表明这个方法可以接收任意数量的对象,相当于将这些对象放在一个对象数组中,保存所有该类型的对象。
- 数组也可以作为最后一个参数传递给有可变参数的方法
枚举类
- 枚举类型是一个类,且可以拥有自己的构造器、方法、和字段
- 所有的枚举类型都是java.lang.Enum的子类,而不是Object类的子类
- 枚举类内部定义的枚举值就是该类的实例且必须在第一行定义,当类初始化时这些枚举值会被实例化
- 枚举类的构造方法用private修饰,所以无法被new实例化
- 枚举值后面有参数时,必须有参数个数且类型相同的构造器与之对应
- 枚举类重写了toString方法,会返回声明中包含的此枚举常量的名称
反射
5.7.1 Class类
- 保存对象的运行时类型标识信息的类名为Class
- 获得Class类型对象的三种方法
- 使用Object类中的getClass()方法
- 使用静态方法forName获得类名对应的Class对象
- 如果类在一个包里,包的名字也作为类名的一部分
- 如果className是一个类名或接口名,这个方法可以正常执行,否则将抛出一个检查型异常
- 使用T.class(T是任意的Java类型(包括基础类型)或void关键字)
- Class类是一个泛型类。T.class的类型是Class<T>
- 每个类都有一个唯一的Class对象,因此可以使用==运算符实现两个类对象的比较
- 对Class类型的对象调用getConstructor方法将得到一个Constructor类型的对象,然后使用newInstance方法构造一个实例
5.7.2 声明异常入门
- 检查型异常:可以提供处理器的异常
- 非检查型异常:直接终止程序
5.7.3 资源
-
URL aboutURL = cl.getResource("about.gif");//找到与类位于同一位置的资源,返回一个可以用来加载资源的URL var icon = new ImageIcon(aboutURL); InputStream stream = cl.getResourceAsStream("data/about.txt");//找到与类位于同一位置的资源,返回一个可以用来加载资源的输入流 var about = new String(stream.readAllBytes(), "UTF-8");
5.7.4 利用反射分析类的能力
-
Field、Method、Constructor分别描述类的字段、方法和构造器
Class cl = Class.forName(name); cl.getModifiers();//修饰符(整数) Modifier.toString(cl.getModifiers());//修饰符(字符串) cl.getDeclaredMethods();//方法(数组) 类中声明的全部字段、方法、构造器,包括私有成员、包成员和受保护成员,但不包括超类成员 cl.getDeclaredMethods()[0].getName();//方法名字 cl.getDeclaredMethods()[0].getParameterTypes();//方法参数类型(数组) cl.getDeclaredMethods()[0].getReturnType()//方法返回值类型 cl.getDeclaredConstructors();//构造器(数组) cl.getDeclaredConstructors()[0].getName();//构造器名字 cl.getDeclaredConstructors()[0].getParameterTypes();//构造器参数类型(数组) cl.getDeclaredFields();//字段(数组) cl.getDeclaredFields()[0].getName();//字段名字 cl.getDeclaredFields()[0].getType();//字段类型 cl.getFields();//类支持的公共字段、方法、构造器 cl.getConstructors(); cl.getMethods(); cl.getDeclaringClass()//定义了这个构造器、方法或字段的类,对表示类的Class对象调用这个方法会返回null
5.7.5 使用反射在运行时分析对象
-
上一节中得知如何查看对象字段的名字和类型
- 获得对应的Class对象
- 调用getDeclaredFields方法
-
如何得到或设置字段的具体内容
-
var harry = new Employee( "Harry Hacker",50000,10,1,1989); class cl = harry.getclass();// the class object representing Employee Field f = cl.getDeclaredField("name");//the name field of the Employee class Object v = f.get(harry);//the value of the name field of the harry object,i.e.,the String object "Harry Hacker" System.out.println(v);//"Harry Hacker"
-
使用f.set(obj,value)设置字段f
5.7.6 使用反射编写泛型数组代码
- Class.getComponentType():返回数组的正确类型
5.7.7 调用任意方法和构造器
-
使用类似得到字段的方法来调用方法或构造器
Method f = MethodTableTest.class.getMethod("square", double.class);//除了方法名之外还要提供方法的参数类型 double y = (Double)f.invoke(null,x);//第一个参数是隐式参数(包含想要调用的方法的类,若为静态方法则是null),其余的参数是显示参数 //返回类型是基本类型时invoke会将其装箱 //使用类似的方法调用任意构造器 Class cl = Random.class; Constructor cons = cl.getConstructor(long.class); Object obj = cons.newInstance(42L)// 传入构造器的参数
继承的设计技巧
- 将公共操作和字段放在超类中
- 不要使用protected关键字
- 任何人都能从类派生子类然后访问protected字段,破坏了封装性
- 同包的所有类都可以访问protected字段而不管是否为该类的子类
- 使用继承实现“is-a”关系
- 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时不要改变预期的行为(不要把改变被覆盖方法本来的作用)
- 使用多态而不要使用类型信息
- 如果两个类中出现了相同的方法,考虑为这两个类定义一个公共的超类或接口,并把该方法放在超类或接口中
- 不要滥用反射