5.类的继承

继承

5.1类、超类和子类
5.1.5多态

在java中,子类引用的数组可以转换成超类引用的数组,而不需要使用强制类型转换:

Manager[] managers = new Manager[10];
Employee[] staff = managers;

staff[0] = new Employee("Harry Hacker", ...);
// 编译器竟然接纳了这个赋值操作。但在这里,staff[0]与manager[0]是相同的引用。
// 这是一种很不好的情形,当调用Manager类特有的方法时,会引发错误。
// 为了确保不发生这类破坏,所有数组都要牢记创建时的元素类型,并负责监督仅将类型兼容
// 的引用存储到数组中。例如,使用new Manager[10]创建的数组是一个经理数组。如果
// 试图存储一个Employee类型的引用就会引发ArrayStoreException异常。
5.1.6理解方法调用

准确地理解如何在对象上应用方法调用非常重要。下面假设要调用x.f(args),隐式参数x声明为类C的一个对象:

  1. 编译器查看对象的声明类型和方法名。需要注意的是,有可能存在多个名字相同但参数类型不一样的方法。编译器将会一一列举C类中所有名为f的方法和其超类中所有名为f而且可访问的方法(超类的私有方法不可访问)。至此,编译器已知道所有可能被调用的候选方法。
  2. 接下来,编译器要确定方法调用中提供的参数类型。如果在所有名为f的方法中存在一个与所提供参数类型完全匹配的方法,就选择这个方法。这个过程称为重载解析。不过,由于允许类型转换(int可以转换成doubleManager可以转换成Employee等等),所以情况可能会变得很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,编译器就会报告一个错误。至此,编译器已经知道需要调用的方法的名字和参数类型。
  3. 如果是privatestaticfinal方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法。这称为静态绑定。与此对应的是,如果要调用的方法依赖于隐式参数的实际类型,那么必须在运行时使用动态绑定。
  4. 程序运行并且采用动态绑定调用方法时,虚拟机必须调用与x所引用对象的实际类型对应的那个方法。假设x的实际类型是D,它是C类的子类。如果D类定义了方法f(String),就会调用这个方法;否则,将在D类的超类中寻找f(String),以此类推。

每次调用方法都要完成这个搜索,时间开销相当大。因此,虚拟机预先为每个类计算一个方法表,其中列出了所有方法的签名和要调用的实际方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在前面的例子中,虚拟机搜索D类的方法表,寻找与调用f(String)相匹配的方法。这个方法既有可能是D.f(String),也有可能是X.f(String),这里的XD的某个超类。这里需要提醒一点,如果调用的是super.f(param),那么编译器将对隐式参数超类的方法表进行搜索。
需要注意的是,在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法必须也要声明为public

5.1.7阻止继承: final类和方法

有时候,可能希望阻止利用某个类定义子类。不允许扩展的类被称为final类。类中的某个特定方法也可以被声明为final。如果这样做,子类就不能覆盖整个方法(final类中的所有方法自动地成为final方法)。
将方法或类声明为final的主要原因是:确保它们不会在子类中改变语义。例如,String类是final类,这意味着不允许任何人定义String的子类。换言之,如果有一个String引用,它引用的一定是一个String对象,而不可能是其他类的对象。

5.2Object: 所有类的超类
5.2.3相等测试与继承

Java语言规范要求equals方法具有下面的特性:

  1. 自反性:对于任何非空引用xx.equals(x)应该返回true
  2. 对称性:对于任何引用xy,当且仅当y.equals(x)返回true时,x.equals(y)返回true
  3. 传递性:对于任何引用xyz,如果x.equals(y)返回truey.equals(z)返回truex.equals(z)也应该返回true
  4. 一致性:如果xy引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
  5. 对于任何非空引用xx.equals(null)应该返回false

