JavaSE语法(9)—— 详细解读Java中的多态及相关知识点



1、前言

  👉博主的JavaSE专栏

  JavaSE语法(8)——【类的继承、重写、组合、final 关键字……

  在上一篇文章中👆,已经介绍了类的继承以及所涉及的知识点,这一篇来详细介绍类的多态性,(阅前提醒:学习多态之前 需要对类的继承以及方法重写知识点有一定的熟悉程度,这里需要用到它们。



2、向上转型和向下转型


 2.1 向上转型

  向上转型 相当于类型转换,子类向父类转换,实际就是创建一个子类对象,将其当成父类对象来使用。

public class Animal {
	//···········	
	//······
}
public class Dog extends Animal{
	//···········	
	//······
}
public class Main{
	public static void main(String[] args) {
		Animal animal = new Dog();
	}
}

  逻辑上,DogAnimal转换是没问题的,毕竟Dog也是Animal(狗本来就是动物);在语法上,跟普通类型的转换差不多,是小范围向大范围转换(包含关系上),不需要强制类型转换。

注意:

  • 必须要有继承关系才能转换。

  • 子类类型转换为父类类型后,不能调用到子类特有的字段与方法。
public class Animal {

    String name;
    
    public void animal() {
        System.out.println("animal的方法!");
    }
}
public class Cat extends Animal{

    public void cat() {
        System.out.println("Cat独有的方法!");
    }
}
public class Main {

    public static void main(String[] args) {

        Cat cat = new Cat();
        Animal animal = cat;//向上转型
        animal.cat();//访问子类的cat()
    }
}

结果:

  • 向上转型的方式:直接赋值、方法传参、方法返回

 2.2 向下转型

  将一个子类对象经过向上转型之后当成父类方法使用,这时无法调用子类的方法。但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换,但这是大范围向小范围转换,需要用到强制转换符。

public class Animal {

    String name;
    
    public void animal() {
        System.out.println("animal的方法!");
    }
}
public class Cat extends Animal{

    public void cat() {
        System.out.println("Cat独有的方法!");
    }
}
public class Main {

    public static void main(String[] args) {

        Animal animal = new Cat();
        //animal.cat();访问子类的cat(),访问失败。
        Cat cat = (Cat)animal;//必须要强制转换!
        cat.cat();
    }
}

结果:

Cat独有的方法!

  如果我再增加一个类:

public class Dog extends Animal{
	//..........
	//.......
}
public class Main {

    public static void main(String[] args) {
		
		Animal animal = new Cat();
		//转换错误,会抛出异常!
		Dog dog = (Dog)animal;
    }
}

结果:
在这里插入图片描述
  所以向下转型是很危险的!一个不注意就会转错类型,那有没有解决办法呢?当然有啦,那就是instanceof运算符👇。

  2.21 instanceof 运算符

  instanceof是双目运算符,左面的操作元素是对象(实例),右面是类。当左面的操作元素是右面的类或其子类所创建的对象时,instanceof运算结果是true,否则是false。

案例:就用上面的例子。

public class Main {
    public static void main(String[] args) {
		
		Animal animal = new Cat();
		//Dog dog = (Dog)animal;//转换错误,会抛出异常!
		//正确处理方式
		if(animal instanceof Dog){
			Dog dog = (Dog)animal;
			//......
		}
		if(animal instanceof Cat){
			Cat cat = (Cat)animal;
			//......
		}
    }
}


3、多态


 3.1 概念及其实现条件

  顾名思义,多态就是有多种状态,当不同的对象去完成同一件事后会有不同的结果。

  先让大家看一下案例,感受一下多态的魅力。

public class Animal {

    String name;
    Animal (String name) {
        this.name = name;
    }
    
    //动物都会发出叫声
    public void call () {
        System.out.println("~~~~~");
    }
}
----------------------------------------------------------------
public class Cat extends Animal{

    Cat(String name) {
        super(name);
    }

    //我有自己的叫声,重写父类的方法
    public void call() {
        System.out.println(name + ":喵喵喵~");
    }
}
---------------------------------------------------------------
public class Dog extends Animal{

    Dog(String name) {
        super(name);
    }

    //我有自己的叫声,重写父类的方法
    public void call() {
        System.out.println(name + ":汪汪汪~");
    }
}
public class Main {

    public static void mainCall(Animal a) {
        a.call();
    }

    public static void main(String[] args) {

        Cat Cat = new Cat("大白");
        Dog Dog = new Dog("大黄");

        mainCall(Cat);
        mainCall(Dog);

    }
}

结果:
大白:喵喵喵~
大黄:汪汪汪~

  在Main类中的 mainCall 方法就是一种行为,让两种不同的对象“猫类”、“狗类”传入这个方法后(也就是让它们做同一件事)会发生不同的结果,代码运行时,当传递不同类对象时,会调用对应类中的方法,这是多态的体现。

  mainCall方法内部并不知道, 也不关注当前的a引用指向的是哪个类型(哪个子类)的实例。此时a调用call()方法可能会有多种不同的表现(和a引用的实例相关), 这种行为就称为 多态。

  那么通过上面的代码,我们可以总结多态发生的条件:

  • 必须是继承体系。
  • 子类必须重写父类的方法。
  • 用父类的类型来调用子类的方法,也就是要向上转型。

   问题: 为什么mainCall中的a调用的call()方法是子类的call()方法?
   答: 这是由于 动态绑定👇


 3.2 动态绑定

  这里重新举例:

public class Animal {

    public void call() {
        System.out.println("Animal的call方法!");
    }
}
public class Dog extends Animal{

    public void call() {
        System.out.println("Dog的call方法!");
    }
}
public class Main {
    public static void main(String[] args) {

        Dog dog = new Dog();
        Animal animal = dog;
        animal.call();
    }
}

结果:

Dog的call方法!

  你也许会问:这不是向上转型了吗?为什么animal调用的是子类的方法呢?先别急,我们看看程序在编译的时候是怎样调用的。

  • 找到字节码 Main.class 文件的文件路径(在out路径下),在上方输入cmd,进入cmd窗口。
    在这里插入图片描述
  • 输入javap -c Main,可以看到汇编代码。
    在这里插入图片描述
      代码编译的时候 写的是调用的父类的call()方法的形式,但是在运行时期,JVM会提取对象的实际类型的方法表、搜索方法签名等一系列的操作( 通俗来讲,就是确定最“本质”的类型)来确定到底调用哪个方法,这就是动态绑定(在运行时期确定)
      还有静态绑定,在编译时,根据用户所传递实参类型就确定了具体调用那个方法,典型代表 函数重载。
    (ps:想进一步了解的可以看看大佬们的文章👉Java的动态绑定和静态绑定


4、Object类

  Object是Java默认提供的一个类。Java里面除了Object类,所有的类都继承了Object类。在类定义的时候就会默继承Object类。即所有类的对象都可以使用Object的引用进行接收。

  • 所有的类都可以让Object来接受
public class A {
    String name;
    int age;
    //......
    //......
}
----------------------------------------
public class Main {
    public static void main(String[] args) {
        A a = new A();
        Object o = a;
        System.out.println(a);
        System.out.println(o);
    }
}

结果:

demo16.A@4eec7777
demo16.A@4eec7777

  这里是地址值,可以看到它们引用的是同一个实例。

  Object类里面也定义了很多方法,我们可以重写这里面的方法来进行实际应用。
在这里插入图片描述


 4.1 获取对象的信息toString方法(打印对象)

  Object类中有一个方法叫toString()的方法,这个方法是返回对象的字符串表示形式。就是打印对象的数据。

  案例:

public class Student {
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
---------------------------------------------
public class Main {
    public static void main(String[] args) {
    
        Student s1 = new Student("小名",20);
    }
}

  Student的实例s1,我们的目的是输出s1的名字与年龄,联想到 基本类型的输出 是System.out.println(这里是基本类型);,那么在输出类的时候是否可以这样操作呢?👇

public class Main {
    public static void main(String[] args) {
    
        Student s1 = new Student("小明,20);
        System.out.println(s1);
    }
}

结果:
在这里插入图片描述

  这里输出的是地址值(这里为什么是这样的值?别急,过几个自然段后再来介绍),然而我们想要的不是这样的结果,怎么做?答案就是重写toString方法。👇

public class Student {
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    //重写toString方法
    public String toString() {

        String str = "name: " + name + " age: " + age;
        return str;
    }
}
---------------------------------------------------
public class Main {
    public static void main(String[] args) {
    
        Student s1 = new Student("小明,20);
        System.out.println(s1);
    }
}

结果:
在这里插入图片描述
  为什么不是调用toString()方法?而是直接就System.out.println(s1);了?来看看println()的源码👇(源码查看方式:Ctrl + 鼠标左键)。
在这里插入图片描述

  这里面重载了很多的println方法,这里只截取了一个,我们以前System.out.println(这里是基本类型);的时候,是调用的上面 形参为基本类型的println方法,当我们要传 一个对象的时候,它这里就用形参为Objectprintln(Object x)来接收了(因为Object是所有类的父类,可以这样接收)。

  那我们的toString()方法在哪呢?看这里的第一行代码 String s = String.valueOf(x);我们点进源码看看👇。

在这里插入图片描述

  当我们传进来的是空值(什么都没引用的引用变量)时,返回的是null。反之,就返回我们重写的 toString()方法的返回值,为什么是重写的toString()方法?因为这里odj向上转型发生了多态,odj本源是Student类型。
在这里插入图片描述

  String s = String.valueOf(x);就是把toString()方法的返回值赋给了stoString()方法的返回值就是对象的具体内容),后面的代码就不用太关注了,就是又经过一些判断就打印了s

  回到最开始的一个问题,在没有重写toString()方法的时候为什么是一串地址值👇?
在这里插入图片描述

  当没有重写的时候,println()调用的是Object类中的toString()方法👇。

  getClass().getName()是类的路径(或者叫名字),Integer.toHexString(hashCode());是地址值。
在这里插入图片描述


 4.2 对象比较equals方法

  我们先看看 基本类型判断是否相等 是怎么操作的👇。

public class Main {
    public static void main(String[] args) {
        int a = 10;
        int b = 13;
        int c = 10;
        System.out.println(a == b);//false
        System.out.println(a == c);//true
    }
}
结果:
false
true

  上面代码没什么问题,那么对象之间的比较是否也可以这样操作呢? 👇

public class Student {
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
------------------------------------------
public class Main {
    public static void main(String[] args) {

        //s1与s2内容相同
        Student s1 = new Student("小明",20);
        Student s2 = new Student("小明",20);

        //s3与s1是引用的同一个实例
        Student s3 = s1;

        //s4与s2是引用的同一个实例
        Student s4 = s2;

        System.out.println(s1 == s2);//false  为什么?按理来说应该是true呀
        System.out.println(s3 == s1);//true
        System.out.println(s4 == s2);//true
    }
}
结果:
false
true
true

  System.out.println(s1 == s2);为什么是false呢?👇

在Java中,用== 进行比较时:

  • 如果==左右两侧是基本类型变量,比较的是变量中的值是否相同。
  • 如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同。

  在上面的代码里,s1 s2两个不同的实例(在堆中两个不同的空间,自然地址不一样),它们仅仅只是内容相同而已。


  哪怎么解决呢?重写Object中的equals方法;

  看看Object中的equals方法👇
在这里插入图片描述
  可以看到equals()方法默认也是按照地址比较的,如果不重写效果与==是一样的👇。

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

        //s1与s2相同
        Student s1 = new Student("小明",20);
        Student s2 = new Student("小明",20);

        //s3与s1是引用的同一个实例
        Student s3 = s1;

        //s4与s2是引用的同一个实例
        Student s4 = s2;

        System.out.println( s1.equals(s2) );//false
        System.out.println( s3.equals(s1) );//true
        System.out.println( s4.equals(s2) );//true
    }
}
结果:
false
true
true

如何重写?👇

public class Student {
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //重写equals方法
    public boolean equals(Object obj) {
    
        if (obj == null) {
            return false ;
        } if(this == obj) {
            return true ;
        } 
        
        // 不是Student类对象
        if (!(obj instanceof Student)) {
            return false ;
        }
        
        Student s = (Student) obj ; // 向下转型,比较属性值
        return this.name.equals(s.name) && this.age==s.age ;
    }
}
public class Main {
    public static void main(String[] args) {

        //s1与s2相同
        Student s1 = new Student("小明",20);
        Student s2 = new Student("小明",20);

        //s3与s1是引用的同一个实例
        Student s3 = s1;

        //s4与s2是引用的同一个实例
        Student s4 = s2;

        System.out.println( s1.equals(s2) );//true
        System.out.println( s3.equals(s1) );//true
        System.out.println( s4.equals(s2) );//true
    }
}
结果:
true
true
true
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值