Effecitive Java 读书笔记 (三)


第四章:泛型


(1)请不要在新代码中使用原生态类型

    声明中具有一个或者多个类型参数的类或者接口,就是泛型类或接口,如List<E>,这其中E表示List集合中元素的类型。在Java中,相对于每个泛型类都有一个原生类与之对应,即不带任何实际类型参数的泛型名称,如List<E>的原生类型List。他们之间最为明显的区别在于List<E>包含的元素必须是E(泛型)类型,如List<String>,那么他的元素一定是String,否则将产生编译错误。和泛型不同的是,原生类型List可以包含任何类型的元素,因此在向集合插入元素时,即使插入了不同类型的元素也不会引起编译期错误。那么在运行,当List的使用从List中取出元素时,将不得不针对类型作出判断,以保证在进行元素类型转换时不会抛出ClassCastException异常由此可以看出,泛型集合List<E>不仅可以在编译期发现该类错误,而且在取出元素时不需要再进行类型判断,从而提高了程序的运行时效率。

     如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。Java设计者只是为了提供兼容性才允许使用原生态类型  

(2)消除非受检警告

     在进行泛型编程时,经常会遇到编译器报出的非受检警告(unchecked cast warnings),如:Set<Lark> exaltation = new HashSet(); 对于这样的警告要尽可能在编译期予以消除。对于一些比较难以消除的非受检警告,可以通过@SuppressWarnings("unchecked")注解来禁止该警告,前提是你已经对该条语句进行了认真地分析,确认运行期的类型转换不会抛出ClassCastException异常。同时要在尽可能小的范围了应用该注解(SuppressWarnings),如果可以应用于变量,就不要应用于函数。尽可能不要将该注解应用于Class,这样极其容易掩盖一些可能引发异常的转换。见如下代码:

<span style="color:#000000;">public <T> T[] toArray(T[] a) {
        if (a.length < size)
            return (T[])Arrays.copyOf(elements,size,a.getClass());
        System.arraycopy(elements,0,a,0,size);
        if (a.length > size)
            a[size] = null;
        return a;
    }</span>
编译该代码片段时,编译器会针对(T[])Arrays.copyOf(elements,size,a.getClass())语句产生一条非受检警告,现在我们需要做的就是添加一个新的变量,并在定义该变量时加入@SuppressWarnings注解,见如下修订代码

