4.泛型

26.请不要使用原生态类型

泛型是在1.5引入的,使用它可以有两点优势:

  1. 向集合插入不正确的类型的时,可以在编译时就抛出错误,而不是在运行时才抛出异常,即有类型检查
  2. 从集合检索元素时,编译器帮我们做隐式转换,而不需要我们显示强转,既有隐式转换

标题中的提到的原生态类型是不带任何实际类型的泛型名称,例如List<E>的原生态类型是List。原生态类型缺乏类型安全性,而继续使用仅仅是为了兼容性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

List list = new ArrayList();
//误插入String和Integer两种不同类型的数据
list.add("123");
list.add(123);
//编译通过,运行时取出强转报错
Integer i = (Integer) list.get(0);

//定义泛型列表
List<String> list = new ArrayList<>(10);
list.add("123");
//编译类型检查,直接报错
list.add(123);
//隐式转换
String str = list.get(0);

而原生类型其实也有几处适用的地方:

  1. 必须在类文字中使用原生类型

    1
    List.class String[].class int.class都是合法的,而List<String.class>和List<?>.class则不合法
  2. 在使用instanceof

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void (Collection<E> collection){
    if(collection instanceof Set){//此处使用原生类型
    Set<?> set = (Set<?>) collection;//可以强转
    Iterator<?> it = set.iterator();
    while (it.hasNext()){
    System.out.println(it.next());
    }
    }
    }

27.消除非受检的警告

在使用泛型的代码中,很容易出现警告,不要忽略他们。每一条警告都可能在运行时抛出ClassCaseException异常,要尽最大努力消除他们。如果无法消除,但能确定引起警告的代码是类型的安全的,就可以在尽可能小的范围内使用@SuppersssWarnings("unchecked")注解禁止该警告。

28.列表由于数组

数组具有协变性,而集合没有

1
2
3
4
5
6
7
//Father[] 依旧是 Son1[] 的父类
Father[] sons = new Son1[1];
//'运行时'抛出异常ArrayStoreException
sons[0] = new Son2();

//List<Father> 不是 ArrayList<Son1>的父类
List<Father> list = new ArrayList<Son1>();

以上很容易看出,由于数组的协变性,很容易随意存入元素,然后再运行时抛出异常。

再举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A<E>{
private E[] arr;
public A(Collection<E> collection){
//这里有unchecked cast 警告
arr = (E[]) collection.toArray();
}
}

class A<E>{
private List<E> list;
public A(Collection<E> collection){
//无任何警告
list = new ArrayList<>(collection);
}
}

只想说明一点,数组和泛型不能很好的混合使用。如果在混合使用时,出现异常,应该立马尝试将数组换成列表。

29.优先使用泛型

直接看实例,忽略下标范围检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Stack{
public Object[] elements;
private int size = 0;
private final int DEFAULT_SIZE = 10;

public Stack(){
elements = new Object[DEFAULT_SIZE];
}

public void push(Object e){
this.elements[size++] = e;
}
public Object pop(){
return elements[--size];
}
}

方案一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Stack<E>{
public E[] elements;
private int size = 0;
private final int DEFAULT_SIZE = 10;

("unchecked")
public Stack(){
//创建时强转
elements = (E[]) new Object[DEFAULT_SIZE];
}

public void push(E e){
this.elements[size++] = e;
}
public E pop(){
return elements[--size];
}
}

方案二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Stack<E>{
public Object[] elements;
private int size = 0;
private final int DEFAULT_SIZE = 10;

public Stack(){
elements = new Object[DEFAULT_SIZE];
}

public void push(Object e){
this.elements[size++] = e;
}
("unchecked")
public E pop(){
return (E) elements[--size];
}
}

原始代码没有使用泛型,容易造成类型安全问题。方案一和方案二都是用了泛型了,更加安全些。

方案一要确保elements中的元素都是通过push存储进去的,不能将elements暴露在外,否则容易造成类型全问题

优先使用方案一,方案一只需强转一次,而方案二在每次pop元素时都要进行强转。

30.优先考虑泛型方法

静态工具方法尤其适合泛型化,例如Collections中的排序和查找算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyTest {
@Test
public void () {
Set<String> set1 = Set.of("1","2");
Set<String> set2 = Set.of("2","3");
Set<String> resultUnion = A.union(set1,set2);
System.out.println(resultUnion);
Set<String> resultIntersection = A.intersection(set1,set2);
System.out.println(resultIntersection);
}
}

class A{
//计算两个集合的并集
public static <E> Set<E> union(Set<E> set1,Set<E> set2){
Set<E> result = new HashSet<>(set1);
result.addAll(set2);
return result;
}
//计算两个集合的交集
public static <E> Set<E> intersection(Set<E> set1,Set<E> set2){
Set<E> result = new HashSet<>(set1);
result.retainAll(set2);
return result;
}
}

下面递归泛型的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyTest {
@Test
public void () {
List<String> list = new ArrayList<>(3);
list.add("1");
list.add("3");
list.add("2");
System.out.println(A.max(list));
}
}

class A{
public static <E extends Comparable<E>> E max(Collection<E> collection){
E max = null;
for (E e : collection){
if(max == null || e.compareTo(max) > 0){
max = e;
}
}
return max;
}
}

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

A是B的子类,但List<A>不是List<B>的子类。可以使用?提高灵活性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyTest {
@Test
public void () {
Stack<A> stack = new Stack<>();
stack.pushAll(new ArrayList<B>());
}
}

class A {}
class B extends A{}

class Stack<E>{
List<E> list;
public Stack(){
list = new ArrayList<>();
}
//E的任何子类的集合都可以传入
public void pushAll(Collection<? extends E> src){
for (E e : src){
list.add(e);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyTest {
@Test
public void () {
Stack<B> stack = new Stack<>();
stack.pushAll(new ArrayList<A>());
}
}
class A {}
class B extends A{}

class Stack<E>{
List<E> list;
public Stack(){
list = new ArrayList<>();
}
//可以传入任何E的父类的集合
public void pushAll(Collection<? super E> dst){
for (E e : list){
dst.add(e);
}
}
}

总结一下,当外来元素要进来,就是用? extents E, 因为传入的元素必须是E以及子类,才能被E接收。

当内部元素要出去,就使用? super E,因为E要出去,必须要使用E以及父类才能接收。

32.谨慎并用泛型和可变参数

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

参考文献

  • [Effective Java中文版(第3版)]