泛型有何优点?为什么要使用它们?

95

我想为任何想要击出漂亮一击的人提供这个简单问题。什么是泛型,泛型的优点是什么,为什么、在哪里以及如何应用它们?请保持基本水平。谢谢。


1
即使集合不是您的API的一部分,我也不喜欢在内部实现中进行不必要的转换。 - Matthew Flaschen
1
也可以查看https://dev59.com/sHRB5IYBdhLWcg3wxZ1K - Clint Miller
4
@MatthewFlaschen,奇怪,你的重复问题指向了这个问题... 矩阵中的故障! - jcollum
1
正如@Ijs所提到的 - 编写适用于许多类型具有相同基本行为的代码 - LCJ
一个关于泛型的链接:http://blogs.msdn.com/kirillosenkov/archive/2008/08/19/how-i-started-to-really-understand-generics.aspx。 - MrBoJangles
显示剩余2条评论
29个回答

135
  • 允许您编写代码/使用库方法,这些方法是类型安全的,即List<string>保证是字符串列表。
  • 由于使用了泛型,编译器可以在编译时对代码进行类型安全检查,即您是否尝试将int放入那个字符串列表中?使用ArrayList会导致运行时错误不够透明。
  • 比使用对象更快,因为它要么避免装箱/拆箱(.net必须将值类型转换为引用类型或反之亦然),要么从对象转换为所需的引用类型进行强制转换。
  • 允许您编写适用于许多具有相同基础行为的类型的代码,即Dictionary<string,int>使用与Dictionary<DateTime,double>相同的基础代码;使用泛型,框架团队只需编写一段代码即可实现两种结果,并具有上述优点。

9
非常好,我喜欢项目符号列表。我认为这是到目前为止最完整而又简洁的答案。 - MrBoJangles
整个解释都是在“集合”上下文中的。我正在寻找更广泛的视角。有什么想法吗? - jungle_mole
好的回答,我只想再添加两个优点,泛型限制除了以下两个好处: 1- 您可以在泛型类型中使用受限类型的属性(例如 where T:IComparable 提供使用 CompareTo) 2- 在您之后编写代码的人将知道它将做什么。 - Hamit YILDIRIM

56

我非常讨厌重复自己。我不想多次输入相同的内容,也不喜欢反复陈述几乎相同,只有微小区别的事情。

所以我们可以这样做:

class MyObjectList  {
   MyObject get(int index) {...}
}
class MyOtherObjectList  {
   MyOtherObject get(int index) {...}
}
class AnotherObjectList  {
   AnotherObject get(int index) {...}
}

如果由于某种原因您不想使用原始集合,我可以构建一个可重复使用的类...

(在这种情况下)
class MyList<T> {
   T get(int index) { ... }
}

现在我效率提高了3倍,而且只需要维护一个副本。为什么你不想减少代码的维护呢?

对于非集合类,例如必须与其他类交互的Callable<T>Reference<T>也是如此。你真的想要扩展Callable<T>Future<T>和每个相关的类来创建类型安全版本吗?

我不想这样。


