泛型
案例
如果现在要录入学生的成绩,内容包括课程名,课程编号和课程成绩,但课程成绩根据不同的课程会有不同的表示方法,一种为分数制(0-100),一种为评价制(优秀,良好…..)。那么如何进行编码才能满足两种成绩类型的存储?
一种是利用Object类存储成绩,这样在创建对象时,Integer和String可以自动装箱,但是要获得某一学生的成绩时,就需要利用强制类型转换获得,但我们事先并不知道这个成绩的表示方法,因此会造成很大的不便
public class Score{
String name;
String id;
Object grade;
}
为了解决这种问题,JDK 5中引入了泛型
泛型
注意
- 泛型中不能写基本数据类型,但是基本类型的数组可以,因为数组本身是引用类型
- 指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型
- 如果不写泛型,类型默认是Object
- 泛型不具备继承性,但是数据具备继承性
例如,若 Zi类是 Fu类 的子类:
public static void method(ArrayList<Fu> arr){}
ArrayList<Fu> arr1 = new ArrayList<>();
ArrayList<Zi> arr2 = new ArrayList<>();
method(arr1);
method(arr2);//会报错
此时,若想使 method(arr2) 不报错,可以把 method 定义中的 <Fu> 换成 <?>
即通配符?, ? 表示不确定的类型,可以进行类型的限定:
- ? extends E:表示可以传递E / E所有的子类类型
- ? super E:表示可以传递E / E所有的父类类型
好处
- 统一数据类型
- 把运行时期的问题提前到了编译时期,避免了强制类型转换可能出现的异常
泛型类
类似于C++中的模版,在定义泛型类时,要在类名后面加 <> ,尖括号里面写的就是你要使用的泛型的类型名(类型参数),可以随便取,但一般使用大写字母 T E ,如果类中有多个泛型,用逗号隔开即可
定义
public class Score<T>{
String name;
String id;
T grade;
}
创建
仍然使用 new 创建,但是要注意,要在类名后加上 <> ,尖括号内写上使用泛型的数据类型,并且 new 后面的类名也要加上 <> ,此时尖括号内的数据类型可写可不写
Score s1<Integer> = new Score<>("数据结构与算法","CS03",95);
Score s2<String> = new Score<>("计算机组成原理","CS05","优秀");
注意
- 类中的静态方法不能使用泛型变量
泛型将数据类型的确定控制在了编译阶段,在编写代码的时候就能明确泛型的类型,如果类型不符合,将无法通过编译。
所以泛型是在具体使用一个对象时才能确定其数据类型,而类中的静态方法不依附于类,因此不能使用泛型变量
public class Score<T>{
String name;
String id;
T grade;
public static void test(){
T g1; //错误
}
}
- 在类中的成员方法中使用泛型变量时,由于其类型还未确定,此时该变量类型为Object类。但也可以通过强转使其变成某一特定的类
- 通配符 “
?” 表示“某种未知的类型”使用?通配符可创建一个可接收任意类型的泛型对象
Test<?> test;
只要写成 Test<?>,编译器就会把所有跟该未知类型有关的操作“降级”成 Object,防止破坏类型安全:
Object o =test.getvalue();//用Object类的对象接收t的值
- 泛型内也可以写泛型
Test<Test<Integer>> test;
泛型与多态
不仅是类,泛型还可以用在抽象类,接口,记录类,密封类,继承中
//抽象类
public abstract class Person<T>{
T grade;
}
//接口
public interface Study<T>{
T study();
}
//继承
public class A<T>{T name;}
public class B extends A<String>{}
对于使用泛型的接口,当子类实现该接口时:
- 实现类直接给出具体类型
- 实现类延续泛型(此时类也要使用泛型):
//直接明确类型
public class Test implements Study<Integer>{
@Override
public Integer study(){
return 1;
}
}
//继续使用泛型
public class Test<T> implements Study<T>{
@Override
public T study(){
return null;
}
}
泛型方法
方法中也可以使用泛型,当某个方法(无论是是静态方法还是成员方法)需要接受的参数类型并不确定时,我们也可以使用泛型来表示
<> 的位置放在返回类型前面:
main(){
test("Hello World"); //输出"Hello World"
test(1); //输出1
public static <T> void test(T obj){
sout(T);
}//返回类型也能写T
}
在很多工具类的方法中,都使用到了泛型方法
例如, Arrays 的排序方法:
Integer[] arr = {1,4,2,5,6};
Arrays.sort(arr,new Comparator<Integer>{
//Comparator为比较器,这里自定义了比较器
@Override
public int compare(Integer o1, Integer o2)
return o1 - o2;
}
}//在JDK21中,sort不是这样写的
可以自定义sort为从大到小,只需要把 return o1 - o2 改为 return o2 - o1 就行了
为什么?
在刚学的时候我也很迷惑,C中我知道自定义比较器时可以 return o1<o2 ,这样就从小到大了,但是这里返回的是int,要根据返回值 >0 <0 =0 做出排序的判断
我们首先要知道的是,如果返回值
0 ——> o1在后面
- <0 ——> o1在前面
- =0 ——> o1,o2顺序不变
因此,根据 return 的不同,就能确定比较函数到底是从小到大还是从大到小
例如,
当返回的是
o1-o2时,如果 o1 = 5 ,o2 = 1,那么o1 - o2 >0,此时o1要放在后面,也就是从小到大了
当返回的是
o2-o1时,如果o1 = 5,o2 = 1,那么o2 - o1<0,此时o1要放在前面,也就是从大到小了
当然,也可以用Lambda表达式简化:
Arrays.sort(arr,(o1,o2) ->(o1 - o2);
//从小到大
注意
如果泛型方法中的返回值为泛型,但是无参数,调用时可以有以下几种方法(记得指明泛型的类型):
public static <T> T test(){
return null;
}
main(){
Main.<String>test();//使用Main类调用test方法
String str = test();//可以推断类型
Score score = new Score();
score.<String>test();//通过类的对象调用
}
泛型的界限
上界
使用泛型时可以通过 extends 关键字规定泛型类只能为某个类或者其子类的类型
例如:
public class Score<T extends Number>{
T value;
public Score(T value){
this.value=value;
}
public T getValue(){
return value;
}
}
此时 T 的类型只能为 Number 或者 Number的子类(因Number 为抽象类,实例化对象时不能用 Number),并且此时 T 还能使用 Number 类中的方法
泛型通配符 ?也支持泛型的界限:
Score<? extends Number> score=new Score(90);
此时使用这个对象的泛型成员:
main(){
Score<? extends Number> score=new Score(90);
Number n = score.getalue();
}
下界
使用 super 关键字规定泛型类只能为某个类或者其父类的类型
但是**下界仅适用于通配符,**对于类型变量来说不支持
使用的不多
类型擦除
这部分主要看泛型的底层实现到底是怎么样的
其实,正如我们在案例中所想的一样,泛型在编译之后,确实会使用 Object 类(有上界时就是处于最上方的类)和强制类型转换
例如,对于下面的代码:
public class Test<T>{
T value;
public Test(T value){
this.value=value;
}
}
main(){
Test<String> test = new Test("优秀");
String str = test.value;
}
编译之后,通过反编译观察为:
public class Test{
Object value;
...
}
main(){
Test test = new Test("优秀");
String str = (String)test.value;
}
因此,泛型其实仅仅是在编译阶段进行类型检查,当程序在运行时,并不会真的去检查对应类型,所以说哪怕是我们不去指定类型也可以直接使用:
main(){
Test test = new Test();//默认原始类型Object,不过此时编译器会给出警告
}
不过,我们思考一个问题,既然继承泛型类之后可以明确具体类型,那么为什么@Override不会出现错误呢?我们前面说了,重写的条件是需要和父类的返回值类型和形参一致,而泛型默认的原始类型是Object类型,子类明确后变为其他类型,这显然不满足重写的条件,但是为什么依然能编译通过呢?
public class Derived extends Test<String>{
@Override
String test(String s) {
return null;
}
}
编译之后通过反编译进行观察,实际上是编译器帮助我们生成了一个桥接方法用于支持重写:
public class Derived extends A {
public Object test(Object obj) { //这才是重写的桥接方法
return this.test((String) obj); //桥接方法调用我们自己写的方法
}
public String test(String str) { //我们自己写的方法
return null;
}
}
由此带来的限制
类型擦除机制其实就是为了方便使用后面集合类(不然每次都要强制类型转换)同时为了向下兼容(老版本JDK)采取的方案。因此,泛型的使用会有一些限制:
- 使用
instanceof类型判断时,不允许使用泛型,只能使用原始类型
Test<String> test = new Test<>();
sout(test instanceof Test<String>);//错误,但不知道为什么IDEA不会报错,<>里面换成别的类型就报错了
原因:泛型使用类型擦除机制。这意味着在编译后,泛型信息会被移除,因此 Score<String> 和 Score<Object> 在运行时被视为相同。这导致在使用 instanceof 时不能使用泛型参数来区分不同的实例。
- 泛型类型不支持创建参数化类型数组
Test<String>[] test = new Test<String>[10]; //错误
只不过只是把它当做泛型类型的数组还是可以用的:
Test<String>[] test = new Test[10]; //错误
test[0] = new Test<String>("1");
这里 new Test<>中的类型只能写 String 类型
但是由于类型擦除,实际上运行时这个Score数组就是被视为 Score[] 的,为什么只能存储 Score<String> 呢?这是因为:
- 尽管
Score<String>[]在运行时被视为Score[],但这并不意味着可以在这个数组中存储任何类型的Score实例。Java 的数组是协变的,但泛型并不是,为了保证类型安全,Java 编译器会在编译期检查类型。(协变和逆变详见下面) - 当你尝试将
Score<Integer>的实例赋值给Score<String>[]时,编译器会认为这是一种类型不匹配,因为Score<Integer>和Score<String>被视为两个不同的类。从类型上讲,Score<Integer>并不是Score<String>的子类,虽然两者都来自于Score类。
原始类型可以创建:
Test[] test = new Test[10];
协变、逆变、抗变
引入
当我们有一个 Test 泛型类时:
public class Test<T>{
T value;
public Test<T>(T value){
this.value = value;
}
}
此时定义一个 String 和 Object类型的 Test 对象:
main(){
Test<String> test1 = new Test<>("1");
Test<Object> test2 = new Test<>("2");
}
如果我们想令 test2 = test1 ,是否可以呢?
在前面的数组部分,一个 String 类型的数组是可以赋值给一个 Object 类型的数组的(发生了隐式类型转换)
但是在泛型,这个操作是不成立的
为什么?
泛型类型的形变
泛型类型 Test<T> 存在以下几种形变:
- 协变 (Covariance):因为Integer是Number的子类,所以
Test<Integer>同样是Test<Number>的子类,可以直接转换 - 逆变(Contravariance):跟上面相反,
Test<Number>可以直接转换为Test<Integer>,前者是后者的子类 - 抗变 (Invariant):
Test<Integer>跟Test<Number>没半毛钱关系,无法互相转换
- 抗变
而Java的泛型默认是抗变的,因此引入中的例子不成立,而数组是协变的
为什么泛型要设置为抗变?
其实是为了类型安全,这里给出具有协变性质的数组说明:
String[] str = {"AAA","BBB"};
Object[] obj = str;
Object[0] = 666;
这里将String类型的数组赋值给了Object类型的数组,随后又令Object的第一个下标对应内容赋值为 666
但是666为 Integer 类,它并不是 String 类,尽管没有编译错误,程序在运行时会出现ArrayStoreException异常
这就是协变带来的缺点,它可能会导致我们使用一个错误的类型进行存取。
- 协变
当我们在使用通配符时,如果规定上界( extends ),就会产生协变性质
Test<? extends Number> test = new Test<>(10);
但是这样存在风险,因为上界是 Number ,所以我们可以使任何其子类的数据存入 test 中:
test.value = 1.5;
导致我们为一个 Integer 类型的变量,赋值了一个 Double 类型的结果,这显然是错误的。
因此,泛型中的协变具有一些限制:
不允许修改或添加任何元素(除了`null`),读取可以正常得到T或其子类型(主要用于读取数据)
- 逆变
当我们在使用通配符时,如果规定下界( super ),就会产生逆变性质
Test<? super Number> test = new Test<>(null);
此时,只要是任何其父类都可以直接转换为? super Number
但是此时 Number 的子类其实也是可以赋值的:
Test<? super Number> test = new Test<>(10);
new Score<>(10)
- 10 → 自动装箱 → 类型为
Integer。 - 菱形推断会推断出完整的泛型实参是
Integer,于是得到Score<Integer>。 - 然而
Score<Integer>并不是Score<? super Number>的子类型,这里之所以能直接赋值,是因为菱形推断会把实参直接推断成菱形左侧的类型,即编译器看见左侧是Score<? super Number>,它会 就地推断成Score<Number>,于是右侧实际上是new Score<Number>(Integer.valueOf(10)),这样才兼容。
逆变的限制:
允许修改或添加T及其子类,但是读取被限制为只能读取Object类型的结果(主要用于写入数据)
总结
- 协变: 不允许修改或添加任何元素(除了
null),读取可以正常得到T或其子类型(主要用于读取数据) - 逆变: 允许修改或添加T及其子类,但是读取被限制为只能读取Object类型的结果(主要用于写入数据)
- 抗变: 修改和读取均不受限制(读写通用)
(实际上到了后面的集合类中,如果类型为协变或是抗变,某些操作同样会受到限制。)