目录
1. 什么是泛型
2. 泛型背景及其语法规则
3. 泛型类的使用
3.1 语法
3.2 示例
3.3 类型推导(Type Inference)
4. 裸类型(Raw Type)
4.1 说明
5. 泛型如何编译的
5.1 擦除机制
5.2 为什么不能实例化泛型类型数组
6. 泛型的上界
6.1 上界语法产生的背景
6.2 语法
6.3 示例
6.4 复杂示例
7. 泛型方法
泛型方法的语法
8. 通配符
8.1 通配符解决什么问题
8.2 通配符的上界
8.3 通配符下界
9. 包装类
9.1 基本数据类型和对应的包装类
9.2 装箱和拆箱
9.3 自动装箱和自动拆箱
1. 什么是泛型
一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。----- 来源《Java编程思想》对泛型的介绍。
public static void func(int a) {}
对于func()
这个代码来说, 去传参只能传int类型的数据.
而泛型就是可以传类型. 也就是说, 如果这里是泛型, 就可以传各种数据类型, 包括自定义的数据类型在内, 都可以作为参数进行传参.
泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数化。
泛型就是 对类型进行参数化.
2. 泛型背景及其语法规则
实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?
答: 我们可以把数组定义为Object数组, 因为所有类都默认继承于Object类.
// JVM会做一些改变 把1 2 3 4 这些数据放到这个数组当中(怎么放的涉及装箱拆箱, 在后文解析)
Object[] array = {1, 2, 3, 4, "hello", "ok"};
可以看到, 因为是array是Object数组, 所以什么类型的数据都可以放.
但是这样写会带来一个问题, 如果要写以下代码:
String[] arr2 = (Object)array; // 报错, array中放的不一定都是String类型的数据
String[] array3 = (String[]) new Object[10]; // 报错, 不能确定Object中都是String类型的数据
实际开发中不要尝试把Object类型的数组强制转换成String类型的数组.
再回到原来的问题:
// 实现一个类
class MyArray {// 类中包含一个数组成员public Object[] obj = new Object[10];// 使得数组中可以存放任何类型的数据public void setVal(int pos, Object val) {obj[pos] = val; // 在 obj 的 pos位置 放 val}// 也可以根据成员方法返回数组中某个下标的值public Object getPos(int pos) {return obj[pos];}
}
现在我们调用一下上面这两个方法.
public class Test {public static void main(String[] args) {MyArray myArray = new MyArray();myArray.setVal(0, 10);myArray.setVal(1, "苹果");myArray.setVal(2, 10.5);}
}
可以看到, 这个类确实非常通用, 将需要的东西往里面放就可以了, 但是接下来就会有问题了.
我们尝试获取一下1下标的值, 由于它是一个字符串:
String str = myArray.getPos(1);
但是编译器直接报红了:
图中提到, 需要类型是String, 但是返回类型是Object. 我们知道, 父类类型给到子类类型是向下转型, 所以在getPos()返回的是Object, 所以我们尝试加上强转为String, 就不会报错了.
String str = (String) myArray.getPos(1);
但是这对开发人员来说很不友好, 如果此时要取2号元素, 那么就必须再看一下它的数据类型是什么, 这降低了开发的效率.
总结两个问题:以上代码实现后我们发现:
- 当前数组是可以存放任何类型数据
- 1号下标本身就是字符串,但是却编译报错。必须进行【强制类型转换】
虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望这个数组只能够持有一种数据类型。而不是同时持有这么多类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象, 让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。
我们在类名的后面加上<T>
:
class MyArray<T> {}
<T>
在这里相当于是一个占位符, 表示当前类是一个泛型类.
T只是一个"参数", 类似于形参变量, 也可以是K, 也可以是V, 只是名字而已.
其实泛型在这里不同的字母代表的是不同的意思, 只不过在这里是为了增强可阅读性.
【规范】类型形参一般使用一个大写字母表示,常用的名称有:
E 表示 Element
K 表示 Key
V 表示 Value
N 表示 Number
T 表示 Type
S, U, V 等等 - 第二、第三、第四个类型
接下来我们把所有的Object改成T:
class MyArray<T> {//public T[] obj = new T[10]; // 把所有的Object改成T, 但是此时这里会报错public T[] obj = (T[]) new Object[10]; // 暂且先写成这样, 但是依然不太好, 后续解释public void setVal(int pos, T val) {obj[pos] = val;}public T getPos(int pos) {return obj[pos];}
}
此时在main中修改代码为:
public class Test {public static void main(String[] args) {MyArray<Integer> myArray = new MyArray<Integer>(); // 改为这样的代码后, 下面代码报错了myArray.setVal(0, 10);myArray.setVal(1, "苹果"); // errmyArray.setVal(2, 10.5); // errString str = (String) myArray.getPos(1); // err}
}
可以发现, 在存元素的时候, 第一个没有报错, 而第二, 三个报错了, 也就是说这里指定了当前把类型作为参数传递的时候, 传的是Integer类型. 所以就能放整型. 于是:
public class Test {public static void main(String[] args) {MyArray<Integer> myArray = new MyArray<Integer>();myArray.setVal(0, 10);myArray.setVal(1, 2);myArray.setVal(2, 6);int a = myArray.getPos(1);// 同理, 要在MyArray中放字符串, 就指定String类型MyArray<String> myArray2 = new MyArray<String>();myArray2.setVal(0, "hello");myArray2.setVal(1, "hello2");String str = myArray2.getPos(0);}
}
可以看到, 当传的类型参数不一样的时候, setVal()的第二个参数就能放不同类型. 所以在我们以前传的是变量的值, 而现在则相当于传递的是类型.
那么相比前面来说, 存的时候, 它自动进行了类型的检查, 看存放的类型是否是Integer/String, 如果不是就会报错.而且, 在取数据的时候, 就不需要进行强转了. 这便是泛型所存在的两个最大意义.
泛型存在的两个最大的意义:
1. 存放元素的时候, 会进行类型的检查
2. 取出元素的时候, 会自动进行类型的转换(即 不需要再进行类型的强转).
以上两步都是 在编译的时候完成的(运行的时候, 是没有泛型的概念的).
泛型主要是编译时期的一种机制, 这种机制有一个概念 —— 擦除机制.
class 泛型类名称<类型形参列表> {// 这里可以使用类型参数
}class ClassName<T1, T2, ..., Tn> {// 一个泛型类的参数列表可以指定多个类型
}
class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {// 这里可以使用类型参数
}class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {// 可以只使用部分类型参数
}
比如:
// 拿到类型后可以定义变量
class TestDemo<K,V> {K k; // 通过 K类型 定义kV v; // 通过 V类型 定义v
}
注意: main中的<>
中填写的必须是类类型, 不能是基本类型!
3. 泛型类的使用
3.1 语法
泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
3.2 示例
MyArray<Integer> list = new MyArray<Integer>();
注意:泛型只能接受类,所有的基本数据类型必须使用包装类!
3.3 类型推导(Type Inference)
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写:
MyArray<Integer> list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 String
4. 裸类型(Raw Type)
4.1 说明
裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList 就是一个裸类型
MyArray list = new MyArray();
注意: 我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制
小结:
- 泛型是将数据类型参数化,进行传递
- 使用<T> 表示当前类是一个泛型类。
- 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换
5. 泛型如何编译的
5.1 擦除机制
以下代码是前文代码放到一起, 我们在IDEA中Build它, 然后查看jclasslib(需要安装 jclasslib bytecode viewer
插件).
class MyArray<T> {//public T[] obj2 = new T[10]; // 把所有的Object改成T, 但是此时这里会报错public T[] obj = (T[]) new Object[10];public void setVal(int pos, T val) {obj[pos] = val;}public T getPos(int pos) {return obj[pos];}
}public class Test {public static void main(String[] args) {MyArray<Integer> myArray = new MyArray<Integer>();myArray.setVal(0, 10);myArray.setVal(1, 2);myArray.setVal(2, 6);int a = myArray.getPos(1);MyArray<String> myArray2 = new MyArray<String>();myArray2.setVal(0, "hello");myArray2.setVal(1, "hello2");String str = myArray2.getPos(0);}
}
先来看setVal(), 描述符中, V代表的是返回值, I代表int, 后面的就是T的类型, 为Object.
可以看到T是Object, 而并不是其他的类型.
getPos()也是Object.
所以可以知道, 擦除机制就是在编译的时候, 把所有的T擦除成了Object.
Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息。
有关泛型擦除机制的文章介绍:Java泛型擦除机制之答疑解惑 - 知乎
5.2 为什么不能实例化泛型类型数组
为什么T[] obj2 = new T[10];
是不对的, 编译器会报错, 而T[] obj = (T[]) new Object[10];
不报错? 那么其实后面这个代码也是不对的.
假设T[] obj2 = new T[10];
编译器能够通过, 那么就意味着可以提供一个方法, 返回T[]
.
public T[] getobj2() {return obj;
}
T[] obj = (T[]) new Object[10];
是不对的, 只不过这里编译器不会报错, 但是我们可以让它报错.
public class Test {public static void main(String[] args) {MyArray<Integer> myArray = new MyArray<Integer>();Integer[] tmp = myArray.getObj2();}
}
T[]擦除成了Object, 那么它要给到Integer[], 显然是不能的, 所以会报错.
数组和泛型之间的一个重要区别是它们如何强制执行类型检查。具体来说,数组在运行时存储和检查类型信息。然而,泛型在编译时检查类型错误。
通俗讲就是:返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Person,运行的时候,直接转给Integer类型的数组,编译器认为是不安全的。
正确的代码(了解):
class MyArray<T> {public T[] array;public MyArray() {}/*** 通过反射创建,指定类型的数组* @param clazz* @param capacity*/public MyArray(Class<T> clazz, int capacity) {array = (T[])Array.newInstance(clazz, capacity);}public T getPos(int pos) {return this.array[pos];}public void setVal(int pos,T val) {this.array[pos] = val;}public T[] getArray() {return array;}
}public class Test {public static void main(String[] args) {MyArray<Integer> myArray1 = new MyArray<>(Integer.class,10);Integer[] integers = myArray1.getArray();}
}
6. 泛型的上界
6.1 上界语法产生的背景
写一个泛型类,类中有一个方法 求一个数组当中的最大值?
我们先来看:
// 泛型类 ALg<T>
class Alg<T> {// 方法: 找到最大值, 传进来的数组为泛型数组T[]public T findMax(T[] array) {T max = array[0]; // 假设最大值是这个数组的 0下标for (int i = 0; i < array.length; i++) {if (max < array[i]) { // 报错!!!max = array[i]; // max 更新为 array[i]}}return max; // 最后的最大值就在max中存}
}
在IDEA中写下以上代码之后, 会看到if (max < array[i]) {
报错.
T一定是一个引用类型, 它不会是基本类型.
如果是基本类型的数组:
public static void main(String[] args) {int[] array = {1, 2, 3, 4};// 通过下标找到元素, 从而使用 大于小于 进行比较System.out.println(array[0] > array[1]); }
但是, 对于findMax()
来说, 传进来的一定是引用类型,这里不能通过大于小于进行比较.于是我们希望可以使用重写compareTo()
方法的办法, 可是:
class Alg<T> {public T findMax(T[] array) {T max = array[0]; for (int i = 0; i < array.length; i++) {//if (max < array[i]) {// T此时传的是Integer, 但是会发现没有compareToif (max./* 没有compareTo? */ (array[i]) < 0) {max = array[i]; }}return max; // 最后的最大值就在max中存}
}// main代码同上, 省略...
由前文的擦除机制我们可以知道, T在编译的时候擦除成了Object, 而Object :
那么它就不具备compareTo这个功能. 所以在这里我们就需要来做一个校验, T如果要去比较, 那么就必须是实现了Comparable接口, 于是就产生了这样一个语法Alg<T extends Comparable<T>>
.
修改代码:
class Alg<T extends Comparable<T>> {// 方法: 找到最大值, 传进来的数组为泛型数组T[]public T findMax(T[] array) {T max = array[0]; // 假设最大值是这个数组的 0下标for (int i = 0; i < array.length; i++) {//if (max < array[i]) {// T此时传的是Integer, 但是会发现没有compareToif (max.compareTo(array[i]) < 0) {max = array[i]; // max 更新为 array[i]}}return max; // 最后的最大值就在max中存}
}
然后max
就可以.
出compareTo
了. 此时代码就不报错了. 即, 要比较大小, max要调用compareTo, 就要【使用泛型的上界这个语法】.
也就是说class Alg<T extends Comparable<T>> {
含义是, T类型实现了Comparable接口, 这个接口也指定了类型是为T.
于是:
public class Test { public static void main(String[] args) {Alg<Integer> alg = new Alg<>();Integer[] array = {1, 2, 3, 4};Integer ret = alg.findMax(array);System.out.println(ret); // 4}
}
接下来我们再举一个反例:
class Person {}public class Test {public static void main(String[] args) {Alg<Person> alg = new Alg<>();}
}
会发现, <Person>
报错了.
因为在检查的时候发现, Alg有<T extends Comparable<T>>
, 而Person没有实现Comparable接口.
所以要让它不报错:
class Person implements Comparable<Person> {public int age;public Person(int age) {this.age = age;}@Overridepublic int compareTo(Person o) {return this.age - o.age;}@Overridepublic String toString() {return "Person{" +"age=" + age +'}';}
}public class Test {public static void main(String[] args) {Alg<Person> alg = new Alg<>();// 定义一个Person[]Person[] people = {new Person(10), new Person(15)};Person person = alg.findMax(people); // 调用findMax的时候就会调用compareToSystem.out.println(person);}
}
在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
6.2 语法
class 泛型类名称<类型形参 extends 类型边界> {// ...
}
6.3 示例
public class MyArray<E extends Number> {// ...
}
只接受 Number 的子类型作为 E 的类型实参
MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型
了解: 没有指定类型边界 E,可以视为 E extends Object
6.4 复杂示例
public class MyArray<E extends Comparable<E>> {// ...
}
E必须是实现了Comparable接口的
7. 泛型方法
对 找数组最大值 的优化:
我们发现, 需要调用findMax(), 必须要new Alg对象. 那么能否不new这个对象, 也就是说, 如果把这个方法变成static, 那么是否就可以不new这个对象?
class Alg2<T extends Comparable<T>> {public static T findMax(T[] array) {T max = array[0];for (int i = 1; i < array.length; i++) {//if(max < array[i]) {if (max.compareTo(array[i]) < 0) {max = array[i];}}return max;}
}
会看到 , 整个代码的T都报错了.
为什么加了static报错? 或者说, 没加static之前, T是如何确定的?
没加static之前, new Alg传了实参Integer, 加了static之后, 意思就是, 要通过Alg.findMax()
直接调用, 显然问题在于这里没有机会传实参, 所以Alg2
上加<T extends Comparable<T>>
或者不加都是没用的, 那, 不加<T extends Comparable<T>>
, 不就更没用了吗, 所以, 这里从语法上要这么改:
class Alg2 {public static <T extends Comparable<T>> T findMax(T[] array) {T max = array[0];for (int i = 1; i < array.length; i++) {//if(max < array[i]) {if (max.compareTo(array[i]) < 0) {max = array[i];}}return max;}
}
就不报错了. 所以当方法是静态的时候, 它不依赖于对象, 这个泛型的传参就要在static后加上<T extends Comparable<T>>
, 所以此时这个方法就成为了一个泛型方法.
public class Test {public static void main(String[] args) {Integer[] array = {1, 2, 3, 4};// 给这个泛型方法传递一个 类型的实参Integer ret = Alg2.<Integer>findMax(array); // <Integer>实际上是省略的, 这里写了出来System.out.println(ret);}
}
泛型方法的语法
方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表) { ... }
8. 通配符
?
用于在泛型的使用,即为通配符
8.1 通配符解决什么问题
通配符是用来解决泛型无法协变的问题的,协变指的就是如果Student
是Person
的子类,那么List<Student>
也应该是List<Person>
的子类。但是泛型是不支持这样的父子类关系的。
泛型 T
是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围.
class Message<T> {private T message; //消息public T getMessage() {return message;}public void setMessage(T message) {this.message = message;}
}public class Test {public static void main(String[] args) {Message<String> message = new Message();message.setMessage("haha");fun(message);}public static void fun(Message<String> temp) {System.out.println(temp.getMessage());}
}
以上程序会带来新的问题,如果现在泛型的类型设置的不是String,而是Integer.
public class Test {public static void main(String[] args) {Message<Integer> message = new Message();message.setMessage(99);fun(message); // 出现错误,只能接收String}public static void fun(Message<String> temp) {System.out.println(temp.getMessage());}
}
我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改。这种情况就需要使用通配符?
来处理.
范例:使用通配符
此时使用通配符" ?
"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
在"?"的基础上又产生了两个子通配符:
? extends 类:设置泛型上限
? super 类:设置泛型下限
8.2 通配符的上界
语法:
<? extends 上界>
<? extends Number>
//可以传入的实参类型是Number或者Number的子类
如图描述了一个继承关系, 当我们写出Plate<? extends Fruit>
这样的代码的时候, 能在Plate中放的就只能有Fruit或者它的子类.
class Food {}class Fruit extends Food {}class Apple extends Fruit {}class Banana extends Fruit {}class Plate<T> { // 设置泛型 shift+f6private T plate;public T getPlate() {return plate;}public void setPlate(T plate) {this.plate = plate;}
}public class Test {public static void main(String[] args) {Plate<Apple> plate1 = new Plate<>();plate1.setPlate(new Apple());fun1(plate1);Plate<Banana> plate2 = new Plate<>();plate2.setPlate(new Banana());fun1(plate2);} // 此时无法在fun函数中对temp进行添加元素,// 因为temp接收的是Fruit和他的子类,此时存储的元素应该是哪个子类无法确定。// 所以添加会报错!但是可以获取元素。public static void fun(Plate<? extends Fruit> temp) {/*不能放东西temp.setPlate(new Apple());temp.setPlate(new Banana());temp.setPlate(new Fruit());*/Fruit fruit = temp.getPlate();}
}
通配符的上界,不能进行写入数据,只能进行读取数据。
8.3 通配符下界
语法:
<? super 下界>
<? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型
class Food {}class Fruit extends Food {}class Apple extends Fruit {}class Banana extends Fruit {}class Plate<T> { // 设置泛型 shift+f6private T plate;public T getPlate() {return plate;}public void setPlate(T plate) {this.plate = plate;}
}public class Test {public static void main(String[] args) {Plate<Fruit> plate1 = new Plate<>();plate1.setPlate(new Fruit());fun(plate1);Plate<Food> plate2 = new Plate<>();plate2.setPlate(new Food());fun(plate2);}public static void fun(Plate<? super Fruit> temp) {temp.setPlate(new Apple());temp.setPlate(new Banana());temp.setPlate(new Fruit());//不能存放Fruit的父类//Fruit fruit = temp.getPlate(); 不能取数据 因为无法知道取出的数据类型是什么?}
}
通配符的下界,不能进行读取数据,只能写入数据。
9. 包装类
在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。
9.1 基本数据类型和对应的包装类
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
除了 Integer 和 Character, 其余基本类型的包装类都是首字母大写。
9.2 装箱和拆箱
int i = 10;// 装箱操作,新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
Integer ii = Integer.valueOf(i);
Integer ij = new Integer(i);// 拆箱操作,将 Integer 对象中的值取出,放到一个基本数据类型中
int j = ii.intValue();
9.3 自动装箱和自动拆箱
可以看到在使用过程中,装箱和拆箱带来不少的代码量,所以为了减少开发者的负担,java 提供了自动机制。
int i = 10;Integer ii = i; // 自动装箱
Integer ij = (Integer)i; // 自动装箱int j = ii; // 自动拆箱
int k = (int)ii; // 自动拆箱
【面试题】
下列代码输出什么,为什么?
public static void main(String[] args) {Integer a = 127;Integer b = 127;Integer c = 128;Integer d = 128;System.out.println(a == b);System.out.println(c == d);
}
这是因为在Java中,对于 Integer`类型的对象,有一个内置的缓存范围为 -128 到 127。当你创建一个在这个范围内的`Integer 对象时,JVM 会尝试重用现有的对象而不是创建一个新的对象。这被称为整数缓存机制。
所以,当 a 和 b 的值都在 -128 到 127 的范围内时,它们会引用相同的对象,因此 a == b 会返回 true。而当 c 和 d 的值为 128 时,它们超出了缓存范围,因此会创建两个不同的对象,所以 c == d 返回 false。