Effective java(四)

泛型(四)

一.请不要再代码中使用原生态类型

   声明具有一个或者多个类型参数的类或者接口就是泛型类或者接口。

  每种泛型定义一种参数化的类型,构成格式为:先是类或者接口的名称,然后是用尖括号(<>)吧对应于泛型形式类型参数的实际参数列表括起来,例如:List<String>,表示元素为String 的列表。

   每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称。例如,与List<E>相对应的原生态类型是List,实际上原生态类型List与Java平台没有泛型之前的接口类型完全一样。

   在没有泛型的时候我们定义一个List往List添加对象,不小心吧对象添加错了,在编译和运行的时候不会出现任何提示,直到运行时才会报错,这已经出错很久了,但是有了泛型,就可以利用改进后的类型声明来代替集合中的这种注释,告诉编译器之前的注释中所隐含的信息。例如

List<Stamp> list = new ArrayList<Stamp>();
 list.add(new Coin());

这个时候编译器就会报错,还会准确的告诉你是哪里错了,添加的时候要添加Stamp对象,而这里添加的是Coin这里当然会报错。

   如果不提供类型参数使用集合类型和其他泛型也仍然是合法的,但是不应该这么做。如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。但是呢,既然不应该使用原生态类型,为什么Java的设计者还要允许使用呢?这是为了兼容性,因为泛型还没出现的时候,已经存在大量没有使用泛型的Java代码,为了让这些代码保持合法性,并且能够与使用泛型的新代码互用。

   原生态List与泛型List<Object>有什么区别呢,前者逃避了泛型检查,后者明显告诉编译器,它能够持任类型的对象。可以将List<string>传递给List,但是不能传递给List<Object>的参数泛型有子类化的规则,List<String>是原生态类型List的一个子类,而不是参数化类型List<Object>的子类型。因此如果使用像List这样的原生态类型,就会失掉类型安全,如果使用List<Object>这样的就不会。

   当然在你不确定或者不在乎集合中的元素类型的情况下你也许会使用原生态类型。但是java中嘿提供了一种安全的替代方法,叫做无限制的通配符类型,如果要使用泛型,但不确定或者不关心实际的类型参数就可以使用一个问号代替。例如List<?>无限制通配符类型和原生态类型的之间的区别,通配符类型是安全的,而原生态类型是不安全的。

  由于泛型信息可以在运行时被擦除,因此在参数化类型而非无限通配符类型上使用instanceof操作符是非法的。

   总之使用原生态类型会在运行时导致异常,因此不要砸新代码中使用,原生态类型只是为了引入泛型之前遗留代码进行兼容和互用而提供的,Set<Object>是参数化类型,表示可以包含任何对象类型的集合;Set<?>则表示一个通配符类型,表示只能宝行某种未知对象类型的集合;Set则是个原生态类型,它脱离了泛型系统,前两种是安全的,最后一种不安全。

二.消除非受检警告

   在用泛型编程的时候,会遇到很多编译器警告,非受检警告,非受检方法调用警告,非受检普通数组创建警告,以及非受检转换警告。

   有许多非受检警告很容易消除,编译器还会细致的提醒是哪里出错了。也有些警告难以消除。要尽量消除每一个受检警告,如果消除了所有的受检警告就可以保证代码是类型安全的,在运行的时候就不会出现ClassCastExcepation异常。

   如果无法消除警告,同时可以证明引起警告的代码是类型安全的的,在这种情况下就可以使用注解@SuppressWarnings("unchecked")来禁止这条警告,如果在禁止之前没有先证实代码是类型安全的,在运行法时仍然会抛出ClassCastException,但是如果忽略掉明明知道是安全的非受检警告,那么当新的一条非受检警告出现的时候也不会注意到。新出现的警告就会淹没在所有的的错误警告中。

   @SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类中都可以。应该始终在经尽可能小的范围中使用,通常是个声明变量或者非常简短的方法或者构造器,不要在整个中使用,这样可能会掩盖了重要的警告。

   将@SuppressWarnings注解放在return语句中是非法的,因为它不是一个声明。每当使用一次SuppressWarnings注解的时候都要写一条注释,说明为什么是安全的。这样可以帮助他人理解代码,更重要的是,可以尽量减少他人修改代码的后导致计算不安全的概率

   总之非受检警告很重要不要忽略。每条警告都有可能在运行的时候抛出ClassCastException异常,要尽量消除这种警告,无法消除的警告,同时又足以证明引起警告的代码是类型安全的,就可以在尽可能小的范围中使用@SuppressWarnings(“unchecked”)注解禁止警告,要注意吧禁止该警告的原因记录下来。

