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向上转型
什么叫向上转型呢,看下这个图
就是父类引用引用子类对象
代码实现:
先是两个类(子类与父类)
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(); } }
执行结果:
此时, 我们发现:
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 进行一定的修改, 改动成本更高.