范型
Table of Contents
新代码不要再使用原生态类
范型术语
术语 | 示例 | 说明 |
原生态类型 | List | 不带任何类型参数的List |
参数化类型 | List<String> | 元素类型为String的List |
实际类型参数 | String | |
范型 | List<E> | 元素为E类型的List |
无限制通配符类型 | List<?> | 元素为任意类型的List |
有限制通配符类型 | List<? extends Number> | 元素为任意类型但是继承Number类的List |
有限制类型参数 | List<E extends Number> | 元素为某个继承Number类的List |
递归类型限制 | List<T extends Comparable<T>> | 元素为继承Comparable范型的List |
范型方法 | static <E> List<E> asList(E[] a) | 参数:元素为E类型的数组,返回值:元素类型为E的List |
类型令牌 | String.class |
范型的优势
- 安全,提供了编译前检查
- 方便,不用显示类型转换
原生态类型唯一存在的目的:兼容性
List和List<Object>的区别
原生态类型逃避了编译器类型检查
public static void main(String[] args) { List<String> strings = new ArrayList<String>(); // 使用原生态类型,只有在运行时候才会抛错 unsafeAdd(strings, new Integer(42)); String s = strings.get(0); // Compiler-generated cast } private static void unsafeAdd(List list, Object o) { list.add(o); }
哪怕使用List<Object>都会进行编译期类型检查
public static void main(String[] args) { List<String> strings = new ArrayList<String>(); // unsafeAdd(List<Object>,Object) cannot be applied // to (List<String>,Integer) // unsafeAdd(strings, new Integer(42)); unsafeAdd(strings, new Integer(42)); String s = strings.get(0); // Compiler-generated cast } private static void unsafeAdd(List<Object> list, Object o) { list.add(o); }
无限制通配符范型
统计任意两个Set的相同元素
// Use of raw type for unknown element type - don't do this! static int numElementsInCommon(Set s1, Set s2) { int result = 0; for (Object o1 : s1) if (s2.contains(o1)) result++; return result; }
无限制通配符类型表示s1, s2可以存放任意类型的元素
// Unbounded wildcard type - typesafe and flexible static int numElementsInCommon(Set<?> s1, Set<?> s2) { int result = 0; for (Object o1 : s1) if (s2.contains(o1)) result++; return result; }
使用无限制通配符类型是为了安全:无法把任意非null的对象放入Collections<?>中,否则会有编译报错
原生态类型的作用
- 类型令牌:List.class, String[].class, int.class是合法的,但是List<?>.class是非法的
- instance of: 运行期所有的范型标记会被去除,所以对于范型使用instance of是非法的
推荐的做法:
// 为了使用instance of使用原生态类型 if (o instanceof Set) { // 转换成无限制通配符类型 Set<?> m = (Set<?>) o; //... }
消除非受检查警告
- 要尽可能地消除每一个非受检警告,比如非受检强制转化警告、非受检方法调用警告、非受检普通数组创建警告,以及非受检转换警告
- 如果无法消除警告,只有在可以证明引起警告的代码是类型安全的情况下用一个@SuppressWarnings("unchecked")注释来禁止这条警告
- 始终在尽可能小的范围中使用SuppressWarnings注释
- 每当使用SuppressWarnings注释时,都要添加一条注释,说明为什么这么做是安全的
列表优先于数组
数组和列表的区别
- 数组是协变的,范型是不可变的
//这是被允许的 Object[] objectArray = new Long[1]; //运行时候抛出java.lang.ArrayStoreException objectArray[0] = "hello world"; //编译不通过:Type mismatch: cannot convert from LinkedList<Long> to List<Object> List<Object> list = new LinkedList<Long>();
- 数组是具体化的,因此数组会在运行时才知道并检查他们的元素类型约束。泛型是通过擦除来实现的,因此泛型只在编译时强化类型信息,并在运行时擦除元素的类型信息
数组和列表不兼容
// 为什么范型数组是非法的,代码第一行就会编译报错 List<String>[] stringLists = new List<String>[1]; // (1) 如果这是合法的 List<Integer> intList = Arrays.asList(42); // (2) Object[] objects = stringLists; // (3) 数组是协变的,这是合法的 objects[0] = intList; // (4) 这也是合法的 String s = stringLists[0].get(0); // (5) 如果第一行是编译通过,到这里运行时才会报错
范型数组是非法的,比如E[],List<E>[],List<String>[]都是非法的
使用列表代替数组
reduce方法没有范型,而且是线程不安全的
interface Function { Object apply(Object arg1, Object arg2); } // Reduction without generics, and with concurrency flaw! static Object reduce(List list, Function f, Object initVal) { synchronized(list) { Object result = initVal; for (Object o : list) result = f.apply(result, o); return result; } }
下面代码会编译报错:list.toArray()的返回值是一个Object[]
interface Function<T> { T apply(T arg1, T arg2); } static <E> E reduce(List<E> list, Function<E> f, E initVal) { // Naive generic version of reduction - won't compile! E[] snapshot = list.toArray(); // Locks list E result = initVal; for (E e : snapshot) result = f.apply(result, e); return result; }
- 手动转换类型
E[] snapshot = (E[]) list.toArray();//Locks List
- 使用列表代替数组
// List-based generic reduction static <E> E reduce(List<E> list, Function<E> f, E initVal) { List<E> snapshot; synchronized(list) { snapshot = new ArrayList<E>(list); } E result = initVal; for (E e : snapshot) result = f.apply(result, e); return result; }
优先考虑范型类型
无法初始化范型数组,数组在初始化的时候具体类型必须是确定的
// Initial attempt to generify Stack = won’t compile! 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; // Eliminate obsolete reference return result; } ... // no changes in isEmpty or ensureCapacity }
- 初始化数组的时候类型转换
//warning: [unchecked] unchecked cast elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
- 把数组声明为Object[],在取出元素后做类型转换
private Object[] elements; public E pop() { //...; E result = (E) elements[--size]; //... return result; }
第二种做法比较安全,但是更繁琐
优先考虑范型方法
简单的范型方法
// Generic method public static <E> Set<E> union(Set<E> s1, Set<E> s2) { Set<E> result = new HashSet<E>(s1); result.addAll(s2); return result; } // Simple program to exercise generic method public static void main(String[] args) { Set<String> guys = new HashSet<String>( Arrays.asList("Tom", "Dick", "Harry")); Set<String> stooges = new HashSet<String>( Arrays.asList("Larry", "Moe", "Curly")); Set<String> aflCio = union(guys, stooges); System.out.println(aflCio); }
范型单例模式
由于identityFunction本身是无状态的,而且范型会在运行时抹去类型信息,所以可以为任意的类提供一个相同的identityFunction
public final class IdentityFunction { private static interface UnaryFunction<T> { T apply(T arg); } private IdentityFunction() { } // Generic singleton factory pattern private static final UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() { @Override public Object apply(Object arg) { return arg; } }; @SuppressWarnings("unchecked") public static <T> UnaryFunction<T> identityFunction() { // IDENTITY_FUNCTION is stateless and its type parameter is // unbounded so it's safe to share one instance across all types. return (UnaryFunction<T>) IDENTITY_FUNCTION; } 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)); } } }
利用通配符来提升灵活性
普通范型的类型是不可变的
extends通配符
// pushAll method without wildcard type - deficient! public void pushAll(Iterable<E> src) { for (E e : src) push(e); } //pushAll(Iterable<Number>) in Stack<Number> // cannot be applied to (Iterable<Integer>) // numberStack.pushAll(integers); Stack<Number> numberStack = new Stack<Number>(); Iterable<Integer> integers = ... ; numberStack.pushAll(integers);
extends通配符:匹配某个类以及它的任意子类
// Wildcard type for parameter that serves as an E producer public void pushAll(Iterable<? extends E> src) { for (E e : src) push(e); }
super通配符
// popAll method without wildcard type - deficient! public void popAll(Collection<E> dst) { while (!isEmpty()) dst.add(pop()); } //compile error! Stack<Number> numberStack = new Stack<Number>(); Collection<Object> objects = ... ; numberStack.popAll(objects);
super通配符:匹配某个类以及它的任意父类
// Wildcard type for parameter that serves as an E consumer public void popAll(Collection<? super E> dst) { while (!isEmpty()) dst.add(pop()); }
PECS
producer- extends, consumer- super
- 生产者(产生范型的元素)-extends:pushAll(Iterable<? extends E> src)
- 消费者(使用范型的元素)-super:popAll(Collection<? super E> dst)
- 即是生产者又是消费者则使用普通范型:static <E> E reduce(… Function<E> f …)
不要在返回值上使用通配符范型,迫使使用接口的人必须考虑范型,设计良好的范型应该是透明的!
递归匹配
有时候类型参数必须和自身绑定的约束相匹配
public interface Comparable<T> { int compareTo(T o); }
只有能和自身类型进行大小比较的类才能用于max方法,换句话说这个类必须实现Comparable<T>接口。最后形成了T,Comaprable<T>的递归匹配
// Returns the maximum value in a list - uses recursive type bound 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; }
递归通配符匹配
Comparable/Comparator永远都是消费者!
public static <T extends Comparable<? super T>> T max(List<? extends T> list) { //这里不能在使用Iterator<T>,不然会编译报错 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; }
优先考虑类型安全的异构容器
在特定情况下可能出现一个容器需要存放不同类型的元素,比如说数据库的一条记录中各个字段,每个字段可能有不同的数据类型
使用key,value构建异构容器
使用Map存放:用一个键值对来表示一个元素,并且键和值之间有类型关系,键的范型跟值的class类型是相同的。让键参数化,而不是值参数化
//使用map的key来做范型,value的class类型就是key private final Map<Class<?>, Object> favorites = new ConcurrentHashMap<>(); 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)); } // Typesafe heterogeneous container pattern - client 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); Class<?> favoriteClass = f.getFavorite(Class.class); //Java cafebabe klose.effj.generics.Favorites System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName()); }