Java泛型 1. 引言 2. 泛型类和接口 3. 类型通配符 4. 泛型方法 5. Java的“菱形”语法与泛型构造器 6. 泛型与数组 7. 擦除和转换  

JDK1.5增加泛型支持很大程度上都是为了让集合能记住其元素的数据类型。在没有泛型之前,一旦把一个对象放入Java集合中,集合就会忘记对象的类型,把所有的对象当成Object类型处理。
当程序从集合中取出对象后,就需要进行强制类型转换,这种强制类型转换不仅使代码臃肿,而且容易引起ClassCastException异常。
增加了泛型支持后的集合,完全可以记住集合中元素的类型,B并可以在编译时检查集合中元素的类型,如果试图想集合中添加不满足类型要求的对象,编译器就会提示错误。
Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。

从JDK1.5以后,Java引入了“参数化类型(parameterized type)”的概念,允许程序在创建集合时指定集合元素的类型,Java的参数化类型被称为泛型(Generic)。
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。

2. 泛型类和接口

2.1 定义泛型接口和类

包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。
例如,Apple<T>类定义构造器,其构造器名依然是Apple,而不是Apple<T>,调用该构造器时却可以使用Apple<T>的形式,当然应该为T形参传入实际的类型参数。

public interface Generic<E> {

    void add(E e);
    Iterator<E> iterator();
    E next();
}

interface MyMap<K, V> {
    Set<K> keySet();
    V put(K k, V v);
}

2.2 从泛型类派生子类 

当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些接口、父类时不能再包含类型形参。

interface ListString<T> extends List<T> {
    @Override
    boolean add(T x);
}

interface ListInteger extends List<Integer> {

}

2.3 并不存在泛型类

不管泛型的实际类型参数是什么,他们在运行时总有相同的类(class),在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或静态变量的声明和初始化中不允许使用类型形参。  

3. 类型通配符

List<String>类并不是List<Object>类的子类。
Java泛型设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。

3.1 使用类型通配符

为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?)将一个问号作为类型实参传给List集合,写作:List<?>,意思是元素类型未知的List。
这个问号被称为通配符,它的元素类型可以匹配任何类型。
但这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中,比如

List<?> list = new ArrayList<Object>();
list.add(new Object());

