Effective java 总结4 - 泛型
第26条 请不要使用原生态类型
声明中具有一个 or 多个类型参数的类或接口, 就是泛型类或接口,统称泛型
每个泛型都定义一个原生态类型(泛型名称,eg: List 的原生态类型为 List)
泛型会自动进行编译期类型检查
如果使用原生态类型,就会失去泛型在安全性和描述性方面的优势
在不确定或者不在乎集合中类型时,使用无限制的通配符替代原生态类型 Set<?>
例外:
- 必须在类文字中使用原生态类型:List.class、String[].class, List.class则是非法的
- 参数化类型使用instanceof操作符非法,无限制通配符可以但没必要,直接使用原生态类型:if(o instanceof Set){ … }
Set : 参数化类型,表示可以包含任何对象的一个集合 (安全)
Set<?>: 通配符类型,只能包含某一种类型对象的集合 (安全)
Set:原生态类型,非泛型(不安全)
参数化类型 | List |
---|---|
实际类型参数 | String |
泛型 | List |
形式类型参数 | E |
无限制通配符类型 | List<?> |
原生态类型 | List |
有限制类型参数 | |
递归类型限制 | <T extends Comparable> |
有限制通配符类型 | List< ? extends Number> |
泛型方法 | static List asList(E[] a) |
类型令牌 | String.class |
第27条 消除非受检警告
尽可能消除每一个非受检警告
如果无法消除,且可以证明引起警告的代码是类型安全的可以使用 @SuppressWarnings(“unchecked”) 禁止警告
@SuppressWarnings("unchecked")
T[] result = (T[]) Arrays.copyof(elements, size, a.getclass());
-
尽可能范围小的使用@SuppressWarnings注解
-
永远不要在整个类上使用@SuppressWarnings注解
-
增加注释,写明为啥引起警告的代码是类型安全的
第28条 列表优于数组
数组与泛型
- 数组是协变的(能够隐式、显式转换),泛型是可变的
- 数组是具体化的(运行时强化元素类型),泛型擦除则只在编译时强化类型信息
- 不可以创建泛型数组,因为不是类型安全的
- E、List、List 等为不可以具体化类型(运行时表示法信息比编译时表示法信息少
- 唯一可以具体化的参数化类型为无限制通配符类型,List<?>, Map<?, ?>,因为对于通配符的方式,最后取出数据需要显示的类型转换
public class Chooser{
private final Object[] choiceArray;
public Chooser(Collection choices){
choiceArray = choices.toArray();
}
public Object choose(){
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choriceArray.length)];
}
}
问题:使用这个类需将 choose方法的返回值改为自己想要的类型
修改成泛型
public class Chooser<T>{
private final T[] choiceArray;
public Chooser(Collection<T> choices){
choiceArray = (T[])choices.toArray();
}
// choose 方法一样
}
问题:有非受检警告, 可以用注解禁止
更好的方法使用 列表代替数组
public class Chooser<T>{
private final List<T> choiceList;
public Chooser(Collection<T> choices){
choiceList = new ArrayList<>(choices);
}
public Object choose(){
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size));
}
}
第29条 优先考虑泛型
public class Stack{
private int size = 0;
private Object[] elements;
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(0 == size) throw new EmptyStackError();
Object result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1)
}
}
}
改为泛型
public class Stack<E>{
private int size = 0;
private E[] elements;
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 Object pop(){
if(0 == size) throw new EmptyStackError();
E result = elements[--size];
elements[size] = null;
return result;
}
...
}
问题:不能创建不可具体化类型的数组 elements = new E[DEFAULT_INITIAL_CAPACITY];
解决1
创建Object数组,类型转换为泛型数组类型,并注解(@SuppressWarnings)消除未受检警告
public Stack(){
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
可读性强,更加简洁, 只需要转换一次(创建数组的时候),可能导致堆污染
解决2
将elements域的类型从E[] 改为Object,每次读取数组元素时强转为 E
private Object[] elements;
...
@SuppressWarnings("unchecked")
E result = (E)elements[--size];
每次读取一个元素数组的时候都要转换一次
应用
public static void main(String[] args){
Stack<String> stack = new Stack<>();
for(String arg: args){
stack.push(arg);
}
while (!stack.isEmpty()){
System.out.println(stack.pop().toUpperCase());
}
}
第30条 优先考虑泛型方法
静态工具方法尤其适合泛型化, eg. collections中所有算法方法都泛型化了
public static Set<E> union(Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
//
Set<String> ss = Set.of("1", "sss", "haha");
Set<String> si = Set.of("aaa");
Set<String> uns = Set.union(ss, si);
泛型单例工厂
public interface GenericityIn<T>{
T apply(T args);
}
public class GenericityImp{
// private static GenericityIn<Object> IDENTITY_FUNCTION = (t) -> t;
private static GenericityIn<Object> IDENTITY_FUNCTION = new GenericityIn<Object>(){
public Object apply(Object arg){
return arg;
}
};
// 恒等函数分发器
@SuppressWarnings("unchecked")
public static <T> GenericityIn<T> identityFunction() {
return (GenericityIn<T>)IDENTITY_FUNCTION;
}
public static void main(String[] args){
String[] ss = {"a", "b"};
GenericityIn<String> gs = identityFunction();
for(String s: ss)
sout(gs.apply(s))
GenericityIn<Integer> gi = identityFunction();
...
}
}
递归类型限制
通常与Comparable接口有关
// 针对可以与自身比较的每个类型E: <E extends Comparable<E>>
public static <E extends Comparable<E>> E max(Collection<E> c){
if(c.isEmpty())
throw new Ill.....
E result = null;
for(E e: c){
if(result == null || e.comparaTo(result) > 0){
result = e;
}
}
return result;
}
泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法更加安全,容易。
第31条 利用有限制通配符来提升API灵活性
类型化参数是不变的, 也就是说 List 与List 没有任何上下关系
有限制的通配符类型(确定了子类型后,每个类型即是自身的子类型,即便没有扩展)
public void pushAll(Iterable<? extends E> src){
for(E e: src){
push(e);
}
}
// use
Stack<Number> ns = new Stack<>();
Iterable<Integer> in = ...;
ns.pushAll(in);
public void popAll(Collection<? super E> dst){
while(!dst.isEmpty()){
dst.add(pop());
}
}
// use
Collection<Object> co = ...;
ns.popAll(co);
为了最大限度的灵活性,要在表生产者或者消费者的输入参数上使用通配符类型,但不用通配符类型作返回类型
PECS ( producer-extends, consumer-super), 所有的Comparble和Comparator都是消费者
public static <T extends Comparable<T>> T max(List<T> list)
// 使用通配符类型声明
// 需要用通配符支持那些不直接实现Comparable(or Comparator)而是扩展实现了该接口的类型
public static <T extends Comparable<? super T> T max(List<? extends T> list){}
如果类型参数只在方法声明中出现一次,可以用通配符取代
// 不能将除了null之外的任何值放入 List<?>
public static void swap(List<?> list, int i, int j){
swapHelper(list, i, j);
}
// 利用私有的辅助方法来捕捉通配符类型,辅助方法必须是泛型方法
private static <E> swapHelper(List<E> list, int i, int j){
list.set(i, list.set(j, list.get(i)));
}
第32条 谨慎并用泛型和可变参数
技术露底:调用一个可变参数方法时,会创建一个数组来存放可变参数,数组应该是一个实现细节,是可见的,但可变参数有泛型或参数化类型时,则会产生混乱
当一个参数化类型的变量指向一个不是该类型的对象时,产生堆污染
static void dangerous(List<String>... stringLists){
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
object[0] = intList; // heap pollution
String s = stringLists[0].get(0); // ClassCastException
}
// 将值保存在泛型可变参数数组参数中是不安全的
Q:显示创建可变参数数组非法,但是泛型可变参数声明方法是合法的?
A:泛型可变参数或者参数化的类型方法在实践中很有用,类库方法中是安全的,eg: Arrays.asList(T… a), Collections.addAll(Collecntion<? super T> c, T… elements), EnumSet.of(E first, E… rest) …
注解SafeVarargs
让带泛型varargs参数的方法设计者能自动禁止客户端的警告,通过方法的设计者做出承诺,声明这是类型安全的
允许另一个方法访问一个泛型可变参数数组是不安全的,例外:1. 将数组传给另一个用SafeVarargs的可变参数方法;2. 将数组传给只计算数组内容部分函数的非可变参数方法
eg:
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists){
List<T> result = new ArrayList<>();
for(List<? extends T> list: lists){
result.addAll(list);
}
}
何时使用SafeVarargs?
对于每一个带有泛型可变参数或者参数化类型的方法时,都使用SafeVarargs注解,并且只能用在静态方法,final实例,和私有实例上(java9)。
泛型可变参数方法安全:
- 没有在可变参数数组中保存任何值
- 没有对不信任的代码开放该数组
用list参数代替可变参数
static <T> List<T> flatten(List<List<? extends T>> lists){
List<T> result = new ArrayList<>();
for(List<? extends T> list: lists){
result.addAll(list);
}
return result;
}
// 前提静态工厂方法List.of 已经使用 SafeVarargs 注解
audience = flatten(List.of(friends, romans, countrymen));
优势:编译器可以证明该方法是类型安全的,这样得到的代码只使用泛型,没有用数组!!!
第33条 优先考虑类型安全的异构容器
class 类: 特殊类,可以获取其他类的属性,名称,方法,基类等。每个类都有Class类对象,由jvm创建
class: 泛型class, 其实和普通List差不多,可以传Class,Class…
异构容器:非异构容器Map<k,v>只能指定key、value为一种类型,而异构容器的Key可以是任意类型
类型令牌:一个类的字面被用在方法中,传达编译时和运行时的类型信息, Class 通常被称为类型令牌
安全的异构容器
public class Favorites{
private Map<Class<?> ,object> favorites = new HashMap<>();
// put
public void <T> putFavorites(Class<T> type, T instance){
favorites.put(type, instance);
// favorites.put(type, type.cast(instance)); // 检验instance是否为type的实例
}
// get
public T getFavorites(Class<T> type){
// cast是Java的转换操作符的动态模拟,只检验参数是否为Class对象所表示的类型的实例,是就返回,不是 抛出异常
return type.cast(favorites.get(type));
}
}
Class 不能传入List 之类的对象,不能用在不可具体化的类型中 。
注解API
public <T extends Annotation> T getAnnotation(Class<T> annotaionType){...}
有限制的类型令牌方法
Class<?> -> Class<? extends Annotation>
// 利用asSubclass 读取类型未知的注解
static Annoation getAnnoation(AnnoatatedElement element, String annoationTypeName){
Class<?> antype = null;
try{
antype = Class.forName(annoationTypeName);
}...
return element.getAnnotation(antype.asSubClass(Annoation.class));
}