1
我不确定我理解了。我并不建议你创建"MyObject"包装器,那会很糟糕。我建议如果你的对象集合是一组汽车,那么你应该编写一个Garage对象。它不会有get(car)这样的方法,而是会有像buyFuel(100)这样的方法,它会购买100美元的燃料,并将其分配给最需要的汽车。我谈论的是真正的业务类,而不仅仅是“包装器”。例如,你几乎永远不会在集合外部获取(car)或循环遍历它们--你不会询问一个对象它的成员,而是要求它为你执行操作。 - Bill K
即使在你的车库里,你也应该有类型安全。所以要么你有List<Cars>,ListOfCars或者你到处转换类型。我觉得只需要尽可能少地声明类型即可。 - James Schek
我同意类型安全性很好,但它只适用于车库类,所以它的帮助作用要小得多。它被封装并且容易受到控制,因此我的观点是,它在新语法的代价下给你带来了一点优势。这就像修改美国宪法使闯红灯非法一样——是的,它应该永远是非法的,但这是否值得修正宪法呢?我还认为,它鼓励人们不要在这种情况下使用封装,这实际上是相对有害的。 - Bill K
Bill,你关于不使用封装的观点可能有道理,但是你的其余论点是一个很差的比较。在这种情况下,学习新语法的成本是微不足道的(与C#中的lambda表达式相比);将其与修宪进行比较毫无意义。宪法没有意图规定交通法规。这更像是从手写的羊皮纸副本切换到印刷版海报上的衬线字体。意思是一样的,但更清晰易读。 - James Schek
示例使概念更具实际意义。谢谢。 - RayLoveless
喜欢第一句话。实际上,我非常喜欢你回答的开头。事实上,我非常喜欢你的介绍。 - joel

22

Java泛型最大的优势之一就是不需要强制类型转换,因为它会在编译时进行类型检查。这将降低ClassCastException在运行时抛出的可能性,并且可以使代码更加健壮。

但我怀疑您已经完全意识到了这一点。

每次看泛型都让我头痛。我发现Java最好的部分是其简单性和最少的语法,而泛型不简单,并且增加了大量新的语法。

起初,我也没有看到泛型的好处。我从1.4语法开始学习Java(尽管当时Java 5已经发布了),当我遇到泛型时,我觉得它要写更多的代码,我真的不理解好处所在。

现代IDE使使用泛型编写代码更容易。

大多数现代、良好的IDE都足够智能,可以帮助使用泛型编写代码,特别是代码自动完成。

以下是使用HashMap创建Map<String,Integer>的示例。我需要输入的代码是:

Map<String, Integer> m = new HashMap<String, Integer>();

实际上,我只需要输入这么多,Eclipse就知道我需要什么:

Map<String, Integer> m = new HaCtrl+Space

确实,我需要从候选列表中选择 HashMap ,但基本上IDE知道要添加什么,包括泛型类型。使用适当的工具,使用泛型并不太困难。

此外,由于类型已知,在从泛型集合检索元素时,IDE 将表现得好像该对象已经是其声明类型的对象 - 没有必要进行强制转换,以便 IDE 知道对象的类型。

泛型的一个重要优势来自它与新的Java 5功能协同工作的方式。以下是将整数放入 Set 并计算其总和的示例:

Set<Integer> set = new HashSet<Integer>();
set.add(10);
set.add(42);

int total = 0;
for (int i : set) {
  total += i;
}

这段代码中使用了三个Java 5的新特性:

首先,泛型和基本类型的自动装箱允许以下代码:

set.add(10);
set.add(42);

整数10被自动装箱为值为10Integer对象(同样适用于42)。然后将该Integer对象放入已知保存Integer对象的Set中。尝试插入String会导致编译错误。

接下来,for-each循环遍历这三个对象:

for (int i : set) {
  total += i;
}

首先,使用包含 Integer Set 在for-each循环中。每个元素被声明为一个 int ,这是允许的,因为 Integer 被拆箱回原始的 int 。而发生这种拆箱的事实是已知的,因为泛型被用来指定 Set 中包含 Integer

泛型可以将Java 5中引入的新功能联系起来,使编码更加简单和安全。大多数时候,IDE都足够聪明,可以帮助您提供良好的建议,因此通常不需要输入太多代码。

而且,老实说,正如从 Set 示例中可以看到的那样,利用Java 5的特性可以使代码更加简洁和健壮。

编辑-没有泛型的示例

以下是上述 Set 示例的示例,没有使用泛型。虽然可能,但并不是很愉快:

Set set = new HashSet();
set.add(10);
set.add(42);

int total = 0;
for (Object o : set) {
  total += (Integer)o;
}

(注意:以上代码将在编译时生成未经检查的转换警告。)

在使用非泛型集合时,输入到集合中的类型是Object类型的对象。 因此,在此示例中,一个Object被添加到集合中。

set.add(10);
set.add(42);
在以上代码中,自动装箱起了作用--原始的int1042被自动装箱成为Integer对象,并且被加入到Set集合中。但是请注意,Integer对象被处理为Object对象,因为没有类型信息可以帮助编译器知道Set集合应该期望什么类型。
for (Object o : set) {

这是至关重要的部分。for-each循环之所以起作用是因为 Set 实现了 Iterable 接口,该接口返回一个带有类型信息(如果有)的 Iterator。(即 Iterator<T>。)

然而,由于没有类型信息,Set 将返回一个 Iterator,该迭代器将把集合中的值作为 Object 返回,这就是为什么在 for-each 循环中检索的元素必须是 Object 类型的原因。

既然从 Set 检索到了 Object,就需要手动将其强制转换为 Integer 才能执行加法操作:

  total += (Integer)o;

这里将Object强制类型转换为Integer。在这种情况下,我们知道这总是有效的,但手动类型转换让我感到代码很脆弱,如果其他地方进行了微小的更改,它可能会被破坏。(我觉得每个类型转换都是等待发生ClassCastException的风险,但我跑题了...)

Integer现在被取消封箱并允许执行加法操作,将结果存入int类型的变量total中。

我希望我能够说明,虽然Java 5的新特性可以与非泛型代码一起使用,但这并不像使用泛型编写代码那样干净和直观。而且,在我看来,要充分利用Java 5的新特性,应该使用泛型,至少可以进行编译时检查,以防止无效的类型转换在运行时抛出异常。


增强型for循环的集成真的很好(尽管我认为该死的循环已经损坏了,因为我不能在非泛型集合中使用完全相同的语法——它应该只使用转换/自动装箱到int,它拥有所有需要的信息!),但你是对的,IDE中的帮助可能很好。我仍然不知道它是否足以证明额外的语法,但至少这是我可以从中受益的东西(这回答了我的问题)。 - Bill K
@Bill K:我已经添加了一个使用Java 5特性但不使用泛型的示例。 - coobird
这是一个很好的观点,我之前没有使用过(我提到我已经卡在1.4和早期版本上多年了)。我同意有一些优点,但我的结论是A)它们往往对那些不正确使用OO的人非常有利,而对那些正确使用OO的人则用处较少;B)你列出的优点确实是优点,但甚至不值得为了理解Java中实现的泛型而进行小的语法更改,更不用说需要进行大量更改了。但至少有一些优点。 - Bill K

