为啥我要关心 Java 没有具体化的泛型?

Posted

技术标签:

【中文标题】为啥我要关心 Java 没有具体化的泛型?【英文标题】:Why should I care that Java doesn't have reified generics?为什么我要关心 Java 没有具体化的泛型? 【发布时间】:2010-12-28 00:28:52 【问题描述】:

这是我最近在一次面试中提出的一个问题,作为候选人希望看到添加到 Java 语言中的东西。 Java 没有reified generics 这通常被认为是一种痛苦,但是当被推送时,候选人实际上无法告诉我如果他们在那里他可以实现什么样的事情。

显然,因为原始类型在 Java 中是允许的(以及不安全的检查),所以可能会破坏泛型并最终得到一个(例如)实际上包含 Strings 的 List<Integer>。如果类型信息被具体化,这显然是不可能的; 但肯定不止这些

人们是否可以发布他们真正想做的事情的示例,具体的泛型是否可用?我的意思是,很明显你可以在运行时得到 List 的类型——但是你会用它做什么呢?

public <T> void foo(List<T> l) 
   if (l.getGenericType() == Integer.class) 
       //yeah baby! err, what now?

编辑:对此进行快速更新,因为答案似乎主要关注需要将Class 作为参数传递(例如EnumSet.noneOf(TimeUnit.class))。我一直在寻找更多类似不可能的东西。例如:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) 
    l1.addAll(l2); //why on earth would I be doing this anyway?

【问题讨论】:

这是否意味着您可以在运行时获得泛型类型的类? (如果是这样,我有一个例子!) 我认为大多数对可具体化泛型的渴望来自那些主要将泛型用于集合的人,并希望这些集合的行为更像数组。 更有趣的问题(对我来说):在 Java 中实现 C++ 风格的泛型需要什么?它在运行时似乎确实可行,但会破坏所有现有的类加载器(因为findClass() 必须忽略参数化,但defineClass() 不能)。正如我们所知,The Powers That Be 保持向后兼容性至关重要。 实际上,Java 确实以非常有限的方式提供了具体化的泛型。我在这个 SO 线程中提供了更多细节:***.com/questions/879855/… JavaOne Keynote 表示 Java 9 将支持物化。 【参考方案1】:

最常困扰我的事情是无法利用跨多个泛型类型的多次调度。以下是不可能的,在很多情况下它是最好的解决方案:

public void my_method(List<String> input)  ... 
public void my_method(List<Integer> input)  ... 

【讨论】:

完全不需要物化来做到这一点。当编译时类型信息可用时,方法选择在编译时完成。 @Tom:由于类型擦除,这甚至无法编译。两者都编译为public void my_method(List input) 。然而,我从来没有遇到过这种需求,仅仅是因为他们不会有相同的名字。如果他们有相同的名字,我会怀疑public &lt;T extends Object&gt; void my_method(List&lt;T&gt; input) 是不是一个更好的主意。 嗯,我倾向于避免使用相同数量的参数进行重载,并且更喜欢 myStringsMethod(List&lt;String&gt; input)myIntegersMethod(List&lt;Integer&gt; input) 之类的东西,即使在 Java 中可以重载这种情况。 @F***:这意味着您必须拥有单独的代码,并防止您从 C++ 中的 &lt;algorithm&gt; 获得的那种优势。 我很困惑,这与我的第二点基本相同,但我得到了 2 票反对?有大神给我指点一下吗?【参考方案2】:

从我遇到这种“需求”的几次中,它最终归结为这个结构:

public class Foo<T> 

    private T t;

    public Foo() 
        this.t = new T(); // Help?
    


假设 T 有一个 default 构造函数,这在 C# 中确实有效。您甚至可以通过typeof(T) 获取运行时类型,并通过Type.GetConstructor() 获取构造函数。

常见的 Java 解决方案是将 Class&lt;T&gt; 作为参数传递。

public class Foo<T> 

    private T t;

    public Foo(Class<T> cls) throws Exception 
        this.t = cls.newInstance();
    


