泛型程序设计
思维导图
提示:以下是本篇文章正文内容,下面案例可供参考
8.1 类型参数
构造函数中可以省略泛型类型
省略的类型可以从变量的类型推断得出,编译器会利用这个信息,不需要进行强制类型转换,编译器知道返回值的类型为String,而不是Object
类型参数的魅力在于:使得程序具有跟好的可读性和安全
ArrayList<String> files = new ArrayList<>();
8.1.2通配符类型的引出
ArrayList<Manager>中的所有元素可以添加到ArrayList<Employee>中
原因:多态,Manager是Employee的子类
反过来则不行,出于灵活性发明了新概念,“通配符类型”
8.2定义简单泛型类
可以定义Pair类,可多个不同的变量类型,其中第一个域和第二个域使用不同的类型
public class Pair<T>{
...
}
public class Pair<T,U>{
...
}
提示:类型变量使用大写类型,E表示集合的元素类型,K和V为键值,T、U、S较为常用——任意类型
8.3 泛型方法
class ArrayAlg{
// 定义方法:
// 前T为方法的泛型类型,后T为方法的返回值
public static <T> T getMiddle(T... a){
return a[a.length / 2];
}
public static void main(String[] args) {
// 调用1
String middle1 = ArrayAlg.<String>getMiddle("a","b","c");
System.out.println(middle1);//b
// 调用2
String middle2 = ArrayAlg.getMiddle("a","b","c");
System.out.println(middle2);//b
}
}
1)定义格式:
修饰符列表 <泛型参数类型> 返回值类型 方法名(方法参数列表){
//方法语句
return ;
}
2)调用格式:
-
object.<泛型参数>方法名(方法参数列表);
-
object.方法名(方法参数列表);
大多数情况下可以,但任意报错:编译器寻找参数列表的共同超类型
8.4 类型变量的限定
限定T只属于某个类或者接口下
public static <T extends Comparable> T min(T[] a){...}
限定了T是Comparable及其实现类
一个类型变量或者通配符可以有多个限定,使用 & 分隔,且至多一个类,如果用类做限定,必须在限定列表的第一个。
<T extends Comparable & Serializable>
8.5 泛型类型和虚拟机
虚拟机没有泛型类型对象,所有对象都属于普通类,所以我们定义的泛型类型和方法都会在虚拟机编译后擦除。
8.5.1 类型擦除
擦除规则:
- 定义一个泛型时都自动提供了一个相应的原始类型(raw type)
[删去类型参数后的泛型类型名] - 把 类型 替换为 限定类型 ,无 限定类型 的变量用 Object
Pair<T> 的原始类型
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;
}
}
编译器将没有方法的接口放在边界列表的末尾:
- 经编译前:<T extends Serializable & Comparable>
- 因为Serializable没有方法,所以放在后面
- 经编译后:<T extends Comparable & Serializable>
8.5.2翻译类型表达式
当程序调用泛型方法时,由于类型擦除
如 public T getFirst();
变成 public Object getFirst();
编译器会自动插入强制类型转换,将返回值Object强制转换
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
编译器把这个方法调用翻译为两条虚拟机指令
- 对原始方法Pair.getFirst调用
- 返回的Object类型强制转换为Employee类型
即使是公有的域也会进行强制类型转换,如
Employee buddy = buddies.first;
8.5.3 翻译类型方法
编译器自动生成合成桥方法,不会导致泛型翻译后的方法重载多态冲突。
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都用它们的限定类型替换
- 桥方法被合成用来保持多态
- 为保持类型安全性,必要时插入强制类型转换
8.5.4 使用注解使警告消失
@SuppressWarning(“unchecked”)注解会关闭方法中所有代码的检查
// 在警告语句前标注
@SuppressWarning("unchecked")
Dictionary<Integer,Components> labelTable = slider.getLabelTable(); // No warning
//标注整个方法
@SuppressWaring("unchecked")
public void configureSlider(){
...
}
8.6 约束和局限性
8.6.1 不能用基本类型实例化类型参数
简而言之就是:不能用类型参数代替基本数据类型
全得是包装类
8.6.2 运行时类型查询只适用于原始类型
无法使用 instanceof 判断,都无法编译通过
public static void main(String[] args) {
Pair<String> stringPair = new Pair<String>("aaa","bbbb");
System.out.println(stringPair instanceof Pair<String>);//Error
System.out.println(stringPair instanceof Pair<T>);//Error
ArrayList<Manager> managerList = new ArrayList<>();
System.out.println(managerList instanceof ArrayList<?>);//always true
System.out.println(managerList instanceof ArrayList);//always true
}
最后的pair调用的方法取决于最后一次的赋值声明,
Pair<Integer> pair,所以调用的是Pair<Integer>的方法
若类型和方法不符合会在运行时报错,并不会编译不通过
public static void main(String[] args) {
Pair<String> stringPair = new Pair<String>("aaa","bbbb");
Pair IntPair = new Pair<>(111,222);
@SuppressWarnings("unchecked")
Pair<Integer> pair = (Pair<Integer>)IntPair;// Warning原生泛型--强转-->精确泛型
System.out.println(pair.getFirst());//调用哪个方法取决于最后一次声明
}
- 使用 instanceof 检查会得到错误
- getClass方法总是会返回原始类型(raw type)
不管Pair<String>或是Pair<Integer>,其对象调用getClass,总会返回Pair.Class
8.6.3 不能创建参数化类型的数组
Pair<String>[] table = new Pair<String>[10];//Error
Pair<String>[] table = new Pair[10];//Warning -- OK
//不安全的创建泛型数组方法
Pair<String>[] table = (Pair<String>[])new Pair<?>[10];//这种方式可以的
//安全而有效的创建泛型数组方法,使用new ArrayList<Pair<T>>(),Array中存储的都是Pair<T>
ArrayList<Pair<String>> pairs = new ArrayList<Pair<String>>();
- 只是不允许创建数组————new Pair<String>[10]
- 声明是合法的
- 使用原生类型是可以创建数组的
- 最好使用 new ArrayList<Pair<T>>(); 来创建泛型数组
8.6.4 Varargs 警告
@SafeVarargs 注解标注方法可以消除创建泛型数组有关限制
public static void main(String[] args) {
Pair<String> pair1 = new Pair<String>("aaa","bbbb");
Pair<String> pair2 = new Pair<String>("aaaa","bbbbb");
Collection<Pair<String>> table = new ArrayList<>();
addAll(table,pair1,pair2);
System.out.println(table);
}
@SafeVarargs
public static <T> void addAll(Collection<T> coll,T... ts){
for (T t:ts
) {coll.add(t);
}
}
8.6.5 不能实例化类型变量
不能使用 new T();
不能使用 T.class;
那么要怎么才能用泛型创建对象?使用Class<T>类
// 用一个静态方法来实例化Pair<T> 中的成员变量
// 核心是 c.newInstance()
public static <T> Pair<T> makePair(Class<T> c){
try{
return new Pair<>(c.newInstance(),c.newInstance());
}catch (Exception e){
return null;
}
}
//调用
Pair<String> pair = makePair(String.class);
Class类本身就是泛型,所以String.class 是一个class<String>的一个实例,且是唯一实例,因此makePair方法能够推断出pair类型
8.6.7 泛型类的静态上下文中类型变量无效
不能再静态域或方法中引用类型变量
泛型禁止用于静态域
泛型禁止用于静态方法作为返回值和参数
普通方法:不带<T>
泛型可以作为静态泛型方法的参数
泛型方法:带<T>
8.6.8 不能抛出或捕获泛型类的实例
- 泛型类不能继承Exception,Throwable
public class Problem <T> extends Exception{} //Error
public class Problem <T> extends Throwable{} //Error
- 泛型类型参数无法用catch捕获
// 这个是
public static <T extends Throwable> void dowork(Class<T> c){
try{
...
}catch(T t){//无法捕获 Error
...
}
}
- 可以在异常处理器中使用泛型类型变量
public static <T extends Throwable> void doWork(T t) throws T{
try{
}catch (Throwable throwable){
t.initCause(throwable);
throw t;
}
}
8.6.10 类型擦除后的冲突
一个类型变量不能同时成为两个接口类型的子类,这个接口类型是同一接口的不同参数化
如:
class Employee implememts Comparable<Employee>{}
class Manager extends Employee implements Comparable<Manager>{} //Error
相当于擦除类型后,子类又实现了同一接口!
8.7 泛型类型的继承规则
泛型和数组的重大区别:
- 可以将Manager[] 数组赋值给 Employee[]变量
- 但是Manager[]里的元素都是Manager,把Manager赋值给超类,但是不能把超类加入该数组
- 这是数组的特别保护
Manager[] managers = new Manager[10];
Employee[] employees = managers;
//java.lang.ArrayStoreException
employees[0] = new Employee();
某一泛型类型 是 生泛型(raw) 的子类
不同的泛型类型虽然继承自一个raw type 泛型
但是它们之间没有如何关系
8.8 通配符类型
1)<?> 非受限通配
写:X 例外set(null)却可以,set(object)却不行
读:✔ 返回声明为Object
2)<? extends T> 受限通配
写:X
读:✔ 返回声明为 T的超类
3)<? super T> 下限通配
写:✔ 写入类型为 T的子类
读:✔ 返回声明为Object
问题提出:由于Pair<Employee>和Pair<Manager>是两个不同的类型,所以该方法不能接收来自Pair<Manager>的参数
方法1:单一泛型类型
public static void printBuddies1(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println("First:" + first+"\n"+"Second:"+second);
p.setFirst(new Employee(2));
p.setSecond(new Employee(1));
}
方法2:使用受限通配,向下匹配(给定上限)
- 接收泛型类型方面:能够接收更多的参数类型,向下接收
- 读方面:可以读取get,因为返回的读取类型为(上限~其子类) 泛型类型范围的公共超类——(Object~上限)
读取的接收声明为(Object~上限)皆可满足 - 写方面:不能写入set
//采用通配符,Pair<Employee>和Pair<Manager>,让参数能够接收{泛型为Employee的子类}的Pair
public static void printBuddies2(Pair<? extends Employee> p) {
//上限通配
//优点:可以使用get,可读取
Employee first = p.getFirst();
Employee second = p.getSecond();
//缺点:无法使用set,不可写入
//p.setFirst();
System.out.println("First:" + first+"\n"+"Second:"+second);
}
方法3:使用下限通配,向上匹配(给定下限)
- 接收泛型类型方面:能够接收更多的参数类型,向上接收
- 读方面:可以读取get,但是接收声明为泛型类型范围的公共超类,只能是Object
所以读取的接收声明为Object - 写方面:可以写入,写入类型为泛型类型范围下限的子类
所以写入的类型为 (下限~其子类)
public static void printBuddies3(Pair<? super Manager> p) {
//下限通配,最低的就是Pair<Manager>,可传入Pair<Employee>
//缺点:可以读取,但是丢失了类型,不知道是Manager的哪一个超类,固用Object吸收
Object first = p.getFirst();
Object second = p.getSecond();
//优点:可以写入,但是写入会小小取小,只能写入比Manager更小的
p.setFirst(new Manager(2,2));
p.setSecond(new Manager(1,1));
System.out.println(p);
}
8.8.4 通配符捕获
通配符不是类型变量, 因此, 不能在编写代码中使用“ ?” 作为一种类型
所以我们要怎么明确我们接收到的通配类型是什么呢?
方法:通过增加辅助方法——泛型方法,用泛型T来捕获通配类型?
// 未捕获前:
? t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
// 捕获后(泛型方法辅助):
public static <T> void swapHelper(Pair<T> p) {
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
在这种情况下,swapHelper 方法的参数 T 捕获通配符。它不知道是哪种类型的通配符,
但是,这是一个明确的类型