不过,就对称性规则来说,当参数不属于同一个类的时候会有一些微妙的结果。例如e.equals(m),这里的e是一个Employee对象,m是一个Manager对象,并且两个对象有相同的姓名、薪水和雇佣日期。如果在Employee.equals中用instanceof进行检测,这个调用将返回true,然而,这意味着反过来调用m.equals(e)也需要返回true。对称性规则不允许这个方法调用返回false或者抛出异常。
这就使得Manager类受到了束缚。这个类的equals方法必须愿意将自己与任何一个Employee对象进行比较,而不考虑经理特有的那部分信息。
就现在来看,有两种完全不同的情形:

  • 如果子类可以有自己的相等性概念,则对称性需求将强制使用getClass检测。
  • 如果由超类决定相等性概念,那么就可以使用instanceof检测,这样可以在不同子类的对象之间进行相等性比较。

下面给出编写一个完美的equals方法的建议:

  1. 显式参数命名为otherObject,稍后需要将它强制转换成另一个名为other的变量。
  2. 检测thisotherObject是否相等:if (this == otherObject) return true;
  3. 检测otherObject是否为null,如果为null,返回false。这项检测是很必要的:if (otherObject == null) return false;
  4. 比较thisotherObject的类,如果equals的语义可以在子类中改变,就使用getClass检测:if (getClass() != otherObject.getClass()) return false;。如果所有的子类都有相同的相等性语义,可以使用instanceof检测:if (!(otherObject instanceof ClassName)) return false;
  5. otherObject强制转换为相应类类型的变量:ClassName other = (ClassName) otherObject;
  6. 现在根据相等性概念的要求来比较字段。使用==比较基本类型字段,使用Objects.equals比较对象字段。如果所有的字段都匹配,就返回true;否则返回falsereturn field1 == other.field1 && Objects.equals(field2, other.field2) && ...;。如果在子类中重新定义equals,就要在其中包含一个super.equals(other)调用。对于数组类型的字段,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。
/**
 * Employee.java
 */
public boolean equals(Object otherObject) {
    // 查看对象是否相同的快速测试
    if (this == otherObject) { return true; }

    // 如果显式参数为null,则必须返回false
    if (otherObject == null) { return false; }

    // 如果类不匹配,他们就不可能相等
    if (getClass() != otherObject.getClass()) { return false; }

    // 现在我们知道,otherObject是一个非空的Employee
    var other = (Employee) otherObject;

    // 测试字段是否具有相同的值
    return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
}

public int hashCode() {
    return Objects.hash(name, salary, hireDay);
}

/**
 * Manager.java
 */
 public boolean equals(Object otherObject) {
     if (!super.equals(otherObject)) { return false; }
     var other = (Manager) otherObject;
     // super.equals检查this和other属于同一个类
     return bonus == other.bonus;
 }

 public int hashCode() {
     return Objects.hash(super.hashCode(), bonus);
 }
5.2.4hashCode方法

散列码是由对象导出的一个整型值。每个对象都有一个默认的散列码,其值由对象的存储地址得出。需要注意的是,字符串的散列码是由内容导出的。
如果重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义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对象。
  • 需要组合多个散列值时,可以调用Objects.hash并提供所有这些参数。这个方法会对各个参数调用Objects.hashCode,并组合这些散列值。如果存在数组类型的字段,那么可以使用静态的Arrays.hashCode方法计算一个散列码,这个散列码由数组元素的散列码组成:
public int hashCode() {
    return Objects.hash(name, salary, hireDay);
}

equalshashCode的定义必须相同:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()返回相同的值。

5.2.5toString方法

最好通过调用getClass().getName()获得类名的字符串,而不要将类名硬编码写到toString方法中:

public String toString() {
    return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]";
}

这样的toString方法也可以由子类调用。
当然,设计子类的程序员应该定义自己的toString方法,并加入子类的字段。如果超类使用了getClass().getName(),那么子类只要调用super.toString()就可以了:

public class Manager extends Employee {
    // ...
    public String toString() {
        return super.toString() + "[bonus=" + bonus + "]";
    }
}
5.3泛型数组列表(List)

