Java核心知识1:泛型机制详解
1 理解泛型的本质
JDK 1.5开始引入Java泛型(generics)这个特性,该特性提供了编译时类型安全检测机制,允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型)。
2 泛型的作用
泛型有四个作用:类型安全、自动转换、性能提升、可复用性。即在编译的时候检查类型安全,将所有的强制转换都自动和隐式进行,同时提高代码的可复用性。
2.1 泛型如何保证类型安全
在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。
比如:没有泛型的情况下使用集合:
public static void noGenericTest() {
// 编译正常通过,但是使用的时候可能转换处理出现问题
ArrayList arr = new ArrayList();
arr.add("加入一个字符串");
arr.add(1);
arr.add('a');
}
有泛型的情况下使用集合:
public static void genericTest() {
// 编译不通过,直接提示异常,Required type:String
ArrayList<String> arr = new ArrayList<>();
arr.add("加入一个字符串");
arr.add(1);
arr.add('a');
}
有了泛型后,会对类型进行验证,所以集合arr在编译的时候add(1)、add('a') 都会编译不通过。
这个过程相当于告诉编译器每个集合接收的对象类型是什么,编译器在编译期就会做类型检查,告知是否插入了错误类型的对象,使得程序更加安全,增强了程序的健壮性。
2.2 类型自动转换,消除强转
泛型的另一个好处是消除源代码中的强制类型转换,这样代码可读性更强,且减少了转换类型出错的可能性。
以下面的代码为例子,以下代码段需要强制转换,否则编译会通不过:
ArrayList list = new ArrayList();
list.add(1);
int i = (int) list.get(0); // 需强转
当重写为使用泛型时,代码不需要强制转换:
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
int i = list.get(0); // 无需转换
2.3 避免装箱、拆箱,提高性能
在非泛型编程中,将简单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。
泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。
object a=1;//由于是object类型,会自动进行装箱操作。
int b=(int)a;//强制转换,拆箱操作。这样一去一来,当次数多了以后会影响程序的运行效率。
使用泛型后
public static T GetValue<T>(T a) {
return a;
}
public static void Main(){
int b=GetValue<int>(1);//使用这个方法的时候已经指定了类型是int,所以不会有装箱和拆箱的操作。
}
2.4 提升程序可复用性
引入泛型的另一个意义在于:适用于多种数据类型执行相同的代码(代码复用)
我们通过下面的例子来说明,代码如下:
private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
3 泛型的使用
3.1 泛型类
泛型类是指把泛型定义在类上,具体的定义格式如下:
public class 类名 <泛型类型1,...> {
// todo
}
注意事项:泛型类型必须是引用类型,非基本数据类型
定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔:
public class GenericClass<ab,a,c> {
// todo
}
当然,这个后面的参数类型也是有规范的,不能像上面一样随意,通常类型参数我们都使用大写的单个字母表,可以任意指定,但是还是建议使用有字面含义的,让人通俗易懂,下面的字母可以参考使用:
T:任意类型 type
E:集合中元素的类型 element
K:key-value形式 key
V: key-value形式 value
N: Number(数值类型)
?: 表示不确定的java类型
这边举个例子,假设我们写一个通用的返回对象,对象中的某个字段的类型不定:
@Data
public class Response<T> {
/**
* 状态
*/
private boolean status;
/**
* 编码
*/
private Integer code;
/**
* 消息
*/
private String msg;
/**
* 接口返回内容,不同的接口返回的内容不一致,使用泛型数据
*/
private T data;
/**
* 构造
* @param status
* @param code
* @param msg
* @param data
*/
public Response(boolean status,int code,String msg,T data) {
this.status = status;
this.code = code;
this.msg = msg;
this.data = data;
}
}
做成泛型类,他的通用性就很强了,这时候他返回的情况可能如下:
先定义一个用户信息对象
@Data
public class UserInfo {
/**
* 用户编号
*/
private String userCode;
/**
* 用户名称
*/
private String userName;
}
尝试返回不同的数据类型:
/**
* 返回字符串
*/
Response<String> responseStr = new Response<>(true,200,"success","Hello Word");
/**
* 返回用户对象
*/
UserInfo userInfo = new UserInfo();
userInfo.setUserCode("123456");
userInfo.setUserName("Brand");
Response<UserInfo> responseObj = new Response<>(true,200,"success",userInfo);
输出结果如下:
{
"status": true,
"code": 200,
"msg": "success",
"data": "Hello Word"
}
// 和
{
"status": true,
"code": 200,
"msg": "success",
"data": {
"user_code": "123456",
"user_name": "Brand"
}
}
3.2 泛型接口
泛型方法概述:把泛型定义在接口上,他的格式如下
public interface 接口名<T> {
// todo
}
注意点1:方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。
public interface GenericInterface<T> {
void show(T value);}
}
public class StringShowImpl implements GenericInterface<String> {
@Override
public void show(String value) {
System.out.println(value);
}}
public class NumberShowImpl implements GenericInterface<Integer> {
@Override
public void show(Integer value) {
System.out.println(value);
}}
注意点2:使用泛型的时候,前后定义的泛型类型必须保持一致,否则会出现编译异常:
// 编译的时候会报错,因为前后类型不一致
GenericInterface<String> genericInterface = new NumberShowImpl();
// 编译正常,前面泛型接口不指定类型,由new后面的实例化来推导。
GenericInterface g1 = new NumberShowImpl();
GenericInterface g2 = new StringShowImpl();
3.3 泛型方法
泛型方法,是在调用方法的时候指明泛型的具体类型 。定义格式如下:
public <泛型类型> 返回类型 方法名(泛型类型 变量名) {
// todo
}
举例说明,下面是一个典型的泛型方法,根据传入的对象,打印它的值和类型:
/**
* 泛型方法
* @param <T> 泛型的类型
* @param c 传入泛型的参数对象
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E等形式的参数常用于表示泛型。
*/
public <T> T genercMethod(T c) {
System.out.println(c.getClass());
System.out.println(c);
return c;
}
public static void main(String[] args) {
GenericsClassDemo<String> genericString = new GenericsClassDemo("Hello World"); //这里的泛型跟下面调用的泛型方法可以不一样。
String str = genericString.genercMethod("brand");//传入的是String类型,返回的也是String类型
Integer i = genericString.genercMethod(100);//传入的是Integer类型,返回的也是Integer类型
}
输出结果如下:
class java.lang.String
brand
class java.lang.Integer
100
从上面可以看出,泛型方法随着我们的传入参数类型不同,执行的效果不同,拿到的结果也不一样。泛型方法能使方法独立于类而产生变化。
3.4 泛型通配符(上下界)
Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:
- 无边界的通配符,使用精确的参数类型
- 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
- 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
结构如下:
// 表示类型参数可以是任何类型
public class B<?> {
}
// 上界:表示类型参数必须是A或者是A的子类
public class B<T extends A> {
}
// 下界:表示类型参数必须是A或者是A的超类型
public class B<T supers A> {
}
上界示例:
class Info<T extends Number>{ // 此处泛型只能是数字类型
private T var ; // 定义泛型变量
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
public String toString(){ // 直接打印
return this.var.toString() ;
}
}
public class demo1{
public static void main(String args[]){
Info<Integer> i1 = new Info<Integer>() ; // 声明Integer的泛型对象
}
}
下界示例:
class Info<T>{
private T var ; // 定义泛型变量
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
public String toString(){ // 直接打印
return this.var.toString() ;
}
}
public class GenericsDemo21{
public static void main(String args[]){
Info<String> i1 = new Info<String>() ; // 声明String的泛型对象
Info<Object> i2 = new Info<Object>() ; // 声明Object的泛型对象
i1.setVar("hello") ;
i2.setVar(new Object()) ;
fun(i1) ;
fun(i2) ;
}
public static void fun(Info<? super String> temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类
System.out.print(temp + ", ") ;
}
}
4 泛型实现原理
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),
将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
泛型本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。
4.1 泛型的类型擦除原则
- 消除类型参数声明,即删除<>及其包围的部分。
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
4.2 擦除的方式
擦除类定义中的类型参数 - 无限制类型擦除
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如
擦除类定义中的类型参数 - 有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如
擦除方法定义中的类型参数
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。