文章目录
1、前言
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();
}
}
逻辑上,Dog
向Animal
转换是没问题的,毕竟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
方法,当我们要传 一个对象的时候,它这里就用形参为Object
的println(Object x)
来接收了(因为Object
是所有类的父类,可以这样接收)。
那我们的toString()
方法在哪呢?看这里的第一行代码 String s = String.valueOf(x);
我们点进源码看看👇。
当我们传进来的是空值(什么都没引用的引用变量)时,返回的是null
。反之,就返回我们重写的 toString()
方法的返回值,为什么是重写的toString()
方法?因为这里odj
向上转型发生了多态,odj
本源是Student
类型。
String s = String.valueOf(x);
就是把toString()
方法的返回值赋给了s
(toString()
方法的返回值就是对象的具体内容),后面的代码就不用太关注了,就是又经过一些判断就打印了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