一旦能够确认数组列表的大小将保持恒定,不再发生变化,就可以调用trimToSize方法。这个方法将存储块的大小调整为保存当前元素数量所需要的存储空间。垃圾回收器将回收多余的存储空间。

5.6枚举类
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;

    private Size(String abbreviation) { this.abbreviation = abbreviation; }
    public String getAbbreviation() { return abbreviation; }
}

枚举的构造器总是私有的。可以省略掉private修饰符。如果声明一个enum构造器为publicprotected,会出现语法错误。
所有的枚举类型都是Enum类的子类。它们继承了这个类的许多方法。

  1. 其中最有用的一个是toString,这个方法会返回枚举常量名。例如,Size.SMALL.toString()将返回字符串SMALL
  2. toString的逆方法是静态方法valueOfSize s = Enum.valueOf(Size.class, "SMALL");
  3. 每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值(实例)的数组:Size[] values = Size.values();
  4. ordinal方法返回enum声明中枚举常量的位置,位置从0开始计数。
5.7反射

反射机制可以用来:

  • 在运行时分析类的能力。
  • 在运行时检查对象。例如,编写一个适用于所有类的toString方法。
  • 实现泛型数组操作代码。
  • 利用Method对象,这个对象很像c++中的函数指针。
5.7.1Class

在程序运行期间,java运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择要执行的正确的方法。不过,可以使用一个特殊的java类访问这些信息。保存这些信息的类名为ClassObject类中的getClass()方法将会返回一个Class类型的实例。
就像Employee对象描述一个特定员工的属性一样,Class对象会描述一个特定类的属性。可能最常用的Class方法就是getName。这个方法将返回类的名字。如果类在一个包里,包的名字也作为类名的一部分。还可以使用静态方法forName获得类名对应的Class对象。如果类名保存在一个字符串中,这个字符串会在运行时变化,就可以使用这个方法
获得Class类对象的第三种方法是一个很方便的快捷方式。如果T是任意的java类型(或void关键字),T.class将代表匹配的类对象。请注意,一个Class对象实际上表示的是一个类型,这可能是类,也可能不是类。例如,int不是类,但int.class是一个Class类型的对象。
虚拟机为每个类型管理一个唯一的Class对象。因此,可以使用==运算符实现两个类对象的比较。如果有一个Class类型的对象,可以用它构造类的实例。调用getConstructor方法将得到一个Constructor类型的对象,然后使用newInstance方法来构造一个实例:

var className = "java.util.Random"; // or any other name of a class with a no-arg constructor
Class cl = Class.forName(className);
Object obj = cl.getConstructor().newInstance();
5.7.3资源

Class类提供了一个很有用的服务可以查找资源文件(关键在于必须和.class字节码文件保持一致)。下面给出必要的步骤:

  1. 获得拥有资源的类的Class对象,例如,ResourceTest.class
  2. 有些方法,如ImageIcon类的getImage方法,接受描述资源位置的URL。则要调用URL url = cl.getResource("about.gif");
  3. 否则,使用getResourceAsStream方法得到一个输入流来读取文件中的数据。

另一个经常使用资源的地方是程序的国际化。与语言相关的字符串,如消息和用户界面标签都存放在资源文件中,每种语言对应一个文件。

5.7.4利用反射分析类的能力

FieldMethodConstructor分别用于描述类的字段、方法和构造器。Modifier类可以用来判断方法或构造器的修饰符,还可以利用Modifier.toString方法将修饰符打印出来。
Class类中的getFieldsgetMethodsgetConstructors方法将分别返回这个类支持的公共字段、方法和构造器的数组,其中包括超类的公共成员。
Class类的getDeclaredFieldsgetDeclaredMethodsgetDeclaredConstructors方法将分别返回类中声明的全部字段、方法和构造器的数组,其中包括私有成员、包成员和受保护成员,但不包括超类的成员。具体的可以通过翻阅API文档查看。

