多态

!!! note 目录

多态

一、类型转换

1.1、基本数据类型转换

在Java中,基本数据类型之间可以进行自动类型转换和强制类型转换。

  1. 自动类型转换(隐式类型转换):当一个表达式中包含不同类型的数据时,系统会自动将其中的低精度数据类型转换为高精度数据类型,以保证精度不丢失。

    自动类型转换的规则如下:

    • byte、short、char类型会自动提升为int类型。
    • 如果表达式中包含了不同类型的数据,系统会自动将低精度的类型转换为高精度的类型。

    示例:

    1
    2
    int x = 10;
    double y = x; // 自动将int类型转换为double类型
  2. 强制类型转换(显式类型转换):在某些情况下,需要将一个数据类型转换为另一个数据类型,这时就需要使用强制类型转换。强制类型转换可以通过将目标类型的数据类型放在被转换的数据类型前面的括号中实现。

    强制类型转换的规则如下:

    • 数据类型范围大的可以强制转换为数据类型范围小的,但可能会导致精度丢失或溢出。
    • 强制类型转换可能会造成数据丢失或溢出,因此需要谨慎使用。

    示例:

    1
    2
    double a = 10.5;
    int b = (int) a; // 强制将double类型转换为int类型

1.2、Java中的向上转型和向下转型

在Java中,向上转型(Upcasting)和向下转型(Downcasting)是面向对象编程中常用的概念,用于处理类之间的继承关系

  1. 向上转型(Upcasting):向上转型是指将子类对象赋值给父类引用变量的过程。这样做是安全的,因为子类对象拥有父类的所有属性和方法。向上转型可以实现多态性,使得代码更加灵活。例如:

    1
    2
    3
    4
    class Animal { }
    class Dog extends Animal { }

    Animal animal = new Dog(); // 向上转型
  2. 向下转型(Downcasting):向下转型是指将父类引用变量转换为子类对象的过程。这种转型可能会导致异常,因为编译器只知道变量的编译时类型,而不知道实际的运行时类型。因此,在进行向下转型时,需要使用强制类型转换,并且需要确保转换是安全的,即实际对象是子类的实例。否则,会抛出 ClassCastException 异常。例如:

    1
    2
    Animal animal = new Dog(); // 向上转型
    Dog dog = (Dog) animal; // 向下转型

需要注意的是,向上转型是自动的,不需要显式地指定类型转换,而向下转型需要显式地使用强制类型转换,并且可能会导致异常,因此需要谨慎使用。

二、多态

  1. 父类型引用指向子类型对象。 Animal a=new Cat(); a.move();
  2. 程序分为编译阶段和运行阶段
    • 编译阶段:编译器只知道a是Animal类型,因此去Animal类找move()方法,找到后,绑定成功,编译通过。这个过程通常被称为静态绑定。
    • 运行阶段:运行时和JVM堆内存中真实的Java对象有关,所以运行时会自动调用真实对象move()方法。这个过程通常被称为动态绑定。
  3. 多态是指:多种形态,编译阶段一种形态,运行阶段另一种形态。
1
2
3
4
5
6
7
8
9
10
11
12
13
//父类
package com.camellia.oop19;

