Effective java笔记(四),泛型

泛型为集合提供了编译时类型检查。

23、不要在代码中使用原生态类型

声明中具有一个或多个类型参数的类或接口统称为泛型。List<E>是一个参数化类,表示元素类型为E的列表。为了提供兼容性,每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称。例如,List<E>的原生态类型为List

使用原生态类型将逃避编译时的类型检查,失掉泛型在安全性和表述性方面的优势。出错时(运行时错误)代码所处的位置与包含错误的代码可能相距很远,难以调试。不应该在代码中使用原生态类型。

原生态类型List与参数化类型List的区别:

  • 前者逃避了泛型检查,后者明确告知编译器,它能够持有任意类型的对象

  • List<String>是原生态类型List的子类型,但不是List<Object>的子类型(泛型不是协变的)。可将List<String>传递给类型List的参数,但不能传给类型List<Object>的参数。

如果要使用泛型,但不确定或者不关心实际的类型参数,可使用无限制的通配符类型(<?>)。

注意:不能将任何元素(除null外)放进Collection<?>中,而且无法猜测能从中得到哪种类型的对象。若无法接受这些限制可使用泛型方法或有限制的通配符类型。

不要在程序中使用原生态类型例外情况:

  • 在类文字中必须使用原生态类型。List.class, String[].class合法,但List<String>.class,List<?>.class不合法。

  • 在参数化类型上使用instanceof操作符是非法的(无限制通配符类型除外,其是可具体化的类型),因为泛型信息在运行时会被擦除。

例如:

if(o instanceof Set) { 
	Set<?> m = (Set<?>) o; //必须转换成通配符类型Set<?>,而不是原生态类型Set
	...
}

原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供。Set是参数化类型,表示可以包含任何对象类型的一个集合。Set<?>是一个通配符类型,表示只能包含某种未知对象类型的一个集合。

24、消除非受检警告

泛型编程时,要尽可能的消除每一个非受检警告。消除所有警告可以保证代码的类型安全,避免出现ClassCastException异常。

如果无法消除警告,同时可以证明引起警告的代码是类型安全的。可以使用@SuppressWarnings("unchecked")注解来禁止这条警告。SuppressWarnings注解可以用在任何粒度的级别中,应该在尽可能小的范围中使用SuppressWarnings注解,永远不要在一个类上使用SuppressWarnings注解。将一个SuppressWarnings注解放在return语句中是非法的,因为它不是一个声明,应该声明一个局部变量来保存返回值,并注解其声明。

例如:ArrayList类的toArray方法

public <T> T[] toArray(T[] a) {
	return (T[]) Arrays.copyOf(elements, size, a.getClass());
}

//改为
public <T> T[] toArray(T[] a) {
	@SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
	return result;
}

当使用@SuppressWarnings("unchecked")注解时,使用注释把禁止该警告的原因记录下来。

25、列表优先于数组

