java泛型综述

本文将讲解泛型的基本用法,内部的实现机制,限制等。希望本文能帮助读者更好的利用泛型解决实际问题。

对泛型的需求是显而易见的。如果没有泛型,我需要为int和string写两个排序函数,虽然代码完全一样。没有泛型,容器中存放的都是Object,调用方法需要进行强制转换,并且需要手动保证容器类型的一致性。

正如大家所知道的,实现泛型有两种基本的方法,泛型class和泛型method。下面这种是泛型method的声明方法,和泛型class略有不同。

class ArrayAlg
{
    public static <T> T getMiddle(T[] a)
    {
        return a[a.length / 2];
    }
}
和泛型class不同的,泛型method的T是编译器自动推算出来的。比如上面的方法,你可以传入

getMiddle(2.1,2,5)

然而上面的用法将会让你会收到一个错误“found:….,required:….”这是因为编译器自动将参数转变成对象,并寻找这些对象公共的父类,而Integer和Double的公共父类之一comparable本身是一个泛型。问题是comparale<Integer>和Comparable<Double>是两个类型,也就是说编译器无法找到一个公共的父类,因此编译器报错。

 

有时我们希望能限制泛型参数的类型,如下所示:

public static <T extends Comparable> Pair<T> minmax(T[] a)

这样做的好处是,只有Comparable的子类才能作为参数传入,同时我们可以调用T的compareTo方法而不需要强制转换。值得注意的是T可以extends多个类型,但是和extends的语法类似,只能有一个class,其他的都是interface。

 

在JVM内部,和C++的泛型不同,java并不会为每个类型生成一份代码,他的策略是使用Object(或者限制类型)替换所有的泛型类型,然后在需要对时候进行强制转换。对于泛型的method,如果T的限制类型有多个,只有第一个保留下来,其他的都被删除。这就是“erasure”=擦除的概念。类型擦除会带来一些问题。比如会干扰多态,解决办法是编译器自动生成bridge方法。在jvm内部,根据返回值和参数类型确定method,因此erasure导致的代码可以编译。

 

使用泛型时存在一些限制:

1. 原始类型不能作为泛型的参数;

2. 运行时类型识别只能作用于raw type,即不带<>的那部分;

3. 不能抛出或者捕获泛型的实例;

4. 参数化类型的数组是不合法的,因为类型擦除后,我们不能将Object[]转化成别的类型的数组。一个替换的办法是使用arraylist<T>;

5.不能实例化类型变量,不过可以传入Class<T>,然后利用反射的newInstance来创建实例;同样的道理,你也不能创建一个泛型的数组,

public static <T extends Comparable> T[] minmax(T[] a) { T[] mm = new T[2]; . . . } // ERROR

标准的做法是:

public class ArrayList<E>
{
private Object[] elements;
@SuppressWarnings("unchecked") public E get(int n) { return (E) elements[n]; }
public void set(int n, E e) { elements[n] = e; } // no cast needed
. . .
}

toarray面临着同样的问题,因为不能新建泛型的数组,他只能使用如下的变形:

Object[] toArray()
T[] toArray(T[] result)
6. 泛型不能用于静态变量。因为静态变量只存在一份,如果两个调用类型不同,将会变成无法实现;

7. 类型擦除后可能导致方法冲突;

 

泛型的通配符规则有两种:extends和super

Pair<? extends Employee>

假设Manager是Employee的子类。

public static void printBuddies(Pair<Employee> p)
{
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " + second.getName() + " are buddies.";
}
对于上面的方法,你不能传入Pair<Manager>因为他们没有继承关系。但是如果将其声明成:

public static void printBuddies(Pair<? extends Employee> p),则可以传入。

咋一看Pair<? extends Employee>并没有奇特之处,但是它和Pair<T extends Employee>却有着关键的区别,区别就在于前者能区分“读”和“写”的方法。当需要将 ? extends Employee作为写的参数时,编译器因为无法确定是employee的哪一个子类,将会拒绝执行,从而保证了类型安全。

 

有时候我们希望将数据结果写入一个比较宽泛的泛型中。比如希望把stack中的所有元素pop到一个list中。这时候我们如果传入list<Object>是不能工作的,但是我们可以传入List<? super Object>则可以。一个原则是如果是要生成一个集合,则用extends,如果是要读取一个集合则用super。

 

有时候需要结合“?”和T两种。原因很简单,因为?不能声明变量,但是T可以。这是常用的做法是声明另外一个方法,接受T为参数。比如:

public static void swap(Pair<?> p)
下面的实现是不行的:
? t = p.getFirst(); // ERROR
p.setFirst(p.getSecond());
p.setSecond(t);
需要另外声明一个方法:

public static <T> void swapHelper(Pair<T> p)
{
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}

这样就可以结合两种泛型的技术。

 

最后总结下,本文描述了基本的泛型,泛型的内部机制,泛型的实例化,泛型的继承,通配符,以及通配符和泛型变量的结合。

posted @ 2013-02-11 22:39  永远是学生  阅读(1542)  评论(0编辑  收藏  举报