一、方法重写
简单来讲,方法重写就是一个子类有一个方法,和父类的某个方法的名称,返回类型,参数一样,那么我们就说子类的这个方法覆盖了父类的那个方法。比如说:
class Animal {
public void makeSound() {
System.out.println("一些奇怪的声音");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪");
}
}
public class Test {
public static void main(String[] args) {
Animal myAnimal = new Dog();
myAnimal.makeSound(); // 输出 "汪汪汪"
}
}
Dog类重写了Animal类的makeSound方法。当我们创建一个Dog对象并将其赋值给Animal类型的引用时,调用makeSound方法将输出"汪汪汪",这表明调用的是Dog类的makeSound方法,体现了多态性。注意,@Override注解是可选的,但它可以帮助编译器检查方法签名是否正确,确保方法确实被重写。
注意,方法重写也叫方法覆盖需要满足以下条件:
- 子类的方法的参数和方法名称必须和父类的完全一样。
- 子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类。
- 子类方法不能缩小父类方法的访问权限。
名称 | 发生范围 | 方法名 | 形参列表 | 返回类型 | 修饰符 |
重载(voerload) | 本类 | 必须一样 | 类型、个数或者顺序至少有一个不同 | 无要求 | 无要求 |
重写(override) | 父子类 | 必须一样 | 完全相同 | 子类重写方法必须是父类方法的子类或者相同 | 子类不能缩小父类的访问范围 |
二、多态
2.1什么是多态
多态是面向对象编程中的一个重要概念,它允许不同类型的对象对同一方法进行不同的实现。具体来说,多态性指的是通过父类的引用变量来引用子类的对象,从而实现对不同对象的统一操作。看这个例子:
// 定义一个基类 Animal
abstract class Animal {
// 抽象方法 makeSound
public abstract void makeSound();
}
// Dog 类继承自 Animal
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪!");
}
}
// Cat 类也继承自 Animal
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵~");
}
}
public class first {
public static void main(String[] args) {
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.makeSound();
a2.makeSound();
// 输出:
// 汪汪!
// 喵喵~
}
}
2.2对象的多态
- 一个对象的编译类型和运行类型可以不同。
- 编译类型在定义对象时就确定了,不能改变。
- 运行类型是可以变换的。
- 初次实例化一个对象时,=左边为编译类型,=右边为运行类型。
public class first {
public static void main(String[] args) {
Animal a1 = new Dog();//a1的编译类型为Animal,运行类型为Dog
Animal a2 = new Cat();//a2的编译类型为Animal,运行类型为Cat
a1 = a2;//a1的运行类型变为Cat
}
}
2.3多态的条件
- 存在继承关系的类之间才能够使用多态性。多态性通常通过一个父类用变量引用子类对象来实现
- 子类必须重写(Override)父类的方法。通过在子类中重新定义和实现父类的方法,可以根据子类的特点行为改变这个方法的行为,如猫和狗吃东西的独特行为。
- 使用父类的引用变量来引用子类对象。这样可以实现对不同类型的对象的统一操作,而具体调用哪个子类的方法会在运行时多态决定。
关于不能调用子类的特有成员这一点是因为在能够调用那些成员是在编译阶段由编译器根据编译类型决定的。最终运行时,要依据于子类的具体实现。
2.4多态的细节
- 属性没有重写的说法,属性的值看编译类型。
- instanceOf操作符,用于判断对象的类型是否为XX类型或XX类型的子类型。
三、向上转型
3.1什么是向上转型
向上转型是指将一个子类的对象引用赋值给其父类类型的引用变量。在向上转型中,子类对象可以被视为父类对象,可以使用父类类型的引用变量来引用子类对象。这样做的好处是可以以统一的方式处理不同类型的对象。
父类类型 引用名 = new 子类类型();
编译类型看左边,运行类型看右边。通过这个引用名,可以调用父类所有成员(需要遵守访问权限),不能调用子类中的特有成员。
3.2向上转型的规则
-
子类对象可以隐式地转型为父类对象,不需要任何显式的类型转换操作。
-
父类引用变量可以引用子类对象,但通过父类引用变量只能访问到子类对象中定义的父类成员,无法访问子类独有的成员。
-
子类对象中重写的方法,在通过父类引用变量调用时,会调用子类中的实现(动态绑定)。
-
向上转型是安全的操作,因为子类对象本身就是一个父类对象。
class Animal {
public void eat() {
System.out.println("Animal is eating.");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog is eating.");
}
public void bark() {
System.out.println("Dog is barking.");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // 向上转型
animal.eat(); // 调用的是 Dog 类中的 eat() 方法
// animal.bark(); // 错误:无法访问 Dog 类中独有的方法
Dog dog = (Dog) animal; // 向下转型
dog.bark(); // 调用 Dog 类中的 bark() 方法
}
}
四、向下转型
4.1什么是向下转型
向下转型(Downcasting)是指将一个父类类型的引用变量转换为其子类类型的引用变量。它与向上转型相反,需要进行显式的类型转换操作。
在某些情况下,当一个对象被向上转型后,它的具体类型信息会丢失,只保留了父类类型的信息。如果我们需要访问子类中特有的成员或调用子类重写的方法,就需要使用向下转型。
子类类型 引用名 = (子类类型) 父类引用;
4.2向下转型的规则
- 只能强转父类的引用,不能强转父类的对象。
- 要求当前被强转的父类的引用必须指向的是当前目标类型的对象。
- 可以调用子类类型中所有的成员。
class Animal {
public void eat() {
System.out.println("Animal is eating.");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog is eating.");
}
public void bark() {
System.out.println("Dog is barking.");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog(); // 向上转型
// 使用向下转型之前,需要先检查对象是否实际上是子类的实例
if (animal instanceof Dog) {
Dog dog = (Dog) animal; // 向下转型
dog.bark(); // 调用 Dog 类中的 bark() 方法
} else {
System.out.println("animal is not an instance of Dog");
}
}
}
五、动态绑定
5.1什么是动态绑定
当一个方法被声明为非静态且非私有时,在继承结构中,如果子类重写了该方法,则具体执行哪个版本的方法是在运行时确定的。这与静态绑定不同,静态绑定(编译时绑定)发生在编译阶段,此时确定了方法的具体实现。这种机制是多态的核心,它使得基类的引用能够根据实际对象类型调用相应的方法实现。
5.2Java的动态绑定机制
- 当调用对象方法时,该方法会和该对象的运行类型绑定。
- 当调用对象属性时,没有动态类型绑定,哪里声明,那里使用。
看这个例子:
class A{
public int i = 10;
public int sum(){
return get() + 5;
}
public int get(){
return i;
}
public int sum1(){
return i + 5;
}
}
class B extends A{
public int i = 20;
public int sum(){
return get()+ 20;
}
public int sum1(){
return i + 10;
}
public int get(){
return i;
}
}
public class first{
public static void main(String[] args) {
A a = new B();
System.out.println(a.sum());//40
System.out.println(a.sum1());//30
}
}
在这段代码中,a.sum()指令,直接调用了B类中的sum方法,在sum方法调用了B类的get方法,所以结果是40。如果将B类的sum方法注释掉,会输出什么呢?
由于B类没有对应的sum方法,会根据继承的规则查找A类是否有sum方法。这里有一个问题,A类的sum方法调用了get方法,但是A类和B类都有get方法,这里应该调用哪一个get方法呢?根据动态绑定规则:当调用对象方法时,该方法会和该对象的运行类型绑定。所以应该优先调用其绑定类型即B类中的get方法,又根据动态绑定规则:当调用对象属性时,没有动态类型绑定,哪里声明,那里使用。所以返回的是B类的i值,输出应该为20+5 = 25。
如果将B类中的get方法也注释掉,这时应当根据继承的规则去A类中查找get方法,但是这个A类的get方法应该返回A的i,所以输出应该为10+5 = 15。
如果不注释掉B类中的get方法,转而注释掉B类的i,答案也会是10+5 = 15。
六、多态数组
多态数组是指数组中的元素可以是某个基类的实例,也可以是该基类的任何子类的实例。通常情况是数组类型定义为父类类型,而里面保存的实际元素类型为子类类型。实际达到的效果就是父类引用指向子类对象。
// 基类 Shape
abstract class Shape {
public abstract void draw();
}
// 子类 Circle 继承自 Shape
class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle.");
}
}
// 子类 Rectangle 继承自 Shape
class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle.");
}
}
public class PolymorphicArrayExample {
public static void main(String[] args) {
// 创建一个 Shape 类型的数组
Shape[] shapes = new Shape[2];
// 向上转型:将 Circle 和 Rectangle 的实例放入数组
shapes[0] = new Circle();
shapes[1] = new Rectangle();
// 遍历数组并调用 draw 方法
for (Shape shape : shapes) {
shape.draw(); // 动态绑定,根据实际对象类型调用相应的方法
}
// 输出:
// Drawing a circle.
// Drawing a rectangle.
}
}
七、多态参数
多态参数是指在方法或构造函数中使用一个基类类型作为形参,但实际传递给该方法或构造函数的对象可以是这个基类的任何子类实例,每传入一个子类对象,都相当于形成了一次多态。
// 基类 Shape
abstract class Shape {
public abstract void draw();
}
// 子类 Circle 继承自 Shape
class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle.");
}
}
// 子类 Rectangle 继承自 Shape
class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle.");
}
}
public class PolymorphicParameterExample {
// 方法接受一个 Shape 类型的参数
public static void display(Shape shape) {
shape.draw(); // 动态绑定,根据实际对象类型调用相应的方法
}
public static void main(String[] args) {
// 创建 Circle 和 Rectangle 的实例
Circle circle = new Circle();
Rectangle rectangle = new Rectangle();
// 传递不同类型的对象给 display 方法
display(circle); // 输出: Drawing a circle.
display(rectangle); // 输出: Drawing a rectangle.
}
}