public class Animal {

public void move(){
System.out.println("动物在移动");
}

public void eat(){
System.out.println("正在吃东西");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Bird 子类
package com.camellia.oop19;

public class Bird extends Animal{

@Override
public void move() {
System.out.println("鸟儿在飞翔");
}

/**
* 这个方法也是子类特有的。
*/
public void sing(){
System.out.println("鸟儿在歌唱!");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Cat 子类
package com.camellia.oop19;

public class Cat extends Animal{

@Override
public void move() {
System.out.println("猫在走猫步");
}

/**
* 这个方法/行为是子类特有的。父类没有。
*/
public void catchMouse(){
System.out.println("猫在抓老鼠");
}
}

2.1、发生在向上转型时的多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.camellia.oop19;

public class Test01 {
public static void main(String[] args) {

Animal a2 = new Cat();

// java程序包括两个重要的阶段:
// 第一阶段:编译阶段
// 在编译的时候,编译器只知道a2的类型是Animal类型。
// 因此在编译的时候就会去Animal类中找move()方法。
// 找到之后,绑定上去,此时发生静态绑定。能够绑定
// 成功,表示编译通过。
// 第二阶段:运行阶段
// 在运行的时候,堆内存中真实的java对象是Cat类型。
// 所以move()的行为一定是Cat对象发生的。
// 因此运行的时候就会自动调用Cat对象的move()方法。
// 这种绑定称为运行期绑定/动态绑定。
//
// 因为编译阶段是一种形态,运行的时候是另一种形态。因此得名:多态。

a2.move();


// 以下代码是编译错误,因为编译器只知道a2是Animal类型,去Animal类中找
// catchMouse()方法了,结果没有找到,无法完成静态绑定,编译报错。

a2.catchMouse();
}
}

2.2、发生在向下转型时的多态

注意: 向下转型使用不当容易发生类型转换异常:ClassCastExcetion。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.camellia.oop19;

public class Test01 {
public static void main(String[] args) {

Animal a2 = new Cat();

// 假如现在就是要让a2去抓老鼠,怎么办?
// 向下转型:downcasting(父--->子)
// 什么时候我们会考虑使用向下转型?
// 当调用的方法是子类中特有的方法。

Cat c2 = (Cat) a2;
c2.catchMouse();

// 多态
Animal x = new Cat();
// 向下转型
Bird y = (Bird) x;

// 为什么编译的时候可以通过?
// 因为x是Animal类型,Animal和Bird之间存在继承关系,语法没问题,所以编译通过了。
// 为什么运行的时候出现ClassCastException(类型转换异常)?
// 因为运行时堆中真实对象是Cat对象,Cat无法转换成Bird,则出现类型转换异常。
// 为什么向下转型容易出问题?
// 因为向下转型将父类x转换成子类y,原则上没问题,子类的都继承了父类的方法,但是x真实指向的堆中不一定是父类。
}
}

2.3、用instanceof运算符避免向下转型时的风险

instanceof 运算符用于检查对象是否是特定类的实例,或者是否是特定类的子类的实例。它的语法如下:

1
object instanceof ClassName

其中 object 是要检查的对象,ClassName 是要检查的类名。

instanceof 运算符的返回结果是一个布尔值,如果 objectClassName 的一个实例或子类的实例,则返回 true;否则返回 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.camellia.oop19;

public class Test01 {
public static void main(String[] args) {

// 多态
Animal x = new Cat();
// 向下转型
Bird y = (Bird) x;

// instanceof运算符的出现,可以解决ClassCastException异常。


// instanceof 运算符的语法规则:
// 1. instanceof运算符的结果一定是:true/false
// 2. 语法格式:
// (引用 instanceof 类型)
// 3. 例如:
// (a instanceof Cat)
// true表示什么?
// a引用指向的对象是Cat类型。
// false表示什么?
// a引用指向的对象不是Cat类型。


// 做向下转型之前,为了避免ClassCastException的发生,一般建议使用instanceof进行判断
System.out.println(x instanceof Bird);

if (x instanceof Bird) {
System.out.println("=========================");
Bird y = (Bird) x;
}
}
}

2.3、多态有什么作用?

通过 instanceof 运算符可以在程序运行时动态确定对象的类型,根据不同的情况做出相应的处理。
这使得程序具有更强的适应性和灵活性,可以根据实际情况采取不同的行动,而不需要在编码时就确定对象的具体类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.camellia.oop19;

public class Test01 {
public static void main(String[] args) {
// 多态
Animal a = new Bird();
a.eat();

// 需求:程序运行阶段动态确定对象
// 如果对象是Cat,请抓老鼠。
// 如果对象是Bird,请唱歌。
if (a instanceof Cat) {
Cat cat = (Cat) a;
cat.catchMouse();
} else if (a instanceof Bird) {
Bird bird = (Bird) a;
bird.sing();
}
}
}

三、开闭原则(OCP)————使用多态实现OCP原则

开放-封闭原则(Open-Closed Principle,OCP):
软件实体(类、模块、函数等)应该对扩展开放对修改关闭。这意味着当需要改变系统的行为时,应该尽量通过扩展而不是修改现有的代码来实现。

3.1、 未使用多态

1
2
3
4
5
6
7
8
9
10
package com.camellia.oop20;

/**
* 宠物猫
*/
public class Cat {
public void eat(){
System.out.println("猫吃鱼");
}
}
1
2
3
4
5
6
7
package com.camellia.oop20;

public class Dog {
public void eat(){
System.out.println("狗狗啃骨头!");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.camellia.oop20;
/**
* 主人类
* 开始业务是喂猫,但是后面业务改变要喂狗了,在没有使用多态的情况下就必须要改变Master,不符合OCP。
*/
public class Master {
public void feed(Cat c) {
c.eat();
}

public void feed(Dog d){
d.eat();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//测试
package com.camellia.oop20;
/**
* 这个案例没有使用多态机制,看看设计上有什么缺陷?
* 不符合OCP。不符合开闭原则。(因为这个功能的扩展是建立在修改Master类的基础之上的。)
* OCP倡导的是什么?进行功能扩展的时候,最好不要修改原有代码,最好是以新增代码来完成扩展。
* 对修改关闭。对扩展开放。
*/
public class Test {
public static void main(String[] args) {
// 创建宠物猫对象
Cat c = new Cat();
Dog d = new Dog();
// 创建主人对象
Master master = new Master();
// 主人喂猫
master.feed(c);
master.feed(d);
}
}

3.2、使用多态

1
2
3
4
5
6
7
8
9
package com.camellia.oop21;
/**
* 宠物类
*/
public abstract class Pet {

public abstract void eat();

}
1
2
3
4
5
6
7
8
9
package com.camellia.oop21;

public class Cat extends Pet{

@Override
public void eat(){
System.out.println("猫吃鱼");
}
}
1
2
3
4
5
6
7
8
9
package com.camellia.oop21;

public class Dog extends Pet{

@Override
public void eat(){
System.out.println("狗狗在啃骨头");
}
}
1
2
3
4
5
6
7
8
package com.camellia.oop21;

public class Master {

public void feed(Pet p){ //这里就是用多态,父类引用指向子类,提高扩展性。
p.eat();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.camellia.oop21;

/**
* 还是主人喂养宠物的案例,使用多态机制,达到OCP原则。
*
* 能用多态尽量使用多态。尽量面向抽象编程。不要面向具体编程。
* 面向抽象编程的好处?降低耦合度,提高扩展力。
*/
public class Test {
public static void main(String[] args) {
// 创建宠物
Cat c = new Cat();
Dog d = new Dog();

// 创建主人
Master master = new Master();
master.feed(c);
master.feed(d);
}
}

3.3、静态方法不存在方法覆盖

1
2
3
4
5
6
7
8
9
package com.camellia.oop22;

public class Animal {

public static void test(){
System.out.println("Animal's test method invoke");
}

}
1
2
3
4
5
6
7
8
9
package com.camellia.oop22;

public class Cat extends Animal{

// 尝试去重写父类的静态方法
public static void test(){
System.out.println("Cat's test method invoke");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.camellia.oop22;

/**
* 方法覆盖针对的是实例方法。和静态方法无关。【方法的覆盖和多态机制联合起来才有意义。】
*/
public class Test {
public static void main(String[] args) {
Animal.test();
//test是静态方法、可以通过类名调用和实例无关。
Cat.test();

Animal a = new Cat();
a.test();
//虽然a指向的是Cat()对象,但是a是Animal类型的实例变量,所以此test是Animal的静态方法。
}
}

3.4、实例变量没有多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.camellia.oop22;

/**
* 方法覆盖针对的是实例方法。和实例变量没有关系。
*/
public class Test2 {
public static void main(String[] args) {
// 多态
A a = new B();
// 实例变量不存在覆盖这一说。
// a.name编译阶段绑定的是A类的name属性,运行的时候也会输出A类的name属性值。
System.out.println(a.name);

// 没有用多态
B b = new B();
System.out.println(b.name);
}
}

class A {
// 实例变量
String name = "张三";
}

class B extends A {
// 实例变量
String name = "李四";
}