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,即形如和<?>的类型参数都被替换为Object。

擦除类定义中的类型参数 - 有限制类型擦除

当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。

擦除方法定义中的类型参数

擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。

Java核心知识1:泛型机制详解的更多相关文章

  1. zookeeper核心知识与投票机制详解

    Zookeeper数据模型与session机制:zookeeper的数据模型有点类似于文件夹的树状结构,每一个节点都叫做znode,每一个节点都可以有子节点和数据,就好像文件夹下面可以有文件和子文件夹 ...

  2. Java 反射 设计模式 动态代理机制详解 [ 转载 ]

    Java 反射 设计模式 动态代理机制详解 [ 转载 ] @author 亦山 原文链接:http://blog.csdn.net/luanlouis/article/details/24589193 ...

  3. Java进阶 | Proxy动态代理机制详解

    一.Jvm加载对象 在说Java动态代理之前,还是要说一下Jvm加载对象的过程,这个依旧是理解动态代理的基础性原理: Java类即源代码程序.java类型文件,经过编译器编译之后就被转换成字节代码.c ...

  4. 【java虚拟机】垃圾回收机制详解

    作者:平凡希 原文地址:https://www.cnblogs.com/xiaoxi/p/6486852.html 一.为什么需要垃圾回收 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分 ...

  5. Java中String对象创建机制详解()

    一String 使用 private final char value来实现字符串存储 二Java中String的创建方法四种 三在深入了解String创建机制之前要先了解一个重要概念常量池Const ...

  6. 转 Java虚拟机5:Java垃圾回收(GC)机制详解

    转 Java虚拟机5:Java垃圾回收(GC)机制详解 Java虚拟机5:Java垃圾回收(GC)机制详解 哪些内存需要回收? 哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无 ...

  7. java面试题之----JVM架构和GC垃圾回收机制详解

    JVM架构和GC垃圾回收机制详解 jvm,jre,jdk三者之间的关系 JRE (Java Run Environment):JRE包含了java底层的类库,该类库是由c/c++编写实现的 JDK ( ...

  8. 【转】java的动态代理机制详解

    java的动态代理机制详解   在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们 ...

  9. Java web 入门知识 及HTTP协议详解

     Java  web  入门知识 及HTTP协议详解 WEB入门 WEB,在英语中web即表示网页的意思,它用于表示Internet主机上供外界访问的资源. Internet上供外界访问的Web资 ...

随机推荐

  1. Golang 数据结构

    每种语言在实现数据结构有些许不同.go 是如何实现的呢? 1. 数组 Array go 中数组是相同的元素组成的集合,计算机会为数组分配一段连续的内存来保存元素,可以利用索引快速访问元素. go 中数 ...

  2. asp.net MVC 事务

    使用事务的目的是什么? 保证事务范围内的代码,要么全部执行,要么全不执行,也就是出错回滚. 写在数据库脚本里很好理解,但是用在应用程序层面,没有看到catcha error rallback的代码,心 ...

  3. RabbitMQ Go客户端教程2——任务队列/工作队列

    本文翻译自RabbitMQ官网的Go语言客户端系列教程,本文首发于我的个人博客:liwenzhou.com,教程共分为六篇,本文是第二篇--任务队列. 这些教程涵盖了使用RabbitMQ创建消息传递应 ...

  4. python中判断为None

    python中经常会有判断一个变量是否为None的情况,这里列举三种方式: if not x if x is None if not x is None *********************** ...

  5. 羽夏看Win系统内核——内存管理篇

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...

  6. 羽夏笔记——Hook攻防基础

    写在前面   本笔记是由本人独自整理出来的,图片来源于网络.本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正. 如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你 ...

  7. 使用docker配置laravel5.5环境

    简介 简述一下我用docker来配置laravel环境 软件镜像 php:7.2-rc-fpm-alpine mysql:5.6 nginx:1.12.2-alpine 配置环境 Ubuntu:16. ...

  8. [Lua游戏AI开发指南] 笔记零 - 框架搭建

    一.图书详情 <Lua游戏AI开发指南>,原作名: Learning Game AI Programming with Lua. 豆瓣:https://book.douban.com/su ...

  9. XStream类对象把List<javaBean>()转成json数据

    [省市联动] Servlet端: XStream把list转成json数据 //JSONArray-->变成数组/集合[] //JSONObject-->变成简单的数据{name:ayee ...

  10. Gradle依赖声明类型

    compileOnly -用于编译生产代码所必需的依赖关系,但不应作为运行时类路径的一部分 implementation(取代compile)-用于编译和运行时 runtimeOnly(取代runti ...