1. 多态基础

可以理解为一个事物的多种形态。其体现就是,用父类的类型声明一个对象变量,但是右边 new 的是其一个子类:

  1. Person p1 = new Student() ; // 父类的引用指向子类的对象
  1. 多态的使用:当调用子父类同名同参数的方法时,实际执行的是子类**重写**父类的方法 ==> 虚拟方法调用<br />Person类:
package pkg2;

public class Person {
    // 属性
    private String name;
    private int age;
    // 构造器
    public Person(String name){
        this.name = name;
    }
    // 方法
    public void showMe(){
        System.out.println("我叫"+name+",我是个人");
    }
    public String getName(){
        return name ;
    }
}

Man 类:

package pkg2;

public class Man extends Person{
    public Man(String name){
        super(name);
    }

    public void showMe(){
        System.out.println("我叫"+getName()+",我是个男人");
    }
    public void showMan(){
        System.out.println("I'm a man");
    }
}

Woman 类:

package pkg2;

public class Woman extends Person{
    public Woman(String name){
        super(name);
    }

    public void showMe(){
        System.out.println("我叫"+getName()+",我是个女人");
    }
    public void showWoman(){
        System.out.println("I'm a woman");
    }
}

test:

package pkg2;

public class test {
    public static void main(String[] args) {
        Person p1 = new Woman("张三");
        Person p2 = new Man("李四");
        p1.showMe();
        System.out.println(p1.getName());
        p2.showMe();
        System.out.println(p2.getName());
    }
}
上述代码中,p1、p2 均使用了多态,他们的 showMe 方法均是**重写后的** showMe 方法,而 getName 没有被重写,依然是父类 Person 的 getName 方法。所以 test 运行结果为:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2643809/1626513688397-23cec82a-1b74-4bbb-8d22-2b5afc379844.png#clientId=u6bfa4e7d-2b73-4&from=paste&height=162&id=uc9834ced&margin=%5Bobject%20Object%5D&name=image.png&originHeight=162&originWidth=741&originalType=binary&ratio=1&size=11673&status=done&style=none&taskId=u6a956be5-5775-45b2-8b72-2bc36afeaa2&width=741)<br />但是,p1、p2 只能调用 Person 类中已有的方法,无法调用 Man 或 Woman 中独有的属性方法!原因很简单,在编译时就确定 p1、p2 是一个Person类型,他拥有的所有属性和方法都是 Person 拥有的属性和方法。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2643809/1626513899643-1774fad6-9a4b-4431-8972-cba9006183a8.png#clientId=u6bfa4e7d-2b73-4&from=paste&height=190&id=uafa43ccb&margin=%5Bobject%20Object%5D&name=image.png&originHeight=190&originWidth=562&originalType=binary&ratio=1&size=23051&status=done&style=none&taskId=u9a5cf4eb-d832-42fd-a604-910edc8f470&width=562)<br />也就是说,p1、p2 所能调用的方法取决于其类型Person,而不是右边用来new的类。<br />总结:**编译看左边,运行看右边**

2. 使用前提

  • 要有类的继承关系
  • 子类要有方法重写
  • 多态只适用于方法,不适用于属性
    • 对属性而言,编译和运行都看左边的,即子父类若有同名属性,多态依然调用父类属性

3. 多态的意义

现给定一个方法func,它以某一个类A作为参数输入。B、C 都是A的子类,由于多态的存在,B、C均可作为func的参数,这样以来就可以用一个方法来表现出某一类的多个子类,这就是“多种形态”的体现。下面给个例子:
Animal父类:

package pkg3;

public class Animal {
    public void eat(){
        System.out.println("动物要吃东西");
    }

    public void shout(){
        System.out.println("动物会叫");
    }
}

Dog子类:

package pkg3;

public class Dog extends Animal{
    public void eat(){
        System.out.println("狗吃肉");
    }

    public void shout(){
        System.out.println("汪!汪!汪!");
    }
}

Cat子类:

package pkg3;

