Java编译器如何处理类似于`public interface A extends B<A>`的代码?

huangapple go评论80阅读模式
英文:

How does java compiler deal with code like `public interface A extends B<A>`

问题

我刚开始学写Java,今天读到了类似这样的代码:

public interface A extends B<A>{
	...
}

public interface B<E extends B<E>>{
    ...
}

当然,我能理解这段代码,但它让我很困惑。看起来好像是...我用自己来创建自己?编译器是如何处理这种情况的呢?

英文:

I just started writing java and today I read some code like this:

public interface A extends B&lt;A&gt;{
	...
}

public interface B&lt;E extends B&lt;E&gt;&gt;{
    ...
}

Of course I can understand the code, but it makes me really confused. It looks like ... I used myself to create myself? How the compiler deal with it?

答案1

得分: 2

为了回答您明确的问题,编译器通过检查约束条件来验证泛型,验证涉及的具体类型是否合法/与泛型引用匹配。之后(对于输出代码),泛型类型会被擦除。这称为“类型擦除”。具体步骤在这里解释:https://docs.oracle.com/javase/tutorial/java/generics/erasure.html

但更有趣的是您显然遇到的语义。

泛型类型的含义不是“用于创建”,而是“关于”(以任何方式)。
List 为例。 List&lt;A&gt; 表示“包含类 A 的实例的列表”。对于调用其中一个实例的代码,这意味着所有引用泛型类型的方法将直接或间接地关联到类型为 A 的对象。

任何实现 public interface B&lt;E extends B&lt;E&gt;&gt; 的类都需要在这方面关注自己。这样做的目的可能是,稍后引用 E 的方法或属性将产生实现它们的类型。一个示例可能是:

public interface Chainable&lt;E extends Chainable&lt;E&gt;&gt; {
  public void append(E followup);

  public E getNext();
}

...

public class MyLink implements Chainable&lt;MyLink&gt; {
  public void append(MyLink l) {
    ...
  }

  public MyLink getNext() {
    ...
  }
}

通过这种方式,确保实现 Chainable 接口的任何类都将具有接受和返回该类对象的方法,而不仅仅是任何对象。

虽然仅以上内容在约束方面非常有用,但我无法看出您的示例 public interface A extends B&lt;A&gt; 的更好用途。原因在于,现在实现 A 接口的任何类都仅仅保证关注 A,而不关注实现类本身(这将更有用)。例如:

public class C implements A {
  public A methodDeclaredInB(A someParam) {
    ...
  }
}

上述方法仅在编译时知道 A,但不知道 C。如果有人改为编写:

public class C implements B&lt;C&gt; {
  public C methodDeclaredInB(C someParam) {
    ...
  }
}

那么类 C 可以代替仅仅使用 A

也许有些情况下仅知道类型 A 就足够了,但通常您希望知道类型 C,因此,以这种方式创建一个(本身非泛型的)扩展 B 的接口 A 似乎是通向上述更冗长但更有用示例的脆弱捷径。

英文:

To answer your explicit question, the compiler validates generics by checking constraints against each other, validating that the specific types in question are legal/matching the generic references. After that (for the output code) generic types are erased. This is called 'type erasure'. The explicit steps are explained here: https://docs.oracle.com/javase/tutorial/java/generics/erasure.html

But more interesting are the semantics of what you apparently encountered.

Generic types don't mean 'use to create' but rather 'regarding' (in whatever way).
Take List for instance. List&lt;A&gt; means 'List containing instances of class A'. For the code calling an instance of that, it means that all methods referencing the generic type will directly or indirectly regard object(s) of type A.

Any class implementing public interface B&lt;E extends B&lt;E&gt;&gt; will be required to regard themselves in that respect. The point of this might be that at a later point methods or attributes that refer to E will then yield the type implementing them. An example could be:

public interface Chainable&lt;E extends Chainable&lt;E&gt;&gt; {
  public void append(E followup);

  public E getNext();
}

...

public class MyLink implements Chainable&lt;MyLink&gt; {
  public void append(MyLink l) {
    ...
  }

  public MyLink getNext() {
    ...
  }
}

This way, it is ensured, that any class implementing Chainable will have a method taking, and one returning an object of that class, rather than just any object.

While the above alone is useful in terms of constraints I cannot see the better use of your example's public interface A extends B&lt;A&gt;. The reason for this is, that any class now implementing A is just guaranteed to regard A but not the implementing class itself (which would be much more useful). Example:

public class C implements A {
  public A methodDeclaredInB(A someParam) {
    ...
  }
}

The method above only knows A at compile time, but not C. If instead someone wrote:

public class C implements B&lt;C&gt; {
  public C methodDeclaredInB(C someParam) {
    ...
  }
}

then class C could be used instead of 'only' A.

There may be cases where it is sufficient to know type A, but usually you want type C, so making a (itself non-generic) interface A that extends B in such a way seems a fragile shortcut to the more verbose but more useful example above.

答案2

得分: 2

是的,编译器的能力确实令人吃惊。如其他答案所说,通用参数是通过引用使用的。但是编译存在(.class 文件)及其依赖关系。注意:一些运行时错误是由于不同步的编译造成的,而且你可以将 .java 文件存储在一个 JAR 文件中 - 我相信)。

经常使用这种循环引用来将子类的类传递给超类,以限制(方法)在子类中的使用。

但是相同的循环行为技巧也可以用于纯类:

public interface Foo {
    Foo ZERO = new Bar();
}

public class Bar implements Foo {
    ... ZERO ...
}

因此,Java 编译器解决了这个循环依赖的问题。

英文:

Yes it is astonishing what the compiler is capable of. As said by the other answer(s), generic parameters are used by reference. But there are compilations (.class) and their dependent relations. Notice: some runtime errors are caused by unsynchronized compilation, and you can store a .java file in a jar - I believe).

That juggling with self-references is often used to pass the class of a child class to a super class for restricting things (methods) to the child class.

But the same trick of cyclic behavior can be done as pure classes:

public interface Foo {
    Foo ZERO = new Bar();
}

public class Bar implements Foo {
    ... ZERO ...
}

So the java compiler solves this chicken/egg problem.

huangapple
  • 本文由 发表于 2020年9月29日 17:19:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/64116558.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定