Java面向对象之组合与多态

简介: 本篇文章是上一篇包和继承文章的后续篇,针对面向对象编程的组合、多态、抽象类与接口方面内容的总结分享,希望各位小主们认真浏览,一定会受益多多哟

1.组合


和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.

例如表示一个学校:


public class Student { 
 ... 
} 
public class Teacher { 
 ... 
} 
public class School { 
 public Student[] students; 
 public Teacher[] teachers; 
}


组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.其关系可解释为a part of

这是我们设计类的一种常用方式之一.

组合表示 has - a(a part of) 语义

在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师.

继承表示 is - a 语义

在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物。

大家要注意体会两种语义的区别


2.多态


多态—我们从字面上来理解就是一种事物的多种形态,理解的很到位,但这句话可不敢在面试官面前这么说(被打),那么它究竟该如何来解释呢,下面我们一起来看看!

要想理解多态还需分小部分内容理解,最后总结理解:


2.1向上转型


什么叫向上转型呢,看下这个图


微信图片_20230110161011.png


就是父类引用引用子类对象

代码实现:

先是两个类(子类与父类)


class Animal {
    public String name;
    public int age;
    public Animal(String name) {
        this.name=name;
    }
}
class Dog extends Animal {
    public Dog(String name){
        super(name);
    }
}

这是关键部分


public class TestDemo {
    public static void main(String[] args) {
        Animal animal = new Dog("hello");
    }//父类引用Animal引用实例化子类对象Dog
}


这就叫做向上转型,为什么叫向上转型呢


在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序猿会画一种 UML 图的方式来表

示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 “向上转型” , 表示往父类的方向转


向上转型发生的时机:


  • 直接赋值
  • 方法传参
  • 方法返回


直接赋值的方式我们已经演示了. 另外两种方式和直接赋值没有本质区别


方法传参:


 

public static void fun(Animal animal) {
    }
    public static void main(String[] args) {
        fun(new Dog("hello"));
    }


方法返回:


 

public static Animal fun1() {
        Dog dog=new Dog("hello");
        return dog;
    }


2.2动态绑定


当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?

对前面的代码稍加修改, 给 两个类加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的日志


class Animal {
    public String name;
    public Animal(String name) {
        this.name=name;
    }
    public void eat() {
        System.out.println(this.name+"吃");
    }
}
class Dog extends Animal {
    public Dog(String name){
        super(name);
    }
    public void eat() {
        System.out.println(this.name+"狼吞虎咽的吃");
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Animal animal1=new Animal("hi");
        animal1.eat();
        Animal animal2=new Dog("hello");
        animal2.eat();
    }
}


执行结果:


微信图片_20230110161252.png


此时, 我们发现:


animal1和animal2虽然都是 Animal 类型的引用, 但是 animal1指向 Animal 类型的实例,animal2指向Dog类型的实例.

针对 animal1和 animal2分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而

animal2.eat() 实际调用了子类的方法。


因此


在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) ,

要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为动态绑定(也叫运行时绑定)


动态绑定的发生条件:


  • 父类引用引用子类对象
  • 通过这个父类引用调用父类和子类同名覆盖的方法


2.3方法重写


针对刚才的 eat 方法来说:

子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为覆写/重写/覆盖(Override).


什么情况下发生重写:


1.方法名相同

2.参数列表相同(个数加类型)

3.返回值相同(也可以不同,如协变类型:子类中方法的返回值是父类中方法返回值的子类)

“public Animal fun();public Dog fun() return null;"

4.是父子类继承关系


注意点:


1.方法不可以是static

2.子类的访问修饰限定,要大于或等于父类的访问修饰

3.private方法不能重写

4.被final修饰的方法不能重写

5.如果方法没有重写,向上转型的引用只能调用父类的方法

6.通过父类引用只能访问父类自己的成员


小总结.重写和重载的区别(动态绑定和静态绑定的区别)


答:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。


2.4向下转型


向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象。

如下代码:实际上还是本类型引用本类型对象


class Animal {
    public String name;
    public Animal(String name) {
        this.name=name;
    }
    public void eat() {
        System.out.println(this.name+"吃");
    }
}
class Dog extends Animal {
    public Dog(String name){
        super(name);
    }
    public void eat() {
        System.out.println(this.name+"狼吞虎咽的吃");
    }
}
class Bird extends Animal {
    public Bird(String name){
        super(name);
    }
    public  void eat() {
        System.out.println(this.name+"小心翼翼的吃");
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Animal animal=new Bird("hi");
        Bird bird=(Bird)animal;//此处注意强制转化
        bird.eat();
    }
}


但如果像这样:将会发生类型转化异常,所以向下转型不安全,只是自己引用自己,并不多用