public class Cat extends Animal{
    public void eat(){
        System.out.println("猫吃鱼");
    }

    public void shout(){
        System.out.println("喵!喵!喵!");
    }
}

test:

package pkg3;

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

        test t1 = new test() ;  // 用这种方法可以调用test自身的方法
        t1.AnimalTest(new Dog());  // Animal animal = new Dog()
        t1.AnimalTest(new Cat());  // Animal animal = new Cat()

    }

    public void AnimalTest(Animal animal){
        animal.eat();
        animal.shout();
    }
}
运行结果:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2643809/1626515281344-95cd565f-3587-4512-b839-43d39e9fb7a7.png#clientId=u6bfa4e7d-2b73-4&from=paste&height=165&id=u12feed6b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=165&originWidth=768&originalType=binary&ratio=1&size=11343&status=done&style=none&taskId=u0adf6762-8dcf-473f-823f-210329d83c5&width=768)<br />可以看到,AnimalTest 虽然以 Animal 作为形参类型,但是可以传入 Dog 和 Cat 型的对象,从而执行 Dog 和 Cat 中重写的方法,这就是多态的意义。<br />如果没有多态,则需要以下两个函数来替代AnimalTest这一个函数。显然,当一个父类拥有很多子类的时候,多态就显得非常有必要。
public void DogTest(Dog dog){
    dog.eat();
    dog.shout();
}

public void CatTest(Cat cat){
    cat.eat();
    cat.shout();
}

4. 虚拟方法调用

image.png
也就是说 ,多态的使用是一个运行时行为,而不是一个编译时行为。
image.png

5. instanceof 关键字

如前文所述,用来多态后,只能调用父类的属性和方法,而不能调用子类的属性和方法。实际上,有了对象的多态性后,内存中实际上是加载了子类独有的属性与方法的,但是由于对象变量声明为父类类型,导致编译时只能调用父类中声明的属性和方法。
也就是说:内存中存在,但是用不了
那么如何才能调用子类特有的属性和方法呢? ==> 强制类型转换

Man m1 = (Man)p2 ;
虽然,子类的功能是大于父类的,但是我们认为 父类类型 > 子类类型,而强制类型转换是向下转换,因此可以讲父类类型的对象变量,转换为子类类型。
  • 向上转型:多态 (子 -> 父)
  • 向下转型:instanceof (父 -> 子)

image.png
a instanaceof A 就是判断对象a实际上(new)是否是类A的实例或者是A的子类的实例,如果是,返回true,反之返回false。

  • 子类对象 instanceof 父类 == true
  • 父类对象 instanceof 子类 == false ```java package pkg3;

public class test { public static void main(String[] args) { Animal a1 = new Cat(); Animal a2 = new Dog(); Animal a3 = new Animal(); if (a1 instanceof Cat){ // true System.out.println(“true1”); } if (a1 instanceof Animal){ // true System.out.println(“true2”); } if (a1 instanceof Object){ // true System.out.println(“true3”); } if (a1 instanceof Dog){ // true System.out.println(“true4”); } if (a3 instanceof Dog){ // true System.out.println(“true5”); } } }

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2643809/1626582975218-5bb12754-b0e8-4ef9-ad68-245950add3d2.png#clientId=u3a09a35a-93e5-4&from=paste&height=151&id=uedc7fba7&margin=%5Bobject%20Object%5D&name=image.png&originHeight=151&originWidth=667&originalType=binary&ratio=1&size=10938&status=done&style=none&taskId=ua737baa3-690f-43dd-a39e-a5089c1bc74&width=667)<br />使用情境:为了避免向下转型时出现 ClassCastException 的异常,我们在向下转型之前,先进行 instanceof 的判断,一旦返回 true,就进行向下转型。
```java
package pkg3;

public class test {
    public static void main(String[] args) {
        Animal a1 = new Cat();
        Animal a2 = new Dog();
        Animal a3 = new Animal();
        if (a1 instanceof Cat) {
            ((Cat) a1).showCat();
        }
    }
}

