Day10 面向对象-继承
1. 学习目标
- 能够实现类的继承,并说出继承的特点
- 能够说出super关键字的作用
- 能够说出Object类的常用方法
- 能够说出Object类常见方法的用处并重写这些方法
- 能够说出四种不同权限的区别
2. 继承
继承是面向对象软件设计中的一个概念,与封装和多态共为面向对象的三个基本特征。继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。
上述代码中,我们发现猫类和狗类具有很多共同的特征和行为,如果把这些相同的特征和行为再次抽象成为一个新的动物类,就可以将这个类定义为猫狗类的父类。
如上图中,猫狗类都可以称为子类,也叫派生类;多个类抽取出来的这个动物类称为父类、超类(superclass)或者基类。
继承描述的是事物之间的所属关系,这种关系是:is-a
的关系。例如,图中猫属于动物,狗也属于动物。可见,父类更通用,子类更具体。我们通过继承,可以使多种事物之间形成一种关系体系。
2.1 继承的好处
子类继承父类,就使得子类对象具有与父类相同的属性,可以调用父类相同的行为。
- 提高代码的复用性。
- 提高代码的扩展性。
- 类与类之间产生了关系,是学习多态的前提。
2.2 继承的语法
使用关键字extends
来实现类之间的继承关系。
class 父类类名 {
...
}
class 子类类名 extends 父类类名 {
...
}
继承演示,代码如下:
/*
* 定义动物类Animal,做为父类
*/
class Animal {
// 定义name属性
public String name;
// 定义age属性
public int age;
// 定义动物的吃东西方法
public void eat() {
System.out.println(age + "岁的" + name + "在吃东西");
}
}
/*
* 定义猫类Cat 继承 动物类Animal
*/
class Cat extends Animal {
// 定义一个猫抓老鼠的方法catchMouse
public void catchMouse() {
System.out.println("抓老鼠");
}
}
/*
* 定义测试类
*/
public class ExtendDemo01 {
public static void main(String[] args) {
// 创建一个猫类对象
Cat cat = new Cat();
// 为该猫类对象的name属性进行赋值
cat.name = "Tom";
// 为该猫类对象的age属性进行赋值
cat.age = 2;
// 调用该猫的catchMouse()方法
cat.catchMouse();
// 调用该猫继承来的eat()方法
cat.eat();
}
}
演示结果:
抓老鼠
2岁的Tom在吃东西
2.3 继承的特点一:成员变量
2.3.1 私有化(private)
- 父类中的成员,无论是公有(public)还是私有(private),均会被子类继承。
- 子类虽会继承父类私有(private)的成员,但子类不能对继承的私有成员直接进行访问,可通过继承的公有方法进行访问。如图所示
/*
* 定义动物类Animal,做为父类
*/
class Animal {
// 定义name属性
private String name;
// 定义age属性
public int age;
// 定义动物的吃东西方法
public void eat() {
System.out.println(age + "岁的" + name + "在吃东西");
}
}
/*
* 定义猫类Cat 继承 动物类Animal
*/
class Cat extends Animal {
// 定义一个猫抓老鼠的方法catchMouse
public void catchMouse() {
System.out.println("抓老鼠");
}
}
/*
* 定义测试类
*/
public class ExtendDemo01 {
public static void main(String[] args) {
// 创建一个猫类对象
Cat cat = new Cat();
// 为该猫类对象的name属性进行赋值
//cat.name = "Tom";// 编译报错
// 为该猫类对象的age属性进行赋值
cat.age = 2;
// 调用该猫的catchMouse()方法
cat.catchMouse();
// 调用该猫继承来的eat()方法
cat.eat();
}
}
2.3.2 成员变量不重名
如果子类父类中出现不重名的成员变量,这时的访问是没有影响的。代码如下:
class Fu {
// Fu中的成员变量。
int num01 = 3;
}
class Zi extends Fu {
// Zi中的成员变量
int num02 = 4;
// Zi中的成员方法
public void show() {
// 访问父类中的num,
System.out.println("num1 = " + num1);
// 访问子类中的num2
System.out.println("num2 = " + num2);
}
}
class ExtendDemo02 {
public static void main(String[] args) {
// 创建子类对象
Zi z = new Zi();
// 调用子类中的show方法
z.show();
}
}
演示结果:
num1 = 3
num2 = 4
2.3.3 成员变量重名
如果子类父类中出现重名的成员变量,这时的访问是有影响的。代码如下:
class Fu {
// Fu中的成员变量。
int num = 3;
}
class Zi extends Fu {
// Zi中的成员变量
int num = 4;
public void show() {
// 访问的num到底是子类还是父类?
System.out.println("num = " + num);
}
}
class ExtendsDemo03 {
public static void main(String[] args) {
// 创建子类对象
Zi z = new Zi();
// 调用子类中的show方法
z.show();
}
}
演示结果:
num = 4
子父类中出现了同名的成员变量时,在子类中需要访问父类中非私有成员变量时,需要使用super
关键字,修饰父类成员变量,类似于之前学过的 this
。
使用格式:
super.父类成员变量名
子类方法需要修改,代码如下:
class Zi extends Fu {
// Zi中的成员变量
int num = 6;
public void show() {
//访问父类中的num
System.out.println("Fu num=" + super.num);
//访问子类中的num
System.out.println("Zi num=" + this.num);
}
}
演示结果:
Fu num = 5
Zi num = 6
小贴士:Fu 类中的成员变量是非私有的,子类中可以直接访问。若Fu 类中的成员变量私有了,子类是不能直接访问的。通常编码时,我们遵循封装的原则,使用private修饰成员变量,那么如何访问父类的私有成员变量呢?对!可以在父类中提供公共的getXxx方法和setXxx方法。
2.4 继承的特点二:成员方法
当类之间产生了关系,其中各类中的成员方法,又产生了哪些影响呢?
2.4.1 成员方法不重名
如果子类父类中出现不重名的成员方法,这时的调用是没有影响的。对象调用方法时,会先在子类中查找有没有对应的方法,若子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法。代码如下:
class Fu{
public void show(){
System.out.println("Fu类中的show方法执行");
}
}
class Zi extends Fu{
public void show2(){
System.out.println("Zi类中的show2方法执行");
}
}
public class ExtendsDemo04{
public static void main(String[] args) {
Zi z = new Zi();
//子类中没有show方法,但是可以找到父类方法去执行
z.show();
z.show2();
}
}
2.4.2 成员方法重名——重写(Override)
如果子类父类中出现重名的成员方法,这时的访问是一种特殊情况,叫做方法重写 (Override)。
- 方法重写 :子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),会出现覆盖效果,也称为重写或者复写。声明不变,重新实现。
代码如下:
class Fu {
public void show() {
System.out.println("Fu show");
}
}
class Zi extends Fu {
//子类重写了父类的show方法
public void show() {
System.out.println("Zi show");
}
}
public class ExtendsDemo05{
public static void main(String[] args) {
Zi z = new Zi();
// 子类中有show方法,只执行重写后的show方法
z.show(); // Zi show
}
}
在父子类的继承关系当中,创建子类对象,访问成员方法的规则:
创建的对象是谁,就优先用谁,如果没有则向上找。
注意事项:
无论是成员方法还是成员变量,如果没有都是向上找父类,绝对不会向下找子类的。
重写(Override)
概念:在继承关系当中,方法的名称一样,参数列表也一样。
重写(Override):方法的名称一样,参数列表【也一样】。覆盖、覆写。
重载(Overload):方法的名称一样,参数列表【不一样】。
方法的覆盖重写特点:创建的是子类对象,则优先用子类方法。
2.4.3 重写的应用
子类可以根据需要,定义特定于自己的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现父类方法,从而进行扩展增强。比如新的手机增加来电显示头像的功能,代码如下:
class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
//智能手机类
class NewPhone extends Phone {
public void sendMessage(){
System.out.println("发短信");
System.out.println("发彩信");
}
}
public class ExtendsDemo06 {
public static void main(String[] args) {
// 创建子类对象
NewPhone np = new NewPhone();
// 调用父类继承而来的方法
np.call();
// 调用子类重写的方法
np.showNum();
}
}
注意:这里重写时,用到super.父类成员方法,表示调用父类的成员方法。
2.4.4 注意事项
-
必须保证父子类之间方法的名称相同,参数列表也相同。
@Override:写在方法前面,用来检测是不是有效的正确覆盖重写。
这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。 -
子类方法的返回值类型必须【小于等于】父类方法的返回值类型(小于其实就是是它的子类)。
-
子类方法的权限必须【大于等于】父类方法的权限修饰符。
小扩展提示:public > protected > 缺省 > private
备注:缺省不是汉字缺省,而是什么都不写,留空。
2.5 继承的特点三:构造方法
当类之间产生了关系,其中各类中的构造方法,又产生了哪些影响呢?
-
即便在子类的构造方法里没有手动使用
super
调用父类构造函数,子类也是会自动调用父类的空参数构造函数。class Animal { String name; int age; Animal() { System.out.println("不带参数的Animal被创建了"); } Animal(String name,int age) { this.name = name; this.age = age; System.out.println("有参数的Animal被创建了!!!"); } } class Dog extends Animal { Dog() { // super(); 会自动调用父类的空参数构造方法 System.out.println("不带参数的Dog被创建了"); } Dog(String name,int age) { //super(); 会自动调用父类的空参数构造方法 this.name = name; this.age = age; System.out.println("有参数的Dog被创建了!!!"); } } public class Test { public static void main(String[] args) { Dog dog1 = new Dog(); // 调用输出结果: // 不带参数的Animal被创建了 // 不带参数的Dog被创建了 Dog dog2 = new Dog("jerry",2); //调用输出结果: // 不带参数的Animal被创建了 // 有参数的Dog被创建了!!! } }
-
如果父类没有空参数构造方法,那么子类也不能有空参数构造方法。而且,子类的构造方法里必须要使用
super
手动调用父类指定的构造方法,不能再使用this.class Animal { String name; int age; Animal(String name, int age) { this.name = name; this.age = age; System.out.println("有参数的Animal被创建了!!!"); } } class Dog extends Animal { // Dog(){} 父类没有空参数构造方法,子类也不能有空参数构造方法 Dog(String name, int age) { // 子类构造方法里,不允许再使用 this,只能使用 super 调用父类的构造方法 // 原因在于,子类每次在创建对象时,都会自动执行super()调用父类的空参数构造方法 // super(); // 会自动调用父类的 super() 构造方法,但是此时父类没有空参数构造方法 // this.name = name; // this.age = age; super(name, age); } }
在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构造方法调用时,一定先调用父类的构造方法。理解图解如下:
2.5.1 super的使用
super用于子类中,用来表示父类对象。使用super.
可以调用父类的方法,访问父类的属性。
注意,如果直接写super()
表示的是调用父类的构造方法,如果要使用super()
调用父类构造方法,super()
语句必须要写在子类构造方法的第一行。
class Animal {
String name;
int age;
String color = "yellow";
Animal(){}
Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("有参数的Animal被创建了!!!");
}
public void demo() {
System.out.println("我是Animal里的demo方法");
}
}
class Dog extends Animal {
String type;
Dog() {
System.out.println("不带参数的Dog被创建了");
}
Dog(String name, int age, String type) {
// 子类的实现方式和父类一致,可以直接使用super调用父类的方法
// this.name = name;
// this.age = age;
super(name, age); // super()表示调用父类的构造方法,必须要写在子类构造方法的第一行
this.type = type;
}
void showColor(){
System.out.println(super.color); // 可以调用父类里的属性
super.demo(); // 也能够调用父类里的方法
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("jerry", 2, "哈士奇");
System.out.println(dog.type);
dog.showColor();
}
}
两种必须要使用super调用父类的构造方法的情况:
- 父类中没有空参数构造方法,此时子类的构造方法里必须使用super调用父类的构造方法。
- 父类的构造方法用到了私有变量,子类使用this无法访问到,需要使用super访问。
class Animal {
private String name;
private int age;
Animal() {
}
Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("有参数的Animal被创建了!!!");
}
}
class Dog extends Animal {
Dog() {
}
Dog(String name, int age) {
// 父类的name和age属性私有,子类无法通过this访问到
// this.name = name;
// this.age = age;
super(name, age);
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("jerry", 2);
}
}
2.5.2 继承的常见写法
class Animal {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Dog extends Animal {
Dog(String name, int age) {
super(name, age);
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("jerry", 2);
}
}
2.6 继承特点
-
Java里只允许单继承,不允许多继承。
class A{} class B{} class C extends A{} // 正确 class C extends A,B{} // 错误,最多只能有一个父类
-
Java里是允许多层继承(继承体系)的。
class A{} class B extends A{} class C extends B{}
-
子父类只是一种相对概念。一个子类可以成为别的类的父类,一个父类也可以成为其他类的子类。
3. this和super
this:当前对象
- 在构造器和非静态代码块中,表示正在new的对象
- 在实例方法中,表示调用当前方法的对象
super:引用父类声明的成员
无论是this和super都是和对象有关的。
3. 2.this和super的使用格式
- this
- this.成员变量:表示当前对象的某个成员变量,而不是局部变量
- this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
- this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
- super
- super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
- super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
- super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错
3.3.避免子类和父类声明重名的成员变量
特别说明:应该避免子类声明和父类重名的成员变量
因为,子类会继承父类所有的成员变量,所以:
-
如果重名的成员变量表示相同的意义,就无需重复声明
-
如果重名的成员变量表示不同的意义,会引起歧义
在阿里的开发规范等文档中都做出明确说明:
4. 权限修饰符
在Java中提供了三种权限修饰符,四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限。
public | protected | 缺省(什么都不写) | private | |
---|---|---|---|---|
同一类中 | √ | √ | √ | √ |
同一包中(子类与无关类) | √ | √ | √ | |
不同包的子类 | √ | √ | ||
不同包中的无关类 | √ |
可见,public具有最大权限。private则是最小权限。
编写代码时,如果没有特殊的考虑,建议这样使用权限:
- 成员变量使用
private
,隐藏细节。 - 构造方法使用
public
,方便创建对象。 - 成员方法使用
public
,方便调用方法。
5. Object 类
java.lang.Object
类是Java语言中的根类,即所有类的父类。这个类里所有方法子类都可以使用。在对象实例化的时候,最终找的父类就是Object。
如果一个类没有特别指定父类, 那么默认则继承自Object类。例如:
public class MyClass /*extends Object*/ {
// ...
}
- API(Application Programming Interface),应用程序编程接口。Java API是一本程序员的
字典
,是JDK中提供给我们使用的类的说明文档。所以我们可以通过查询API的方式,来学习Java提供的类,并得知如何使用它们。在API文档中是无法得知这些类具体是如何实现的,如果要查看具体实现代码,那么我们需要查看src源码。
根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个,今天我们主要学习其中的4个。
5.1 toString方法
方法签名:public String toString()
①默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"
②通常是建议重写
③如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()
因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。
class Person {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() { // 重写了 toString 方法
return "姓名:" + this.name + ",年龄:" + this.age;
}
}
class Test {
public static void main(String[] args) {
Person p1 = new Person("张三", 18);
// 当我们打印一个对象时,输出的结果就是这个对象 toString 方法的返回值
System.out.println(p1); // 姓名:张三,年龄:18
}
}
通常情况下打印一个对象时,默认就是调用对象的toString()
方法,如果这个对象没有重写toString()
方法,会调用Object类的toString()
方法。但是需要注意的是,打印 char 类型数组时,不会调用 char 类型数组的 toString()
方法。
注意:
char[] arr = [97,98,99];
System.out.println(arr);
System.out.println(arr.toString());
上述两段代码是有区别的。
5.2 equals方法
public boolean equals(Object obj):用于判断当前对象this与指定对象obj是否“相等”
①默认情况下,equals方法的实现等价于与“==”,比较的是对象的地址值
②我们可以选择重写,如果重写equals,那么一定要遵循如下几个原则:
- 自反性:x.equals(x)返回true
- 传递性:x.equals(y)为true, y.equals(z)为true,然后x.equals(z)也应该为true
- 一致性:只要参与equals比较的属性值没有修改,那么无论何时调用结果应该一致
- 对称性:x.equals(y)与y.equals(x)结果应该一样
- 非空对象与null的equals一定是false
class Test {
public static void main(String[] args) {
Person p1 = new Person("张三", 18);
Person p2 = new Person("张三", 18);
System.out.println(p1 == p2); // false
System.out.println(p1.equals(p2)); // true
}
}
class Person {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
return this.name == ((Person) obj).name && this.age == ((Person) obj).age;
}
}
5.3 hashCode方法
public int hashCode():返回每个对象的hash值。
如果重写equals,那么通常会一起重写hashCode()方法,hashCode()方法主要是为了当对象存储到哈希表(后面集合章节学习)等容器中时提高存储和查询性能用的,这是因为关于hashCode有两个常规协定:
- ①如果两个对象的hash值是不同的,那么这两个对象一定不相等;
- ②如果两个对象的hash值是相同的,那么这两个对象不一定相等。
重写equals和hashCode方法时,要保证满足如下要求:
-
①如果两个对象调用equals返回true,那么要求这两个对象的hashCode值一定是相等的;
-
②如果两个对象的hashCode值不同的,那么要求这个两个对象调用equals方法一定是false;
-
③如果两个对象的hashCode值相同的,那么这个两个对象调用equals可能是true,也可能是false
public static void main(String[] args) {
System.out.println("Aa".hashCode());//2112
System.out.println("BB".hashCode());//2112
}
5.4 getClass方法
public final Class<?> getClass():获取对象的运行时类型对象
因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法
public static void main(String[] args) {
Object obj = new Person();
System.out.println(obj.getClass());//运行时类型
}
面向对象-多态
学习目标
- 能够说出什么是多态
- 能够说出多态的特点以及作用
- 能够说出引用数类型转换方式作用以及注意事项
- 能够使用instanceof关键字判断一个对象是否属于某一个类
1. 多态
多态是继封装、继承之后,面向对象的第三大特性。
生活中,比如一条叫旺财的狗,我们可以说它是一条狗,也可以说它是一个动物;一个叫张三的男性,我们可以称它为男人,也可以称它为人,同时还能称呼他是一个动物。
多态: 就是指同一事物,具有多个不同表现形式。
1.1 前提
- 继承或者实现【二选一】
- 方法的重写【意义体现:不重写,无意义】
- 父类引用指向子类对象【格式体现】
class Test {
public static void main(String[] args) {
// 父类的引用 stu 指向了一个 new Student() 子类对象
Person stu = new Student();
stu.sleep(); // 子类重写了父类的方法,执行的是子类自己的方法
}
}
class Person {
void sleep() {
System.out.println("人正在睡觉");
}
}
class Student extends Person {
@Override
void sleep() {
System.out.println("学生正在教室里睡觉");
}
}
1.2 多态的意义
查看一下代码,思考这段代码是否可以进行优化:
class Test {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
func(dog);
func(cat);
}
private static void func(Dog dog) {
dog.eat();
}
private static void func(Cat cat) {
cat.eat();
}
}
class Dog {
public void eat() {
System.out.println("小狗正在啃骨头");
}
}
class Cat {
public void eat() {
System.out.println("小猫正在吃鱼");
}
}
上述代码中,根据不同的参数类型,我们定义了不同的func
方法,这个方法出现了重载。但是,我们从func
方法的内部实现上来看,这两个方法都是调用对象的eat
方法。
其实,在上述代码里,我们可以考虑给Dog
类和Cat
类定义一个父类,通过传递父类参数的形式,来简化代码。
实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。
class Test {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
func(dog);
func(cat);
}
private static void func(Animal animal) { // 参数只需要指定 Animal 父类即可
animal.eat();
}
}
class Animal{ // 定义一个 Animal 类,让Dog和Cat都继承自它
public void eat() {
System.out.println("动物正在吃东西");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("小狗正在啃骨头");
}
}
class Cat extends Animal {
public void eat() {
System.out.println("小猫正在吃鱼");
}
}
上述代码的简化,并不只是简单的替换,同时还是一种扩展。当我们再出现新类型(例如Pig类)的对象时,我们只需要让Pig类继承自Animal类,就可以直接把这个Pig类型的对象传给func
方法作为参数使用。这样就大大的提高了代码的灵活性,便于代码后期的扩展。
实际上,多态在我们程序中是大量存在的,因为有了多态的存在,才使得Java语言变得更加的灵活以扩展,实现了低耦合高内聚的编码思想。
1.3 多态的特点
父类引用指向子类对象的这种多态形式,在调用方法和使用属性时,有一些细节需要我们注意。
1.3.1 成员方法的特点
观察一下代码,思考代码的执行结果。
package com.atguigu.java;
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();
animal.eat(); // 会调用 Dog 类的 eat 方法
// animal.watchDog(); 编译会报错
((Dog) animal).watchDog();
}
}
class Animal { // 定义一个 Animal 类,让Dog和Cat都继承自它
public void eat() {
System.out.println("动物正在吃东西");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("小狗正在啃骨头");
}
public void watchDog() {
System.out.println("小狗正在看家");
}
}
对于调用方法,记住一点编译看左边,运行看右边。
也就是说,在编译时要看等号左边声明的类型里是否有这个方法,如果有这个方法就编译通过,反之不通过。而在运行时,是看等号右边到底创建的是一个什么类型的对象,会直接调用这个对象具体的方法。
1.3.2 成员变量的特点
直接通过对象名称访问成员变量,等号左边是谁,优先用谁。
public class Test {
public static void main(String[] args) {
Animal animal = new Dog();
System.out.println(animal.type); // 动物
}
}
class Animal { // 定义一个 Animal 类,让Dog和Cat都继承自它
String type = "动物";
public void eat() {
System.out.println("动物正在吃东西");
}
}
class Dog extends Animal {
String type = "狗";
@Override
public void eat() {
System.out.println("小狗正在啃骨头");
}
}
1.4 引用数据类型转换
多态的转型分为向上转型与向下转型两种:
-
向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。
使用格式:
父类类型 变量名 = new 子类类型(); 如:Animal a = new Cat();
-
向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。
使用格式:
子类类型 变量名 = (子类类型) 父类变量名; 如:Cat c =(Cat) a;
1.4.1 类型转换的意义
调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。
public class Test {
public static void main(String[] args) {
Animal dog = new Dog();
// dog.watchDoor(); 编译报错,调用方法时,编译时看左边的类型,是Animal,Animal类里没有 watchDoor方法
((Dog) dog).watchDoor();
}
}
class Animal {
String type = "动物";
}
class Dog extends Animal {
public void watchDoor() {
System.out.println("狗正在看门");
}
}
1.4.2 转型的异常
转型的过程中,一不小心就会遇到这样的问题,请看如下代码:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse 【运行报错】
}
}
这段代码可以通过编译,但是运行时,却报出了 ClassCastException
,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,格式如下:
变量名 instanceof 数据类型
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false。
所以,转换前,我们最好先做一个判断,代码如下:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
if (a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
} else if (a instanceof Dog){
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse
}
}
}
class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse 【运行报错】
}
}
这段代码可以通过编译,但是运行时,却报出了 `ClassCastException` ,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。
为了避免ClassCastException的发生,Java提供了 <span style="color:red">instanceof</span> 关键字,给引用变量做类型的校验,格式如下:
```java
变量名 instanceof 数据类型
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false。
所以,转换前,我们最好先做一个判断,代码如下:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
if (a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
} else if (a instanceof Dog){
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse
}
}
}