三.列表优先于数组

   数组与泛型相比,有两个重要的不同点。数组是协变的,表示如果Sub为Super的子类型,那么数组类型Sub[ ]就是Super[ ]的子类。相反,泛型则是不可变的:对于任意两个不同的类型Type1和Type2,List<Type1> 和 List<Type2> ,List<Type1> 既不是List<Type2>的子类型,也不是List<type2>的超类型。

   第二大区别在于,数组是具体化的。因此数组会在运行时才知道并检查它们的元素类型约束。泛型只是在编译时强化它们的元素类型信息,并在运行时丢弃它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。

   因此数组和泛型不能很好地混合使用。 为什么创建泛型数组是非法的呢?因为它不是类型安全的。

   数组和泛型有着非常不同的类型规则。数组是协变且可具体化的;泛型是不可变且可擦除的。因此,数组提供了运行时的类型安全,反之,对于泛型也一样。一般来说数组和泛型不能很好地混合使用,如果你发现自己将他们混合起来使用,并且得到了比那一警告,第一反应应该是用列表代替数组。

四.优先考虑泛型

简单的堆栈实现:

public class Stack {
    pprivate Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITAL_CAPACITY = 16;
    
    public Stack() {
        elements = new Object[DEFAULT_INITAL_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);
    }
}

强化这个类来利用泛型,将该类泛型化可以提高类型的安全性,方便客户端使用(无需显式强制转换类型)

首先用类型参数替换所有的Object类型:

public class Stack<E> {
    pprivate E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITAL_CAPACITY = 16;
    
    public Stack() {
        elements = new E[DEFAULT_INITAL_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);
    }
}

这里不能创建不可具体化的类型的数组,所以(new E[DEFAULT)INITIAL_CAP]是不允许的)

解决方法:

1.创建一个Object数组,并将它转换成泛型数组类型:

elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];

错误变成一条警告,因为可以保证类型安全,所以可以用SupressWarning注释忽略掉该警告。

2.将elements域的类型从E[]改为Object[]:

E result = (E) elements[--size];

产生一条警告,因为可以保证类型安全,所以所以可以用SupressWarning注释忽略掉该警告。

实际中选择第二种方法较多,因为pop方法经常会被调用,频繁地类型转换会耗费时间。

鼓励优先使用列表而非数组。实际上并不总能在泛型中使用列表。为了提高性能,列表不是Java提供的基本实现,如ArrayList就需要在数组上实现,而某些类,如HashMap为了提高性能,也在数组上实现。

五.优先考虑泛型方法

   编写泛型方法与编写泛型类型相类似:

public static Set union(Set s1, Set s2) {
	Set result = new HashSet(s1);
	result.addAll(s2);
	return result;
}

   编写完这个方法后会有警告,下面来修正这个警告,是方法变成类型安全的,将方法申明变成一个类型参数 ,类型参数的命名惯例与泛型方法以及泛型相同:

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
	Set<E> result = new HashSet<E>(s1);
	result.addAll(s2);
	return result;
}

   union方法的局限性在于,三个集合的类型必须全部相同。利用有限制的通配符类型,可以使这个方法变得更加灵活。泛型方法的一个显著特性是,不需要明确指定类型参数的值,可以利用泛型方法调用所提供的类型推导,使创建参数化类型实例的过程变得更加轻松,在调用函数构造器的时候,要明确传递类型参数的值可能有麻烦,类型参数出现了变量声明的左右两边,显得冗余:

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();  
//这样调用是不是简洁多了

    有时,会需要创建不可变但又适合于许多不同类型的对象。由于泛型是通过擦除实现的,可以给所有必要的类型参数使用单个对象,但是需要编写一个静态工厂方法重复地给每个必要的类型参数分发对象。这种模式最常用于函数对象。假设有一个接口,描述了一个方法,该方法接受和返回某个类型T的值:

public interface UnaryFunction<T> {
	T apply(T arg);
}

     现在假设要提供一个恒等函数,如果在每次需要的时候都重新创建一个这样会很浪费,因为它是无状态的。如果泛型被具体化,那个每个类型都必须持有相应类型的桓等函数,但是在运行时擦除类型信息后,它们并没有什么区别,所以在这种情况下,只需要一个泛型单例就够了。例如:

private static UnaryFunction<Object> INDENTITY_FUNCTION =  
new UnaryFunction<Object> {      
    public Object apply(Object arg) {
         return arg; 
    }  
};    

