目录
1. 多态
1.1 多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。比如吃饭这件事,猫吃猫粮,狗吃狗粮,人吃饭。再比如与人沟通这件事,中国人说中文,美国人说英语、法国人说法语。
在上一篇文章中(【JavaSE】继承那些事儿) ,提到过一句话,继承是对共性的抽取,实现代码的复用。就是对子类来说,它既有父类中的属性和方法,又有自己不同于其他子类,独特的属性和方法。而对父类里的方法来说,虽是共性,但依旧可以因为子类的不同,出现五花八门的形式,这就是多态。可以说,多态是针对父类而言的。
1.2 多态实现条件
在 java 中要实现多态,必须要满足如下几个条件,缺一不可:
1. 必须在继承体系下
2. 子类必须要对父类中方法进行重写
3. 通过父类的引用调用重写的方法多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
1.3 向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型()
Animal animal = new Cat("元宝",2);
写这么一个动物类,及其两个子类 Cat 类和 Bird 类:
class Animal{
protected String name;
protected int age;
protected double weight;
public Animal() {
}
public Animal(String name, int age, double weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
public void eat(){
System.out.println(this.name+ " 在吃饭!");
}
public void sleep(){
System.out.println(this.name+" 在睡觉!");
}
}
class Cat extends Animal{
public Cat() {
}
public Cat(String name, int age, double weight) {
super(name, age, weight);
}
public void mew(){
System.out.println(this.name+" 喵~喵~喵~");
}
}
class Bird extends Animal{
public Bird() {
}
public Bird(String name, int age, double weight) {
super(name, age, weight);
}
public void fly(){
System.out.println(this.name+" 正在天空翱翔!");
}
}
那么该如何向上转型呢?有下面几种方式:
1.3.1 直接赋值
public class Text {
public static void main(String[] args) {
Animal animal1 = new Cat("Hello Kitty",1,7);
animal1.sleep();
animal1.eat();
//animal1.mew(); 编译报错
Animal animal2 = new Bird("AngryBird",3,5.5);
animal2.eat();
animal2.sleep();
}
}
按理说,等号两边的数据类型应该相同,否则会报错。但上述代码能运行的一个主要原因就是,两者有一个继承的关系。
1.3.2 方法传参
public class Text {
public static void function(Animal animal){
animal.eat();
animal.sleep();
}
public static void main(String[] args) {
Animal animal1 = new Cat("Hello Kitty",1,7);
function(animal1);
Animal animal2 = new Bird("AngryBird",3,5.5);
function(animal2);
}
}
1.3.3 方法返回
public class Text {
public static Animal function(String type){
if("鸟".equals(type)){
return new Bird("AngryBird",3,5.5);
}else{
return new Cat("Hello Kitty",1,7);
}
}
public static void main(String[] args) {
Animal animal1 = function("猫");
animal1.sleep();
animal1.eat();
Animal animal2 = function("鸟");
animal2.sleep();
animal2.eat();
}
}
向上转型导致的一个结果,就是父类的引用,是不能访问子类中父类所没有的属性及方法的,即子类特有的属性和方法。
1.3.4 向上转型的优缺点
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
1.4 重写
重写(override):也称为覆盖。重写是子类对父类非静态、非 private 修饰,非 final 修饰,非构造方法的实现过程进行重新编写。重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
1.4.1 重写的条件
1. 方法名相同
2. 形参数目、顺序相同
3. 返回值相同
再次利用上面动物类及两个子类的例子:
class Animal{
......
......
public void eat(){
System.out.println(this.name+ " 在吃饭!");
}
public void sleep(){
System.out.println(this.name+" 在睡觉!");
}
}
class Cat extends Animal{
......
......
public void eat(){
System.out.println(this.name + " 在吃主人给的小零食~");
}
public void sleep(){
System.out.println(this.name+" 正在狗窝里呼呼大睡");
}
}
class Bird extends Animal{
......
......
public void eat(){
System.out.println(this.name+ " 叼着一只肥美的大青虫");
}
public void sleep(){
System.out.println(this.name+" 站在枝桠上睡大觉,沐浴着月光,微风吹拂着");
}
}
重写之后,我们的IDEA会出现以下的图标:
我们可以利用注解 @Override 来检查重写的语法规则是否正确:
@Override
public void eat(int a){
System.out.println(this.name + " 在吃主人给的小零食~");
}
像这样的代码,@Override 那行的代码就会有警告。
同样也可以让编译器自动生成:右击 Generate -> Override Methods -> Animal 类中 -> eat()
1.4.2 重写注意事项
1. private 修饰的方法不能被重写
2. static 修饰的方法不能被重写
3. 子类重写方法的访问权限要大于等于父类被重写的方法
4. 被 final 修饰的方法不能被重写,此时这个方法被称作密封方法
5. 构造方法不能被重写
1.4.3 重载与重写的区别
区别 | 重写(override) | 重载(overload) |
参数列表 | 必须相同 | 必须不同 |
返回类型 | 必须相同【除非可以构成父子类关系】 | 可以不同 |
访问限定符 | 有一定的要求,见上文 | 可以不同 |
即:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
【重写的设计原则】
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容, 并且添加或者改动新的内容。例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的类,可能还在有用户使用,正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我 们当今的需求了。
1.5 通过父类的引用,调用这个父类和子类重写的方法
接下来,让我们来看看,向上转型 -> 重写 之后,再一次使用父类引用调用重写的方法会怎么样:
public class Text {
public static void main(String[] args) {
Animal animal1 = new Cat("Hello Kitty",1,7);
animal1.sleep();
animal1.eat();
Animal animal2 = new Bird("AngryBird",3,5.5);
animal2.sleep();
animal2.eat();
}
}
输出:
Hello Kitty 正在狗窝里呼呼大睡
Hello Kitty 在吃主人给的小零食~
AngryBird 站在枝桠上睡大觉,沐浴着月光,微风吹拂着
AngryBird 叼着一只肥美的大青虫
向上转型 -> 重写 -> 父类引用调用子类重写父类的方法,这三个过程合在一起,会发生 动态绑定:编译的时候,会调用父类的方法,但是在运行的时候,帮我们调用了子类重写的方法。
查看编译完成之后的字节码文件,可以佐证我的说法:
只是动态绑定会帮我们调用子类的重写方法。
动态绑定是多态的基础!
动态绑定与静态绑定的区别:
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
2. 多态的优缺点
为了说明多态的优点,利用以下画图形的例子来说明
class Shape{
void draw(){
System.out.println("画图");
}
}
class Rectangle extends Shape{
void draw(){
System.out.println(" ⬜");
}
}
class Circle extends Shape{
void draw(){
System.out.println(" ⚪");
}
}
class Triangle extends Shape{
void draw(){
System.out.println(" 🔺");
}
}
class Flowers extends Shape{
void draw(){
System.out.println("~❀~");
}
}
public class Text {
public static void main(String[] args) {
Rectangle rect = new Rectangle();
Circle cir = new Circle();
Triangle tri = new Triangle();
Flowers flower = new Flowers();
Shape[] shapes = {flower,rect,flower,cir,flower,tri,flower,tri,flower,cir,flower,rect,flower};
for(Shape shape : shapes){
shape.draw();
}
}
输出
~❀~
⬜
~❀~
⚪
~❀~
🔺
~❀~
🔺
~❀~
⚪
~❀~
⬜
~❀~
由上面画图形的例子,可以看到多态的优点:
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
什么叫 "圈复杂度" ? 圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙, 那么就比较简单容易理解。而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂。因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度"。如果一个方法的圈复杂度太高, 就需要考虑重构。不同公司对于代码的圈复杂度的规范不一样。一般不会超过 10 。
在上面画图形的例子中,如果我们使用 if-else 语句来实现,会是怎么样子的呢?
public class Text {
public static void main(String[] args) {
Rectangle rect = new Rectangle();
Triangle tri = new Triangle();
Circle cir = new Circle();
Flower flo = new Flower();
String[] shapes = {"flower","rectangle","flower","triangle","flower","circle","flower"};
for(String s : shapes){
if(s.equals("flower")){
flo.draw();
}else if(s.equals("rectangle")){
rect.draw();
}else if(s.equals("triangle")){
tri.draw();
}else{
cir.draw();
}
}
}
}
2. 可扩展能力更强,如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
比如我要扩展一个 ❤ 的图形:
class Love extends Shape{
@Override
void draw(){
System.out.println("♥");
}
}
对于类的调用者来说, 只要创建一个新类的实例就可以了, 改动成本很低。
而对于不用多态的情况, 就要把 if - else 进行一定的修改, 改动成本更高。
虽然是,多态还诸多有点,但还是存在缺点的:
1. 属性没有多态性,当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
2. 构造方法没有多态性
class B {
public B() {
// do nothing
func();// 以后不要这样写 !!
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
public D () {
super();
}
@Override
public void func() {
//System.out.println("fafdadssa!!!!!");
System.out.println("D.func() " + num+" 因为父类此时还没有走完!");
}
}
public class Test4 {
public static void main(String[] args) {
D d = new D();
}
}
输出
D.func() 0 因为父类此时还没有走完!
构造 D 对象的同时, 会调用 B 的构造方法。 B 的构造方法中调用了 func 方法, 此时会触发动态绑定,会调用到 D 中的 func 此时 D 对象自身还没有构造,此时 num 处在未初始化的状态, 值为 0。如果具备多态性,num 的值应该是1。所以在构造函数内,尽量避免使用实例方法,除了 final 和private 方法。
结论:
"用尽量简单的方式使对象进入可工作状态",尽量不要在构造器中调用方法(如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成),可能会出现一些隐藏的但是又极难发现的问题。