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. 简悦+Logseq 搭建本地化个人知识库

    最近在少数派上看到了 简悦 +Logseq 个人知识库搭建 | 从零开始完全指南 - 少数派, 一时间感觉打开了新世界,其实我很早就买了简悦 2.0,但由于一直没有很好的使用场景,外加配置实在过于复杂 ...

  2. python写一个web目录扫描器

    用到的模块urliib error #coding = utf-8 #web目录扫描器 by qianxiao996 #博客地址:https://blog.csdn.net/qq_36374896 i ...

  3. Django1.11 添加markdown语法支持

    pip install markdown 在view.py 的视图界面:导入,圈起来的那两个包 对post进行处理, models.py 详情如下  测试,效果如图

  4. Prometheusbu部署使用-1

    Prometheus+grafana部署使用 主机列表: 192.168.161.130 : Prometheus 192.168.161.128 : node-1 192.168.161.129 : ...

  5. 为什么我们调用 start()方法时会执行 run()方法,为什么 我们不能直接调用 run()方法?

    当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码. 但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码, 只会把 run 方法当作普通方法 ...

  6. Elasticsearch 对于大数据量(上亿量级)的聚合如何实现?

    Elasticsearch 提供的首个近似聚合是 cardinality 度量.它提供一个字段的基数, 即该字段的 distinct 或者 unique 值的数目.它是基于 HLL 算法的.HLL 会 ...

  7. 哪个类包含 clone 方法?是 Cloneable 还是 Object?

    java.lang.Cloneable 是一个标示性接口,不包含任何方法,clone 方法在 object 类中定义.并且需要知道 clone() 方法是一个本地方法,这意味着它是由 c 或 c++ ...

  8. js--事件循环机制

    前言 我们知道JavaScript 是单线程的编程语言,只能同一时间内做一件事,按顺序来处理事件,但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行,这又是为什么呢?本文来总结一下js 的事件 ...

  9. 单例模式 | C++ | Singleton模式

    Singleton 模式 单例模式(Singleton Pattern)是 C++/Java等语言中最简单的设计模式之一.这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式. 这种模式实 ...

  10. 论Hello World 有多少种输出方法:

    论Hello World 有多少种输出方法: C: printf("Hello Word!"); C++: cout<<"Hello Word!"; ...