Java-泛型

Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数类型,也就是说所操作的数据类型被指定为一个参数。为了向下兼容,虚拟机其实是不支持泛型,所以Java实现的是一种伪泛型机制,也就是说Java在编译期擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码,所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息。

为什么使用泛型?

泛型可以增强编译时错误检测,减少因类型问题引发的运行时异常(只要编译期没有警告,那么运行期就不会出现ClassCastException) 泛型具有更强的类型检查 泛型可以避免类型转换 泛型可以泛型算法,增加代码复用性

Java中的泛型

泛型类 泛型接口 泛型方法

泛型类格式:class name<T1, T2, ..., Tn> 定义格式:private <K,V> boolean compare(Pair<K,V> p1, Pair<K,V> p2) 调用格式:Util.<K, V>compare(p1,p2)

常见类型变量名称

最常见的类型变量名有:

E:元素(在Java集合框架中有广泛的应用) K:键 N:数字 T:类型 V:值 S,U,V 等:第二,第三,第四个类型

参数化类型
  • 参数化类型:

把类型当参数一样传递 <数据类型>只能是引用类型(泛型的副作用)

  • 举个例子:

Plate<T>中的”T”称为类型参数 Plate<Banana>中的”Banana”称为实际类型参数 Plate<T> 整个称为泛型类型 Plate<Banana>整个称为参数化的类型ParameterizedType

类型参数VS类型实参

“类型参数”与“类型变量”的不同 Foo<T>中的T为类型参数 Foo<String>中的String为类型变量

The Diamond钻石运算符

JDK7以下版本 Box<Integer> integerBox = new Box<Integer>(); JDK7及以上版本 Box<Integer> integerBox1 = new Box<>();// The Diamond(菱形) 类型推断

原始类型

缺少实际类型变量的泛型就是一个原始类型 举例:

 

java

代码解读

复制代码

class Box<T>{} Box b = new Box(); //这个Box就是Box<T>的原始类型

泛型擦除

整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

java

代码解读

复制代码

ArrayList<Int> arr1 = new ArrayList(); ArrayList<String> arr2 = new ArrayList(); //result true System.out.println(arr1.getClass() == arr2.getClass()); // true

由此可见通过运行时获取的类信息是完全一致的,泛型类型被擦除了。擦除后只留下原始类型,这里也就是ArrayList

  • 代码查看:
  • 示例:

ConditionalPlate

 

java

代码解读

复制代码

