Bootstrap

Java初学-多态

Java初学6.6.1

多态可以提高代码的复用性,有利于代码的维护。

多态或对象具有多种形态,是面向对象的三大特征之一,多态是建立在封装和继承的基础上的。

重载与重写就体现了多态。-方法的多态

对象的多态:

  1. 一个对象的编译类型和运行类型可以不一致。

    在Java中,一个对象的“编译类型”和“运行类型”可以不一致,这是Java语言支持动态绑定(也被称为晚绑定或运行时多态)的一个重要特性。理解这两者的区别对于掌握面向对象编程中的继承和多态概念至关重要。

编译类型:

编译类型指的是在编译时,变量或方法引用所声明的类型。它是编译器在编译代码时所依据的类型信息。编译类型决定了可以调用哪些方法和属性,以及可以执行哪些操作。例如,声明了一个变量为 Parent 类型:

Parent obj = new Child();//如果 Child 类是 Parent 类的子类,那么可以将一个 Child 对象的引用赋给一个类型为 Parent 的引用变量。这是Java多态性的一种体现,即一个引用变量可以指向其自身类或任何子类的实例。

这里,obj 的编译类型是 Parent。obj是引用变量。

运行类型:

运行类型指的是对象实际的类型,也就是创建对象的那个类的类型。在上面的例子中,尽管 obj 的编译类型是 Parent,但是它的运行类型实际上是 Child,因为 obj 引用的对象是由 Child 类创建的。-谁创建的对象,运行类型就是谁

动态绑定与多态

Java 中的动态绑定机制使得在运行时,JVM 能够确定调用哪个方法的正确版本。这意味着,即使一个变量的编译类型是父类,如果它引用的是子类对象,那么在运行时,将调用子类中重写的方法。例如:

Parent obj = new Child();

obj.method(); // 如果Child重写了method,将调用Child的method()方法。

在运行时,JVM 会检查 obj 引用的实际对象类型(运行类型),如果 Child 类重写了 method(),那么 obj.method() 调用的就是 Child 类中的 method() 方法,而不是 Parent 类中的 method() 方法。

这种特性使得Java能够实现动态多态,允许我们编写更加灵活和可扩展的代码。例如,我们可以编写一个方法,接受一个父类类型的参数,但实际上可以接受任何继承自该父类的子类对象,从而实现对不同子类对象的统一处理。

总之,编译类型和运行类型之间的差异是Java多态性的基础,它允许我们在不知道具体子类的情况下,编写出能够处理多种不同类型对象的通用代码。

2.编译类型在定义对象时,确定后不能改变。

在Java中,“编译类型在定义对象时,确定后不能改变”这句话意味着当创建一个对象时,所使用的引用变量的类型(即编译类型)在编译时就被确定下来,并且这个类型在整个程序的执行过程中不会改变。Java是一种静态类型语言,这意味着类型检查在编译阶段完成,而不是在运行时。比如:

Dog myDog = new Dog();

这里,myDog 的编译类型就是 Dog 类。这意味着在编译阶段,编译器就知道 myDog 将引用一个 Dog 对象,因此它可以检查所有对 myDog 的操作是否符合 Dog 类的方法和属性。例如,可以调用 myDog.bark(),如果 bark() 方法是 Dog 类的一部分。

一旦 myDog 的类型被确定,就不能将其重新声明为另一个类的类型,比如 Cat。下面的代码会导致编译错误:

Dog myDog = new Dog();

myDog = new Cat(); // 编译错误:无法将类型 'Cat' 赋予类型 'Dog'

然而,如果 Cat 和 Dog 都继承自同一个父类,比如 Animal,那么可以将引用类型改为父类,这样就可以接受任何子类的对象:

Animal myPet = new Dog();

myPet = new Cat(); // 这是合法的,因为 'Cat' 和 'Dog' 都是 'Animal'

但是,即使在这种情况下,myPet 的编译类型仍然是 Animal,并且你只能通过 Animal 类型的方法和属性来访问它。这就是为什么在Java中,编译类型在定义对象时确定,并且之后不能改变。

3.引用变量的编译类型一旦声明就无法改变,但是它所指向的对象的运行类型可以在程序运行过程中改变。

在Java中,一个对象的运行时类型在其生命周期内是不会改变的。一旦一个对象被创建,它的类(也就是运行时类型)就被确定下来了,这个类不会在之后的代码执行过程中发生变化。

当我们说“运行时类型”,我们指的是对象实际所属的类。例如:

Parent obj = new Child();

这里的 obj 的运行时类型是 Child,即使它的编译类型是 Parent。这意味着如果 Child 类重写了 Parent 类中的任何方法,那么通过 obj 调用这些方法时,实际上调用的是 Child 类的实现,这是Java多态性的体现。