(不一定需要作为构造函数参数传递,作为方法参数也可以,以上只是一个例子,为了简洁也省略了try-catch

对于所有其他泛型类型构造,实际类型可以通过反射的帮助轻松解析。以下问答说明了用例和可能性:

Get generic type of java.util.List How to get the generic type at runtime? Get actual type of generic type argument on abstract superclass

【讨论】:

这在 C# 中有效(例如)?你怎么知道 T 有默认构造函数? 是的,确实如此。这只是一个基本示例(假设我们使用 javabeans)。重点在于,使用 Java,您无法在 runtime 期间通过 T.classT.getClass() 获取类,因此您可以访问其所有字段、构造函数和方法。它也使施工变得不可能。 这对我来说似乎真的很弱,因为它是“大问题”,特别是因为这可能只在与围绕构造函数/参数等的一些非常脆弱的反射结合使用时才有用。 它确实可以在 C# 中编译,前提是您将类型声明为:public class Foo where T: new()。这会将 T 的有效类型限制为包含无参数构造函数的类型。 @Colin 您实际上可以告诉 java 一个给定的泛型类型需要扩展多个类或接口。请参阅docs.oracle.com/javase/tutorial/java/generics/bounded.html 的“多重边界”部分。 (当然,您不能告诉它该对象将是不共享可以抽象为接口的方法的特定集合之一,这可以在某种程度上使用模板专业化在 C++ 中实现。)【参考方案3】:

类型安全浮现在脑海中。如果没有具体化的泛型,向下转换为参数化类型总是不安全的:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

此外,抽象会泄漏更少 - 至少是那些可能对有关其类型参数的运行时信息感兴趣的那些。今天,如果您需要任何关于某个泛型参数类型的运行时信息,您还必须传递它的Class。这样,您的外部接口取决于您的实现(无论您是否使用 RTTI 来处理您的参数)。

【讨论】:

是的 - 我有办法解决这个问题,我创建了一个 ParametrizedList,它复制源集合检查类型中的数据。它有点像Collections.checkedList,但可以从一个集合开始。 @tackline - 好吧,一些抽象会减少泄漏。如果您需要在实现中访问类型元数据,外部接口会告诉您,因为客户端需要向您发送一个类对象。 ... 意味着使用具体的泛型,您可以添加类似T.class.getAnnotation(MyAnnotation.class)(其中T 是泛型类型)之类的东西,而无需更改外部接口。 @gustafc:如果您认为 C++ 模板可以为您提供完整的类型安全,请阅读:kdgregory.com/index.php?page=java.generics.cpp @kdgregory:我从未说过 C++ 是 100% 类型安全的——只是擦除会损害类型安全。正如您自己所说,“事实证明,C++ 有自己的类型擦除形式,称为 C 样式指针强制转换。”但是 Java 只做动态转换(不重新解释),所以具体化会将整个插入到类型系统中。【参考方案4】:

您可以在代码中创建通用数组。

public <T> static void DoStuff() 
    T[] myArray = new T[42]; // No can do

【讨论】:

对象有什么问题?无论如何,对象数组都是引用数组。这不像对象数据位于堆栈上 - 它都在堆中。 类型安全。我可以在 Object[] 中放任何我想要的东西,但只能在 String[] 中放字符串。 Ran:不要讽刺:你可能喜欢使用脚本语言而不是 Java,那么你在任何地方都可以灵活地使用无类型变量! 数组在两种语言中都是协变的(因此不是类型安全的)。 String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object(); 两种语言都可以正常编译。运行不顺利。【参考方案5】:

这是一个老问题,有很多答案,但我认为现有的答案是不恰当的。

“reified”只是表示真实,通常只是类型擦除的反义词。

Java 泛型相关的大问题:

这种可怕的装箱要求以及原语和引用类型之间的断开。这与具体化或类型擦除没有直接关系。 C#/Scala 解决了这个问题。 没有自我类型。由于这个原因,JavaFX 8 不得不删除“构建器”。与类型擦除完全无关。 Scala 解决了这个问题,不确定 C#。 没有声明端类型差异。 C# 4.0/Scala 有这个。与类型擦除完全无关。 不能超载void method(List&lt;A&gt; l)method(List&lt;B&gt; l)。这是由于类型擦除造成的,但非常小。 不支持运行时类型反射。这是类型擦除的核心。如果你喜欢在编译时验证和证明尽可能多的程序逻辑的超级高级编译器,你应该尽可能少地使用反射,这种类型的擦除不应该​​打扰你。如果您喜欢更零散、更脚本化、更动态的类型编程,并且不太关心编译器能否尽可能多地证明您的逻辑正确,那么您需要更好的反射和修复类型擦除很重要。

【讨论】:

大多数情况下,我发现序列化案例很难。您经常希望能够嗅出被序列化的通用事物的类类型,但由于类型擦除而您被阻止了。很难做这样的事情deserialize(thingy, List&lt;Integer&gt;.class) 我认为这是最好的答案。尤其是描述由于类型擦除而真正导致哪些问题以及哪些只是 Java 语言设计问题的部分。我告诉人们开始擦除抱怨的第一件事是 Scala 和 Haskell 也在通过类型擦除工作。【参考方案6】:

通过具体化,序列化会更直接。我们想要的是

deserialize(thingy, List<Integer>.class);

我们要做的是

deserialize(thing, new TypeReference<List<Integer>>());

看起来很丑,而且工作起来很时髦。

在某些情况下,说类似的话会很有帮助

public <T> void doThings(List<T> thingy) 
    if (T instanceof Q)
      doCrazyness();
  

这些事情不常咬人,但一旦发生就会咬人。

【讨论】:

这个。这是一千倍。每次我尝试用 Java 编写反序列化代码时,我都会感叹自己没有在做其他事情【参考方案7】:

我对 Java Geneircs 的了解非常有限,除了其他答案已经提到的点之外,还有一个场景在 Maurice Naftalin 和 Philip Walder 所著的 Java Generics and Collections 一书中进行了解释,其中具体化的泛型很有用。

由于类型不可具体化,因此不可能有参数化异常。

例如以下表格的声明无效。

class ParametericException<T> extends Exception // compile error

这是因为 catch 子句检查抛出的异常是否与给定类型匹配。该检查与instance test的检查相同,由于类型不可具体化,上述形式的语句无效。

如果上面的代码是有效的,那么下面的异常处理方式是可能的:

try 
     throw new ParametericException<Integer>(42);
 catch (ParametericException<Integer> e)  // compile error
  ...

本书还提到,如果 Java 泛型的定义类似于 C++ 模板的定义方式 定义(扩展)它可能会导致更有效的实施,因为这提供了更多 优化的机会。但没有提供更多的解释,所以知识渊博的人的任何解释(指针)都会有所帮助。

【讨论】:

这是一个有效的观点,但我不太确定为什么如此参数化的异常类会有用。您能否修改您的答案以包含一个简短示例,说明何时这可能有用? @oxbow_lakes:对不起,我对 Java 泛型的了解非常有限,我正在尝试改进它。所以现在我想不出任何参数化异常可能有用的例子。会试着考虑一下。谢谢。 它可以替代异常类型的多重继承。 性能提升是目前,类型参数必须从Object继承,需要对原始类型进行装箱,这会带来执行和内存开销。【参考方案8】:

如果对泛型进行具体化,数组可能会更好地发挥作用。

【讨论】:

当然,但他们仍然会遇到问题。 List&lt;String&gt; 不是 List&lt;Object&gt; 同意 - 但仅适用于基元(整数、长整数等)。对于“常规”对象,这是相同的。由于原语不能是参数化类型(一个更严重的问题,至少恕我直言),我不认为这是一个真正的痛苦。 数组的问题在于它们的协方差,与具体化无关。【参考方案9】:

我有一个将 jdbc 结果集呈现为迭代器的包装器,(这意味着我可以通过依赖注入更轻松地对源自数据库的操作进行单元测试)。

API 看起来像Iterator&lt;T&gt;,其中 T 是某种类型,只能使用构造函数中的字符串来构造。迭代器然后查看从 sql 查询返回的字符串,然后尝试将其与 T 类型的构造函数匹配。

在当前实现泛型的方式中,我还必须传入我将从结果集中创建的对象的类。如果我理解正确,如果泛型被具体化,我可以调用 T.getClass() 获取它的构造函数,然后不必强制转换 Class.newInstance() 的结果,这样会更整洁。

基本上,我认为它使编写 API(而不是仅仅编写应用程序)更容易,因为您可以从对象中推断出更多内容,因此需要更少的配置......我不明白注释,直到我看到它们被用在诸如 spring 或 xstream 之类的东西中,而不是大量的配置中。

【讨论】:

但是在我看来,通过课堂似乎更安全。无论如何,从数据库查询中反射性地创建实例对于诸如重构之类的更改(在您的代码和数据库中)都非常脆弱。我想我正在寻找的东西只是 不可能 提供类【参考方案10】:

避免对原始(值)类型进行装箱是一件好事。这在某种程度上与其他人提出的数组投诉有关,并且在内存使用受到限制的情况下,它实际上可能会产生重大影响。

在编写能够反映参数化类型很重要的框架时,也存在几种类型的问题。当然,这可以通过在运行时传递一个类对象来解决,但这会掩盖 API 并给框架的用户带来额外的负担。

【讨论】:

这基本上归结为能够做到new T[],其中 T 是原始类型!【参考方案11】:

这并不是说你会取得任何非凡的成就。它只会更容易理解。类型擦除对于初学者来说似乎很困难,最终需要了解编译器的工作方式。

我的观点是,泛型只是一个额外的,可以节省大量的冗余转换。

【讨论】:

这当然是在Java中的泛型。在 C# 和其他语言中,它们是一个强大的工具【参考方案12】:

这里的所有答案都错过了让我一直头疼的事情,因为类型被删除了,你不能继承一个泛型接口两次。当您想要制作细粒度的界面时,这可能会成为一个问题。

    public interface Service<KEY,VALUE> 
           VALUE get(KEY key);
    

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!

【讨论】:

【参考方案13】:

这是我今天遇到的一个问题:没有具体化,如果你编写一个接受泛型项目的可变参数列表的方法......调用者可以认为它们是类型安全的,但不小心传入任何旧的 crud,并炸毁你的方法。

这似乎不太可能发生? ...当然,直到...您使用 Class 作为数据类型。此时,您的调用者会很高兴地向您发送大量 Class 对象,但一个简单的拼写错误会向您发送不符合 T 的 Class 对象,并引发灾难。

(注意:我可能在这里犯了一个错误,但是在“泛型可变参数”周围搜索,上面似乎正是您所期望的。使这成为一个实际问题的事情是使用 Class,我认为- 来电者似乎不太小心:()


例如,我使用的范例是使用 Class 对象作为映射中的键(它比简单映射更复杂 - 但从概念上讲就是这样)。

例如这在 Java 泛型(简单示例)中效果很好:

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    
        // find the entities that are mapped (somehow) from that class. Very type-safe
    

例如在 Java 泛型中没有具体化,这个接受任何“类”对象。它只是之前代码的一个很小的扩展:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    
        // find the entities that are mapped (somehow) to ALL of those classes
    

上述方法必须在单个项目中编写数千次 - 因此人为错误的可能性变得很高。调试错误被证明“不好玩”。我目前正在尝试寻找替代方案,但不要抱太大希望。

【讨论】:

以上是关于为啥我要关心 Java 没有具体化的泛型?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Java 不允许 Throwable 的泛型子类?

java中的泛型

Java开发知识之Java中的泛型

java的泛型

不明白java中的泛型和抽象类有啥区别,感觉他们作用一样啊,为啥要用2种方法呢

Java泛型边界问题,super关键字