我想我会把这个垒球提供给任何想从公园里踢出去的人。 什么是泛型,泛型的优点是什么,为什么,在哪里,应该如何使用它们? 请保持基本。 谢谢。
-
允许您编写代码/使用类型安全的库方法,即保证List 是字符串列表。
-
由于使用了泛型,因此编译器可以对代码进行编译时检查,以确保类型安全,即您是否试图将int放入该字符串列表中?使用ArrayList会导致该错误不太透明。
-
它比使用对象要快,因为它可以避免装箱/拆箱(.net必须将值类型转换为引用类型,反之亦然),也可以将对象强制转换为所需的引用类型。
-
允许您编写适用于许多具有相同基础行为的类型的代码,即Dictionary 使用与Dictionary 相同的基础代码;使用泛型,框架团队也只需编写一段代码即可实现上述两个优点。
我真的很讨厌重复自己。我讨厌经常输入相同的东西。我不喜欢在有些许差异的情况下多次重述。
而不是创建:
1 2 3 4 5 6 7 8 9
| class MyObjectList {
MyObject get(int index) {...}
}
class MyOtherObjectList {
MyOtherObject get(int index) {...}
}
class AnotherObjectList {
AnotherObject get(int index) {...}
} |
我可以建立一个可重用的类...(如果您出于某种原因不想使用原始集合)
1 2 3
| class MyList< T > {
T get(int index) { ... }
} |
现在,我的效率提高了3倍,并且只需要维护一份副本即可。为什么您不想维护更少的代码?
对于必须与其他类进行交互的非收集类(例如Callable< T >或Reference< T >)也是如此。您是否真的要扩展Callable< T >和Future< T >以及所有其他关联的类以创建类型安全的版本?
我不。
无需类型转换是Java泛型的最大优点之一,因为它将在编译时执行类型检查。这将减少在运行时抛出ClassCastException的可能性,并可以导致更健壮的代码。
好。
但是我怀疑您完全意识到这一点。
好。
Every time I look at Generics it gives
me a headache. I find the best part of
Java to be it's simplicity and minimal
syntax and generics are not simple and
add a significant amount of new
syntax.
Ok.
起初,我也没有看到泛型的好处。我从1.4语法开始学习Java(即使当时还没有Java 5),当我遇到泛型时,我觉得要编写的代码更多,而且我真的不明白这样做的好处。
好。
现代IDE使使用泛型编写代码更加容易。
好。
大多数现代的,体面的IDE都足够聪明,可以协助使用泛型编写代码,尤其是代码完成。
好。
这是用HashMap制作Map的示例。我必须输入的代码是:
好。
1
| Map<String, Integer> m = new HashMap<String, Integer>(); |
确实,要创建一个新的HashMap,要输入的内容很多。但是,实际上,我只需要在Eclipse知道我需要什么之前键入以下内容即可:
好。
Map m = new Ha Ctrl + Space
好。
的确,我确实需要从候选列表中选择HashMap,但是基本上IDE知道要添加的内容,包括通用类型。使用正确的工具,使用泛型还不错。
好。
另外,由于类型是已知的,因此从通用集合中检索元素时,IDE会像该对象已经是其声明类型的对象一样起作用-无需强制IDE强制转换即可知道该对象的类型是。
好。
泛型的主要优势来自其与Java 5新功能的良好配合方式。这是将整数扔进Set并计算其总数的示例:
好。
1 2 3 4 5 6 7 8
| Set<Integer> set = new HashSet<Integer>();
set.add(10);
set.add(42);
int total = 0;
for (int i : set) {
total += i;
} |
在这段代码中,提供了三个Java 5新功能:
好。
泛型
自动装箱和拆箱
每个循环
好。
首先,原语的泛型和自动装箱允许以下几行:
好。
1 2
| set.add(10);
set.add(42); |
整数10自动装箱到值为10的Integer中。 (与42相同)。然后,将该Integer扔入已知保存Integer的Set中。尝试放入String会导致编译错误。
好。
接下来,for-each循环将所有这三个条件都采用:
好。
1 2 3
| for (int i : set) {
total += i;
} |
首先,在每个循环中使用包含Integer的Set。每个元素都声明为一个int,并且由于Integer被取消装箱回到原语int,因此被允许。并且这种拆箱操作发生的事实是众所周知的,因为使用泛型指定了Set中保存的Integer。
好。
泛型可以是将Java 5中引入的新功能结合在一起的粘合剂,它只是使编码更简单,更安全。而且大多数时候,IDE足够聪明,可以为您提供良好的建议,因此,通常来说,键入的内容不会太多。
好。
坦率地说,从Set示例可以看出,我觉得利用Java 5功能可以使代码更加简洁和健壮。
好。
编辑-没有泛型的示例
好。
下面是上述Set示例的说明,没有使用泛型。可能,但并不完全令人满意:
好。
1 2 3 4 5 6 7 8
| Set set = new HashSet();
set.add(10);
set.add(42);
int total = 0;
for (Object o : set) {
total += (Integer)o;
} |
(注意:以上代码将在编译时生成未经检查的转换警告。)
好。
使用非泛型集合时,输入到集合中的类型是类型为Object的对象。因此,在此示例中,Object是要被add放入集合中的内容。
好。
1 2
| set.add(10);
set.add(42); |
在上述各行中,自动装箱正在起作用-将原始int值10和42自动装箱到Integer对象中,然后将这些对象添加到Set中。但是,请记住,Integer对象被作为Object处理,因为没有类型信息可以帮助编译器知道Set应该是什么类型。
好。
这是至关重要的部分。 for-each循环起作用的原因是因为Set实现了Iterable接口,该接口返回带有类型信息(如果存在)的Iterator。 (即Iterator< T >。)
好。
但是,由于没有类型信息,因此Set将返回Iterator,该Iterator将以Object s的形式返回Set中的值,这就是为什么在for-each循环中要检索的元素必须的类型为Object。
好。
现在,从Set中检索了Object,需要将其手动转换为Integer以执行加法:
好。
在此,从Object到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 >表示对于任何T类型,都有一种List类型,其元素为T s。所以List实际上是一个类型构造函数。它以一个类型作为参数,并构造另一个类型作为结果。
这是我每天使用的泛型类型的几个示例。首先,一个非常有用的通用接口:
1 2 3
| public interface F<A, B> {
public B f(A a);
} |
该接口表示,对于A和B两种类型,有一个函数(称为f),该函数采用A并返回B。当实现此接口时,A和B可以是您想要的任何类型,只要您提供接受前者并返回后者的函数f。这是该接口的示例实现:
1 2 3 4 5
| F<Integer, String> intToString = new F<Integer, String>() {
public String f(int i) {
return String.valueOf(i);
}
} |
在泛型之前,通过使用extends关键字进行子类化来实现多态。使用泛型,我们实际上可以消除子类化,而可以使用参数多态性。例如,考虑用于计算任何类型的哈希码的参数化(通用)类。而不是重写Object.hashCode(),我们将使用如下通用类:
1 2 3 4 5 6 7 8 9 10 11
| 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泛型的限制的文章。
泛型的一个巨大胜利就是它们可以避免子类化。子类化往往会导致脆弱的类层次结构难以扩展,并且难以在不查看整个层次结构的情况下单独理解类。
在泛型之前,您可能需要将Widget之类扩展为FooWidget,BarWidget和BazWidget的类,使用泛型,您可以拥有一个采用Foo,Bar或< x24>在其构造函数中给出Widget,Widget和Widget。
泛型避免了装箱和拆箱对性能的影响。基本上,请看ArrayList vs List < T >。两者都具有相同的核心功能,但是List < T >会快很多,因为您不必对对象进行装箱。
泛型的最大好处是代码重用。假设您有很多业务对象,并且您将为每个实体编写非常相似的代码以执行相同的操作。 (即Linq to SQL操作)。
使用泛型,您可以创建一个类,该类将能够给定从给定基类继承的任何类型的给定类型,或者实现给定接口,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| 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
1
| Dictionary<int, string> dictionary = new Dictionary<int, string>(); |
而编译器/ IDE会完成其余的繁重工作。特别是使用"字典",您可以将第一种类型用作键(无重复值)。
举一个很好的例子。假设您有一个名为Foo的课程
1 2 3 4
| public class Foo
{
public string Bar() { return"Bar"; }
} |
例子1
现在,您要具有Foo对象的集合。您有两个选项LIst或ArrayList,它们的工作方式都相似。
1 2 3 4 5 6
| 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。但是由于Foo的通用列表只能包含Foos,因此您可以直接在它们上调用Bar()。
1 2 3 4 5 6 7 8 9 10
| foreach(object o in al)
{
Foo f = (Foo)o;
f.Bar();
}
foreach(Foo f in fl)
{
f.Bar();
} |
-
类型化的集合-即使您不想使用它们,也可能不得不从其他库或其他来源处理它们。
-
类创建中的泛型键入:
公共课Foo < T > {
公共T get()...
-
避免强制转换-我一直不喜欢这样的事情
新的比较器{
public int compareTo(Object o){
如果(o classof classIcareAbout)...
本质上,您在检查仅应存在的条件,因为接口是以对象表示的。
我对仿制药的最初反应与您的相似-"太混乱了,太复杂了"。我的经验是,使用它们一段时间后,您会习惯它们,没有它们的代码感觉不太清晰,也不太舒服。除此之外,java世界的其余部分都使用它们,因此您最终将不得不使用该程序,对吗?
您是否从未编写过方法(或类),其中方法/类的关键概念未紧密绑定到参数/实例变量的特定数据类型(请考虑链接列表,max / min函数,二进制搜索)等)。
您是否从未希望过可以重用算法/代码而不必求助于n-paste重用或损害强类型(例如,我希望使用List的字符串,而不是我希望是字符串的List! )?
这就是为什么您应该使用泛型(或更好的东西)的原因。
不要忘记,泛型不仅被类使用,它们也可以被方法使用。例如,采用以下代码段:
1 2 3 4
| private <T extends Throwable> T logAndReturn(T t) {
logThrowable(t); // some logging method that takes a Throwable
return t;
} |
它很简单,但是可以非常优雅地使用。令人高兴的是,该方法返回的结果与给定的一样。当您处理需要重新抛出给调用者的异常时,这将有所帮助:
1 2 3 4
| ...
} catch (MyException e) {
throw logAndReturn(e);
} |
关键是,您不会通过将其传递给方法来丢失该类型。您可以抛出正确的异常类型,而不仅仅是Throwable,这是没有泛型的全部操作。
这只是通用方法的一种简单使用示例。泛型方法还可以做很多其他的事情。在我看来,最酷的是使用泛型进行类型推断。请看以下示例(摘自Josh Bloch的Effective Java 2nd Edition):
1 2 3 4 5 6
| ...
Map<String, Integer> myMap = createHashMap();
...
public <K, V> Map<K, V> createHashMap() {
return new HashMap<K, V>();
} |
这并不能解决很多问题,但是当泛型类型很长(或嵌套;即Map>)时,确实可以减少混乱。
正如Mitchel指出的那样,主要优点是无需定义多个类即可进行强类型化。
这样,您可以执行以下操作:
1 2
| List<SomeCustomClass> blah = new List<SomeCustomClass>();
blah[0].SomeCustomFunction(); |
如果没有泛型,则必须将blah [0]强制转换为正确的类型才能访问其功能。
无论如何,jvm强制转换...它隐式创建将通用类型视为"对象"的代码,并创建对所需实例化的强制转换。 Java泛型只是语法糖。
我知道这是一个C#问题,但是泛型也用在其他语言中,并且它们的用途/目标非常相似。
Java集合从Java 1.5开始使用泛型。因此,使用它们的一个好地方是在创建自己的类似集合的对象时。
我几乎在每个地方都可以看到一个示例,Pair类具有两个对象,但是需要以通用方式处理这些对象。
1 2 3 4 5 6 7 8 9 10
| 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方法):
1 2 3 4 5
| public class FooManager <F extends Foo>{
public void setTitle(F foo, String title) {
foo.setTitle(title);
}
} |
尽管它本身并不是很有趣,但是知道每当您使用FooManager时,您都知道它将处理MyClass类型,并且MyClass扩展了Foo,这很有用。
从Sun Java文档中,响应"我为什么要使用泛型?":
"泛型为您提供了一种向编译器传达集合类型的方法,以便可以对其进行检查。一旦编译器知道集合的元素类型,编译器便可以检查您是否一致地使用了集合并可以插入对从集合中取出的值进行正确的强制转换...使用泛型的代码更清晰,更安全....编译器可以在编译时验证在运行时不违反类型约束[强调我的]。程序编译时没有警告,我们可以肯定地说它不会在运行时引发ClassCastException。使用泛型的最终效果是提高可读性和鲁棒性,尤其是在大型程序中。
泛型允许您创建强类型化的对象,但不必定义特定类型。我认为最有用的示例是List和类似的类。
使用通用列表,您可以随心所欲地拥有一个列表列表,并且您始终可以引用强类型,而不必进行转换或类似数组或标准列表的操作。
要添加/扩展的几件事(从.NET角度而言):
泛型类型使您可以创建基于角色的类和接口。这已经用更基本的术语说过了,但是我发现您开始使用以与类型无关的方式实现的类来设计代码-这导致了高度可重用的代码。
方法的通用论点可以做同样的事情,但是它们也有助于将"告诉不问"原理应用于转换,即"给我我想要的,如果不能,请告诉我原因"。
总之,泛型使您可以更精确地指定要执行的操作(更强的键入)。
这对您有几个好处:
最主要的原因是它们提供了类型安全性
1
| List<Customer> custCollection = new List<Customer>; |
相对于
1
| object[] custCollection = new object[] { cust1, cust2 }; |
作为一个简单的例子。
使用泛型(尤其是集合/列表)的另一个优点是您可以获得编译时类型检查。当使用通用列表而不是对象列表时,这真的很有用。
已经提到了诸如"类型安全"和"无强制转换"之类的明显好处,因此也许我可以谈谈其他一些"好处",希望对它有帮助。
首先,泛型是一个与语言无关的概念,而IMO,如果您同时考虑常规(运行时)多态性,则可能更有意义。
例如,我们从面向对象设计中了解到的多态性具有运行时概念,即在调用者对象随程序执行时在运行时确定,并根据运行时类型相应地调用相关方法。在泛型中,这个想法有些相似,但是所有事情都是在编译时发生的。这是什么意思,以及您如何利用它?
(让我们坚持使用通用方法来保持紧凑)这意味着您仍然可以在单独的类上使用相同的方法(就像您以前在多态类中所做的一样),但是这次它们由编译器自动生成,取决于设置的类型在编译时。您可以根据编译时给出的类型对方法进行参数化。因此,不必像在运行时多态性(方法重写)中那样为每种类型从头开始编写方法,而应让编译器在编译期间完成工作。这具有明显的优势,因为您无需推断系统中可能使用的所有可能的类型,这无需更改代码即可使其具有更高的可伸缩性。
类的工作方式几乎相同。您对类型进行参数化,并且代码由编译器生成。
一旦有了"编译时间"的概念,就可以使用"有界"类型,并限制可以通过类/方法作为参数化类型传递的内容。因此,您可以控制要传递的内容是有力的,尤其是您的框架正在被其他人使用。
1 2 3
| public interface Foo<T extends MyObject> extends Hoo< T >{
...
} |
现在没有人可以设置MyObject以外的其他功能。
另外,您可以在方法参数上"强制"类型约束,这意味着可以确保两个方法参数都依赖于同一类型。
1 2 3
| public <T extends MyObject> foo(T t1, T t2){
...
} |
希望所有这些都有意义。
我在使用SpringORM和Hibernate实现的GenericDao中使用了它们,例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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子类化而将它们仅为其设计的实体传递给他们。
1 2 3 4 5 6 7 8
| 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
} |
我的小框架更加健壮(具有过滤,延迟加载,搜索之类的功能)。我只是在这里简化了一个例子
我像史蒂夫和您一样,在一开始就说"太凌乱和复杂",但现在我看到了它的优势
如果您的集合包含值类型,则在将它们插入到集合中时不需要对对象进行装箱/拆箱操作,因此您的性能会大大提高。诸如resharper之类的很酷的插件可以为您生成更多代码,例如foreach循环。
泛型使您可以对应该包含任何对象的对象和数据结构使用强类型。从通用结构(装箱/拆箱)中检索对象时,它还消除了繁琐而昂贵的类型转换。
一个同时使用两者的示例是一个链表。如果只能使用对象Foo,则链表类有什么好处?要实现可处理任何类型对象的链表,如果您希望链表仅包含一种类型的对象,则链表和假设节点内部类中的节点必须是通用的。
使用泛型进行收集既简单又干净。即使您在其他任何地方都花钱,从收藏中获得的收益对我来说也是一个胜利。
1 2 3 4
| List<Stuff> stuffList = getStuff();
for(Stuff stuff : stuffList) {
stuff.do();
} |
与
1 2 3 4 5 6
| List stuffList = getStuff();
Iterator i = stuffList.iterator();
while(i.hasNext()) {
Stuff stuff = (Stuff)i.next();
stuff.do();
} |
要么
1 2 3 4 5
| List stuffList = getStuff();
for(int i = 0; i < stuffList.size(); i++) {
Stuff stuff = (Stuff)stuffList.get(i);
stuff.do();
} |
仅凭这一点就值得泛型的边际"成本",而且您不必成为泛型大师就可以使用它并获得价值。
泛型还使您能够创建更多可重用的对象/方法,同时仍提供特定于类型的支持。在某些情况下,您还会获得很多性能。我不了解Java泛型的完整规范,但是在.NET中,我可以在Type参数上指定约束,例如,实现接口,构造函数和派生。
我曾经就这个话题发表过演讲。您可以在http://www.adventuresinsoftware.com/generics/上找到我的幻灯片,代码和音频记录。