package com; import java.util.ArrayList; import java.util.List; public class ConditionalPlate<T> implements Plate<T> { private List<T> items = new ArrayList<T>(10); public ConditionalPlate(){ } @Override public void set(T t) { items.add(t); } @Override public T get(){ int index = items.size() -1; if(index>= 0){ return items.get(index); }else{ return null; } } @Override public String toString() { return "Plate{" + "items=" + items + '}'; } // @Override // public boolean equals(T t) { // return super.equals(t); // } @Override public boolean equals(Object obj) { return super.equals(obj); } }

Plate

 

java

代码解读

复制代码

package com; public interface Plate<T> { public void set(T t); public T get(); }

  • javac编译源文件
  • javap -c 查看生成的字节码
 

yaml

代码解读

复制代码

PS E:\project\com> javac .\ConditionalPlate.java .\Plate.java PS E:\project\com> javap -c .\ConditionalPlate.class Compiled from "ConditionalPlate.java" public class com.ConditionalPlate<T> implements com.Plate<T> { public com.ConditionalPlate(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: new #2 // class java/util/ArrayList 8: dup 9: bipush 10 11: invokespecial #3 // Method java/util/ArrayList."<init>":(I)V 14: putfield #4 // Field items:Ljava/util/List; 17: return public void set(T); Code: 0: aload_0 1: getfield #4 // Field items:Ljava/util/List; 4: aload_1 5: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 10: pop 11: return public T get(); Code: 0: aload_0 1: getfield #4 // Field items:Ljava/util/List; 4: invokeinterface #6, 1 // InterfaceMethod java/util/List.size:()I 9: iconst_1 10: isub 11: istore_1 12: iload_1 13: iflt 27 16: aload_0 17: getfield #4 // Field items:Ljava/util/List; 20: iload_1 21: invokeinterface #7, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 26: areturn 27: aconst_null 28: areturn public java.lang.String toString(); Code: 0: aload_0 1: getfield #4 // Field items:Ljava/util/List; 4: invokedynamic #8, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/util/List;)Ljava/lang/String; 9: areturn public boolean equals(java.lang.Object); Code: 0: aload_0 1: aload_1 2: invokespecial #9 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z 5: ireturn }

1: invokespecial #1 // Method java/lang/Object."<init>":()V 可以看到ConditionalPlate构造函数里显示的类型已经是object

功能:保证了泛型不在运行时出现

  • 类型消除应用的场合:

编译器会把泛型类型中所有的类型参数替换为它们的上(下)限,如果没有对类型参数做出限制,那么就替换为Object类型。因此,编译出的字节码仅仅包含了常规类,接口和方法。 在必要时插入类型转换以保持类型安全。 生成桥方法以在扩展泛型时保持多态性

  • Bridge Methods 桥方法

当编译一个扩展参数化类的类,或一个实现了参数化接口的接口时,编译器有可能因此要创建一个合成方法,名为桥方法。它是类型擦除过程中的一部分

  • 示例代码查看桥方法:

BananaPlate

 

java

代码解读

复制代码

package com; import java.util.ArrayList; import java.util.List; public class BananaPlate implements Plate<Banana> { private List<Banana> items = new ArrayList<>(10); @Override public void set(Banana banana) { items.add(banana); } @Override public Banana get() { return items.get(0); } }

javap -c 查看字节码

 

yaml

代码解读

复制代码

PS E:\project\com> javap -c .\BananaPlate.class Compiled from "BananaPlate.java" public class com.BananaPlate implements com.Plate<com.Banana> { public com.BananaPlate(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: new #2 // class java/util/ArrayList 8: dup 9: bipush 10 11: invokespecial #3 // Method java/util/ArrayList."<init>":(I)V 14: putfield #4 // Field items:Ljava/util/List; 17: return public void set(com.Banana); Code: 0: aload_0 1: getfield #4 // Field items:Ljava/util/List; 4: aload_1 5: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 10: pop 11: return public com.Banana get(); Code: 0: aload_0 1: getfield #4 // Field items:Ljava/util/List; 4: iconst_0 5: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 10: checkcast #7 // class com/Banana 13: areturn public java.lang.Object get(); Code: 0: aload_0 1: invokevirtual #8 // Method get:()Lcom/Banana; 4: areturn public void set(java.lang.Object); Code: 0: aload_0 1: aload_1 2: checkcast #7 // class com/Banana 5: invokevirtual #9 // Method set:(Lcom/Banana;)V 8: return }

查看字节码可以看到有两个get函数,一个是自定义返回值的get,一个是object默认的get

 

csharp

代码解读

复制代码

public com.Banana get(); 10: checkcast #7 // class com/Banana

通过字节码可以看到这里的返回值类型被强转了

泛型擦除的残留

查看class文件的时候泛型是没有擦除的,还是能看到泛型

这里看到的其实是签名而已,还保留了定义的格式,这样子,对于分析字节码是有好处的

泛型方法的类型擦除

消除方法:同对泛型类的处理 无限制:替换为Object 有限制:替换为第一受限类型

Java编译器具体是如何擦除泛型的?
  1. 检查泛型类型,获取目标类型
  2. 擦除类型变量,并替换为限定类型 如果泛型类型的类型变量没有限定(<T>),则用Object作为原始类型 如果有限定(<T extends XClass>),则用XClass作为原始类型 如果有多个限定(T extends XClass1&XClass2),则使用第一个边界XClass1作为原始类
  3. 在必要时插入类型转换以保持类型安全
  4. 生成桥方法以在扩展时保持多态性
不可具体化类型

可具体化类型和不可具体化类型的定义: 可具体化类型:就是一个可在整个运行时可知其类型信息的类型。 包括:基本类型、非泛型类型、原始类型和调用的非受限通配符。 不可具体化类型:无法整个运行时可知其类型信息的类型,其类型信息已在编译时被擦除: 例如:List<String>List<Number>,JVM无法在运行时分辨这两者

堆污染: 发生时机:当一个参数化类型变量引用了一个对象,而这个对象并非此变量的参数化类型时,堆污染就会发生。 分模块对代码进行分别编译,那就很难检测出潜在的堆污染,应该同时编译

带泛型的可变参数问题: T...将会被翻译为T[],根据类型擦除,进一步会被处理为Object[],这样就可能造成潜在的堆污染 避免堆污染警告 @SafeVarargs:当你确定操作不会带来堆污染时,使用此注释关闭警告 @SuppressWarnings({"unchecked", "varargs"}):强制关闭警告弹出(不建议这么做)

泛型,继承和子类型

给定两种具体的类型A和B(例如Fruit和Apple), 无论A和B是否相关, MyClass<A>MyClass<B>都没半毛钱关系, 它们的公共父对象是Object

受限的类型参数

功能:对泛型变量的范围作出限制 格式: 单一限制:<U extends Number> 多种限制:<U extends A & B & C> extends表达的意义:这里指的是广义上“扩展”,兼有“类继承”和“接口实现”之意 多种限制下的格式语法要求:如果上限类型是一个类,必须第一位标出,否则编译错误

泛型算法实现的关键:利用受限类型参数

通配符

泛型中的问号符“?”名为“通配符” 受上下限控制的通配符 通配符匹配

通配符的适用范围:

参数类型 字段类型 局部变量类型 返回值类型(但返回一个具体类型的值更好)

非受限通配符

两个关键使用场合:

写一个方法,而这方法的实现可以利用Object类中提供的功能时 泛型类中的方法不依赖类型参数时 如List.size()方法,它并不关心List中元素的具体类型

List<XXX>List<?>的一个子类型 理解List<Object>List<?>的不同:差在NULL处理,前者不支持,而后者却可接受一个null入表

受上限控制的通配符

语法格式:<? extends XXX> 优点:扩大兼容的范围

List<XXX>要比List<? extends XXX>更加严格,因为前者仅能匹配XXX列表,然而后者却可同时匹配XXX及其子类的列表

关键词:及其

缺点:

不能set任何元素,但是可以set(null)可以 get类型也不是随便一个子类都能接收

 

java

代码解读

复制代码

Plate<? extends Fruit> fruitPlate = xiaoLiMa.getSnack(applePlate); //这时候小明再从盘子里面那苹果吃,发现不行了 xiaoMing.eat((Apple) fruitPlate.get()); //实际上 Fruit fruit = fruitPlate.get(); Object object = fruitPlate.get();

但是这种不是严格的限制,反射可破

 

java

代码解读

复制代码

public Plate<? extends Fruit> getSnack(Plate<Apple> applePlate){ Plate<? extends Fruit> fruitPlate = applePlate; //不能存放任何元素 try{ Method set = fruitPlate .getClass() .getMethod("set",Object.class); set.invoke(fruitPlate,new Banana()); //set.invoke(fruitPlate,new Beef());//什么都能放了 安全没法保证 }catch(Exception e){} // fruitPlate.set(new Apple()); // fruitPlate.set(new Banana()); //放null还是可以 fruitPlate.set(null); return fruitPlate; }

<? extends T>上界通配符 相当于”只读“,但是通过反射可以写数据进去 反射破坏了泛型的特性了,当拿出来的时候不能转成T就会报错了

有下限通配符

功能:限定了类型的下限,也就它必须为某类型的父类 格式:<? super A>

List<XXX>List<? super XXX>要更加严格。因为前者仅仅兼容XXX类型的列表,而后者却兼容XXX及其任何XXX超类的列表

关键词:及其

缺点:

只能set数据,不能get数据

 

java

代码解读

复制代码

public static void scene03() { Plate<? super Fruit> lowerfruitPlate = new AIPlate<Food>(); lowerfruitPlate.set(new Apple()); lowerfruitPlate.set(new Banana()); // lowerfruitPlate.set(new Food()); // Fruit newFruit1 = lowerfruitPlate.get(); // Apple newFruit3 = lowerfruitPlate.get(); Object newFruit2 = lowerfruitPlate.get(); }

可以把Plate<Fruit>以及它的基类Plate<Food>转成Plate<? super Fruit>它可以存数据但是取出来后 泛型信息丢失了,只能用Object存放

<?>

不能存也不能取

 

java

代码解读

复制代码

public static void scene05() { //<?> == <? extends Object> Plate<?> fruitPlate = new AIPlate<Apple>(); // Fruit fruit = fruitPlate.get(); // fruitPlate.set(new Apple()); fruitPlate.toString(); Object object = fruitPlate.get(); fruitPlate.set(null); }

Plate<?>其实就是Plate<? extends Object>

Java泛型PECS原则

如果你只需要从集合中获得类型T , 使用<? extends T>通配符 如果你只需要将类型T放到集合中, 使用<? super T>通配符 如果你既要获取又要放置元素,则不使用任何通配符。例如List<Apple> PECS即 Producer extends Consumer super, 为了便于记忆。

  • 为何要PECS原则?

提升了API的灵活性

示例:

 

java

代码解读

复制代码

public static void scen07() { List<Apple> src = new ArrayList<>(); src.add(new Apple(1)); List<Apple> dest = new ArrayList<>(10); dest.add(new Apple(2)); System.out.println(dest); copy(dest,src); System.out.println(dest); List<Banana> src1 = new ArrayList<>(); src1.add(new Banana(1)); List<Banana> dest1 = new ArrayList<>(10); dest1.add(new Banana(2)); copy1(dest1,src1); List<Fruit> dest2 = new ArrayList<>(10); dest2.add(new Banana()); // List<Apple> src = new ArrayList<>(); // List<Food> dest2 = new ArrayList<>(10); // Test1.<Food>copy2(dest2,src); Test1.<Fruit>copy3(dest2,src1); } public static void copy(List<Apple> dest, List<Apple> src) { Collections.copy(dest,src); } public static <T> void copy1(List<T> dest, List<T> src) { Collections.copy(dest,src); } public static <T> void copy2(List<? super T> dest, List<T> src) { Collections.copy(dest,src); } public static <T> void copy3(List<? super T> dest, List<? extends T> src) { Collections.copy(dest,src); }

通配符捕获
 

java

代码解读

复制代码

void foo(List<?> i) { fooHelper(i); } /** * 在此示例中,代码正在尝试执行安全操作,那么如何解决编译器错误? * 你可以通过编写捕获通配符的私有帮助器方法来修复它。在这种情况下, * 你可以通过创建私有帮助器方法fooHelper来解决此问题 * * 由于使用了辅助方法,编译器在调用中使用推断来确定T是CAP#1(捕获变量)。该示例现在可以成功编译。 * 按照约定,辅助方法通常命名为originalMethodNameHelper */ private <T> void fooHelper(List<T> l) { l.set(0, l.get(0)); }

泛型不是被擦除了吗? 那为何还与反射有关?
 

java

代码解读

复制代码

import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Map; /** * ParameterizedType * 具体的范型类型, 如Map<String, String> * 有如下方法: * * Type getRawType(): 返回承载该泛型信息的对象, 如上面那个Map<String, String>承载范型信息的对象是Map * Type[] getActualTypeArguments(): 返回实际泛型类型列表, 如上面那个Map<String, String>实际范型列表中有两个元素, 都是String * Type getOwnerType(): 返回是谁的member.(上面那两个最常用) */ public class TestType { Map<String, String> map; //擦除 其实在类常量池里面保留了泛型信息 public static void main(String[] args) throws Exception { Field f = TestType.class.getDeclaredField("map"); System.out.println(f.getGenericType()); // java.util.Map<java.lang.String, java.lang.String> System.out.println(f.getGenericType() instanceof ParameterizedType); // true ParameterizedType pType = (ParameterizedType) f.getGenericType(); System.out.println(pType.getRawType()); // interface java.util.Map for (Type type : pType.getActualTypeArguments()) { System.out.println(type); // 打印两遍: class java.lang.String } System.out.println(pType.getOwnerType()); // null } }

泛型约束
  • 无法利用原始类型来创建泛型

解决方法:使用它们的包装类

  • 无法创建类型参数的实例

变通方案:利用反射就是可以

  • 无法创建参数化类型的静态变量

原因:静态变量是类所有,共享决定了其必须能确定。但多个类型作为参数传入此类的多个实例时,就会无法确定究竟赋予此静态变量哪个实例对象所传入的参数了

  • 无法对参数化类型使用转换或者instanceof关键字 ,但需要注意通配符的情况

  • 无法创建参数化类型的数组

  • 无法创建、捕获或是抛出参数化类型对象 ,但却可以在throw后使用类型参数

  • 当一个方法的所有重载方法的形参类型擦除后,如果它们具有了相同的原始类型,那么此方法不可重载

原因:此情境下,类型擦除会产生两个同签名的方法

使用泛型以及泛型擦除带来的影响(副作用)

泛型类型变量不能使用基本数据类型

比如没有ArrayList<int>,只有ArrayList<Integer>.当类型擦除后,ArrayList的原始类中的类型变量(T)替换成Object,但Object类型不能存放int值

不能使用instanceof 运算符
 

java

代码解读

复制代码

ArrayList<String> strings = new ArrayList<>(); if(strings instanceof ArrayList<?>){} //ArrayList<?>可以 // if(strings instanceof ArrayList<String>){ } ArrayList<String> 不可以

因为擦除后,ArrayList<String>只剩下原始类型,泛型信息String不存在了,所有没法使用instanceof

泛型在静态方法和静态类中的问题
 

java

代码解读

复制代码

class Test2<T>{ // public static T one; //不可以 // public static T test(T t){} //不可以 public static <T> T test1(T t){return t;} }

因为泛型类中的泛型参数的实例化是在定义泛型类型对象(比如ArrayList<Integer>)的时候指定的,而静态成员是不需要使用对象来调用的,所有对象都没创建,如何确定这个泛型参数是什么

泛型类型中的方法冲突
 

java

代码解读

复制代码

// @Override // public boolean equals(T t) { // return super.equals(t); // } @Override public boolean equals(Object obj) { return super.equals(obj); }

因为擦除后两个equals方法变成一样的了

没法创建泛型实例

因为类型不确定

 

java

代码解读

复制代码

//2. Cannot Create Instances of Type Parameters 无法创建类型参数的实例 class Test02 { //你无法创建一个类型参数的实例。例如,下面代码就会引起编译时错误: public static <E> void append(List<E> list) { // E elem = new E(); // compile-time error // list.add(elem); } //通过反射创建一个参数化类型的实例 public static <E> void append(List<E> list, Class<E> cls) throws Exception { E elem = cls.newInstance(); // OK list.add(elem); } }

没有泛型数组

因为数组是协变,擦除后就没法满足数组协变的原则

 

java

代码解读

复制代码

public static <T> void scene05() { // Plate<Apple>[] applePlates = new Plate<Apple>[10];//不允许 // T[] arr = new T[10];//不允许 Apple[] apples = new Apple[10]; Fruit[] fruits = new Fruit[10]; System.out.println(apples.getClass()); //class [Lcom.zero.genericsdemo02.demo02.Apple; System.out.println(fruits.getClass()); //class [Lcom.zero.genericsdemo02.demo02.Fruit; fruits = apples; // fruits里面原本是放什么类型的? Fruit or Apple // Apple[] fruits[0] = new Banana();//编译通过,运行报ArrayStoreException //Fruit是Apple的父类,Fruit[]是Apple[]的父类,这就是数组的协变 //如果加入泛型后,由于擦除机制,运行时将无法知道数组的类型 Plate<?>[] plates = new Plate<?>[10];//这是可以的 }

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值