一:多态
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个生活中的例子:
在生活中,我们每个人的声音都不是完全相同的,这是因为我们通过声带的振动和空气的共鸣来产生声音,但是不同的人振动产生的波又不同,所以我们发出的声音也就有所不同,这就是不同对象完成某个行为会有不同的结果,这就是多态
1.1方法的重写
重写(override):也称为覆盖。重写是子类对父类方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
重写的存在是为了实现面向对象编程中的多态性和继承机制。因为在生活中,我们不可能原封不动的继承父类,应该是取其精华,去其糟粕,所以对于好的那一部分,我们可以选择保留下来,对于不好的一部分,那么我们可以进行改正(也就是重写),这就是重写在现实生活中的意义
当子类继承自父类并且需要修改父类已经定义的方法时,可以使用重写(Override)来实现。重写的基本格式如下:
public class ParentClass { public void methodName() { // 父类方法的实现 } } public class ChildClass extends ParentClass { @Override public void methodName() { // 子类重写的方法实现 } }
在重写中,注意以下几点:
- 父类和子类的方法签名必须完全一致,包括方法名、参数列表和返回类型。
- 关键字
@Override
是注解,用于标记该方法是对父类方法的重写,它是可选的,但推荐使用。 - 子类重写后的方法权限要 ≥ 父类被重写方法的访问权限,例如,如果父类方法是
protected
访问修饰符,子类中可以使用protected
或public
访问修饰符进行重写。
下面是一个代码示例:
class Animal { public void makeSound() { System.out.println("动物发出声音"); } } class Cat extends Animal { @Override public void makeSound() { System.out.println("猫发出喵喵的声音"); } }
注意:
- 重写的前提是继承,只有当一个类继承了另一个类,并且这个类重新写了另一个类也存在的方法,这才叫重写,也叫覆盖
- 构造方法和静态方法可以重载,但是不能重写
- 被static,private,final修饰的方法都不能够重写
1.1.1构造方法和静态方法为什么不能被重写?
这是因为在面向对象的继承中,子类会继承父类的非私有成员(包括非静态成员和构造方法的实现),但不会继承父类的静态成员和构造方法的实现。这是由继承的特性所决定的。
因为对于构造方法来说,我们并没用必要让子类继承父类的构造方法,我们只需要通过super调用一次父类的构造方法即可
而静态成员和静态方法是属于类的,是这个类特有的,我们不能继承,如果继承了,那么这个类也会有这个属性,这就不好了
所以构造方法和静态方法虽然能够重载,但是不能够重写,因为重写的前提是继承,但是构造方法和静态方法连继承都实现不了,何谈重写呢?
1.1.2 为什么被private,final修饰的方法都不能够重写?
首先重写的前提是JVM需要找到父类中和子类中两个方法签名一样的方法,但是如果你的方法被private修饰了,那么JVM找不到你的方法,无法进行覆盖,自然也就构成不了重写了,JVM只会认为你在子类中定义了一个新的方法,仅此而已
被final修饰的方法不能重写,这在讲述final的三个作用的时候提到过,这里不过多解释
1.2重写和重载的区别
重载是一词多义,而重写是核心重写,进行覆盖,举个例子:
重写就是将之前的箭头进行改造,而重载则是一次发出多个箭头
区别点 | 重写(override) | 重载(overload) |
参数列表 | 不能修改 | 必须修改 |
返回类型 | 不能修改 | 不做要求 |
访问限定符 | 子类的权限要大于等于父类的权限 | 不做要求 |
1.3向上转型和向下转型
1.3.1向上转型
在Java中,向上转型是指将一个子类的实例赋值给其父类的引用变量。这样做的好处是可以实现多态,通过使用父类的引用来调用子类的方法。
首先,我们定义一个Apple
类:
public class Apple { public void eat() { System.out.println("吃苹果"); } }
接下来,我定义一个GreenApple
类,它继承了Apple
类:
public class GreenApple extends Apple { public void taste() { System.out.println("青苹果的味道爽口"); } }
现在,我们可以进行向上转型的演示。
向上转型是将一个子类的实例赋值给父类类型的引用变量。例如:
Apple apple = new GreenApple(); // 向上转型 apple.eat(); // 打印青苹果的味道爽口
在上面的示例中,我们将GreenApple
类的实例赋值给了Apple
类型的引用变量apple
。通过向上转型,我们可以调用父类中定义的方法,但不能调用子类中特有的方法。
在之前我们说过,引用的类型代表的是这个引用所能管理的空间,因为apple的类型是Apple,所以apple只能管理父类的属性和方法,而无法访问到子类的类型和方法,但是为什么我们最后打印了青苹果的味道爽口呢?原因是发生了动态绑定
1.3.2 动态绑定
在Java中,动态绑定是指在运行时确定要调用的方法,而不是在编译时确定。它是面向对象编程中的一个重要特性,通过继承和方法重写实现多态性。在理解动态绑定之前,首先需要了解静态绑定。
静态绑定是在编译时确定要调用的方法。当使用一个引用变量调用一个方法时,编译器根据引用变量的类型来决定要调用的方法。这种绑定发生在编译时期,因此也被称为前期绑定。
但是,当涉及到多态性时,静态绑定就无法满足需求了,这时就需要使用动态绑定。
动态绑定是通过运行时动态确定要调用的方法。当一个子类继承了父类并重写了其中的方法时,通过父类引用指向子类对象时,会根据实际的对象类型来决定调用哪个方法。具体的绑定在运行时发生,所以也被称为后期绑定。
动态绑定的原理是基于Java的多态性。多态性允许以一个父类的引用来引用子类对象,这样就可以在编译时期不确定具体的对象类型。在运行时,Java通过动态绑定机制,根据实际的对象类型来决定调用哪个方法。
也就是说,在我们通过父类引用调用子类对象的时候,虽然我们调用的是父类的方法,但是在运行的时候被绑定到子类中去了,所以运行的是子类的方法(父类和子类的方法要构成重写)
1.3.3向下转型
向下转型是不安全的,举个例子:对于一个青苹果来说,一个青苹果当然是苹果,但是苹果就一定是青苹果吗?
向上转型后,父类引用只能调用父类自己的属性和方法,并不能调用子类特有的属性和方法,但是在某些情况下我们确实需要使用子类的属性和方法,所以java就有了向下转型的概念
在Java中,向下转型是指将父类对象转换为子类对象的过程。具体来说,在继承关系中,当一个子类实例被赋值给一个父类引用时,可以使用向下转型将父类引用强制转换为子类引用,以便访问子类特有的成员和方法。下面是一个简单的代码示例:
class Apple { public void eat() { System.out.println("吃苹果"); } } class GreenApple extends Apple { public void sourTaste() { System.out.println("这个苹果有酸味"); } } public class Main { public static void main(String[] args) { Apple apple = new GreenApple(); // 向上转型 apple.eat(); // 调用父类方法 GreenApple greenApple = (GreenApple) apple; // 向下转型 greenApple.sourTaste(); // 调用子类特有方法 } }
这段代码的输出结果是:
吃苹果
苹果有酸味
我们通过向下转型,成功调用了父类的方法,这就是向下转型的作用,但是向下转型有着强者类型转换,在使用的时候非常不安全,使用者使用的时候必须要谨慎
1.3.4 关键字instanceof
instanceof 是 Java 中的关键字,用于判断一个对象是否属于某个类或其子类的实例。它的用法非常简单,语法如下:
object instanceof class
其中,object 为要检查的对象,class 为要检查的类。
instanceof 运算符的运算结果是一个布尔值,如果 object 是 class 类的实例或者是 class 类的子类的实例,则返回 true,否则返回 false。
所以我们可以通过instanceof关键字来保证向下转型的安全性,下面通过代码示例来讲解:
class Animal { public void sound() { System.out.println("Making sound..."); } } class Dog extends Animal { public void sound() { System.out.println("Barking..."); } public void fetch() { System.out.println("Fetching ball..."); } } class Cat extends Animal { public void sound() { System.out.println("Meowing..."); } public void climb() { System.out.println("Climbing tree..."); } } public class Main { public static void main(String[] args) { Animal animal1 = new Dog(); Animal animal2 = new Cat(); // 向下转型前的类型检查 if (animal1 instanceof Dog) { Dog dog = (Dog) animal1; dog.sound(); // 输出:Barking... dog.fetch(); // 输出:Fetching ball... } // 向下转型前的类型检查 if (animal2 instanceof Cat) { Cat cat = (Cat) animal2; cat.sound(); // 输出:Meowing... cat.climb(); // 输出:Climbing tree... } } }
animal2 instanceof Cat就是用于判断animal2指向的空间是否是cat类型或者cat子类类型,如果是返回True
1.4多态
在java中要实现多态,有两个条件
- 发生重写
- 向上转型
在Java中,多态是面向对象编程的一个重要概念,它允许使用父类类型的引用变量来引用子类类型的对象,并根据实际对象的类型来调用相应的方法。
以下是一个简单的代码示例来说明多态的概念:
class Animal { public void sound() { System.out.println("动物发出声音"); } } class Dog extends Animal { @Override public void sound() { System.out.println("狗发出汪汪声"); } } class Cat extends Animal { @Override public void sound() { System.out.println("猫发出喵喵声"); } } public class PolymorphismExample { public static void main(String[] args) { Animal animal1 = new Dog(); Animal animal2 = new Cat(); animal1.sound(); // 输出:狗发出汪汪声 animal2.sound(); // 输出:猫发出喵喵声 } }
在上面的代码中,Animal是一个父类,它有一个sound()方法。Dog和Cat都是Animal的子类,它们分别重写了sound()方法。
在main()方法里,我们创建了一个Dog对象并将其赋值给一个Animal类型的引用变量animal1。同样地,我们也创建了一个Cat对象并将其赋值给另一个Animal类型的引用变量animal2。
当我们调用animal1.sound()时,由于animal1指向一个Dog对象,所以会调用Dog类中重写的sound()方法,输出"狗发出汪汪声"。
同样地,当我们调用animal2.sound()时,由于animal2指向一个Cat对象,所以会调用Cat类中重写的sound()方法,输出"猫发出喵喵声"。
通过使用父类类型的引用变量来引用子类类型的对象,我们可以灵活地调用子类中覆盖了的方法,这就是多态的体现。多态性让我们的代码更灵活和可扩展,使得我们可以根据需要动态地选择调用合适的对象方法。
1.5避免在构造方法中调用重写的方法
在构造方法中调用重写的方法是一个需要谨慎处理的问题。这是因为构造方法是用于初始化对象的特殊方法,在这个阶段,对象还没有完全初始化完成。下面我会详细解释为什么要避免这样做,并通过一个简单的代码示例来说明。
- 对象未完全初始化:当我们创建一个对象时,构造方法会被调用进行初始化。在构造方法中调用重写的方法可能会导致对象处于一个未完全初始化的状态。这可能导致对象在调用其他方法时出现意料之外的行为或错误。
- 动态绑定的影响:在构造方法中调用的方法可能会被子类重写,并且根据实际运行时类型进行动态绑定。在构造方法期间,对象的实际类型可能是它的子类类型,这可能导致动态绑定产生意外的行为。
下面是一个示例代码来说明这个问题:
class Parent { Parent() { System.out.println("Parent构造方法"); printMessage(); // 调用重写方法 } void printMessage() { System.out.println("Parent的printMessage方法"); } } class Child extends Parent { private String message = "Child的message"; Child() { System.out.println("Child构造方法"); } @Override void printMessage() { System.out.println(message); } } public class Main { public static void main(String[] args) { Child child = new Child(); } }
当我们运行上述代码时,将会打印出以下内容:
Parent构造方法 null Child构造方法
让我们来详细解释每个输出的含义:
- 首先,我们创建了一个名为
Child
的对象,并调用它的构造方法。由于Child
类继承自Parent
类,因此在创建Child
对象时,会首先执行Parent
类的构造方法。在执行Parent
类的构造方法之前,会先执行其成员变量的初始化,也就是message
变量。 - 在
Parent
类的构造方法中,我们首先打印了Parent构造方法
,然后调用了printMessage()
方法。由于printMessage()
方法被Child
类重写了,因此实际上会调用Child
类中的printMessage()
方法。然而,在这一刻,Child
类的message
变量尚未被初始化,因此它的值为null
。 - 接下来,控制流转到
Child
类的构造方法。我们打印了Child构造方法
,该方法执行完毕后,程序结束。
当我们在构造方法中调用重写方法时,实际上是在动态绑定的影响下发生的。在父类的构造方法中调用的方法,会根据实际创建的子类对象来确定最终要调用的方法。在我们的示例中,由于Child
对象还未完全初始化,导致其成员变量message
的值为null
。因此,在执行Child
类中的printMessage()
方法时,打印的message
值为null
。所以我们要避免在构造方法中调用重写的方法
在这里我再解释一下为什么这段代码会发生动态绑定:
因为在父类类构造方法中调用重名方法时,此时子类对象已经存在,所以方法调用会委托给子类对象,因此我们在调用printMessage()方法的时候,所以实际上会调用Child类中的printMessage()方法。