泛型的意义:适用于多种数据类型执行相同的代码(代码复用)
通过一个例子来阐述,先看下下面的代码:
public static int add(byte x, byte y) { return x + y; public static int add(short x, short y) { return x + y; public static int add(int x, int y) { return x + y; 如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法。 通过泛型,我们可以复用为一个方法: private static <T extends Number> int add(T x, T y) { return x.intValue() + y.intValue(); (1.1) 泛型类型约束 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 import java.util.ArrayList; import java.util.List; * @author weikeqin public class JavaGenericsTest1 { public static void main(String[] args) { List list = new ArrayList<>(); list.add(1); list.add(true); list.add("a"); list.add(new Object()); int[] arr = new int[0]; list.add(arr); System.out.println(list); // [1, true, a, java.lang.Object@421faab1, [I@2b71fc7e] 我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型) 在取出集合元素时需要人为的强制类型转化到具体的目标类型,可能出现java.lang.ClassCastException异常。 像弱类型语言Python PHP等经常出类似的问题,也是这个原因。 // list中只能放String, 不能放其它类型的元素 List<String> list = new ArrayList<String>(); (2) 泛型的基本使用 泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。 (2.1) 泛型类 1.简单泛型类 * @author weikeqin public class DictModel<T> { // 此处类型可以自定义,常用的是 T、K、E,这里用的是T,T是type的简称 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value public void addDict(String key, T value) { dict.put(key, value); * @param key * @return public T getDict(String key) { return dict.get(key); 2.多元泛型 * @author weikeqin public class DictModelV2<K, V> { // 此处指定了两个泛型类型 private Map<K, V> dict; public DictModelV2() { dict = new HashMap<>(); * @param key * @param value public void addDict(K key, V value) { dict.put(key, value); * @param key * @return public V getDict(K key) { return dict.get(key); 2.2 泛型接口/** * @author weikeqin public interface DatasourceParser<E> { * @return List<E> parserData(); * @author weikeqin public class MysqlDatasourceParser<E> implements DatasourceParser<E>{ @Override public List<E> parserData() { return null; (2.3) 泛型方法 泛型方法,是在调用方法的时候指明泛型的具体类型。 * @author weikeqin public class CacheClient { public <V> void setCache(String key, V v) { // 定义泛型方法时,必须在返回值前边加一个<>,来声明这是一个泛型方法 public <V> V getCache(String key) { V value = null; // .... return value; 泛型方法可以在调用的时候指明类型,更加灵活。 (3) 泛型的上下限(3.1) 使用泛型遇到的问题cannot be converted to/** * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); // ... public static void funD(List<B> list) { // java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A> funC(list); 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决 * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<? extends A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); public static void funD(List<B> list) { funC(list); (3.2) 泛型上下限的引入 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。 * @author weikeqin public class DictModel<T extends Number> { // 此处泛型T只能是数字类型 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); // 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
public static int add(byte x, byte y) { return x + y; public static int add(short x, short y) { return x + y; public static int add(int x, int y) { return x + y; 如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法。 通过泛型,我们可以复用为一个方法:
如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法。
通过泛型,我们可以复用为一个方法:
private static <T extends Number> int add(T x, T y) { return x.intValue() + y.intValue(); (1.1) 泛型类型约束 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 import java.util.ArrayList; import java.util.List; * @author weikeqin public class JavaGenericsTest1 { public static void main(String[] args) { List list = new ArrayList<>(); list.add(1); list.add(true); list.add("a"); list.add(new Object()); int[] arr = new int[0]; list.add(arr); System.out.println(list); // [1, true, a, java.lang.Object@421faab1, [I@2b71fc7e] 我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型) 在取出集合元素时需要人为的强制类型转化到具体的目标类型,可能出现java.lang.ClassCastException异常。 像弱类型语言Python PHP等经常出类似的问题,也是这个原因。 // list中只能放String, 不能放其它类型的元素 List<String> list = new ArrayList<String>(); (2) 泛型的基本使用 泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。 (2.1) 泛型类 1.简单泛型类 * @author weikeqin public class DictModel<T> { // 此处类型可以自定义,常用的是 T、K、E,这里用的是T,T是type的简称 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value public void addDict(String key, T value) { dict.put(key, value); * @param key * @return public T getDict(String key) { return dict.get(key); 2.多元泛型 * @author weikeqin public class DictModelV2<K, V> { // 此处指定了两个泛型类型 private Map<K, V> dict; public DictModelV2() { dict = new HashMap<>(); * @param key * @param value public void addDict(K key, V value) { dict.put(key, value); * @param key * @return public V getDict(K key) { return dict.get(key); 2.2 泛型接口/** * @author weikeqin public interface DatasourceParser<E> { * @return List<E> parserData(); * @author weikeqin public class MysqlDatasourceParser<E> implements DatasourceParser<E>{ @Override public List<E> parserData() { return null; (2.3) 泛型方法 泛型方法,是在调用方法的时候指明泛型的具体类型。 * @author weikeqin public class CacheClient { public <V> void setCache(String key, V v) { // 定义泛型方法时,必须在返回值前边加一个<>,来声明这是一个泛型方法 public <V> V getCache(String key) { V value = null; // .... return value; 泛型方法可以在调用的时候指明类型,更加灵活。 (3) 泛型的上下限(3.1) 使用泛型遇到的问题cannot be converted to/** * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); // ... public static void funD(List<B> list) { // java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A> funC(list); 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决 * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<? extends A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); public static void funD(List<B> list) { funC(list); (3.2) 泛型上下限的引入 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。 * @author weikeqin public class DictModel<T extends Number> { // 此处泛型T只能是数字类型 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); // 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
private static <T extends Number> int add(T x, T y) { return x.intValue() + y.intValue();
(1.1) 泛型类型约束 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 import java.util.ArrayList; import java.util.List; * @author weikeqin public class JavaGenericsTest1 { public static void main(String[] args) { List list = new ArrayList<>(); list.add(1); list.add(true); list.add("a"); list.add(new Object()); int[] arr = new int[0]; list.add(arr); System.out.println(list); // [1, true, a, java.lang.Object@421faab1, [I@2b71fc7e] 我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型) 在取出集合元素时需要人为的强制类型转化到具体的目标类型,可能出现java.lang.ClassCastException异常。 像弱类型语言Python PHP等经常出类似的问题,也是这个原因。 // list中只能放String, 不能放其它类型的元素 List<String> list = new ArrayList<String>(); (2) 泛型的基本使用 泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。 (2.1) 泛型类 1.简单泛型类 * @author weikeqin public class DictModel<T> { // 此处类型可以自定义,常用的是 T、K、E,这里用的是T,T是type的简称 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value public void addDict(String key, T value) { dict.put(key, value); * @param key * @return public T getDict(String key) { return dict.get(key); 2.多元泛型 * @author weikeqin public class DictModelV2<K, V> { // 此处指定了两个泛型类型 private Map<K, V> dict; public DictModelV2() { dict = new HashMap<>(); * @param key * @param value public void addDict(K key, V value) { dict.put(key, value); * @param key * @return public V getDict(K key) { return dict.get(key); 2.2 泛型接口/** * @author weikeqin public interface DatasourceParser<E> { * @return List<E> parserData(); * @author weikeqin public class MysqlDatasourceParser<E> implements DatasourceParser<E>{ @Override public List<E> parserData() { return null; (2.3) 泛型方法 泛型方法,是在调用方法的时候指明泛型的具体类型。 * @author weikeqin public class CacheClient { public <V> void setCache(String key, V v) { // 定义泛型方法时,必须在返回值前边加一个<>,来声明这是一个泛型方法 public <V> V getCache(String key) { V value = null; // .... return value; 泛型方法可以在调用的时候指明类型,更加灵活。 (3) 泛型的上下限(3.1) 使用泛型遇到的问题cannot be converted to/** * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); // ... public static void funD(List<B> list) { // java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A> funC(list); 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决 * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<? extends A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); public static void funD(List<B> list) { funC(list); (3.2) 泛型上下限的引入 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。 * @author weikeqin public class DictModel<T extends Number> { // 此处泛型T只能是数字类型 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); // 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 import java.util.ArrayList; import java.util.List; * @author weikeqin public class JavaGenericsTest1 { public static void main(String[] args) { List list = new ArrayList<>(); list.add(1); list.add(true); list.add("a"); list.add(new Object()); int[] arr = new int[0]; list.add(arr); System.out.println(list); // [1, true, a, java.lang.Object@421faab1, [I@2b71fc7e] 我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型) 在取出集合元素时需要人为的强制类型转化到具体的目标类型,可能出现java.lang.ClassCastException异常。 像弱类型语言Python PHP等经常出类似的问题,也是这个原因。 // list中只能放String, 不能放其它类型的元素 List<String> list = new ArrayList<String>(); (2) 泛型的基本使用 泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。 (2.1) 泛型类 1.简单泛型类 * @author weikeqin public class DictModel<T> { // 此处类型可以自定义,常用的是 T、K、E,这里用的是T,T是type的简称 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value public void addDict(String key, T value) { dict.put(key, value); * @param key * @return public T getDict(String key) { return dict.get(key); 2.多元泛型 * @author weikeqin public class DictModelV2<K, V> { // 此处指定了两个泛型类型 private Map<K, V> dict; public DictModelV2() { dict = new HashMap<>(); * @param key * @param value public void addDict(K key, V value) { dict.put(key, value); * @param key * @return public V getDict(K key) { return dict.get(key); 2.2 泛型接口/** * @author weikeqin public interface DatasourceParser<E> { * @return List<E> parserData(); * @author weikeqin public class MysqlDatasourceParser<E> implements DatasourceParser<E>{ @Override public List<E> parserData() { return null; (2.3) 泛型方法 泛型方法,是在调用方法的时候指明泛型的具体类型。 * @author weikeqin public class CacheClient { public <V> void setCache(String key, V v) { // 定义泛型方法时,必须在返回值前边加一个<>,来声明这是一个泛型方法 public <V> V getCache(String key) { V value = null; // .... return value; 泛型方法可以在调用的时候指明类型,更加灵活。 (3) 泛型的上下限(3.1) 使用泛型遇到的问题cannot be converted to/** * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); // ... public static void funD(List<B> list) { // java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A> funC(list); 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决 * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<? extends A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); public static void funD(List<B> list) { funC(list); (3.2) 泛型上下限的引入 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。 * @author weikeqin public class DictModel<T extends Number> { // 此处泛型T只能是数字类型 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); // 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
import java.util.ArrayList; import java.util.List; * @author weikeqin public class JavaGenericsTest1 { public static void main(String[] args) { List list = new ArrayList<>(); list.add(1); list.add(true); list.add("a"); list.add(new Object()); int[] arr = new int[0]; list.add(arr); System.out.println(list); // [1, true, a, java.lang.Object@421faab1, [I@2b71fc7e] 我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型) 在取出集合元素时需要人为的强制类型转化到具体的目标类型,可能出现java.lang.ClassCastException异常。 像弱类型语言Python PHP等经常出类似的问题,也是这个原因。 // list中只能放String, 不能放其它类型的元素 List<String> list = new ArrayList<String>(); (2) 泛型的基本使用 泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。 (2.1) 泛型类 1.简单泛型类 * @author weikeqin public class DictModel<T> { // 此处类型可以自定义,常用的是 T、K、E,这里用的是T,T是type的简称 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value public void addDict(String key, T value) { dict.put(key, value); * @param key * @return public T getDict(String key) { return dict.get(key); 2.多元泛型 * @author weikeqin public class DictModelV2<K, V> { // 此处指定了两个泛型类型 private Map<K, V> dict; public DictModelV2() { dict = new HashMap<>(); * @param key * @param value public void addDict(K key, V value) { dict.put(key, value); * @param key * @return public V getDict(K key) { return dict.get(key); 2.2 泛型接口/** * @author weikeqin public interface DatasourceParser<E> { * @return List<E> parserData(); * @author weikeqin public class MysqlDatasourceParser<E> implements DatasourceParser<E>{ @Override public List<E> parserData() { return null; (2.3) 泛型方法 泛型方法,是在调用方法的时候指明泛型的具体类型。 * @author weikeqin public class CacheClient { public <V> void setCache(String key, V v) { // 定义泛型方法时,必须在返回值前边加一个<>,来声明这是一个泛型方法 public <V> V getCache(String key) { V value = null; // .... return value; 泛型方法可以在调用的时候指明类型,更加灵活。 (3) 泛型的上下限(3.1) 使用泛型遇到的问题cannot be converted to/** * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); // ... public static void funD(List<B> list) { // java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A> funC(list); 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决 * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<? extends A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); public static void funD(List<B> list) { funC(list); (3.2) 泛型上下限的引入 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。 * @author weikeqin public class DictModel<T extends Number> { // 此处泛型T只能是数字类型 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); // 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
import java.util.ArrayList; import java.util.List; * @author weikeqin public class JavaGenericsTest1 { public static void main(String[] args) { List list = new ArrayList<>(); list.add(1); list.add(true); list.add("a"); list.add(new Object()); int[] arr = new int[0]; list.add(arr); System.out.println(list); // [1, true, a, java.lang.Object@421faab1, [I@2b71fc7e] 我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型) 在取出集合元素时需要人为的强制类型转化到具体的目标类型,可能出现java.lang.ClassCastException异常。 像弱类型语言Python PHP等经常出类似的问题,也是这个原因。 // list中只能放String, 不能放其它类型的元素 List<String> list = new ArrayList<String>();
我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型)
在取出集合元素时需要人为的强制类型转化到具体的目标类型,可能出现java.lang.ClassCastException异常。 像弱类型语言Python PHP等经常出类似的问题,也是这个原因。
// list中只能放String, 不能放其它类型的元素 List<String> list = new ArrayList<String>();
(2) 泛型的基本使用 泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。 (2.1) 泛型类 1.简单泛型类 * @author weikeqin public class DictModel<T> { // 此处类型可以自定义,常用的是 T、K、E,这里用的是T,T是type的简称 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value public void addDict(String key, T value) { dict.put(key, value); * @param key * @return public T getDict(String key) { return dict.get(key); 2.多元泛型 * @author weikeqin public class DictModelV2<K, V> { // 此处指定了两个泛型类型 private Map<K, V> dict; public DictModelV2() { dict = new HashMap<>(); * @param key * @param value public void addDict(K key, V value) { dict.put(key, value); * @param key * @return public V getDict(K key) { return dict.get(key); 2.2 泛型接口/** * @author weikeqin public interface DatasourceParser<E> { * @return List<E> parserData(); * @author weikeqin public class MysqlDatasourceParser<E> implements DatasourceParser<E>{ @Override public List<E> parserData() { return null; (2.3) 泛型方法 泛型方法,是在调用方法的时候指明泛型的具体类型。 * @author weikeqin public class CacheClient { public <V> void setCache(String key, V v) { // 定义泛型方法时,必须在返回值前边加一个<>,来声明这是一个泛型方法 public <V> V getCache(String key) { V value = null; // .... return value; 泛型方法可以在调用的时候指明类型,更加灵活。 (3) 泛型的上下限(3.1) 使用泛型遇到的问题cannot be converted to/** * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); // ... public static void funD(List<B> list) { // java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A> funC(list); 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决 * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<? extends A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); public static void funD(List<B> list) { funC(list); (3.2) 泛型上下限的引入 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。 * @author weikeqin public class DictModel<T extends Number> { // 此处泛型T只能是数字类型 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); // 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。
1.简单泛型类
2.多元泛型
/** * @author weikeqin public interface DatasourceParser<E> { * @return List<E> parserData(); * @author weikeqin public class MysqlDatasourceParser<E> implements DatasourceParser<E>{ @Override public List<E> parserData() { return null; (2.3) 泛型方法 泛型方法,是在调用方法的时候指明泛型的具体类型。 * @author weikeqin public class CacheClient { public <V> void setCache(String key, V v) { // 定义泛型方法时,必须在返回值前边加一个<>,来声明这是一个泛型方法 public <V> V getCache(String key) { V value = null; // .... return value; 泛型方法可以在调用的时候指明类型,更加灵活。 (3) 泛型的上下限(3.1) 使用泛型遇到的问题cannot be converted to/** * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); // ... public static void funD(List<B> list) { // java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A> funC(list); 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决 * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<? extends A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); public static void funD(List<B> list) { funC(list); (3.2) 泛型上下限的引入 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。 * @author weikeqin public class DictModel<T extends Number> { // 此处泛型T只能是数字类型 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); // 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
/** * @author weikeqin public interface DatasourceParser<E> { * @return List<E> parserData(); * @author weikeqin public class MysqlDatasourceParser<E> implements DatasourceParser<E>{ @Override public List<E> parserData() { return null;
(2.3) 泛型方法 泛型方法,是在调用方法的时候指明泛型的具体类型。 * @author weikeqin public class CacheClient { public <V> void setCache(String key, V v) { // 定义泛型方法时,必须在返回值前边加一个<>,来声明这是一个泛型方法 public <V> V getCache(String key) { V value = null; // .... return value; 泛型方法可以在调用的时候指明类型,更加灵活。 (3) 泛型的上下限(3.1) 使用泛型遇到的问题cannot be converted to/** * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); // ... public static void funD(List<B> list) { // java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A> funC(list); 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决 * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<? extends A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); public static void funD(List<B> list) { funC(list); (3.2) 泛型上下限的引入 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。 * @author weikeqin public class DictModel<T extends Number> { // 此处泛型T只能是数字类型 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); // 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
泛型方法,是在调用方法的时候指明泛型的具体类型。
泛型方法可以在调用的时候指明类型,更加灵活。
/** * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); // ... public static void funD(List<B> list) { // java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A> funC(list); 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决 * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<? extends A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); public static void funD(List<B> list) { funC(list); (3.2) 泛型上下限的引入 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。 * @author weikeqin public class DictModel<T extends Number> { // 此处泛型T只能是数字类型 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); // 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
/** * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); // ... public static void funD(List<B> list) { // java: incompatible types: java.util.List<xxx.B> cannot be converted to java.util.List<xxx.A> funC(list); 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决 * @author weikeqin public class A { public static void funA(A a) { // ... public static void funC(List<? extends A> list) { // ... * @author weikeqin public class B extends A { public static void funB(B b) { funA(b); public static void funD(List<B> list) { funC(list);
为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。 <? extends Number> 表示该类型参数可以是Number(上边界)或者Number的子类类型。 代码里修改 List<? extends A> list 后解决
<? extends Number>
List<? extends A> list
(3.2) 泛型上下限的引入 在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。 * @author weikeqin public class DictModel<T extends Number> { // 此处泛型T只能是数字类型 private Map<String, T> dict; public DictModel() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); // 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制, 如:类型实参只准传入某种类型的父类或某种类型的子类。
// 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict); (3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
// 只能接收Integer或Object类型的泛型,Integer类的父类只有Object类 public static void fun(DictModel<? super Integer> dict) { System.out.print(dict);
(3.3) 小结<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。 (3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
<?> 无限制通配符 <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类 <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类 // 使用原则《Effictive Java》 // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限 1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>; 2. 如果它表示一个 T 的消费者,就使用 < ? super T>; 3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
(3.4) 实际例子private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大 //工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
private <E extends Comparable<? super E>> E max(List<? extends E> e1) { if (e1 == null){ return null; //迭代器返回的元素属于 E 的某个子类型 Iterator<? extends E> iterator = e1.iterator(); E result = iterator.next(); while (iterator.hasNext()){ E next = iterator.next(); if (next.compareTo(result) > 0){ result = next; return result; 上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大
上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大
<E extends Comparable<? super E>>
extends Comparable<…>
//工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!"); (4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
//工资低于2500元的上斑族并且站立的乘客车票打8折 public static <T extends Staff & Passenger> void discount(T t){ if(t.getSalary()<2500 && t.isStanding()){ System.out.println("恭喜你!您的车票打八折!");
(4) 泛型数组 泛型数组的坑比较多,看看下面几个例子,能否理解。 List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]] List<String>[] lsa = new List[10]; // 编译通过 List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2)); List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[] List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>' List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null] List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]] List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null] JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List 合理使用public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
泛型数组的坑比较多,看看下面几个例子,能否理解。
List[] arr1 = new List[3]; // 编译通过,执行通过 arr1[0] = null; arr1[1] = Collections.EMPTY_LIST; arr1[2] = Collections.singletonList("abc"); System.out.println(Arrays.toString(arr1)); // [null, [], [abc]]
List<String>[] lsa = new List[10]; // 编译通过
List<String>[] arr2 = new ArrayList[3]; // 编译通过,执行报错 Exception in thread "main" java.lang.ArrayStoreException: java.util.Collections$EmptyList arr2[0] = null; arr2[1] = Collections.EMPTY_LIST; System.out.println(Arrays.toString(arr2));
List<String>[] arr3 = new ArrayList<String>[3]; // 编译错误 java: generic array creation
List<String>[] arr4 = new ArrayList<?>[3]; // 编译错误 java: incompatible types: java.util.ArrayList<?>[] cannot be converted to java.util.List<java.lang.String>[]
List<String>[] arr5 = new ArrayList<>[10]; // 编译错误 java: cannot create array with '<>'
List<String>[] arr6 = (List<String>[]) new ArrayList<?>[3]; // 编译通过 执行通过 arr6[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr6[1] = list; System.out.println(Arrays.toString(arr6)); // [null, [abc], null]
List<?>[] arr7 = new ArrayList<String>[3]; // 编译错误 java: generic array creation
List<?>[] arr8 = new ArrayList<?>[3]; // 编译通过 arr8[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr8[1] = list ; List<Integer> list2 = new ArrayList<>(); list2.add(1); list2.add(1000); arr8[2] = list2 ; System.out.println(Arrays.toString(arr8)); // [null, [abc], [1, 1000]]
List<String>[] arr9 = new ArrayList[3]; // 编译通过,执行通过 arr9[0] = null; List<String> list = new ArrayList<>(); list.add("abc"); arr9[1] = list ; System.out.println(Arrays.toString(arr9)); // [null, [abc], null]
JVM在创建int数组时,用的是 newarray 命令,对应的class类型是 int JVM在创建String数组时,用的是 anewarray 命令,对应的class类型是 class java/lang/String JVM在创建List数组时用的是 anewarray 命令,对应的class类型是 class java/util/List
int
class java/lang/String
class java/util/List
public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); (5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size);
(5) 深入理解泛型 Java泛型的类型擦除 generics (5.1) 泛型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。 泛型的类型擦除原则: 消除类型参数声明,即删除<>及其包围的部分。 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。 为了保证类型安全,必要时插入强制类型转换代码。 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。 (5.2) 如何进行擦除(5.2.1) 擦除类定义中的类型参数 1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
Java泛型的类型擦除 generics
泛型的类型擦除原则:
1.无限制类型擦除 当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。 2.有限类型擦除 当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object
<T extends Number>
Number,<? super Number>
/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。 (5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
/** * @author weikeqin public interface Info<T> { T info(T var); * @author weikeqin public class BridgeMethodTest implements Info<Integer> { @Override public Integer info(Integer var) { return null; [weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$ 通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T); 而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。 Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。
[weikeqin@weikeqin u3 ]$javac Info.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javac BridgeMethodTest.java [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap Info.class Compiled from "Info.java" public interface Info<T> { public abstract T info(T); [weikeqin@weikeqin u3 ]$ [weikeqin@weikeqin u3 ]$javap BridgeMethodTest.class Compiled from "BridgeMethodTest.java" public class BridgeMethodTest implements Info<java.lang.Integer> { public BridgeMethodTest(); public java.lang.Integer info(java.lang.Integer); public java.lang.Object info(java.lang.Object); [weikeqin@weikeqin u3 ]$
通过javap命令反编译,可以看到接口Info的方法反编译后其实是抽象方法 public abstract T info(T);
Info
public abstract T info(T);
而接口的实现类BridgeMethodTest原来定义的一个方法 public Integer info(Integer var) 变成了2个方法 public java.lang.Integer info(java.lang.Integer) public java.lang.Object info(java.lang.Object) 。
BridgeMethodTest
public Integer info(Integer var)
public java.lang.Integer info(java.lang.Integer)
public java.lang.Object info(java.lang.Object)
Java编译器在BridgeMethodTest中自动增加了两个方法:默认构造方法和参数为Object的info方法,参数为Object的info方法就是“桥接方法”。
(5.2.3) 擦除方法定义中的类型参数 擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的 (5.3) 如何证明泛型擦除 import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的
import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
import java.util.ArrayList; import java.util.List; * @author weikeqin public class GenericsErasureTest { public static void main(String[] args) { List<String> list1 = new ArrayList<String>(); list1.add("abc"); List<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true 通过这个例子可以看到打印的结果是true 看看JVM底层是怎么做的 [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$ 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list1.add(“abc”); 对应的指令 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 这个是 list2.add(123); 对应的指令 可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
通过这个例子可以看到打印的结果是true
看看JVM底层是怎么做的
[weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$java GenericsErasureTest.java [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap GenericsErasureTest.class Compiled from "GenericsErasureTest.java" public class GenericsErasureTest { public GenericsErasureTest(); public static void main(java.lang.String[]); [weikeqin@weikeqin u4 ]$ [weikeqin@weikeqin u4 ]$javap -v GenericsErasureTest.class Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/generics/u4/GenericsErasureTest.class Last modified May 29, 2022; size 750 bytes SHA-256 checksum 8ba88fabd745b783d104df1e2e7b96bb3d8ef33c3091ac2ff1c88aad5219fba1 Compiled from "GenericsErasureTest.java" public class GenericsErasureTest minor version: 0 major version: 61 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #38 // GenericsErasureTest super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // java/util/ArrayList #8 = Utf8 java/util/ArrayList #9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V #10 = String #11 // abc #11 = Utf8 abc #12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z #13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z #14 = Utf8 add #15 = Utf8 (Ljava/lang/Object;)Z #16 = Methodref #17.#18 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #17 = Class #19 // java/lang/Integer #18 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer; #19 = Utf8 java/lang/Integer #20 = Utf8 valueOf #21 = Utf8 (I)Ljava/lang/Integer; #22 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream; #23 = Class #25 // java/lang/System #24 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Methodref #2.#29 // java/lang/Object.getClass:()Ljava/lang/Class; #29 = NameAndType #30:#31 // getClass:()Ljava/lang/Class; #30 = Utf8 getClass #31 = Utf8 ()Ljava/lang/Class; #32 = Methodref #33.#34 // java/io/PrintStream.println:(Z)V #33 = Class #35 // java/io/PrintStream #34 = NameAndType #36:#37 // println:(Z)V #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Z)V #38 = Class #39 // GenericsErasureTest #39 = Utf8 GenericsErasureTest #40 = Utf8 Code #41 = Utf8 LineNumberTable #42 = Utf8 main #43 = Utf8 ([Ljava/lang/String;)V #44 = Utf8 StackMapTable #45 = Class #46 // "[Ljava/lang/String;" #46 = Utf8 [Ljava/lang/String; #47 = Utf8 SourceFile #48 = Utf8 GenericsErasureTest.java public GenericsErasureTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: new #7 // class java/util/ArrayList 18: dup 19: invokespecial #9 // Method java/util/ArrayList."<init>":()V 22: astore_2 23: aload_2 24: bipush 123 26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 32: pop 33: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_1 37: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 40: aload_2 41: invokevirtual #28 // Method java/lang/Object.getClass:()Ljava/lang/Class; 44: if_acmpne 51 47: iconst_1 48: goto 52 51: iconst_0 52: invokevirtual #32 // Method java/io/PrintStream.println:(Z)V 55: return LineNumberTable: line 12: 0 line 13: 8 line 15: 15 line 16: 23 line 18: 33 line 19: 55 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 51 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/util/ArrayList, class java/util/ArrayList ] stack = [ class java/io/PrintStream, int ] SourceFile: "GenericsErasureTest.java" [weikeqin@weikeqin u4 ]$
9: ldc #10 // String abc 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
这个是 list1.add(“abc”); 对应的指令
26: invokestatic #16 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 29: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
这个是 list2.add(123); 对应的指令
list2.add(123);
可以看到在进行list.add时都是使用 Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); 如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i));
如何理解泛型的编译期检查? 类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?
Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢 public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
public static void main(String[] args) { ArrayList<String> list = new ArrayList<String>(); list.add("123"); list.add(123);//编译错误 在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。 这个类型检查是针对谁的呢
在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。
这个类型检查是针对谁的呢
public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$ 父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); 虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。 如何理解基本类型不能作为泛型类型? 比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。 如何理解泛型类型不能实例化 因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。 泛型数组:能不能采用具体的泛型类型进行初始化?public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String 如何理解泛型的多态? 类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$
public static void main(String[] args) { ArrayList<String> list1 = new ArrayList(); list1.add("1"); //编译通过 list1.add(1); //编译错误 String str1 = list1.get(0); //返回类型就是String ArrayList list2 = new ArrayList<String>(); list2.add("1"); //编译通过 list2.add(1); //编译通过 Object object = list2.get(0); //返回类型就是Object 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。 new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String
类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String str2 = new ArrayList<String>().get(0); //返回类型就是String
如何理解泛型的多态?
类似 (5.2.2) 擦除接口及实现类中的类型参数 桥接方法和泛型的多态 import java.util.HashMap; import java.util.Map; * @author weikeqin public class DictModelV1<T> { private Map<String, T> dict; public DictModelV1() { dict = new HashMap<>(); * @param key * @param value 类型由T指定 public void addDict(String key, T value) { dict.put(key, value); * @param key * @return 返回值的类型由外部决定 public T getDict(String key) { return dict.get(key); * @author weikeqin public class DictModelV6 extends DictModelV1<Integer> { public DictModelV6() { super(); * @param key * @param value @Override public void addDict(String key, Integer value) { super.addDict(key, value); * @param key * @return @Override public Integer getDict(String key) { return super.getDict(key); [weikeqin@weikeqin u1 ]$javac DictModelV1.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javac DictModelV6.java [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV1.class Compiled from "DictModelV1.java" public class DictModelV1<T> { public DictModelV1(); public void addDict(java.lang.String, T); public T getDict(java.lang.String); [weikeqin@weikeqin u1 ]$ [weikeqin@weikeqin u1 ]$javap DictModelV6.class Compiled from "DictModelV6.java" public class DictModelV6 extends DictModelV1<java.lang.Integer> { public DictModelV6(); public void addDict(java.lang.String, java.lang.Integer); public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String); public void addDict(java.lang.String, java.lang.Object); [weikeqin@weikeqin u1 ]$
父类反编译 可以看到有2个方法 子类反编译 addDict方法 变成 public void addDict(java.lang.String, java.lang.Integer); public void addDict(java.lang.String, java.lang.Object); getDict 方法变成 public java.lang.Integer getDict(java.lang.String); public java.lang.Object getDict(java.lang.String);
public void addDict(java.lang.String, java.lang.Integer);
public void addDict(java.lang.String, java.lang.Object);
public java.lang.Integer getDict(java.lang.String);
public java.lang.Object getDict(java.lang.String);
虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。 这样,类型擦除就和多态有了冲突。 JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。
比如,我们没有ArrayList,只有ArrayList,为什么? 因为当类型擦除后,ArrayList的原始类型变为Object,ArrayList只能使用包装类,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作 java/lang/Integer.valueOf:(I)Ljava/lang/Integer;。
java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
因为在Java编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。
public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。 泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
public static void main(String[] args) { List<String>[] lsa = new List[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String 由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。
由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。
泛型数组:如何正确的初始化泛型数组实例? 尽量别用泛型数组,坑比较多。 如果要用,指明类型,用反射去创建。 public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
尽量别用泛型数组,坑比较多。
如果要用,指明类型,用反射去创建。
public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100); public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
public static void main(String[] args) { Class clz = Integer.class; Integer[] arr = (Integer[]) Array.newInstance(clz, 100);
public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。 参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
public class GenericsArrayUseTest { public static void main(String[] args) { ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100); Integer[] array = arrayToken.create(); public class ArrayWithTypeToken<T> { private T[] array; public ArrayWithTypeToken(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); public void put(int index, T item) { array[index] = item; public T get(int index) { return array[index]; public T[] create() { return array; 所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。
所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。
参考资料[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra
[1] Java 基础 - 泛型机制详解[2] Java“禁止”泛型数组[3] generics[4] generics-extra