网站首页 > java教程 正文
在通过一个示例来讨论它们之前,让我们先分别快速了解一下擦除和重载。
擦除概述
Java在编译时使用类型擦除来强制类型约束和与旧字节码的向后兼容性。基本上,在编译时,所有的类型参数都被替换为Object(任何泛型都必须可以转换为Object)或类型边界(extends或super)。接下来,在运行时,编译器擦除的类型将被我们的类型替换。类型擦除的一个常见情况涉及泛型。
泛型类型的擦除
实际上,编译器将无界类型(如E、T、U等)用有界Object替换。这通过以下类型擦除的类示例来强制类型安全:
public class ImmutableStack<E> implements Stack<E> {
private final E head;
private final Stack<E> tail;
// ...
编译器应用类型擦除将E替换为Object:
public class ImmutableStack<Object> implements Stack<Object> {
private final Object head;
private final Stack<Object> tail;
// ...
如果E参数被绑定,则编译器使用第一个绑定类。例如,在类`class Node<T extends Comparable<T>> {...}`中,编译器会将T替换为Comparable。同样,在类`class Computation<T extends Number> {...}`中,T的所有出现都将被编译器替换为上限Number。
检查以下情况,这是方法类型擦除的经典案例:
public static <T, R extends T> List<T> listOf(T t, R r) {
List<T> list = new ArrayList<>();
list.add(t);
list.add(r);
return list;
}
// 使用这个方法
List<Object> list = listOf(1, "one");
这是如何工作的?当我们调用`listOf(1, "one")`时,我们实际上是将两种不同的类型传递给了泛型参数T和R。编译器的类型擦除将T替换为Object。这样,我们就可以在ArrayList中插入不同的类型,代码运行得很好。
擦除和桥接方法
桥接方法是编译器为覆盖边缘情况而创建的。具体来说,当编译器遇到参数化接口的实现或参数化类的扩展时,它可能需要生成一个桥接方法(也称为合成方法),作为类型擦除阶段的一部分。例如,考虑以下参数化类:
public class Puzzle<E> {
public E piece;
public Puzzle(E piece) {
this.piece = piece;
}
public void setPiece(E piece) {
this.piece = piece;
}
}
以及这个类的扩展:
public class FunPuzzle extends Puzzle<String> {
public FunPuzzle(String piece) {
super(piece);
}
@Override
public void setPiece(String piece) {
super.setPiece(piece);
}
}
类型擦除将`Puzzle.setPiece(E)`修改为`Puzzle.setPiece(Object)`。这意味着`FunPuzzle.setPiece(String)`方法并没有重写`Puzzle.setPiece(Object)`方法。由于方法的签名不兼容,编译器必须通过桥接(合成)方法来适应泛型类型的多态性,以确保子类型按预期工作。让我们在代码中突出显示这个方法:
/* Decompiler 8ms, total 3470ms, lines 18 */
package modern.challenge;
public class FunPuzzle extends Puzzle<String> {
public FunPuzzle(String piece) {
super(piece);
}
public void setPiece(String piece) {
super.setPiece(piece);
}
// $FF: synthetic method
// $FF: bridge method
public void setPiece(Object var1) {
this.setPiece((String)var1);
}
}
现在,每当你在堆栈跟踪中看到桥接方法时,你就知道它是什么以及为什么在那里。
类型擦除和堆污染
你是否见过未检查的警告?我确定你见过!这是所有Java开发人员都会遇到的问题之一。它们可能在编译时作为类型检查的结果出现,或在运行时作为类型转换或方法调用的结果出现。在这两种情况下,我们谈论的是编译器无法验证涉及某些参数化类型的操作的正确性。并非每个未检查的警告都是危险的,但确实存在我们必须考虑和处理的情况。
堆污染的一个特例是,当某个类型的参数化变量指向一个不是该类型的对象时,我们就容易遇到导致堆污染的代码。涉及varargs参数的方法的一个很好的候选者是这样的场景。检查以下代码:
public static <T> void listOf(List<T> list, T... ts) {
list.addAll(Arrays.asList(ts));
}
listOf()的声明将导致此警告:可能的堆污染来自参数化varargs类型T。那么这里发生了什么?故事从编译器将形式T...参数替换为数组开始。在应用类型擦除后,T...参数变为T[],并最终变为Object[]。因此,我们为可能的堆污染打开了一扇门。但是,我们的代码只是将Object[]的元素添加到了List<Object>中,所以我们处于安全区域。
换句话说,如果你知道varargs方法的主体不容易生成特定异常(例如,ClassCastException)或在不适当的操作中使用varargs参数,那么我们可以指示编译器抑制这些警告。我们可以通过@SafeVarargs注解来实现这一点,如下所示:
@SafeVarargs
public static <T> void listOf(List<T> list, T... ts) { … }
@SafeVarargs是一个提示,表明被注解的方法将仅在适当的操作中使用varargs形式参数。更常见但不太推荐的是使用@SuppressWarnings({"unchecked", "varargs"}),它只是简单地抑制此类警告,而不声明varargs形式参数不会在不适当的操作中使用。
现在,让我们来处理这段代码:
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
Main.listOf(ints, 1, 2, 3);
Main.listsOfYeak(ints);
}
public static void listsOfYeak(List<Integer>... lists) {
Object[] listsAsArray = lists;
listsAsArray[0] = Arrays.asList(4, 5, 6);
Integer someInt = lists[0].get(0);
listsAsArray[0] = Arrays.asList("a", "b", "c");
Integer someIntYeak = lists[0].get(0); // ClassCastException
}
这次,类型擦除将List<Integer>...转换为List[],这是Object[]的子类型。这允许我们进行赋值:`Object[] listsAsArray = lists;`。但是,请查看最后两行代码,其中我们创建了一个List<String>并将其存储在`listsAsArray[0]`中。在最后一行,我们尝试从`lists[0]`访问第一个Integer,这显然会导致ClassCastException。这是使用varargs的不当操作,因此不建议在这种情况下使用@SafeVarargs。我们应该认真对待以下警告:
// unchecked generic array creation for varargs parameter
// of type java.util.List<java.lang.Integer>[]
Main.listsOfYeak(ints);
// Possible heap pollution from parameterized vararg
// type java.util.List<java.lang.Integer>
public static void listsOfYeak(List<Integer>... lists) { … }
现在,你已经熟悉了类型擦除,让我们简要地介绍一下多态重载。
多态重载概述
由于重载(也称为“即席”多态性)是面向对象编程(OOP)的核心概念,我相信你对Java方法重载很熟悉,所以我不会坚持这个概念的基本理论。
我也知道有些人不同意重载是一种多态形式,但那是另一个不会在这里讨论的话题。我们将更加实际,并跳入一系列旨在突出重载某些有趣方面的测验。更具体地说,我们将讨论类型优势。让我们处理第一个测验(wordie是一个最初为空的字符串):
static void kaboom(byte b) { wordie += "a";}
static void kaboom(short s) { wordie += "b";}
kaboom(1);
会发生什么?如果你回答编译器会指出找不到适合kaboom(1)的方法,那么你是对的。编译器寻找一个接受整数参数的方法,kaboom(int)。好的,这很简单!下一个:
static void kaboom(byte b) { wordie += "a";}
static void kaboom(short s) { wordie += "b";}
static void kaboom(long l) { wordie += "d";}
static void kaboom(Integer i) { wordie += "i";}
kaboom(1);
我们知道前两个kaboom()是无用的。那么kaboom(long)和kaboom(Integer)呢?你是对的,将调用kaboom(long)。如果我们删除kaboom(long),则调用kaboom(Integer)。
在原始类型重载中,编译器首先寻找一对一的匹配。如果这种尝试失败,编译器将寻找接受比当前域更广泛的原始域的重载版本(例如,对于int,它寻找int、long、float或double)。如果这也失败,编译器将检查接受装箱类型(Integer、Float等)的重载。
以下面的情况为例:
static void kaboom(Integer i) { wordie += "i";}
static void kaboom(Long l) { wordie += "j";}
kaboom(1);
这次,wordie将会是"i"。调用了kaboom(Integer),因为不存在kaboom(int/long/float/double)。如果我们有一个kaboom(double),则该方法将比kaboom(Integer)具有更高的优先级。
在装箱类型重载中,编译器首先寻找一对一的匹配。如果失败,编译器将不会考虑任何接受比当前域更广泛的装箱类型的重载版本(当然,更狭窄的域也被忽略)。它查找Number作为所有装箱类型的超类。如果找不到Number,编译器将向上遍历层次结构,直到达到java.lang.Object,这是道路的尽头。
现在让我们稍微复杂一点:
static void kaboom(Object... ov) { wordie += "o";}
static void kaboom(Number n) { wordie += "p";}
static void kaboom(Number... nv) { wordie += "q";}
kaboom(1);
这次,哪个方法将被调用?你可能会想到kaboom(Number),对吧?至少,我的简单逻辑让我认为这是一个常识性的选择。而且它是正确的!如果我们移除kaboom(Number),编译器将调用varargs方法kaboom(Number...)。这是有道理的,因为kaboom(1)使用了一个参数,所以kaboom(Number)应该比kaboom(Number...)具有更高的优先级。但是,如果我们调用kaboom(1,2,3),逻辑就会反转,因为kaboom(Number)不再代表这次调用的有效重载,而kaboom(Number...)是正确的选择。
但是,这种逻辑之所以适用,是因为Number是所有装箱类(Integer、Double、Float等)的超类。现在考虑以下情况:
static void kaboom(Object... ov) { wordie += "o";}
static void kaboom(File... fv) { wordie += "s";}
kaboom(1);
这次,编译器将“绕过”kaboom(File...)并调用kaboom(Object...)。基于相同的逻辑,调用kaboom(1, 2, 3)也会调用kaboom(Object...),因为没有kaboom(Number...)。
在重载中,如果调用具有单个参数,则具有单个参数的方法比其varargs对应项具有更高的优先级。另一方面,如果调用具有多个相同类型的参数,则调用varargs方法,因为具有单个参数的方法不再适用。当调用具有单个参数但只有varargs重载可用时,将调用该方法。
这导致了以下示例:
static void kaboom(Number... nv) { wordie += "q";}
static void kaboom(File... fv) { wordie += "s";}
kaboom();
这次,kaboom()没有参数,编译器无法找到唯一匹配项。这意味着对kaboom()的引用是模糊的,因为两个方法都匹配(modern.challenge.Main中的kaboom(java.lang.Number...)和modern.challenge.Main中的kaboom(java.io.File...))。
在捆绑的代码中,你可以进一步探索多态重载并测试你的知识。此外,尝试挑战自己,并在等式中引入泛型。
擦除与重载
现在,基于之前的经验,检查以下代码:
void print(List<A> listOfA) {
System.out.println("Printing A: " + listOfA);
}
void print(List<B> listofB) {
System.out.println("Printing B: " + listofB);
}
会发生什么?这是一个擦除和重载冲突的情况。类型擦除将List<A>替换为List<Object>,并将List<B>替换为List<Object>。因此,重载是不可能的,我们会得到一个错误,如“名称冲突:print(java.util.List<modern.challenge.B>)和print(java.util.List<modern.challenge.A>)具有相同的擦除。”
为了解决这个问题,我们可以向这两个方法中的一个添加一个虚拟参数:
void print(List<A> listOfA, Void... v) {
System.out.println("Printing A: " + listOfA);
}
现在,我们可以对这两个方法进行相同的调用:
new Main().print(List.of(new A(), new A()));
new Main().print(List.of(new B(), new B())); // 注意:这里实际上会编译错误,因为第二个print方法已被修改
但是请注意,上面的第二个调用实际上会导致编译错误,因为我们修改了第二个print方法以接受一个额外的Void...参数。通常,为了处理类型擦除导致的重载问题,你会通过改变方法名称、添加不同类型的参数或使用其他技术来区分它们。
猜你喜欢
- 2024-11-01 Java | 深入理解方法调用的本质(含重载与重写区别)
- 2024-11-01 Java中的方法重载和方法重写的区别
- 2024-11-01 经典回答:《重载和重写的区别》Java面试冲击月薪40K高薪
- 2024-11-01 java 核心技术-12版 卷Ⅰ- 4.6 对象构造 4.6.1重载
- 2024-11-01 Java语言基础图解-第二阶段(继承-重载-重写-多态-抽象-接口)
- 2024-11-01 阿瑟Java (17):重载、重写有区别吗?
- 2024-11-01 Java基础——构造器重载 & this关键字
- 2024-11-01 「Java面试题」常规Java面试题分享
- 2024-11-01 Java每日一题之重载和重写有什么区别?
- 2024-11-01 Java里方法重写override与方法重载overload有什么区别?
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)