15

如果你在1.5版本发布前搜索Java bug数据库,你会发现NullPointerExceptionClassCastException多七倍的错误。因此,它似乎不是一个很好的功能来查找错误,或者至少不是在一些简单的烟测试之后仍然存在的错误。

对我来说,泛型的巨大优势在于它们能够在代码中记录重要的类型信息。如果我不想在代码中记录类型信息,那么我会使用动态类型语言,或者至少使用更具有隐式类型推断的语言。

将对象的集合保留给自身并不是一个糟糕的风格(但一般的做法是忽略封装)。这取决于你正在做什么。使用泛型将集合传递给“算法”稍微容易进行检查(在编译时或之前)。


6
不仅仅被记录下来了(这本身就是一个巨大的胜利),它还受到编译器的强制执行。 - James Schek
好观点,但是通过将集合封装在定义“这种类型对象的集合”的类中,这也可以实现吧? - Bill K
1
詹姆斯:是的,这就是关于它被记录在代码中的重点。 - Tom Hawtin - tackline
我不认为我知道使用集合进行“算法”处理的好库(可能意味着您正在谈论“函数”,这让我相信它们应该是集合对象中的方法)。我认为JDK几乎总是提取一个漂亮、干净的数组,它是数据的副本,因此不能被搞乱——至少大多数情况下是如此。 - Bill K
此外,难道不是拥有一个描述集合使用方式并限制其使用的对象更好的编码文档形式吗?我发现在良好的代码中,泛型所提供的信息非常冗余。 - Bill K
汤姆——抱歉,只是想与那些认为“在代码中”是匈牙利命名法的人区分开来... - James Schek

11
Java中的泛型实现了参数多态性。通过类型参数,您可以将参数传递给类型。就像String foo(String s)这样的方法模拟了某些行为,不仅适用于特定的字符串,而且适用于任何字符串s,所以像List<T>这样的类型也模拟了某些行为,不仅适用于特定的类型,而且适用于任何类型List<T>表示对于任何类型T,都有一个类型的List,其元素是T。因此,List实际上是一个类型构造函数。它将一个类型作为参数,并构造另一个类型作为结果。

