我想更好地了解当Java编译器遇到对如下方法的调用时会发生什么。
<T extends AutoCloseable & Cloneable>
void printType(T... args) {
System.out.println(args.getClass().getComponentType().getSimpleName());
}
// printType() prints "AutoCloseable"
我很清楚,没有类型
无论如何,如果切换了接口的顺序,结果仍然是一样的。
<T extends Cloneable & AutoCloseable>
void printType(T... args) {
System.out.println(args.getClass().getComponentType().getSimpleName());
}
// printType() prints "AutoCloseable"
这让我做了更多的调查,看看当接口发生变化时会发生什么。在我看来,编译器使用某种严格的顺序规则来决定哪个接口是最重要的,而接口在代码中出现的顺序并不起作用。
<T extends AutoCloseable & Runnable> // "AutoCloseable"
<T extends Runnable & AutoCloseable> // "AutoCloseable"
<T extends AutoCloseable & Serializable> // "Serializable"
<T extends Serializable & AutoCloseable> // "Serializable"
<T extends SafeVarargs & Serializable> // "SafeVarargs"
<T extends Serializable & SafeVarargs> // "SafeVarargs"
<T extends Channel & SafeVarargs> // "Channel"
<T extends SafeVarargs & Channel> // "Channel"
<T extends AutoCloseable & Channel & Cloneable & SafeVarargs> // "Channel"
问:当有多个边界时,Java编译器如何确定参数化类型的varargs数组的组件类型?
我甚至不确定JLS是否对此说了什么,我通过谷歌搜索找到的信息都没有涵盖这个特定的主题。
这是一个非常有趣的问题。本规范的相关部分为§15.12.4.2。评估参数:
如果所调用的方法是一个变量arity方法m
,则它必须有n个变量
如果用k调用m
≠ n个实际参数表达式,或者,如果使用k=n个实际参数表达式调用m
,并且第k个参数表达式的类型与T[]
的赋值不兼容,然后,论点列表(e
e
1
>,,,,
e<代码>e
n-1
}
),其中|
T[]
|表示对
T[]
的擦除(§4.6)。
有趣的是,关于“some
T
”实际上是什么,它很模糊。最简单、最直接的解决方案是调用方法的声明参数类型;这将与分配兼容,并且使用不同类型没有实际优势。但是,正如我们所知,javac
并没有走这条路,而是对所有参数使用某种公共基类型,或者根据数组元素类型的未知规则选择一些边界。现在,您甚至可能会发现一些应用程序依赖于这种行为,假设通过检查数组类型在运行时获得有关实际T
的一些信息。
这导致了一些有趣的后果:
static AutoCloseable[] ARR1;
static Serializable[] ARR2;
static <T extends AutoCloseable & Serializable> void method(T... args) {
ARR1 = args;
ARR2 = args;
}
public static void main(String[] args) throws Exception {
method(null, null);
ARR2[0] = "foo";
ARR1[0].close();
}
javac
决定在这里创建一个实际类型为Serializable[]
的数组,尽管在应用类型擦除后,该方法的参数类型是AutoClosable[]
,这就是为什么可以在运行时分配字符串的原因。因此,它只会在最后一条语句中失败,即尝试使用
线程“main”java中出现异常。lang.CompatibleClassChangeError:类java。String不实现请求的java接口。lang.AutoCloseable
虽然我们可以将任何
可序列化的
对象放入数组中,但实际上的问题是,正式声明类型自动关闭[]
的静态
字段引用的是实际类型可序列化的[]
的对象。
尽管这是HotSpot JVM的一种特定行为,但我们迄今为止都没有做到这一点,因为当涉及接口类型(包括接口类型数组)时,它的验证器不会检查分配,而是将检查实际类是否实现了接口的时间推迟到最后一刻,即在尝试实际调用接口方法时。
有趣的是,类型转换是严格的,当它们出现在类文件中时:
static <T extends AutoCloseable & Serializable> void method(T... args) {
AutoCloseable[] a = (AutoCloseable[])args; // actually removed by the compiler
a = (AutoCloseable[])(Object)args; // fails at runtime
}
public static void main(String[] args) throws Exception {
method();
}
虽然上面示例中
javac
对Serializable[]
的决定似乎是任意的,但应该清楚的是,无论它选择哪种类型,其中一个字段分配都只能在类型检查松懈的JVM中进行。我们还可以强调问题的更基本性质:
// erased to method1(AutoCloseable[])
static <T extends AutoCloseable & Serializable> void method1(T... args) {
method2(args); // valid according to generic types
}
// erased to method2(Serializable[])
static <T extends Serializable & AutoCloseable> void method2(T... args) {
}
public static void main(String[] args) throws Exception {
// whatever array type the compiler picks, it would violate one of the erased types
method1();
}
虽然这实际上并没有回答实际规则
javac
使用什么的问题(除了它使用“一些T
”),但它强调了按预期处理为varargs参数创建的数组的重要性:您最好不要关心的任意类型的临时存储(不要分配给字段)。
通常,当编译器遇到对参数化方法的调用时,它可以推断类型(JSL 18.5.2)并可以在调用者中创建正确类型的vararg数组。
这些规则主要是说“找到所有可能的输入类型并检查它们”(如void、三元运算符或lambda)的技术方式。其余的都是常识,比如使用最具体的公共基类(JSL 4.10.4)。例子:
public class Test {
private static class A implements AutoCloseable, Runnable {
@Override public void close () throws Exception {}
@Override public void run () {} }
private static class B implements AutoCloseable, Runnable {
@Override public void close () throws Exception {}
@Override public void run () {} }
private static class C extends B {}
private static <T extends AutoCloseable & Runnable> void printType( T... args ) {
System.out.println( args.getClass().getComponentType().getSimpleName() );
}
public static void main( String[] args ) {
printType( new A() ); // A[] created here
printType( new B(), new B() ); // B[] created here
printType( new B(), new C() ); // B[] which is the common base class
printType( new A(), new B() ); // AutoCloseable[] - well...
printType(); // AutoCloseable[] - same as above
}
}
AutoCloseable
当然,从调用中获取
AutoCloseable[]
可能看起来很奇怪,因为Java代码无法做到这一点。但实际上,实际类型并不重要。在语言层面上,args
是T[]
,其中T
是一个“虚拟类型”,同时是a和B(JSL 4.9)。
编译器只需要确保其用法满足所有约束,然后就知道逻辑是正确的,不会出现类型错误(Java泛型就是这样设计的)。当然,编译器仍然需要创建一个真正的数组,并为此创建一个“通用数组”。因此,警告“
未经检查的通用数组创建
”(JLS 15.12.4.2)。
换句话说,只要您只传入
AutoCloseable
由于
printType
不关心vararg数组类型,getComponentType()
不重要,也不应该重要。如果想要获取接口,请尝试返回数组的getGenericInterfaces()
。
由于类型擦除(JSL 4.6),T
的接口顺序确实会影响(JSL 13.1)编译的方法签名和字节码。将使用第一个接口AutoClosable
,例如在printType
中调用AutoClosable.close()
时不会进行类型检查。
- 但这与问题的方法调用的类型干扰无关,即为什么要创建和传递
AutoClosable[]
。许多类型安全在擦除之前都会检查,因此顺序不会影响类型安全。我认为这是JSL所指的“类型的顺序......重要的只是擦除......是由第一种类型决定的”
(JSL 4.4)。这意味着顺序在其他方面是无关紧要的。 - 不管怎样,这个擦除规则确实会导致角落情况,例如添加
printType(AutoCloseable[])
触发编译错误,当添加printType(Runnable[])
时不会。我相信这是一个意想不到的副作用,真的超出了范围。 - P. S.挖得太深可能会导致精神错乱,考虑到我认为我是Ovis ary,查看源到汇编,并努力用英语而不是J S L回答。我的理智分数是b e yon d r e a l numb ers。T u r n̪͓͓̭̯̕ ̱̱̞̠̬ͅb a c k。b e f o̼͕̱͎̬̟̪r҉͏̛̣̼͙͍͍̠̫͙e
编译以下Java代码: 此外,编译以下代码: 以下代码将给我一个编译错误: 因此,看起来,如果其中一个边界类型本身是类型参数,就不可能有多个边界。但为什么呢? 据我所知,如果声明了多个边界,那么最多一个边界可能是一个类(其他所有边界都是接口),并且它必须是第一个边界。 但是,既然在我的示例中是一个接口,为什么编译器会抱怨呢?毕竟,T是接口还是类应该没有任何区别。 我试图在网上找到答案,但显然我的谷
我知道在Java泛型中,当使用具有多个边界的类型参数时,编译器会将类型信息擦除到“最左边的边界”(即列表中的第一个类/枚举或接口)。那么为什么以下代码编译没有问题呢? 类型参数不应该被视为对象吗??(因此不允许我调用close()或append())??
问题内容: 是否可以使用CSS3选择器选择具有给定类名称的第一个元素?我的考试没有成功,所以我认为不是吗? 问题答案: 不,仅使用一个选择器是不可能的。的伪类选择其的第一个元素类型(,等)。将类选择器(或类型选择器)与该伪类一起使用意味着,如果元素具有给定的类(或属于给定的类型) _并且_是其同级中的第一个元素,则选择该元素。 不幸的是,CSS没有提供仅选择类的首次出现的选择器。解决方法是,可以使
我想创建一个rest来在服务器和客户端之间进行通信。 下面给出的构造函数: 对于普通类型,我可以执行以下操作: 如何传递泛型类型,如: 以下代码无效: 我遇到一个运行时错误:无法强制转换为ParameteredType。
问题内容: 我有一个看起来像这样的方法: 因此,我期望传递的集合是实现Marshallable接口的Enum。如果我在运行时具有具体的Enum类型,则可以正常工作,但是我编写了一个测试方法,该方法从类对象动态创建一个Enum列表(实现Marshallable),并且无法将此列表传递给上面的方法。 标记的行将给出编译错误。我不知道如何在不更改方法签名的情况下传递列表。 问题答案: 您需要对泛型辅助方
问题内容: 以下两个签名是否相同? 和 问题答案: 不,两个签名不相同。根据Java语言规范的第8章: 如果两个方法具有相同的名称和参数类型,则它们具有相同的签名。 如果满足以下所有条件,则两个方法或构造函数声明M和N具有相同的参数类型: 它们具有相同数量的形式参数(可能为零) 它们具有相同数量的类型参数 (可能为零) … 由于两种方法共享的类型参数数量不同,因此签名也不相同。 在实际情况下,使用