然而,需要注意的是,虽然对象的运行时类型本身不会改变,但是引用该对象的引用变量(例如obj)可以被重新赋值为指向其他对象的引用,只要那些对象是兼容类型即可。例如:

Animal myPet = new Dog();

myPet = new Cat();

Parent obj1 = new Child();

Parent obj2 = new AnotherChild();

obj1 = obj2; // 如果 AnotherChild 也是 Parent 的子类,那么这句话合法。

在这个例子中,obj1 的运行时类型最初是 Child,但在 obj1 = obj2; 这一行之后,obj1 现在指向一个 AnotherChild 类型的对象,但 obj1 的编译类型仍然是 Parent。

因此,尽管对象的运行时类型在其生命周期内是固定的,指向该对象的引用变量可以被重新赋值以指向其他具有兼容运行时类型的对象。这种重新赋值不改变原有对象的运行时类型,只改变了引用变量所指向的对象。

package PolyMorphicTest;

public class PolyObject {
    public static void main(String[] args) {
    Animal01 animal01 = new Dog01();
    animal01.say();
    Animal01 animal02 = new Cat01();
    animal02.say();
    Animal01 animal03 = new Animal01();
    animal03.say();
    animal02=new Dog01();
    animal03=new Cat01();
    animal02.say();
    animal03.say();
    }
}
class Animal01{
    public void say(){
        System.out.println("Animal01嘻嘻~");
    }
}
class Cat01 extends Animal01{
    public void say(){
        System.out.println("Cat01嘻嘻~");
    }
}
class Dog01 extends Animal01{
    public void say(){
        System.out.println("Dog01嘻嘻~");
    }
}

package PolyMorphicTest;