public <T> T[] toArray(T[] a) {
        if (a.length < size) {
            //TODO: 加入更多的注释,以便后面的维护者可以非常清楚该转换是安全的。
            @SuppressWarnings("unchecked") T[] result = 
                (T[])Arrays.copyOf(elements,size,a.getClass());
            return result;
        }
        System.arraycopy(elements,0,a,0,size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

(3 )列表优于数组

    数组和泛型相比,有两大不同:

    1.数组是协变的,而泛型则是不可变的 。即如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型,即对于任何两个不同类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。

    2.数组是具体化的,数组会在运行时才知道并检查它们的元素类型约束。泛型则是通过擦除实现的。因此泛型只有在编译时强化它们的类型信息,并在运行时丢弃它们的元素类型信息

    创建泛型数组 如:new List<String> ,new List<E>[] 是非法的,因为它不是类型安全的 。

  

为什么创建泛型数组是非法的呢?

       因为他不是类型安全的。要是他合法,编译器在其他正确的程序中发生的转换就会在运行时失败,并出现一个ClassCastExecption异常。这样就违背了泛型系统提供的基本保证。


为了更具体的对此证明进行说明,考虑一下代码片段:

 

List<String>[] stringLists = new ArrayList<String>[1];//1
List<Integer> integers = Arrays.asList(42);//2
Object[] objects = stringLists;//3
objects[0] = integers;//4
String s = stringLists[0].get(0);//5

       我们假设第一行是合法的,他创建了一个泛型数组,第二行创建并初始化了一个包含单个元素的List<Integer>。第三行将List<String>数组保存到一个Object数组变量中,这是合法的,因为数组是协变的。第四行将List<Integer>保存到Object数组里唯一的元素中,这是可以的,因为泛型是通过擦除实现的:

 

List<Integer>实例的运行时类型是List,List<String>[]实例的运行时类型则是List[],因此这种安排不会产生ArrayStoreExecption异常。但现在我们有麻烦了。我们将一个List<Integer>实例保存到了原本声明只包含List<String>实例的数组中,在第五行,我们从这个数组唯一的列表中获取了唯一的元素。编译器自动的将获取到的元素转换成String,但是是一个Integer,因此,我们在运行时会得到一个ClassStoreExecption异常,为了防止这种情况出现,就在创建泛型数组的时候(第一行)就产生了编译时的错误。


    像E, List<E>和List<String>等这样的类型称作是不可具体化的类型,所谓不可具体化的类型是指在运行时表示法包含的类型信息比它在编译时表示法包含的类型信息更少。唯一可具体化的参数化类型是无限制的通配符类型,如List<?>, Map<?, ?>等,创建无限制通配符类型的数组是合法的。

     JDK泛型有个小局限是:不能创建基本类型的泛型,如List<int>, List<float>就会产生编译时错误,可以通过使用基本类型的包装类来避开这个限制。

      当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是数组类型E[]。这样可能会损失一些性能或简洁性,但是换回的却是更高的类型安全性和互用性。见如下示例代码:  

static Object reduce(List l, Function f, Object initVal) {
        Object[] snapshot = l.toArray();
        Object result = initVal;
        for (Object o : snapshot) {
            return = f.apply(result,o);
        }
        return result;
    }
    interface Function {
        Object apply(Object arg1,Object arg2);
    }
    事实上,从以上函数和接口的定义可以看出,如果他们被定义成泛型函数和泛型接口,将会得到更好的类型安全,同时也没有对他们的功能造成任何影响,见如下修改为泛型的示例代码:

static <E> E reduce(List<E> l,Function<E> f,E initVal) {
        E[] snapshot = l.toArray();
        E result = initVal;
        for (E e : snapshot) {
            result = f.apply(result,e);
        }
        return result;
    }
    interface Function<E> {
        E apply(E arg1,E arg2);
    }
这样的写法回提示一个编译错误,即E[] snapshot = l.toArray();是无法直接转换并赋值的。修改方式也很简单,直接强转就可以了,如E[] snapshot = (E[])l.toArray();在强转之后,仍然会收到编译器给出的一条警告信息,即无法在运行时检查转换的安全性。尽管结果证明这样的修改之后是可以正常运行的,但是这样的写法确实也是不安全的,更好的办法是通过List<E>替换E[],见如下修改后的代码:

static <E> E reduce(List<E> l,Function<E> f,E initVal) {
        E[] snapshot = new ArrayList<E>(l);
        E result = initVal;
        for (E e : snapshot) {
            result = f.apply(result,e);
        }
        return result;
    }

     总而言之,数组和泛型有这不同的的类型规则。数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样,一般来说,数组和泛型不能很好的混合使用,如果你发现自己将他们混合使用,并且得到了编译时错误或者警告,你的第一反应就是应该用List列表代理数组。

 

(4 )优先考虑泛型

看一个简单的堆栈实现的:

public class Stack {
        private Object[] elements;
        private int size = 0;
        private static final int DEFAULT_INITIAL_CAPACITY = 16;
        public Stack() {
            elements = new Object[DEFAULT_INITIAL_CAPACITY];
        }
        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;
        }
        private void ensureCapacity() {
            if (elements.length == size)
                elements = Arrays.copyOf(elements,2 * size + 1);
        }
    }

这个类是泛型化的主要备选对象,换句话说,可以适当的强化这个类来利用泛型。根据实际情况来看,必须转换从堆栈弹出的对象,以及可能运行时失败的那些转换。将类泛型化的第一个步骤是给他们声明一个或者多个类型的参数,在这个示例中有一类型参数,他表示堆栈的元素类型,这个参数的名称通常为 E


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 e) {
            ensureCapacity();
            elements[size++] = e;
        }
        public E pop() {
            if (size == 0)
                throw new EmptyStackException();
            E result = elements[--size];
            elements[size] = null;
            return result;
        }
        public boolean isEmpty() {
            return size == 0;
        }
        private void ensureCapacity() {
            if (elements.length == size)
                elements = Arrays.copyOf(elements,2 * size + 1);
        }
    }

