您的位置:首页 > 编程语言 > Java开发

泛型(Generics)

2017-05-20 19:18 183 查看
泛型于Java1.5中被提出,这个期待已久的泛型增强了系统类型,在提供运行时安全类型,允许一个类型或者方法在不同类型的对象上执行操作。它给集合框架增加了编译时的安全类型和减少了强制转换类型的繁琐工作。

简介

简单的泛型

泛型和子类型化

通配符

泛型方法

交互遗留代码

要点(The Fine Print)

类文字作为运行时类型标记(Class Literals as Runtime-Type Tokens)

更加有趣的通配符

将遗留代码改为使用泛型

鸣谢

简介

对Java编程语言,JDK5.0引进了一些新的扩展,其中的一项是泛型。

本节将会介绍泛型。你可能从其他的语言中暑期相似的结构,如c++中的模板,如果这样,你将会看到这两者之间的相似点和不同点。而如果你从其他地方没了解过,不熟悉这中结构,也没关系,你将作为一个全新的初学者,不用学习任何混杂的概念。

泛型允许你从类型中抽象出来。最常见的例子是集合类型,如集合层次结构中框架。

下面是一个简短的典型例子:

List myIntList = new LinkedList();//1
myIntList.add(new Integer(0));//2
Integer x = (Integer)myIntList.iterator().next();//3


第三行中的强制转换有些令人烦恼。在开始,程序知道什么样的类型数据被放到指定的集合中,然后,强制转换又是必须的,编译只能保证迭代器返回的使一个Object类型,为了保证分配给Integer类型变量是类型安全的,则必须要强制转换。

强制转换不仅混乱,而且,由于程序员的错误,可能造成运行时错误。

假如程序员能够表达他们的意图,并将限定为只能存放指定的类型?这就是泛型的核心思想。下面的版本片段是使用泛型来实现上述的例子:

List<Integer> myIntList = new LinkedList<Integer>();//1
myIntList.add(new Integer(0));//2
Integer x = myIntList.iterator().next();//3


注意myIntList中声明的变量类型。它说明了这不是一个任意类型的集合,而是一组Integer的集合,写成List.我们成List是一个泛型接口,它需要一个类型参数,在这个例子中,就是Integer。我们同时在创建集合对象的时候指定了类型参数。

请注意,第三行中的强制转换已经不见了。

现在,你可能认为我们所实现的是移动了杂乱的部分。替代第三行中的强制转换为Integer,我们在第一行中有Integer作为类型参数。然而,这里面有一个很大的区别,编译器可以在编译时校验类型的正确性。当我们将myIntList声明为List,它告诉我们了一些关于变量myIntList,无论何时何地使用它,编译器都会保证它正确性。相反,强制转换只告诉了我们,程序员只在部分代码中认为它是正确的。

整体影响,尤其是在大型程序中,它提高了可读性和鲁棒性。

简单的泛型

以下是摘自java.util包中List和Iterator接口的定义片段:

public interface List<E>{
void add(E x);
Iterator<E> iterator();
}

public interface Iterator<E>{
E next();
boolean hasNext();
}


这段代码应该都比较熟悉,除了尖括号中的东西。这些是List和Iterator接口的正式类型参数的声明。

类型参数可以再整个泛型声明中使用,几乎任何使用传统类型的地方(尽管存在一些重要的约束;查阅要点 部分。在简介部分,我们看到声明为泛型类型List的调用,如List< Integer>,在调用中(通常称为参数化类型),所有形式类型参数(这个例子中的E)都被实际类型(这个例子中的Integer)替代)。

你可能想象List< Integer>代表了List的一个版本,其中E全部都被Integer替代:

public interface IntegerList{
void add(Integer x);
Iterator<Integer> iterator();
}


这种直觉是有意义的,但是它也是一种误导。

有意义是因为参数化类型List< Integer> 确实有方法,这些方法和扩展的类似。

误导,是因为泛型声明不是使用这种方式来扩展。没有代码的拷贝,包括源码、字节码、磁盘和内存。如果你是一个c++编程者,你就会明白这和c++中的模板有很大不同点。

泛型类型声明一次编译并且使用所有,并生成单一一个文件,和普通类或者接口一样。

类型参数和普通方法或者构造函数中使用的参数类似。很像一个方法有形式值参数,这些参数描述方法对它操作的多种值。一个泛型声明有形式类型参数。当一个方法被调用,实际参数替代形式参数,并且方法体被执行,当一个泛型声明被调用,实际类型参数替代形式类型参数。

一个命名约定的注释。我们建议你们使用简介但有意义的名字来命名形式类型参数,最好避免使用小写字符,使它容易和普通的类或者接口区分开来。很多容器类型使用E,作为元素,如上面的例子中使用。我们将会后续的例子中看到其他的约定。

泛型和子类型化

现在测试一下你对泛型的理解。下面代码片段是否合法?

List<String> ls = new ArrayList<String>();//1
List<Object> lo = ls;


