Java:泛型的理解
本文源自参考《Think in Java》,多篇博文以及阅读源码的总结
前言
Java中的泛型每各人都在使用,但是它底层的实现方法是什么呢,为何要这样实现,这样实现的优缺点有哪些,怎么解决泛型带来的问题。带着好奇,我查阅资料进行了初步的学习,在此与诸位探讨。
一 类型参数
学过JAVA的人都知道泛型,明白大概怎么使用。在类上为:class 类名 {},在方法上为:public void 方法名 (T x){}。泛型的实现使得类型变成了参数可以传入,使得类功能多样化。
具体可分为5种情况:
- T是成员变量的类型
- T是泛型变量(无论成员变量还是局部变量)的类型参数,常见如
Class<T
>,List<T>
。 - T是方法抛出的Exception(要求
<T extends Exception>
) - T是方法的返回值
- T是方法的参数
1.1 泛型的实现
JAVA的泛型是基于编译器实现的,使用了擦除的方法实现,这是因为java1.5之后才出现了泛型,为了保持向后兼容而做出的妥协。
所谓擦除就是JAVA文件在编译成字节码时类型参数会被擦除掉,单独记录在其他地方。并且用类型参数的父类代替原有的位置。
假设参数类型的占位符为T,擦除规则如下:
<T>
擦除后变为Obecjt
<? extends A>
擦除后变为A
<? super A>
擦除后变为Object
这种规则叫做保留上界
编译器擦除类型参数后,通过JAVA的强制转换保证了类型参数在使用时的正确。如:在类型参数T中传入了类A,那么编译器会在所有类A将返回(抛出)类型参数T的代码处加上(A)进行强转.
举个栗子:
ArrayList<String> list = new ArrayList<String>();
list.add("123");
String b = list.get(0);
在编译后会变成
ArrayList list = new ArrayList();//没有参数即默认为Object
list.add("123");
String b = (String) list.get(0);
并且会在带有类型参数类的子类中形成桥方法保证了多态性。
具体参考官方解释如下
- Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
- Insert type casts if necessary to preserve type safety.
- Generate bridge methods to preserve polymorphism in extended generic types.
二 通配符?
在带有类型参数的类内部,代码仍然按照参数类型擦除后的父类来处理。但是擦除存在一个问题,在这种机制下泛型是不变的,而没有逆变和协变。
2.1 逆变与协变
协变和逆变网上有很多解释,显得模糊不清,我参考几个编程语言的官方解释后给出一个比较宽泛的定义。协变指能够使用比原始声明类型的派生程度更大(更具体的)的类型,逆变指能够使用比原始声明类型的派生程度更小(不太具体的)的类型。
如:
Object obj = new String("123");
这就是协变,将String这个更具体的(子类)类型赋给了原本较宽泛定义(父类)的类型Object。
JAVA不允许将父类赋给子类,自然Java不支持逆变。
网上很多博文说JAVA泛型也有逆变,我是不赞同的,那只是一种模拟的逆变,即有部分逆变的特性而且看起来像逆变,具体分析后文会给出
2.2 Java中的逆变与协变
在JAVA中,
List<Integer> b = new ArrayList<Integer>()
List<NumFber> a = b;
是无法通过编译器检查的。不允许这样做有一个很充分的理由:这样做将破坏要泛型的类型安全。如果能够将List<Integer>
赋给List<Number>
。那么下面的代码就允许将非Integer
的内容放入 List<Integer>
:
List<Integer> b = new ArrayList<Integer>();
List<Number> a = b; // illegal
a.add(new Float(3.1415));
因为a
是List<Number>
,所以向其添加Float
似乎是完全可行的。但是如果a
实际是List<Integer>
,那么这就破坏了蕴含在b
中定义的类型声明 —— 它是一个整数列表,这就是泛型类型不能协变的原因。但也因此使得泛型失去了多态的拓展性。
2.3 通配符解决协变
Java官方通过加入了通配符?
来解决泛型协变的问题。这样就能通过编译了:
List<Integer> b = new ArrayList<Integer>();
List<? extends Number> a = b;
可以解读为a
是一种带有Number
的List
集合类,在从a
中取出数据的时候统一当做Number
处理就行了。同时这也是符合里氏替换原则的
但是编译器会禁止你将将类Integer
放入a,即a.add(new Integer(1))//illegal
这也很合理,因为你声明的a
本来就没有限定a
包含的具体是哪个Number
子类,因此不准任何变量的添加保证了泛型的安全性。
解决往a
添加对象的方法也很简单
List<Object> b = new ArrayList<Object>();
List<? super Number> a = b;
a
是某种Number
父类的List
集合类,将ArrayList<Object>
赋给a
也是合情合理的,Object
确实是Number
的父类。这也符合里氏替换原则的
(网上大部分博文说这就是逆变,但是仔细想想逆变的官方定义,在JAVA中可以理解为:类T
是类S
的子类,而类A<T>
是类A<S>
的父类。仔细看看List<? super Number>
和List<Object>
的关系,在这里T
是Number
,而S
是Object
,但是List<? super Number>
从逻辑上来看真的是List<Object>
的子类吗,如果单纯从字面上来看List<? super Number>
是带有Number
父类的集合类,根据保留上界的擦除方法,应该擦除为List<Object>
,将一个List<Object>
赋给另一个List<Object>
是不存在任何逆变的。我在疑惑之下去谷歌查阅了资料,也没有英文资料说明JAVA泛型里这属于逆变)
Java:泛型的理解的更多相关文章
- 对java泛型的理解
正确的应用java泛型的特性可以更好的实现编程的开闭原则(对扩展开放,对修改关闭),这得益于java泛型提供的在程序运行时获取对象声明类型的特性. 静态语言的特性是在程序编译前进行声明,这样程序在编译 ...
- java泛型的理解
总体介绍泛型: 1.泛型是计算机程序中一种重要的思维方式,它将数据结构和算法与数据类型相分离,使得同一套数据结构和算法,能够应用于各种数据类型,而且还可以保证类型安全,提高可读性.在Java中,泛型广 ...
- Java泛型深入理解(转载)
原文地址 http://blog.csdn.net/sunxianghuang/article/details/51982979 泛型之前 在面向对象编程语言中,多态算是一种泛化机制.例如,你可以将 ...
- 对于Java泛型的理解
源起:查看COLLECIOTNS类 Q1:为什么java需要泛型? 因为java对于对象类型的确认在编译期,那么强制类型转换就可以通过编译,但是运行时的错误却无法避免,那么泛型的存在可以避免强制类型转 ...
- Java泛型深入理解
泛型的优点: 泛型的主要优点就是让编译器保留參数的类型信息,执行类型检查,执行类型转换(casting)操作,编译器保证了这些类型转换(casting)的绝对无误. /******* 不使用泛型类型 ...
- 你对Java泛型的理解够深入吗?
泛型 泛型提供了一种将集合类型传达给编译器的方法,一旦编译器知道了集合元素的类型,编译器就可以对其类型进行检查,做类型约束. 在没有泛型之前: /** * 迭代 Collection ,注意 Coll ...
- Java泛型简单理解
优点1: 没有使用泛型,向list集合中添加非字符串,运行时会报错:类型不匹配 ObjectList.java: package cn.nxl2018; import java.util.ArrayL ...
- 关于Java泛型深入理解小总结
1.何为泛型 首先泛型的本质便是类型参数化,通俗的说就是用一个变量来表示类型,这个类型可以是String,Integer等等不确定,表明可接受的类型,原理类似如下代码 int pattern; //声 ...
- java 泛型的理解与应用
为什么使用泛型? 举个例子: public class GenericTest { public static void main(String[] args) { List list = new A ...
随机推荐
- luogu 2331
给出 $n * 1$ 的矩阵,选出 $k$ 个互不重叠的子矩阵,使得其最大$sum[i]$ 为列的前缀和设 $f[i][j]$ 表示前 $i$ 个数选出 $j$ 个互不重叠的子矩阵的最大价值若第 $i ...
- windows游戏编程 创建WIN32一个HelloWOrld程序
本系列文章由jadeshu编写,转载请注明出处.http://blog.csdn.net/jadeshu/article/details/22449085 作者:jadeshu 邮箱: jades ...
- MySQL数据分析-(5)数据库设计之ER模型
大家好,我是jacky,很高兴跟大家分享本课时的内容,从本节课开始,就开始了我们第二章的学习,第一章我们抛出了若干问题,从第二章开始往后,都是解决问题的一个过程: 第一章的案例中,我们拿手机销售公司举 ...
- 2016百度之星资格赛 Problem A(前缀积与求逆元)
题意:给出一个字符串,每次询问给出x和y要求算出从x到y的每个字符的(ASCII 码值-28)的值的积(mod9973). 分析:首先的想法肯定是算出每个位置的前缀积,然后只要F[y]/F[x-1]即 ...
- slax linux的定制
由于数据结构教学的需要,需要用到linux,要求就是小,启动快,可定制性强,恰好slax正好满足要求,以下就是定制slax linux的过程记录: 什么是Slax Slax是一个基于Linux的Liv ...
- BZOJ3907网格
这东西是拿Cat思想搞得组合数学. 首先做这个需要会用网格法或折线法分析Cat的$C_{2n}^n-C_{2n}^{n-1}$是怎么来的. 网格法:假如没有限制,从(0,0)到(n,n)的方案数为$C ...
- Golang文件操作整理
基本操作 文件创建 创建文件的时候,一定要注意权限问题,一般默认的文件权限是 0666 关于权限的相关内容,具体可以参考鸟叔p141 这里还是再回顾下,文件属性 r w x r w x r w x,第 ...
- ubuntu mysql 的安装、配置、简单使用,navicat 连接
MySQL 的安装 1. 先更新 apt 安装中心: apt update 里面会有默认最新的mysql 的包. 2.安装msyql : sudo apt-get install mysql-serv ...
- OLAP、OLTP的介绍CBO/RBO
OLTP与OLAP的介绍 数据处理大致可以分成两大类:联机事务处理OLTP(on-line transaction processing).联机分析处理OLAP(On-Line Analytical ...
- vim 快捷键 清空文件所有内容
vim清空文件所有内容 在使用vim编辑器的时候,有时候编辑一个文件,而文件内容比较多,如果需要快速清空整个文件,可以使用一下命令: 在命令模式下,首先执行 gg 这里是跳至文件首行 再执行: dG ...