5.7.5利用反射在运行时分析对象
var harry = new Employee("Harry Hacker", 50000, 10, 1, 1989);
Class cl = harry.getClass();
Field f = cl.getDeclaredField("name");
// 取消访问控制的安全检查,可以提升反射效率
f.setAccessible(true);
Object v = f.get(harry);    // Harry Hacker

需要注意的是,反射机制的默认行为受限于java的访问控制。不过,可以调用FieldMethodConstructor对象的setAccessible方法覆盖java的访问控制。如果想要设置一个对象数组的可访问标志,可以使用AccessibleObject.setAccessible(cl.getDeclaredFields(), true)方法。

5.7.6使用反射编写泛型数组代码
public static Object goodCopyOf(Object a, int newLength) {
    Class cl = a.getClass();
    if (!cl.isArray()) { return null; }
    Class componentType = cl.getComponentType();
    int length = Array.getLength(a);
    Object newArray = Array.newInstance(componentType, newLength);
    System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
    return newArray;
}
// 这个方法可以用来扩展任意类型的数组,而不仅是对象数组。为了能够实现上述操作,应该将参数类型声明为Object。
5.7.7调用任意方法和构造器

Method类有一个invoke方法,允许调用包装在当前Method对象中的方法:Object invoke(Object, Object... args)。第一个参数是隐式参数,其余的对象提供了显式参数。对于静态方法,第一个参数可以忽略,即可以将它设置为null
想要获得Method对象,一种是通过调用getDeclaredMethods方法,再循环遍历;另一种是调用getMethod方法,后者需要提供方法名和相应的参数类型。
可以使用类似的方法调用任意的构造器。将构造器的参数类型提供给Class.getConstructor方法,并把参数值提供给Constructor.newInstance方法。

5.7.8类加载时机(静态加载和动态加载)
  1. 当创建对象时(new)。
  2. 当子类被加载时,父类也会被加载。
  3. 调用类中的静态成员时。
  4. 通过反射动态加载。
5.7.9类加载过程

在这里插入图片描述

加载阶段

JVM在该阶段的主要目的是将字节码从不同的数据源(可能是class文件,也可能是jar包,甚至是网络)转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class对象。

连接阶段

连接阶段-验证:

  1. 目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  2. 包括:文件格式验证(是否以魔数0xcafebabe开头)、元数据验证、字节码验证和符号引用验证。
  3. 可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。

连接阶段-准备:
JVM会在该阶段对静态变量,分配内存并默认初始化。这些变量所使用的内存都将在方法区中进行分配。

public class Person {
    // age是实例变量,不是静态变量,因此在准备阶段,是不会分配内存的
    private int age = 27;
    // idCard是静态变量,在准备阶段分配内存,但是会默认初始化,只有在初始化阶段才会赋值
    private static String idCard = "xxxxxx";
    // name是静态常量,和静态变量不一样,准备阶段就会赋值
    private static final String name = "zhangsan";
}

连接阶段-解析:
虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化
  1. 到初始化阶段,才真正开始执行类中定义的java程序代码,此阶段是执行<clinit>()方法的过程。
  2. <clinit>()方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并
  3. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
/**
 * 1.加载Test类,并生成Test的Class对象。
 * 2.连接阶段,num = 0。
 * 3.初始化阶段,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并,因此num = 100
 */
class Test {
    static {
        System.out.println("静态代码块执行");
        num = 300;
    }

    static int num = 100;

    public Test() {
        System.out.println("构造器执行");
    }
}
5.8继承的设计技巧
  1. 将公共操作和字段放在超类中
  2. 不要使用受保护的字段。不过,protected方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。
  3. 使用继承实现is-a关系。使用继承很容易达到节省代码量的目的,但切记不能滥用。
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 在覆盖方法时,不要改变预期的行为
  6. 使用多态,而不要使用类型信息
  7. 不要滥用反射
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值