public static void main(String[] args) {
        Animal animal=new Dog("hi");
        Bird bird=(Bird)animal;//此处注意强制转化
        bird.eat();
 }
 //Exception in thread "main" java.lang.ClassCastException: com.bilibili.demo1.Dog cannot be cast to com.bilibili.demo1.Bird
  at com.bilibili.demo1.TestDemo.main(TestDemo.java:30)


所以会使用关键字instanceof可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.


public static void main(String[] args) {
        Animal animal=new Dog("hi");
        if(animal instanceof Bird) {
        Bird bird=(Bird)animal;//此处注意强制转化
        bird.eat();
        }
 }


2.5 在构造方法中调用重写的方法(一个坑)


在主函数中实例化构造dog对象需要先调用Animal的构造方法,而Animal的构造方法中又有重写的普通方法,此时会调用父类还是子类的方法呢?我们一起来看看:


class Animal {
    public String name;
    public Animal(String name) {
        this.name=name;
        eat();
    }
    public void eat() {
        System.out.println(this.name+"吃");
    }
}
class Dog extends Animal {
    public Dog(String name){
        super(name);
    }
    @Override
    public void eat() {
        System.out.println(this.name+"狼吞虎咽的吃");
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Dog dog = new Dog("hello");
    }
}
//运行结果:
hello狼吞虎咽的吃


说明也是调用了子类的方法,此时也发生了动态绑定


2.6 理解多态


有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了,我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况

代码示例: 打印多种形状


class Shape { 
 public void draw() { 
 // 啥都不用干
 } 
} 
class Cycle extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("○"); 
 } 
} 
class Rect extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("♦"); 
 } 
} 
class Flower extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("❀"); 
 } 
} 
/我是分割线// 
// Test.java 
public class Test { 
 public static void main(String[] args) { 
 Shape shape1 = new Flower(); 
 Shape shape2 = new Cycle(); 
 Shape shape3 = new Rect(); 
 drawMap(shape1); 
 drawMap(shape2); 
 drawMap(shape3); 
 } 
 // 打印单个图形
 public static void drawShape(Shape shape) { 
 shape.draw(); 
 } 
}



在这个代码中, 分割线上方的代码是类的实现者编写的, 分割线下方的代码是类的调用者编写的.

当类的调用者在编写drawMap这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为多态。


使用多态的好处是什么?


1) 类调用者对类的使用成本进一步降低.


封装是让类的调用者不需要知道类的实现细节.

多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可

因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低


2) 避免使用大量的 if - else


例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:


public static void drawShapes() { 
 Rect rect = new Rect(); 
 Cycle cycle = new Cycle(); 
 Flower flower = new Flower(); 
 String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"}; 
for (String shape : shapes) { 
 if (shape.equals("cycle")) { 
 cycle.draw(); 
 } else if (shape.equals("rect")) { 
 rect.draw(); 
 } else if (shape.equals("flower")) { 
 flower.draw(); 
 } 
 }
}


如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单


public static void drawShapes() { 
 // 我们创建了一个 Shape 对象的数组. 
 Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), 
 new Rect(), new Flower()}; 
 for (Shape shape : shapes) { 
 shape.draw(); 
 } 
}


3) 可扩展能力更强.


如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.


class Triangle extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("△"); 
 } 
}


对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.

而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.


相关文章
|
7天前
|
存储 安全 Java
Java面向对象最新超详细总结版!
Java面向对象最新超详细总结版!
25 7
Java面向对象最新超详细总结版!
|
15小时前
|
Java 编译器 ice
【Java开发指南 | 第二十六篇】Java多态
【Java开发指南 | 第二十六篇】Java多态
8 1
|
3天前
|
Java
【JAVA基础篇教学】第五篇:Java面向对象编程:类、对象、继承、多态
【JAVA基础篇教学】第五篇:Java面向对象编程:类、对象、继承、多态
|
4天前
|
Java
java面向对象——包+继承+多态(一)-2
java面向对象——包+继承+多态(一)
17 3
|
4天前
|
SQL Java 编译器
java面向对象——包+继承+多态(一)-1
java面向对象——包+继承+多态(一)
16 2
|
6天前
|
Java 编译器 C++
Java 多态
5月更文挑战第3天
|
14天前
|
存储 Java 开发工具
【Java探索之旅】用面向对象的思维构建程序世界
【Java探索之旅】用面向对象的思维构建程序世界
11 0
|
14天前
|
Java
java使用面向对象实现图书管理系统
java使用面向对象实现图书管理系统
|
15天前
|
Java
Java语言---面向对象的三大特征之继承
Java语言---面向对象的三大特征之继承
|
15天前
|
机器学习/深度学习 Java Python
Java面向对象知识体系---基础版
Java面向对象知识体系---基础版