6. == 和 equals

  • == 是一个运算符,可以用于基本数据类型和引用数据类型。
  • equals 是Object中的一个方法。

    6.1 ==

  1. 如果比较的是基本数据类型变量,则比较两个变量保存的值是否一样,且不要求类型相同,这点和C一致
  2. 如果比较的是引用数据类型变量,则比较两个对象的地址值是否相同,即两个引用是否指向同一个实体 ```java package pkg2;

public class test { public static void main(String[] args) { Person p1 = new Person(“123”,12) ; Person p2 = new Person(“123”,12) ; System.out.println(p1 == p2); } }

    上述代码中,虽然 p1 和 p2 的类型是一样的,但是 p1、p2 分别在堆中开辟了各自的空间,即指向了不同的实体,所以 p1 == p2 返回的false。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/2643809/1626583784510-ee2607db-5ac1-4c86-9fe1-8391c58b6b77.png#clientId=u3a09a35a-93e5-4&from=paste&height=107&id=uafcd9868&margin=%5Bobject%20Object%5D&name=image.png&originHeight=107&originWidth=558&originalType=binary&ratio=1&size=8637&status=done&style=none&taskId=u65ec4fbc-e573-4d19-a86c-d128ea08dec&width=558)<br />但如果 p1 直接赋给 p2,那么 p1 == p2 显然是成立的。
```java
package pkg2;

public class test {
    public static void main(String[] args) {
        Person p1 = new Person("123",12) ;
        Person p2 = p1 ;
        System.out.println(p1 == p2);
    }
}

image.png
对于一般的类均是这样,但是有个特殊的类需要另行分析,就是 String 类。想理解 String 类型的特殊性,就要先理解 Java 中的常量池
image.png
Java中的常量池,实际上分为两种形态:静态常量池运行时常量池
所谓静态常量池,即.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和*符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名
  • 属性名称和描述符
  • 方法名称和描述符

运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池 。 运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享 , 例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
也就是说,如果两个 String 型的变量 s1、s2 指向了一个相同的字符串常量,由于编译期间每个字符串常量都被放进class文件的常量池中来实现复用,所以载入运行时常量池后,s1、s2 实际上指向了常量池中的同一片地址,故 s1 == s2。

package pkg2;

public class test {
    public static void main(String[] args) {
        String s1 = "123" ;
        String s2 = "123" ;
        System.out.println(s1 == s2);  // true
    }
}
但是,如果某一个 String 变量不是常量,而是从内存中 new 出来一片堆空间,那么它就不指向上述常量池了,故和其他的 String 型指向的地方就不同了。
package pkg2;

public class test {
    public static void main(String[] args) {
        String s1 = new String("123") ;
        String s2 = "123" ;
        String s3 = new String("123") ;
        System.out.println(s1 == s2);
        System.out.prSintln(s1 == s3);
    }
}

image.png

6.2 equals()

  1. equals() 只适用用引用数据类型
  2. 如果类不重写 equals 方法的话,就调用 Object 中的 equals 方法,而 Object 中定义的 equals 实际上和 == 是一致的,即比较两个对象是否指向同一个实体。

    public boolean equals(Object anObject) {
         if (this == anObject) {
             return true;
         }
         if (anObject instanceof String) {
             String aString = (String)anObject;
             if (!COMPACT_STRINGS || this.coder == aString.coder) {
                 return StringLatin1.equals(value, aString.value);
             }
         }
         return false;
     }
    
  3. 但是,可以看到,当 anObject 是String类时,equals就不是 == 了,此时 equals 将比较两个 String 变量的内容是否一样,不再考虑地址了。

image.png

6.3 重写 euqals

由于对一般类而言 Object 中 euqal 和 == 是等价的,所以类基本都要重写 equals 方法,让它比较两个类的内容是否一样。
IDEA 会快捷重写 equals 方法,按下 Alt + Ins 即可选择,用它重写的 equals 即可比较两个类的内容是否一样。

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

image.png