泛型方法
泛型类就是有一个或多个类型变量的类。以一个简单的Pair类为例,这个类使我们可以只关注泛型而不用为数据存储的细节而分析
定义一个简单的泛型类
package com.zhd.PairTest;
//作为一个泛型类
public class Pair<T> {
//类型变量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;
}
}
package com.zhd.PairTest;
//泛型类就相当于普通类工厂
public class PairTest1 {
public static void main(String[] args){
String[] words={"Mary","had","a","boyfriend"};
Pair<String> mm=ArrayAlg.minmax(words);//调用这个类的静态方法
System.out.println("min ="+mm.getFirst());
System.out.println("max ="+mm.getSecond());
// System.out.println("a".compareTo("B"));//简单判断下验证上面的值输出是否正确 是对的!
// System.out.println("a".compareTo("b"));//可以看出小写字符比大写字符在字典顺序中要靠后
// String middle=ArrayAlg.<String>getMiddle("John","Q","Public");
String middle=ArrayAlg.getMiddle("John","Q","Public");//类型参数<String>也可以省略 编译器可以推导出你参数的类型
System.out.println("调用带类型参数的方法"+middle);
// double DBmiddle=ArrayAlg.getMiddle(3.14,5,9);
}
}
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++){
//compareTo方法如果字符串相同返回0 按照字典顺序如果第一个字符串比第二个字符串靠前 则返回一个负整数 反正则返回正整数
if(min.compareTo(a[i])>0)//最小值比它大
min=a[i];//取最靠前的字符串
if(max.compareTo(a[i])<0)//最大值比它小
max=a[i];//取最靠后的字符串
}
return new Pair<>(min,max);//first is min, second is max
}
public static <T> T getMiddle(T... a){//还可以定义一个带类型参数的方法 注意格式 ...应该代表可能是数组也可能是普通类
return a[a.length/2];
}
}
我们还可以定义一个泛型方法,这是一个带有类型参数的方法
注意的是类型变量<T>
是放在修饰符public static
后面,并在返回类型T
前面
public static <T> T getMiddle(T... a)
泛型方法可以在普通类中定义,也可以在泛型类中定义
在普通类中定义
public static <T> T getMiddle(T... a){
//还可以定义一个带类型参数的方法 注意格式 ...应该代表可能是数组也可能是普通类
return a[a.length/2];
}
在泛型类中定义
public <T> T arrayGetMiddle(T...a){
return a[a.length/2];
}
//类型参数<String>也可以省略 编译器可以推导出你参数的类型
String middle=ArrayAlg.getMiddle("John","Q","Public");
var pair=new Pair<>();
String arrayMiddle=pair.arrayGetMiddle("John","Q","Public");
System.out.println("调用带类型参数的方法(普通类中定义的)"+middle);
System.out.println("调用带类型参数的方法(泛型类中定义的)"+arrayMiddle);
结果截图
注意的是,大部分情况
String middle=ArrayAlg.<String>getMiddle("John","Q","Public");
类型参数也可以省略 编译器可以推导出你参数的类型
String middle=ArrayAlg.getMiddle("John","Q","Public");
但是要注意有一种情况, double DBmiddle=ArrayAlg.getMiddle(3.14,5,9);
运行如下
也就是说编译器把参数自动装箱成为1个Double和两个Integer对象,然后寻找这些类的共同超类型,事实上它找到了2个超类型:Number和Comparable接口,就无法判断到底用哪个,解决方法是把所有参数写成double值
double DBmiddle=ArrayAlg.getMiddle(3.14,5.0,9.0);
这样就可以运行了
类型变量的限定
有时类或方法需要对类型变量加以约束。
下面以一个计算一个数组最小元素为例,这里我们给smallest的变量名定义是T但是我们用到了smallest.compareTo,也就是说这个T这个类型所属的类必须要有一个compareTo的方法,那又怎么知道T所属的类有这个方法呢?这里我们就必须做出限定,对类型变量T设置一个限定 <T extends Comparable>
,反过来说,泛型方法min只能在实现了Comparable接口的类(如String,LocalDate等)的数组上调用,如果你的类没有CompareTo这个方法,调用min就会产生编译错误
当然了为什么实现了Comparable接口是用关键字extends而不是implements,毕竟<T extends Comparable>
这里面的Comparable是个接口呀。下面记法如下
<T extends BoundingType>
- 表示T是限定类型(bounding type)的子类型,T和限定类型可以是类,也可以是接口,选择关键字extends的原因是它更接近子类型的概念。
- 一个类型变量或通配符可以有多个限定,如:限定类型用“&”分隔,逗号“,”用来分隔类型变量
< T extends Comparable & Serializable,U extends Comparable>
class ArrayAlg3{
public static <T extends Comparable> 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;
}
}
在Java继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类,如果有一个类作为限定,它必须是限定列表的第一个限定
泛型代码和虚拟机
类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除,并替换为其限定类型(或者对于无限定的变量则替换成Object)
如Pair<T>
的原始类型如下:
package com.zhd.PairTest;
//作为一个泛型类
public class Pair {
//类型变量T 在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型
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;
}
public <Object> Object arrayGetMiddle(Object...a){
return a[a.length/2];
}
}
就是T全部变成Object 然后类名去掉了参数类型 那其实它就变成了一个普通的类
在程序中可以包含Pair<String> ; Pair<LocalDate>
但类型擦除后他们都会变成原始的pair类型。
这里是这样的规矩
- 如果T没有限定 则类型擦除全部把T变成 Object ,如果有类型限定,则用第一个限定来替换类型变量
转换泛型表达式(这里其实就是在讲内部的运行)
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换,
这里做个例子,
Pair<Employee> buddies=...;
Employee buddy=buddies.getFirst();
getFirst擦除类型之后返回Object,编译器会自动插入转换到Employee的强制类型转换。也就是说编译器把这个方法转换成两条虚拟器命令
- 对原始Pair.getFirst 的调用
- 将返回的Object类型强制转换为Employee类型
转换泛型方法
类型擦除也会出现在泛型方法中。
public static <T extends Comparable>T min(T[] a)
擦除后
public static Comparable min(Comparable[] a)
那这种类型擦除不是会和多态冲突吗,子类继承超类的方法,签名都一样,如果方法擦除了,那到底要调用子类的方法还是超类的方法呢。这里为了解决这个问题,编译器会在DateInterval类中生成一个桥方法
public void setSecond(Object second){setSecond(LocalDate second);//桥方法}
var interval=new DateInterval(...);
Pari<LocalDate> pair=interval;//OK assignment to superclass
pair.setSecond(aDate)
运行pair.setSecond(aDate)会调用上面重新构造的桥方法
总结
总之,对于Java泛型转换只需记住以下几个事实
- 虚拟器中没有泛型,只有普通的类和方法
- 所有类型参数都会替换为它们的限定类型
- 会合成桥方法来保持多态
- 为了保持类型安全性,必要时会插入强制类型转换
泛型的限制与局限性
- 不能用基本类型实例化类型参数,可以理解,因为擦除后是Object类型,Object不能存储int double这些基本类型
- 运行时类型查询只适用于原始类型,就是你不能用
if(a instanceof Pair<String>)
这种去判断,因为上面说了,会有类型擦除,如果你试图查询一个对象是否属于泛型类型,你就会的到一个编译器错误,当然了,getclass返回的肯定也是擦除后的Pair.class,所以不能查询 - 不能创建参数化类型的数组 如
var table=new Pair<String>[10];//ERROR
如果要收集参数化类型对象,拜托,ArrayList:ArrayList<Pair<String>>
用它吧,不香吗
ArrayList<Pair<String>> table=new ArrayList<Pair<String>>();
Pair<String> pair1=new Pair<String>("听妈妈的话","夜曲");
Pair<String> pair2=new Pair<String>("超人不会飞","迷迭香");
table.add(pair1);
table.add(pair2);
System.out.println(table.get(0).getFirst()+" "+table.get(0).getSecond());
System.out.println(table.get(1).getFirst()+" "+table.get(1).getSecond());
- Varages警告 向参数个数可变的方法传递一个泛型类型实例,因为刚才我们不是说了泛型类型是不能创建参数化类型的数组的,那以addAll方法为例,
public static <T> void addAll(Collection<T> coll,T...ts){
for(T t:ts)
coll.add(t);
}
这样的话ts其实就是数组了,那他的类型是T诶,我们不是说这种类型参数是不能构建数组的,所以,我们需要一个注解,@SafeVarargs
经过调试发现在运行到coll.add(t);
这个t是加不进去的,也就是return false ,最终数组里面一个pair都加入不了 那当我们给了个注解之后,好像也没啥用 ,调试是这样的
当运行到add的时候 就会转到这个方法中,返回false,也就是根本无法把这个类型对象放到这个数组中
t什么的都生成了,就是加不进去,不要理这个吧,主要还是上面那只数组列表来搞这个类型参数数组。
- 不能构造泛型数组
- 泛型类的静态上下文中类型变量无效
- 不能抛出或捕获泛型类的实例
- 可以取消对检查型异常的检查