目录
这两条主要讲述了泛型对于类与方法的构建能起到很多积极的作用。
类的构建优先考虑泛型
如果某一个类需要支持特定的数据类型,在不考虑泛型的情况下,只有通过使用Object类型,然后在客户端程序中进行强制类型转换来实现:
import java.util.EmptyStackException;
public class StackFirst {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public StackFirst(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
sureCapacity());
elements[size++] = e;
}
public Object pop(){
if(sureEmpty()){
throw new EmptyStackException;
}
Object result = elements[--size];
elements[size] = null;
return result;
}
private void sureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
private boolean sureEmpty(){
return elements.length == 0;
}
}
//客户端
public class ApplicationFirst {
public static void main(String[] args) {
StackFirst stack = new StackFirst();
stack.push(1);
stack.push(2);
stack.push(3);
int first = (int) stack.pop();
int second = (int) stack.pop();
int third = (int) stack.pop();
}
}
这种方式有个问题,如果客户端的强制类型转换出错了,编译器是无法感知到的,这个错误只能在程序运行过程中返回ClassCastException:
public class ApplicationFirst {
public static void main(String[] args) {
StackFirst stack = new StackFirst();
stack.push(1);
stack.push(2);
stack.push(3);
String first = (String) stack.pop();//错误转换
int second = (int) stack.pop();
int third = (int) stack.pop();
}
}
(base) MacBook-Pro:chapter5$ javac Stack/ApplicationFirst.java//编译正常
(base) MacBook-Pro:chapter5$ java Stack/ApplicationFirst//运行异常
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at Stack.ApplicationFirst.main(ApplicationFirst.java:10)
而且,在不违反封装原则的前提下,客户端不应该知道类的内部构造,因此直接通过强制转换极易发生风险(除非有详细的文档进行说明)。
而泛型通过将数据类型参数化的方式解决了这个问题。
public class Stack<T>{
...
}
只要客户端能够在初始化类实例的时候指定参数T的类型,就可以安全的使用特定类来构建实例(这里的类型安全是由编译器来保证的,但对于支持泛型的数组(E[ ])可能需要进行人工确认,这个后面会说)。
如何解决泛型数组的问题
上文提到当类中含有数组时,不能直接创建不可具体化类型的数组(前一篇文章有提到,不可具体化类型指的是其在运行时包含的信息比编译时包含的信息要少的类型,各种非无限通配符类型的泛型都属于这种情况),这里有两种方式解决这个问题:
创建Object数组然后进行强制转换
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]
由于编译器并不知道E[ ]的数据类型,因此在进行强制类型转换的时候会产生一个告警,告诉程序员这里编译器并不能保证强制转换的正确性。
(base) MacBook-Pro:chapter5$ javac -Xlint Stack/StackSecond.java
Stack.java:12: warning: [unchecked] unchecked cast
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
^
required: E[]
found: Object[]
where E is a type-variable:
E extends Object declared in class StackSecond
这就需要程序员进行人工判断,如果判断这个转换并不会危及程序的类型安全性,就可以通过@SupressWarning关键字达到忽略告警的目的。这里一般要确保相关数组保存在一个私有域中,并且被类方法调用的几个场景与转换的类都是类型一致的(都是E)。
将Object数组中的元素进行强制转换
elements = new Object[DEFAULT_INITIAL_CAPACITY];
E element = (E) elements[0];
相比于第一种方法,这种方法的强制转换次数会更多,所以第一种方法更常用。但是第一种方法会导致堆污染(堆污染指的是在Java中,当一个参数化类型的变量引用了一个不是该参数化类型的对象时,导致运行时类型与编译时类型不匹配的情况)。
方法的构建优先考虑泛型
尤其是静态方法特别适合泛型化,它能够有效扩展方法的使用范围,这里有几个典型的场景
- 涉及两个潜在泛型类交互的方法,比如文中提到的union方法将两个Set<T>集合进行合并。
- 对于一些不可变但又可以适用于许多不同类型的对象,可以通过一个泛型的静态工厂方法,让它可以给每一种类型参数分发对象。这里作者举了一个恒等函数分发器的例子,通过UnaryOperator<T>接口来实现
import java.util.function.UnaryOperator;
public class UnarrayFunction {
private static UnaryOperator<Object> IDENTITY_FN = (t)->t;
public static <T> UnaryOperator<T> identityFunction(){
return (UnaryOperator<T>) IDENTITY_FN;
}
}
//客户端
public class Application {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
UnaryOperator<Integer> sameNum = UnarrayFunction.identityFunction();
for (Integer integer : list) {
System.out.println(sameNum.apply(integer));
}
}
}
这里简单介绍一下UnaryOperator<T>接口,UnaryOperator
是Java 8引入的一个功能性接口(只有一个抽象方法apply,因此可以直接通过lambda表达式来实现apply方法),它属于java.util.function
包的一部分(继承了Function<T, T>接口,即输入和输出都为T类型)。这个接口用于定义一个接收单个输入参数并产生相同类型输出结果的操作。UnaryOperator
通常用于实现简单的变换操作,例如数值计算、字符串处理等。