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,即形如和<?>的类型参数都被替换为Object。
擦除类定义中的类型参数 - 有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。
擦除方法定义中的类型参数
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。
Java核心知识1:泛型机制详解的更多相关文章
- zookeeper核心知识与投票机制详解
Zookeeper数据模型与session机制:zookeeper的数据模型有点类似于文件夹的树状结构,每一个节点都叫做znode,每一个节点都可以有子节点和数据,就好像文件夹下面可以有文件和子文件夹 ...
- Java 反射 设计模式 动态代理机制详解 [ 转载 ]
Java 反射 设计模式 动态代理机制详解 [ 转载 ] @author 亦山 原文链接:http://blog.csdn.net/luanlouis/article/details/24589193 ...
- Java进阶 | Proxy动态代理机制详解
一.Jvm加载对象 在说Java动态代理之前,还是要说一下Jvm加载对象的过程,这个依旧是理解动态代理的基础性原理: Java类即源代码程序.java类型文件,经过编译器编译之后就被转换成字节代码.c ...
- 【java虚拟机】垃圾回收机制详解
作者:平凡希 原文地址:https://www.cnblogs.com/xiaoxi/p/6486852.html 一.为什么需要垃圾回收 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分 ...
- Java中String对象创建机制详解()
一String 使用 private final char value来实现字符串存储 二Java中String的创建方法四种 三在深入了解String创建机制之前要先了解一个重要概念常量池Const ...
- 转 Java虚拟机5:Java垃圾回收(GC)机制详解
转 Java虚拟机5:Java垃圾回收(GC)机制详解 Java虚拟机5:Java垃圾回收(GC)机制详解 哪些内存需要回收? 哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无 ...
- java面试题之----JVM架构和GC垃圾回收机制详解
JVM架构和GC垃圾回收机制详解 jvm,jre,jdk三者之间的关系 JRE (Java Run Environment):JRE包含了java底层的类库,该类库是由c/c++编写实现的 JDK ( ...
- 【转】java的动态代理机制详解
java的动态代理机制详解 在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们 ...
- Java web 入门知识 及HTTP协议详解
Java web 入门知识 及HTTP协议详解 WEB入门 WEB,在英语中web即表示网页的意思,它用于表示Internet主机上供外界访问的资源. Internet上供外界访问的Web资 ...
随机推荐
- SSM集成Thymeleaf
创建项目 Spring+SpringMVC+MyBatis的配置文件 数据库内容 dao层+service层+controller层 映射文件 前端简单页面 配置tomcat,运行显示 总体项目架构 ...
- CentOS 7 源码安装 Zabbix 6.0
Zabbix 主要有以下几个组件组成: Zabbix Server:Zabbix 服务端,是 Zabbix 的核心组件.它负责接收监控数据并触发告警,还负责将监控数据持久化到数据库中. Zabbix ...
- 浅析Java反射--Java
前言 上篇文章我们提到了可以使用反射机制破解单例模式.这篇文章我们就来谈一谈什么是反射,反射有什么用,怎么用,怎么实现反射. 概述 Java的反射(reflection)机制:是指在程序的运行状态中, ...
- 『现学现忘』Docker基础 — 33、Docker数据卷容器的说明与共享数据原理
目录 1.数据卷容器的说明 2.数据卷容器共享数据原理 3.总结 4.练习:MySQL实现数据共享 1.数据卷容器的说明 (1)什么是数据卷容器 一个容器中已经创建好的数据卷,其它容器通过这个容器实现 ...
- Apache Tomcat如何高并发处理请求
介绍 作为常用的http协议服务器,tomcat应用非常广泛.tomcat也是遵循Servelt协议的,Servelt协议可以让服务器与真实服务逻辑代码进行解耦.各自只需要关注Servlet协议即可. ...
- MySQL面试题--常见的四种隔离级别
什么是事务 事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消.也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做. 事务的结束有 ...
- 为什么redis 需要把所有数据放到内存中?
答:Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数 据写入磁盘.所以 redis 具有快速和数据持久化的特征.如果不将数据放在内存中, 磁盘 I/O 速度为严重影响 red ...
- SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来 访为 map. ConcurrentHashMap 使用分段锁来保证在多线程下的性能. ConcurrentHa ...
- BeanFactory – BeanFactory 实现举例?
Bean 工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从正真的应用代码中分离. 最常用的BeanFactory 实现是XmlBeanFactory 类.
- Kafka 消费者是否可以消费指定分区消息?
Kafa consumer消费消息时,向broker发出fetch请求去消费特定分区的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,customer拥 ...