public class PolyTest {
    public static void main(String[] args) {
        Master master = new Master("超级大帅哥");
        Dog dog01 = new Dog("张sult");
        Cat cat01 = new Cat("与姜昆");
        Pig pig  = new Pig("勺子长");
        Fish fish =new Fish("鳜鱼~");
        Bone bone = new Bone("铁锅炖大鹅~");
        PPFood ppFood = new PPFood("猪猪宝贝小辣条~");
        master.Feed(cat01,fish);
        master.Feed(dog01,bone);
        master.Feed(pig,ppFood);
    }
}
class Food{
    private String name;
    public Food(String name){
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
class Fish extends Food{
    public Fish(String name){
        super(name);
    }

}
class Bone extends Food{
    public Bone(String name){
        super(name);
    }
}
class PPFood extends Food{
    public PPFood(String name){
        super(name);
    }
}
class Animal{
    private String name;
    public Animal(String name){
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
class Cat extends Animal{
    public Cat(String name){
        super(name);
    }
}
class Pig extends Animal{
    public Pig(String name){
        super(name);
    }
}
class Dog extends Animal{
    public Dog(String name){
        super(name);
    }
}
class Master{
    private String name;
    public Master(String name){
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
   /* public void Feed(Dog dog,Bone bone){
        System.out.println(name + "不会请无敌ugly的"+ dog.getName() +"吃"+ bone.getName());
    }
    public void Feed(Cat cat,Fish fish){
        System.out.println(name + "不会请无敌ugly的"+ cat.getName() +"吃"+ fish.getName());
    }//结果是方法的重载越来越多,引出多态。*/
    //animal的编译类型是Animal,可以指向/接受Animal的子类对象。
    public void Feed(Animal animal,Food food){
        System.out.println(name + "不会请无敌ugly的"+ animal.getName() +"吃"+ food.getName());
    }
}

多态的类型转换的不同方面

  1. 向上转型。

在面向对象编程中,“向上转型”(Upcasting)是指将一个子类对象的引用转换为它的父类引用类型。这是多态的一个常见应用,因为向上转型是自动进行的,并不需要显式类型转换。

当一个子类对象被提升到父类引用时,你只能访问父类中定义的那些成员和方法,而不能访问子类特有的成员。这是因为从父类的角度看,它并不知道这个对象实际上是哪个子类的实例,所以只能保证操作不会超出父类的范围。

public class Animal {

public void speak() {

System.out.println("sound");

}

}

public class Dog extends Animal {

    public void speak() {

        System.out.println("Woof!");

    }

public void wagTail() {    

   System.out.println("Dog is wagging its tail.");

    }

}

你可以这样进行向上转型:

Animal myAnimal = new Dog(); // 向上转型,无需显式转换

myAnimal.speak(); // 输出 "Woof!" 因为Dog重写了speak方法

// myAnimal.wagTail(); // 这行代码会出错,因为Animal类中没有wagTail方法

在这个例子中,myAnimal是一个Animal类型的引用,但它实际上指向的是Dog类型的对象。当调用speak()方法时,由于Dog重写了这个方法,所以输出的是"Dog"的声音"woof"。但是,如果尝试调用Dog特有的wagTail()方法,则会因为Animal类中没有该方法而编译失败。即能够调用父类中的所有能访问的(非私有)成员(方法和属性),但是不能调用子类特有的成员。

向上转型是实现动态多态的基础,因为在运行时,实际执行的方法是由对象的实际类型决定的,而不是引用的类型。这种机制使得编写灵活、可扩展的代码成为可能。

2.向下转型。

向下转型是在面向对象编程中,将一个父类类型的引用转换为其子类类型的过程。这是与向上转型相对的概念,向上转型是自动的,而向下转型则需要显式的类型转换

  1. 格式:子类类型 引用名 = (子类类型) 父类引用;
  2. 只能强行转换父类的引用,不能强行转换父类的对象。
  3. 父类的引用必须指向当前目标类型的对象。
  4. 向下转型后,可以调用子类类型中所有的成员。

当希望利用子类特有的功能,而这些功能在父类中是没有定义的。例如,假设有一个Vehicle类和两个子类Car和Bicycle,并且Car类有一个特有的方法honkHorn()。如果你有一个Vehicle类型的引用,你可能需要将其向下转型为Car类型,以便调用honkHorn()方法。

public class Vehicle {

    public void move() {

        System.out.println("Vehicle");

    }

}

public class Car extends Vehicle {

    public void honkHorn() {

       System.out.println("Honk!");

    }

}

public class Main {

    public static void main(String[] args) {

        Vehicle vehicle = new Car(); // 向上转型

        vehicle.move(); // 可以调用Vehicle的move()方法

            Car car = (Car) vehicle; // 向下转型。cat的编译类型是?运行类型是?

            //其中,vehicle 必须是指向 Car 对象的引用,才能安全地完成向下转型为 Car 类型。

//编译类型:car 的编译类型是 Car,因为在代码中明确地将其声明为 Car 类型。

//运行类型:car 的运行类型取决于 vehicle 在运行时实际指向的对象类型。在给出的例子中,vehicle 初始化为 new Car(),所以其运行类型也是 Car。

 car.honkHorn(); // 调用Car特有的honkHorn()方法

        }

    }

在上述代码中,首先创建了一个Car对象并将其赋值给Vehicle类型的引用vehicle。然后,可以安全地进行向下转型,并调用Car特有的honkHorn()方法。

需要注意的是,向下转型时,如果父类引用实际上并不指向子类对象,那么运行时会抛出ClassCastException异常。

属性的值

package PolyMorphicTest;

public class PolyTest0001 {
    public static void main(String[] args) {
        A x = new B();
        System.out.println(x.a);//此时输出的值为520
    }
}
class A {
    int a=520;
}
class B extends A {
    int a=1314;
}

在代码示例中,尽管 B 类覆盖了 a 成员变量,但是当通过 A 类型的引用 x 访问 a 时,访问的是 A 类中的 a 成员变量,而不是 B 类中覆盖的 a。这是因为成员变量的访问不是基于动态绑定的,也就是说,成员变量的访问不遵循多态的规则,而是根据引用的静态类型(也就是编译类型)来决定的。

A x = new B();

System.out.println(x.a);

这里,x 是 A 类型的引用,尽管它实际上指向的是 B 类型的对象,但是当通过 x 访问 a 时,编译器会查找 A 类中的 a,而不会考虑 B 类中的覆盖版本。因此,输出的结果是 A 类中的 a 的值,即 520。

这种行为与方法的调用不同,方法调用遵循动态绑定的原则,也就是说,当一个方法被调用时,实际调用的是对象运行时类型中定义的方法,而不是引用类型中定义的方法。这就是多态的基本原理。

总结一下,成员变量的访问不受多态的影响,而是严格基于引用的类型。所以在代码中,x.a 将始终引用 A 类中的 a 变量,即使 x 实际上指向的是 B 类型的对象。

instanceOf操作符

instanceof 是 Java 中的一个二元操作符,用于测试对象是否是特定类(或接口)的实例,或者是该类(或接口)的子类的实例。这个操作符返回一个布尔值,如果对象是给定类或接口的实例,或者属于其子类,则返回 true;否则返回 false。

有:

public class Animal {}

public class Dog extends Animal {}

public class Cat extends Animal {}

现在,可以使用 instanceof 来检查一个对象是否是 Dog 或者 Animal 的实例:

public class Main {

    public static void main(String[] args) {

        Animal animal = new Dog();

       if (animal instanceof Dog) {

            System.out.println("The object is an instance of Dog.");

        }       

        if (animal instanceof Animal) {

            System.out.println("The object is an instance of Animal or a subclass.");

        }    

        if ((animal instanceof Cat)) {

            System.out.println("The object is not an instance of Cat.");

        }

    }

}

在这个例子中,animal 引用实际上指向的是 Dog 类的实例。因此,animal instanceof Dog 和 animal instanceof Animal 都会返回 true,而 animal instanceof Cat 返回 false。

instanceof 是 Java 中实现类型检查和安全向下转型的关键操作符。在处理多态性和需要基于对象类型做不同操作的场景中非常有用。

*Java的动态绑定机制

Java语言中的动态绑定机制是实现多态的关键部分,它允许在运行时确定方法的调用版本,而非在编译时就做出决定。这一机制使Java能够支持方法的重写(overriding),并允许父类引用调用子类中重写的方法。

动态绑定的工作原理

在Java中,当一个方法被调用时,JVM(Java虚拟机)会查找该方法的实现。如果方法调用是通过一个对象引用进行的,且该引用的类型是一个类或接口,那么JVM将根据运行时对象的实际类型来决定调用哪个方法。这就是动态绑定。

编译时绑定与运行时绑定

编译时绑定(静态绑定):发生在编译阶段,例如构造函数调用、静态方法调用、私有方法调用、final方法调用和局部变量的访问等。这些绑定在编译时就可以确定,因为它们不依赖于对象的实际类型。

运行时绑定(动态绑定):发生在运行阶段,例如通过对象引用调用的实例方法。这些绑定依赖于对象的实际类型,因此只能在运行时确定。

class Animal {

    void speak() {

        System.out.println("Some generic sound");

   }

}

class Dog extends Animal {

    void speak() {

       System.out.println("Woof!");

   }

}

class Cat extends Animal {

   void speak() {

       System.out.println("Meow!");

   }

}

public class Main {

    public static void main(String[] args) {

        Animal myPet = new Dog(); // 向上转型

        myPet.speak(); // 运行时绑定,输出 "Woof!"

        

        myPet = new Cat(); // 更改引用,指向Cat实例

        myPet.speak(); // 再次运行时绑定,这次输出 "Meow!"

    }

}

虽然 myPet 的类型是 Animal,但在运行时,JVM会检查 myPet 实际引用的对象类型,并调用相应类中重写的 speak() 方法。

动态绑定是Java中实现多态的关键,它允许父类引用调用子类的方法,只要子类重写了父类中的方法。这种机制增强了代码的灵活性和复用性。

package PolyMorphicTest;

public class DynamicBinding {
    public static void main(String[] args) {
        Dynamic y = new DynamicBind();
        System.out.println(y.sum());//3000
        System.out.println(y.sum01());//2000
    }
}
class Dynamic{
    public int x=100;
    public int sum(){
        return getI()+100;//200
    }
    public int getI(){
        return x;//100
    }
    public int sum01(){
        return x+200;//300
    }
}
class DynamicBind extends Dynamic{
    public int x=1000;
    public int sum(){
    return x+2000;//3000
    }
    public int getI(){
        return x;//1000
    }
    public int sum01(){
        return x+1000;//2000
    }
}

当注销public int sum(){
    return x+2000;//3000
    }这部分时System.out.println(y.sum());输出1100。

其中,1100=1000+100,1000来自子类DynamicBind中的getI()方法,100来自Dynamic中的public int x=100;这说明方法调用时,实际调用哪个方法取决于对象的运行时类型,而字段访问时,访问哪个字段则取决于引用的类型。

y 是 Dynamic 类型的引用,但实际上指向的是 DynamicBind 类型的对象。当调用 y.sum() 方法时,由于 sum() 方法在 DynamicBind 中被重写了,所以会调用 DynamicBind 中的 sum() 方法。然而,当注释掉 DynamicBind 中的 sum() 方法重写,y.sum() 将调用 Dynamic 类中的 sum() 方法,这时动态绑定机制就决定了调用 Dynamic 类的 sum() 方法。

在 Dynamic 类的 sum() 方法中,有一行代码 return getI() + 100;。这里,getI() 方法调用遵循动态绑定,因此会调用 DynamicBind 类中的 getI() 方法,因为它在运行时是 DynamicBind 类型的对象。DynamicBind 类中的 getI() 方法返回 1000,这是子类中的字段 x 的值。

但是,当 sum() 方法中的 + 100 发生时,这里的 100 是来自于 Dynamic 类中定义的字段 x 的值。即使 y 实际上指向的是 DynamicBind 类型的对象,由于字段访问是基于静态绑定的,所以仍然会访问 Dynamic 类中定义的字段 x,而不是 DynamicBind 类中的 x。

方法调用遵循动态绑定,而字段访问则遵循静态绑定。

;