学习笔记:Java泛型
一、 为什么要使用泛型?
Java核心技术卷中写到:“使用泛型机制编写的程序代码要比那些杂乱的使用Object变量,然后在进行强制类型转换的代码具有更好的安全性和可读性。泛型(Generic)意味着编写的代码可以被很多的不同类型的对象重用”。
比如未使用泛型的ArrayList集合,只是维护一个Object类型数组。在实现中没有类型推断,只能在运行时检测错误,在获取值时必须进行强制类型转换,可能会出现ClassCastExcepyion异常。
@Test
public void test2(){
ArrayList a = new ArrayList();
a.add(132);
a.add(456);//没有进行类型检查,可以添加任意Object对象
a.add("Hello");
for (Object o :a) {
String str = (String) o;//进行强制类型转换,编译时不会报错,运行时会出现java.lang.ClassCastException异常
System.out.println(str);
}
Java SE 5.0增加了泛型机制,使用泛型的Arrsylist类,有一个类型参数
@Test
public void test1(){
Collection<Integer> c = new ArrayList<Integer>();//类型推断
c.add(12);
c.add(22);//能在编译时而不是在运行时检测错误
c.add(52);
//c.add("good"); 只能添加Integer类的对象
for (Integer o :c) { //不用进行强制类型转换,编译器已经知道o的数据类型,且运行时不会报错
System.out.println(o);
}
}
二、 定义泛型类
“一个泛型类就是具有一个或多个类型变量的类”。如何实现?引入一个或多个类型变量,用< >括起来,并放在类名的后面。
作用是什么?类定义的类形变量可以指定类的内部结构的数据类型(比如属性的数据类型,方法的返回类型,方法的形参类型,成员变量的数据类型)。
//开发一个泛型Apple类,要求有一个重量属性weight在测试类中实例化不同的泛型对象,
public class Apple<T> {
private T weight;//指定属性的数据类型
public void setWeight(T weight){//指定方法的形参类型
this.weight = weight;
}
public T getWeight(){//指定方法的返回值类型
return weight;
}
//泛型类定义构造器方式和一般类一样
public Apple(){
}
//泛型类定义构造器方式和一般类一样
public Apple(T weight){
this.weight = weight;
}
}
“用具体的类型替换类型变量就可以实例化泛型类型。”不能用基本数据类型指定类型变量,而要用其对应的包装类。
@Test
public void test1(){
Apple<String> a1 = new Apple<>("500克");
Apple<Integer> a2 = new Apple<>(500);
Apple<Double> a3 = new Apple<>(500.0);
System.out.println(a1.getWeight());//返回值类型是String类型
System.out.println(a2.getWeight());//返回值类型是Integer类型
System.out.println(a3.getWeight());//返回值类型是Double类型
}
}
三、 怎么定义泛型方法
什么是泛型方法?“带有类型参数的方法”。泛型方法可以定义在普通类中,也可以定义在泛型类中。
类型变量放在权限修饰符的后面,返回值类型的前面。
public class Arrayex {
public static <E> void toArray(E[] e, Collection<E> c){//类型变量修饰形参
for (E ex: e){//类型变量修饰局部变量
c.add(ex);
}
}
}
可以看出,泛型方法定义的类型变量可以定义方法的内部结构(形参,局部变量)以及方法的返回值类型。
调用泛型方法?“调用时,在方法名的尖括号中放入具体的类型,也可以省略,编译器有足够的信息推断出所调用的方法。
@Test
public void test2(){
Collection<String> c1 = new ArrayList<>();
String[] str1 = new String[]{"C","Python","Java","C++"};
Arrayex.<String>toArray(str1,c1);//输入第一个参数后,编译器会类型推断,指定第二个参数为容纳String类型的Collection集合
System.out.println(c1);
}
}
显然,在泛型类和泛型方法的定义和调用过程中,可以体验到类型变量在类或方法的定义中充当的Object类型,当调用时指定具体传入数据类型,帮助编译器检查传入数据类型、类型推断的作用。
四、类型变量的限定
“在类或方法中有时需要对类型变量加以约束”。对比Object类等类型指定的类或方法的结构,通常继承的子类也能满足代码内部的使用。
“对于类型变量,可以指定为任何一个类的对象,假如内部调用了对象所属类的compareTo方法,怎么才能确信T对象所属类有CompareTo方法呢?解决方案是将T限定为实现了Comparable接口。
<T extends Compareable>
表示T是其实现了Comparable接口或者是绑定类型的子类型。一个类型变量可以有多个限定。限定类型用“&”分隔,用逗号来分隔类型变量。
<T super xxxClass>
表示类型变量指定的范围为xxxClass及其父类。
//返回数組元素最大值
public static <E extends Emploee & Comparable> E max(E[] e){
if (e == null || e.length == 0) return null;
E maxplayer = e[0];
for (int i = 1; i < e.length; i++) {
if (maxplayer.compareTo(e[i])<0){
maxplayer = e[i];
}
}
return maxplayer;
}
在此方法中,类型变量限定为实现Comparable接口并且是Employee及其子类。
创建Person类、Employee类、Mannager类,三个类是多层继承关系,Person重写了compareTo方法对salary属性。
@Test
public void test1(){
Emploee[] emploees = new Emploee[5];
emploees[0] = new Emploee("1号员工",12,100);
emploees[1] = new Emploee("2号员工",13,110);
emploees[2] = new Emploee("3号员工",13,120);
emploees[3] = new Manager("1号管理",12,100,50);
emploees[4] = new Manager("2号管理",14,130,50);
Object o = Emploee.max(emploees);//返回工资最高的人
System.out.println(o);//名字 2号管理工资是 130
}
}
五、泛型变量与擦除
“无论何时定义一个泛型类,都自动提供一个相应的原始类型,类名就是删除类型参数后的泛型类名”。
擦除类型变量,原始类型会替换为第一个限定类型,如果没有给定就用Object类型来替换。
“当程序调用泛型方法时,如果擦除返回类型,编译器会插入强制类型转换。”
在虚拟机中没有泛型,只有普通的类和方法。所有的类型参数都用它们的限定类型替换。桥方法被合成来保持多态(方法类型变量擦除时,调用来自父类的方法可能会变成另外一个父类方法,不在同名同参数产生重载,与多态冲突)。
六、泛型的约束与局限性
运行时类型查询只适用于原始类型。 使用instanceOf或涉及泛型类型的强制类型转换表达式都会看到一个编译器警告。
不能实例化泛型类参数化类型的数组。
不能实例化泛型类的类型变量。
在静态属性或者静态方法中不能使用泛型类的类型变量。
不能抛出或捕获泛型类的实例。