- 定义简单泛型类
public class Pair
{
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 newValue)
{
first = newValue;
}
public void setSecond(T newValue)
{
second = newValue;
}
}
//
Pair nXX;
- 泛型方法
class ArrayAlg
{
public static T getMiddle(T... a)
{
return a[a.length / 2];
}
}
String middle = ArrayAlg.getMiddle("John", "Q", "Public");
// 编译器可从参数推断时,可以省略
- 类型变量的限定
class ArrayAlg
{
public static T min(T[] a)
{
if(a == null || a.length == 0) return null;
T smallest = a[0];
for(int i = 1; i < a.length; i++)
{
if(smallest.compareTo(a[i]) > 0)
{
smallest = a[i];
}
return smallest;
}
}
}
// 对泛型类型变量T设置一个限定
public static T min(T[] a) ...
// 一个类型变量或通配符可以有多个限定
T extends Comparable & Serializable
泛型代码和虚拟机
虚拟机没有泛型类型对象--所有对象都属于普通类。
- 类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型。
类型变量会被擦除,并替换为其限定类型。对无限定的变量则替换为Object。
// Pair的原始类型如下所示
public class Pair
{
private Object first;
private Object second;
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 newValue)
{
first = newValue;
}
public void setSecond(Object newValue)
{
second = newValue;
}
}
原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为Object。
- 转换泛型表达式
编写一个泛型方法调用时,如擦除了返回类型,编译器会插入强制类型转换。
Pair buddies = ...;
Employee buddy = buddies.getFirst();
getFirst擦除类型后的返回类型是Object。
编译器自动插入转换到Employee的强制类型转换。
也即,编译器把这个方法调用转换为两条虚拟机指令:
1.对原始方法Pair.getFirst的调用
2.将返回的Object类型强制转换为Employee类型。
当访问一个泛型字段时,也要插入强制类型转换。
// 会在结果字节码中插入强制类型转换
Employee buddy = buddies.first;
- 转换泛型方法
类型擦除也会出现在泛型方法中。
public static T min(T[] a)
是整个一组方法,而擦除类型之后,只剩下一个方法
public static Comparable min(Comparable[] a)
方法的擦除带来了两个复杂问题。
class DateInterval extends Pair
{
public void setSecond(LocalDate second)
{
if(second.compareTo(getFirst()) >= 0)
{
super.setSecond(second);
}
}
}
var interval = new DateInterval(...);
Pair pair = interval;
// 若按c++的泛型,这里产生动态绑定,调用DateInterval的setSecond
// java的泛型在类型擦除实现下,基类的setSecond(Object)会由于类型匹配而不参与考虑
// 为解决此问题,编译器在DateInterval类中生成一个桥方法
// public void setSecond(Object){ setSecond((LocalDate)second);}
pair.setSecond(aDate);
// 桥方法可能会变得更奇怪
// 假设DateInterval类也覆盖了getSecond方法
class DateInterval extends Pair
{
public LocalDate getSecond(){return (LocalDate)super.getSecond();}
}
// 在DateInterval类中,有两个getSecond方法
// LocalDate getSecond()
// Object getSecond()
在虚拟机中,会由参数类型和返回类型共同指定一个方法。
故,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确地处理这种情况。
// 一个方法覆盖另一个方法时,可以指定一个更严格的返回类型
public class Employee implements Cloneable
{
public Employee clone() throws CloneNotSupportedException{ ... }
}
Object.clone和Employee.clone方法被称为有协变的返回类型。
实际上,Employee类有两个克隆方法
Employee clone()
Object clone()
合成的桥方法会调用新定义的方法。
java泛型转换,
1.虚拟机中没有泛型
2.所有的类型参数都会替换为它们的限定类型
3.合成的桥方法来保持多态
4.为保持类型安全性,必要时会插入强制类型转换
- 调用遗留代码
限制与局限性
- 不能用基本类型实例化类型参数
没有Pair只有Pair
- 运行时类型查询只适用于原始类型
- 不能创建参数化类型的数组
var table = new Pair[10]; // error
// 因为擦除后table为Object[]
// 数组会记住它的元素类型,如试图存储其他类型的元素,会抛出一个ArrayStoreException异常。
- Varargs警告
向参数个数可变的方法传递一个泛型类型的实例
public static void addAll(Collection coll, T... ts)
{
for(T t : ts)
{
coll.add(t);
}
}
Collection> table = ...;
Pair pair1 = ...;
Pair pair2 = ...;
addAll(table, pair1, pair2);
为调用这个方法,java虚拟机需建立一个Pair数组,而我们已经知道不可创建泛型数组。
此时,会得到警告。
抑制警告
// 1.为包含addAll调用的方法增加注解@SuppressWarnings("unchecked")
// 2.用@SafeVarargs注解addAll方法本身
- 不能实例化类型变量
// 以下是非法的
public Pair()
{
first = new T();
second = new T();
}
// 可以用
Pair p = Pair.makePair(String::new);
makePair方法接收一个Supplier,这是一个函数式接口
public static Pair makePair(Supplier constr)
{
return new Pair<>(constr.get(), constr.get());
}
比较传统的解决方法是通过反射调用Constructor.newInstance方法来构造泛型对象。
public static Pair makePair(Class cl)
{
try
{
return new Pair<>(
cl.getConstructor().newInstance(),
c1.getConstructor().newInstance());
}
catch(Exception e)
{
return null;
}
}
Pair p = Pair.makePair(String.class);
- 不能构造泛型数组
数组本身也带有类型,用来监控虚拟机中的数组存储,这个类型会被擦除。
public static T[] minmax(T... a)
{
T[] mm = new T[2];// error
}
如果数组仅仅作为一个类的私有实例字段,
则可将这个数组的元素类型声明为擦除的类型并使用强制类型转换。
String[] names = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");
构造表达式String::new指示一个函数,给定所需长度,会构造一个指定长度的String数组。
public static T[] minmax(IntFunction constr,
T... a)
{
T[] result = constr.apply(2);
}
// 老式
public static T[] minmax(T... a)
{
var result = (T[])Array.newInstance(a.getClass().getComponentType(), 2);
...
}
// ArrayList的toArray
Object[] toArray()
T[] toArray(T[] result)
- 泛型类的静态上下文中类型变量无效
不能在静态字段或方法中引用类型变量
禁止使用带有类型变量的静态字段和方法。
- 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类的对象。
泛型类扩展Throwable甚至都是不合法的。
catch子句不能使用类型变量。
- 可以取消对检查型异常的检查
java异常处理原则是,需为检查型异常提供一个处理器。
但可以利用泛型取消这个机制。
static void throwAs(Throwable t) throws T
{
throw (T)t;
}
假设上述方法包含在接口Task中,如有一个检查型异常e,并调用
Task.throwAs(e);
编译器会认为e是一个非检查型异常。
- 注意擦除后的冲突
泛型类型被擦除后,不允许创建引发冲突的条件。
通配符类型
- 通配符类型
在通配符类型中,允许类型参数发生变化。
Pair extends Employee>
上述通配符类型表示任何泛型Pair类型。它的类型参数是Employee的子类。
public static void printBuddies(Pair p)
{
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName()
+ " and "
+ second.getName()
+ " are buddies.");
}