Java运行时转换为通用接口而不丢失类型信息

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

Java runtime cast to generic interface without losing type information

问题

我有一个通用接口,可以由抽象基类的不同子类来实现。

interface PropertyOne<T> {
  T type();
  List<String> values();
}

在基类中,我定义了一个小的辅助方法,用于检查当前实例是否实现了该接口,并在可能的情况下进行类型转换:

<T> Optional<T> toProperty(Class<T> clazz){
    if (clazz.isAssignableFrom(this.getClass())){
      return Optional.of(clazz.cast(this));
    }
    return Optional.empty();
}

我例如使用这个辅助方法来获取并打印这些值:

void printValues() {
    List<String> values = toProperty(PropertyOne.class)
      .map(p -> p.values())
      .orElse(Collections.emptyList());
    System.out.println(values);
}

但是这会导致一个不安全的类型转换警告,因为values()现在返回的是一个List而不是List<String>。我知道在编译代码后泛型类型信息会丢失,但由于values的返回类型始终是List,其中的元素类型是String,所以这应该是安全的,对吗?

使用以下样板代码可以在没有警告的情况下运行。

if ( this instanceof PropertyOne<?>){
   List<String> vs = ((PropertyOne<?>)this).values();
   System.out.println(vs);
}

我也可以对第一个变体进行手动转换,例如 .map(p -> ((PropertyOne<?>)p).values()) 以消除警告,但这会增加很多样板代码。

所以我有两个问题:

  1. 为什么首先丢失了List<String>类型信息?
  2. 是否可以直接动态转换为通配符/无界类型,而无需随后手动进行到PropertyOne<?>的转换?

(注意:以上内容为经过翻译的部分,已排除代码部分。)

英文:

I have a generic interface that can be implemented by different subclasses of an abstract base class.

interface PropertyOne&lt;T&gt; {
  T type();
  List&lt;String&gt; values();
}

In the base class I defined a small helper method that checks if the current instance implements that interface and cast to it if possible:

&lt;T&gt; Optional&lt;T&gt; toProperty(Class&lt;T&gt; clazz){
    if (clazz.isAssignableFrom(this.getClass())){
      return Optional.of(clazz.cast(this));
    }
    return Optional.empty();
}

I use this helper method for example to get and print those values:

void printValues() {
    List&lt;String&gt; values = toProperty(PropertyOne.class)
      .map(p -&gt; p.values())
      .orElse(Collections.emptyList());
    System.out.println(values);
}

But this gives me a unsafe cast warning, since values() now returns a List instead of a List&lt;String&gt;. I know that the generic type information is lost after compiling the code, but since the return type of values is always a List of type String it should be safe anyway?

Using the following boilerplate code works without warnings.

if ( this instanceof PropertyOne&lt;?&gt;){
   List&lt;String&gt; vs = ((PropertyOne&lt;?&gt;)this).values();
   System.out.println(vs);
}

I can do a manual cast for the first variant as well like .map(p -&gt; ((PropertyOne&lt;?&gt;)p).values()) to get rid of the warning but this again adds a lot of boilerplate.

So I got two questions:

  1. Why is the List&lt;String&gt; type information lost in the first place?
  2. Is it possible to dynamically cast to a wildcard/ unbounded type directly without manually doing a cast to PropertyOne&lt;?&gt; afterwards?

答案1

得分: 0

因为 Class&lt;T&gt; 上的泛型相当有限,不能以这种方式使用(我想说 j.l.Class 上的泛型是有问题的,但可能有点过于苛刻。它们也有一些用途,只是... 不多)。泛型可以表示类实例无法表示的内容;类实例(java.lang.Class 的实例)可以表示泛型无法表示的内容。具体来说,List&lt;String&gt; 无法表示为类实例,而 int.class 是类实例,不能用泛型表示。

换句话说,一旦您试图在 java.lang.Class&lt;T&gt; 实例中传达完整的泛型类型(例如 PropertyOne&lt;String&gt;),您就输了——j.l.Class 中的那个 T 只能是没有泛型的类型。

所以,这不是正确的做法。

您的 toProperty 方法必须去掉。不是因为这篇帖子中提到的问题;而是因为您试图在从(可选包装的)toProperty 调用中派生的 PropertyOne 实例上调用 type() 方法。