会报编译错误:The method add(capture#1-of ?) in the type List<capture#1-of ?> is not applicable for the arguments (Object)
因为程序无法确定list集合中元素的类型,所以不能向其中添加对象。
根据其咱们List<E>接口定义的diamante可以发现:add方法有类型参数E作为集合的元素类型,所以传给add的参数必须是E类的对象或其子类的对象。
但因为在该例中不知道E是什么类型,所以程序无法将任何对象放入该集合。唯一的例外是null,它是所有引用类型的实例。
另一方面,程序可以调用get方法来返回List<?>集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是,它总是一个Object。
因此,把get的返回值赋值给一个Object类型的变量,或者放在任何希望是Object类型的地方都可以。

3.2 设定类型通配符的上限

当直接使用List<?>这种形式时,即表明这个List集合可以使任何泛型List的父类。但还有一种特殊的情形,程序不希望这个List<?>是任何泛型List的父类,只希望它代表某一类泛型List的父类。
List<? extends A>标示所有A泛型List的父类--只要List尖括号里的类型是A的子类型即可,把A称为这个通配符的上限。
类似,由于程序无法确定这个受限制通配符的具体类型,所以不能把A对象或其子类的对象加入这个泛型集合中。

3.3 设定类型通配符下限

Java允许设定通配符的下限:<? super Type>, 这个通配符标示它必须是Type本身,或时Type的父类。
实现将src集合里面的元素复制到dest集合里面,然后返回最后一个被复制的元素。

public class GenericSuper {
    public static void main(String[] args) {
        List<Number> list1 = new ArrayList<>();
        List<Integer> list2 = new ArrayList<>();
        // copy实际返回的T类型为Number类型不对
        // Integer i = copy(list1, list2);
        Integer in = copy1(list1, list2);
    }

    public static <T> T copy(Collection<T> dest, Collection<? extends T> src) {
        T last = null;
        for (T ele : src) {
            last = ele;
            dest.add(ele);
        }
        return last;
    }

    public static <T> T copy1(Collection<? super T> dest, Collection<T> src) {
        T last = null;
        for (T ele : src) {
            last = ele;
            dest.add(ele);
        }
        return last;
    }
}

  

3.4 设定类型形参的上限

Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是上限类型,要么是该上限类型的子类。

class A {

}
class Human<T extends A> { // 设置上限
    private Human<A> h = new Human<>();
}

定义了一个Human泛型类,该Human类型的类型形参的上限是A类,这表明使用Human类时为T形参传入的实际类型参数只能是A或A类的子类。
在一种极端情况下,需要为类型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该类型形参必须是其父类的子类,并且上限多个上限接口。

class A {

}

interface B {

}

class AB extends A implements B {

}

class Human<T extends A & B> { // 设置上限
    private Human<AB> h = new Human<>();
}

与类同时继承父类,实现接口类似的是,为类型形参指定多个上限时,所有的接口上限必须位于类上限之后。
也就是说,如果需要为类型形参指定类上限,类上限必须位于第一位。

4. 泛型方法

在定义类、接口时可以使用类型形参,在该类的方法定义和成员变量定义、接口的方法定义中,这些类型形参可被当成普通类型来用。在另外一些情况下,定义类、接口时没有使用类型形参,但定义方法时想自己定义而理性形参,这也是可以的,Java5提供了堆泛型方法的支持。
泛型方法的定义格式如下:
修饰符 <T, S> 返回值类型 方法名(形参列表) {

}
泛型方法方法的方法定义比普通方法的定义多了类型形参声明,类型形参声明以尖括号括起来,多个类型形参之间以逗号分隔,所有的类型形参声明放在方法修饰符和方法返回值类型之间。

class Haha {
    public <T> void smile(T t) {

    }
}

与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,比如Human里面的S,而接口、类声明中定义的类型形参则可以在整个接口、类中使用,比如Human里面的T。方法中的泛型参数无须显式传入实际类型参数。

class Human<T extends A & B> { // 设置上限
    private Human<AB> h = new Human<>();

    public <S> boolean eat(S s) {
        return true;
    }

    public <S> void run(T t, S s) {

    }
}

4.1 泛型方法和类型通配符的区别  

大多数是都可以使用泛型方法来代替类型通配符。
采用类型通配符:

interface Collction1<E> {
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
}

采用泛型方法:

interface Collction2<E> {
    <T> boolean containsAll(Collection<T> c);
    <T extends E> boolean addAll(Collection<T> c);
}

上面方法使用<T extends E>泛型形式,这时定义类型形参时设定上限(其中E是接口定义的类型形参,在接口里E可当成普通类型使用)
上面两个方法中类型形参T只使用了一次,类型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。
泛型方法允许类型形参被用来标示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

如果有需要,也可以同时使用泛型方法和通配符。

interface Collections1 {
    public <T> void copy(List<T> dest, List<? extends T> src);
    public <T, S extends T> void copy1(List<T> dest, List<S> src);
}

5. Java的“菱形”语法与泛型构造器

Java也允许在构造器中声明类型形参,这就是所谓的泛型构造器。
一旦定义了泛型构造器,可以让Java根据数据参数的类型来推断类型形参的类型,也可以显式为构造器中的类型形参指定实际的类型。

public class GenericConstructor {
    public static void main(String[] args) {
        new Foo("sdf");
        new Foo(12);
        new  <String>Foo("ddd");
    }
}

class Foo {
    public <T> Foo(T t) {
        System.out.println(t);
    }
}

菱形语法允许调用构造器时在构造器后使用一对<>代表泛型信息。但如果程序显式指定了泛型构造器中声明的而理性形参的实际类型,则不可以使用菱形语法。

public class GenericConstructor {
    public static void main(String[] args) {
        new Foo<>("sdf");
        new <Integer>Foo<String>(12);
        // 如果显式指定泛型构造器中声明的T形参是String,此时不能使用菱形语法。
        // new  <String>Foo<>("ddd");
    }
}

class Foo<E> {
    public <T> Foo(T t) {
        System.out.println(t);
    }
}

6. 泛型与数组

Java泛型有一个很重要的设计原则:如果一段代码在编译时没有提出“未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。基于这个原因,所以数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素类型包含类型变量或类型形参的数组。
也就是说,只能声明List<String>[]形式的数组,但不能创建ArrayList<String>[10]这样的数据对象,但可以new ArrayList<?>[10]

7. 擦除和转换  

为了与老的Java老的代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型参数,如果没有为这个泛型指定实际的类型参数,则该类型参数被称为raw type(原始类型),默认是声明该类型参数时指定的第一个上限类型。
当吧一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在<>之间的类型信息都将被扔掉。比如一个List<String>类型被转换为List,则该List对集合元素的类型检查变成了类型参数的上限(即Object)。

public class GenericSweep {
    public static void main(String[] args) {
        Orange<Integer> or = new Orange<>(6);
        // 当把or赋给一个不带泛型信息的or1变量时,编译器就会丢失or对象的泛型信息
        // 因为Orange的类型形参的上限是Number类,所以编译器依然知道or1的getSize返回Number类型,但具体是Number的哪个子类就不清楚了。
        Orange or1 = or;
        Number n1 = or1.getSize();
        // or1只知道size的类型是Number
        //Integer i = or1.getSize();
    }

}

class Orange<T extends Number> {
    T size;

    public Orange(T size) {
        this.size = size;
    }

    public T getSize() {
        return size;
    }
}

从逻辑上,List<String>是List的子类,如果直接把一个List对象赋给一个List<String>对象应该引起编译错误,但实际上不会。
对泛型而言,可以直接把一个List对象赋给一个List<String>对象,编译器仅仅提示“未经检查的转换”。

public class GenericSweep1 {

    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(8);
        li.add(9);
        List list = li;
        List<String> ls = list;
        // 运行时异常
        System.out.println(ls.get(0));

    }
}

定义一个List<Integer>对象,这个List对象保留了集合元素的类型信息。当把这个List对象赋给一个List类型的list后,编译器就会丢失前者的泛型信息,即丢失list集合里元素的类型信息,这是典型的擦除。