数组与泛型的区别:

  • 数组是协变的(covariant,表示如果Sub为Super的子类型,则Sub[]就是Super[]的子类型);而泛型是不可变的(因为List不是List的子类型)。所以说数组是有缺陷的。
  • 数组是具体化的,运行时检查它们的元素类型约束。而泛型只在编译时强化它们的类型信息,运行时丢弃这些信息。擦除使泛型可以与没有使用泛型的代码随意进行互用。
  • Object[] obj = new Long[1]; //正确
    obj[0] = "I don't fit in"; //出错
    
    List<Object> list = new ArrayList<Long>(); //出错
    

    基于上面的原因,数组和泛型不能很好的混用,不能创建泛型数组。例如,new List<E>[], new List<String>[], new E[]都是非法的。E、List<E>、List<String>被称作不可具体化的类型,具体的说,不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。唯一可具体化的参数类型为无限制的通配符类型类型,如List<?>Map<?,?>。所以创建无限制通配符类型的数组是合法的。

    • 泛型不能返回它的元素类型数组。

    • 可变参数方法与泛型结合使用会产生警告,因为每当调用可变参数方法时,都会创建一个数组存放参数。除了使用@SuppressWarnings("unchecked")注解把它们禁止,并避免在API中混合使用泛型和可变参数外,别无它法。

    代码E[] ele = (E[]) new Object[10];无法再运行时检查转换的安全性,因为元素类型会在运行时从泛型中被擦除。不可具体化的类型的数组转换只能在特殊情况下使用。

    数组是协变且可以具体化的;泛型是不可变的且可以被擦除。数组提供了运行时的类型安全,而泛型提供了编译时的类型检查。但数组和泛型不能很好地混用,列表应该优先于数组使用。

    26、优先考虑泛型

    	public class Stack<E> {
    		private E[] elements;
    		private int size = 0;
    		private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    		public Stack() {
    			elements = new E[DEFAULT_INITIAL_CAPACITY]; //产生错误,不能创建泛型数组
    		}
    
    		public void push(E o) {
    			ensureCapacity();
    			elements[size++] = o;
    		}
    
    		public E pop() {
    			if(size == 0) 
    				throw new EmptyStackException();
    			E result = elements[--size];
    			elements[size] = null; //【避免内存泄漏】
    			return result;
    		}
    
    		private void ensureCapacity() {
    			if(elements.length == size) {
    				elements = Arrays.copyOf(sx, 2*size + 1);
    			}
    		}
    	}
    

    如上所示,不能创建不可具体化的类型的数组,解决这个问题的办法:

    方法一:创建一个Object的数组,并将它转换成泛型数组类型,证明这个转化是否安全,若安全,使用注解消除警告并给出注释。

    //The elements array will contain only E instances from push(E)
    //This is sufficient to ensure type safety, but the runtime type of the array won't be E[];
    //it will always be object[]!
    @SuppressWarnings("unchecked")
    public Stack() {
    	elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];
    }
    

    方法二:将elements域的类型改为Object[], 并为所用使用Object[]数组中元素的地方进行强转

    private Object[] elements;
    ...
    public Stack() {
    	elements = new Object[DEFAULT_INITIAL_CAPACITY]; //产生错误,不能创建泛型数组
    }
    public E pop() {
    	if(size == 0) 
    		throw new EmptyStackException();
    	E result = (E) elements[--size];
    	elements[size] = null; //【避免内存泄漏】
    	return result;
    }
    ...
    

    第25条鼓励优先使用列表(List),但有时为了性能或程序的兼容性不得不使用数组。如,HashMap。这时应该特别注意这个问题。

    注意:基本数据类型不能用于泛型,这是java泛型系统的局限性。可以使用基本包装类型代替

    27、优先考虑泛型方法

    静态工具方法尤其适合于泛型化。声明类型参数的类型参数列表,处在方法的修饰符(public、static、final等)及其返回类型之间。

    泛型方法的一个显著特点是,无需明确指定类型参数的值,调用泛型构造器时必须指定(jdk1.8后不需要),编译器可通过返回类型和传入的参数类型进行类型推导。

    编写一个恒等函数,若每次需要时都重新创建一个,这会很浪费,因为它是无状态的。如果泛型被具体化了,每个类型都需要一个恒等函数,但它们被擦除以后,就只需要一个泛型单例。例如:

    public iterface UnaryFunction<T> {
        T apply(T arg);
    }
    
    private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() {
        pubic Object apply(Object arg){ return arg;}
    }
    
    @SuppressWarnings("unchecked")
    public static <T> UnaryFunction<T> identityFunction(){
        return (UnaryFunction<T>) IDENTITY_FUNCTION;
    }
    
    public static void main(String[] args) {
        String[] strings = {"aaa", "bbb", "ccc"};
        UnaryFunction<String> sameString = identityFunction();
        for(String s : strings) {
            System.out.println(sameString.apply(s));
        }
    }
    

    通过某个包含该类型参数本身的表达式来限制类型参数,被称为递归类型限制。如,<T extends Comparable<T>>表示针对可以与自身进行比较的每个类型T

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

    参数化类型是不可变的,对于两个截然不同的类型Type1和Type2,List既不是List的子类型,也不是它的超类型。

    有时我们需要的灵活性要比不可变类型所能提供的更多。如:

    //泛型类Stack中的方法
    public void pushAll(Iterable<E> src) {
    	for(E e: src) 
    		push(e);
    }
    
    //将Integer类型放入Number类型栈中
    Stack<Number> numberStack = new Stack<Number>();
    Iterable<Integer> integers = ...
    numberStack.pushAll(integers); //报错,pushAll(Iterable<Number>) cannot be applied to (Iterable<Integer>)
    

    可以通过有限制的通配符类型来解决这个问题,pushAll的输入参数应该为“E的某个子类型的Iterable接口”;类似的方法popAll(从栈中弹出每个元素,并将这些元素添加到指定的集合中)的输入参数应该为“E的某个超类的集合”。

    public void pushAll(Iterable<? extends E> src) {
    	for(E e: src) 
    		push(e);
    }
    
    public void popAll(Collection<? super E> dst) {
    	while(!isEmpty) {
    		dst.add(pop());
    	}
    }
    

    结论
    为了获得最大限度的灵活性,要在表示生产者或消费者的输入参数上使用通配符类型。若输入参数既是生产者又是消费者,使用严格的类型匹配,不要使用通配符。PECS表示producer-extends, consumer-super,若T是生产者使用<? extends T>,若T是消费者使用<? super T>

    注意:不要用通配符类型作为返回类型

    若编译器不能推断你所希望它拥有的类型,可以使用一个显式的类型参数。例如:

    import java.util.*;
    public class Test {
    	public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
    		Set<E> result = new HashSet<>(s1);
    		result.addAll(s2);
    		return result;
    	}
    
    	public static void main(String[] args) {
    		Set<Integer> integers = new HashSet<>();
    		integers.add(1);integers.add(2);
    
    		Set<Double> doubles = new HashSet<>();
    		doubles.add(3.0);doubles.add(4.0);
    
    		Set<Number> numbers = Test.<Number>union(integers, doubles); //显示的类型推断
    		Set<Number> numbers = Test.union(integers, doubles);//jdk1.8后不报错,之前报错
    	}
    }
    

    Comparable,Comparator始终是消费者,所以Comparable<? super T>优先于Comparable<T>,Comparator一样
    T extends Comparable<? super T>表示T类型不仅能和T类型作比较,还能和T类型的父类型作比较。例如:

    public static <T extends Comparable<? super T> T max(List<? extends T> list) {
    	Iterator<? extends T> i = list.iterator(); //iterator()方法返回T的某个子类型的iterator
    	T result = i.next();
    	while(i.hasNext()) {
    		T t = i.next();
    		if(t.compareTo(result) > 0) {
    			result = t;
    		}
    	}
    	return result;
    }
    

    如果类型参数只在方法声明中出现一次,就可以使用通配符代替它。有限制的类型参数使用有限制的通配符取代它,无限制的类型参数使用无限制的通配符类型取代它。例如,

    public static <E> void swap(List<E> list, int i, int j);
    public static void swap(List<?> list, int i, int j); //无限制通配符
    

    第二种声明用于swap方法会有一个问题,下面这个实现不能编译:

    public static void swap(List<?> list, int i, int j) {
    	list.set(i, list.set(j, list.get(i)));//编译报错
    }
    

    不能将元素放回到刚刚从中取出的列表中,问题在于list的类型为List<?>,你不能把null之外的任何值放到List<?>中。解决办法是编写一个私有的辅助方法来捕捉通配符类型。如:

    public static void swap(List<?> list, int i, int j) {
    	swapHelper(list, i, j);
    }
    
    private static <E> void swapHelper(List<E> list, int i, int j){
    	list.set(i, list.set(j, list.get(i)));
    }
    

    通过swapHelper,我们可以导出swap这个比较好的基于通配符的声明到外部API接口

    29、优先考虑类型安全的异构容器

    泛型常用于容器(集合以及单元素的容器),每个容器只能有固定数目的类型参数,可以通过将类型参数放在键上而不是容器上来避开这一限制。当一个类的字面文字被用在方法中,来传达编译时和运行时的类型信息时,就被称作 type token

    例如:

    import java.util.*;
    public class Favorites {
    	private Map<Class<?>, Object> favorites = new HashMap<>();
    
    	public <T> void putFavorite(Class<T> type, T instance){
    		if(type == null) {
    			throw new NullPointerException("Type is null");
    		}
    		favorites.put(type, type.cast(instance)); //用Class的cast方法,防止使用原生态形式的Class对象
    	}
    
    	public <T> T getFavorite(Class<T> type){
    		return type.cast(favorites.get(type)); //Class的cast方法
    	}
    
    	public static void main(String[] args) {
    		Favorites f = new Favorites();
    		f.putFavorite(String.class, "java");
    		f.putFavorite(Integer.class, 0xcafebabe);
    		f.putFavorite(Class.class, Favorites.class);
    		String favoriteString = f.getFavorite(String.class);
    		int favoriteInteger = f.getFavorite(Integer.class);
    
    		System.out.printf("%s %x", favoriteString, favoriteInteger);
    	}
    }
    

    上面代码中,Favorites实例是类型安全的,同时也是异构的(它的所有键都是不同类型),因此Favorites被称作类型安全的异构容器。Class的cast方法:将对象引用动态地转换成Class对象所表示的类型,cast方法是Java的cast操作符的动态模拟,它检验它的参数是否为Class对象所表示的类型的实例。如果是,就返回参数;否则抛出ClassCastException异常。

    注意:由于无限制通配符类型的关系,你可能认为将不能把任何东西放进这个Map中,但事实正好相反。Map<Class<?>, Object>中通配符类型是嵌套的,这里通配符类型指的不是Map而是键的类型。

    Favorites类的局限性:不能用在不可具体化的类型中。如,不能保存List<String>

    可以使用有限制的类型令牌(bounded type token)来限制传给方法的类型。如,

    public <T extends Annotation> T getAnnotation(Class<T> annotationType)
    

    annotationType表示注解类型的有限制的类型令牌,如果元素有这种类型的注解,该方法就将它返回,若没有返回null。
    Class的asSubclass(Class clazz)方法:将class对象转换成其参数表示的类的一个子类,若转换成功,返回它的参数。

posted @ 2016-09-24 18:17  Alent  阅读(752)  评论(0编辑  收藏  举报