泛型
什么是泛型
泛型是JDK5中引入的特性,它提供了编译时类型安全检测机制,该机制允许在编译是检测到非法的类型。
它的本质是参数化类型,也就是说操作的数据类型被指定为一个参数。
也就是将类型有原来的具体类型参数化,然后在使用/调用时传入具体的类型。
这种参数类型可以用在类、方法和接口中,分别被称为泛型类、泛型方法、泛型接口。
泛型定义格式
<类型>:指定一种类型的格式,这里的类型可以看成是形参。
<类型1,类型2…>:指定多种类型的格式,多种类型之间用逗号隔开,这里的类型可以看成是形参。
将来具体调用时给定类型可以看成实参,并且实参的类型只能是引用数据类型。
泛型的好处
- 把运行时期的问题提前到编译期间
- 避免了强制类型转换
案例
ArrayList使用泛型与不使用泛型比较
package main.java.demo1;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* ArrayList 使用泛型与不使用泛型比较
*
* @author Anna.
* @date 2024/4/1 21:34
*/
public class GenericDemo1 {
public static void main(String[] args) {
List<String> listStr = new ArrayList<String>();
listStr.add("123");
// listStr.add(123); // 编译时会报错提示不允许设置数据类型与设置泛型类型不一致
Iterator<String> stringIterator = listStr.iterator();
while (stringIterator.hasNext()){
String str = stringIterator.next(); // 避免强制类型转换问题 next()拿到的数据就是设置的泛型数据
System.out.println(str);
}
List list = new ArrayList();
list.add("123");
list.add(123);
Iterator iterator = list.iterator();
while (iterator.hasNext()){
Integer b = (Integer) iterator.next(); // 这里会报数据转换异常
System.out.println(b);
}
}
}
执行结果
泛型的使用
泛型可以用在类、方法和接口中,分别被称为泛型类、泛型方法、泛型接口
泛型类
格式: 修饰符 class 类名<类型>{}
范例:
public class Generic<T>{}
注意:
此处T可以随便写为任意表示,常见的如T、E、K、V等形式的参数常用于表示泛型。
Java 常见的泛型标识以及其代表含义如下:
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key 的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
N :代表 Number(数值类型)
R :代表 return(返回值)
泛型方法
格式: 修饰符 <类型> 返回值类型 方法名(类型 变量名称){}
范例:
public <T> void show(T t){}
泛型接口
格式: 修饰符 interface 接口名称<类型>{}
范例:
public interface Generic<T>{}
示例
定义泛型接口Show.java
package main.java.demo2;
/**
* 定义泛型接口
*
* @author Anna.
* @date 2024/4/1 22:03
*/
public interface Show<T> {
void show(T t);
}
定义泛型接口实现类ShowImpl.java
package main.java.demo2;
/**
* 定义泛型接口类型实现
*
* @author Anna.
* @date 2024/4/1 22:03
*/
public class ShowImpl<T> implements Show<T> {
@Override
public void show(T t) {
System.out.println("泛型接口t = " + t);
}
}
定义泛型类GenericDo.java
package main.java.demo2;
/**
* 定义泛型类
*
* @author Anna.
* @date 2024/4/1 22:05
*/
public class GenericDo<T> {
public void show(T t){
System.out.println("泛型类t = " + t);
}
}
定义泛型方法GenericDo1.java
package main.java.demo2;
public class GenericDo1 {
/**
* 定义泛型方法
*
* @param t
* @return void
* @author Anna.
* @date 2024/4/1 22:11
*/
public <T> void show(T t){
System.out.println("泛型方法t = " + t);
}
}
测试GenericDemo2.java
package main.java.demo2;
public class GenericDemo2 {
public static void main(String[] args) {
// 调用泛型类
GenericDo<String> genericDo = new GenericDo<String>();
genericDo.show("123");
// genericDo.show(false); // 编译会报错
GenericDo<Boolean> genericDo1 = new GenericDo<Boolean>();
genericDo1.show(false);
// 调用泛型接口
Show<String> show1 = new ShowImpl<String>();
show1.show("123");
// show1.show(false); // 编译会报错
Show<Boolean> show2 = new ShowImpl<Boolean>();
show2.show(false);
// 调用泛型方法
GenericDo1 genericDo3 = new GenericDo1();
genericDo3.show(false);
genericDo3.show("123");
}
}
执行结果
类型通配符
类型通配符
为了表示各种泛型List的父类,可以使用类型通配符
格式: <?>
List<?>:表示元素类型未知的List,它的元素可以匹配<font color="red"><b>任何的类型</b></font>。<br/>
这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素添加到其中。
类型通配符上限
如果说我们不希望List<?> 是任何泛型List的父类,只希望它代表一类泛型List的父亲,可以使用类型通配符的上限
格式: <? extends 类型>
List<? extends Number>:它表示的类型是Number或者其子类型。
类型通配符下限
除了可以指定类型通配符的上限,我们也可以指定类型通配符的下限
格式: <? supper 类型>
List<? supper Number>:它表示的类型是Number或者其父类型。
示例
首先我们看一下Number的继承关系,如下图:
从图中我们可以看出,Number的父类是Object,子类包含 Byte, Integer, Long等
测试代码
package main.java.demo3;
import java.util.ArrayList;
import java.util.List;
public class GenericDemo03 {
public static void main(String[] args) {
// 测试通配符上限
List<? extends Number> list1 = new ArrayList<Object>(); // 编译报错
List<? extends Number> list2 = new ArrayList<Number>();
List<? extends Number> list3 = new ArrayList<Integer>();
// 测试通配符下限
List<? super Number> list4 = new ArrayList<Object>();
List<? super Number> list5 = new ArrayList<Number>();
List<? super Number> list6 = new ArrayList<Integer>(); // 编译报错
}
}
测试结果
<? extends T>与<? super T> 对比
结论:
(1)对于<? extends 类型>,编译器将只允许读操作,不允许写操作。即只可以取值,不可以设值。
(2)对于<? super 类型>,编译器将只允许写操作,不允许读操作。即只可以设值(比如 set 操作),不可以取值(比如 get 操作)。
已 Java 标准库的 Collections 类定义的 copy() 方法为例子
import java.util.List;
public class Collections {
// 把 src 的每个元素复制到 dest 中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
// 获取 src 集合中的元素,并赋值给变量 t,其数据类型为 T
T t = src.get(i);
// 将变量 t 添加进 dest 集合中
dest.add(t);// 添加元素进入 dest 集合中
}
}
}
如果反过来,我们可以看到编译器编译失败
import java.util.List;
public class Collections {
// 把 dest 的每个元素复制到 src 中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
// 获取 dest 集合中的元素,并赋值给变量 t,其数据类型为 T
T t = dest.get(i);
// 将变量 t 添加进 src 集合中
src.add(t);// 添加元素进入 src 集合中
}
}
}
编译结果
copy() 方法的另一个好处是可以安全地把一个 List< Integer >添加到 List< Number >,但是无法反过来添加。
这个很好理解,List< Number > 集合中可能有 Integer、Float 等对象,所以肯定不能复制到List< Integer > 集合中;而 List< Integer > 集合中只有 Integer 对象,因此肯定可以复制到 List< Number > 集合中。
PECS 原则
我们何时使用 extends,何时使用 super 通配符呢?为了便于记忆,我们可以用 PECS 原则:Producer Extends Consumer Super。
即:如果需要返回 T,则它是生产者(Producer),要使用 extends 通配符;如果需要写入 T,则它是消费者(Consumer),要使用 super 通配符。
还是以 Collections 的 copy() 方法为例:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
T t = src.get(i); // src 是 producer
dest.add(t); // dest 是 consumer
}
}
}
需要返回 T 的 src 是生产者,因此声明为List<? extends T>,需要写入 T 的 dest 是消费者,因此声明为List<? super T>。
类型擦除
泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。
换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。
假如我们给 ArrayList 集合传入两种不同的数据类型,并比较它们的类信息。代码如下:
public class GenericType {
public static void main(String[] args) {
ArrayList<String> arrayString = new ArrayList<String>();
ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
System.out.println(arrayString.getClass() == arrayInteger.getClass());// true
}
}
在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。
我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为true。
明明我们在 <> 中传入了两种不同的数据类型,按照上文所说的,它们的类型参数 T 不是应该被替换成我们传入的数据类型了吗,那为什么它们的类信息还是相同呢?
这是因为,在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Object >类型。
再看一个例子,假设定义一个泛型类如下:
public class Caculate<T> {
private T num;
}
在该泛型类中定义了一个属性 num,该属性的数据类型是泛型类声明的类型参数 T ,这个 T 具体是什么类型,我们也不知道,它只与外部传入的数据类型有关。将这个泛型类反编译。
代码如下:
public class Caculate {
public Caculate() {}// 默认构造器,不用管
private Object num;// T 被替换为 Object 类型
}
可以发现编译器擦除了 Caculate 类后面的泛型标识 < T >,并且将 num 的数据类型替换为 Object 类型,而替换了 T 的数据类型我们称之为原始数据类型。
那么是不是所有的类型参数被擦除后都以 Object 类进行替换呢?
答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数。
再看一个例子,假设定义一个泛型类如下:
public class Caculate<T extends Number> {
private T num;
}
将其反编译:
public class Caculate {
public Caculate() {}// 默认构造器,不用管
private Number num;
}
可以发现,使用到了 extends 语法的类型参数 T 被擦除后会替换为 Number 而不再是 Object。
extends 和 super 是一个限定类型参数边界的语法,extends 限定 T 只能是 Number 或者是 Number 的子类。
也就是说,在创建 Caculate 类对象的时候,尖括号 <> 中只能传入 Number 类或者 Number 的子类的数据类型,所以在创建 Caculate 类对象时无论传入什么数据类型,Number 都是其父类,于是可以使用 Number 类作为 T 的原始数据类型,进行类型擦除并替换。
gitee源码
git clone https://gitee.com/dchh/JavaStudyWorkSpaces.git