Java泛型
一、基础
1.1 什么是泛型
-
泛型是 JDK5 的一个新特性,是将类型明确的事情延后到创建对象或调用方法时,再去明确的特殊的类型;
-
泛型的本质是参数化类型,它提供了编译时类型安全监测机制,通过这个机制,我们可以在编译时检查类型安全,泛型所有的强制转换都是自动、隐式的,只要编译时没有问题,运行时就不会出现 ClassCastException(类型转换异常),极大地提高代码的可读性、复用性及安全性;
-
泛型可以用在类、接口和方法的创建中,被称为泛型类、泛型接口、泛型方法。
1.2 泛型的作用
-
没有泛型前,我们通过对类型 Object 的引用,来实现参数的“任意化”。缺点是:要做显式的强制类型转换,开发者需要对实际参数类型在可以预知的情况下进行。
-
引入泛型后,我们可以在使用时,再确定具体的类型。在调用方法时,也不用强转对象、写很多重复方法了。
1.3 泛型的优点
- 代码可复用:一套代码可支持不同的类型;
- 可读性更高:不用强制转换,代码更加简洁;
- 程序更安全:在编译时就检查类型安全,如在编译时没有警告,运行时就不会出现 ClassCastException (类型转换异常),降低 crash 率;
- 稳定性更强:在创建集合时,就限定了集合元素的类型。因此,在取出元素时,不需要强制类型转换了。
1.4 泛型的使用场景
- 使用在不想写多个重载函数的场景;
- 使用在用户希望返回他自定义类型的返回值场景。例如:Json 返回 Java bean ;
- 在使用反射的应用中,也经常使用泛型。例如:Class<T> ;
- 使用在约束对象类型的场景,用来定义边界(T extends ...)。例如:JDK 集合 List,Set ;
- 使用在网页、资源分析或返回的场景。
1.5 泛型的设计原则
编译时没有出现警告,运行时就不会出现 ClassCastException 异常。
1.6 泛型的实现原理
Java 中的泛型,基本上都是在编译器这个层次来实现的。在生成的 Java 字节码里面,没有包含泛型中的类型信息。我们在使用泛型的时候添加的类型参数,将在编译时被擦除掉。这也是 Java 的泛型也被称作为“伪泛型”的原因。
1.6.1 类型擦除(Type Erasure)
测试代码:
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());
System.out.println(arrayString.getClass()==arrayInteger.getClass());
}
}
输出:
class java.util.ArrayList
true
注意:是类型相同,而不是对象相同。
在这个示例中,我们定义了两个 ArrayList 数组:
数组一:ArrayList<String> 泛型类型,只可以存储字符串;
数组二:ArrayList<Integer> 泛型类型,只可以存储整型。
最后,我们通过 ArrayString 对象、和 ArrayInteger 对象的 GetClass 方法,获取它们的类信息并比较,发现结果为 true
这是为什么呢,明明我们定义了两种不同的类型呀?这是因为:
在编译期,所有的泛型信息都会被擦除,List<Integer> 和 List<String> 类型在编译后,都会变成 List 类型(原始类型)。
1.7 泛型的特性
Java 中的泛型,只在编译的时候有效。
- 我们在编译时,先检验泛型结果,再将泛型信息擦除掉,最后在对象进入和离开方法的边界处,添加上类型检查和类型转换的方法。
- 在编译后,程序会采取去泛型化的措施,即泛型信息不会进入到运行时阶段。
1.8 泛型的规则限制
- 不可以使用泛型地形参创建对象;
- 在泛型类中,不可以给静态成员变量定义泛型;
- 泛型类不可以继承 java.lang.Throwable 类;
- Java 泛型不可以使用基本类型;
- 泛型类不可以初始化一个数组;
- Java 泛型不可以进行实例化;
- Java 泛型不可以直接进行类型转换;
- Java 泛型不可以直接使用 instanceof 运算符,进行运行时类型检查;
- Java 泛型不可以创建确切类型的泛型数组;
- Java 泛型不可以定义泛型异常类、或 catch 异常;
- Java 泛型不可以作为参数进行重载。
1.9 泛型常用术语
11.10 泛型的类型表示
2二、为什么要使用泛型
1、在 JDK5 之前,集合中没有泛型,我们是通过继承来实现泛型的程序设计的,使用继承实现有两个弊端
- 取值时,需要强制类型转换,反之,得到的都是 Object。
- 编译时,不会检查错误。
2、在 JDK5 之后,集合中有了泛型,我们就可以使用泛型来实现了,泛型提供了编译时类型安全检测机制,该机制:
- 允许我们在编译时检查类型安全,且所有的强制转换都是自动和隐式的,只要编译时不出现问题,运行时就不会出现ClassCastException(类型转换异常)。
- 可以极大地提高代码的可读性、复用性及安全性。
2.1 实例
电商系统中,有普通用户、商户用户两种类型。当用户获取信息详情时,系统要先将其中的敏感信息设置为“空”,再返回给用户。此时,我们需要写一个通用方法,将敏感字段设置为“空”。具体怎么实现呢?
我们可能想到的三种实现方式:继承、方法重载机制、泛型机制
2.1.1 第一种实现方式:继承
在 Java 中,所有的类都继承了 Object ,我们可以将 Object 作为传入参数,再使用反射去操作字段,设置 password 为空。
public class Client {
public static void main(String[] args) {
// 初始化
ShopUser shopUser = new ShopUser(0L, "shopUser", "123456");
ClientUser clientUser = new ClientUser(0L, "clientUser", "123456");
// 输出原始信息
System.out.println("过滤前:");
System.out.println(" " + shopUser);
System.out.println(" " + clientUser);
// 执行过滤
shopUser = (ShopUser) removeField(shopUser);
clientUser = (ClientUser) removeField(clientUser);
// 输出过滤后信息
System.out.println("过滤后:");
System.out.println(" " + shopUser);
System.out.println(" " + clientUser);
}
public static Object removeField(Object obj) throws Exception {
Set<String> fieldSet = new HashSet();
fieldSet.add("password");
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
if (fieldSet.contains(field.getName())) {
field.setAccessible(true);
field.set(obj, null);
return obj;
}
}
return obj;
}
}
运行结果
过滤前:
ShopUser{id=0, username='shopUser', password='123456'}
ClientUser{id=0, username='clientUser', password='123456'}
过滤后:
ShopUser{id=null, username='shopUser', password='null'}
ClientUser{id=null, username='clientUser', password='null'}
通过继承实现,运行结果没问题,但要强制转换对象,代码不够简洁。
2.1.2 第二种实现方式:方法重载机制
Java 不是有方法重载机制吗,我们再来试试这个方法。
业务方法:
public static ShopUser removeField(ShopUser user) throws Exception {
// 强转,并返回对象
return (ShopUser) remove(user);
}
public static ClientUser removeField(ClientUser user) throws Exception {
// 强转,并返回对象
return (ClientUser) remove(user);
}
核心方法:
// 把敏感字段设置为空
public static Object remove(Object obj) throws Exception {
// 需要过滤的敏感字段
Set<String> fieldSet = new HashSet<String>();
fieldSet.add("password");
// 获取所有字段:然后获取这个类所有字段
Field[] fields = obj.getClass().getDeclaredFields();
// 敏感字段设置为空
for (Field field : fields) {
if (fieldSet.contains(field.getName())) {
// 开放字段操作权限
field.setAccessible(true);
// 设置空
field.set(obj, null);
}
}
// 返回对象
return obj;
}
}
通过方法重载机制实现,重复方法会很多,我们每添加一个供应商用户,就要再写一个方法、修改源码,简直太繁琐了,还极容易出错。
2.1.3 第三种实现方式:泛型
public static <T> T removeField(T obj) throws Exception {
Set<String> fieldSet = new HashSet();
fieldSet.add("password");
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
if (fieldSet.contains(field.getName())) {
field.setAccessible(true);
field.set(obj, null);
return obj;
}
}
return obj;
}
通过泛型实现,我们就不用把参数的类型代码写死,并且可以在使用的时候,再去确定具体的类型;在调用方法的时候,也不用强转对象、写很多重复方法,代码更加简洁、安全。
三、哪些情况下需要使用泛型
当接口、类及方法中操作的引用数据类型不确定时,就可以用泛型来表示,这样能避免强转,将运行问题转移到编译期。
需要注意的是:
- 泛型实际代表什么类型,取决于调用者传入的类型,如果没传,默认是 Object 类型。
- 使用带泛型的类创建对象时,等式两边指定的泛型类型必须一致。原因是:编译器检查对象调用方法时只看变量;而程序在运行期间调用方法时,就要考虑对象具体类型了。
- 等式两边,可以在任意一边使用泛型,在另一边不使用(考虑向后兼容)。
四、泛型类、泛型接口、泛型方法
4.1 泛型类
4.1.1 泛型类的定义
泛型类,是在实例化类时指明泛型的具体类型,是拥有泛型特性的类,本质还是一个 Java 类,可以被继承。泛型类是最常见的泛型使用方式,最常见的运用就是各种集合类和接口,例如List、ArrayList 等。
4.1.2 泛型类的格式
格式:public class 类名<数据类型,…> { }
示例:public class Generic<T>{ }
关于数据类型:
- 可以用任意字母来代表,例如 T ,或者 T,E,K,V等形式的参数。
- 也可以用多个英文字母,如果用多个英文字母,需要用逗号隔开,例如
public class Generic<T,K,V>{ }
。
4.1.3 泛型类的使用
- 定义一个类,在该类名后面添加类型参数声明部分,由尖括号分隔。
- 每一个类型参数声明部分,可以包括一个或多个类型参数,参数间用逗号隔开。
- 使用 <T> 来声明一个类型持有者名称,将 T 当作一个类型来声明成员、参数、返回值类型(此处T可以写为任意标识)。
4.1.4 泛型类的示例代码
public class GenericsBox<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
}
4.1.5 泛型类的使用注意事项
- 泛型的类型参数(包括自定义类),只能是类类型(String、Integer),不能是简单类型(int,double)。
- 不能对确切的泛型类型使用 instanceof 操作,会导致编译时出错。
- 使用泛型时,如果传入泛型实参,会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用;如果不传入泛型类型实参,在泛型类中,使用泛型的方法或成员变量定义的类型,可以是任何的类型。
- 若泛型类型已确定,则只能是其本身,其子类不能使用。
4.2 泛型接口
4.2.1 泛型接口的定义
将泛型用在接口 interface 上,被称为泛型接口,泛型接口经常被用在各种类的生产器中。
4.2.2 泛型接口的格式
格式:修饰符 interface 接口名 <类型>{ }
示例:public interface ShangHaiJieKou<T> { }
泛型接口格式,类似于泛型类的格式;接口中的方法的格式,类似于泛型方法的格式。
以下有两个类,和一个接口,我们以接口类为例:
1. Genericimpl(接口实现类)
2. Generic_泛型接口<T>(interface接口类)
3. GenericDemo_泛型接口(main方法实现类)
4.2.3 泛型接口的使用
- 定义一个接口,在该接口名后面,添加类型参数声明部分,用尖括号分隔。
- 每一个类型参数声明部分,可以包括一个或多个类型参数,参数间用逗号隔开。
4.2.4 泛型接口的示例代码
public interface GenericsInterface<T> {
public abstract void genericsInterface1(T element);
public abstract <T> void genericsInterface2();
}
4.2.5 泛型接口的使用注意事项
实现泛型的类,必须传入泛型实参,然后在实现类中用该实参来替换 T 。
4.3 泛型方法
4.3.1 泛型方法的定义
泛型方法,是在调用方法时指明泛型的具体类型,既可用在类和接口上,还可用在方法上。
4.3.2 泛型方法的格式
- 格式:
修饰符 <类型> 返回值类型方法名(类型变量名){}
- 示例:
public <T> void show(T t){}
4.3.3 泛型方法的使用
- 所有泛型方法声明,都有一个类型参数声明部分,由尖括号分隔。该类型参数的声明部分,在方法返回值类型之前。
- 每一个类型参数声明部分,都包括一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数可被用来声明返回值类型,作为泛型方法得到的实际参数类型的占位符。
4.3.4 泛型方法的示例代码
打印各种类型的数组中的元素
public static <T> void printLog(T[] inputArray) {
for (T element : inputArray) {
LogUtil.e("打印数组中的元素", element + "");
}
}
4.3.5 编程原则
泛型方法是为了调高代码的重用性和程序安全性,尽量设计泛型方法去解决问题,如果设计泛型方法,可以取代泛型整个类,就选择泛型方法。另外,泛型方法可以搞定的,就不建议使用泛型类了。
五、泛型类的继承
泛型类被继承的情况有两种:子类明确泛型类的类型参数变量、子类不明确泛型类的类型参数变量。
5.1 子类明确泛型类的类型参数变量
把泛型定义在接口上
public interface Inter<T> {
public abstract void show(T t);
}
定义一个子类来实现接口
public class InterImpl implements Inter<String> {//子类明确泛型类的类型参数变量为String:
@Override
public void show(String s) {
System.out.println(s);
}
}
测试代码
public class mingquefanxingcanshu {
public static void main(String[] args) {
Inter<String> i = new InterImpl();
i.show("hello java!");
}
}
5.2 子类不明确泛型类的类型参数变量
把泛型定义在接口上
public interface Inter<T> {
public abstract void show(T t);
}
定义一个子类来实现接口
public class InterImpl<T> implements Inter<T> {//实现类不明确泛型类的类型参数变量,实现类也要定义出<T>类型的
@Override
public void show(T s) {//子类方法也要继承类的数据类型T
System.out.println(s);
}
}
测试代码
public class mingquefanxingcanshu {
public static void main(String[] args) {
Inter<String> i = new InterImpl();
i.show("hello java!");
}
}
5.3 泛型类的继承注意事项
- 实现类要的是重写父类的方法,返回值的类型需要同父类一样。
- 类上声明的泛形,只对非静态成员有效。
六 、泛型通配符
6.1 泛型通配符的基本概述
泛型通配符用 ? 表示,代表不确定的类型,是泛型的一个重要组成。
相关术语及范例:
- ArrayList<E>,称为泛型类型,此处的 E ,称为类型参数变量。
- ArrayList<Integer>,称为参数化的类型 ParameterizedType ,此处的Integer,称为实际类型参数。
6.2 为什么要用泛型通配符
泛型通配符的引入,是为了解决类型被限制死之后,不能动态根据实例来确定的缺点。先来看代码示例,我们现在有这样一个函数
public void test(List<Number> data) {
}
根据泛型规则,这个函数只能传进来 List<Number> 这一种类型, List<Object> 和 List<Integer> 是传不进来的。如果我们既要泛型,又想把这两个类型的子类、或者父类的泛型传进去,就需要使用到通配符泛型。
6.3 泛型通配符的优点
可以在保证运行时类型安全的基础上,提高参数类型的灵活性。
6.4 泛型通配符的作用
泛型本身不支持协变和逆变,通配符能解决协变和逆变的问题。
在 Java 中:
- 数组可以协变。例如:Dog extends Animal , Animal[] 与 Dog[] 是可以兼容的。
- 集合不可以协变。List<Animal> 不是 List<Dog> 的父类,为了建立两个集合之间的联系,此时需要引用通配符来解决。
泛型 T 是确定的类型,而通配符更加灵活(不确定),更多用于扩充参数的范围。泛型 T就像是变量,将传来的一个具体的类型拿来使用,而通配符则是一种规定,规定你能传哪些参数,就像是一个特殊的实际类型参数。
6.5 泛型通配符的使用场景
- 用于类型参数中。
- 用于实例变量,或者局部变量中。
- 有时也可作为返回类型,例如Object的getClass方法。
注意:泛型通配符只能用于泛型类的使用(声明变量、方法的参数),不能用于泛型定义、New 泛型实例。
6.6 泛型通配符的三种形式
- <?> :无边界通配符(基本使用)
- <? extends T> :固定上边界通配符(又称有上限的通配符)
- <? super T> :固定下边界通配符(又称有下限的通配符)
6.6.1 无边界通配符 <?>
无边界通配符的概述:
- 英文全称 Unbounded Wildcards ,代表任意类型,是万能通配符 ;
- 采用 <?> 的语法形式,来声明使用该类通配符。例如: List<?> ,表示元素类型未知的 List ,它的元素可以匹配任何的类型。这种带通配符的 List ,仅表示它是各种泛型 List 的父类,并不能把元素添加到其中。
无边界通配符的作用:可以让泛型能够接受未知类型的数据。
6.6.2 固定上边界通配符 <? extends T>
固定上边界通配符的概述:
- 英文全称 Upper Bounded Wildcards 。
- 采用 <? extends 类型> 的语法形式,来声明使用该类通配符。例如:List<?extends Number> ,表示 Number 或者其子类型。需要注意的是,此处虽然使用了 extends 关键字,但却不仅限于继承了父类 类型 的子类,也可以代指实现了接口 类型 的类。
- 代表类型变量的范围有限,只能传入某种类型、或者它的子类,适合频繁往外读取数据的场景。
固定上边界通配符的作用:
当我们不希望 List<?> 是任何泛型 List 的父类,只希望它代表某一类泛型 List 的父类时,我们就使用固定上边界通配符的泛型,来接受指定类、以及其子类类型的数据。
6.6.3 固定下边界通配符 <? super T>
固定下边界通配符的概述:
- 英文全称 Lower Bounded Wildcards。
- 采用 <? super 类型> 的语法形式 ,来声明使用该类通配符。例如: List<?super Number> ,它表示的类型是 Number 或者其父类型。
- 代表类型变量的范围有限,只能传入某种类型、或者其父类,适合频繁插入数据的场景。
固定下边界通配符的作用:
我们可以使用固定下边界通配符的泛型,来接受指定类及其父类类型的数据。需要重点注意的是,一个泛型可以单独指定类型通配符的上边界、或下边界,但不能同时指定上边界、下边界。
6.7 泛型通配符的使用
我们确定类型变量时,如果不能明确类型变量,就可以使用泛型通配符(泛型通配符代表不确定的类型)。反之,如果我们能明确地知道类型变量,就不需要使用泛型通配符。
6.7.1 代码示例1 :不需要使用泛型通配符
我们明确地知道 count() 方法用在哪儿,在编码时,可以直接指明这是一个 Integer 集合。
public static Integer count(List<Integer> list) {
int total = 0;
for (Integer number : list) {
total += number;
}
list.add(total);
return total;
}
在调用方法时,如果不传指定的数据进来,编译会报错:
public static void main(String[] args) {
// 不传指定数据,编译报错
List<String> strList = Arrays.asList("0", "1", "2");
int totalNum = count(strList);
}
即便我们绕过了编译,程序也很可能无法正常运行:
public static void main(String[] args) {
// 绕过了编译,运行报错
List strList1 = Arrays.asList("0", "1", "2");
int totalNum = count(strList1);
}
所以,如果你已经明确自己要做什么,清楚地知道类型变量,就没必要使用泛型通配符。
6.7.2 代码示例2 :使用泛型通配符,实现一些通用算法
但是,在一些通用方法中,什么类型的数据都能传进来,不能确认类型变量,这时候该怎么办呢?此时,我们就可以使用泛型通配符,在不用确认类型变量的情况下,来实现一些通用算法。例如:我们需要写一个通用方法,把传入的 List 集合输出到控制台。
public static void print(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
Integer 集合,可以运行:
public static void main(String[] args) {
//Integer 集合,可以运行
List<Integer> intList = Arrays.asList(0, 1, 2);
print(intList);
}
String 集合,可以运行:
public static void main(String[] args) {
//String 集合,可以运行
List<String> strList = Arrays.asList("0", "1", "2");
print(strList);
}
List<?> list 指不确定 List 集合装的是什么类型,有可能是 Integer、或 String、或其他。但这不用理会,我们只需要传一个 List 集合进来,这个方法就能正常运行了。这就是泛型通配符的作用所在。
我们还可以使用泛型通配符,来实现一些特殊算法。泛型通配符也适用于一些特殊算法,但可使用的范围不大。例如:用户分为普通用户、商家用户,但用户有一些特殊功能,其它角色都没有。这时候,又该怎么办呢?我们可以通过给泛型通配符设定边界,来限定类型变量的范围。
6.8 PECS原则
泛型通配符的用法有些复杂,为了更好的帮助我们理解、及正确使用 Java 泛型通配符。Joshua Bloch 在《Effective Java 》第 3 版中提出了 PECS 原则。PECS 的英文全称是 Producer Extends , Consumer Super ,字面意思是“读取时使用 extends ,写入时使用 super ”。
也就是说:
- 参数化类型表示一个生产者,我们就使用 <? extends T> ;
- 参数化类型表示一个消费者,我们就使用 <? super T> 。
6.9 通配符与类型参数的区别
一般而言,通配符能干的事情都可以用类型参数替换。例如:
public void testWildCards(Collection<?> collection){}
可以用以下来取代:
public <T> void test(Collection<T> collection){}
值得注意的是,如果用泛型方法来取代通配符,上面代码中的 collection 是能进行写操作的,但要进行强制转换。
public <T> void test(Collection<T> collection){
collection.add((T)new Integer(12));
collection.add((T)"123");
}
需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。
public class Test2 <T,E extends T>{
T value1;
E value2;
}
如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。
public T test1(T t){
return value1;
}
6.10 泛型通配符和泛型方法的使用选型
在实际应用场景中,有很多时候,我们可以使用泛型方法、来替代泛型通配符。使用通配符:
public static void test(List<?> list) {
}
使用泛型方法:
public <T> void test2(List<T> t) {
}
既然通配符和泛型方法都可用,具体该怎样选型呢?
通配符和泛型方法的选型参考:
- 参数之间的类型有依赖关系的,又或者返回值与参数之间有依赖关系的,使用泛型方法。
- 参数之间的类型没有依赖关系的,使用通配符。
七、Java 泛型中的 T,E,K,V
本质上都是通配符,只是一种编码约定俗成。我们可以将 T 换成 A-Z 之间的任意字母,不会影响程序的正常运行。但是,如果换成其他的字母代替 T ,可读性可能会差一些。通常情况下,T,E,K,V,?
是这样约定的:
资料来源:
Java泛型基础最全详解,超级详细