1、为什么要使用泛型程序设计
泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用
2、定义简单泛型类
一个泛型类就是具有一个或多个类型变量的类。
示例代码
package com.java01.day08;
/**
* @description:
* @author: ju
* @date: 2020-05-14 17:22
*/
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 void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
Pair类引入了一个泛型变量T,用尖括号<>括起来,并放在类名的后面。泛型类可以有多个类型变量。例如:
public class Pair<T,U> {…}
类定义中的类型变量指定方法的返回类型以及域或局部变量的的类型。例如:
private T first;
private T second;
在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字和值的类型。T(需要时还可以使用临近的U和S)表示“任意类型”。
用具体的类型替换类型变量就可以实例化泛型类型,例如:
Pair < String>
换句话说,泛型类可看做普通类的工厂。
示例代码
public 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 = 1; 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);
}
}
public class PairTest1 {
public static void main(String[] args){
String[] words = {"Mary", "Lisa", "zhangsan", "wwangwu"};
Pair<String> stringPair = ArrayAlg.minMax(words);
System.out.println("min = " + stringPair.getFirst());
System.out.println("max = " + stringPair.getSecond());
}
}
3、泛型方法
定义一个带类型参数的简单方法
public class ArrayAlg {
public static <T> T getMiddle(T...t){
return t[t.length/2];
}
}
类型变量放在修饰符的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型。
String middle1 = ArrayAlg.<String>getMiddle("Join", "Mary", "Lisa");
在大多数情况下,方法调用中可以省略< String>类型参数。
String middle2 = ArrayAlg.getMiddle("Join", "Mary", "Lisa");
4、类型变量的限定
有时,类或方法需要对类型变量加以约束。
public static <T> T min(T[] a){
if (a == null || a.length == 0){
return null;
}
String min = a[0];
for (int i = 1; i<a.length ; i++){
if (min.compareTo(a[i]) > 0){
min = a[i];
}
}
return min;
}
例如上述代码,怎么确保T所属的类有compareTo方法呢?对于这种情况我们就要对T限定为实现了Comparable接口的类:
public static <T extends Comparable> T min(T[] a){...}
在此为什么使用关键字extends而不是implements?
< T extends Comparable>表示T应该绑定Comparable类型的子类型。T和绑定类型可以是类,也可以是接口。选择关键字extends的原因是更接近子类的概念。
一个类型变量或通配符可以有多个限定,例如:
T extends Comparable & Serializable
限定类型用“&”分隔,而逗号用来分隔类型变量。
示例代码
package com.java01.day08;
import java.util.Calendar;
import java.util.GregorianCalendar;
/**
* @description:
* @author: ju
* @date: 2020-05-15 09:15
*/
public class PairTest2 {
public static void main(String[] args){
GregorianCalendar[] calendar = {
new GregorianCalendar(1997, Calendar.DECEMBER, 23),
new GregorianCalendar(1890, Calendar.JANUARY, 3),
new GregorianCalendar(2020, Calendar.FEBRUARY, 11),
new GregorianCalendar(2019, Calendar.APRIL, 24)
};
Pair<Comparable> pair = ArrayAlg2.minmax(calendar);
System.out.println("min = " + pair.getFirst());
System.out.println("max = " + pair.getSecond());
}
}
class ArrayAlg2{
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 = 1; 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);
}
}
5、泛型代码和虚拟机
虚拟机没有泛型类型对象——所有类型都属于普通类。
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删除类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定的变量用Object)。
例如:Pair的原始类型如下:
public 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 void setFirst(Object first) {
this.first = first;
}
public Object getSecond() {
return second;
}
public void setSecond(Object second) {
this.second = second;
}
}
因为T是一个无限定的变量,所以直接用Object替换。
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。
假设声明了一个不同类型:
public 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;
}
}
}
原始类型:
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
public Interval(Comparable first, Comparable second){
if (first.compareTo(second) <= 0){
lower = first;
upper = second;
}else {
lower = second;
upper = first;
}
}
}
5.1 翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如:
Pair<Employee> employeePair = new Pair<>();
Employee first = employeePair.getFirst();
编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法Pair.getFirst的调用
- 将返回的Object类型强制转换为Employee类型
当存取一个泛型域时也要插入强制类型转换:Employee first = employeePair.first;
5.2 翻译泛型方法
类型擦除也会出现在泛型方法中。
public static <T extends Comparable> T min(T[] a){...}
擦除类型后:
public static Comparable min(Comparable[] a){...}
方法擦除带来一个问题:
类型擦除与多态发生了冲突,要想解决这个问题,就需要编译器在DateInterval类中生成一个桥方法
@Override
public void setSecond(Object second){
setSecond((Date)second);
}
Java泛型转换的事实:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。
- 为保持类型安全性,必要时插入强制类型转换。
6、约束与局限性
6.1 不能用基本类型实例化类型参数
不能用类型参数代替基本类型。因此,没有Pair< double> 只有Pair< Double>;其原因是类型擦除之后,Pair类含有Object类型的域,而Object不能存储double值。
6.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。例如:
public class Test {
public static void main(String[] args){
Pair<Employee> employeePair = new Pair<>();
Pair a = new Pair();
if (employeePair instanceof Pair<Employee>){
//error
}
if (employeePair instanceof Pair<T>){
//error
}
Pair<Employee> b = a; //warning警告
if (a.getClass() == employeePair.getClass()){
//true 因为两次调用getClass都将返回Pair.class
}
}
}
无论何时使用instanceof 或涉及泛型类型的强制类型转换表达式都会看到一个编译警告;getClass方法总是返回原始类型。
6.3 不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:
Pair<String>[] table = new Pair<String>[16]; //error
只是不允许创建这些数组,而声明类型为Pair< String>[]的变量仍是合法的。不过不能用new Pair< String>[16]初始化这个变量。
6.4 Varargs警告
向参数个数可变的方法传递一个泛型类型的实例
public static <T> void addAll(Collection<T> coll, T...ts){
for (T t: ts){
coll.add(t);
}
}
Collection<Pair<String>> coll = ...;
Pair<String> stringPair1 = new Pair<>();
Pair<String> stringPair2 = new Pair<>();
addAll(coll, stringPair1, stringPair2);
为了调用这个方法,java虚拟机必须建立一个Pair< String>数组,这就违反了前面的规则,不过对于这种情况,规则有所放松,会给你一个警告,不会报错。
可以采取两种方式来抑制这个警告。一种方法是为包含addAll调用的方法添加标注@SuppressWarnings("unchecked")
;另一种方法是在addAll方法上直接添加标注@SafeVarargs
@SafeVarargs
public static <T> void addAll(Collection<T> coll, T...ts){
6.5 不能实例化类型变量
不能使用像new T(…) ,new T[…],或T.class这样的表达式中的类型变量;类型擦除将T改变为Object,而且,本意肯定不希望调用new Object()。
必须像下面这样设计API以便可以支配Class对象:
public static <T> Pair<T> makePair(Class<T> c){
try {
return new Pair<>(c.newInstance(), c.newInstance());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
可以使用下列方式调用:
Pair<String> pair = makePair(String.class);
注意:Class类本身是泛型,String.class是一个Class< String> 的实例(也是唯一实例)
如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为Object[],并在获取元素时进行类型转换。例如ArrayList可以这样实现:
class ArrayList<E>{
private E[] elements;
@SuppressWarnings("unchecked")
public E[] getElements(int n) {
return (E[]) elements[n];
}
public void setElements(int n , E e) {
elements[n] = e;
}
}
但这种写法可能会导致类型转换的错误出现。
可以利用反射,调用newInstance():
public static <T extends Comparable> T[] minMax(T...a){
T[] tt = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
}
6.6 泛型类的静态上下文类型变量无效
不能再静态域或方法中引用类型变量
6.7 不能抛出或捕获异常类的实例
既不能抛出也不能捕获泛型类对象。实际上,甚至泛型类扩展Throwable都是不合法的。
public class Problem<T> extends Exception{
//ERROR
}
try {
coding...
} catch (T t) { //ERROR
t.printStackTrace();
}
不过,在异常规范中,使用类型变量是允许的
public class Problem<T> extends Exception{
public static <T extends Throwable> void doWork(T t) throws T{
try {
...
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
6.8 注意擦除后的冲突
泛型规范说明还提到另外一个原则:“要想支持擦除的转换,就需要强制限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一个的不同参数化。”
7、泛型类型的继承规则
无论S和T有什么联系,通常Pair< S>和Pair< T>是没有什么联系的。
永远可以将参数化类型转换为一个原始类型。例如:Pair< Employee> 是原始类型Pair的一个子类。
泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。
8、通配符类型
Pair<? extends Employee> 表示任何泛型Pair类型,它的类型参数是Employee的子类。
8.1 通配符的超类型限定
通配符类型与类型限定十分相似,但是,还有一个附加的能力,即可以指定一个超类限定:? super Manage
这个通配符限制为Manage的所有超类型。
带有超类型的限定通配符,可以为方法提供参数,但不能使用返回值。
直观的讲,带有超类型限定的通配符可以向泛型对象写入,拥有子类型限定的通配符可以从泛型对象读取。
public static < T extends Comparable< T>> T min(T[] a) { … }
这样一个接口,当处理一个GregorianCalendar对象的数组时,就会出现问题,GregorianCalendar是Calendar的子类,并且Calendar实现了Comparable< Calendar>,而不是Comparable< GregorianCalendar>,所以这种情况下,我们就要用到超类型:
public static <T extends Comparable<? super T>> T min(T[] a) { ... }
8.2 无限定通配符
还可以使用无限定的通配符。
Pair<?>有如下方法:
? getFirst()
void setFirst(?)
getFirstde的返回值只能赋值给一个Object,setFirst不能被调用,甚至不能用Object调用。
Pair<?>和Pair的本质不同在于:可以用任意Object对象调用原始的Pair类的setObject方法。
8.3 通配符捕获
编写一个交换一个pair元素的方法:
public static void swap(Pair<?> p){
? t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
不能在编写代码中使用“?”作为一种类型,也就是说上述代码是非法的。
我们可以写一个辅助方法swapHelper:swapHelper是一个泛型方法
public static <T> void swapHelper(Pair<T> p){
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
public static void swap(Pair<?> p){
swapHelper(p);
}
9、反射和泛型
Class类是泛型的,例如,String.class是一个Class< String> 的实例(也是唯一实例)。
Class< T>中的方法就使用了类型参数:
public T newInstance()
返回一个实例,这个实例所属的类由默认的构造器获得。它的返回类型被声明为T,其类型与Class<T>描述的类相同,这样就免除了类型转换。
public T cast(Object obj)
如果给定的类型确定是T的一个子类,cast方法就会返回一个现在声明为类型T的对象,否则,抛出一个BadCastException异常。
public T[] getEnumConstants()
如果这个类不是enum类或者类型T的枚举值的数组,返回null
public native Class<? super T> getSuperclass();
public Constructor<T> getConstructor(Class<?>... parameterTypes)
public Class<?>[] getDeclaredClasses()
getConstructor和getDeclaredClasses返回一个Constructor<T>对象
9.1 使用Class< T>参数进行类型匹配
public static <T> Pair<T> makePair(Class<T> tClass) throws IllegalAccessException, InstantiationException {
return new Pair<>(tClass.newInstance(), tClass.newInstance())
}
调用makePair(Employee.class)
编译器可以推断出这个方法将返回一个Pair< Employee>。
9.2 虚拟机中的泛型类型信息
java泛型的卓越特性之一是在虚拟机中泛型类型的擦除。
为了表达泛型类型的声明,java.lang.reflect包中提供了一个新的接口Type:
这个接口包含下列子类型:
- Class类,描述具体类型
- TypeVariable接口,描述类型变量
- WildcardType接口,描述通配符
- ParameterizedType接口,描述泛型类或接口类型
- GenericArrayType接口,描述泛型数组