除此之外,您在 values() 调用上遇到问题的原因似乎与实际的相关泛型部分无关,因为过时的 Java 规则:一旦您进入 "原始" 模式,一切都是 原始的,即使那些不需要原始模式的事物也是如此。

因此,在 toProperty(PropertyOne.class) 中,这将返回一个 Optional&lt;PropertyOne&gt; - 一个原始类型,因为那是不允许的;您需要例如 PropertyOne&lt;?&gt;,它与 PropertyOne 是不同的(后者是原始的;前者不是)- 但是您不能在 Class&lt;T&gt; 中这样做,因为 Class&lt;T&gt; 有问题。现在您处于原始模式,对此原始类型调用 values() 将会返回一个原始列表(仅为 List),您可能会认为这仍然会给您一个 List&lt;String&gt;,因为 values() 的返回类型不依赖于 PropertyOne 的泛型,但实际情况并非如此,Java 语言规范不允许。

有几种方法可以解决这个难题,但主要是您在这里误用了泛型,您实际上无法做任何这些事情。请记住,泛型是编译器想象出来的东西:在运行时根本不存在。没有办法在运行时检查任何内容。一旦您开始将泛型和诸如 instanceofx.isAssignableFrom 等运行时结构混合使用,它永远不会奏效。

如果必须这样做,您可以使用一种称为 "超级类型标记" 的东西——您可以在搜索引擎中查找这方面的信息(在关键词中加入 "java"),这些标记可以相当精确地表示您可以放入泛型中的任何内容。但这仍然听起来像是个错误;但我无法告诉您在这里必须做什么,因为您的问题描述涉及到一个有问题的解决方案,而不是有问题的解决方案试图解决的问题。

英文:

Because the generics on Class&lt;T&gt; are quite limited and cannot be used in this fashion (I'd say the generics on j.l.Class are broken, but that's perhaps a bit harsh. They also have some use, just.. not much). Generics can represent things that a class instance cannot; class instances (an instance of java.lang.Class can represent things that generics cannot. Specifically, List&lt;String&gt; cannot be expressed as a class instance, and int.class is a class instance that cannot be expressed in generics.

In other words, the moment you try to communicate a full generified type (such as PropertyOne&lt;String&gt;) in a java.lang.Class&lt;T&gt; instance, you lost the game - that T in j.l.Class can only ever be a generics-less type.

So, this is not how to do it.

your toProperty method has got to go. Not for the issues in this post; it's about trying to invoke type() on your PropertyOne instance derived from the (optional-wrapped) toProperty call.

Separate from that, the reason that you're running into trouble on your values() invocation, which seems to have no actual relevant generics parts to it, is because of archaic java rules: Once you go 'raw', EVERYTHING is raw, even the things that don't need it.

So, in toProperty(PropertyOne.class), that would end up returning an Optional&lt;PropertyOne&gt; - a raw type, because that's not okay; you'd need e.g. PropertyOne&lt;?&gt; which is different from PropertyOne (that last one is raw; the former is not) - but you can't do that with Class&lt;T&gt;, because Class&lt;T&gt; is broken. Now you're in raw territory, and invoking values() on this raw type will end up returning a raw list (just List), you'd think that still gives you a List&lt;String&gt; as the return type of values() is not dependent on the generics of PropertyOne but that's just not how it works, java lang spec says no.

There are a few ways out of this conundrum, but mostly you're just misusing generics here, you can't really do any of this. Remember, generics are a figment of the compiler's imagination: They do not exist at runtime whatsoever. There's no way to runtime check anything. Once you start mixing generics and runtime constructs such as instanceof or x.isAssignableFrom, it's never going to work out.

If you must, you may get there with something called 'Super Type Tokens' - you can google for that (toss in 'java' as a keyword), these CAN represent quite precisely anything you can stick inside generics. But that still sounds like a mistake; but I can't really tell you what you have to do here, your problem description is talking about a broken solution and not about the problem that your broken solution is trying to solve.

huangapple
  • 本文由 发表于 2020年9月1日 06:40:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/63679075.html
匿名

发表评论

匿名网友

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

确定