以下是我每天使用的几个泛型类型的示例。首先是非常有用的泛型接口:

public interface F<A, B> {
  public B f(A a);
}

这个接口表示,对于两种类型AB,有一个函数(称为f)接受一个A并返回一个B。当你实现这个接口时,AB可以是任何你想要的类型,只要你提供一个函数f,它接受前者并返回后者。下面是接口的一个示例实现:

F<Integer, String> intToString = new F<Integer, String>() {
  public String f(int i) {
    return String.valueOf(i);
  }
}

在泛型出现之前,通过使用extends关键字进行子类化来实现多态性。有了泛型,我们可以摆脱子类化,改用参数多态性。例如,考虑一个用于计算任何类型哈希码的参数化(泛型)类。我们将使用泛型类而不是覆盖Object.hashCode():

public final class Hash<A> {
  private final F<A, Integer> hashFunction;

  public Hash(final F<A, Integer> f) {
    this.hashFunction = f;
  }

  public int hash(A a) {
    return hashFunction.f(a);
  }
}

这比使用继承更加灵活,因为我们可以坚持使用组合和参数化多态主题,而不会锁定脆弱的层次结构。

然而,Java的泛型并不完美。您可以抽象类型,但无法抽象类型构造函数,例如。也就是说,您可以说“对于任何类型T”,但无法说“对于任何带有类型参数A的类型T”。

我写了一篇关于Java泛型限制的文章,在这里。

泛型的一个巨大优势是它们让您避免子类化。子类化往往导致脆弱的类层次结构,这些结构难以扩展,并且单独理解这些类需要查看整个层次结构。

在泛型之前,您可能会有像WidgetFooWidgetBarWidgetBazWidget这样的类,而有了泛型,您可以拥有一个单一的泛型类Widget<A>,它在其构造函数中接受FooBarBaz,以给您Widget<Foo>Widget<Bar>Widget<Baz>


如果 Java 没有接口,那么通过子类化将是一个好的观点。但是,让 FooWidget、BarWidget 和 BazWidget 都实现 Widget 不是正确的吗?尽管我可能错了,但如果我错了,这是一个非常好的观点。 - Bill K
为什么需要一个动态泛型类来计算哈希值?使用具有适当参数的静态函数是否更高效? - Anton Shepelev

10

泛型避免了装箱和拆箱的性能损失。基本上,看看ArrayList与List<T>。两者都做同样的核心事情,但是List<T>会更快,因为你不需要进行装箱和解箱操作。


这就是它的精髓,不是吗?很好。 - MrBoJangles
并不完全如此;它们对于类型安全和避免引用类型的强制转换也很有用,而且根本不涉及装箱/拆箱。 - ljs
那么接下来的问题是,“为什么我会想要使用ArrayList呢?”冒昧地回答一下,我认为只要你不指望频繁地装箱和拆箱它们的值,ArrayList就很好用。有什么评论吗? - MrBoJangles
不,它们在 .net 2 中已经过时了;只有用于向后兼容性。ArrayList 比较慢,不安全,并且需要装箱/拆箱或强制转换。 - ljs
如果您没有存储值类型(例如int或struct),则在使用ArrayList时不会进行装箱 - 这些类已经是“对象”。使用List<T>仍然更好,因为它具有类型安全性,如果您确实想要存储对象,则使用List<object>更好。 - Wilka
是的,但如果您使用引用类型,则会涉及强制转换,我相信这也会带来性能损失。更不用说不安全的类型和代码膨胀了! - ljs

7

泛型最大的好处是代码重用。假设您有很多业务对象,并且您将为每个实体编写非常相似的代码以执行相同的操作(例如Linq to SQL操作)。

使用泛型,您可以创建一个类,该类将能够在给定任何继承自给定基类或实现给定接口的类型时进行操作,如下所示:

public interface IEntity
{

}

public class Employee : IEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int EmployeeID { get; set; }
}

public class Company : IEntity
{
    public string Name { get; set; }
    public string TaxID { get; set }
}

