我为什么要关心Java没有泛型泛型?


102

这是我最近在一次采访中提出的一个问题,候选人希望将其添加到Java语言中。人们通常认为Java没有通用化泛滥的痛苦,但是当Java 泛化后,候选人实际上并不能告诉我他在那里可以实现的目标。

显然,由于Java中允许使用原始类型(以及不安全的检查),因此有可能破坏泛型并以List<Integer>实际包含Strings的a结尾。如果类型信息得到证实,这显然变得不可能。但是肯定不止于此

人们可以发布他们真正想做的事的示例吗?我的意思是,显然您可以List在运行时获取a的类型-但是您将如何处理?

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

编辑:对此的快速更新,因为答案似乎主要是担心需要将a 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?

这是否意味着您可以在运行时获取通用类型的类?(如果是这样,我有个例子!)
James B

我认为对可仿制泛型的大多数渴望来自那些主要将泛型用于集合的人,并且希望这些集合的行为更像数组。
kdgregory

2
(对我而言)更有趣的问题:在Java中实现C ++样式泛型会怎样?它在运行时当然看起来是可行的,但是会破坏所有现有的类加载器(因为findClass()必须忽略参数化,但defineClass()不能这样做)。正如我们所知,具有向后兼容性的权力至关重要。
kdgregory

1
实际上,Java确实以非常受限的方式提供了泛型泛型。我提供这个SO线程的详细信息:stackoverflow.com/questions/879855/...
理查德·戈麦斯

JavaOne大会主题演讲表明,Java的9将支持物化。
Rangi Keen 2014年

Answers:


81

从我几次遇到这种“需求”开始,它最终可以归结为这种构造:

public class Foo<T> {

    private T t;

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

}

假设T具有默认构造函数,则此方法在C#中有效。您甚至可以通过获取运行时类型,typeof(T)并通过获取构造函数Type.GetConstructor()

常见的Java解决方案是传递Class<T>as参数。

public class Foo<T> {

    private T t;

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

}

(它不一定需要作为构造函数参数传递,因为方法参数也可以,上面仅是一个示例,try-catch为简便起见也省略了)

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


5
这在C#中有效(例如)?您如何知道T具有默认构造函数?
Thomas Jung

4
是的,确实如此。这只是一个基本示例(假设我们使用javabeans)。整个问题是,对于Java,您无法在运行时通过T.class或获取类T.getClass(),因此您可以访问其所有字段,构造函数和方法。这也使得建造不可能。
BalusC

3
这似乎真的弱到我的“大问题”,特别是因为这是唯一的可能是有用的,连同周围的构造函数/参数等一些非常脆反思
oxbow_lakes

33
只要您将类型声明为:public class Foo <T>其中T:new(),它就可以在C#中编译。这会将T的有效类型限制为包含无参数构造函数的T的有效类型。
马丁·哈里斯

3
@Colin实际上,您可以告诉Java,扩展一个以上的类或接口需要给定的泛型类型。请参阅docs.oracle.com/javase/tutorial/java/generics/bounded.html的“多重界限”部分。(当然,您不能告诉它该对象将是一个特定的对象集,该对象集不共享可以抽象到接口的方法,可以使用模板专门化在C ++中实现该方法。)
JAB

99

最让我困扰的是无法利用多种泛型类型的多重调度。以下是不可能的,并且在许多情况下这将是最佳解决方案:

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

5
绝对不需要进行修改就可以做到这一点。当编译时类型信息可用时,方法选择在编译时完成。
Tom Hawtin-大头钉

26
@Tom:由于类型擦除,它甚至无法编译。两者都被编译为public void my_method(List input) {}。但是,我从来没有遇到过这种需要,仅仅是因为它们不会使用相同的名称。如果它们具有相同的名称,我想问一下是否public <T extends Object> void my_method(List<T> input) {}是一个更好的主意。
BalusC,2009年