上面用泛型的 代码 有一处会编译不通过: elements = new E[DEFAULT_INITIAL_CAPACITY]  原因是你不能创建不可具体化类型的数组,如E.

解决方法有两种:(1)创建一个Object的数组,并将它转换成泛型数组类型。: elements = (E[ ])new Object[DEFAULT_INITIAL_CAPACITY] 。使用此方法你自己必须确保未受检的转换不会危及到程序的类型安全性 (2)将elements域的类型从E[ ] 改为Object[ ]. 并将

<span style="font-size:18px;"> E result = elements[--size];  改为 E result = (E[ ])elements[--size];</span><span style="font-size:18px;">
这两种方案第一种 更为常用。

泛型类的使用 实例:

</span>

      绝大多数泛型就像Stack示例一样。因为它们的类型参数没有限制,你可以创建任何对象引用类型的Stack,但要不能创建基本类型的Stack,可以通过使用基本包装类型来避开这条限制

      总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来的更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。 只要时间允许,就把现在的类型都泛型化     


(5 )优先考虑泛型方法

     就如类可以从泛型中受益一般,方法也是一样。静态工具方法尤其适合于泛型化。Collections中的所有的“算法”方法,例如(binarySearch和sort)都泛型化了。

     泛型方法使用示例:

public class GenericMethod {
	  public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2) {
		          Set<E> result = new HashSet<E>(s1);
		           result.addAll(s2);
		         return result;
		       }
	public static void main(String[] args) {
		//Set<String> guys = new HashSet<String>(Arrays.asList("TOM","Dick","Harry"));
		 Set<String> guys = new HashSet<String>();
		 guys.addAll(Arrays.asList("TOM","Dick","Harry")) ;
		Set<String> stooge = new HashSet<String>(Arrays.asList("T32","fssgf","gfdhg"));
		Set<String> aflCio = union(guys, stooge);
		System.out.println(aflCio)    	
	}
}
此泛型方法实现了两个集合的合并

      泛型方法的一个显著特性:在调用泛型函数时无须指定函数的参数类型,而是通过Java编译器的类型推导(即编译器通过检查方法参数来计算类型参数的值)来填充该类型信息,

 泛型静态方法工厂方法

      见如下泛型对象的构造:
      Map<String,List<String>> anagrams = new HashMap<String,List<String>>();
      很明显,以上代码在等号的两边都显示的给出了类型参数,并且必须是一致的。为了消除这种重复,可以编写一个泛型静态方法工厂方法,与相应使用的每个构造器相对应 如:

  public static <K,V> HashMap<K,V> newHashMap() {
        return new HashMap<K,V>();
    }

      我们的调用方式也可以改为:Map<String,List<String>> anagrams = newHashMap();   

泛型单例工厂

泛型单例工厂模式用于创建不可变但又适合于许多不同类型的对象,由于泛型是通过类型檫除实现的,因此可以给所有必要的类型参数使用单个对象,例子如下:

public class GenericMethod2 {
	public interface UnaryFunction<T> {
        T apply(T arg);
    }
    private static UnaryFunction<Object> IDENTITY_FUNCTION 
        = new UnaryFunction<Object>() {
            public Object apply(Object arg) {
                return arg;
            }
        };

