第五章:继承
5.1类,超类,子类
5.1.1定义子类
Java中,所有继承都是public继承,而没有private和protected继承
对于超类中的private是不可访问的。不同的包继承protected
多称之为超类—子类
5.1.2覆盖方法
super.超类方法 调用超类方法。
对于super的一些理解:
super其实和this是不完全相同的概念的。super不是一个对象的引用,例如,不能将super赋值给另一个对象变量,它只是一个只是编译器调用超类方法的特殊关键字
5.1.3子类构造器
super(…)调用超类的构造器,如果不写,则默认调用无参构造器。
注:是不是和this很相像呢Java真是太有趣了
JVM能够自动进行动态绑定实现多态。动态绑定是默认的行为,如果不希望一个方法是虚拟的,可以将他标记为final。
5.1.4继承层次
由一个公共超类派生出来的所有类的集合称之为继承层次。
在继承层次中,从一个特定的类到其祖先的路径成为该类的继承链
注意:Java中不支持多继承。
5.1.5多态
替换规则:程序中出现超类对象的任何地方都可以使用子类对象替换
子类多态成为超类之后,只能调用超类中的方法
不能将超类的引用赋给子类变量
警告:在Java中,子类的引用的数组可以转换成超类引用的数组,而不需要使用强制类型转换。
package com.package1;
public class Test {
public static void main(String[] args) {
Son[] sons = new Son[10];
Father[] fathers =sons;//fathers和sons引用的都是同一个数组
fathers[0] = new Father();
sons[0].fun();
}
}
class Father{
}
class Son extends Father{
public void fun(){
System.out.println("fun");
}
}
//运行时报错:Exception in thread "main" java.lang.ArrayStoreException: com.package1.Father
Son son = (Father) new Father();
//而这样写会直接报错,上面的错误更隐蔽,更难发现,所以一定要注意
5.1.6理解方法调用
对于覆写的一些注意事项:
-
虽然返回类型不是签名的一部分,但是在覆盖一个方法时,需要保证返回类型的兼容性。允许子类将覆盖方法的返回类型改为原返回类型的子类型。
class Father{ public int fun(){ System.out.println("Father fun"); return 1; } } class Son extends Father{ public void fun(){ System.out.println("Son fun"); } } //报错:java: com.package1.Son中的fun()无法覆盖com.package1.Father中的fun()返回类型void与int不兼容
class Father{ public Father fun(){ return null; } } class Son extends Father{ public Son fun(){ return null; } } //这样是可以通过编译的,而且称fun方法有 可协变 的返回类型
-
在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。public>protected>defult(默认,不写)>private。
修饰词 本类 同一个包的类 继承类 其他类 private √ × × × 无(默认) √ √ × × protected √ √ √ × public √ √ √ √
方法调用的顺序,对于x.f(a)方法,为类C中的方法
- 编译器查看方法名和声明类型,一一列举C类在含有f的方法和超类中可访问的方法。
- 根据方法签名,找到合适的方法,这个过程称之为重载解析。而且由于存在类型转换,这个过程可能会很复杂。如果找不到,就报错。现在编译器已经知道了要调用的方法的名字和参数类型
- 如果是static,private,final方法,编译器可以准确的知道指向那一个方法,称之为静态绑定。否则,动态绑定。
- 如果是动态绑定,先在子类寻找这个方法,如果子类没有,就在超类寻找这个方法。
由于每次调用都会循环这个寻找的过程,很耗时。实际上,JVM会预先为每一个类计算了一个方法表。
5.1.7组织继承:final类和方法
final类:不能被继承,final方法:不能被覆写
将类声明为final,只有其中的方法自动变成final
应当仔细思考哪些方法应该声明为final。多态性的混乱使用会造成程序混乱
如果应该方法很短,并且没有被覆盖,编译器就能对他就行内联优化处理。例如内联调用e.getName会被替换成访问e.name。
5.1.8强制类型转换
-
只能在继承层次内进行强制类型转换
-
在将超类强制转换成子类之前,应该用instanceof进行检查。
if(staff[1] instanceof Manager){//boolean result = obj instanceof Class boos = (Manager) staff[1]; }
对于向下转型的理解:本质上要求,对于堆内存来讲,不能将超类转化为子类,这是核心原则。
Son son = (Son) new Father(); //报错,试图将超类堆内存转化为子类 Father father = new Son(); Son son = (Son) father; //不报错,因为father指向的就是子类的堆内存,第二句的转换本质上是换一个名字而已。
5.1.9抽象类
abstract
abstract的方法不需要实现,包含abstract方法的类必须声明为abstract。abstract类中也可以包含字段和具体方法,但是不推荐这样用。
哪怕没有抽象方法也可以声明为abstract类,抽象类不能实例化。
注:c++虚函数标明这个方法使用动态绑定,纯虚函数还标明不需要实现,相当于Java中的抽象方法
5.1.10受保护访问
protected数据访问类型只能由同一个包中的类访问。例如自继承的另一个包的子类不能直接访问超类的字段
5.2Object类
5.2.1Object类的对象
只有基本类型(整型,浮点型,布尔型,数值…)才不是Object的子类,才不是对象
所有数组类型,包括对象数组或者基本类型都扩展了Object类
5.2.2 equals方法
比较的是是否指向同一个对象
5.2.3相等与继承
equals方法要求满足:自反性,对称性,传递性,一致性(引用不变的情况下,反复调用,结果不变)
为了满足对称性,使用getclass方法,如果类不同,则直接返回flase
但是这又与软件设计原则“替换原则”(超类能做到的事情,替换成子类完全没影响),又有些设计人员使用了instanceof测试,核心思想是,对应的字段相同,就返回true,但是这又违反了自反性。
综上,两种实现的目的不同,不同的类使用了不同的设计,还需具体问题具体分析
static boolean equals(xxx[] a, xxx[] b);
//数组长度相同,对应元素相同返回true
static boolean equals(Object a, Object b);
//如果a,b都为null,返回true,只有有一个为null,返回false,否则返回a.equals(b);
5.2.4 hashCode方法
不同的对象的hashcode不同。Object类定义了hashcode方法,
Object类中hashcode是基于储存地址得出的。String重写了hashcode方法,是基于内容的
String a = "wuhu";
StringBuilder b = new StringBuilder(a);
System.out.println(a.hashCode()+" "+b.hashCode());
String c = "wuhu";
StringBuilder d = new StringBuilder(c);
System.out.println(c.hashCode()+" "+d.hashCode());
//result:
// 3660907 460141958
// 3660907 1163157884
如果覆写equals方法,必须覆写hashcode方法:
equals与hashCode的定义必须相容,如果x.equals(y)返回true。那么x.hashCode和y.hashCode的值必须相同。例如,重写了equals方法,只比较员工的ID,那么必须重写hashCode方法,重写的方法只能以ID来生成hashcode
import java.util.Objects;
public class Application {
public static void main(String[] args) {
Employee a = new Employee(1,"wuhu");
Employee b = new Employee(1,"qifei");
System.out.println(a.equals(b));
System.out.println(a.hashCode());
System.out.println(b.hashCode());
}
}
class Employee{
private int id;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return id == employee.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
}
//rsult:
// true
// 32
// 32
对于数组类型的字段,可以使用静态的Arrays.hashCode方法,它基于数组元素的散列码(hashCode)形成。
Objects.hashCode(Object a),a为null返回0,否则返回a.hashCode()
5.2.5 toString()方法
格式一般为 ObjectName[name1=data1,name2=data2…]
只要对象与一个字符串以 + 相连,就会自动调用这个对象的toString方法。
int[] a = {1,2,3,4};
System.out.println(a.toString());
数组类型是没有覆写toString方法的。若要得到想要的输出,需要调用Arrays.toString(),对于二维数组,如上文提到的,需要用Arrays.deepToString方法
int[] a = {1,2,3,4};
int[][] b = {{1,2},{3,4}};
System.out.println(Arrays.toString(a));
System.out.println(Arrays.deepToString(b));
// result:
// [1, 2, 3, 4]
// [[1, 2], [3, 4]]
5.3 泛型数组列表
ArrayList,一个有类型参数的泛型类
5.3.1声明数组列表
ArrayList<Employee> employees1 = new ArrayList<Employee>();
ArrayList<Employee> employees2 = new ArrayList<>();//这种省略称之为棱形语法
var employees3 = new ArrayList<Employee>();
//但是使用棱形语法+var,就会成为一个ArrayList<Object>
var errorDemo = new ArrayList<>();
ArrayList<Employee> employees1 = new ArrayList<>();
employees1.ensureCapacity(100);//设置所需的最小容量,或者说已经确定了使用过程中不会超过这个值
//也可以在定义的时候直接标明初始容量
ArrayList<Employee> employees2 = new ArrayList<>(10);
如果add的时候数组列表满了,他就会新建更大的+拷贝,这无疑会影响响应时间,所以可以添加一个初始值来提高效率
size()方法返回当前元素个数。
一旦确定了数组列表的大小保持核定,就可以调用trimToSize方法,去除未使用的空间。但是注意,如果要新加元素,这又势必会带来移动拷贝存储块的时间开销。
5.3.2访问数组元素
不能使用[]来访问。但可以使用set和get方法
ArrayList<Employee> employees = new ArrayList<>(10);
employees.set(0,new Employee());
//报错:
/*
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:248)
*/
//因为set方法是用来修改 已有 的值的。
//应该用 add 方法来添加新值
//可以用remove来删除
没有指明泛型类时,可能会带来强转的错误
5.3.3类型化与始数组列表的兼容性
将原始的ArrayList赋值给一个类型化的ArrayList会得到警告,而且使用强制类型转换符并不能避免这个警告
Java中有一个泛型类型限制规则:出于兼容性的考虑,编译器检查到没有发现违反规则的现象之后,就将所有的类型化数组全部转化为原始ArrayList对象。在程序运行时,所有的数组列表都是一样的,即虚拟机中没有类型参数。
确定了问题是可以忽略的,可以使用@SuppressWarnings(“unchecked”)来忽略警告
5.4对象包装器与自动装箱
包装器:与基本类型对应的类(例如Integer Long …)
包装器是不可变的,而且定义为final,不能有子类。
ArrayList 其中T必须为类,而不能为基本数据类型。
除非使用ArrayList 的操作便利性重要于int[] 的效率性,否则由于值包装在对象中,会造成处理效率的损失。
自动装箱:
ArrayList<Integer> list = new ArrayList<>();
list.add(12);
//会自动变换成
list.add(Integer.valueOf(12));
自动拆箱
int a = list.get(0);
//会自动变换成
int a = list.get(0).intValue();
Integer n = 3;
n++;
//并没有违背上面提到的包装器是不可变的这一点
//实际操作是,进行拆箱,拆箱之后自增,自增只会再装箱,并指向新的包装器
比较两个包装器对象的时候应该使用equals方法。
装箱与拆箱都是编译器的操作,而不是虚拟机.
5.5参数数量可变方法
亦称之为”变参“方法
对于printf方法的声明:
public PrintStream printf(String fmt, Object... args){
return format(fmt,args);
}
// 其中...是具体代码的一部分
Object…等同于Object[]
package com.package1;
public class Test {
public static void main(String[] args) {
System.out.println(Util.max(10,11,10.99,29));
//编译器将会传递 new double[] {10,11,10.99,29}
}
}
class Util{
public static double max(double... a){
double max = Double.NEGATIVE_INFINITY;
for (double v:a
) {
if(v>max){
max=v;
}
}
return max;
}
}
//result is 29
允许数组作为最后一个参数传递给由可变参数的方法。
5.6枚举类
本质上,枚举类型是一个类
public class Application {
public static void main(String[] args) {
}
}
public enum Size{
}
//会报错,不能有两个public类
enum Size{
SMALL , MEDIUM , LARGE , EXTRA_LARGE
}
//其中的为它的四个实例,而且不可能构造新的对象。所以在比较两个枚举类型的时候,可以直接用==,而不需要用equals
enum Size{
SMALL("S") , MEDIUM("M") , LARGE("L") , EXTRA_LARGE("XL");
//为枚举类型增加构造器,方法和字段
private String abbreviation;
Size(String abbreviation) {
this.abbreviation = abbreviation;
}
public String getAbbreviation() {
return abbreviation;
}
}
System.out.println(Size.SMALL.getAbbreviation());
//result is "S"
public class Application {
public static void main(String[] args) {
System.out.println(Size.SMALL.getAbbreviation()+Size.SMALL.getSize());
}
}
enum Size{
SMALL("S",1) , MEDIUM("M",2) , LARGE("L",3) , EXTRA_LARGE("XL",4);
private String abbreviation;
private int size;
Size(String abbreviation, int size) {
this.abbreviation = abbreviation;
this.size = size;
}
public String getAbbreviation() {
return abbreviation;
}
public int getSize() {
return size;
}
}
//result is "S1"
如上文所言,本质上枚举类型是一个类,其中的枚举值它的为实例。添加的字段和方法是每一个实例都有的,而且要位于枚举实例的下方。
public class Application {
public static void main(String[] args) {
Size size = Size.valueOf("SMALL");
System.out.println(size.toString());
Size[] sizes = Size.values();
for(Size size1 : sizes){
System.out.print(size1.toString());
System.out.println(" "+size1.ordinal());
//返回枚举常量在enum声明中的位置,从0开始计数
}
}
}
enum Size{
SMALL, MEDIUM, LARGE, EXTRA_LARGE
}
//result is
//SMALL
//SMALL 0
//MEDIUM 1
//LARGE 2
//EXTRA_LARGE 3
5.7 反射
能够分析类的能力的程序称之为反射。
功能:
- 在运行时分析类的能力
- 在运行时检查对象。例如,编写一个适用于所有类的toString方法
- 实现泛型数组操作代码
- 利用Method对象,类似于C++中的函数指针
5.7.1 class类
程序运行期间,JVM为所有的对象维护一个运行时类型标识,这个信息会跟踪每一个对象所属的类,利用运行时的类型信息选择要执行的正确的方法。
package com.package1;
public class Test {
public static void main(String... args) throws NoSuchFieldException, IllegalAccessException {
Father father = new Son();
Class<? extends Father> fatherClass = father.getClass();
System.out.println(fatherClass.getName());
//输出是 Son,再次印证了Class储存的是堆内存里的对象的信息
}
}
class Father{
}
class Son extends Father{
}
可以用Class类访问这些信息Class类实际上是一个泛型类,例如Employee.class类型是Class。这样就更复杂了,大多数情况下,可以忽略类型参数,使用原始的class类
import java.util.Random;
public class Application {
public static void main(String[] args) {
Employee e = new Employee();
Class cl =e.getClass();//可以使用getclass
System.out.println(cl.getName());
//如果在包里面,包的名字也将作为类的一部分
Class clss = Random.class;//也可以只要。。.class
System.out.println(clss.getName());
//也可以使用静态forName方法获得,注意有可能会有异常
try {
Class class2 = Class.forName("java.util.Random");
System.out.println(class2.getName());
}catch (Exception exception){
exception.printStackTrace();
}
}
}
class Employee{
}
//Employee
//java.util.Random
//java.util.Random
getName方法在应用于数组类型的时候会返回有些奇奇怪怪的名字
Class a = int.class;
Class b = double[].class;
System.out.println(a.getName());
System.out.println(b.getName());
// int
// [D
// 鉴于历史原因,getName对于数组类型会返回奇奇怪怪的名字
JVM为每一种类型管理唯一的Class对象,所以可以通过==来比较。必须类完全相同才会返回true,子类都不行。
可以使用getConstructor方法得到一个Constructor对象,然后调用这个对象的newInstance方法构造一个实例。
如果这个类没有显示的无参构造方法,getConstructor方法会抛出一个异常
package com.package1;
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) throws Exception {
Employee e = new Employee();
Class cl = e.getClass();
Constructor constructor = cl.getConstructor();
// Constructor getConstructor(Class...parameterTypes)
Object e2 = constructor.newInstance();
//Object newInstance(Object... params)将param传递到构造器,
//来构造这个构造器声明类的一个新实例
System.out.println(e2.toString());
}
}
class Employee {
public Employee() {
}
}
5.7.2 声明异常入门
异常:
- 检查性:编译器检查程序员是否知道这个异常并做好准备来处理后果
- 非检查性:编译器不期望程序员为这些异常提供处理器(hander),例如数组越界,访问null值引用等等
5.7.3 资源
与类关联的数据文件,称之为资源。例如图片,文档什么的
可以使用Class的getResource方法获得资源的URL,或者getResourceAsStream获得输入流。
如果没有找到,返回null,所以不需要加错误处理。
URL getResource(String name);
InputStream getResourceAsStream(String name);
5.7.4 利用反射分析类的能力
Field,Method,Construcror分别用于描述类的字段,方法,构造器
package com.package1;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
public class Test {
public static void main(String[] args) throws Exception {
Employee e = new Employee();
Class cl = e.getClass();
Method[] methods = cl.getMethods();
for(Method method : methods){
int modifiers = method.getModifiers();
//返回一个整数,用不同的0、1位描述所使用的修饰符
//使用Modifiers的toString方法可以将这个整数转化为字符
System.out.println(Modifier.toString(modifiers)+" "+method.getName());
}
}
}
class Employee {
public void test(){
}
}
getFields,getMethods,getConstructors 方法分别返回这个类的公共字段,公共方法,公共构造器的数组,其中也包括超类的成员
而getDeclareFields,getDeclareMethods,getDeclaredConstructors返回所有的字段,方法,构造器 的数组
package com.package1;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;
public class Test {
public static void main(String[] args) throws Exception {
String name;
if(args.length> 0 ){
name = args[0];
}
else {
Scanner in = new Scanner(System.in);
System.out.println("输入想要查看的类");
name = in.next();
}
Class cl = Class.forName(name);
Class superCl = cl.getSuperclass();
String modifiers = Modifier.toString(cl.getModifiers());//class 的修饰符
if(modifiers.length() > 0){
System.out.print(modifiers+" ");
}
System.out.print("class "+name);
if(superCl != null & superCl != Object.class){
System.out.print(" extends"+superCl.getName());
}
System.out.print("\n{\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("\n}");
}
private static void printConstructors(Class cl){
Constructor[] constructors = cl.getDeclaredConstructors();
for(Constructor c : constructors){
int modifies = c.getModifiers();
System.out.print(" "+Modifier.toString(modifies)+" ");
System.out.print(cl.getName()+"(");
Class[] parameterTypes = c.getParameterTypes();
for(int i = 0 ; i < parameterTypes.length ; i++){
if(i > 0){
System.out.print(", ");
}
System.out.print(parameterTypes[i].getName());
}
System.out.println(")");
}
}
private static void printMethods(Class cl){
Method[] methods = cl.getDeclaredMethods();
for(Method m : methods){
Class returnType = m.getReturnType();
String name = m.getName();
System.out.print(" ");
String modifies = Modifier.toString(m.getModifiers());
if(modifies.length() > 0){
System.out.print(modifies+" ");
}
System.out.print(returnType+" "+name+"(");
Class[] paraTypes = m.getParameterTypes();
for(int i = 0 ; i < paraTypes.length ; i++){
if(i > 0 ){
System.out.print(", ");
}
System.out.print(paraTypes[i].getName());
}
System.out.println(")");
}
}
private static void printFields(Class cl){
Field[] declaredFields = cl.getDeclaredFields();
for(Field f : declaredFields){
Class type = f.getType();
String name = f.getName();
System.out.print(" ");
String modifies = Modifier.toString(f.getModifiers());
if(modifies.length() > 0){
System.out.print(modifies+" ");
}
System.out.println(type.getName()+" "+name+";");
}
}
}
5.7.5 使用反射在运行时分析对象
利用反射机制可以查看在线编译时还不知道的对象字段
对于FIeld类型对象f,obj是某个包含f字段的对象,f.get(obj)返回一个对象,这个对象的值为obj的字段值
package com.package1;
import java.lang.reflect.Field;
public class Test {
public static void main(String... args) throws NoSuchFieldException, IllegalAccessException {
Util util = new Util();
Class cl = util.getClass();
Field field = cl.getDeclaredField("a");
Object o = field.get(util);
System.out.println(o);
}
}
class Util{
private int a = 1;
}
//报错:
Exception in thread "main" java.lang.IllegalAccessException: Class com.package1.Test can not access a member of class com.package1.Util with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
由于a是一个私有字段,get和set方法都会抛出IllegalAccessException异常。
Java安全机制允许查看一个对象有哪些字段,但是除非拥有访问权限,否则不允许读写那些字段的值。
但是可以通过调用Field,Method,Constructor的超类AccessibleObject的setAccessible方法覆盖Java本省的访问控制。
如果不允许访问,setAccessible调用会抛出异常。访问可以被模块系统和安全管理器拒绝
但是:能够不受控的访问内内部的日子已经屈指可数,例如如下报错:
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.package1.ObjectAnalyzer (file:/C:/Users/NieGuevara/Desktop/JavaRelearn/out/production/JavaRelearn/) to field java.util.ArrayList.serialVersionUID
WARNING: Please consider reporting this to the maintainers of com.package1.ObjectAnalyzer
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
5.7.6 使用反射编写泛型数组代码
个人觉得此处才是反射的最常用的应用场景:识别传入的Object对象实际的类型
反射是多态向下转型的解决方案。
package com.package1;
import java.lang.reflect.*;
public class Test {
public static void main(String[] args) throws Exception {
int b[] = {1,2,3,4};
b = (int[]) copy(b,10);
System.out.println(b.length);//10
//其中:为了能够实现不仅仅堆对象数组操作,将a声明为Object而非Object[]
//因为,例如int[]能够转化为Object,但不能转化为对象数组Object[]!
}
private static Object copy(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;
}
}
5.7.7 调用任意方法和构造器
Method的invoke方法可以实现调用任意类的方法
Object invok(Object obj, Object... args)
第一个参数是隐式参数,对于static方法,可以忽略,即设置为null.
可以通过getDeclaredMethod获得所有的方法,或者通过getMethod(String name, Class… parameterTypes)获得指定的方法(注意:此处传递的是签名,也就是还包含参数类型为第二个参数,以此来确定方法)
出于效率,安全性,可维护性的考虑,应该减少使用Method对象,更好的方法是使用接口,以及Java8中引入的lambda表达式