java篇 类的进阶0x07:继承

文章目录

  • 继承
    • 继承的语法
    • 继承的作用与特点
      • 继承与组合的区别
    • 覆盖 overide
      • @Override
      • 构造方法无法覆盖
    • super:和父类对象沟通的桥梁
      • super 到底指哪个父类的实例
      • super 严格意义上并非真的是一个父类的引用
      • super 调用父类的构造方法
        • 子类构造方法被调用时,Java 会主动去调用父类的无参构造方法
    • 父类引用和子类引用的关系
      • 如何理解一个类型的引用
      • 如何理解一个类型的对象
      • 强制类型转换
    • 多态
      • 静态多态:重载(Overload)
      • 动态多态:覆盖(Override)
    • 继承里的静态方法

继承

继承的语法

在类名后使用 extends 再加上 要继承的类名。

public class SonClass extends FatherClass{}

继承的作用与特点

子类(SubClass)继承了父类(SuperClass)的方法和属性(如果处在不同包,则仅继承 public 方法和 public 属性;若在同一个包则缺省的方法和属性也能被继承)

使用子类的引用可以调用父类的共有(public)方法

使用子类的引用可以访问父类的共有(public)属性

子类的引用可以一物二用,既可以当作父类的引用使用,又可以当作子类的引用使用。

java 中只允许一个类有一个直接的父类,即所谓”单继承“。子类可以再被其他类继承,也就是说可以一代继承一代的。

虽然说 A 类可以被 B 类继承,B 类又可以被 C 类继承,C 类又可以被 D 类继承,但一般不会有“祖父类”、“曾祖父类”、或者“一级父类”、“二级父类”的称呼。只要在这条继承链上的,都可以说是上游的“父类”,比如说 C 是 D 的父类,B 也是 D 的父类,A 也是 D 的父类。(其实想想本质上 D 虽然是通过一级一级继承下来的,但是本质完全可以变为直接从 A 继承,然后增加相应的方法和属性,完全可以这么理解,所以说 A 是 D 的父类并不无道理。)

子类继承了父类的所有属性和方法(只要是对子类的见的),但是子类不能访问父类的 private 成员(包括方法和属性)。

继承与组合的区别

继承 vs. 组合: 其实继承的方式有点像是把一个父类的引用(/实例)包含进子类中,使得子类引用(/子类实例)可以调用父类的 public 属性和方法。那么为什么不用一个普通的类,然后在这个普通类里创建一个 A 类的引用(/实例)(也可以使用到 A 类的 public 方法/属性),而非得去继承 A 类(同样实现使用 A 类的 public 方法/属性)?

这里其实有区别,前者(非继承)实际上是一种“组合”(该类中包含 A 类引用(/实例):has-A)方式,而后者(继承)实际上是一种“是”(该类就是 A 类的一中特殊形式:is-A)的方式。

对问题的理解,就决定了到底是使用“组合”还是“继承”的方式来编写代码(两种方式并没有说一定哪种更好,而是要看要解决什么实际问题),这就上升到如何设计系统去解决问题,对实际问题理解得好,那么设计的系统就可以很简单,否则就可能把系统设计的很复杂。

比如说实际问题是:手机到底是一种特殊的手电筒,还是手机仅仅是组合了一个特殊手电筒(闪光灯)的东西?

  • 如果你的理解是前者,那么手机应该是继承手电筒:is-A;

  • 如果你的理解是后者,那么手机应该是组合了手电筒:has-A。

其实用组合的方式是可以完全代替继承来实现相应的功能的,但是很多时候,可能会把程序逻辑设计得非常繁琐复杂。如果本身,这个实际问题是 has-A,你用组合方式可能还设计得挺正常的。但如果这个实际问题是 is-A,你非得用组合的方式去解决,那你设计的问题就可能很绕了。

比如说 A 类,是一个公共类,别的公司设计的,你拿来用,实现自己的功能。

  1. 首先你为了更好完成 B 类(你自己写的类)无法去修改 A 类的代码

    • 一来 A 类是别人公司的代码你改不了;

    • 二来就算 A 类是你公司的,你有权去改,但是因为 A 是公共类,不仅仅只有你的 B 类在用,可能其他的 C、D、E、F 类都在用,你为了 B 类改 A 类,会直接影响 C、D、E、F 类的正常运作,显然也是不行的。

  2. 你不修改 A 类,就只能在自己的 B 类中做完所有的逻辑设计。那么考虑如下场景:A 类中提供了 public 方法,实现商品购买功能(.buy()),而你在 B 类中希望去限制购买数量(A 类没有提供相应的购买且限制的方法),那么你就得自己在 B 类中写一个方法(.buyThing()),但问题是你并不能阻止用户调用没有限制的 A 类暴露出来的 public 方法来购买(buy()),那么你的限制设计变得没有意义了,完全可以被绕过。

// 用组合的方式,在 B 类中创建一个 A 类的实例
public class A{// 购买加入购物车public void buy(int count){xxx;}// 付款public void pay(double price){xxxx;}xxx;
}public class B{// 创建一个 A 类实例,以便调用 A 类的 public 方法/属性public A createA(){return new A();}// 购买加入购物车(并限制购买数量)public void buyThing(int count){if (count > 5){						// 做一个数量限制过滤xxxx;}xxx;}    
}// 别人调用
public class TestUse{public static void main(String[] args){B b = new B();// 实现购买加入购物车b.buyThing(3);		// 调用 B 类暴露的 public 购买加入购物车方法(buyThing())// 这里你如何避免用户不去使用 A 类暴露的 public 购买加入购物车方法(buy())?// 用户的确可以完全绕过你在 B 类中设置的购买限制// b.createA().buy(10);// 实现付款b.createA().pay(12.3);		// 调用 A 类暴露的 public 付款方法(pay())}
}

当然,如果 A 类真的完全由你设计,并且最初就把这个类设计成缺省的访问控制,即只有同一个包中的其他类才能访问,就可以通过将组合类放入和 A 的同一个包中,然后将组合类设计成 public,然后用调用类(非同一包中)去导入这个 public 的组合类。那么的确能起到“无法绕过组合类的限制直接调用 A 类方法”的效果。但是这有非常多的限制,需要调用类不在 A 所在的包中,组合类又得放到和 A 同一个包中。而现实中提供一个类出来基本都是 public 的,所以只有继承才能做到你无论把这些类放到哪(不管是不是同一个包中)都能实现你想要的限制过滤。

当然,如果需求仅仅是原封不动地调用 A 类的功能,那么用这种组合方式还是可以的,比如说一个手机类,包含一个闪光灯类,这样的组合。使用时创建一个手机类的实例,再用手机类的方法创建闪光灯类的实例,直接沿用闪光灯的方法和属性。这个时候,如果选择用继承,反而不太好。并且继承只能单一继承,但手机类可以同时包含闪光灯类、键盘类、听筒类、麦克风类、锂电池类、屏幕类等,你要是拿手机类继承了闪光灯类,那么其他类还是需要以组合的方式进入这个手机类,这样设计完全没有必要,并且也不合理。

覆盖 overide

子类并不是只能把父类的方法拿过来(也能在父类的方法/属性基础上添加子类自己的新的(与父类中不同名的)方法和属性),而是还能通过覆盖来替换父类中不适合的方法。(对于子类来说,父类的方法并不能不要,但是可以改)

覆盖才是继承的精髓。

通过使用和父类方法签名一样,而且返回值也必须一样的方法,可以让子类覆盖掉父类的方法。

方法签名一样很好理解,但注意这里返回值也必须得一模一样!兼容的数据类型都不行(比如说子类返回值是 int 父类返回值的 long,或者反过来,子类返回值是 long,父类返回值是 int)【这点已经实际尝试过了,会报错,说类型不兼容。】

至于访问修饰符,覆盖的话,子类的该方法的开放权限应该是大于等于父类的该方法开放权限。

