我想为任何想要击出漂亮一击的人提供这个简单问题。什么是泛型,泛型的优点是什么,为什么、在哪里以及如何应用它们?请保持基本水平。谢谢。
我非常讨厌重复自己。我不想多次输入相同的内容,也不喜欢反复陈述几乎相同,只有微小区别的事情。
所以我们可以这样做:
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>
和每个相关的类来创建类型安全版本吗?
我不想这样。
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 Ha
Ctrl+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
被自动装箱为值为10
的Integer
对象(同样适用于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);
在以上代码中,自动装箱起了作用--原始的int
值10
和42
被自动装箱成为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的新特性,应该使用泛型,至少可以进行编译时检查,以防止无效的类型转换在运行时抛出异常。
如果你在1.5版本发布前搜索Java bug数据库,你会发现NullPointerException
比ClassCastException
多七倍的错误。因此,它似乎不是一个很好的功能来查找错误,或者至少不是在一些简单的烟测试之后仍然存在的错误。
对我来说,泛型的巨大优势在于它们能够在代码中记录重要的类型信息。如果我不想在代码中记录类型信息,那么我会使用动态类型语言,或者至少使用更具有隐式类型推断的语言。
将对象的集合保留给自身并不是一个糟糕的风格(但一般的做法是忽略封装)。这取决于你正在做什么。使用泛型将集合传递给“算法”稍微容易进行检查(在编译时或之前)。
String foo(String s)
这样的方法模拟了某些行为,不仅适用于特定的字符串,而且适用于任何字符串s
,所以像List<T>
这样的类型也模拟了某些行为,不仅适用于特定的类型,而且适用于任何类型。List<T>
表示对于任何类型T
,都有一个类型的List
,其元素是T
。因此,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>会更快,因为你不需要进行装箱和解箱操作。
泛型最大的好处是代码重用。假设您有很多业务对象,并且您将为每个实体编写非常相似的代码以执行相同的操作(例如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方法中,相同的服务被重复使用但传入不同的类型。非常优雅!
还有许多其他很好的理由可以使用泛型来完成您的工作,这是我最喜欢的一种。
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());
foreach(object o in al)
{
Foo f = (Foo)o;
f.Bar();
}
foreach(Foo f in fl)
{
f.Bar();
}
Dictionary<int, string> dictionary = new Dictionary<int, string>();
编译器/集成开发环境会完成其余的繁重工作。特别是,字典类型让你可以使用第一个类型作为键(无重复值)。
类型化集合 - 即使您不想使用它们,您也可能需要处理来自其他库、其他来源的类型化集合。
类创建中的通用类型:
public class Foo < T> { public T get()...
避免强制转换 - 我一直不喜欢像这样的东西
new Comparator { public int compareTo(Object o){ if (o instanceof classIcareAbout)...
在这里,您基本上正在检查应该只存在于对象术语中的条件。
我的最初反应与您类似 - “太乱了,太复杂了”。我的经验是,使用一段时间后,您会习惯使用它们,并且没有它们的代码感觉不太明确指定,也不太舒适。除此之外,Java世界的其余部分都在使用它们,所以您最终必须跟上程序,对吧?
编写适用于许多类型具有相同基本行为的代码
。 - LCJ