泛型定义
Java泛型(generics)是JDK5中引入的一个新特性, 泛型提供了编译时期的类型安全检查机制, 这机制允许程序员在编译时监测到非法的类型. 泛型的本质是不确定的类型(参数类型), 也就是说所操作的数据类型被指定为一个参数类型,这个参数泛型不会存在于JVM虚拟机,所以说在java中泛型其实是一种伪泛型.
为什么使用泛型?
- 可以增强编译时错误监测,减少因类型问题引发的运行时的异常
(),具有更强的类型检查.- 可以避免类型转换.
- 方法的形参中使用泛型,增加程序的复用性.
public class Generics{
public static void main(String[] args){
gMethod01()
gMethod02()
}
static void gMethod01{
List list = new ArrayList();//没有使用泛型
list.add("hello");
String str = list.get(0);// 需要强转(向下转型)
}
static void gMethod02{
List list = new ArrayList<>();//使用了泛型
list.add("hello");
String str = list.get(0);//不需要强转
}
}
泛型的种类
- 泛型类
泛型类格式
public class calssName
{} - 泛型接口
泛型接口格式
public interface IName
{} - 泛型方法
泛型方法格式
private
boolean gMethod(K k1,V v1) {};
使用泛型方法
MethodUtil.
gMethod(k1,v1)
/**
泛型类(泛型接口 interface className
class calssName
泛型通常使用的字母(来自官方建议),
E - Element(Java Collections Framework广泛使用)
K - Key
N - Number
T - Type
V - value
S,U,V ......
*/
interface Generics{ //泛型接口
}
interface Generics{ //泛型接口
}
//泛型接口的使用
/*
这种是不确定泛型T的类型, 必须在类名后面添加声明泛型T
*/
class GenericsImpl implements Generics{ /
}
/*
这种是已经确定了泛型T的类型为String, 类前面不要加上泛型的声明.
*/
class GenericsImpl2 implements Generics{
}
public class Box{ //泛型类
private T t;
public void setT(T t){ //注意这不是泛型方法.
this.t = t;
}
public T getT(){ // 注意这不是泛型方法
return t;
}
public void test1(List lists){ // 注意这不是泛型方法
}
public void test2(List> list){ //注意这不是泛型方法, 通配符"?" 后面会后介绍
}
public void testT(T t){ // 泛型方法 (此T 非类上面的T)
}
}
类型参数&类型实参
Box
中的T 为类型参数
Box中的String为类型实参
The Diamond钻石运算符也叫菱形运算符
JDK7以下版本
Box
strBox = new Box ();
JDK7及以上版本
Box
strBox = new Box<>(); // The Diamond(菱形) 类型推断
类型推断在本文后面有解释
原始类型
缺少实际类型变量的泛型就是一个原始类型 后面会介绍原始类型和普通类型的区别.
public Box{
public static void main(String[] args){
Box box = new Box(); //这个Box 就是Box 的原始类型
ArrayList list = new ArrayList();// 这个ArrayList 就是 ArrayList的原始类型
}
}
受限的类型参数
对泛型变量的范围作出限制
单一限制:
多种限制:
extends表达的含义: 这里指的是广义上的"扩展",兼有"类继承" 和 "接口实现" 之意
多种限制下的格式语法要求:如果上限类似是一个类, 必须第一个位置标出,否则编译失败()
单一限制
public class Box{
private T t;
public setT(T t){
this.t = t;
}
public T getT(){
return t;
}
public void inspect1(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: "+ u.getClass().getName)
}
public void inspect2(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: "+ u.getClass().getName)
}
public static void main(String[] args){
Box integerBox1 = new Box<>();
integerBox1.set(new Integer(10));
integerBox1.inspect1(10L);//succes
integerBox1. inspect1("some text") //编译失败 err
integerBox1. inspect2("some text") //编译成功 ok
}
}
多重限制
public class Test{
static class A{
}
static class A1{
}
static interface B{
}
static interface C{
}
static class D{ // err 编译失败,
}
static class D2{//err 编译失败,因为java是单继承
}
/*
具有多个限定的类型变量是范围中列出的所有类型的子类型.
范围中列出的子类型, 最多只能有一个类,并且这个类必须在第一个位置,
否则编译失败.
*/
static class D1{ // OK 编译成功
}
}
为什么使用受限类型?
因为使用受限类型,在方法或者是类中泛型实例可以使用受限类里面的公有方法. 从而可以达到代码复用.
class Fruits{
public boolean isFruits(T t){
if (t instanceof Fruits){
return true;
}
return false;
}
}
class Apple extends Fruits{
}
class Orange extends Fruits{
}
public class Test{
// 计算水果有多少个?
/*
方法的实现很简单,但是不能编译, 因为isFruits方法仅适用于水果类型,
要解决此问题, 请使用T extends Fruits 来限定类型参数
*/
public static int countFruits1(T[] fruitArray){
int count = 0;
for(T e: fruitArray) {
if (e.isFruits(e)){ //err 编译失败
count ++ ;
}
}
return count;
}
//使用了限定类型参数, 就能使用Fruits类里面的isFruits方法.
public static int countFruits2(T[] fruitArray){
int count = 0;
for(T e: fruitArray) {
if (e.isFruits(e)){ //ok 编译成功
count ++ ;
}
}
return count;
}
public static void main(String[] args){
/*
把参数类型的检测,提前到编译时期,减少运行报异常
*/
String[] sts = new String[10];
countFruits1(sts);
countFruits2(sts);//err 编译失败,因为参数限定了为水果类.
Apple[] apples = new Apple[10];
countFruits1(apples);// ok
countFruits2(apples) ;//ok
}
}
泛型的类型关系(继承与子类型)
Integer 继承 Number, 但是 Box
不等于 Box
Box与 Box 的父类是Object.
ArrayList 继承 List , List 继承 Collection, 而 List
是等于 ArrayList ;例如: List
lists = new ArrayList<>();
泛型类型关系示例
class Box{
}
public class Test{
public static void main(String[] args){
Object someObject = new Integer(10);
someObject = someInteger;// 第一组ok
/*
由于Integer是一种Object,因此允许分配,当时Integer也是Number的一种,
因此下面的代码也是有效的.
*/
someMethod(new Integer(10)); //ok
someMethod(new Double(10.0))//ok
/*
给定两种具体类型A 和 B(例如Number 和Integer),无论A和B是否相
关,MyClass 与MyClass 的公共父对象都是Object.
其实在jvm 里面是没有泛型的, 泛型其实是伪泛型,在jvm里面要进行泛型
的擦除,虚拟机是不知道泛型的类型的. 大部分全都擦除为Object类型.
例如 Box.class 其实与Box.class 是相等的.
Box box = new Box<>();
Box box1 = new Box<>();
if (box.getClass() == box1.getClass()){
System.out.println("true");
}
*/
boxTest(new Box());//err 编译失败
}
public static void someMethod(Number n){
}
//Box 与Box 没有任何关系
public static void boxTest(Box){
}
}
泛型类型推断
理解编译器是如何利用目标类型来推算泛型变量的值 ?
类型推断是Java编译器查看每个方法调用和相应声明以确定适用的类型参数的能力.
推断算法确定参数的类型,以及确定结果是否被分配或返回类型(如果有).最后,
推断算法尝试找到所有仪器适用的具体类型.
当我们没有确定某个泛型的类型的时候, 虚拟机就会去类型推断
//这是没有确定泛型类型的时候
Serializable s1 = pick("d",new ArrayList());
//这是我们已经确定的泛型类型,虚拟机就不会进行类型推断
Serializable s2 = Test.pick("d",new ArrayList);
目标类型有: 变量声明; 赋值; 返回语句; 数组初始化器; 方法或构造函数初始
Lambda表达主体; 条件表达式; 转换表达式.
来自官方例子
public class Test{
static T pick(T t1,T t2){
return t1
}
public static void main(String[] args){
/*
1, String
public final class String implements java.io.Serializable,.....
2,ArrayList
public class ArrayList extends AbstractList implements
List,RandomAccess,Cloneable,java.io.Serializable
从上面分析看出, 推算到Serializable 是他们共有的一个类 . 所以推断出
是Serializable
*/
Serializable s = pick("d",new ArrayList());
}
}
Java泛型 PECS( )的原则
为何要PECS原则?
为了提升API的灵活性.
PECS原则总结
如果要从集合中读取类型T的数据,()并且不能写入,可以使用 (? extends) 通配符.(Producer Extends)
如果要从集合中写入类型T的数据,并且不需要读取(),可以使用 (? super) 通配符.(Consumer Super)
如果既要存又要取,那么就不要使用任何通配符.
注意:
如果我用反射可以绕过上面的权限吗? 好像在的jdk1.6之后(待考证), 是不能调用的.编译不会报错,但是运行会报异常
通配符( )
泛型中的问号"?" 叫"通配符"
通配符有两种,受上下控制的通配符和不受控制的通配符.
通配符的适用范围:
- 参数类型
- 字段类型
- 局部变量类型
- 返回值类型.(注意: 访问一个具体类型的值较好)
上限通配符
public class Test{
public static void main(String[] args){
List integerList = Arrays.asList(1,2,3);
/*
err 编译失败 因为在上面泛型的类型关系的时候,
虽然Integer 继承 Number但是List
与List没有任何关系.所以不能调用sumOfList方法.
如果对integerList进行求和运算,需要重新写一个sumOfIntegerList方法,
不能使用方法重载, 因为泛型被擦除了.
这时候通配符"?" 就出来. 把下面的方法改造一下(? extends Number)
*/
sumOfList(integerList); // 编译失败
sumOfList1(integerList); // 编译成功
List doubleList = Arrays.asList(1.1,2.2,3.3);
sumOfList1(doubleList); // 编译成功
}
/*
要编写在Number类型的列表和Number的子类型(如Integer,Double和Float)
上工作的方法,一般会指定List extends Number>; List比
List extends Number> 更具有局限性,因为前者只匹配Number类型的列表,
而后者匹配Number 类型的列表或其任何子类.
*/
public static double sumOfList1(List extends Number> list){
// extends 叫上限,只可读,不能写入. 上面PECS 有介绍.
list.add(1); // 这种是编译报错的
/*
注意: 如果我用反射可以调用吗? 最新版的jdk 是不能调用的.
会报 UnsupportedOperationException
*/
//反射代码
/*Class> clazz = list.getClass();
Method addMethod = class.getMethod("add",java.lang.Object.class);
addMethod.setAccessible(true);
addMethod.invoke(list,10);
System.out.println(list.toString());*/
double s = 0.0;
for (Number number : list){
s += number.doubleValue();
}
return s;
}
public static double sumOfList(List list){
double s = 0.0;
for (Number number : list){
s += number.doubleValue();
}
return s;
}
}
下限通配符
//CS Consumer消费者 list理解为消费者 添加数据.
public static double addNumber(List super Integer> list){
//PECS原则的 PE(Producer extends )原则
//当只想从集合获取元素,把这个集合看成生产者,使用 extends T>.
//PESC原则CS(Consumer super)原则
// 当你想增加元素到集合中, 把这个集合看成消费者, 请使用 super T>.
Integer tmp = list.get(0) //编译失败, 违背了PECS原则.
for(int i = 1;i <= 10){
list.add(i);//编译成功.
}
}
上限和下限在Collections源码中的使用.
Collections.java-->
public static void copy(List super T> dest, List extends T> src) {
//code....
}
不受限的通配符
//泛型退化掉了, 不能使用List中任何依赖类型参数T 的方法,只能使用List里
//面的方法.
public static void printList1(List> list){
list.add("sss");//编译错误.
list.add(1); //编译失败
list.size(); //编译成功
list.add(null);//编译成功
list.get(2); //编译成功
list.contains(2); //编译成功
}
不受限的通配符,主要是用在类型的检测和匹配上面.
泛型擦除
JVM虚拟机, 是不支持泛型的. 在虚拟机里面运行的时候, 是没有泛型了. 在C++ 里面有temple(泛型) , kotlin 也是伪泛型.
为什么java会使用伪泛型,因为在jdk5之前是没有泛型, 主要是为了向下兼容,才引入了伪泛型.
功能: 保证了泛型不在运行时出现
类型消除应用的场所:
编译器会把泛型类型中所有的类型参数替换为它们的上(下)限,如果没有对类型参数做出限制, 那么就替换为Object类型. 因此,编译出的字节码仅仅包含了常规类,接口 和方法.
在必要时编译器会插入类型转换以保持类型安全.
编译器生成桥方法以及在扩展泛型时保持多态性.
Brdge Methods 桥方法
当编译一个扩展参数化类的类,或一个实现了测试化接口的接口是, 编译器有可能因此要创建一个合成方法, 名为桥方法. 它是类型擦除过程中的一部分.
/*
如果类型参数不受限制, 则将通用类型中的所有类型参数替换为其边界(上下限)或Object.
因此, 产生的字节码仅包含普通的类, 接口和方法.
必要时插入类型转换,以保持类型安全.
生成桥接方法以在扩展的泛型类型中保留多肽.
类型擦除可确保不会为参数化类型创建新的类,因此, 泛型不会产生运行是开销. */
public class TypeErasure{
static class Pair{
private T value;
public T getValue(){
return value;
}
public void setValue(T value){
this.value = value;
}
}
public static void main(String[] args){
Pair pair = new Pair<>();
pair.setValue("myString");
System.out.println("pair: "+ pair.getValue());
}
}
下面是TypeErasure.class 字节码. 看到setValue(Ljava/lang/Object;)V 说明 泛型T , 已经擦除成Object了.
下面是泛型擦除.如果有extends, 一般会把T擦除成第一个泛型实参.
interface ITypeE{
void inType();
}
class TypeE implements ITypeE{
@Override
public void inType() {
}
}
public class TypeErasure {
private T iTpeE ;
public T getT(){
return iTpeE;
}
public void setT(T iTpeE){
this.iTpeE = iTpeE;
}
public static void main(String[] args) {
TypeErasure typeETypeErasure = new TypeErasure<>();
typeETypeErasure.setT(new TypeE());
ITypeE t = typeETypeErasure.getT();
System.out.println(t.getClass());
}
}
如果有extends, 一般会把T擦除成第一个泛型实参.
下面是擦除编译器使用桥方法的实例.
File:ITypeE.java
public interface ITypeE {
void inType(T t);
}
File:TypeE.java
public class TypeE implements ITypeE{
@Override
public void inType(Integer integer) {
}
}
TypeE.class,在编译扩展参数化类或实现参数化接口的类或接口时,作为类型擦除过程的一部分,编译器可能需要插件一个称为桥接方法的综合方法,. 你通常不必担心桥接方法.如果在堆栈跟踪的时候, 可能会出现疑惑.
在字节码中, 桥接方法会调用当前方法, 在下图的第39行.}
方法擦除带来的问题.
在普通方法中,不能重写equals(T value)方法,因为T 会把类型擦除成Object类型,
public class TypeE{
public boolean equals(T t) { //err 编译失败
}
}
思考:
泛型无法使用原始类型来创建泛型
无法创建类型参数的实例
无法创建参数化类型的静态变量
无法对参数化类型使用转换或者instanceof关键字
无法创建参数化类型的数组()
无法创建,捕获或者是抛出参数化类型对象
当一个方法的所有重载方法的形参类型擦除后,如果他们是具有相同的原始类型,那么次方法不可重载.
面试中遇到问题(思考...)
数组(Array)中可以用泛型吗?
你可以把List
传递给一个接收List
Java中Set与Set>到底区别在哪里 ?.(Java中List> 和原始的List 的区别?)
Java中List> 和List
之间的区别?
List>是一个未知类型的List,而List其实一个任意类型的List. 你可以把List ,List 赋值给List>, 却不能把List 赋值给List
Java中的泛型是什么? 使用泛型的好处是什么?
泛型是一种参数化类型的机制.
好处:
1,代码类型检测提前
2,代码复用.
....
泛型是怎么工作, 泛型如何擦除?
什么是泛型中的限定通配符,和非限定通配符?
List extends T> 和 List super T>之间有什么区别?
泛型类型变量能不能是基本数据类型?为什么?
ArrayList
arrayList = new ArrayList ();
if(arrList instanceof ArrayList)
if(arrayList instancesof ArrayList>)中那个if可以运行,为什么?
C++ 模板和java泛型之间有何不同?
C++ 里面会使用宏指令,它会替换成模板代码.
java是伪泛型.
最后来个面试附加题
- Plate
- Plate
- Plate>
- Plate
- Plate extends T>
- Plate supter T>
它们之间的区别?