Java学习之旅第二季-18:转型
之前在介绍继承时,提到到两个类具有 “is-a” 的关系时,就适合使用继承。就是说一个子类的实例是一个父类的类型。那么如果是使用代码表达这个意思,就应该这么实现:
Shape shape = new Square(1); // 实例化一个边长为 1 的正方形
这句话理解为:将一个Square类型的实例赋值给Shape类型的变量。口语化理解就是:一个正方形是图形。
这就是一种类型的转换,可以称为转型,在 Java 中支持两种转型:向上转型与乡下转型。这两种方式我们在介绍基本数据类型之间的互相转换时见过。不过现在我们关注的是引用数据类型之间的转换。
18.1 向上转型
所谓向上转型(upcasting),就是指将子类的实例赋值给父类类型的变量。
上面的例子就是向上转型的例子,看起来似乎并没有什么特别之处。但是如果将父类型作为参数或者作为方法的返回值类型就能很灵活的实现很多功能。
案例:比较两个图形之间面积的大小
这个需求中并没有明确是哪两种具体的图形,可能是正方形,长方形,三角形,圆形,椭圆等这些之间,甚至可能还有未知的图形,那么如果针对具体图象实现计算面积的方法,完全没法实现。
这时我们可以考虑使用它们的父类型作为参数实现比较逻辑,然后传入这个父类型的子类型实例。
下面按照这个思路先声明父类型,由于具体的面积计算规则不合适再父类实现,所以使用抽象类及抽象方法很合适:
public abstract class Shape {public abstract double getArea();
}
接下来,声明一个操作类,并声明一个静态方法用于实现案例中的需求:
public class ShapeOpr {/*** 比较两个图形的面积大小** @param shape1 第一个图形* @param shape2 第二个图形* @return 如果第一个图形面积大于第二个图形面积,返回 1;* 如果第一个图形面积小于第二个图形面积,返回 -1;* 如果第一个图形面积等于第二个图形面积,返回 0;*/public static int compareTo(Shape shape1, Shape shape2) {if (shape1.getArea() > shape2.getArea()) {return 1;} else if (shape1.getArea() < shape2.getArea()) {return -1;}return 0;}
}
有了上述的方法,我们就可以任意传入 Shape 类的子类实例,如下面有一个正方形类和一个圆形类:
public class Square extends Shape {private double size; // 边长public Square(double size) {this.size = size;}@Overridepublic double getArea() {return size * size;}
}public class Circle extends Shape {private double radius; // 半径public Circle(double radius) {this.radius = radius;}@Overridepublic double getArea() {return Math.PI * radius * radius;}
}
编写测试类,在main方法中直接创建上述两个图形的实例,传入比较的方法中,就能得到结果。
static void main() {Shape square = new Square(2);Shape circle = new Circle(1);int result = ShapeOpr.compareTo(square, circle);System.out.println(result);
}
可以体会下这种针对父类实现逻辑的写法,只要写一个方法,对于任意子类型的实例都可以传入,父类型的方法调用最终会应用到子类型的实例上。这体现的其实是面向对象的特征之一:多态。
对于向上转型的写法存在的一个缺点是:通过父类型声明的对象无法访问到子类型特有的方法。
比如在Square类中有一个计算周长的方法:
public class Square extends Shape {private double size;public Square(double size) {this.size = size;}@Overridepublic double getArea() {return size * size;}/*** 计算正方形周长** @return 周长*/public double getPerimeter() {return size * 4;}
}
那么按照下面的写法,将无法调用到计算周长的方法:
Shape square = new Square(2);
square.getPerimeter(); // 编译错误
这种情况下得使用向下转型的语法,将父类行的变量强行转型为子类型。
18.2 向下转型
与向上转型相反的是向下转型(downcasting),它是指将父类的实例赋值给子类型的变量。
使用的语法类似于范围大的数值类型的数据赋值给范围小的数值类型变量,需要在数据前使用小括号,其中放的是范围小的数值类型。如:
int num = (int)1.23; // 强制类型转换
引用数据类型的情况类似,在做向下转型时,也需要使用强制类型转换,且运行时还会检查,此时有可能抛出类转换异常(java.lang.ClassCastException)。比如:
Shape shape = new Circle(2); // 向上转型
Circle circle = (Circle) shape; // 向下转型
但是携程下面的代码,编译时也不会出错:
Shape shape = new Circle(2); // 向上转型
Square square = (Square) shape; // 向下转型,编译无措但运行时出错
在上面第2行的向下转型时,会出现异常,出错信息提示 Circle 不能转型为 Square :
Exception in thread "main" java.lang.ClassCastException: class com.laotan.article18.Circle cannot be cast to class com.laotan.article18.Square
为了能顺利的向下转型成功,需要在转型前做一个判断,确定是否可以进行向下转型,这就是Java提供的操作符 Instanceof,它也是一个关键字。
18.3 instanceof 操作符
该关键字判断一个对象的数据类型是否是某类型或其子类型,这样能确保向下转型时不会出错。它会同时做编译时和运行时的检查,例如:
Shape shape = new Circle(2); // 向上转型
if (shape instanceof Square) { // falseSquare square = (Square) shape;
}
if (shape instanceof Circle) { // trueCircle circle = (Circle) shape;
}
经过 instanceof 的运算,就能确保后续向下转型的成功。
关于instanceof 的几个注意点:
- null不属于任何类型,即 instanceof 后无论是什么引用类型,结果永远为false
boolean b1 = null instanceof String; // false
boolean b2 = null instanceof Integer; // false
- 声明对象的类与目标类之间是父子类的关系,比如刚才声明shape对象的类Shape与目标类Square会哦Circle都是父子关系,如果没有任何关系,就不能使用该关键字:
Shape shape = new Circle(2);
if(shape instanceof String){ // 语法错误
}
上面第2行的代码编译时就会出错,因为Shape类与String类并不是父子关系,instanceof 在这里不适应。
instanceof 比较常用的场景是重写Object类的equals的方法时,可以参考
18.4 instanceof 模式匹配
从 Java 16开始引入了instanceof 模式匹配的语法,意思是如果对象经过 instanceof 判断能够匹配目标类型,则对应分支中无需再做类型强转,直接使用其后的变量接收即可,如下所示:
Shape shape = new Circle(2);
if (shape instanceof Square square) {System.out.println(square.getArea());
}
if (shape instanceof Circle circle) {System.out.println(circle.getArea());
}
上述读2行与第4行的写法就是模式匹配的体现,如果哪个分支成立,则最后声明的变量就是向下转型之后的变量,在后续代码块中直接使用即可。
使用该变量时需要注意其作用域:
-
可以在if中使用逻辑与(&&)运算,但不能使用 &
-
也可以用于 while 和 for 循环中,但用于do-while结构时需留意
public void method(Object obj) {if (obj instanceof String s) {System.out.println(s.length());} else if (obj instanceof Integer i && i > 0) { // 先转型赋值,未成功则短路;使用&则无短路效果System.out.println(i.intValue());} else {System.out.println("Error");}for (int num = 0; (obj instanceof Integer i) && i > 0; num++) {System.out.println(i);}do {System.out.println(i); // 编译错误,执行第一次循环时 i 还未声明} while ((obj instanceof Integer i) && i > 0);
}
18.5 switch 模式匹配
在 Java 21 中引入了switch 模式匹配的语法,这种语法允许在 switch 语句中使用类型检查和模式匹配。比如:
public void method(Object obj) {if (obj == null) {return;}switch (obj) {case String s -> System.out.println(s.length());case Integer i -> System.out.println(i.intValue());case Object o-> System.out.println();}
}
在switch后的小括号中传入对象,则可以在case后进行类型的对比。如果某一条成立,即向下转型为类型后对应的变量,在 -> 后可以直接使用。
语法注意点:
- case后的类型需要注意顺序,子类型在父类型之前,比如第8行放在第6行之前就不成立
- 不能出现重复或导致不可到达后面case的类型
- 可以增加一个 default 语句,但在本例中其实与第8行出现了重复,如果替换掉第8行与Object的对比,效果是相同的
public void method(Object obj) {if (obj == null) {return;}switch (obj) {case String s -> System.out.println(s.length());case Integer i -> System.out.println(i.intValue());default -> System.out.println();}
}
18.6 小结
本小节介绍了Java中的向上转型与向下转型机制。向上转型是将子类实例赋值给父类变量,体现多态性,适用于统一处理不同子类对象的场景(如比较图形面积)。但父类变量无法调用子类特有方法,需通过向下转型解决。向下转型需强制类型转换,但可能引发 ClassCastException,因此需配合 instanceof 进行类型检查。Java 16引入的模式匹配语法可简化转型代码,自动完成类型转换与作用域限定。最后还介绍了Java 21引入的switch模式匹配语法。