public class DataService<ENTITY, DATACONTEXT>
    where ENTITY : class, IEntity, new()
    where DATACONTEXT : DataContext, new()
{

    public void Create(List<ENTITY> entities)
    {
        using (DATACONTEXT db = new DATACONTEXT())
        {
            Table<ENTITY> table = db.GetTable<ENTITY>();

            foreach (ENTITY entity in entities)
                table.InsertOnSubmit (entity);

            db.SubmitChanges();
        }
    }
}

public class MyTest
{
    public void DoSomething()
    {
        var dataService = new DataService<Employee, MyDataContext>();
        dataService.Create(new Employee { FirstName = "Bob", LastName = "Smith", EmployeeID = 5 });
        var otherDataService = new DataService<Company, MyDataContext>();
            otherDataService.Create(new Company { Name = "ACME", TaxID = "123-111-2233" });

    }
}

注意在上面的DoSomething方法中,相同的服务被重复使用但传入不同的类型。非常优雅!

还有许多其他很好的理由可以使用泛型来完成您的工作,这是我最喜欢的一种。


5
为了举个好例子,想象一下你有一个名为Foo的类。
public class Foo
{
   public string Bar() { return "Bar"; }
}

例子1 现在你想要一个Foo对象的集合。你有两个选项,List或ArrayList,两者的工作方式相似。

Arraylist al = new ArrayList();
List<Foo> fl = new List<Foo>();

//code to add Foos
al.Add(new Foo());
f1.Add(new Foo());

在以上代码中,如果我试图添加一个FireTruck类而不是Foo类,ArrayList将会添加它,但是泛型列表中的Foo将会抛出异常。
例如二:
现在您有两个数组列表,并且想要在每个上调用Bar()函数。由于ArrayList填充了对象,因此您必须在调用bar之前对它们进行类型转换。但是由于泛型列表只能包含Foos,因此您可以直接在其上调用Bar()。
foreach(object o in al)
{
    Foo f = (Foo)o;
    f.Bar();
}

foreach(Foo f in fl)
{
   f.Bar();
}

我猜你在看问题之前就回答了。那些两种情况对我帮助不大(如问题所述)。是否还有其他情况下它会有用处? - Bill K

5
我喜欢使用它们,因为它们提供了一种快速的方式来定义自定义类型(我无论如何都会使用它们)。
例如,您可以使用字典代替定义一个包含字符串和整数的结构体,并且不需要实现一整套对象和方法来访问这些结构体的数组等等。
Dictionary<int, string> dictionary = new Dictionary<int, string>();

编译器/集成开发环境会完成其余的繁重工作。特别是,字典类型让你可以使用第一个类型作为键(无重复值)。


5
  • 类型化集合 - 即使您不想使用它们,您也可能需要处理来自其他库、其他来源的类型化集合。

  • 类创建中的通用类型:

    public class Foo < T> { public T get()...

  • 避免强制转换 - 我一直不喜欢像这样的东西

    new Comparator { public int compareTo(Object o){ if (o instanceof classIcareAbout)...

在这里,您基本上正在检查应该只存在于对象术语中的条件。

我的最初反应与您类似 - “太乱了,太复杂了”。我的经验是,使用一段时间后,您会习惯使用它们,并且没有它们的代码感觉不太明确指定,也不太舒适。除此之外,Java世界的其余部分都在使用它们,所以您最终必须跟上程序,对吧?


1
为了准确避免转换:根据Sun文档,编译器会执行转换操作。您只需避免在代码中编写强制转换。 - Demi
也许我似乎陷入了一连串不愿超过1.4的公司,所以谁知道,在我达到5之前,我可能会退休。最近我也在思考分叉“SimpleJava”可能是一个有趣的概念——Java将继续添加功能,对于我看到的很多代码来说,这不是一件健康的事情。我已经处理过那些无法编写循环的人了——我不想看到他们试图将泛型注入他们毫无意义的展开循环中。 - Bill K
@Bill K:很少有人需要使用泛型的全部功能。只有当你正在编写一个本身是泛型的类时,这才是必要的。 - Eddie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接