1
嗯,我倾向于避免完全使用相同数量的参数进行重载,并且更喜欢类似的东西myStringsMethod(List<String> input)myIntegersMethod(List<Integer> input)即使在Java中这种情况下也可以重载。
Fabian Steeg

4
@Fabian:这意味着您必须有单独的代码,并避免使用<algorithm>C ++ 带来的好处。
David Thornley,2009年

我很困惑,这与我的第二点本质上是相同的,但是我得到了2张赞成票吗?有人愿意启发我吗?
rsp

34

想到类型安全。如果不使用泛型,则向下转换为参数化类型将始终是不安全的:

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)。


1
是的-我有一种解决方法,可以创建一个ParametrizedList复制源集合检查类型中的数据的方法。有点像,Collections.checkedList但是可以从一个集合开始。
oxbow_lakes

@tackline-好吧,一些抽象将减少泄漏。如果您需要在实现中访问类型元数据,则外部接口会告诉您,因为客户端需要向您发送一个类对象。
gustafc

...意味着使用通用化泛型,您可以添加类似T.class.getAnnotation(MyAnnotation.class)(其中T是泛型类型)的内容,而无需更改外部接口。
gustafc

@gustafc:如果您认为C ++模板可以提供完全的类型安全性,请阅读以下内容:kdgregory.com/index.php?
kdgregory 2009年

@kdgregory:我从来没有说过C ++是100%类型安全的-只是擦除会损害类型安全。就像您自己说的那样,“事实证明,C ++具有自己的类型擦除类型,称为C样式指针转换。” 但是Java只进行动态转换(不重新解释),因此,将其整型插入类型系统中。
gustafc

26

您将能够在代码中创建通用数组。

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

对象有什么问题?无论如何,对象数组就是引用数组。这不像对象数据位于堆栈上,而是全部放在堆中。
拉恩·比隆

19
类型安全。我可以将所需的任何内容放入Object [],但只能将Strings放入String []。
Turnor

3
冉:毫不讽刺:您可能想使用脚本语言而不是Java,然后便可以在任何地方灵活地键入类型!
飞羊

2
两种语言中的数组都是协变的(因此不是类型安全的)。String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();两种语言都可以正常编译。运行notsofine。
马丁

16

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

“修饰的”仅表示真实的,通常仅表示与类型擦除相反的类型。

与Java泛型相关的大问题:

  • 这种可怕的拳击要求以及原语和引用类型之间的断开。这与验证或类型擦除没有直接关系。C#/ Scala修复了此问题。
  • 没有自我类型。因此,JavaFX 8必须删除“构建器”。绝对与类型擦除无关。Scala修复了此问题,不确定C#。
  • 没有声明方类型差异。C#4.0 / Scala拥有此功能。绝对与类型擦除无关。
  • 不能超载void method(List<A> l)method(List<B> l)。这是由于类型擦除引起的,但是非常小。
  • 不支持运行时类型反射。这是类型擦除的核心。如果您喜欢在编译时验证并证明您的程序逻辑尽可能多的超高级编译器,则应尽可能少地使用反射,并且这种类型的擦除不会打扰您。如果您喜欢更多的修补程序,脚本,动态类型编程,并且不太在乎编译器是否证明了您的逻辑尽可能正确,那么您需要更好的反映,并且修复类型擦除很重要。

1
大多数情况下,我很难处理序列化案例。您通常希望能够嗅出要序列化的通用事物的类类型,但是由于类型擦除而使您停滞不前。这很难做到这样deserialize(thingy, List<Integer>.class)
Cogman 2014年

3
我认为这是最好的答案。尤其是描述由于类型擦除导致哪些问题真正成为根本问题的部分以及仅属于Java语言设计问题的部分。我告诉人们开始擦除抱怨的第一件事是Scala和Haskell也在按类型擦除工作。
aemxdp 2014年

13

通过序列化,序列化将更加简单。我们想要的是

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