  • 即如果父类的该方法是 public,子类只能是 public;
  • 如果父类的该方法是缺省的(同一包中的其他类可访问),子类可选缺省或 public;
  • 如果父类的该方法是 private,则子类继承时根本没有继承到这个方法,因此不存在覆盖的概念。

Kevin:这里其实就是说,假如父类 A 和子类 B 放到同一个包中,调用类放在另一个包中,本来调用类是无法访问 A 类的缺省方法的,但 B 继承并覆盖该缺省方法,将其访问修饰符修改为 public 的话,就有可能将 A 类的该方法暴露出去。

public class A{public void sayHello(String name){System.out.println("Hello " + name + " !");}
}public class B extends A{public void sayHello(String uname){							// 覆盖,返回值类型要和父类一模一样,方法签名也要一致(形参名相同与否倒无所谓),方法签名相同但返回值类型不是一模一样的话会报错。访问修饰符的权限要大于或等于父类的访问权限(指要更开放或同等开放程度)System.out.println("Hello " + uname + ", welcome!");}
}

覆盖的前提是子类继承了父类的该方法,如果本身父类的这个方法就不对子类可见,那么子类并没有继承该方法,那么创建同方法签名的方法时,实际上是在子类中增添了一个和其他普通的方法没有区别的新方法而已,并不是覆盖父类的方法。如果是这样的话,就不存在对方法签名和返回值类型相同的限制。

  • 这里注意所谓的不可见,是整条继承链上的不可见。比如说:

    类继承链(靠左为靠右的父类):A、B、C、D。调用类 TestUse

    • 如果 A、B、C 类在同一个包,D 在另一个包,A、B、C 类存在一个覆盖方法 buy()是缺省的访问修饰符号,那么,D类实际上因为看不到 buy()这个方法,所以没有继承到这个方法,因此 D 中定义 buy()是等于在添加一个新方法,而不是在做方法覆盖,因此没有返回值类型的限制。

    • 如果这条链中,有一个 类 和 D 是在同一个包,比如说 B 类,那么 B 类因为也没有能继承 A 类的该方法(因为看不到 A 类的该方法),所以 B 类的 buy()是个新方法,哪怕 C 类看不见 B 类的这个新方法,也就也没有继承,即便 C 类和 A类在同一个包中,C 类就是没有继承到 buy(),因为从继承链来说,buy()没有被继承到 B,往后就更加不会有这个方法继承下去。所以导入 C 类,也是无法用 C 类实例调用 buy()。而 D 类中又定义了 buy(),那这个算是 D 新增添的方法么?因为 C 类没有这个方法嘛。但实际上并不能。只要是在同一条类继承链中,又能看到这条链上的其他同签名方法,那么就得遵循返回值类型也要相同的覆盖规则。不能说 B 没继承到buy(),是 B 自己新添加的,而 D 也没有继承到 buy(),是 D 自己新添加的。D要有同签名的方法 buy() 就只能让返回值类型和前面的 B 的buy()返回值类型一模一样,并且并不能妄图将 D 的buy()访问修饰符改为 private 来躲避发现。因为覆盖的访问修饰符只能开放更大的权限而不能缩小(倒是可以把 B 的buy()设置为 private,这样 D 等于看不到 B 的 buy(),不存在要遵循覆盖规则的问题)

      Kevin:简而言之,一条类继承链上的某个方法,即便中间出现继承中断,只要继承链上的两个类,后代类能看到前辈类的同方法签名的方法,就得把返回值类型设置成一样的,否则就会报错。

Kevin:其实最简单规避这些特殊容易出错的情况的方式就是,将子类和父类放到不同包中,让子类只继承父类的公共(public)方法/属性。在使用同签名方法时,返回值一律都设置成相同类型。这样就能避免错乱,并且从系统设计的角度,同方法签名的父类子类的方法,就应该是返回值类型相同的。

IDEA TIPS: 当 A 方法对于子类 B 是可见的(不是 private ,或修饰符缺省却在同一个包,或 public 时),IDEA 才显示 A 和 B 该方法的覆盖跳转标志(代码左侧行号会有一个蓝色圆圈标志,点击会跳转到其覆盖方法或被覆盖方法)。

关于连续继承链中的方法覆盖问题

IDEA TIPS: 如果是继承关系是:C 继承 B,B 继承 A,然后A中的方法 buy() 被 B 的 buy() 覆盖了,并且 C 的 buy() 再次把 B 的 buy() 覆盖了,那么 C 的 buy() 代码左边是“蓝色圆圈+向上箭头”,点击会先跳转到 B 类的buy() ,而 B 的 buy() 左边是两个标志:“蓝色圆圈+向上箭头” 和 “蓝色圆圈+向下箭头”,点击下箭头会跳回 C 的 buy(),点击上箭头会跳转到 A 的 buy()。而 A 的 buy() 左边是“蓝色圆圈+向下箭头”,点击会让你选择到底是跳转到 B 的 buy() 还是 C 的 buy()

疑问1: 继承链(靠左为父类):A、B、C 都有 buy()方法(B、C 都对 A 中的 buy() 进行覆盖),那么 B b = new B();,b调用 buy() 时,会不会受到子类中的覆盖方法影响?

经过实验,不会。

public class A{public void buy(){System.out.println("调用的是 A 的 buy()。");}
}
public class B extends A{public void buy(){System.out.println("调用的是 B 的 buy()。");}
}
public class C extends B{public void buy(){System.out.println("调用的是 C 的 buy()。");}
}// 调用类
public class TestUse{public static void main(String[] args){B b = new B();b.buy();								// 调用的是 B 的 buy()。}
}

多态: 子类对父类的方法进行覆盖,就实现了同一个方法,有不同的行为,这就是所谓的“多态”。

@Override

覆盖方法可以在方法的上方标注“@Override”(注意 O 要大写)(这是一种[[…/…/基础语法/注释|注解]])。但也可以不加,不加不会影响编译运行。

加了的好处:

  • 易读: 增加代码的易读性,更容易区分哪些方法是普通方法,哪些方法是覆盖父类的方法。
  • 语法检查: 加了之后,java 会帮你检查当前方法到底是否属于覆盖方法。因为有时候写代码的时候会把要覆盖的方法的方法签名搞错,如果不加@Override,程序不会报错,java 会认为你是在子类中新定义了一个方法(而非覆盖父类的方法),但你自己却以为覆盖了父类方法。如果加了@Override,java 会帮你确认这个是否是覆盖了父类的方法,如果不是(比如说你方法签名搞错了),java 就会给你报错(java: 方法不会覆盖或实现超类型的方法),让你编译运行不成功。
public class A{public void sayHello(String name){System.out.println("Hello " + name + " !");}
}public class B extends A{@Overridepublic void sayHello(String uname){System.out.println("Hello " + uname + ", welcome!");}
}

构造方法无法覆盖

Kevin:突发奇想,子类能否定义构造方法,将父类同方法签名构造方法覆盖。

public class A{public A(){						// 即便不在父类 A 中显式定义构造方法也一样,实验结果不变。}
}
public class B extends A{public A(){						// 报错:方法声明无效; 需要返回类型。这是因为如果和类名不同,就只能是一个普通的方法,就得有返回值类型。}
}// 这里另外一个可能就是,覆盖本身就是要求方法签名相同且返回值类型一模一样。构造方法根本就不能写返回值类型,缺少了这个条件。而如果你非得在子类中定义一个如下的方法:
public class B extends A{public A A(){						return new A();}
}
// 也没有意义。语法是不报错了,但本身就不是构造方法,B b = new B() 时并不会自动调用,仅仅是添加了一个 b.A() 返回一个 A 实例对象的普通方法罢了。

super:和父类对象沟通的桥梁

子类对象里可以认为有一个特殊的父类的对象,这个父类对象和子类对象之间通过 super 关键字来沟通。

使用 super 可以调用父类的方法和属性(当然必须要满足访问控制符的控制)。

设计上,父类的属性尽可能还是不要让子类能公开访问使用:

语法上 super 的确可以调用父类的属性,但程序设计上,不应该把父类的属性设置成可以被子类直接使用,而应该将父类的属性都设置为 private。(由于父类属性设置成 private 就不会被子类继承)子类应该自己定义自己的属性,如果非得用到父类的属性,比如说调用父类的方法时,会用到父类的某些属性,也得是通过调用父类的方法super.xxx()来访问父类的属性,而非让子类直接就能 super.父类属性 来获取父类属性。

super 到底指哪个父类的实例

注意 super.方法(),比如说 super.buy()super 不一定是指这个子类的直接父类,可以是继续往前推,只要是在这条类继承链上的任何一个父类。而如果说不只有一个 buy()呢(比如说发生过覆盖、重载之类)?那就看当前子类它能访问到哪个父类的这个 buy() 方法,如果发生了覆盖,就是指覆盖后的那个(反正就是把所有父类看作一个整体,去调用父类的方法,如果发生了覆盖,那么肯定只认“最新版本”,也就是覆盖后的方法),如果是重载,就选相应的重载方法,如果 A、B、C、D、E 类这样继承下去(当前子类为 E),只有 A 才有定义 buy() 方法(并且 E 能访问到该方法),那么 E 中调用的 super.buy()就是指 A 中的 buy()

public class A{public void buy(int count){xxx;}    
}public class B extends A{public void sayHello(){System.out.println("Hi!");}
}public class C extends B{public void VipBuy(boolean isVIP){xxxx;super.sayHello();					// 用 super 调用父类(B)的 sayHello() 方法xxx;super.buy(3);						// 用 super 调用父类(A)的 buy() 方法 xxxxx;}
}

super 严格意义上并非真的是一个父类的引用

super 虽然看似一个父类的引用(/父类的实例),但严格意义上,super 并不是父类的引用,只是它起到了接近是父类的引用的效果,但本质上它不是一个真正的父类引用(/父类的实例)。super 和 this (自引用,自引用是真的是自己类的一个如假包换的实例)是不同的,不是可以简单模拟的。只不过是 super 的确能调用父类的可见的(通常就是 public)的方法、属性。

其实如果 super 真的是一个如假包换的父类引用,那就不是继承了,更像是之前提到的“组合”方式。

下面验证一下 this 和 super 本质的不同。this 是真的就是一个实例,但 super 并不是一个真正的实例。

public class A{}public class B extends A{public B tryCreateB{return new B();					// 这是没有任何问题的,返回一个新的 B 实例}public B tryReturnB{return this;					// 这里也没有任何问题,返回当前的 B 实例}// public A tryCreateA{//     return super;				// 不注释掉的话,都不需要运行,这么写 IDEA 就会提示要在super后加上.xxx,强行运行,会报错。// }
}// 调用类
public class TestUse{public static void main(String[] args){B b = new B();System.out.println(b);System.out.println(b.tryCreateB());System.out.println(b.tryReturnB());// b.tryCreateA();				// 去除注释,强行运行会报错。}
}

当然在实际使用中,可以近似地理解为创建子类对象的时候,同时创建了一个隐藏的父类对象。

如果说 java 创建一般的类的对象,是创建了一个能记录数据的小本子,然后放到公告板上;那么创建子类的对象,同样也是创建了一个小本子,但除了这个小本子,还隐藏地创建了它之前所有的父类(继承链上的所有的父类)的对象的小本子,然后把这些父类的小本子和当前子类的小本子装订到一块,作为一个新的小本子,放到公告板上。

当然这只是可以这么理解。因为这些父类的小本子实际上无法单独拿出来,但你用 super.xxx 访问父类属性时,这些数据是肯定有地方存储的,就是在这些父类小本子上。

super 调用父类的构造方法

使用 super 调用父类的构造方法,必须是子类构造方法的第一个语句。

同样地, super 调用构造方法,不可以使用 super 访问父类的属性和方法,不可以使用子类成员变量和方法。

super()调用构造方法时,也是调用 <init>方法,和普通类调用构造方法时会调用 <init> 方法一样。

// 不可以使用 super 访问父类的属性和方法
super(super.getName());	// 不合法,不可以在super调用构造方法时,使用super访问父类的方法/属性

语法:

public class A{}public class B extends A{public B(){super();}
}

这里,可以回想起,在构造方法里调用重载的构造方法(this(参数列表))时,必须将调用写在方法体的第一行,后面可以继续有代码。

那么这里就和也同样要写在第一行的 super(参数列表) 发生冲突了。实际上如果同时出现 this()super(),不管谁在第一行谁在第二行都是非法的(就很无解了)。要如何解决呢?可以另外创建一个方法(比如叫 init(),方法名随便起)把一些公共的初始化的东西放进去。

public class A{}public class B extends A{public B(){super();init();}public B(int count){init();}public void init(){xxxx;}
}

为什么 java 要制造这种冲突呢?因为如果允许在构造方法中同时使用 super()this() 的话,比如说下面(当然是非法的,现在假设它合法),就可能会出现构造一个子类对象, super() 被调用不止一次的情况,显然是有问题的。

public class A{}public class B extends A{public B(){super();this(3);}public B(int count){super();}}

子类构造方法被调用时,Java 会主动去调用父类的无参构造方法

子类调用构造方法时,实际上一定会调用父类的构造方法,不管你有没有显式地在子类的构造方法中写入 super()

如果子类构造方法中的第一行没有写 super(参数列表)(即默认缺省的情况下),java 实际上就会去找父类中没有任何参数的构造方法去调用。如果父类中没有不带任何参数的构造方法,就会报错。

public class A{public A(){System.out.println("Creating an A objcet...");}public A(int number){System.out.println("Creating " + number + " A objcets...");}
}public class B extends A{public B(){System.out.println("Creating a B object...");}public B(int count){System.out.println("Creating "+ count + " B objects...");}
}// 调用类
public class TestUse{public static void main(String[] args){B b = new B();							}
}
// 输出两行:
// Creating an A objcet...
// Creating a B object...// 若改为
public class TestUse{public static void main(String[] args){B b = new B(3);							}
}
// 输出两行:
// Creating an A objcet...
// Creating 3 B object...// 说明的确会自动在 B 的构造方法中第一行插入无参数的 super();// 若此时将 A 类的无参数构造方法注释掉(或直接删掉)而保留 A 类的带参数自定义的构造方法(就等于 java 不会给 A 自动添加无参数构造方法,A直接就没有了无参构造方法),那么无论调用的是 B b = new B(); 还是 B b = new B(3); 都会报错。如果想不报错,需要在 B 的构造方法中显式地在第一句中调用 A 中的带参数构造方法。!!!!并且要强调的是,是得B中所有的构造方法都得明确写入 A 中的带参数构造方法,不要以为只在你打算调用的构造方法中写就行了,只要有一个没写,就报错!!!!public class B extends A{public B(){super(10);		System.out.println("Creating a B object...");}public B(int count){super(30);System.out.println("Creating "+ count + " B objects...");}
}// 即便你用的是 B b = new B(); 而非 B b = new B(10); 也需要在 public B(int count) 方法的第一行加入 super(参数值),而不是说你不用哪个构造方法就可以不用管,编译运行反正只要漏了其中一个没显示加入super(xxxx),都会报错。

如果父类中没有缺省的构造方法(无参的构造方法),那么子类的所有构造方法中都必须在第一行调用父类有参数的构造方法。

疑问1: “super 调用构造方法,不可以使用 super 访问父类的属性和方法,不可以使用子类成员变量和方法。”这句话怎么理解?

public class A{public int count = 3;public A(int number){}
}public class B extends A{public int age = 1;/* 方式1:// 调用子类成员属性public B(){						super(this.age);			// 报错:无法在调用超类型构造器之前引用this}若改为 agepublic B(){						super(age);					// 报错:无法在调用超类型构造器之前引用age}// 调用父类属性public B(){super(count);				// 报错:无法在调用超类型构造器之前引用count}若改为public B(){super(this.count);			// 报错:无法在调用超类型构造器之前引用this}若改为public B(){super(super.count);			// 报错:无法在调用超类型构造器之前引用super}*//* 方式2:public B(){super(3);this.age = 2;System.out.println(this.age);		// 2 ,不报错this.count = 10;					// 这里即便是 super.count = 10; 下面两行的输出也是一样的System.out.println(this.count);		// 10,不报错System.out.println(super.count);	// 10,不报错}*/}

因此,和普通类的构造方法中类似,普通类中调用构造方法/调用重载构造方法(this())时不能使用成员属性/成员方法,但在构造方法体内是可以使用成员属性/成员方法的。

super()调用父类构造方法时也一样,只是说不能将父类的方法/属性、子类的成员方法/属性作为 super() 的参数传入,这样会报错。但可以在子类的方法体内调用父类的方法/属性、子类的成员方法属性。

父类引用和子类引用的关系

这里最想传达的一个观点就是:一个自定义类型的引用,是可以指向这个类型的对象、或这个类型的子类(或子类的子类…)的对象。

  • 这有什么用呢?
    • 比如说面对现实的问题,对人类来创建一个类,其实例就是一个普通人对象。人类这个类可以再分(创建子类),分成亚洲人类、非洲人类、欧洲人类、美洲人类等,然后比如亚洲人类,还可以继续再分(再创建子类),分成中国人、日本人、韩国人等,中国人又可以再分(创建子类)为不同省份或不同民族的类别。而此时,实际上可以直接用最开始的父类——人类的引用,指向任意一个子类的对象,比如说亚洲人、中国人、广东人…
      • 这又有什么用呢?
        • 如果这些父类、子类中都有覆盖一个方法,比如说这些类中都各自定义了方法签名和返回值类型一模一样的方法 buy(),那么同样是人类的引用,比如说:Human human = new 子类();,调用这个覆盖方法时(human.buy();)实际调用的是其对应的子类中的那个 buy() 方法。这就等于说,同样调用一个方法,但是因为父类引用指向了不同的子类对象,会有不同的行为(执行不同子类中的覆盖方法)。这其实就是“多态”。

IDEA 中提供了查看类的继承链的功能:菜单栏 Navigate --> Type Hierarchy (ctrl + h)

父类引用可以指向子类对象,子类引用不可以指向父类的对象。

父类引用可以指向子类对象。

更进一步:即,可以用子类(以及子类的子类)的引用给父类的引用赋值。(即继承链(靠左为父类):A、B、C。允许A a = new C();跨越 B)

  • 因为子类继承了父类的方法和属性,所以父类的对象能做到的,子类的对象肯定能做到。换句话说,我们可以在子类的对象上,执行父类的方法。
  • 当父类的引用指向子类的实例(或者父类的实例),只能通过父类的引用,像父类一样操作子类的对象。也就是说:“名”的类型,决定了能执行哪些操作。
// 父类引用可以指向子类对象
FatherClass a = new SonClass();		// 正常编译运行,不会报错。

反过来,子类引用指向父类对象则不行。因为父类并没有子类的属性和方法。父类对象无法执行子类的这些方法、访问子类的这些属性。

更进一步:父类(以及父类的父类)的引用不能给子类的引用赋值。(除非这个父类的引用指向的对象本来就是一个子类的对象,但即便是这样也不能直接给子类的引用赋值,而还需要通过强制类型转换才能赋值给子类的引用。)

疑问1: 但是父类引用指向子类的话,子类对象也没有父类本身的私有属性啊。

有这个问题,其实是对调用其他类的场景还比较模糊。

实际上比如说 A 类是父类,B 类继承自 A 类(B 类是 A 类的子类),那么真的用 A、B类去创建对象的实际上并不是 A、B类自己写个 public static void main 方法来做这些事,而是第三个类,一个调用它们的类。那么对于这个类来说,通常也不和 A、B类在同一个包中,所以需要导入 A、B 类, 用 A 或 B 的构造方法创建对象,然后调用相应的方法。但导入类(且处于不同包中)本身能导入的也就只有 public 的公开出来的方法和属性。所以其实理解上,基本只要去考虑父类与子类的 public 属性和 public 方法。

而的确,从继承的角度来看,子类就是继承了父类的所有 public 的方法和属性,除此之外,子类还可能增添自己独有的 public 方法和属性。

因此,的确就是:

  • 父类的对象能做到的,子类对象肯定能做到。子类对象上可以执行父类的方法。
  • 而父类没有子类的方法,父类对象无法执行子类的方法。

因此就有:父类引用可以指向子类对象,子类引用不可以指向父类的对象。

// 子类引用不能指向父类对象
SonClass b = new FatherClass();	// 非法,会报错:不兼容的类型,FatherClass 无法转换为 SonClass

如何理解一个类型的引用

Kevin:一个类型的引用(也就是说“名”的类型,int a就是 a 这个引用的“名”是 int,a 是 “int 类型” 的引用),应该是能执行这个类型(的成员)特有的方法,访问/修改这个类型(的成员)的特有属性的。即引用的类型,决定引用可以调用的方法、属性。

  • 更进一步:如果一个子类的对象,同时被父类引用 a 和子类引用 b 指向,那么虽然它们指向的是同一个对象,a就是只能调用父类的方法,而b则是可以调用子类的方法(因为继承问题,也包含了父类的方法),如果 a 也想执行子类的方法,则需要先进行强制类型转换,将引用类型转变成该子类,才能调用子类的方法:((SonClass)a).子类方法()

所以假设 SonClass 继承自 FatherClass,父类的引用(不论指向的是父类对象还是子类对象),它能执行的都是父类的方法(这里要思考一下是,如果指向的是子类对象,执行的是否是子类覆盖后的方法),访问、调用的也是父类的属性。

经过实验,FatherClass a = new SonClass(); 父类引用指向子类对象时,如果子类覆盖了父类的某个方法,那么用这个父类引用 a 调用这个方法时,调用的实际上是子类(截止至该子类的)覆盖后的方法版本(而不会管这个子类之后还有没有子类对这个方法有更新的覆盖方法版本)。

  • 但如果是子类不是覆盖该方法,而是增加了这个方法的一个重载方法,父类引用是无法调用这个重载方法的,强行调用会报错。

  • 如果继承链是(靠左为父类) A、B、C,B、C 都对同一个 A 中的方法(.buy())进行了覆盖,那么 A a = new B(); 后,a.buy()会不会因为多了一个子类 C 的覆盖方法(.buy()),而调用之,而不是调用 B 中的覆盖方法(.buy())呢?

    • 经过实验,并不会。实际上

      public class A{public void buy(){System.out.println("这是 A 的 buy。");}
      }
      public class B extends A{public void buy(){				// B 中 覆盖一次System.out.println("这是 B 的 buy。");}
      }
      public class C extends B{public void buy(){				// C 中再次覆盖System.out.println("这是 C 的 buy。");}
      }public class D extends B{			// D 也继承自 Bpublic void buy(){				// D 中再次覆盖System.out.println("这是 D 的 buy。");}
      }A a = new B();
      a.buy();		// 调用的是 B 的覆盖方法 buy()A a2 = new C();
      a2.buy();		// 调用的是 C 的覆盖方法 buy()A a3 = new D();
      a3.buy();		// 调用的是 D 的覆盖方法 buy()// 这就很有意思了,这个父类引用调用同一个方法,实际上调用的实际类型对象中定义的覆盖方法。而不管这个类有没有子类、“兄弟类”、“亲戚类”,不管其他类中有没有进一步地覆盖这个方法。
      // 这种设计就叫多态。
      

而经过实验,父类引用a也的确不能调用子类中添加的新方法,强行调用子类有但父类没有的方法,会报错。而父类引用 a (指向子类对象)调用父类的方法是没有问题的。

如果父类引用(比如叫:a)指向子类对象,临时又想调用一下子类新增加的方法,则可以用强制类型转换的方式将a转成成子类引用,然后调用子类新增加的方法:

FatherClass a = new SonClass();// 强制类型转换
((SonClass)a).Sonclass新增的方法();

对于引用类型的“名”决定它的行为,打个不太恰当的比方:

唐王李渊和他的儿子李世民,李世民的心腹部将也在朝里为官(打着李渊的旗号),作为朝臣他有这个朝臣应该行使的职权(李渊赋予的职权)。

同样地,李世民的心腹部将在李世民的秦王府里(打着李世民的旗号)也有相应的职位,行使秦王府里的相应职权。

但如果这个臣子(哪怕是李世民的心腹部将)站的李渊的队(还打着李渊的旗号),从这个朝堂上职权上看,他就无法管理秦王府的内务,无法行使秦王府里的职权。他得重新打上李世民的旗号(前提得是能转打李世民的旗号,如果本身不是李世民的心腹部将,自然不允许你转打李世民的旗号)才能行使在秦王府里的职权。

这叫“在其位,才能谋其政”,你得打着某一边的旗号,才能行使那一边的职权。

一个类型的引用,可以说是对应一种官职名称以及其所能行使的职权。

如何理解一个类型的对象

如果把一个类型的引用,比喻成一种官职名称以及其所能行使的职权,那么一个类型的对象就是真的具备行使这些职权能力的人。

打个不太恰当的比方:

唐王李渊的朝臣的官位,对应了他在朝堂上的官职,也对应了相应可以行使的职权。

同样,李世民秦王府中的职位,也对应了在秦王府中可以行使的职权。

对象就是实实在在的个体,到底是李渊的朝臣(而非李世民的心腹),还是李世民的心腹部将。

你可以让李世民的心腹部将打李渊的旗号(唐朝臣子),在朝堂上行使职权,他也有这样的能力。

但你不能让李渊的朝臣(非李世民的心腹)去秦王府行使职权,因为他不是李世民的心腹部将,挂名本身就不合法,更加不懂得如何打理秦王府,他没这个经验,也没有这个能力。

强制类型转换

null 可以强制类型转换成任何引用类型,这是合法操作,不会报错。(这是因为 null 是所有引用类型的缺省值)

实际测试过,其实 null 不用强制转换成该引用类型,也是可以直接赋值给该引用类型的。不管是自定义引用类型,还是 String 这类 java 本身定义的引用类型。

  • 如果确定一个父类的引用指向的是子类对象,那么可以使用强制类型转换,把这个父类引用指向的对象(实际上是子类对象)赋值给一个子类的引用;

  • 但如果一个父类引用指向的就是一个父类对象,则无法将这个父类引用指向的对象(实际上是父类对象)赋值给一个子类的引用。

    打个不太恰当的比方,唐王李渊和他儿子李世民,李世民的心腹部将当然是可以打着李渊的旗号的(毕竟领着唐朝的俸禄),但这些实际上是李世民的心腹部将,可以随时转到李世民的旗下。但如果这个打着李渊旗号的臣子本身就不是李世民的心腹,自然不能转到李世民的旗下。(否则李世民小团伙关起门来策划玄武门之变的时候不就有被忠于李渊的臣子举报的风险了嘛 ( ̄ε(# ̄) 哇哈哈哈哈)

public class A{}
public class B extends A{}// 调用类
public class TestUse{public static void main(String[] args){// (实际)子类对象赋值给子类引用A a1 = new B();B b11 = a1;				// 报错:不兼容的类型:A 无法转换为 BB b12 = (B)a1;			// 合法,正常编译运行,不会报错。// (实际)父类对象赋值给子类引用A a2 = new A();B b21 = a2;				// 报错:不兼容的类型:A 无法转换为 BB b22 = (B)a2;			// 报错:A 不能被映射成 B}
}

多态

能够调用哪些方法,取决于引用类型。但具体调用的是哪个方法,取决于实例是什么类型。

父类的引用指向不同类型的对象,调用某个覆盖方法时,实际执行的方法取决于对象的类型,而非引用的类型。

  • 即,能调用哪些方法,是引用决定的,具体执行哪个类的覆盖方法,是引用指向的对象决定的。

    这就是覆盖的精髓。覆盖是多态的一种,并且是最重要的一种。

当我们用父类的引用指向子类的实例,并调用某个方法时,实际调用的是子类的该方法,如果子类里没有覆盖这个方法,就去父类里找,如果父类里也没有,就去父类的父类里找…

如果能让一个引用指向这个对象,就说明这个对象肯定是这个引用的类型或者其子类的一个实例(否则赋值会发生ClassCastException)。

疑问1: 如果继承链(靠左为父类):A、B、C。A中有buy()方法(里面还调用了sayHi()方法),B、C 中也分别对 buy() 进行了覆盖,并且,在自己的buy() 调用了父类的buy(),会出现什么情况?如果 A 中还有一个sayHi()方法,C 中也覆盖了这个 sayHi()方法,并在C 的 buy()中调用了 sayHi() ,会发生什么?

public class A{public void buy(){System.out.println("调用的是 A 的 buy()。");this.sayHi();}public void sayHi(){System.out.println("Hi, 这里是 A 的 HI。");}
}
public class B extends A{public void buy(){super.buy();System.out.println("调用的是 B 的 buy()。");}
}
public class C extends B{public void buy(){super.buy();System.out.println("调用的是 C 的 buy()。");this.sayHi();				// 如果注释了这一行,也不会影响 A a = new C();a.buy();的第二行输出结果}public void sayHi(){System.out.println("Hi, 这里是 C 的 HI。");}
}// 调用类
public class TestUse{public static void main(String[] args){A a = new C();a.buy();}
}// 输出结果:
/*
调用的是 A 的 buy()。
Hi, 这里是 C 的 HI。    // 这行很意外吧?对的,当实例是C时,它连这个A的buy()里面调用的sayHi()也替换为了C的sayHi()。
调用的是 B 的 buy()。
调用的是 C 的 buy()。
Hi, 这里是 C 的 HI。*/// 调用类
public class TestUse {public static void main(String[] args) {A a = new B();a.buy();}
}
// 输出结果:
/*
调用的是 A 的 buy()。
Hi, 这里是 A 的 HI。
调用的是 B 的 buy()。*/

结论: 对一个实例执行某个方法时,不管这个方法还调用了哪些方法,这些凡是有被覆盖的方法,都选用覆盖方法的版本(从最先的父类沿着继承链到当前继承类[含]为止的最新覆盖版本)进行替换。

疑问2: 如果调用的是 this,那么 this 是指定义那个方法(包含this)的类,还是对象所属的类呢?

public class A{public void buy(){System.out.println("调用的是 A 的 buy()。");this.sayHi();}public void sayHi(){System.out.println("Hi, 这里是 A 的 HI。");}public void returnThis(){								// 增加返回 this 对象的函数System.out.println("返回一个this对象 "+this);}public void thisHi(){									// 增加包含用 this来调用函数 的函数this.sayHi();}
}
public class B extends A{public void buy(){super.buy();System.out.println("调用的是 B 的 buy()。");}
}
public class C extends B{public void buy(){super.buy();System.out.println("调用的是 C 的 buy()。");this.sayHi();}public void sayHi(){System.out.println("Hi, 这里是 C 的 HI。");}
}// 调用类
public class TestUse {public static void main(String[] args) {A a = new A();A b = new B();A c = new C();System.out.println("a的调用结果:");a.returnThis();a.thisHi();System.out.println("b的调用结果:");b.returnThis();b.thisHi();System.out.println("c的调用结果:");c.returnThis();c.thisHi();}
}// 输出结果:
/*
a的调用结果:
返回一个this对象 org.test.extendstest.polytest.A@3d075dc0			// 可以看到是 A 类对象
Hi, 这里是 A 的 HI。
b的调用结果:
返回一个this对象 org.test.extendstest.polytest.B@214c265e			// 可以看到是 B 类对象
Hi, 这里是 A 的 HI。							// B类对象中能调用到的,是A类继承过来的sayHi,而调用不到C的覆盖sayHI()
c的调用结果:
返回一个this对象 org.test.extendstest.polytest.C@448139f0			// 可以看到是 C 类对象
Hi, 这里是 C 的 HI。							// C类对象能调用到的,是C类的覆盖sayHI()*/

结论: this 也是指当前的对象,哪怕 this 出现在父类的调用方法中,仍然是指当前的对象。即使是在继承自父类的代码里,去调用一个方法(包括含有 this的方法),也是先从子类开始,一层层继承关系地找。

其实回想一下,在一个类中调用自己的方法,是可以直接写方法名的,但实际上隐含了 this.方法名(),前面的 this.。所以在父类方法 (methodA()) 中调用父类的方法(methodB()),实际上是 this.methodA() 中调用 this.methodB(),而 this实际上是当前对象(如上例 ”疑问1“ 中的子类对象 B、C 的对象),所以就能解释为什么会先从子类方法中找覆盖方法,找不到就往上一级的父类里找,再找不到再往上上一级找了,以及为什么“在父类方法 (methodA()) 中调用父类的方法(methodB())时,连里面的 methodB()”也被替换为当前子类对象的覆盖方法,因为methodB本质上也是带this.的(this.methodB),所以是要从当前子类对象开始找。

无论一个方法是使用哪个引用被调用的,它都是在实际的对象上执行的。执行的任何一个方法,都是这个对象所属的类的方法。如果没有,就去父类找,再没有,就去父类的父类找,依次寻找,直到找到。

这也是 java 选择单继承的重要原因之一。以为在发生继承时,多态已经很复杂了,如果存还在多继承,那么多继承的情况下,如果使用不当,多态可能会非常复杂,以至于使用的代价超过其带来的好处。

  • 如果存在多继承(一个类继承自几个父类)一个类有多个父类,那么当前的方法到底调用哪个父类的方法?这就很复杂了。

静态多态:重载(Overload)

覆盖 vs. 重载

覆盖: 用这个引用类型来调用方法;(并且注意,覆盖需要“方法签名相同+返回类型相同”)

重载: 调用别的方法时,用这个类型引用作为参数;(并且注意,重载仅仅需要“方法签名相同”)

重载调用哪个方法,和参数的引用类型相关,和引用实际指向的类型无关。

注意这三个概念:形参引用类型、实参引用类型、对象类型

重载,看的是“实参引用类型”到底和“形参引用类型”匹配,调用那个类型匹配的方法。

public class A {
}
public class B extends A{
}
public class C extends B{
}// 重载方法的类
public class MethodTest {public void sayHi(A aObject) {System.out.println("调用的是 A 类型重载的 sayHi() " + aObject);}public void sayHi(B bObject) {System.out.println("调用的是 B 类型重载的 sayHi() " + bObject);}public void sayHi(C cObject) {System.out.println("调用的是 C 类型重载的 sayHi() " + cObject);}// 重载的参数类型,相同位置,不一定要有继承或者兼容的关系,完全可以 free style,比如说定义为 String 或 int。public void sayHi(String str) {System.out.println("调用的是 String 类型重载的 sayHi() " + str);}public void sayHi(int abc){System.out.println("调用的是 int 类型重载的 sayHi() " + abc);}
}// 调用类
public class TestUse {public static void main(String[] args) {A a = new C();MethodTest methodTest = new MethodTest();methodTest.sayHi(a);methodTest.sayHi((B)a);methodTest.sayHi((C)a);methodTest.sayHi((String)null);			// 这里之所以要将 null 强制转为 String,是因为多个重载方法参数都是引用类型,你直接用 null 为实参,java 不知道具体调用哪个重载方法。如果本身重载方法就只有形参为String与int两个类型,而没有上面的A、B、C自定义引用类型,则可以直接 null,而不用强制转换为 String。methodTest.sayHi(3);methodTest.sayHi((A)null);				// 这3个调用就更加极端了,进一步说明调用哪个方法与实际指向对象无关,methodTest.sayHi((B)null);				// 而只与实参类型有关。methodTest.sayHi((C)null);				//}
}
// 输出
/*
调用的是 A 类型重载的 sayHi() org.test.extendstest.polyoverloadtest.C@214c265e
调用的是 B 类型重载的 sayHi() org.test.extendstest.polyoverloadtest.C@214c265e
调用的是 C 类型重载的 sayHi() org.test.extendstest.polyoverloadtest.C@214c265e
调用的是 String 类型重载的 sayHi() null
调用的是 int 类型重载的 sayHi() 3
调用的是 A 类型重载的 sayHi() null
调用的是 B 类型重载的 sayHi() null
调用的是 C 类型重载的 sayHi() null*/// 可以看到,同样是一个实际为 C 类的对象,根据指向它的引用类型不同,调用的重载方法也不同。

疑问1: 如果存在一个继承链(靠左为父类):A、B、C,一个方法并没有同时定义三个重载方法(分别以A、B、C类型为参数),而是只定义了 B类型为形参的方法,那么分别传入 A、B、C类型的实参,会怎样?

public class A {
}
public class B extends A{
}
public class C extends B{
}// 重载方法的类
public class MethodTest {
//    public void sayHi(A aObject) {
//        System.out.println("调用的是 A 类型重载的 sayHi() " + aObject);
//    }public void sayHi(B bObject) {System.out.println("调用的是 B 类型重载的 sayHi() " + bObject);}//    public void sayHi(C cObject) {
//        System.out.println("调用的是 C 类型重载的 sayHi() " + cObject);
//    }public void sayHi(String str) {System.out.println("调用的是 String 类型重载的 sayHi() " + str);}public void sayHi(int abc){System.out.println("调用的是 int 类型重载的 sayHi() " + abc);}
}// 调用类
public class TestUse {public static void main(String[] args) {A a = new C();MethodTest methodTest = new MethodTest();methodTest.sayHi(a);						// 报错methodTest.sayHi((B)a);						// 调用的是 B 类型重载的 sayHi() org.test.extendstest.polyoverloadtest.C@214c265emethodTest.sayHi((C)a);						// 调用的是 B 类型重载的 sayHi() org.test.extendstest.polyoverloadtest.C@214c265e
//        methodTest.sayHi((String)null);
//        methodTest.sayHi(3);}
}// 若重载方法类为
public class MethodTest {public void sayHi(A aObject) {System.out.println("调用的是 A 类型重载的 sayHi() " + aObject);}public void sayHi(B bObject) {System.out.println("调用的是 B 类型重载的 sayHi() " + bObject);}//    public void sayHi(C cObject) {
//        System.out.println("调用的是 C 类型重载的 sayHi() " + cObject);
//    }public void sayHi(String str) {System.out.println("调用的是 String 类型重载的 sayHi() " + str);}public void sayHi(int abc){System.out.println("调用的是 int 类型重载的 sayHi() " + abc);}
}
// 调用类的输出结果不变
public class TestUse {public static void main(String[] args) {A a = new C();MethodTest methodTest = new MethodTest();methodTest.sayHi((B)a);						// 调用的是 B 类型重载的 sayHi() org.test.extendstest.polyoverloadtest.C@214c265emethodTest.sayHi((C)a);						// 调用的是 B 类型重载的 sayHi() org.test.extendstest.polyoverloadtest.C@214c265e}
}

这说明,“重载”仍旧只看实参类型与哪个形参相匹配就调用哪个方法,但如果都不匹配的话,则尝试将该实参类型强制转换,看是否有能匹配的方法以供调用,并且优先匹配类继承链上与该实参类型相近的父类,如果不满足,则再往上一级父类转换。

但父类无法向子类进行转换以匹配子类为形参的重载方法。

静态多态,调用的方法和实参实际指向的对象无关,只与实参引用本身的类型相关。因为调用时参数类型是确定的,所以在编译期间就可以明确地知道哪个方法被调用。如果存在多种可能,则会有编译错误。

确定调用哪个方法只需要引用的类型,这叫做静态多态。即在编译期就知道该调用哪个方法。

如果引用类型没有完全匹配的,则会根据类继承关系,沿着参数当前类型,往“根”父类方向转换以找到最贴近的参数类型匹配方法。

这里再次表明,java 单继承是多么的必要,如果是多继承,压根就不知道往哪条继承链上去就近转换类型匹配重载方法,整个程序设计就很容易搞得特别复杂。(有些编程语言就是多继承的,比如:c++)

无论是静态方法,还是成员方法,重载寻找方法的顺序是一样的。这里就不赘述了。

动态多态:覆盖(Override)

覆盖的核心:寻找调用哪个方法。

  • 两个角色:
    1. 引用的类型,调用的方法签名必须是这个类型中定义了的;
    2. 引用实际指向的对象,它决定是通过这个签名调用的实际是哪个类的方法。
public class A {public void sayHi() {System.out.println("调用 A 定义的 sayHi " + this);}public void sayHi(int num) {System.out.println("调用 A 定义的带参数 sayHi " + num + " " + this);}
}
public class B extends A {public void sayHi() {System.out.println("调用 B 定义的 sayHi " + this);}
}
public class C extends B{public void sayHi() {System.out.println("调用 C 定义的 sayHi "+ this);}
}// 调用类
public class TestUse {public static void main(String[] args) {A a = new C();a.sayHi();	// (覆盖)调用 C 定义的 sayHi org.test.extendstest.polyoverloadtest.C@3d075dc0// 实际对象为 C 类型,但 C 中没有定义重载的 sayHi(int),向上找,B中也没有,那么就是继承自 A 的 sayHi(int)a.sayHi(3);	// (重载) 调用 A 定义的带参数 sayHi 3 org.test.extendstest.polyoverloadtest.C@3d075dc0A a2 = new B();a2.sayHi();	// (覆盖)调用 B 定义的 sayHi org.test.extendstest.polyoverloadtest.B@7b23ec81}
}// 输出结果:
/*
调用 C 定义的 sayHi org.test.extendstest.polyoverloadtest.C@3d075dc0
调用 A 定义的带参数 sayHi 3 org.test.extendstest.polyoverloadtest.C@3d075dc0
调用 B 定义的 sayHi org.test.extendstest.polyoverloadtest.B@7b23ec81*/

Kevin对多态的总结:

  • 静态多态:重载
    • 不同的类型的引用(与引用指向的对象无关)作为参数,匹配到的重载方法不同。
  • 动态多态:覆盖
    • 不同的对象(与引用类型无关),调用同方法签名的方法,实际调用是(类继承链中)不同类的覆盖方法。

勿忘初心:程序的执行就是找到要执行的代码,并且知道执行的代码能访问哪些数据,数据从哪里来。

  • 多态核心问题就是:要调用哪个类的哪个方法,这个方法用到的数据(this 引用)是谁。

继承里的静态方法

继承里的静态方法能实现类似覆盖的效果,也和覆盖一样遵循 “方法签名相同且返回值类型要一模一样” 的要求,但实际上这不能称作覆盖。因为覆盖最重要的一点是需要根据实际的对象类型来选择实际执行的方法,但静态方法到底选择哪个类里面的同签名静态方法,不取决于实际对象的类型,而是指向这个对象的引用的类型。

而且虽然静态方法可以用指向对象(甚至 null)的引用来调用,但最正确的调用方式还是应该用类名来调用静态方法。

// 方法签名不同时,返回值类型可以不同,因为这是重载,不是覆盖。
public class A {public static double sayHi(){System.out.println("调用的是 A 中定义的 sayHi");return 1;}
}
public class B extends A{}
public class C extends B{public static int sayHi(int abc){								System.out.println("这里调用的是 C 中定义的 sayHi");return 1;}
}// 调用类
public class TestUse {public static void main(String[] args) {A c1 = new C();c1.sayHi();				// 调用的是 A 中定义的 sayHiC c2 = new C();c2.sayHi();				// 调用的是 A 中定义的 sayHi		(说明 C 的确能继承 A 中的静态方法)c2.sayHi(3);			// 这里调用的是 C 中定义的 sayHi		(发生了重载)}
}
// 输出:
// 调用的是 A 中定义的 sayHi
// 调用的是 A 中定义的 sayHi
// 这里调用的是 C 中定义的 sayHi// ------------------------------------------------------------------------------------------------------
// 方法签名相同时,返回值类型要相同,否则报错,也就是说和成员方法的覆盖要求是一样的。
public class A {public static double sayHi(){System.out.println("调用的是 A 中定义的 sayHi");return 1;}
}
public class B extends A{}
public class C extends B{public static int sayHi(){								// 报错:返回类型int与double不兼容。即一定得一模一样,能发生自动转换的类型也不能。System.out.println("这里调用的是 C 中定义的 sayHi");return 1;}
}// 调用类
public class TestUse {public static void main(String[] args) {A c = new C();c.sayHi();}
}// ------------------------------------------------------------------------------------------------------
// 方法签名相同,且返回值类型一模一样时,实际上也不是发生覆盖,而是调用哪个方法和引用的类型有关,与引用指向的对象类型无关。
public class A {public static double sayHi(){System.out.println("调用的是 A 中定义的 sayHi");return 1;}
}
public class B extends A{}
public class C extends B{public static double sayHi(){System.out.println("这里调用的是 C 中定义的 sayHi");return 1;}
}// 调用类
public class TestUse {public static void main(String[] args) {A c1 = new C();c1.sayHi();				// 调用的是 A 中定义的 sayHiA.sayHi();				// 调用的是 A 中定义的 sayHi((A)null).sayHi();		// 调用的是 A 中定义的 sayHi((B)c1).sayHi();		// 调用的是 A 中定义的 sayHiB.sayHi();				// 调用的是 A 中定义的 sayHi((B)null).sayHi();		// 调用的是 A 中定义的 sayHiC c2 = new C();c2.sayHi();				// 这里调用的是 C 中定义的 sayHiC.sayHi();				// 这里调用的是 C 中定义的 sayHi((C)null).sayHi();		// 这里调用的是 C 中定义的 sayHi}
}
// 输出:
// 调用的是 A 中定义的 sayHi
// 调用的是 A 中定义的 sayHi
// 调用的是 A 中定义的 sayHi
// 调用的是 A 中定义的 sayHi
// 调用的是 A 中定义的 sayHi
// 调用的是 A 中定义的 sayHi
// 这里调用的是 C 中定义的 sayHi
// 这里调用的是 C 中定义的 sayHi
// 这里调用的是 C 中定义的 sayHi// 即是说,调用哪个类中的静态方法,压根不看实际对象的类型,而是看引用类型,哪怕这个引用类型指向的“对象”是 null(即压根不存在对象)。

因为“覆盖”实际上是在 this 自引用上做文章,this 调用哪个方法取决于这个引用所指向的对象的类型,但静态方法本身没有 this 自引用,没有访问 this 自引用,所以没有覆盖的功能。

另外,如果一个方法是静态的,那么只能用静态的方法去覆盖,反之亦然,如果一个方法不是静态的,你不能用静态方法来覆盖。

public class A{public static void sayHi(){System.out.println("This is from A.");}
}
public class B extends A{public static void sayHi(){System.out.println("This is from B.");}
}/*
你不能写成:
// A
public static void sayHi(){
// B
public void sayHi(){或// A
public void sayHi(){
// B
public static void sayHi(){
*/// 这两种写法都不行

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/9741.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

为什么 SSH(安全终端)的端口号是 22 !!

导读为什么 SSH&#xff08;安全终端&#xff09;的端口号是 22 呢&#xff0c;这不是一个巧合&#xff0c;这其中有个我&#xff08;Tatu Ylonen&#xff0c;SSH 协议的设计者&#xff09;未曾诉说的故事。 将 SSH 协议端口号设为 22 的故事 1995 年春我编写了 SSH 协议的最…

E2E工程问题:小周期转大周期Gateway

摘要&#xff1a; 本文讨论一个具体的工程问题&#xff0c;E2E报文对应的信号&#xff0c;由小周期转大周期导致的E2E校验失败问题。 工程中&#xff0c;网关节点很重要的一个功能就是路由。当然&#xff0c;E2E&#xff08;End to End&#xff09;报文也可路由&#xff0c;但…

ChatGPT在智能推送和个性化广告中的应用如何?

ChatGPT在智能推送和个性化广告领域具有广泛的应用潜力。智能推送和个性化广告是指根据用户的个性化需求和兴趣&#xff0c;精准地向用户推送相关的信息和广告内容。ChatGPT作为一种预训练的通用语言模型&#xff0c;具有强大的语言理解和生成能力&#xff0c;可以在智能推送和…

【spring boot】spring boot下代码运行逻辑

概括 springboot是一种java开发框架&#xff0c;采用注解开发形式&#xff0c;大大简化了SSM框架下的大量配置&#xff0c; 目前springboot想要实现一个功能&#xff0c;一般通用方式是建立这几个包&#xff1a;controller&#xff0c;config&#xff0c;mapper&#xff0c;e…

5分钟开发一个AI论文抓取和ChatGPT提炼应用

5分钟开发一个AI论文抓取和ChatGPT提炼应用 第一步 点击“即刻开始” -选择模板 python -修改标题 “AIPaper”&#xff0c;项目标识“AIPaper”&#xff0c;点击“创建项目” 第二步 在编程区域右侧AI区域&#xff0c;输入框输入以下内容&#xff1a; 请根据下面的内容&…

Java后端程序员不得不知道的 API 接口常识

说实话&#xff0c;我非常希望自己能早点看到本篇文章&#xff0c;大学那个时候懵懵懂懂&#xff0c;跟着网上的免费教程做了一个购物商城就屁颠屁颠往简历上写。 至今我仍清晰地记得&#xff0c;那个电商教程是怎么定义接口的&#xff1a; 管它是增加、修改、删除、带参查询&…

ELK 使用kibana查询和分析nginx日志

背景&#xff1a;使用kibana查询和分析nginx请求日志&#xff0c;方便开发人员查询系统日志和分析系统问题。 setp 1、定义Index patterns 2、定义Discover(Search 查询数据) 3、定义Visualizations 3.1 定义Vertical Bar 3.2 、Choose a source 3.3、定义图表 4、定义…

【一文搞懂】—带霍尔编码器的直流有刷减速电机

文章目录 一、直流有刷电机二、减速比三、霍尔编码器3.1 霍尔编码器3.2 霍尔编码器测速原理 四、测速程序设计4.1 跳变沿检测4.2 计算转速 一、直流有刷电机 宏观上说直流有刷电机由固定部分&#xff08;定子&#xff09;和旋转部分&#xff08;转子&#xff09;组成。在定子上…

同一份数据,Redis为什么要存两次

Redis作为目前最主流的高性能缓存&#xff0c;里面有很多精妙的设计&#xff0c;其中有一种数据类型&#xff0c;当在存储的时候会同时采用两种数据结构来进行分别存储&#xff0c;那么 Redis 为什么要这么做呢&#xff1f;这么做会造成同一份数据占用两倍空间吗&#xff1f; …

Reinforcement Learning with Code 【Chapter 7. Temporal-Difference Learning】

Reinforcement Learning with Code This note records how the author begin to learn RL. Both theoretical understanding and code practice are presented. Many material are referenced such as ZhaoShiyu’s Mathematical Foundation of Reinforcement Learning, . 文章…

高层金属做power mesh如何避免via stack

随着工艺精进&#xff0c;pr要处理的层次也越来越多&#xff0c;如何选择power plan的层次尤为关键&#xff0c;一方面决定ir drop的大小&#xff0c;影响着芯片的功能&#xff0c;一方面决定绕线资源&#xff0c;影响面积。 选择高层metal做power mesh的关键在于厚金属&#…

局域网内主机ping不通,但是可以调用对方http接口(防火墙阻止了icmp协议)(关闭防火墙或者启用ICMP回显请求(ICMPv4-In))

文章目录 背景可能的原因问题排查及解决 背景 局域网内有一台主机&#xff0c;ping它ping不通&#xff0c;但是可以调用它的http接口&#xff0c;很诡异。。。 可能的原因 可能的原因有以下几种&#xff1a; 防火墙设置&#xff1a;局域网内的主机可能设置了防火墙&#xff…

勘探开发人工智能应用:地震层位解释

1 地震层位解释 层位解释是地震构造解释的重要内容&#xff0c;是根据目标层位的地震反射特征如振幅、相位、形态、连续性、特征组合等信息在地震数据体上进行追踪解释获得地震层位数据的方法。 1.1 地震信号、层位与断层 图1.1 所示为地震信号采集的过程&#xff0c;地面炮…

opencv-21 alpha 通道详解(应用于 图像增强,合成,蒙版,特效 等)

什么是alpha 通道&#xff1f; Alpha通道是计算机图形学中用于表示图像透明度的一种通道。在一个图像中&#xff0c;通常会有三个颜色通道&#xff1a;红色&#xff08;R&#xff09;、绿色&#xff08;G&#xff09;、蓝色&#xff08;B&#xff09;&#xff0c;它们合在一起…

macOS 源码编译 Percona XtraBackup

percona-xtrabackup-2.4.28.tar.gz安装依赖 ╰─➤ brew install cmake ╰─➤ cmake --version cmake version 3.27.0brew 安装 ╰─➤ brew update╰─➤ brew search xtrabackup > Formulae percona-xtrabackup╰─➤ brew install percona-xtrabackup╰─➤ xtr…

scrcpy2.0+实时将手机画面显示在屏幕上并用鼠标模拟点击2023.7.26

想要用AI代打手游&#xff0c;除了模拟器登录&#xff0c;也可以直接使用第三方工具Scrcpy&#xff0c;来自github&#xff0c;它是一个开源的屏幕镜像工具&#xff0c;可以在电脑上显示Android设备的画面&#xff0c;并支持使用鼠标进行交互。 目录 1. 下载安装2. scrcpy的高级…

最常见的设计模式(代码示例)

文章目录 为什么要学习设计模式单例模式哪些地方使用单例模式懒汉模式和饿汉模式的区别单例的特性饿汉模式与懒汉模式的区别 工厂模式Spring 工厂模式创建Bean为什么Spring IOC要使用工厂设计模式创建Bean呢各个工厂模式的区别简单工厂&#xff08;一个工厂生产不同的具体产品&…

Go语言开发小技巧易错点100例(八)

往期回顾&#xff1a; Go语言开发小技巧&易错点100例&#xff08;一&#xff09;Go语言开发小技巧&易错点100例&#xff08;二&#xff09;Go语言开发小技巧&易错点100例&#xff08;三&#xff09;Go语言开发小技巧&易错点100例&#xff08;四&#xff09;Go…

100多个常用快捷键

Ctrl A: 全选Ctrl C: 复制Ctrl V: 粘贴Ctrl X: 剪切Ctrl Z: 撤销Ctrl Y: 重做Ctrl S: 保存Ctrl P: 打印Ctrl F: 查找Ctrl H: 替换Ctrl G: 转到Ctrl N: 新建Ctrl O: 打开Ctrl W: 关闭窗口Ctrl Q: 退出程序Ctrl F1: 折叠/展开功能区Ctrl F5: 刷新Ctrl F7: 拼写…

【论文笔记】RCM-Fusion: Radar-Camera Multi-Level Fusion for 3D Object Detection

原文链接&#xff1a;https://arxiv.org/abs/2307.10249 1. 引言 目前的一些雷达-相机融合3D目标检测方法进行实例级的融合&#xff0c;从相机图像生成3D提案&#xff0c;并与雷达点云相关联以修正提案。但这种方法没有在最初阶段使用雷达&#xff0c;依赖于相机3D检测器&…