我想我会把这个垒球提供给任何想把它打出公园的人。什么是泛型,泛型的优点是什么,为什么,在哪里,如何使用它们?请保持相当基本。谢谢。
29 回答
- 允许您编写代码/使用类型安全的库方法,即保证 List<string> 是字符串列表。
- 由于使用了泛型,编译器可以对代码执行编译时检查以确保类型安全,即您是否尝试将 int 放入该字符串列表中?使用 ArrayList 会导致它是一个不太透明的运行时错误。
- 比使用对象更快,因为它可以避免装箱/拆箱(.net 必须将值类型转换为引用类型,反之亦然)或从对象转换为所需的引用类型。
- 允许您编写适用于具有相同基础行为的许多类型的代码,即 Dictionary<string, int> 使用与 Dictionary<DateTime, double> 相同的基础代码;使用泛型,框架团队只需要编写一段代码就可以实现上述两种结果。
我真的很讨厌重复自己。我讨厌比我必须更频繁地输入相同的东西。我不喜欢多次重复有细微差别的事情。
而不是创建:
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 倍,而且我只需要维护一份副本。为什么不想维护更少的代码?
对于必须与其他类交互的非集合类(例如 aCallable<T>
或 a )也是如此。Reference<T>
你真的想扩展Callable<T>
和 Future<T>
其他所有相关的类来创建类型安全的版本吗?
我不。
不需要类型转换是 Java 泛型的最大优势之一,因为它将在编译时执行类型检查。这将减少在运行时抛出 s 的可能性ClassCastException
,并且可以产生更健壮的代码。
但我怀疑你完全意识到这一点。
每次看到泛型都让我头疼。我发现 Java 最好的部分是它的简单性和最小的语法,泛型并不简单,并且添加了大量的新语法。
起初,我也没有看到泛型的好处。我从 1.4 语法开始学习 Java(尽管当时 Java 5 已经出来了),当我遇到泛型时,我觉得要写更多的代码,我真的不明白它的好处。
现代 IDE 使使用泛型编写代码变得更加容易。
大多数现代的、体面的 IDE 都足够聪明,可以帮助使用泛型编写代码,尤其是代码完成。
这是一个Map<String, Integer>
用HashMap
. 我必须输入的代码是:
Map<String, Integer> m = new HashMap<String, Integer>();
事实上,为了制作一个新的HashMap
. 然而,实际上,在 Eclipse 知道我需要什么之前,我只需要输入这么多:
Map<String, Integer> m = new Ha
Ctrl+Space
没错,我确实需要HashMap
从候选列表中进行选择,但基本上 IDE 知道要添加什么,包括泛型类型。使用正确的工具,使用泛型并不算太糟糕。
此外,由于类型是已知的,当从泛型集合中检索元素时,IDE 将表现得好像该对象已经是其声明类型的对象——不需要强制转换让 IDE 知道对象的类型是。
泛型的一个关键优势来自于它与 Java 5 新特性的良好配合。这是一个将整数放入 aSet
并计算其总数的示例:
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
被自动装箱为Integer
值为 的10
。(同样适用42
)。然后将Integer
其扔进Set
已知持有Integer
s 的 中。尝试抛出 aString
会导致编译错误。
接下来,for for-each 循环采用所有这三个:
for (int i : set) {
total += i;
}
首先,Set
包含Integer
s 用于 for-each 循环。每个元素都被声明为 anint
并且是允许的,因为Integer
被拆箱回原语int
。并且这种拆箱发生的事实是众所周知的,因为泛型用于指定Integer
在Set
.
泛型可以成为结合 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;
}
(注意:上面的代码会在编译时产生未经检查的转换警告。)
使用非泛型集合时,输入到集合中的类型是 type 的对象Object
。因此,在此示例中,aObject
是被add
编入集合的内容。
set.add(10);
set.add(42);
在上面的几行中,自动装箱正在发挥作用——原始int
值10
并被自动装箱42
到Integer
对象中,这些对象被添加到Set
. 但是,请记住,Integer
对象是作为Object
s 处理的,因为没有类型信息可以帮助编译器知道Set
应该期望什么类型。
for (Object o : set) {
这是至关重要的部分。for-each 循环起作用的原因是因为它Set
实现了Iterable
接口,该接口返回Iterator
带有类型信息(如果存在)的信息。( Iterator<T>
,即。)
但是,由于没有类型信息,因此Set
将返回 an Iterator
,它将返回Set
as中的值Object
,这就是为什么在 for-each 循环中检索的元素必须是 type 的原因Object
。
现在Object
从 中检索到Set
,需要Integer
手动将其强制转换为 a 以执行添加:
total += (Integer)o;
在这里,从 anObject
到 an执行类型转换Integer
。在这种情况下,我们知道这将始终有效,但是手动类型转换总是让我觉得它是脆弱的代码,如果在其他地方进行微小的更改可能会损坏。(我觉得每个类型转换都是ClassCastException
等待发生,但我离题了......)
现在Integer
已将 拆箱int
并允许对int
变量执行加法运算total
。
我希望我能说明 Java 5 的新特性可以与非泛型代码一起使用,但它不像使用泛型编写代码那样干净和直接。而且,在我看来,要充分利用 Java 5 中的新特性,应该研究泛型,如果至少允许编译时检查以防止无效类型转换在运行时抛出异常。
如果您在 1.5 发布之前搜索 Java 错误数据库,您会发现NullPointerException
比ClassCastException
. 因此,寻找错误似乎不是一个很棒的功能,或者至少是在经过一些冒烟测试后仍然存在的错误。
对我来说,泛型的巨大优势在于它们在代码中记录了重要的类型信息。如果我不想在代码中记录类型信息,那么我会使用动态类型语言,或者至少是一种具有更多隐式类型推断的语言。
将对象的集合保留为自身并不是一种糟糕的风格(但常见的风格是有效地忽略封装)。这取决于你在做什么。使用泛型(在编译时或之前)将集合传递给“算法”稍微容易一些。
Java 中的泛型促进了参数多态性。通过类型参数,您可以将参数传递给类型。就像一个方法String foo(String s)
模拟一些行为一样,不仅仅是一个特定的字符串,而是任何字符串s
,所以一个类型List<T>
模拟一些行为,不仅仅是一个特定的类型,而是任何类型。List<T>
说对于任何 type T
,都有一个 type 的List
元素是T
s。实际上也是List
一个类型构造函数。它将一个类型作为参数并构造另一个类型作为结果。
以下是我每天使用的几个泛型类型的示例。首先,一个非常有用的通用接口:
public interface F<A, B> {
public B f(A a);
}
这个接口表示,对于某些两种类型,A
和B
,有一个函数(称为f
),它接受一个A
并返回一个B
。当你实现这个接口时,A
可以B
是你想要的任何类型,只要你提供一个函数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”。
泛型的一个巨大胜利是它们可以让你避免子类化。子类化往往会导致难以扩展的脆弱的类层次结构,以及如果不查看整个层次结构就难以单独理解的类。
在泛型之前,您可能拥有像Widget
扩展了FooWidget
、、BarWidget
和这样的类BazWidget
,使用泛型,您可以拥有一个泛型类,该类Widget<A>
采用Foo
,Bar
或Baz
在其构造函数中为您提供Widget<Foo>
,Widget<Bar>
和Widget<Baz>
.
泛型避免了装箱和拆箱对性能的影响。基本上,看看 ArrayList 与 List<T>。两者都做相同的核心事情,但 List<T> 会快很多,因为您不必对对象进行装箱/装箱。
泛型的最大好处是代码重用。假设您有很多业务对象,并且您将为每个实体编写非常相似的代码来执行相同的操作。(IE 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 方法中不同类型的相同服务的重用。真优雅!
在你的工作中使用泛型还有很多其他重要的理由,这是我最喜欢的。
我只是喜欢它们,因为它们为您提供了一种快速定义自定义类型的方法(无论如何我都使用它们)。
因此,例如,无需定义由字符串和整数组成的结构,然后必须实现一整套对象和方法来了解如何访问这些结构的数组等等,您只需制作一个 Dictionary
Dictionary<int, string> dictionary = new Dictionary<int, string>();
编译器/IDE 完成了其余的繁重工作。尤其是 Dictionary 允许您使用第一种类型作为键(无重复值)。
类型化的集合——即使你不想使用它们,你也可能不得不从其他库、其他来源处理它们。
类创建中的通用类型:
public class Foo < T> { public T get()...
避免铸造 - 我一直不喜欢这样的事情
new Comparator { public int compareTo(Object o){ if (o instanceof classIcareAbout)...
您实际上是在检查一个只应该存在的条件,因为接口是用对象表示的。
我对泛型的最初反应与你的相似——“太杂乱,太复杂”。我的经验是,在使用它们一段时间后,你会习惯它们,没有它们的代码感觉不太明确,而且不太舒服。除此之外,Java 世界的其余部分都在使用它们,所以您最终将不得不使用该程序,对吧?
举一个很好的例子。想象一下你有一个名为 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 的 Generic List 会导致抛出异常。
例二。
现在您有了两个数组列表,并且您想在每个列表上调用 Bar() 函数。由于 hte ArrayList 中充满了对象,因此您必须先转换它们,然后才能调用 bar。但是由于 Foo 的通用列表只能包含 Foos,因此您可以直接在这些上调用 Bar()。
foreach(object o in al)
{
Foo f = (Foo)o;
f.Bar();
}
foreach(Foo f in fl)
{
f.Bar();
}
您是否曾经编写过方法(或类),其中方法/类的关键概念没有紧密绑定到参数/实例变量的特定数据类型(想想链表、最大/最小函数、二进制搜索, ETC。)。
你有没有希望你可以重用算法/代码而不诉诸剪切-粘贴重用或损害强类型(例如,我想要一个List
字符串,而不是List
我希望是字符串的东西!)?
这就是为什么您应该使用泛型(或更好的东西)。
不要忘记泛型不仅仅被类使用,它们也可以被方法使用。例如,采用以下代码段:
private <T extends Throwable> T logAndReturn(T t) {
logThrowable(t); // some logging method that takes a Throwable
return t;
}
它很简单,但可以非常优雅地使用。好消息是该方法返回它给出的任何内容。当您处理需要重新抛出给调用者的异常时,这会有所帮助:
...
} catch (MyException e) {
throw logAndReturn(e);
}
关键是通过方法传递它不会丢失类型。您可以抛出正确类型的异常,而不仅仅是 a Throwable
,这将是您在没有泛型的情况下所能做的。
这只是泛型方法的一种用途的简单示例。使用泛型方法可以做很多其他巧妙的事情。在我看来,最酷的是使用泛型进行类型推断。举个例子(取自 Josh Bloch 的 Effective Java 2nd Edition):
...
Map<String, Integer> myMap = createHashMap();
...
public <K, V> Map<K, V> createHashMap() {
return new HashMap<K, V>();
}
这并没有多大作用,但是当泛型类型很长(或嵌套;即Map<String, List<String>>
)时,它确实减少了一些混乱。
正如 Mitchel 所指出的,主要优势是无需定义多个类的强类型。
这样,您可以执行以下操作:
List<SomeCustomClass> blah = new List<SomeCustomClass>();
blah[0].SomeCustomFunction();
如果没有泛型,您必须将 blah[0] 转换为正确的类型才能访问其功能。
jvm 无论如何都会强制转换......它隐式地创建将泛型类型视为“对象”的代码,并创建对所需实例化的强制转换。Java 泛型只是语法糖。
我知道这是一个 C# 问题,但泛型也用于其他语言,它们的用途/目标非常相似。
Java 集合从 Java 1.5 开始使用泛型。因此,使用它们的好地方是当您创建自己的类似集合的对象时。
我几乎在任何地方都能看到的一个例子是 Pair 类,它包含两个对象,但需要以通用方式处理这些对象。
class Pair<F, S> {
public final F first;
public final S second;
public Pair(F f, S s)
{
first = f;
second = s;
}
}
每当您使用这个 Pair 类时,您都可以指定您希望它处理哪种类型的对象,并且任何类型转换问题都将在编译时出现,而不是在运行时出现。
泛型也可以使用关键字“super”和“extends”定义其边界。例如,如果你想处理一个泛型类型,但你想确保它扩展了一个名为 Foo 的类(它有一个 setTitle 方法):
public class FooManager <F extends Foo>{
public void setTitle(F foo, String title) {
foo.setTitle(title);
}
}
虽然它本身不是很有趣,但知道每当您处理 FooManager 时,您知道它将处理 MyClass 类型,并且 MyClass 扩展了 Foo 是很有用的。
来自 Sun Java 文档,以回应“我为什么要使用泛型?”:
“泛型为您提供了一种将集合的类型传达给编译器的方法,以便对其进行检查。一旦编译器知道集合的元素类型,编译器就可以检查您是否一致地使用了该集合并可以插入对从集合中取出的值进行正确的强制转换...使用泛型的代码更清晰、更安全....编译器可以在编译时验证在运行时没有违反类型约束[强调我的]。因为程序编译时没有警告,我们可以肯定地说它不会在运行时抛出 ClassCastException。使用泛型的净效果,尤其是在大型程序中,提高了可读性和鲁棒性。[强调我的]“
泛型允许您创建强类型的对象,但您不必定义特定类型。我认为最有用的例子是 List 和类似的类。
使用通用列表,您可以拥有任何您想要的列表列表,并且您始终可以引用强类型,您不必转换或像使用数组或标准列表那样做任何事情。
泛型允许您对应该能够容纳任何对象的对象和数据结构使用强类型。在从通用结构(装箱/拆箱)中检索对象时,它还消除了繁琐且昂贵的类型转换。
一个同时使用两者的例子是链表。如果链表类只能使用对象 Foo,它会有什么好处?要实现可以处理任何类型对象的链表,如果您希望列表仅包含一种类型的对象,则链表和假设节点内部类中的节点必须是通用的。
如果您的集合包含值类型,则它们在插入集合时不需要对对象进行装箱/取消装箱,因此您的性能会显着提高。像 resharper 这样的很酷的插件可以为你生成更多的代码,比如 foreach 循环。
使用泛型(尤其是集合/列表)的另一个优点是您可以获得编译时类型检查。这在使用通用列表而不是对象列表时非常有用。
最重要的一个原因是它们提供了类型安全
List<Customer> custCollection = new List<Customer>;
相反,
object[] custCollection = new object[] { cust1, cust2 };
作为一个简单的例子。
总之,泛型允许您更精确地指定您打算做什么(更强的类型)。
这对您有几个好处:
因为编译器更了解你想要做什么,它允许你省略很多类型转换,因为它已经知道类型是兼容的。
这还可以让您更早地获得有关程序正确性的反馈。以前在运行时会失败的事情(例如,因为无法将对象转换为所需的类型),现在在编译时会失败,您可以在测试部门提交神秘的错误报告之前修复错误。
编译器可以做更多的优化,比如避免装箱等。
需要添加/扩展的几件事(从 .NET 的角度来看):
泛型类型允许您创建基于角色的类和接口。这已经在更基本的术语中说过了,但是我发现您开始使用以与类型无关的方式实现的类来设计代码 - 这会产生高度可重用的代码。
方法上的通用参数可以做同样的事情,但它们也有助于将“告诉不要问”原则应用于强制转换,即“给我我想要的,如果你不能,你告诉我为什么”。
例如,我在使用 SpringORM 和 Hibernate 实现的 GenericDao 中使用它们,看起来像这样
public abstract class GenericDaoHibernateImpl<T>
extends HibernateDaoSupport {
private Class<T> type;
public GenericDaoHibernateImpl(Class<T> clazz) {
type = clazz;
}
public void update(T object) {
getHibernateTemplate().update(object);
}
@SuppressWarnings("unchecked")
public Integer count() {
return ((Integer) getHibernateTemplate().execute(
new HibernateCallback() {
public Object doInHibernate(Session session) {
// Code in Hibernate for getting the count
}
}));
}
.
.
.
}
通过使用泛型,我对这个 DAO 的实现强制开发人员只通过子类化 GenericDao 来传递它们为它们设计的实体
public class UserDaoHibernateImpl extends GenericDaoHibernateImpl<User> {
public UserDaoHibernateImpl() {
super(User.class); // This is for giving Hibernate a .class
// work with, as generics disappear at runtime
}
// Entity specific methods here
}
我的小框架更健壮(具有过滤、延迟加载、搜索等功能)。我只是在这里简化了给你一个例子
我和史蒂夫和你一样,一开始说“太乱太复杂”,但现在我看到了它的优点
已经提到了诸如“类型安全”和“无强制转换”之类的明显好处,所以也许我可以谈谈其他一些“好处”,我希望它会有所帮助。
首先,泛型是一个独立于语言的概念,IMO,如果您同时考虑常规(运行时)多态性,它可能更有意义。
例如,我们从面向对象设计中知道的多态性有一个运行时概念,其中调用者对象在运行时随着程序执行的进行而被计算出来,并根据运行时类型相应地调用相关方法。在泛型中,这个想法有点相似,但一切都发生在编译时。这是什么意思以及你如何使用它?
(让我们坚持使用泛型方法以保持紧凑)这意味着您仍然可以在不同的类上使用相同的方法(就像您之前在多态类中所做的那样)但是这次它们是由编译器自动生成的,取决于类型集在编译时。你在编译时给你的类型参数化你的方法。因此,与其像在运行时多态性(方法覆盖)中那样为每个类型从头编写方法,不如让编译器在编译期间完成工作。这有一个明显的优势,因为您不需要推断系统中可能使用的所有可能类型,这使得它在不更改代码的情况下更具可扩展性。
类的工作方式几乎相同。您对类型进行参数化,代码由编译器生成。
一旦您了解了“编译时间”的概念,您就可以使用“有界”类型并限制可以通过类/方法作为参数化类型传递的内容。因此,您可以控制要传递的内容,这是一个强大的东西,尤其是您有一个被其他人使用的框架。
public interface Foo<T extends MyObject> extends Hoo<T>{
...
}
现在没有人可以设置除了 MyObject 之外的东西。
此外,您可以对方法参数“强制”类型约束,这意味着您可以确保两个方法参数都依赖于相同的类型。
public <T extends MyObject> foo(T t1, T t2){
...
}
希望所有这些都有意义。
我曾经就这个话题发表过演讲。您可以在http://www.adventuresinsoftware.com/generics/找到我的幻灯片、代码和录音。
对集合使用泛型非常简单明了。即使您在其他任何地方都使用它,从收藏中获得的收益对我来说也是一种胜利。
List<Stuff> stuffList = getStuff();
for(Stuff stuff : stuffList) {
stuff.do();
}
对比
List stuffList = getStuff();
Iterator i = stuffList.iterator();
while(i.hasNext()) {
Stuff stuff = (Stuff)i.next();
stuff.do();
}
或者
List stuffList = getStuff();
for(int i = 0; i < stuffList.size(); i++) {
Stuff stuff = (Stuff)stuffList.get(i);
stuff.do();
}
仅此一项就值得仿制药的边际“成本”,而且您不必成为仿制药大师即可使用它并获得价值。
泛型还使您能够创建更多可重用的对象/方法,同时仍提供特定于类型的支持。在某些情况下,您还会获得很多性能。我不知道 Java 泛型的完整规范,但在 .NET 中,我可以指定对 Type 参数的约束,例如实现接口、构造函数和派生。
使程序员能够实现泛型算法 - 通过使用泛型,程序员可以实现适用于不同类型集合的泛型算法,可以自定义,并且是类型安全的并且更易于阅读。
Stronger type checks at compile time - A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find.
Elimination of casts.