我们要做的是

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

看起来丑陋,时髦。

在某些情况下,说出类似

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

这些东西不会经常被咬,但是它们在发生时也会被咬。


这个。这个的一千倍。每次我尝试用Java编写反序列化代码时,我都会花一整天的时间哀叹我没有在其他事情上工作
2014年

11

我对Java Geneircs的了解非常有限,除了已经提到的其他答案以外,《Java Generics and Collections》一书还介绍了一种情况。 Maurice Naftalin和Philip Walder,其中泛化的泛型很有用。

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

例如,以下形式的声明无效。

class ParametericException<T> extends Exception // compile error

这是因为catch子句检查引发的异常是否与给定类型匹配。此检查与实例测试执行的检查相同,并且由于类型不可更改,因此上述语句形式无效。

如果以上代码有效,则可以按以下方式进行异常处理:

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

该书还提到,如果Java泛型的定义类似于C ++模板的定义(扩展)方式,则可能会导致更有效的实现,因为这会提供更多的优化机会。但是除了提供更多解释之外,其他任何有经验的人的解释(指针)都将是有帮助的。


1
这是有道理的,但是我不太确定为什么如此参数化的异常类会有用。您能否修改答案以包含简短示例说明何时可能有用?
oxbow_lakes

@oxbow_lakes:对不起,我对Java泛型的了解非常有限,我正在尝试对其进行改进。因此,现在我无法想到任何可以使用参数化异常的示例。会尝试考虑一下。谢谢。
sateesh

它可以替代异常类型的多重继承。
meriton

性能方面的改进是,当前,类型参数必须继承自Object,这需要对原始类型进行装箱,这会增加执行和内存开销。
Meriton

8

如果对数组进行了泛化,它们可能会对泛型起到更好的作用。


2
可以,但是他们仍然会有问题。List<String>不是List<Object>
Tom Hawtin-大头钉

同意-但仅适用于图元(整数,长整数等)。对于“常规”对象,这是相同的。由于原语不能是参数化类型(更严重的问题,至少是恕我直言),所以我不认为这是真正的痛苦。
然·比隆

3
数组的问题在于它们的协方差,与验证无关。
递归

5

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

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

按照当前实现泛型的方式,我还必须传入将要从结果集中创建的对象的类。如果我正确理解,如果泛型得到了修正,我可以只调用T.getClass()获得其构造函数,然后不必强制转换Class.newInstance()的结果,这将变得更加整洁。

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


但是对我来说,通过课堂似乎更安全。无论如何,从数据库查询中反射地创建实例对于更改(无论如何都是在代码和数据库中)进行重构都是极其脆弱的。我想我在寻找无法提供该类课程的情况
oxbow_lakes 2009年

5

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

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


2
这本质上归结为new T[]在T是原始类型的情况下能够执行的操作!
oxbow_lakes

2

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

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


这当然是Java中的泛型。在C#和其他语言中,它们是功能强大的工具
基本的

1

遗漏所有答案的事情一直困扰着我,因为这些类型被删除了,您不能两次继承通用接口。当您要制作细粒度的接口时,这可能是个问题。

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

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

0

这是今天引起我注意的一个问题:未经验证,如果您编写一个接受通用项目的变量列表的方法,则调用方可以认为它们是类型安全的,但无意中传入了任何旧的粗体,并破坏了您的方法。

似乎不太可能发生?...当然,直到...您将Class用作数据类型。此时,您的呼叫者会很高兴向您发送许多Class对象,但是一个简单的错字便会向您发送不遵守T的Class对象,以及发生灾难。

(注意:我可能在这里犯了一个错误,但是在“泛型varargs”周围进行搜索时,以上内容似乎正是您所期望的。使这成为实际问题的是我使用了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 Generics中没有经过验证的情况下,该对象接受任何“ Class”对象。而且这只是先前代码的微小扩展:

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.