@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> indentityFunction() {
	return (UnaryFunction<T>)INDENTITY_FUNCTION;
}

   通过某个包含该类型参数本身的表达式来限制类型参数是允许的,这就是递归类型限制。递归类型限制最普遍的用途与Comparable接口有关,它定义类型的自然顺序。许多 方法都带有一个实现Comparable接口的元素列表,为了对列表进行排序,并在其中进行搜索,计算出它的最小值或者最大值等等。要完成这其中的任何一项工作要求列表中的每个元素都能够与列表中的其他元素相比较,下面是如何表达这种约束条件的一个示例:

public static <T extends Comparable<T>> T max(List<T> list) {...}

    总之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更安全,也更加容易。就像类型一样,你应该确保新的方法可以不用转换就能使用,这通常意味着要将它们泛型化。并且就像类型一样,还应该将现有的方法泛型化,使新用户使用起来更加轻松,且不会破坏现有的客户端。

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

   参参数化类型是不可变的。对于任何两个不同的类型Type1与Type2,List<T1>与List<T2>没有父子类关系。

   有时候需要的灵活性要比不可变类型所能提供的更多,考虑下面的的公共API;

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

假设增加一个方法,按顺序将一系列的元素放到堆栈中:

public void pushAll(Iterable<E> src) {
    for(E e : src)
        push(e);
}

 如果尝试这样做,从逻辑上讲是可以的,因为Integer是Number的一个子类型:

Stack<Number> s = new Stack<Number>();
Iterable<Integer> integers = ...;
s.pushAll(integers);

但是实际运行的时候会出现错误信息,如前面所述,参数化类型是不可变的.

   Java提供了一种特殊的参数化类型,称作有限制的通配符类型,来处理这种情况。pushAll的参数应该为 ‘E的某个子类型的Iterable接口’如下:

public void pushAll(Iterable<? extends E> src){
    for(E e : src){
        push(e);
    }
}

   如果要编写一个pushAll() 与popAll() 相呼应,popAll()弹出每个元素,并添加到指定的集合中,通配符同样也可以将解决,popAll得到输入参数为“E的某种超类的集合”:

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

   为了获得最大的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型,如果某个输入参数即是生产者 ,又是消费者,那么通配符类型就没有什么好处:因为我们需要的是严格的类型匹配,不是用任何通配符得到的。

记住基本的原则:producer-extends,consumer-super(PECS),所有的comparable和comparator都是消费者。

七.优先考虑类型安全的异构容器

    泛型最常用于集合,如Set和Map,以及单元素的容器,都是充当被参数化了的容器。这样就限制了每个容器智能有固定的数目的类型参数,这种是我们平常比较普遍用到的也是我们想要的,但是,有时候我们也会需要更多的灵活性。像数据库那样有任意多的列,而且还是安全的。

   将键(key)进行参数化而不是容器参数化。然后将参数化的键提交给容器,来插入或者获取值。用泛型系统来确保值的类型与它的键相符。

public class Favorites {
	private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();
	public <T> void putFavorite(Class<T> type, T instance) {
		if (type == null)
			throw new NullPointerException("Type is null");
		favorites.put(type, instance);
	}
	public <T> T getFavorite(Class<T> type) {
		return type.cast(favorites.get(type));
	}
}
 
public static void main(String[] args) {
	Favorites f = new Favorites();
	f.putFavorite(String.class, "Java");
	f.putFavorite(Integer.class, 0xcafebale);
	f.putFavorite(Class.class, Favorites.class);
	String favoriteString = f.getFavorite(String.class);
	int favoriteInteger = f.getFavorite(Integer.class);
	Class<?> favoriteClass = f.getFavorite(Class.class);
	System.out.printf("%s %x %s%n", favoriteString, favoriteInteger,favoriteClass);
}

Favorites 是安全的,总是按照键返回正确的值,同时也是异构的,因为它不像普通的map,它所有的键都是不同类型,所以Favorites 是类型安全的异构容器。

这是Favorites 的完整实现:

public class Favorites {
    private Map<Class<?> , Object> favorites = new HashMap<Class<?> , Object>();
    public <T> void putFavorite(Class<T> type , T instance) {
        if (type == null) throw new NullPointerException("Type is null");
        favorites.put(type , instance);
    }
    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

Favorites 有两种局限性:一是恶意用户可以通过使用原生态形式的Class来破坏年Favorites实例的类型安全。这种方式可以通知在putFavorite中进行类型检查来确保实例对象进行检查;二是它不能用在不可具体化的类型中。比如说可以存储喜爱的String,String[],但是不能存储List<String>。因为 List<String>.class是语法错误。因为在运行时他们的类型会被擦除,所在List<String>与List<Integer>实际上是共用一个Class。如果需要限制些可以传递给方法的类型,则可以使用有限制的通配符类型。

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值