当前位置: 首页 > news >正文

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(菱形) 类型推断

原始类型

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

class Box<T>{} 
Box b = new Box(); //这个Box就是Box<T>的原始类型
泛型擦除
ArrayList<Int> arr1 = new ArrayList();
ArrayList<String> arr2 = new ArrayList();//result true
System.out.println(arr1.getClass() == arr2.getClass()); // true

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

  • 代码查看:
  • 示例:

ConditionalPlate

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(){}@Overridepublic void set(T t) {items.add(t);}@Overridepublic T get(){int index = items.size() -1;if(index>= 0){return items.get(index);}else{return null;}}@Overridepublic String toString() {return "Plate{" +"items=" + items +'}';}//    @Override
//    public boolean equals(T t) {
//        return super.equals(t);
//    }@Overridepublic boolean equals(Object obj) {return super.equals(obj);}
}

Plate

package com;public interface Plate<T> {public void set(T t);public T get();
}
  • javac编译源文件
  • javap -c 查看生成的字节码
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_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: aload_05: new           #2                  // class java/util/ArrayList8: dup9: bipush        1011: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V14: putfield      #4                  // Field items:Ljava/util/List;17: returnpublic void set(T);Code:0: aload_01: getfield      #4                  // Field items:Ljava/util/List;4: aload_15: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z10: pop11: returnpublic T get();Code:0: aload_01: getfield      #4                  // Field items:Ljava/util/List;4: invokeinterface #6,  1            // InterfaceMethod java/util/List.size:()I9: iconst_110: isub11: istore_112: iload_113: iflt          2716: aload_017: getfield      #4                  // Field items:Ljava/util/List;20: iload_121: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;26: areturn27: aconst_null28: areturnpublic java.lang.String toString();Code:0: aload_01: getfield      #4                  // Field items:Ljava/util/List;4: invokedynamic #8,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/util/List;)Ljava/lang/String;9: areturnpublic boolean equals(java.lang.Object);Code:0: aload_01: aload_12: invokespecial #9                  // Method java/lang/Object.equals:(Ljava/lang/Object;)Z5: ireturn
}

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

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

  • 类型消除应用的场合:

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

  • Bridge Methods 桥方法

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

  • 示例代码查看桥方法:

BananaPlate

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

javap -c 查看字节码

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_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: aload_05: new           #2                  // class java/util/ArrayList8: dup9: bipush        1011: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V14: putfield      #4                  // Field items:Ljava/util/List;17: returnpublic void set(com.Banana);Code:0: aload_01: getfield      #4                  // Field items:Ljava/util/List;4: aload_15: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z10: pop11: returnpublic com.Banana get();Code:0: aload_01: getfield      #4                  // Field items:Ljava/util/List;4: iconst_05: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;10: checkcast     #7                  // class com/Banana13: areturnpublic java.lang.Object get();Code:0: aload_01: invokevirtual #8                  // Method get:()Lcom/Banana;4: areturnpublic void set(java.lang.Object);Code:0: aload_01: aload_12: checkcast     #7                  // class com/Banana5: invokevirtual #9                  // Method set:(Lcom/Banana;)V8: return
}

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

 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类型也不是随便一个子类都能接收

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

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

    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数据

    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存放

<?>

不能存也不能取

    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的灵活性

示例:

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

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);  // trueParameterizedType pType = (ParameterizedType) f.getGenericType();System.out.println(pType.getRawType());                               // interface java.util.Mapfor (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 运算符
ArrayList<String> strings = new ArrayList<>();
if(strings instanceof ArrayList<?>){}  //ArrayList<?>可以// if(strings instanceof ArrayList<String>){ }  ArrayList<String> 不可以

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

泛型在静态方法和静态类中的问题
class Test2<T>{
//    public static T one; //不可以
//    public static T test(T t){} //不可以public static <T> T test1(T t){return t;}
}

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

泛型类型中的方法冲突
//    @Override
//    public boolean equals(T t) {
//        return super.equals(t);
//    }@Overridepublic boolean equals(Object obj) {return super.equals(obj);}

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

没法创建泛型实例

因为类型不确定

//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();   // OKlist.add(elem);}
}
没有泛型数组

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

    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];//这是可以的}

http://www.mrgr.cn/news/18304.html

相关文章:

  • 速盾:防御ddos攻击的几大有效方法是什么?
  • 大模型企业应用落地系列九》多模态具身智能》端到端强化学习人形机器人
  • Python进阶————闭包与装饰器
  • 【网络安全】网络安全防护体系
  • 23:【stm32】ADC模数转换器
  • 字符串地指针表示方式
  • 三台机器,第一台机器可以ssh到第二台机器,第二台机器可以ssh到第三台机器,请问第一台机器上怎么通过ssh 直接从第三台机器scp文件到第一台机器?
  • 使用JavaScript读取手机联系人列表:从理论到实践
  • 服务器重启后能够自动重启Jar包
  • LeetCode字母异位词分组
  • 秋招/春招投递公司记录表格
  • 每一次逾越都是不可替代的成长![我是如何克服编程学习过程中的挫折感】
  • 虚拟机输入ip addr不显示IP地址
  • 算法之哈希表
  • 详解 Go 语言测试
  • 花生壳的登录及获取二级域名
  • 分贝通助力元气森林企业支出一体化降本提效
  • 使用 Bodybuilder 项目简化前端ES查询
  • C语言基础(三十三)
  • Java-数据结构-链表-LinkedList(一) (^_−)☆