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
的一个对象:
- 编译器查看对象的声明类型和方法名。需要注意的是,有可能存在多个名字相同但参数类型不一样的方法。编译器将会一一列举
C
类中所有名为f
的方法和其超类中所有名为f
而且可访问的方法(超类的私有方法不可访问)。至此,编译器已知道所有可能被调用的候选方法。- 接下来,编译器要确定方法调用中提供的参数类型。如果在所有名为
f
的方法中存在一个与所提供参数类型完全匹配的方法,就选择这个方法。这个过程称为重载解析。不过,由于允许类型转换(int
可以转换成double
,Manager
可以转换成Employee
等等),所以情况可能会变得很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,编译器就会报告一个错误。至此,编译器已经知道需要调用的方法的名字和参数类型。- 如果是
private
、static
、final
方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法。这称为静态绑定。与此对应的是,如果要调用的方法依赖于隐式参数的实际类型,那么必须在运行时使用动态绑定。- 程序运行并且采用动态绑定调用方法时,虚拟机必须调用与
x
所引用对象的实际类型对应的那个方法。假设x
的实际类型是D
,它是C
类的子类。如果D
类定义了方法f(String)
,就会调用这个方法;否则,将在D
类的超类中寻找f(String)
,以此类推。每次调用方法都要完成这个搜索,时间开销相当大。因此,虚拟机预先为每个类计算一个方法表,其中列出了所有方法的签名和要调用的实际方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在前面的例子中,虚拟机搜索
D
类的方法表,寻找与调用f(String)
相匹配的方法。这个方法既有可能是D.f(String)
,也有可能是X.f(String)
,这里的X
是D
的某个超类。这里需要提醒一点,如果调用的是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
方法具有下面的特性:
- 自反性:对于任何非空引用
x
,x.equals(x)
应该返回true
。- 对称性:对于任何引用
x
和y
,当且仅当y.equals(x)
返回true
时,x.equals(y)
返回true
。- 传递性:对于任何引用
x
、y
和z
,如果x.equals(y)
返回true
,y.equals(z)
返回true
,x.equals(z)
也应该返回true
。- 一致性:如果
x
和y
引用的对象没有发生变化,反复调用x.equals(y)
应该返回同样的结果。- 对于任何非空引用
x
,x.equals(null)
应该返回false
。不过,就对称性规则来说,当参数不属于同一个类的时候会有一些微妙的结果。例如
e.equals(m)
,这里的e
是一个Employee
对象,m是一个Manager
对象,并且两个对象有相同的姓名、薪水和雇佣日期。如果在Employee.equals
中用instanceof
进行检测,这个调用将返回true
,然而,这意味着反过来调用m.equals(e)
也需要返回true
。对称性规则不允许这个方法调用返回false
或者抛出异常。
这就使得Manager
类受到了束缚。这个类的equals
方法必须愿意将自己与任何一个Employee
对象进行比较,而不考虑经理特有的那部分信息。
就现在来看,有两种完全不同的情形:下面给出编写一个完美的
equals
方法的建议:
- 显式参数命名为
otherObject
,稍后需要将它强制转换成另一个名为other
的变量。- 检测
this
与otherObject
是否相等:if (this == otherObject) return true;
- 检测
otherObject
是否为null
,如果为null
,返回false
。这项检测是很必要的:if (otherObject == null) return false;
。- 比较
this
与otherObject
的类,如果equals
的语义可以在子类中改变,就使用getClass
检测:if (getClass() != otherObject.getClass()) return false;
。如果所有的子类都有相同的相等性语义,可以使用instanceof
检测:if (!(otherObject instanceof ClassName)) return false;
。- 将
otherObject
强制转换为相应类类型的变量:ClassName other = (ClassName) otherObject;
。- 现在根据相等性概念的要求来比较字段。使用
==
比较基本类型字段,使用Objects.equals
比较对象字段。如果所有的字段都匹配,就返回true
;否则返回false
:return 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,则必须返回falseif (otherObject == null) { return false; }// 如果类不匹配,他们就不可能相等if (getClass() != otherObject.getClass()) { return false; }// 现在我们知道,otherObject是一个非空的Employeevar 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();}
}
不过,还可以做得更好:
public int hashCode() {return Objects.hash(name, salary, hireDay);
}
equals
与hashCode
的定义必须相同:如果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
构造器为public
或protected
,会出现语法错误。
所有的枚举类型都是Enum
类的子类。它们继承了这个类的许多方法。
- 其中最有用的一个是
toString
,这个方法会返回枚举常量名。例如,Size.SMALL.toString()
将返回字符串SMALL
。toString
的逆方法是静态方法valueOf
:Size s = Enum.valueOf(Size.class, "SMALL");
。- 每个枚举类型都有一个静态的
values
方法,它将返回一个包含全部枚举值(实例)的数组:Size[] values = Size.values();
。ordinal
方法返回enum
声明中枚举常量的位置,位置从0开始计数。
5.7反射
反射机制可以用来:
5.7.1Class
类
在程序运行期间,java运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择要执行的正确的方法。不过,可以使用一个特殊的java类访问这些信息。保存这些信息的类名为
Class
。Object
类中的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
字节码文件保持一致)。下面给出必要的步骤:
- 获得拥有资源的类的
Class
对象,例如,ResourceTest.class
。- 有些方法,如
ImageIcon
类的getImage
方法,接受描述资源位置的URL。则要调用URL url = cl.getResource("about.gif");
。- 否则,使用
getResourceAsStream
方法得到一个输入流来读取文件中的数据。另一个经常使用资源的地方是程序的国际化。与语言相关的字符串,如消息和用户界面标签都存放在资源文件中,每种语言对应一个文件。
5.7.4利用反射分析类的能力
Field
、Method
和Constructor
分别用于描述类的字段、方法和构造器。Modifier
类可以用来判断方法或构造器的修饰符,还可以利用Modifier.toString
方法将修饰符打印出来。
Class
类中的getFields
、getMethods
和getConstructors
方法将分别返回这个类支持的公共字段、方法和构造器的数组,其中包括超类的公共成员。
Class
类的getDeclaredFields
、getDeclaredMethods
和getDeclaredConstructors
方法将分别返回类中声明的全部字段、方法和构造器的数组,其中包括私有成员、包成员和受保护成员,但不包括超类的成员。具体的可以通过翻阅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的访问控制。不过,可以调用
Field
、Method
或Constructor
对象的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类加载时机(静态加载和动态加载)
- 当创建对象时(
new
)。- 当子类被加载时,父类也会被加载。
- 调用类中的静态成员时。
- 通过反射动态加载。
5.7.9类加载过程
加载阶段
JVM在该阶段的主要目的是将字节码从不同的数据源(可能是
class
文件,也可能是jar包,甚至是网络)转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class
对象。
连接阶段
连接阶段-验证:
- 目的是为了确保
class
文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。- 包括:文件格式验证(是否以魔数
0xcafebabe
开头)、元数据验证、字节码验证和符号引用验证。- 可以考虑使用
-Xverify:none
参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。
连接阶段-准备:
JVM会在该阶段对静态变量,分配内存并默认初始化。这些变量所使用的内存都将在方法区中进行分配。
public class Person {// age是实例变量,不是静态变量,因此在准备阶段,是不会分配内存的private int age = 27;// idCard是静态变量,在准备阶段分配内存,但是会默认初始化,只有在初始化阶段才会赋值private static String idCard = "xxxxxx";// name是静态常量,和静态变量不一样,准备阶段就会赋值private static final String name = "zhangsan";
}
连接阶段-解析:
虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
- 到初始化阶段,才真正开始执行类中定义的java程序代码,此阶段是执行
方法的过程。
() 方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。
() - 虚拟机会保证一个类的
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的
() 方法,其他线程都需要阻塞等待,直到活动线程执行
() 方法完毕。
()
/*** 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继承的设计技巧
- 将公共操作和字段放在超类中。
- 不要使用受保护的字段。不过,
protected
方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。- 使用继承实现
is-a
关系。使用继承很容易达到节省代码量的目的,但切记不能滥用。- 除非所有继承的方法都有意义,否则不要使用继承。
- 在覆盖方法时,不要改变预期的行为。
- 使用多态,而不要使用类型信息。
- 不要滥用反射。
标签:
相关文章
-
无相关信息