文章目录
- 为什么要使用泛型程序设计
- 类型参数的好处
- 谁想成为泛型程序员
- 定义简单泛型类
- 泛型方法
- 类型变量的限定
- 泛型代码和虚拟机
- 类型擦除
- 转换泛型表达式
- 转换泛型方法
- 类型擦除与多态会发生冲突
- 桥方法实现多态
- 桥方法与可协变的返回类型
- 调用遗留代码
- 限制与局限性
- 泛型类型的继承规则
- 8.8 通配符类型
- 8.8.1通配符概念
- 8.8.2 通配符的超类型限定
- 8.8.3 无限定通配符
- 8.8.4 通配符捕获
- 8.9 反射和泛型
- 8.9.1 泛型 Class 类
- 8.9.2使用Class\<T>参数进行类型匹配
- 8.9.3 虚拟机中的泛型类型信息
- 8.9.4 类型字面量
- 个人理解:为什么要擦除?
为什么要使用泛型程序设计
泛型程序设计(generi c programming)意味着编写的代码可以对多种不同类型的对象重
用。
类型参数的好处
它们会让你的程序更易读,也更安全。
谁想成为泛型程序员
实现泛型类典型问题:
一个程序员可能想要将一个ArrayList<Manager>中的所有元素添加到一个ArrayList<Employee>中去。不过,当然反过来就不行了。如何允许前一个调用,而不允许后一个调用呢?
Java设计者为此发明了通配符类型。
定义简单泛型类
- 类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型。变量E表示集
合的元素类型,K和V分别表示表的键和值的类型。T、U和S表示“任意类型”。
泛型方法
- 类型变量放在修饰符的后面,并在返回类型的前面。
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 = ArrayAIg.getMiddle(3.14, 1729, 0);
编译器将把参数自动装箱为1个Double和2个Integer对象,然后寻找这些类的共同超类型。事实上,它找到了 2个超类型:Number和Comparable接口,Comparable接口本身也是一个泛型类型。在这种情况下,可以采取的补救措施是将所有的参数都写为double值。
类型变量的限定
- 限制T只能是实现了 Comparable接口。
public static <T extends Comparable> T min(T[] a)
-
为什么使用关键字extends而不是implements ?毕竟,Comparable是一个接口。下面的记法
<T extends BoundingType>表示T应该是限定类型的子类型。T和限定类型可以是类,也可
以是接口。选择关键字extends的原因是它更接近子类型的概念,并且Java的设计者也不打
算在语言中再添加一个新的关键字(如sub)。 -
一个类型变量或通配符可以有多个限定,例如:T extends Comparable & Serializable。限定类型用“&”分隔,而逗号用来分隔类型变量。
-
可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,它必须是限定列表中的第一个限定。
泛型代码和虚拟机
对于Java泛型的转换,需要记住以下几个事实:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都会替换为它们的限定类型。(类型擦除)
- 会合成桥方法来保持多态。
- 为保持类型安全性,必要时会插人强制类型转换。
类型擦除
- 对于泛型类,类型变量会被擦除,并替换为其原始类型。
Java泛型与C++模板有很大的区别。C++为每个模板的实例化产生不同的类型,这一现象称为“模板代码膨胀”。Java不存在这个问题的困扰。
- 原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为0bject。
转换泛型表达式
- 调用泛型方法时,如果擦除了返回类型,编译器会插人强制类型转换。
Pair<Employee> buddies =...;
Employee buddy = buddies.getFirst();
getFirst擦除类型后的返回类型是0bject。编译器自动插人转换到Employee的强制类型转换。也就是说,编译器把这个方法调用转换为两条虚拟机指令:
• 对原始方法Pair.getFirst的调用。
• 将返回的Object类型强制转换为Employee类型。
- 变量擦除与参数约束无关。
private T first; public void setFirst(T newValue) { first = newValue; }
擦除后private Object first; public void setFirst(Object newValue) { first = newValue; }
。但Pair<Employee>调用setFirst(T newValue)方法时,编译器要求只能传入Employee及其子类型(Manager)的对象。不可能传入一个Object对象,保证了泛型的安全性。
- 泛型安全闭环:f(T t)方法传入参数T t有严格的参数类型控制,保证了更改器方法一定是安全的。这样一来,保证了调用get方法时编译器自动进行强制转换的安全。
转换泛型方法
- 类型擦除也会出现在泛型方法中。
public static <T extends Comparable> T min(T[] a)
是整个一组方法,而擦除类型之后,只剩下一个方法:public static Comparable min(Comparable[] a)
。其中,类型参数T已经被擦除了,只留下了限定类型Comparable。
- 类型擦除与多态会发生冲突,编译器会使用桥方法。
类型擦除与多态会发生冲突
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 newValue) { first = newValue; }
public void setSecond(T newValue) { second = newValue; }
}
(1)DateInterval类是Pair的子类,Pair是个泛型类。
public class DateInterval extends Pair<LocalDate> {
//重写的方法
public void setSecond(LocalDate second) {
System.out.println("DateInterval.setScond() is called");
if(second.compareTo(getFirst())>=0)
super.setSecond(second);
}
}
(2)DateInterval类被擦除后变成:
public class DateInterval extends Pair { //Pair<LocalDate>被擦除
//重写的方法
public void setSecond(LocalDate second) {
System.out.println("DateInterval.setScond() is called");
if(second.compareTo(getFirst())>=0)
super.setSecond(second);
}
}
(3)此时,DateInterval还有一个从Pair继承的setSecond方法:
public void setSecond(Object second) // 泛型类Pair的泛型方法在JVM中被擦除成object
(4)显然,DateInterval的public void setSecond(LocalDate second)
方法,不是对Pair的public void setSecond(Object second)
方法的重写。因为参数类型不同。所以无法自动实现多态。理论上,DateInterval有两个不相关的方法。即类型擦除与多态发生了冲突。
桥方法实现多态
(1) 下面的例子可以看出,泛型实现了多态。
此处实现了多态,调用的是DateInterval.setScond()方法。只有second的日期在first之后才会给second赋值。年份换成1999年,输出的就是null。
public static void main(String[] args) {
DateInterval dateInterval = new DateInterval();
Pair<LocalDate> superDateInterval=dateInterval; // 子类对象赋值给父类变量
superDateInterval.setFirst(LocalDate.of(2000, 12, 22));
superDateInterval.setSecond(LocalDate.of(1999, 12, 22)); //not work,second为null
superDateInterval.setSecond(LocalDate.of(2008, 12, 22)); // work,second为2008-12-22
System.out.println(superDateInterval.getSecond());
}
(2) 为了解决类型擦除与多态的冲突,编译器在DateInterval类中生成了一个桥方法。
public void setSecond(Object second) { setSecond((LocalDate) second); }
DateInterval的桥方法
public void setSecond(Object second) { setSecond((LocalDate) second); }
是Pair的public void setSecond(Object second)
方法的重写。所以,多态性让superDateInterval调用的是superDateInterval的桥方法,而桥方法会调用DateInterval的public void setSecond(LocalDate second)
方法。
(3)利用反射查看桥方法
public static void main(String[] args) {
DateInterval dateInterval = new DateInterval();
Pair<LocalDate> superDateInterval=dateInterval; // 子类对象赋值给父类对象
superDateInterval.setFirst(LocalDate.of(2000, 12, 22));
//此处实现了多态,调用的是DateInterval.setScond()方法。只有second的日期在first之后才会给second赋值。年份换成1999年,输出的就是null。
superDateInterval.setSecond(LocalDate.of(2008, 12, 22));
System.out.println(superDateInterval.getSecond());
Method[] declaredMethods = dateInterval.getClass().getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(Modifier.toString(declaredMethod.getModifiers())+" "+declaredMethod.getReturnType()
+" "+declaredMethod.getName()
+"("+declaredMethod.getParameterTypes()[0]+")");
}
}
桥方法与可协变的返回类型
假设Datelnterval类也覆盖了 getSecond方法:
class Datelnterval extends Pair<LocalDate>
{
public LocalDate getSecond() { return (LocalDate) super.getSecond(); }
}
在Datelnterval类中,有两个getSecond方法:
LocalDate getSecond() // defined in Datelnterval
Object getSecond() // overrides the method defined in Pair to call the first method
- 不能这样编写Java代码(两个方法有相同的参数类型是不合法的,在这里,两个方法都没有参数)。但是,在虚拟机中,会由参数类型和返回类型共同指定一个方法。因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确地处理这种情况。
- 桥方法会调用新定义的方法
LocalDate getSecond()
。
调用遗留代码
设计Java泛型时,主要目标是允许泛型代码和遗留代码之间能够互操作。
- Swing用户界面工具包提供了一个JSlider类,有方法void setLabelTable(Dictionary table)。
- 在Java 5之前,Dictionary类实现为一个Object实例映射。Java 5把Dictionary实现为一个泛型类,不过JSlider从未更新,JSlider使用的依旧是没有类型参数,是原始类型的Dictionary类。
- 这种不匹配,会带来兼容性问题。
Dictionary<Integer, Components> labelTable = slider.getLabelTablel); // warning
警告: 确保标签表确实包含Integer和Component对象。
限制与局限性
- 不能用基本类型实例化类型参数,用对应的包装器类。
基本类型不是对象,无法擦除为object。
- 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
- 不能创建参数化类型的数组.
var table = new Pair<String>[10]; //error
4. Varargs 警告
- 不能实例化类型变量,不能在类似newT(…)的表达式中使用类型变量。
public class PairTest2
{
public static void main(String[] args)
{
Pair p1=Pair.makePair(new Supplier<String>() {
public String get()
{
return "hhh";
}
});
Pair p2=Pair.makePair(String.class);
}
}
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 newValue) { first = newValue; }
public void setSecond(T newValue) { second = newValue; }
public static <T> Pair<T> makePair(Supplier<T> constr) { //用Supplier创建
return new Pair<>(constr.get(),constr.get());
}
public static <T> Pair<T> makePair(Class<T> cl) { //用反射创建
try {
return new Pair<>(cl.getConstructor(String.class).newInstance("123"),cl.getConstructor(String.class).newInstance("456"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
- 不能构造泛型数组
理解问题:为什么
return (E) elements[n];
可以用强制类型转换。而不能:T a = new T(); return (T) a;
。
- 实际上new T()会报错,因为不能实例化类型变量,同时,(T) a这个强制类型转换也不满足强转的条件: a instanceof T为假。经过类型擦除后,new T() 变成new object() , 所以a实际是一个object对象,不能把父类对象强转成子类对象。进一步揭示了为什么不能实例化类型变量。
- 观察上面的代码,发现elements是对象变量而不是new出来的对象。elements[n]擦除为object,但elements[n]指向了某个T类型,所以可以进行强制转换。
数组父类为object,但B继承了A,那么A[]类型的引用就可以指向B[]类型的对象
- 泛型类的静态上下文中类型变量无效
静态字段属于类,两个不同对象只有一个属于类的singleInstance字段,不可能既是A类型又是B类型。
-
不能抛出或捕获泛型类的实例
-
可以取消对检查型异常的检查
-
注意擦除后的冲突
当Pair<T>重写equals方法时,即使加override注解也会报错。原因:泛型类重写equals方法时,为了实现多态,JVM会自动生成桥方法:boolean equals(Object),桥方法会调用重写的方法boolean equals(T),但由于擦除后T就是Object,所以重写的方法与巧方法冲突。、
泛型类型的继承规则
- 无论S与T有什么关系,通常,Pair<S>与Pair都没有任何关系
没有继承关系,不可能多态或者强制转型。
- 总是可以将参数化类型转换为一个原始类型。例如,Pair<Employee>是原始类型Pair的一个子类型。
会会产生类型错误:
Pair pair=new Pair<Integer>(1,2); pair.setFirst("牛");
// only a compile-time warning
运行时才会报错ClassCastException。
但失去的只是泛型程序设计提供的附加安全性,更重要的是与遗留代码交互。
- 泛型类可以扩展或实现其他的泛型类。
ArrayList<T>类实现了List<T>接口。这意味着,一个ArrayList<Manager>可以转换为一个List<Manager>。但是,如前面所见,ArrayList<Manager>不是一个ArrayList<Employee>或List<Employee>。
8.8 通配符类型
8.8.1通配符概念
-
在通配符类型中,允许类型参数发生变化。例如,通配符类型
Pair <? extends Employee>
-
类型 Pair<Manager> 是 Pair<? extends Employee〉的子类型
- <? extends T>: 可调用get方法,不能调用set方法。get方法的返回值赋值给一个T类型或者T的子类型的对象变量。
8.8.2 通配符的超类型限定
-
- <? superT>: 可调用get方法,不能调用set方法。调用set方法时,只能传递T类型的对象。get返回值只能赋给一个object。
8.8.3 无限定通配符
- Pair<?>
- get方法的返回值只能赋值给一个Object。setFirst方法不能被调用,甚至不能用Object
调用。 - Pair<?>和Pair本质的不同在于:可以用任意Object对象调用原始Pair类的setFirst
方法。
8.8.4 通配符捕获
- 通配符不是类型变量,因此,不能在编写代码中使用“?”作为一种类型。
? t = p.getFirst(); // ERROR
p.setFirst(p.getSecond());
p.setSecond(t);
- swapHelper方法的参数T捕获通配符。
public static void swap(Pair<?> p) { swapHelper(p); }
public static <T> void swapHelper(Pair<T> p)
{
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
- 编译器必须能够保证通配符表示单个确定的类型。例如,ArrayList<Pair<T>>中的T永远不能捕获ArrayList<Pair<?>>中的通配符。数组列表可以保存两个Pair<?>,其中的?分别有不同的类型。 (
有待考证
)
var ceo = new Manager("Gus Greedy", 800000, 2003, 12, 15);
var cfo = new Manager("Sid Sneaky", 600000, 2003, 12, 15);
Employee employee1=new Employee("employee1",80000,1999,11,22);
Employee employee2=new Employee("employee2",80000,1999,11,22);
var buddies = new Pair<Manager>(ceo, cfo);
ceo.setBonus(1000000);
cfo.setBonus(500000);
Manager[] managers = { ceo, cfo };
ArrayList<Pair<?>> arrayList=new ArrayList<>(); // ArrayList存储了不同类型的两个pair
arrayList.add(new Pair<Manager>(ceo,cfo));
arrayList.add(new Pair<Employee>(employee1,employee2));
PairAlg.swap(arrayList.get(1)); //可行
PairAlg.swap(arrayList.get(0)); //可行
System.out.println();
8.9 反射和泛型
- 反射允许你在运行时分析任意对象。
- 泛型类型参数你将得不到太多信息,因为它们已经被擦除了。
在下面的小节中,我们将学习利用反射可以获得泛型类的哪些信息。
8.9.1 泛型 Class 类
Class类是泛型的。Class类的常见方法:
8.9.2使用Class<T>参数进行类型匹配
Class<T>参数用于解决无法new T()的局限,创建T对象。
8.9.3 虚拟机中的泛型类型信息
- 原始的Pair类知道它源于泛型类Pair<T>,尽管一个Pair类型的对象无法区分它是构造为Pair<String>还是Pair<Employee>。 即可通过反射获得泛型类的所有信息。
- java.lang.reflect包中的接口Type
...
public static void printType(Type type, boolean isDefinition)
{
if (type instanceof Class)
{
var t = (Class<?>) type;
System.out.print(t.getName());
}
else if (type instanceof TypeVariable) //类型变量
{
var t = (TypeVariable<?>) type;
System.out.print(t.getName());
if (isDefinition)
printTypes(t.getBounds(), " extends ", " & ", "", false);
}
else if (type instanceof WildcardType) //通配符
{
var t = (WildcardType) type;
System.out.print("?");
printTypes(t.getUpperBounds(), " extends ", " & ", "", false);
printTypes(t.getLowerBounds(), " super ", " & ", "", false);
}
else if (type instanceof ParameterizedType) // 泛型类或接口
{
var t = (ParameterizedType) type;
Type owner = t.getOwnerType();
if (owner != null)
{
printType(owner, false);
System.out.print(".");
}
printType(t.getRawType(), false);
printTypes(t.getActualTypeArguments(), "<", ", ", ">", false);
}
else if (type instanceof GenericArrayType) // 泛型数组
{
var t = (GenericArrayType) type;
System.out.print("");
printType(t.getGenericComponentType(), isDefinition);
System.out.print("[]");
}
}
...
8.9.4 类型字面量
个人理解:为什么要擦除?
- 一个泛型类和泛型方法里,由于参数是T类型,不可能调用任何具体类的个性化函数。比如String.subString方法。因为当T不是String而是Integer时,T没有subSTring方法。
- 擦除后变为原始类型,也保证了不会调用任何具体类的个性化函数。
- 泛型具有的是各种类型的共同之处抽取出来的,不关心个性化的东西,核心在不同类型的共同之处。
- 调用泛型方法时,传入一个String,擦除后,调用泛型方法时,参数类型是object。而泛型方法类不会有任何String个性化方法,保证了object可以完成泛型方法,不可能出现调用String的subSTring方法,而object无法提供的情况。而当返回时,想要String对象,编译器会自动强制类型转换。
- 假如说,你想个性化泛型类的方法,只能继承,使用多态。