Contents

Effective Java 2 Generic

第23条:请不要在新代码中使用原生态类型

原生态类型是在泛型提出之前的类型

每个泛型都定义了一个原生态类型(raw type),即不带任何实际类型参数的泛型名称。如List相对应的原生态类型是List。 如下代码:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
    List<String> s = new ArrayList<String>();
    unsafeadd(s, new Integer(10));
    String str = s.get(0);
}

public static void unsafeadd(List list, Object o){
    list.add(o);
}

运行时会报错:java.lang.ClassCastException

这时由于原生态类型List.add(o)逃避了编译器的检查,它允许插入任何对象,而实际只能插入String。

总之,原生态类型只是为了引入泛型之前的遗留代码进行兼容和互用而提供的。

  • Set<Object>是个参数化类型,表示可以包含任何对象类型的一个集合。
  • Set<?>则是一个通配符类型,表示只能包含某种未知对象类型的一个集合。
  • Set则是个原生类型,它脱离了泛型系统,它是不安全的,前2中是安全的。

第24条:消除非受检警告

用Generic编程的时候,会遇到许多编译器警告。 如果无法消除警告,并且可以证明引起警告的代码是类型安全的,就可以用一个 @SuppressWarnings(“unchecked”)注解来禁止这条警告,同时写明原因。

第25条:列表优先于数组

数组与泛型不同点:

1.数组是协变的(covariant),

解释:如果Sub是Super的子类型,那么Sub[]就是Super[]的子类型。

而泛型是不可变的(invariant),

解释:type1和type2为2个不同类型,则List和List也为2个不同的类型。

1
2
3
4
5
6
7
//Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "hello";//Throws ArrayStoreException

//Won't compile!
List<Object> list = new ArrayList<Long>();//Incompatible types
list.add("hello");

可以看出数组在运行时才发现错误,而列表在编译时就发现了错误。

2.数组是具体化的,

所以会在运行时才知道并检查它们的元素类型约束。

而泛型是通过擦除来实现的。

即在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息泛型擦除补充

Java里是禁止创建泛型数组的。最好的方法是使用集合类型List<E>

1
2
3
4
5
//如下创建时非法的!!
List<String>[] s = new List<String>[1];//Won't compile!

//解决方法使用集合类型List<E>
List<List<String>> d = new ArrayList<List<String>>();//通过compile

第26条:优先考虑泛型

本条目让你自己实现一个简单generic的堆栈。下面这个类是泛型化的主要备选对象。

 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
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static  final int DEFAULT_INITIAL_CAPCITY = 16;

    public Stack(){
        elements = new Object[DEFAULT_INITIAL_CAPCITY];
    }

    public void push(Object e){
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop(){
        if (size == 0) throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    public boolean isEmpty(){
        return size == 0;
    }

    public void ensureCapacity(){
        if (elements.length == size){
            elements = Arrays.copyOf(elements,2 * size + 1);
        }
    }
}

这个Stack类可以插入任何类型的对象,而产生不安全性。我们可以通过泛型来强化编译时的类型。如下:

 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
public class GenericStack<E> {
    private Object[] elements;
    private int size = 0;
    private static  final int DEFAULT_INITIAL_CAPCITY = 16;

    public GenericStack(){
        elements = new Object[DEFAULT_INITIAL_CAPCITY];
    }

    public E push(E e){
        ensureCapacity();
        elements[size++] = e;
        return e;
    }

    public E pop(){
        if (size == 0) throw new EmptyStackException();
        @SuppressWarnings("unchecked")E result = (E)elements[--size];
        elements[size] = null;
        return result;
    }

    public boolean isEmpty(){
        return size == 0;
    }

    public void ensureCapacity(){
        if (elements.length == size){
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
} 

第27条:优先考虑泛型方法

如Arrays里的sort如下:

1
2
3
4
5
6
7
public static <T> void sort(T[] a, int fromIndex, int toIndex,
                            Comparator<? super T> c) {
    if (LegacyMergeSort.userRequested)
        legacyMergeSort(a, fromIndex, toIndex, c);
    else
        TimSort.sort(a, fromIndex, toIndex, c);
}

其中类型参数列表为<T> 返回类型为void

泛型方法特点: 无需明确指定类型参数的值,不像调用泛型构造器的时候是必须指定的。编译通过检查方法参数的类型来计算类型参数的值。

下面的泛型方法,计算出列表中最大的值。其中T必须要实现Comparable接口,进行了类型限制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static <T extends Comparable<T>> T max(List<T> list){
    Iterator<T> i = list.iterator();
    T result = i.next();
    while (i.hasNext()){
        T t = i.next();
        if(t.compareTo(result) > 0)
            result = t;
    }
    return result;
}

总结,泛型方法使用更加安全方便。当你确保新方法可以不用转换就能使用,这通常意味着要将它们泛型化。

第28条:利用有限制通配符来提升API的灵活性

参数化类型是不可变的,如:你可以将任何对象放到List<Object>中,却只能将字符串放到List<String>中。 如下代码:

1
2
3
4
5
6
7
8
9
//26条里的GenericStack<E>里pushAll方法
public void pushAll(Iterable<E> src){
	while (src.hasNext()){
            push(src.next());
        }
}

GenericStack<Number> numberStack = new GenericStack<Number>();
numberStack.pushAll(DataList.iterator());//这里将编译时报错

虽然Integer是Number的一个子类型,但是参数化类型是不可变的,所以这里会报错。

解决办法:通过有限制的通配符类型。修改pushAll的参数类型为E的某个子类型的Iterable接口,如下:

1
2
3
4
5
public void pushAll(Iterator<? extends E> src){
    while (src.hasNext()){
        push(src.next());
    }
}

同理可以使用:Collection<? super E>来实现popAll,如下

1
2
3
4
5
public void popAll(Collection<? super E> dst){
    while (!isEmpty()){
        dst.add(pop());
    }
}

以上表明:为了获得更大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型

口诀:PECS(producer-extends,consumer-super)

如果参数为producer就使用<? extends T>,如果参数为consumer就使用<? super T>

所以27条的max函数可以修改成如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static <T extends Comparable<? super T>> T max(List<? extends T> list){
    Iterator<? extends T> i = list.iterator();
    T result = i.next();
    while (i.hasNext()){
        T t = i.next();
        if(t.compareTo(result) > 0)
            result = t;
    }
    return result;
}

第29条:优先考虑类型安全的异构容器

泛型最常用于集合和单元素容器,它在其中限制容器只能有固定的类型参数。

如:Map<String,Integer> map = new HashMap<String, Integer>();

一旦初始化好了,map的key就只能是String类型,value只能是Integer类型。

但有时,我们想保证key和value的类型相符,并且不确定类型。这时考虑类型安全的异构容器。

如下的DatabaseRow类表示一个数据库行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class DatabaseRow {
    private Map<Class<?>, Object> row = new HashMap<Class<?>, Object>();

    public <T> void putRowData(Class<T> type, T instance){
        if (type == null) throw new NullPointerException();
        row.put(type,instance);
    }

    public <T> T getRowData(Class<T> type){
        return type.cast(row.get(type));//因为得到的是Object类型,所以要动态转换为T类型
    }
}

由上看出:

  1. 类型安全:当你getRowData一个String.class类型的时候,一定会返回一个String类型。
  2. 异构的:不像普通的map,它的所有键都不是同类型的。(防止put相同类型key,把value覆盖了)
  3. putRowData时,value的类型必须和key的类型一致。

总结:

  • 集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上面而不是容器上来避开这一限制。
  • 对于这种类型安全的异构容器,可以用Class对象做为键(称作类型令牌),也可以通过定制的键类型。