泛型
一、泛型定义
1.泛型引入
在正式讨论泛型之前,我们需要明白泛型存在的意义是什么。这里笔者提出一个问题:能否实现一个类:类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?
看到这个问题,基础好的读者可能会回答:用Object
定义数组即可。不得不说这是一个非常好的点子,因为我们知道Object
类是所有类的父类,哪怕是基本类型也会转成对应的包装类。Talk is cheap, show me the code~
class MyArray {
public Object[] array = new Object[10];
public Object getValue(int pos) {
return this.array[pos];
}
public void setValue(int pos, Object val) {
if(pos < array.length) {
this.array[pos] = val;
}
}
}
public class Test {
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.setValue(0, 1);
myArray.setValue(1, 2.0);
myArray.setValue(2, 'a');
myArray.setValue(3, "abc");
myArray.setValue(4, new String[2]);
// 注意下面两行
String str = (String)myArray.getValue(3);
String[] arr = (String[])myArray.getValue(4);
}
}
如上所示,我们可以正常将元素存入MyArray
这个类中,但是在取出元素的时候,我们却需要注意强转。这就不太符合我们的使用习惯了,我们能不能不需要强转就获得数据呢?毕竟,现在只是有限个元素,我们能够根据存入的元素去判断强转成什么类型。而且在实际开发中,如果要对这样的类进行抽象,我们还是希望:不同实例能够存放不同类型的数据,同一个实例存放同一个类型的数据。
面对上面两个诉求,各位读者应该能感受到创造泛型的目的了吧。**所谓泛型:就是数据类型的泛化,即数据类型参数化。泛型类相当于一个容器,同一个容器能够持有不同类型的对象,而且这样的容器可以有多个。如果我们让数据类型参数化之后,就能够利用编译器来帮助我们对数据类型做检查。**这一段话并不那么容易理解,下面我们改造一下上方的代码,让各位读者能够对照概念理解。
class MyArray<T> { // 注解1
public T[] array = (T[])new Object[10]; // 注解3
public T getValue(int pos) {
return this.array[pos];
}
public void setValue(int pos, T val) {
if(pos < array.length) {
this.array[pos] = val;
}
}
}
public class Test {
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<Integer>(); // 注解2
myArray1.setValue(0,0);
myArray1.setValue(1,1);
System.out.println(myArray1.getValue(1));
MyArray<String> myArray2 = new MyArray<>();
myArray2.setValue(0,"abc");
myArray2.setValue(1,"hello");
System.out.println(myArray2.getValue(1));
}
}
接下来咱们就对上面的代码进行一些解释。
- 注解1:代码是如何实现 数据类型参数化 的呢?
我们能够看到**MyArray
这个类名之后多了<T>
这样的一个东西**。**<T>
是一个占位符,代表了当前这个类是泛型类,而且泛型类中的成员除了能够使用正常的数据类型之外,还能够使用类型参数列表中出现的数据类型,即T类型
。**这时数据类型就如同调用方法时传入的一个参数一样,我们就实现了 数据类型参数化 这个目的了。
- 注解2:如何利用编译器对数据做 类型检查 呢?
**类型检查是针对引用的。**谁是引用,就用这个引用调用泛型方法,然后就会对这个引用调用的方法进行类型检测,这跟它真正引用的对象无关。
- 注解3:为什么
new Object[10]
没有被写成new T[10]
呢?
**因为在Java
中是不能够new
泛型数组的,数组在 new
时不能确定类型,那么就无法在内存开空间,所以编译器直接强制规定无法创建泛型实例。**而接触过擦除机制的读者可能会疑惑,这两种写法有什么区别呢?被擦除之后本质上是相同的,这样写有什么意义呢?我们应该如何创建类型T
的数组?实际上,这两种写法都不是真正规范的写法,这个坑我们最后再填。
2.泛型的好处
(1)提升了程序的健壮性和规范性
(2)编译时检查添加元素的类型,提高了安全性
(3)减少了类型转换的次数,提高效率
(4)在类声明时通过一个标识可以表示属性类型、方法的返回值类型、参数类型
二、泛型语法
1.基本语法
通过上面的示例,我们已经接触到一些泛型的语法了,接下来我们正式介绍泛型的相关语法。
// 1.泛型类的定义,注意:类型参数列表【必须是引用类型】,不能够是简单类型。
class 泛型类名<类型参数列表> {}
// 2.泛型类的实例化
泛型类名<类型参数列表> 泛型类的引用 = new 泛型类名<类型参数列表>();
// 3.类型推导下的实例化:编译器能够根据上下文推导出类型实参
泛型类名<类型参数列表> 泛型类的引用 = new 泛型类名<>();
// 4.裸类型:跟Object类一样使用了。兼容原有API的产物,免得因为原API是非泛型而报错。
泛型类名 泛型类的引用 = new 泛型类名();
对于类型参数列表这里多说几句,习惯来讲,一些特定的字母,代表的是特定的含义。
比如:T
就代表了 Type
,即数据类型;E
表示ElementK
;K
表示Key
;V
表示Value
;N
表示Number
。
2.泛型上界
(1)泛型上界的产生
为什么我们需要泛型上界呢?因为擦除机制会将所有的泛型都擦成Object
类型,**但是有时候Object
类型不好进行一些操作,我们需要直接擦成其他的类型。**这就需要用到泛型上界,它的语法形式如下:
class 泛型类名<类型参数 extends 类型边界> {}
笔者举一个简单的示例:
class MyArray<T extends Number> {
public T[] array = (T[])new Object[10];
public T getValue(int pos) {
return this.array[pos];
}
public void setValue(int pos, T val) {
if(pos < array.length) {
this.array[pos] = val;
}
}
}
public class Test {
public static void main(String[] args) {
MyArray<Integer> myArray1 = new MyArray<>();
myArray1.setValue(0,0);
myArray1.setValue(1,1);
System.out.println(myArray1.getValue(1));
// error
MyArray<String> myArray2 = new MyArray<>();
myArray2.setValue(0,"abc");
myArray2.setValue(1,"hello");
System.out.println(myArray2.getValue(1));
}
}
在这个示例中,**为什么myArray2
会报出语法错误呢?**在这个示例中,泛型T
只能是Numer
类及其子类,而String
既不是Number
类本身,也不是它的子类。
推广到整个泛型上界则是:类型参数必须是类型边界本身 及 其子类。
(2)一个复杂的泛型上界示例
上面这示例比较简单,接下来展示一个比较复杂的示例。在展示这个示例之前,请各位读者先思考一下:如何编写一个泛型类,使得能够找出数组的最大值?
部分读者可能会把代码写成下面这的形式:
为什么这样直接比较不行呢?因为T[]
并不一定是基本类型中的数值类型,直接这样比较会有安全问题,不符合规范。正确的做法其实是要接上Comparble
接口类,代码如下所示:
class MyArray<T extends Comparable<T>> {
public T findMaxValue(T[] array) {
T maxValue = array[0];
for (int i = 0; i < array.length; i++) {
if(array[i].compareTo(maxValue) > 0) {
maxValue = array[i];
}
}
return maxValue;
}
}
3.泛型方法
(1)普通泛型成员方法
上面所展示的只是普通的成员方法,如果我们需要写一个泛型方法应该如何改造呢?泛型方法的语法格式如下:
访问修饰限定符 <类型参数列表> 返回值类型 方法名(形参列表)
接下来我们直接将上方的普通成员方法改造成普通泛型方法吧~
class MyArray<T extends Comparable<T>> {
public <T extends Comparable<T>> T findMaxValue(T[] array) {
T maxValue = array[0];
for (int i = 0; i < array.length; i++) {
if(array[i].compareTo(maxValue) > 0) {
maxValue = array[i];
}
}
return maxValue;
}
}
public class Test {
public static void main(String[] args) {
Integer[] arr = {1,2,3,21,54,12,65,78};
MyArray<Integer> myArray = new MyArray<>();
System.out.println(myArray.findMaxValue(arr));
}
}
(2)静态泛型成员方法
正常的泛型方法就这么改造完毕了,而对应的当然还有静态类型的泛型成员方法。
按照以往的思路,咱们直接给普通泛型成员方法加上static
关键字就能改造完成,但是出了一些意外:
这个报错翻译过来的意思是:“MyArray.this”
不能从静态上下文引用。为什么会有这个报错呢?因为静态方法是属于类本身而不是类实例对象的,上图静态方法所使用的T
其实就来自于MyArray<T extends Comparable<T>>
实例化之后的那个T
。因此,如果要写一个静态泛型方法,泛型类上的占位符其实可以省略,因为根本不会用上。
那如何是静态泛型方法成为泛型的方法呢?具体语法形式如下:
访问修饰限定符 static<类型参数列表> 返回值类型 方法名(形参列表)
按照语法所说,可以写出如下代码:
class MyArray {
public static<T extends Comparable<T>> T findMaxValue(T[] array) {
T maxValue = array[0];
for (int i = 0; i < array.length; i++) {
if(array[i].compareTo(maxValue) > 0) {
maxValue = array[i];
}
}
return maxValue;
}
}
public class Test {
public static void main(String[] args) {
Integer[] arr = {1,2,3,21,54,12,65,78};
// 注意:可以省略 <Integer> ,编译器能够推导出数据类型。
int max = MyArray.<Integer>findMaxValue(arr);
System.out.println(max);
}
}
4.通配符
(1)通配符的产生
在泛型中我们会看到**?
的使用,这其实是通配符**。通配符是用来解决泛型无法协变的问题。什么是协变呢?所谓协变指的是:如果Student
是Person
的子类,那么对应的List<Student>
是List<Person>
的子类。但是泛型无法协变,也就说:在泛型的语法里List<Student>
与List<Person>
不是父子类关系。
无法协变本质上是在表明:类型参数只参与类型的检查,但不参与类型的组成。类型的检查其实对于类加载中的验证阶段,编译阶段则是类加载的解析阶段,运行阶段是 初始化+使用 的阶段。因为擦除机制的存在,所有的类型参数都被擦成了Object
,如果这都参与类型组成就没有意义了。
我们可以进行一些实验,证明一下:
// 实验1
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass()); // true
}
}
// 实验2
public class Test {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
// 利用反射能够成功存入字符串
list.getClass().getMethod("add", Object.class).invoke(list, "asd");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
在没有通配符之前,如果我们要写一个能够让泛型访问的公共方法是做不到的。因为公共方法无法使参数类型一致,编译器会进行类型检查,因此只能拆开成两个方法。
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> message1 = new Message<>();
message1.setMessage("请给我点个赞吧~");
Test.fun1(message1);
Message<Integer> message2 = new Message<>();
message2.setMessage(1);
Test.fun2(message2);
}
// 使用通配符
public static void fun1(Message<?> temp) {
System.out.println(temp.getMessage());
}
// 不使用通配符:必须拆成两个方法
public static void fun1(Message<String> temp) {
System.out.println(temp.getMessage());
}
public static void fun2(Message<Integer> temp) {
System.out.println(temp.getMessage());
}
}
(2)通配符的上界与下界
上下界的产生也是有原因的,咱们先做一些前置准备:
class Plate<T> {
private T plate; // 消息
public T getPlate() {
return plate;
}
public void setPlate(T plate) {
this.plate = plate;
}
}
1)通配符的上界
接收的是 类型边界本身 及 其子类。具体语法:
<? extends 类型边界>
使用示例
public class Test {
public static void main(String[] args) {
Plate<String> plate = new Plate<>();
Plate.setPlate("路哲萧笑欢迎你!");
fun(plate);
}
// 不能够使用 setPlate() ,因为 Plate参数化的类型是什么没法确定
public static void fun(Plate<? extends Fruit> temp) {
System.out.println(temp.getPlate());
// 必须使用 类型上界 接收
Fruit fruit = temp.getPlate();
System.out.println(fruit));
}
}
2)通配符的下界
接收的是 类型边界本身 及 其所有的父类。具体语法:
<? super 类型边界>
使用示例,一般用于添加元素
public class Test {
public static void main(String[] args) {
Plate<String> plate = new Plate<>();
Plate.setPlate("路哲萧笑欢迎你!");
fun(plate);
}
public static void fun(Plate<? super Fruit> temp) {
System.out.println(temp.getPlate());
// 直接添加 类型边界本身 及 其子类
temp.setPlate(new Apple());
temp.setPlate(new Banana());
//------------------------------
// error1:不知道获取的是本身还是父类
Fruit fruit = temp.getPlate();
System.out.println(fruit));
// error2:不能添加类型边界的父类,向下转型有风险!
temp.setPlate(new Food());
}
}
5.八大注意事项
(1)不能用基本类型实例化类型参数
由于基本类型不继承自Object
类,也不属于任何类,所以不能转换为Object
类型。
List<Integer> // 正确
List<int> // error
(2)运行时类型检查只能检查原始类型
Test<String> test = new Test<>();
test instanceof Test<String> // 报错,但可以使用 test instanceof MyInterface
test.getClass() == Test.class // 正确,且为真,反射
因为类型擦除之后,Test<String>
只剩下原始类型,泛型信息String
不存在了。
(3)不能使用静态的类型变量字段
private static T singleInstance; // 错误
(4)不能实例化 类型变量 和 泛型数组
T t1 = new T(); // 错误
T[] t2 = new T[2]; // 错误
(5)不能创建参数化类型数组
public class Test<T> {
private T key;
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
}
public class MainTest {
public static void main(String[] args) {
Test[] p = new Test[10]; // 这种可以编译通过,但是不安全。
Test<?>[] p = new Test<?>[10]; // 与上面效果一样
}
// error
public static void main(String[] args) {
Test<Long>[] p = new Test<Long>[10]; // 假设这里可以运行
Object[] o = p;
o[0] = new Test<String>(); // 数组中会存储一个不是Pair<Long>类型的数据
}
}
(6)异常类型不能用泛型类型
(7)关于继承1 —— 类型擦除与多态的冲突和解决方法
class MyTest<T> {
public void test(T obj) {
// do something
}
}
public class Test extends MyTest<String> {
@Override
public void test(String obj) {
// do something
}
}
注意,在泛型擦除后,子类Test
中的test
方法和父类中的test
方法并不属于重载关系,不具有多态性,是重写。
// 泛型在编译期,虽然你看着擦除之后是Object 和 String,但是其实都是Object
// 父类方法
public void test(Object obj) { ... }
// 子类方法
public void test(String obj) { ... }
如果子类重写了,直接调用子类的,子类没有重写,调用自动生成的桥方法。桥方法是由于泛型类型擦除而自动生成的方法,它用于连接原始类型和泛型类型之间的关系。
public void test(Object obj) {
test((String)obj);
}
注意:如果是常规的两个方法,它们的方法签名是一样的,虚拟机根本不能分别这两个方法。如果是我们自己编写Java
代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过 参数类型 和 返回类型 来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来”不合法”的事情,然后交给虚拟器去区别。
(8)关于继承2 —— 泛型中参数化类型不考虑继承关系
// 错误1:ArrayList<Object> list2 = new ArrayList<String>();
ArrayList<Object> list1 = new ArrayList<Object>();
list1.add(new Object());
list1.add(new Object());
ArrayList<String> list2 = list1; // 编译错误
我们先假设第四行代码编译正确。当我们使用list2
调用get()
方法取值的时候,返回的都是String
类型的数据(上面提到了,类型检测是根据引用来决定的),可是它实际上已经被我们存放了Object
类型的对象,这样就会有ClassCastException
了。所以为了避免这种极易出现的错误,Java
不允许进行这样的引用传递。
// 错误2:ArrayList<String> list2 = new ArrayList<Object>();
ArrayList<String> list1 = new ArrayList<String>();
list1.add(new String());
list1.add(new String());
ArrayList<Object> list2 = list1; //编译错误
这样的情况比第一种情况好的多,最起码,在我们用list2
取值的时候不会出现ClassCastException
,因为是从String
转换为Object
。可是,这样做有什么意义呢,泛型出现的一个原因就是去解决类型转换的问题。我们使用了泛型,到头来还是要自己强转,那就违背了泛型设计的初衷。所以Java
不允许这么干。同样,如果又用list2
往里面add()
新的对象,那么取出来的时候,我们怎么知道我取出来的到底是String
类型的,还是Object
类型的呢?
三、擦除机制
在探究擦除机制之前,我们先做好前置的准备工作。
首先,下载好插件jclasslib
其次,重新Build
编译 ,生成最新的字节码文件。擦除机制发生在编译期,而不是运行期。
最后,在选中某个类之后,在View
中点击Show Bytecode with Jclasslib
就能够看到字节码了
通过对比方法与字节码的信息,我们会发现:擦除机制就是——所有的T
都被擦除Ljava/lang/Object
。
用代码解释的话,如下所示:
// ------------编译前-------------
public class Test<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
// ------------编译后-------------
public class Test {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
最后我们填上最初T[] a = (T[]) new Object[N];
的坑。真正正确的写法需要用到反射:
class MyArray<T> {
public T[] array;
public MyArray() {}
public MyArray(Class<T> clazz, int capacity) {
array = (T[]) Array.newInstance(clazz, capacity);
}
public T getValue(int pos) {
return this.array[pos];
}
public void setValue(int pos,T val) {
this.array[pos] = val;
}
public T[] getArray() {
return array;
}
}
public class Test {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray<>(Integer.class,10);
Integer[] integers = myArray.getArray();
}
}
结语
写到这里,我们总算是理顺了泛型的知识啦。这些知识还是有点难理解的,主要是一些语法规则我们需要去遵守,大家可以通过看源码的方式对照学习!
最后,如果你觉得本文对你有帮助的话,就请点个赞支持一下博主吧!如果文中有任何不对或者疑惑的地方,希望不吝赐教。