    public static <T> UnaryFunction<T> identityFunction() {
        return (UnaryFunction<T>)IDENTITY_FUNCTION;
    } <pre name="code" class="java">    public static void main(String[] args) {
        String[] strings = {"jute","hemp","nylon"};
        UnaryFunction<String> sameString = identityFunction();
        for (String s : strings)
            System.out.println(sameString.apply(s));
        
        Number[] numbers = {1,2.0,3L};
        UnaryFunction<Number> sameNumber = identityFunction();
        for (Number n : numbers)
            System.out.println(sameNumber.apply(n));
    }
}
泛型方法类型限制
对于该静态函数,如果我们为类型参数添加更多的限制条件,如参数类型必须是Comparable<T>的实现类,
这样我们的函数对象便可以基于该接口做更多的操作,而不仅仅是像上例中只是简单的返回参数对象。如:

public static <T extends Comparable<T>> T max(List<T> l) {
        Iterator<T> i = l.iterator();
        T result = i.next();
        while (i.hasNext()) {
            T t = i.next();
            if (t.compareTo(result) > 0)
                result = T;
        }
        return result;
    }

(6 )利用有限制通配符来提升API的灵活性

泛型通配符

由于泛型参数化类型是不可变的,对于任何类型的Type1和Type2而言,List<Type1>既不是List<Type2>的子类型,也不是它的超类型,由此会产生可以将任何对象放进List<Object>中,却只能将字符串放在List<String>中的问题,解决此类问题我们需要使用泛型的通配符。

例子如下:

自定义堆栈的API如下:

  1. public class Stack<E> {  
  2.     public Stack();  
  3.     public void push(E e);  
  4.     public E pop();  
  5.     public boolean isEmpty();  
  6.     public void pushAll(Iterable<E>  src){  
  7.     for(E e : src){  
  8.     push(e);  
  9. }  
  10. }  
  11.     public void popAll(Collection<E> dst){  
  12.     while(!isEmpty()){  
  13.     dst.add(pop());  
  14. }  
  15. }  
  16. }  

上述代码编译完全没有问题,但是如果想完美运行还需要使用泛型通配符。

(1).生产者限制通配符extends:

使用如下的测试数据对pushAll方法进行测试:

  1. Stack<Number> numberStack = new Stack<Number>();  
  2. Iterable<Integer> integers = …;  
  3. numberStack.pushAll(integers);  

在运行时pushAll方法会报参数类型不匹配错误,解决这个问题可以使用限制通配符类型,将pushAll方法修改如下:

  1. public void pushAll(Iterable<? extends E>  src){  
  2.     for(E e : src){  
  3.     push(e);  
  4. }  
  5. }  

Iterable<? extends E>的意思是集合元素的类型是自身的子类型,即任何E的子类型,在本例子Integer是Number的子类,因此正好符合此意。

(2).消费者限制通配符super:

使用下面的测试数据对popAll方法进行测试:

  1. Stack<Integer> integerStack = new Stack< Integer>();  
  2. Iterable<Number> numbers = …;  
  3. integerStack.popAll(numbers);  

在运行时popAll方法会报参数类型不匹配错误,解决这个问题可以使用限制通配符类型,将popAll方法修改如下:

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

Collection<? super E>的意思是集合元素的参数类型是自身的超类型,即任何E的超类,在本例中可以将Integer类型的元素添加到其超类Number的集合中。

PECS原则,即producer-extends,consumer-super.如果参数化类型表示一个T生产者,就使用<? extends T>如果它表示一个消费者,就使用<? super T>

(3).无限制通配符?:

对于同时具有生产者和消费者双重身份的对象来说,无限制通配符?更合适,一个交互集合元素的方法声明如下:

  1. public static void swap(List<?> list, int i, list j);  
一般来说,如果类型参数只在方法声明中出现一次,就可以使用通配符取代它,如果是无限制的类型参数,就使用无限制通配符?代替

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值