向上类型转换
对于引用变量,在程序中有两种形态:一种是编译时类型,这种引用变量的类型在声明它的时候就决定了;另一种则是运行时类型,这种变量的类型由实际赋给它的对象决定。
当一个引用变量的编译时类型和运行时类型不一致时,就出现了多态(Polymorphism)
对面向对象语言来说,所有的对象(Object),或者说类的实例,本质上都是引用变量。因此,多态最主要就是针对对象来说的:声明时引用变量指向的对象的类型,和运行时引用变量指向的对象的类型不一致。
class BaseClass
{
public int book = 6;
public void base()
{
System.out.println("父类的普通方法");
}
public void test()
{
System.out.println("父类被覆盖的方法");
}
}
public class SubClass extends BaseClass
{
// 覆盖
public String book = "Java疯狂讲义"; // 同名实例
public void test()
{
System.out.println("子类的覆盖父类的方法");
}
public void sub()
{
System.out.println("子类的普通方法");
}
public static void main(String[] args)
{
var bc = new BaseClass(); // 声明一个BaseClass的对象,编译时和运行时的类型一致,不存在多态
System.out.println(bc.book); // 6
bc.base(); // 父类的方法
bc.test(); // 父类的方法
//-------------
var sc = new SubClass(); // 声明一个SubClass的对象,同样不存在多态
System.out.println(sc.book); // 子类实例变量覆盖了父类的实例变量,输出"Java疯狂讲义"
sc.base(); // 子类方法覆盖了父类的方法
sc.test(); // 子类的普通方法
//-------------
BaseClass polymophicBC = new SubClass(); // 编译时类型是BaseClass,运行时类型是SubClass,发生了多态
System.out.println(polymophicBC.book); // 输出6,是父类的实例变量
polymophicBC.base(); // 执行父类的base方法
polymophicBC.test(); // 执行当前类,也就是运行时类型SubClass的test方法
// polymophicBC的编译时类型是BaseClass,没有提供sub方法
// 因此调用sub方法时会出现编译错误
// polymophicBC.sub();
}
}
- 28~31行是标准的对象的声明与使用;
- 33~36行是标准的继承;
- 38~41行出现了多态。
对变量
polymophicBC
来说,编译时类型是BaseClass
(声明语句左端),运行时类型是SubClass
(声明语句右端)。把一个子类对象赋给一个父类引用变量,在这个过程中发生了什么?
类型转换。
多态在Java中实现的机制就是把子类对象赋值给父类引用变量,这实际上就是一种类型装换,具体也叫向上转型(up-casting)。这种类型转换由系统自动完成。
从声明语句左边来看:
- BaseClass bc = new BaseClass();
- BaseClass polymophicBC = new SubClass();
bc
和polymophicBC
都是BaseClass
引用类型,但是它俩在执行同名函数test()
时却产生了不同的结果。这种调用同一个方法却出现不同行为特征的现象,就是多态。
多态机制下,父类引用变量在运行时总是调用子类的方法,也就是说呈现出子类的行为特征而不是父类的行为特征。
对象的实例变量不具有多态性。
- 第39行,
book
仍然是父类的实例变量。
引用变量在编译阶段只能调用其编译时类型拥有的方法,但是在运行时可以执行其运行时类型拥有的方法。
- 第44行,
BaseClass
不具有sub()
方法,因此不能调用,发生编译错误; - 但第41行,
BaseClass
具有test()
方法,因此可以调用,且在运行时执行的是SubClass
的同名方法。
使用
var
时,并不能改变编译时类型,因此也可能会发生多态:
var v1 = new SubClass(); // 自动推断是SubClass,没有多态
var v2 = polymophicBC(); // 赋值,v2自动推断是BaseClass
// 此时调用sub方法,遵照多态机制,会发生编译错误
// v2.sub();
强制类型转换
按照上面规则,引用变量只能调用编译时类型拥有的方法,即使它的运行时类型对象实际上包含了远不止这些方法。
如何让这个引用变量调用运行时类型所拥有的方法呢?
既然普通的多态依赖的是向上转型,即把子类对象赋给父类引用变量,类似于我们把double基本变量赋给float。那么也可以反过来,执行强制类型转换。
强制类型转换借助类型转换运算符,和C++类似,就是()
。
类型转换运算符可以实现基本类型之间的转型,也能实现引用变量的转型。
请注意,强制类型转换不是万能的,受到如下约束:
- 基本类型之间转型只能在数值类型中进行(整数型、字符型、浮点型)。数值型和布尔型之间不能转换(C++中是可以的)。
- 引用类型转换只能在具有继承关系(直接继承或间接继承都行)的类型之间进行。
强制类型转换在这里,就是把父类实例转换为子类类型。即其编译时类型是父类类型,运行时类型是子类类型。这时候可以使用强制类型转换。
public class ConversionTest
{
public static void main(String[] args)
{
var d = 13.4; // float
var l = (long) d; // 强制类型转换
//------
var in = 5; // int
// var b = (boolean) in; // 错误,数值型不能转换为布尔型
//------
Object obj = "Hello"; // 向上转型,"Hello"是String,是Object的子类。这实际上就是多态,只不过这时候obj不能执行String拥有的方法
var objStr = (String) obj; // 强制类型转换,父类/基类和子类,正常
System.out.println(objStr); // 做为String类型输出
//------
Object objPri = Integer.valueOf(5); // 向上转型,运行时类型是Integer
var in = (Integer) objPri; // 强制类型转换,基类和子类,正常
// var str = (String) objPri; // objPri运行时时Integer,和String不存在继承关系,运行时会报错(类型转换异常,ClassCastException)
}
}
再解读一下第12行:
- 对
objStr
,虽然使用了var
,但由于使用了强制类型转换符(String)
,自动推断它是String
类型;- 此时
obj
是Object
类型;- 因此,将
obj
赋值给objStr
,实际上是把父类对象赋值给子类引用变量,这就和之前的upcasting正好相反,我们也可以称之为downcasting
小结
- 把子类对象(右)赋给父类引用变量(左)时,触发向上转型,这种转型是自动的、总是成功的。这种转型表明这个引用变量编译时是父类类型,运行时是子类类型。它表现出的是子类的行为方式,但是编译时不能调用子类的方法。同时,实例变量仍然是父类的。
- 使用强制类型转换可以把一个引用变量转换成其子类类型。这种转换必须是显式的,而且不一定成功(若两端不存在继承关系)。
instanceof
使用instanceof
运算符可以判断是否可以执行类型转换,以避免出现ClassCastException
:
if (objPri instanceof String)
{
var str = (String) objPri;
}
instanceof
用来判断前面的对象是否是后面的类或者其子类的实例,是的话返回true
,否则返回false
。
在Java 17中,为instanceof
增加了快捷用法,来简化上面的判断代码块:
// 传统instanceof,先判断,再转换,最后使用
if (obj instanceof String) // 先判断
{
var s = (String) obj; // 再转换
System.out.println(s.toUpperCase()); // 最后使用
}
// Java 17的模式匹配,同时完成判断和类型转换
if (obj instanceof String s)
{
System.out.println(s.toUpperCase());
}