泛型
1 为什么要使用泛型?
在引入泛型之前,程序员必须使用Object
编写适用于多种类型的代码,很繁琐,也很不安全。例如,以下代码:
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
由于它的方法接受或返回一个Object
对象,因此只要不是原始类型之一,就可以随意传递任何想要的数据。
Java 5
中泛型(Generic
)的引入成为Java
程序设计语言自最初发行以来最显著的变化。随着泛型的引入,Java
有了一个表述能力很强的类型系统,允许设计者详细的描述变量和方法的类型要如何变化。它提高了代码的类型安全——可以在编译时检测到更多错误,增强代码的稳定性。
generic [dʒəˈnerɪk] 一般的,通用的;
泛型在定义类、接口和方法时使用类型(类和接口)成为参数,与方法声明中使用的形式参数非常类似。区别在于形式参数的输入是值,类型参数的输入是类型。
泛型程序设计(generic programming
)意味着编写的代码可以对多种不同类型的对象重用。 在Java
引入泛型之前,泛型程序设计是用继承来实现的,例如,有一个ArrayList
类,它只维护一个Object
引用的数组,因此它可以收集任何类型的对象。
public class ArrayList { // before generic classes
private Object[] elementData;
public Object get(int i) { }
public void add(Object o) { }
}
这种写法存在两个问题:
- 没有错误检查,可以向数组列表中添加任何类型的值:
files.add(new File("..."));
- 当获取一个值时必须进行强制类型转换,由于数据类型不确定,可能会导致转换出错:
ArrayList files = new ArrayList();
String fileName = (String) files.get(0);
对于这个调用,编译和运行都不会出错。不过在其他地方,如果将get
的结果强制类型转换为String
类型,就会产生一个错误。
泛型提供了一个更好的解决方案:类型参数(type parameter
)。ArrayList
类有一个类型参数用来指示元素的类型:
var files = new ArrayList<String>();
如果用一个明确的类型而不是var
声明的一个变量,可以通过使用<>
语法省略构造器中的类型参数,省略的类型可以从变量的类型推导中得出:
ArrayList<String> files = new ArrayList<>();
Java 9
扩展了<>
语法的使用范围,原来不接受这种语法的地方也可以使用,例如,可以对匿名子类使用<>
语法:
ArrayList<String> passwords = new ArrayList<>() { // diamond OK is Java 9
public String get(int n) {
return super.get(n).replaceAll(".", "*");
}
}
编译器也可以充分利用这个类型信息,调用get
的时候,不需要进行强制类型转换。编译器知道返回值类型是String
,而不是Object
:String fileName = files.get(0);
使用类似ArrayList
的泛型类很容易。大多数程序员会使用类似ArrayList<String>
这样的类型,就好像它们是Java
内置的类型一样。
编译器还知道ArrayList<String>
的add
方法有一个类型为String
的参数,这比有一个Object
类型的参数要安全的多。编译器可以检查数据类型,防止插入错误类型的对象。例如:
files.add(new File("...")); // can only add String objects to an ArrayList<String>
是无法通过编译的。出现编译错误要比运行时出现类的强制类型转换异常好得多。
与非泛型代码相比,使用泛型的代码具有许多优点:
- 在编译时进行更强的类型检测。
Java
编译器将强类型检查应用于通用代码,如果代码违反类型安全,则会发出错误。修复编译时错误比修复运行时错误更容易。 - 消除类型转换。
// 以下不带泛型的代码需要强制转换:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
// 当使用泛型重写时,代码不需要强制转换:
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // no cast
- 使程序员能够实现通用算法。 通过使用泛型,程序员可以实现对不同类型的集合进行工作,可以自定义类型安全且易于阅读的泛型算法。
问:Java
中的泛型是什么,使用泛型的好处是什么?
Java 1.4(Java 4)
或更早版本的开发中,在集合中存储对象并在使用前进行类型转换十分不方便(ArrayList
),泛型就是为了防止这种情况发生,它提供了编译期的类型安全,确保只能把正确类型的对象放入集合中,避免运行时出现ClassCastException
。
2 泛型类和泛型方法
2.1 泛型类
泛型类(generic class
)就是有一个或多个类型变量的类。 例如,Pair
类引入了一个类型变量T
,用<>
括起来,放在类名的后面:
public class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
public void setFirst(T first) {
this.first = first;
}
public void setSecond(T second) {
this.second = second;
}
}
泛型类还可以有多个类型变量。 例如:
public class Pair<T, U> { }
类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型, 例如:
private T first; // use the type variable
可以用具体的类型替换类型变量来实例化(instantiate
)泛型类型, 例如:Pair<String>
。可以把结果想像成一个普通类,它有以下构造器以及方法:
Pair<String>();
Pair<String>(String, String);
String getFirst();
String getSecond();
void setFirst(String);
void setSecond(String);
换句话说,泛型类相当于普通类的工厂。
以下代码中,静态的minmax
方法便利了数组并同时计算出最大值和最小值,它用一个Pair
对象返回两个结果:
class PairTest1 {
public static void main(String[] args) {
String[] words = {"Mary", "had", "a", "little", "lamb"};
Pair<String> mm = ArrayAlg.minmax(words);
System.out.println("min = " + mm.getFirst()); // min = Mary
System.out.println("max = " + mm.getSecond()); // max = little
}
}
class ArrayAlg {
public static Pair<String> minmax(String[] a) {
if (a == null || a.length == 0) return null;
String min = a[0];
String max = a[0];
for (int i = 0; i < a.length; i++) {
if (min.compareTo(a[i]) > 0) min = a[i];
if (max.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min, max);
}
}
2.2 泛型方法
以下定义了一个带有类型参数的方法:
class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
泛型方法可以在普通类中定义,也可以在泛型类中定义。
当调用一个泛型方法时,可以把具体类型放在<>
中,放在方法名前面,也可以省略类型参数, 编译器可以推断出类型:
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
// 或者
String middle = ArrayAlg.getMiddle("John", "Q.", "Public");
几乎在所有情况下,泛型方法的类型推导都能正常工作。但是也会出现问题,比如:
double middle = ArrayAlg.getMiddle(3.14, 1729, 0);
编译器将把参数自动装箱为1
个Double
和2个Integer
对象,然后寻找这些类的共同超类,找到了2
个超类型:Number
和Comparable
接口,Comparable
接口本身也是一个泛型类型。这这种情况下,可以采取的补救措施是将所有的参数都写为double
类型:
double middle = ArrayAlg.getMiddle(3.14, 1729.0, 1.00);
问:如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
public T put (K key, V value) {
return cache.put(key value);
}
3 类型变量的限定
有时候类或方法需要对类型变量加以约束:
class ArrayAlg {
public static <T> T min(T[] a) {
if (a == null || a.length == 0) return null;
T smallest = a[0];
for (int i = 0; i < a.length; i++) {
if (smallest.compareTo(a[i]) > 0) smallest = a[i];
}
return smallest;
}
}
这里有一个问题:变量smallest
的类型为T
,这意味着它可以是任何一个类的对象,但是无法知道T
所属的类是否有compareTo
方法。解决这个问题的方法是限制T
只能是实现Comparable
接口(包含一个方法compareTo
的标准接口)的类。可以通过对类型变量T
设置一个限定(bound
)来实现这一点:
bound [baʊnd] 必然的,肯定的;受约束的
public static <T extends Comparable> T min(T[] a)
实际上Comparable
接口本身就是一个泛型类。此时,泛型方法min
只能在实现了Comparable
接口的类。
<T extends BoundingType>
:表示T
应该是限定类型(bounding type
)的子类(subtype
)。T
和限定类型可以是类,也可以是接口。 选择关键字extends
的原因是它更接近子类型的概念。
subtype [ˈsʌbˌtaɪp] 子类型,亚类,下级类
一个类型变量或通配符可以有多个限定,例如:T extends Comparable & Serializable
,限定类型用&
分隔,而逗号用来分隔类型变量。
在Java
的继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,它必须是限定列表中的第一个限定。
class ArrayAlg {
public static <T extends Comparable> Pair<T> minmax(T[] a) {
if (a == null || a.length == 0) return null;
T min = a[0];
T max = a[0];
for (int i = 0; i < a.length; i++) {
if (min.compareTo(a[i]) > 0) min = a[i];
if (max.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min, max);
}
}
class PairTest2 {
@RequiresApi(api = Build.VERSION_CODES.O)
public static void main(String[] args) {
LocalDate[] birthdays = {
LocalDate.of(1906, 12, 9),
LocalDate.of(1815, 12, 10),
LocalDate.of(1903, 12, 3),
LocalDate.of(1910, 6, 22),
};
Pair<LocalDate> mm = ArrayAlg.minmax(birthdays);
System.out.println("min = " + mm.getFirst()); // min = 1815-12-10
System.out.println("max = " + mm.getSecond()); // max = 1910-06-22
}
}
4 泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类,编译器“擦除”类型参数。
Java
中的泛型基本上都是在编译器这个层次来实现的。在生成的`字节码文件中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
如在代码中定义的List
和List
等类型,在编译后都会变成List
。JVM
看到的只是List
,而由泛型附加的类型信息对JVM
来说是不可见的。Java
编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java
的泛型实现方法与C++
模版机制实现方式之间的重要区别。
4.1 类型擦除
每个泛型类型,都会有一个相应的原始类型(raw type
),这个原始类型的名字就是去掉类型参数后的泛型类型名。类型参数会被被擦除(erased
),并替换为其限定类型(对于无限定的变量则替换为Object
)。
raw [rɔː] 生的,未煮过的;天然的
例如,Pair<T>
的原始类型如下所示:
class Pair {
private Object first;
private Object second;
public Pair() {
first = null;
second = null;
}
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
public void setFirst(Object first) {
this.first = first;
}
public void setSecond(Object second) {
this.second = second;
}
}
因为T
是一个无限定的变量,所以直接用Object
替换。在程序中可以包含不同类型的Pair
,例如,Pair<String>
或Pair<LocalDate>
,不过擦除类型后,它们都会变成原始的Pair
类型。
原始类型用第一个限定来替换类型变量,如果没有给定限定,被替换为Object
。 例如,类Pair<T>
中的类型变量没有显式的限定,因此,原始类型用Object
替换T
。假定声明了一个稍有不同的类型:
class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
public Interval(T first, T second) {
if (first.compareTo(second) <= 0) {
lower = first;
upper = second;
} else {
lower = second;
upper = first;
}
}
}
原始类型Interval
如下所示:
class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
public Interval (Comparable first, Comparable second) {
}
}
4.2 转换泛型表达式
泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。 例如对于以下这个语句序列:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
getFirst
擦除类型后的返回类型时Object
。编译器自动插入转换到Emplyee
的强制类型转换。也就是说,编译器把这个方法调用转换为两条虚拟机指令:
- 对原始方法
Pair.getFirst()
的调用 - 将返回的
Object
类型强制转换为Employee
类型
访问一个泛型字段时也要插入强制类型转换。 假设Pair
类的first
字段和second
字段都是公共的,表达式Employee buddy = buddies.first;
也会在结果字节码中插入强制类型转换。
4.3 转换泛型方法
类型擦除也会出现在泛型方法中。 例如:
public static <T extends Comparable> T min(T[] a)
在进行类型擦除后,只剩下一个方法:
public static Comparable min(Comaprable[] a)
此时,类型参数T
已经被擦除了,只留下了限定类型Comparable
。
方法的擦除带来了两个复杂问题:
class DateInterval extends Pair<LocalDate> {
public void setSecond (LocalDate second) {
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
}
}
日期区间是一对LocalDate
对象,而且需要覆盖这个方法来确保第二个值永远不小于第一个值。这个类型擦除后:
class DateInterval extends Pair { // after erasure
public void setSecond (LocalDate second) { }
}
还有另一个从Pair
继承的setSecond()
方法,即:
public void setSecond(Object second);
这显然是一个不同的方法,因为它有不同类型的参数Object
,而不是LocalDate
。不过,不应该不一样,考虑一下语句序列:
var interval = new DateInterval();
Pair<LocalDate> pair = interval; // OK-- assignment to superclass
pair.setSecond(aDate);
如果希望setSecond
调用具有多态性,会调用最合适的那个方法。由于ppir
引用DateInterval
对象,所以应该调用DateInterval.setSecond
。问题在于类型擦除与多态发生冲突。为了解决这个问题,编译器在DateInterval
类中生成一个桥方法(bridge method
):
public void setSecond (Object second) {
setSecond((LocalDate) second);
}
这样做是可行的,跟踪以下语句执行:pair.setSecond(aDate);
变量pair
已经声明为类型Pair<LocalDate>
,并且这个类型只有一个名为setSecond
的方法,即setSecond(Object)
。虚拟机在pair
引用的对象上调用这个方法。这个对象是DateInterval
类型,因而将会调用DateInterval.setSecond(Object)
方法。这个方法是合成的桥方法。它会调用DateInterval.setSecond(LocalDate)
,这是正确的。
假设DateInterval
类也覆盖了getSecond
方法:
class DateInterval extends Pair<LocalDate> {
public LocalDate getSecond() {
return (LocalDate) supre.getSecond();
}
}
在DateInterval
类中,有两个getSecond
方法:
LocalDate getSecond(); // defined in DateInterval
Object getSecond(); // override the method defined in Pair to call the first method
但是,不能这样编写Java代码,在虚拟机中,会由参数类型和返回类型共同指定一个方法。因此,编译器可以为两个仅返回类型不同的方法生成字节码。
对于Java
泛型:
- 虚拟机中没有泛型,只有普通的类和方法
- 所有类型参数都会替换为它们的限定类型
- 会合成桥方法来保持多态
- 为保持类型安全,必要时插入强制类型转换
问:Java
泛型是如何工作的?什么是类型擦除?
泛型是通过类型擦除来实现的,编译器在编译期擦除了所有类型相关的信息,把泛型转换成了原始类型,在运行时不存在类型信息。例如List<String>
在运行时仅用一个List
来表示,这样做的目的,是确保能和Java 5
之前的版本类库进行兼容。
5 限制与局限性
5.1 不能用基本类型实例化类型参数
不能用基本类型代替类型参数,因此,没有Pair<double>
,只有Pair<Double>
,其原因在于类型擦除,擦除完之后,Pair
类含有Object
类型的字段,而Object
不能存储double
值。
这样做与Java
语言中基本类型的独立状态相一致。这并不是一个致命的缺陷——只有8
种基本类型,而且即使不能接受包装器类型(wrapper type
),也可以使用单独的类和方法来处理。
5.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型,因此,所有的类型查询只产生原始类型。 比如:
if (a instanceof Pair<String>) // error
实际上仅仅测试a
是否是任意类型的一个Pair
。下面的测试同样如此:
if (a instanceof Pair<T>) // error
或强制类型转换:
Pair<String> p = (Pair<String>) a; // waring-can only test that a is a Pair
为提醒这一风险,在试图查询一个对象是否属于某个泛型类型,会得到一个编译器错误(使用instanceof
时),或者得到一个警告(使用强制类型转换时)。
同样的道理,getClass
方法总是返回原始类型。例如:
Pair<String> stringPair = ... ;
Pair<Employee> employeePair = ... ;
if(stringPair.getClass() == employeePair.getClass())// they are equal
其比较的结果是true
,这是因为两次getClass
调用都返回Pair.class
5.3 不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:
var table = new Pair<String>[10]; // error
擦除之后,table
的类型是Pair[]
,可以把它转换为Object[]
:
Object[] objarray = table;
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStore-Execption
异常:
objArray[0] = "Hello" // error--component type is Pair
不过对于泛型类型,擦除会使用这种机制无效。 以下赋值:
objarray[0] = new Pair<Employee>();
尽管能够通过数组存储的检查,但仍会导致一个类型错误,出于这个原因,不允许创建参数化类型的数组。需要说明的是,只是不允许创建这种数组,而声明类型为Pair<String>[]
的变量仍是合法的。不过不能用new Pair<String>[10]
初始化这个变量。
可以声明通配符类型的数组,然后进行强制类型转换:
var table = (Pair<String>[]) new Pair<?>[10];
结果将是不安全的。 如果再table[0]
中存储一个Pair<Employee>
,然后对table[0].getFirst()
调用一个String
方法,会得到一个ClassCastException
。
5.4 Varargs
警告
向参数个数可变的方法传递一个泛型类型的实例。比如以下方法,它的参数个数是可变的:
public static <T> void addAll(Collection<T> coll, T... ts) {
for (T t: ts) coll.add(t);
}
实际上参数ts
是一个数组,包含提供的所有实参:
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);
为了调用这个方法,Java
虚拟机必须建立一个Pair<String>
数组,这就违反了前面的规则。不过,对于这种情况,规则有所放松,你只会得到一个警告,而不是错误。
可以采用两种方法一直这个警告。一种方法是为包含addAll
调用的方法增加注解@SuppressWarnings("unchecked")
。或者在Java7中,还可以用@SafeVarargs
直接注解addAll
方法:
@SafeVarags
public static <T> void addAll(Collection<T> coll, T... ts)
现在就可以提供泛型类型来调用这个方法了。对于任何只需要读取参数数组元素的方法(这肯定是最常见的情况),都可以使用这个注解。
@SageVarags
只能用于声明为static
、final
或(Java 9)private
的构造器和方法。 所有其他方法都可能被覆盖,使得这个注解没有什么意义。
5.5 不能实例化类型变量
不能在类似new T(...)
的表达式中使用类型变量。 例如,下面的Pair<T>
构造器就是非法的:
public Pair() {
first = new T();
second = new T();
} // error
类型擦除将T
变成Object
,所以执行的是new Object();
在Java 8之后,最好的解决方法是让调用者提供一个构造器表达式。 例如:Pair<String> p = Pair.makePair(String::new);
makePair
方法接收一个Supplier<T>
,这是一个函数式接口,表示一个无参数而且返回类型为T
的函数:
public static T Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get(), const.get());
}
比较传统的解决方法是通过反射调用Constructor.newInstance()
方法来构造泛型对象。 细节有些复杂,不能调用以下方法实现:
first = T.class.getConstructor().newInstance() // error
表达式T.class
是不合法的,因为它会擦除为Object.class
。必须适当的设计API
以便得到一个Class
对象,如下所示:
public static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<>(cl.getConstructor().newIntstance(), cl.getConstructor().newInstance());
} catch (Execption e) {
return null;
}
}
这个方法可以如下调用:Pair<String> p = Pair.makePair(String.class);
注意,Class
类本身是泛型的。例如String.class
是一个Class<String>
的实例(事实上,它是唯一的实例)。因此,makePair
方法能够推断出所建立的对组(pair
)的类型。
5.6 不能构造泛型数组
就像不能实例化泛型实例一样,也不能实例化数组。 不过原因有所不同,数组可以填充null
值,看上去好像可以安全构造,不过,数组本身也带有类型,用来监控虚拟机中的数据存储,这个类型会被擦除。例如:
public static <T extends Comparable> T[] minmax(T... a) {
T[] mm = new T[2]; // error
}
类型擦除会让这个方法总是构造Comparable[2]
数组。
如果数组仅仅作为一个类的私有实例字段,可以将这个数组的元素类型声明为擦除类型并使用强制类型转换。 例如,ArrayList
类可以如下实现:
public class ArrayList<E> {
private Object[] elements;
@SuppressWarnings("unchecked") public E get(int n) { return (E) elements[n]; }
public void set(int n, E e) { elements[n] = e } // no cast needed
}
但实际的实现没有这么清晰:
public class ArrayList<E> {
private E[] elements;
public ArrayList() {
elements = (E[]) new Object[10];
}
}
这里,强制类型转换E[]
是一个假象,而类型擦除使其无法察觉。
这个技术并不适用于minmax
方法,因为minmax
方法返回一个T[]
数组,如果类型不对,就会得到运行时错误结果。假设实现以下代码:
public static <T extends Comparable> T[] minmax(T... a) {
var result = new Comparable[2]; // array of erased type
// ...
return (T[]) result; // compiles with warning
}
以下调用:String[] names = ArrayAlg.minmax("Tom", "Dick", "Harry");
编译时不会有任何警告。当方法返回后Comparable[]
引用强制转换为String[]
时,将会出现ClassCastExecption
异常。
在这种情况下,最好让用户提供一个数组构造器表达式:String[] names = ArayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");
。构造器表达式String::new
指示一个函数,给定所需的长度,会构造一个指定长度的String
数组。
minmax
方法使用这个参数生成一个有正确类型的数组:
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a) {
T[] result = constr.apply(2);
// ...
}
比较老式的方法式用反射,并调用Array.newInstance
:
public static <T extends Comparable> T[] minmax(T... a) {
var result = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
// ...
}
ArrayList
类的toArray
方法就没有这么幸运。它需要生成一个T[]
数组,但没有元素类型。因此,有以下两种不同的形式:
Object[] toArray()
T[] toArray(T[] result)
第二个方法接收一个数组参数。如果数组足够大,就使用这个数组。否则,用result
的元素类型构造一个足够大的新数组。
5.7 泛型类的静态上下文中类型变量无效
不能在静态字段或方法中引用类型变量。例如,下面的做法实际上行不通:
public class Singleton<T> {
private static T singleInstance; // error
public static T getSingleInstance() { // error
if (singleInstance == null) { // construct new instance of T
return singleInstance;
}
}
}
如果这样可行,程序就可以声明一个Singleton<Random>
共享一个随机数生成器,另外声明一个Singleton<JFileChooser>
共享一个文件选择器对话框。但是,这样式行不通的。类型擦除之后,只剩下Singleton
类,它只包含一个singleInstance
字段。因此,禁止使用带有类型变量的静态字段和方法。
5.8 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类的对象。实际上,泛型类扩展Throwable
设置都是不合法的。 例如,以下定义就不能正常编译:
public class Problem<T> extends Exception { } // Error--can't extend Throwable
catch
子句不能使用类型变量。例如,以下方法将不能正常编译:
public static <T extends Throwable> void doWork(Class<T> t) {
try {
// do work
} catch (T e) {
Logger.global.info(...);
}
}
不过,在异常规范中使用类型变量是允许的。以下方法是合法的:
public static <T extends Throwable> void d {
// do work
} catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
5.9 可以取消对检查型异常的检查
Java
异常处理的一个基本原则是,必须为所有检查型异常提供一个处理器。不过可以利用泛型取消这个机制。 关键在于以下方法:
@SuppressWarning("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T {
throw (T) t;
}
假设这个方法包含在接口Task
中,如果有一个检查型异常e
,并调用:Task.<RuntimeException>throwAs(e);
。编译器就会认为e
是一个非检查型异常。以下代码会吧所有异常都转换为编译器所认为的非检查型异常:
try {
// do work
} catch (Throwable t) {
Task.<RuntimeExecption>throws(t);
}
下面使用这个技术解决一个棘手的问题。要在一个线程中运行代码,需要把代码放在一个实现了Runnable
接口的类在run
方法中。不过这个方法不允许抛出检查型异常。我们将提供一个从Task
到Runnable
的适配器,它的run
方法可以抛出任何异常:
interface Task {
void run() throws Exception
@SuppressWarngings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T {
throw (T) t;
}
static Runnable asRunnable(Task task) {
return () -> {
try {
task.run();
} catch (Exception e) {
Task.<RuntimeException>throwAs(e);
}
}
}
}
例如,以下程序运行了一个线程,它会抛出一个检查型异常:
public class Test {
public static void main(String[] args) {
var thread = new Thread(Task.asRunnable(() -> {
Thread.sleep(1000);
System.out.println("Hello World!");
throw new Execption("Check this out!");
}));
thread.start();
}
}
Thread.sleep
方法声明为抛出一个InterruptedException
,我们不再需要捕获这个异常。由于我们没有中断这个线程,这个异常不会抛出。不过,程序会抛出一个检查型异常。运行程序时,会得到一个堆栈轨迹。
这有什么意义呢?正常情况下,必须捕获一个Runnable
的run
方法中的所有检查型异常,把他们“包装”到非检查型异常中,因为run
方法声明为不抛出任何检查型异常。不过这里并没有做这种“包装”,只是抛出异常,并“欺骗”编译器,让它相信这不是一个检查型异常。
通过使用泛型类、擦除和@SuppressWarnings
注解,就能消除Java
类型系统的部分基本限制。
问:如何阻止Java
中的类型未检查的警告?
如果把泛型和原始类型混合起来使用,Java 5的javac编译器会产型类型未检查的警告:
List<String> rawList = new ArrayList();
使用了未检查或称为不安全的操作。这种警告可以使用@SuppressWarnings("unchecked")
注解来屏蔽。
5.10 注意擦除后的冲突
当泛型类型擦除后,不允许创建引发冲突的条件。 以下例子中,假定Pair
类增加一个equals
方法,如下所示:
public class Pair<T> {
public boolean equals(T value) {
return first.equals(value) && second.equals(value);
}
}
考虑一个Pair<String>
。从概念上讲,它有两个equals
方法:
boolean equals(String) // defined in Pair<T>
boolean equals(Object) // inherited from Object
但是,方法boolean equals(T)
擦除后就是boolean equals(Object)
这会与Object.equals
冲突。补救的方法是重新命名引发冲突的方法。
泛型规范说明还引用了另外一个原则:为了支持擦除类型转换,需要施加一个限制——倘若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类。 例如,下述代码是非法的:
class Employee implements Comparable<Employee> { }
class Manager extends Employee implements Comparable<Manager> { } // error
Manager
会实现Comparable<Employee>
和Comparable<Manager>
,这是同一接口的不同参数化。
这一限制与类型擦除的关系并不是十分明显。毕竟,以下非泛型版本是合法的。
class Employee implements Comparable { }
class Manager extends Employee implements Comparable { }
其原因非常微妙,有可能与合成的桥方法产生冲突。实现了Comparable<X>
的类会获得一个桥方法:public int compareTo(Object other) { return compareTo((X) other);}
不能对不同的类型X
有两个这样的方法。
6 泛型类型的继承规则
如果有一个类和一个子类,如Employee
和Manager
。Pair<Manager>
是Pair<Employee>
的一个子类吗?——不是。例如,下面的代码将不能编译成功:
Manager[] topHonchos = ...;
Pair<Employee> result = ArrayAlg.minmax(topHonchos); // Error
minmax
方法返回Pair<Manager>
,而不是Pair<Employee>
,并且这样的赋值是不合法的。
无论S
和T
有什么联系,通常Pair<S>
与Pair<T>
没有什么联系。 这一限制看起来过于严格,但对于类型安全非常必要。假设允许将Pair<Manager>
转换为Pair<Employee>
:
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<Employee> employeeBuddies = managerBuddies; // illegal, but suppose it wasn't
employeeBuddies.setFirst(lowlyEmployee);
最后一句是合法的。但是employeeBuddies
和managerBuddies
引用了同样的对象。现在讲CFO和一个普通员工组成一对,这对于Pair<Manager>
来说应该是不可能的。
必须注意泛型与Java
数组之间的重要区别:可以将一个Manager[]
数组赋给一个类型为Employee[]
的变量:
Manager[] managerBuddies = { ceo, cfo };
Employee[] employeeBuddies = managerBuddies; // OK
数组带有特别的保护,如果试图将一个低级别的雇员存储到employeeBuddies[0]
,虚拟机将会抛出ArrayStoreException
。
总是可以将参数化类型转换为一个原始类型。 例如Pair<Employee>
是原始类型Pair
的一个子类型。在与遗留代码衔接时,这个转换非常必要。
转换成原始类型之后会产生类型错误吗——会
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair rawBuddies = managerBuddies; // OK
rawBuddies.setFirst(new File("...")); // only a compile-tiem warning
当使用getFirst
获得外来对象并赋给Manager
变量时,与通常一样,会抛出ClassCastException
异常。这里失去的只是泛型程序设计提供的附加安全性。
泛型类可以扩展或实现其他的泛型类,就这一点而言,与普通的类没有什么区别。例如,ArrayList<T>
类实现List<T>
接口。意味着,一个ArrayList<Manager>
可以被转换为一个List<Manager>
。但是,一个ArrayList<Manager>
不是一个ArrayList<Employee>
或List<Employee>
。
7 通配符类型
7.1 通配符概念
通配符类型中,允许类型参数变化。 例如,通配符类型Pair<? extends Employee>
,表示任何泛型Pair
类型,它的类型参数是Employee
的子类,如Pair<Manager>
,但不是Pair<String>
。
假设要编写一个打印雇员对的方法,像这样:
public static void printBuddies(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " +second.getName() + " are buddies.")
}
不能将Pair<Manager>
传递给这个方法,这一点很受限制。解决的方法很简单:使用通配符类型
public static void printBuddies(Pair<? extends Employee> p)
类型Pair<Manager>
是Pair<? extends Employee>
的子类型:
使用通配符会通过Pair<? extends Employee>
引用破坏Pair<Manager>
吗?
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wildcardBuddies.setFirst(lowlyEmployee); // compile-time error
这可能不会引起破坏。对setFirst
的调用有一个类型错误:
? extends Employee getFirst()
void setFirst(? extends Employee)
这样讲不可能调用setFirst()
方法。编译器只知道需要某个Employee
的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型,?
不能用来匹配。
使用getFirst
就不存在这个问题**:将getFirst
的返回值赋给一个Employee
的引用完全合法**。
这就是引入有限定的通配符的关键之处。现在已经有办法区分安全的访问器方法和不安全的更改器方法了。
7.2 通配符的超类型限定
通配符限定与类型限定十分类似,但是,还有一个附加能力,即可以指定一个超类型限定(supertype bound),例如:? super Manager
,这个通配符限制为Manager
的所有超类型。
可以为方法提供参数,但不能使用返回值。 例如,Pair<? super Manager>
方法:
void setFirst(? super Manager)
? super Manager getFirst()
这不是真正的Java
语法,但是可以看出编译器知道什么。编译器无法知道setFirst
方法的具体类型,因此调用这个方法时不能接受类型为Employee
或Object
的参数,只能传递Manager
类型的对象,或者某个子类型(如Executive
)对象。另外,如果调用getFirst
,不能保证返回对象的类型,只能把它赋给一个Object
。
下面是一个典型的示例,有一个经理的数组,并且想把奖金最高和最低的经理放在一个Pair
对象中,Pair
的类型是什么?在这里Pair<Employee>
是合理的,Pair<Object>
也是合理的。下面的方法将可以接受任何适当的Pair
:
public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {
if (a.length == 0) return;
Manager min = a[0];
Manager max = a[0];
for (int i = 1; i < a.length; i++ ) {
if (min.getBonus() > a[i].getBonus()) min = a[i];
if (max.getBonus() < a[i].getBonus()) max = a[i];
}
result.setFirst(min);
result.setSecond(max);
}
直观的讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
下面是超类型限定的另一种应用。**Comparable
接口本身就是一个泛型类型。**声明如下:
public interface Comparable<T> {
public int compareTo(T other);
}
在此,类型变量指示了other
参数的类型。例如:String
类实现Comparable<String>
,它的compareTo
方法被声明为public int compareTo(String other)
,显式的参数有一个正确的类型。接口是一个泛型接口之前,other
是一个Object
,并且这个方法的实现需要强制类型转换。
由于Comparable
是一个泛型类型,也许可以把ArrayAlg
类的min
方法做的更好一些?可以这样声明:public static <T extends Comparable<T>> T min(T[] a)
:
看起来,这样写比只使用T extends Comparable
更彻底,并且对许多类来讲,工作得更好。例如,如果计算一个String
数组的最小值,T
就是String
类型的,而String
是Comparable<String>
的子类型。但是,处理一个LocalDate
对象的数组时,会出现一个问题。LocalDate
实现了ChronoLocalDate
,而ChronoLocalDate
扩展了Comparable<ChronoLoacalDate>
。因此,LocalDate
实现的是Comparable<ChronoLocalDate>
而不是Comparable<LocalDate>
。
在这种情况下,超类型可以用来解决:public static <T extends Comparable<? super T>> min(T[] a)
。现在compareTo
方法写成:int compareTo(? super T)
。它可以声明为使用类型T
的对象,或者也可以是使用T
的一个超类型对象。无论如何,向compareTo
方法传递一个T
类型的对象是安全的。
注释:子类型限定的另一个常见的用法是作为一个函数式接口的参数类型。例如Collection
接口有一个方法:
default boolean removeIf(Predicate<? super E> filter)
这个方法会删除所有满足给定条件的元素。例如,删除所有有乞丐散列码的员工:
ArrayList<Employee> staff = ...;
Predicate<Object> oddHashCode = obj -> obj.hashCode() % 2 != 0;
staff.removeIf(oddHashCode);
如果希望传入一个Predicate<Object>
,而不只是Predicate<Employee>
。super
通配符可以实现。
7.3 无限定通配符
无限定的通配符,例如Pair<?>
和Pair
, 看起来好像一样,其实有很大的区别。类型Pair<?>
有以下方法:
? getFirst();
void setFirst(?);
getFirst
的返回值只能赋给一个Object
,setFirst
方法不能被调用。甚至不能用Object
调用。Pair<?>
和Pair
本质的不同在于:可以用任意Object
对象调用原始Pair
类的setFirst
方法。
无限定通配符对于很多简单操作非常有用。例如,下面的方法可用来测试一个对组是否包含一个null
引用,它不需要是假的类型:
public static boolean hasNull (Pair<?> p) {
return p.getFirst() == null || p.getSecond() == null;
}
通过将hasNulls
转换成泛型方法,可以避免使用通配符类型:
public static <T> boolean hasNulls(Pair<T> p)
带通配符的代码可读性更好。
7.4 通配符捕获
下面编写一个方法来交换对组的元素:
public static void swap(Pair<?> p)
通配符不是类型变量, 因此,不能在编写代码中使用?
作为一种类型。也就是说,以下代码是非法的:
? t = p.getFirst(); // error
p.setFirst(p.getSecond());
p.setSecond(t);
这是一个问题,在交换的时候必须临时保存第一个元素。可以写一个赋值方法swapHelper
,如下所示:
public static <T> void swapHelper(Pair<T> p) {
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
swapHelper
是一个泛型方法,而swap
不是,它有一个固定的Pair<T>
类型参数的参数。现在可以由swap
调用swapHelper
:
public static void swap(Pair<?> p) {
swapHelper(p);
}
在这种情况下,swapHelper
方法的参数T
捕获通配符。 它不知道通配符指示哪种类型,但是,这是一个明确的类型,并且从T <swapHelper>
的定义可以清楚地看到T
指示那个类型。
在这种情况下,并不是一定要使用通配符。可以直接把<T> void swap(Pair<T> p)
实现为一个没有通配符的泛型方法。不过,考虑下面的这个例子,这里通配符类型很自然地出现在计算中间:
public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
minmaxBonus(a, result);
PairAlg.swapHelper(result); // OK--swapHelper captures wildcard type
}
在这里,通配符捕获即使是不可避免的。
通配符捕获只有在非常限定的情况下才是合法的。编译器必须能够保证通配符表示单个确定的类型。例如ArrayList<Pair<T>>
中的T
永远不能捕获ArrayList<Pair<?>>
中的通配符,数组列表可以保存两个Pair<?>
,其中?
分别由不同的类型。
问:什么是泛型中的限定通配符和非限定通配符?
限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>
,它通过确保类型必须是T
的子类来设定类型的上界,另一种是<? super T>
,它通过确保类型必须是T
的父类来设定类型的下界。泛型类型必须用限定内的类型进行初始化,否则会导致编译错误。<?>
表示了非限定通配符,因为<?>
可以用任意类型来替代。
问:List<? extends T>
和List<? super T>
之间有什么区别?
List<? extends T>
可以接受任何继承自T
类型的List
,而List<? super T>
可以接受任何T
的父类构成的List
。例如,List<? extends Number>
可以接受List<Integer>
或List<Float>
。
问:编写一段泛型程序来实现LRU
缓存(最近最少使用)
缓存的大小是固定的,并且支持get
和put
操作,当缓存已满时,put
操作将删除最近最少使用的缓存。
LRU
高速缓存可以使用两个数据结构HashMap
和一个用于存储数据的双向链表来实现
class Node<T, U> {
T key;
U value;
Node<T, U> prev;
Node<T, U> next;
public Node(T key, U value, Node<T, U> prev, Node<T, U> next) {
this.key = key;
this.value = value;
this.prev = prev;
this.next = next;
}
}
class LRUCache<K, V> {
private Node<K, V> lru;
private Node<K, V> mru;
private Map<K, Node<K, V>> container;
private int capacity;
private int currentSize;
public LRUCache(int capacity) {
this.capacity = capacity;
this.currentSize = 0;
lru = new Node<>(null, null, null, null);
mru = lru;
container = new HashMap<>();
}
public V get(K key) {
Node<K, V> tempNode = container.get(key);
if (tempNode == null) {
return null;
} else if (tempNode.key == mru.key) {
return mru.value;
}
Node<K, V> nextNode = tempNode.next;
Node<K, V> prevNode = tempNode.prev;
if (tempNode.key == lru.key) {
nextNode.prev = null;
lru = nextNode;
} else if (tempNode.key != mru.key) {
prevNode.next = nextNode;
nextNode.prev = prevNode;
}
tempNode.prev = mru;
mru.next = tempNode;
mru = tempNode;
mru.next = null;
return tempNode.value;
}
public void put(K key, V value) {
if (container.containsKey(key)) {
return;
}
Node<K, V> myNode = new Node<K, V>(key, value, mru, null);
mru.next = myNode;
container.put(key, myNode);
mru = myNode;
if (currentSize == capacity) {
container.remove(lru.key);
lru = lru.next;
lru.prev = null;
} else if (currentSize < capacity) {
if (currentSize == 0) {
lru = myNode;
}
currentSize++;
}
}
}
问:可以把List<String>
传递给一个接受List<Object>
参数的方法吗?
不可以,编译错误,因为List<Object>
可以存储任何类型的对象,包括String
、Integer
等等,而List<String>
只能用来存储String
类型的数据。
List<Object> objectList;
List<String> stringList;
objectList = stringList; // compilation error incompatible types
问:Java
中List<?>
和List<Object>
之间的区别是什么?
List<?>
是一个未知类型的List
,而List<Object>
其实是任意类型的List
。可以把List<String>
、List<Integer>
赋值给List<?>
,却不能把List<String>
赋值给List<Object>
。
List<?> listOfAnyType;
List<Object> listOfObject = new ArrayList<>();
List<String> listOfString = new ArrayList<>();
List<Integer> listOfInteger = new ArrayList<>();
listOfAnyType=listOfString; // legal
listOfAnyType=listOfInteger; // legal
listOfObject=listOfString; // compiler error - in-convertible types
问:Java
中List<Object>
和原始类型List
之间的区别?
在编译期编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用Object
作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String
和Integer
。
问:Java
中的List<String>
和原始类型List
之间的区别?(类似于原始类型和带参数类型之间有什么区别)
带参数类型是类型安全的,由编译器保证,但原始类型是不安全的,不能把String
之外的任何其他类型的Object
存入String
类型的List
中,但是可以把任何类型的对象存入原始List
中。使用泛型的带参数类型不需要进行类型转换,但对于原始类型,则需要进行显式的类型转换。
问:Array
中可以使用泛型吗?
Array
不支持泛型,这也是为什么Joshua Bloch在Effective Java中建议用List
来代替Array
,因为List
可以提供编译期的类型安全保证,而Array
却不能。
参考
https://blog.csdn.net/t131452n/article/details/78068906
https://blog.csdn.net/cunchi4221/article/details/107472028?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromBaidu-1.not_use_machine_learn_pai&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromBaidu-1.not_use_machine_learn_pai