第一行肯定是合法的,这个问题中比较棘手的是第二行,这可以归结为问题:String类型的List集合是Object类型的List集合吗,大部分人直觉的答案是“当然了”。

好吧,来看看下面的几行:

lo.add(new Object());//3
String s = ls.get(0);//4


这里,我们有别名为ls和lo。当访问ls,String类型的集合,使用lo,我们可以将抽象类对象插入。ls将不能再继续保留String,当我们试图从中获取时,我们将会获得意料之外的。

当然,Java编译器将会阻止这样发生,编译时,第二行将会报错。

通常情况,如果Foo是Bar子类型(子类或者子接口),G是某种泛型类型,那么G< Foo>不是G< Bar>的子类型。这也许是你学习泛型的最难部分,因为它和我们根深蒂固的思维相违背了。

我们不应该假设集合不会改变,我们的直觉可能引导我们认为这事情是不可变的。

例如,如果车辆管理所部门向人口普查局提供一份司机名单,这看起来似乎是有道理的。我们认为,List< Driver>是一种 List< Person>,假设Driver是Person的一个子类型。事实上,传递过去的只是注册司机的一份拷贝。然而,人口普查调查局可以将那些不是司机的人员添加到列表中,破坏DMV’s记录。

为了处理这种情况,考虑更灵活的泛型是有用的,到目前为止我们所看到的规则是相当严格的。

通配符

思考写一个程序,将一个集合中的元素输出存在的问题。下面是一个你可能写出来的例子,jdk5.0之前版本:

void printCollection(Collection c){
Iterator i = c.iterator();
for(k=0;k<c.size();k++){
System.out.println(i.next());
}
}


然而,这里有一个天真的写法,使用泛型:

void printCollection(Collection<Object> c){
for(Object e:c){
System.out.println(e);
}
}


这里的问题是新版本比老版本更加没用。老版本的代码可以被任何类型参数的集合调用,而新版本的代码只能使用Collection< Object>,这个我们刚刚演示的,不是所有集合的超级类型!

什么是所有类型集合的超级类型呢?它可以写为Collection

void printCollection(Collection<?> c){
for(Object e : c){
System.out.println(e);
}
}


现在,我们叫它为任何类型的集合。注意printCollection(),我们仍然能够从集合c读取元素并将它们赋给Object类型。这样总是安全的,由于不管集合的实际类型,它总能够存放对象。然而,存放抽象对象是不安全的:

Collection<?> c = new ArrayList<String>();
c.add(new Object());// compile time error


由于我们不知道集合c代表什么样的类型,我们不能给它添加对象。方法add()需要参数类型为E,即元素的类型。当实际类型参数是?,它代表一些未知类型。任何我们传递给add的参数必须是未知类型的子类型。由于我们不知道它是什么类型,我们不能传递任何东西。唯一的异常就是null,这是任何类型的成员。

另外,给出一个List

public abstract class Shape{
public abstract void draw(Canvas c);
}

public class Circle extends Shape{
private int x,y,radius;
public void draw(Canvas c){
...
}
}

public class Rectangle extends Shape{
private int x,y,width,height;
public void draw(Canvas c){
...
}
}

pubic class Canvas{
public void draw(Shape s){
s.draw(this);
}
}


任何画图操作都会包含许多形状,假设它们用一个list集合表示,那将会很方便的使用Canvas中的方法来实现它们:

public void drawAll(List<Shape> shapes){
for(Shape s : shapes){
s.draw(this);
}
}


现在,类型规则要求drawAll()只能在类型为Shape的集合中才能被调用,比如,它不能再List< Circle>集合中被调用。麻烦的是,由于所有的方法从集合中获取shapes,所以它仅能在List< Circle>中被调用。我们真正需要的是可以接收任何形状的参数的方法:

public void drawAll(List<? extends Shape> shapes){
...
}


这是一个非常小但是非常重要的不同点,我们使用List< ? extends Shape>替代List< Shape>。现在,drawAll()将会接收任何Shape的子类,所以如果我们有需要,我们可以在List< Circle>中调用它。

List

public void addRectangle(List<? extends Shape> shapes){
shapes.add(0,new Rectangle());
}


&nsbp;&nsbp;&nsbp;&nsbp; 你应该能够明白为什么以上代码是不被允许的。shapes.add()中第二个参数类型是? extends Shape,也就是Shape的一个未知子类型。由于我们不知道它是什么类型,我们不知道它是否是Rectangle的一个超级类型;它可能不是这种类型,所以传递一个Rectangle是不安全的。

&nsbp;&nsbp;&nsbp;&nsbp; 有限通配符是我们需要处理之前例子的一种方式,它传递数据给人口普查局。我们的示例假设数据由从名称(表示为字符串)映射到人(由引用类型,例如Person或其子类型,例如驱动)来表示。Map < K,v>是一个具有两种类型参数的泛型,表示映射的键和值。

待续。。。

泛型方法

交互遗留代码

要点

类文字作为运行时类型标记

更加有趣的通配符

将遗留代码改为使用泛型

鸣谢
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息