Java泛型
Contents
💠
-
- 1.1. 简单使用
- 1.2. 类型擦除
- 1.3. 约束和局限性
- 1.4. 泛型类型的继承规则
- 1.5. 通配符类型
- 1.5.1. 子类 类型限定的通配符 extends
- 1.5.2. 基类 类型限定的通配符 super
- 1.5.3. 无限定通配符
- 1.5.4. 通配符捕获
- 1.6. 反射和泛型
💠 2024-07-10 00:40:24
泛型
泛型程序设计划分为三个熟练级别 基本级别就是仅仅使用泛型类,典型的是像ArrayList这样的集合–不必考虑他们的工作方式和原因,大多数人会停留在这个级别.直到出现了什么问题. 当把不同的泛型类混合在一起的时候,或是对类型参数一无所知的遗留代码进行对接时,可能会看到含糊不清的错误消息.如果这样的话,就需要系统的进行学习Java泛型来系统地解决问题.
泛型类可以看作普通类的工厂 – Java核心技术卷 2004(1.5)
参考: Java总结篇系列:Java泛型
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。
那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
参考: Java深度历险(五)——Java泛型
简单使用
例如该行定义 : public abstract class RoomCache<P extends PlayerBO, M extends MemberBO, V extends VideoDataBO<M>, R extends RoomBO<M, V>> extends AbstractCache<PlatformRoomId, R> {}
- 类型变量使用大写的一个字母这是代表:
E
集合的元素类型K V
表示表的关键字和值的类型T U S
等就表示任意类型
|
|
-
场景1:
public static <T extends Comparable<T>> T min(T[] list);
- 限定了入参和返回值是 是实现了Comparable接口的某个类型 因为Comparable也是一个泛型类, 所以也进行限定类型
- 这样的写法要比 T extends Comparable 更为彻底
- 例如计算一个String数组的最小值 T 就是 String类型的, String是Comparable
的子类型 - 但是当处理GregorianCalendar, GregorianCalendar是Calendar的子类, 并且Calendar实现了
Comparable<Calendar>
- 因此GregorianCalendar实现的是
Comparable<Calendar>
, 而不是Comparable - 这种情况下
public static <T extends Comparable<? super T>> T min(T[] list)
就是安全的
- 但是当处理GregorianCalendar, GregorianCalendar是Calendar的子类, 并且Calendar实现了
-
场景2:
public static <T extends ExcelTransform> List<T> importExcel(Class<T> target)
- 该方法实现了, 传入继承了ExcelTransform接口的类对象, 得到该类的List集合
<T extends ExcelTransform> boolean
这样写编译没报错, 那么就是说, 就是一个泛型的定义, 后面进行引用, 省的重复写- 简单的写法就是
public static <T> List<T> importExcel(Class<T> target)
-
场景3: Spring4.x 添加的泛型依赖注入 , 使用的JPA就是依赖该技术 spring学习笔记(14)——泛型依赖注入
-
场景4: 泛型嵌套以及传递问题 实际代码
- 本来的设想是只要声明了具有泛型约束的类, 就应该不用再声明该类中的泛型类型, 但是由于Java的泛型只是在编译前存在, 编译后就被擦除了, 所以没法做到这样简洁的约束
对于应用程序员, 可能很快的学会掩盖这些声明, 想当然地认为库程序员做的都是正确的, 如果是一名库程序员, 一定要习惯于通配符
否则还要用户在代码中随意地添加强制类型转换直至可以通过编译.
super 只能用于通配符
类型擦除
-
不同于C++的泛型,C++是将模板类组合出来生成一个新的类,Java则是进行类型擦除,然后再类型强转
-
例如
public static <T extends Comparable> T min (T[] list)
- 擦除后
public static Comparable min(Comparable[] list)
- 擦除后
-
例如该方法签名
public static <T extends Comparable & Serializable> T getMax(T[]list)
- 限制了必须是实现了两个接口的类才能使用, 估计为了少创关键字所以使用的是extends关键字来表示T要实现两个接口
- 同样的可以加在类的签名上,进行限制类的泛型类型
public class Pair <T extends Comparable>{}
在Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多只有一个类,如果用一个类作为限定,他必须是限定列表中的第一个
注意:泛型记录在类字节码中的 Signature LocalVariableTypeTable 属性上, 参考: Java泛型-4(类型擦除后如何获取泛型参数)
约束和局限性
以下代码示例:涉及的类Pair在上述的代码中已经定义, Human和Student是继承关系
-
不能使用基本类型 实例化类型参数
- 也就是说没有
Pair<double>
只有Pair<Double>
- 因为类型擦除后,类型是Object并不能放double的值, 但是这样做与Java语言中基本类型的独立状态相一致.
- 但是 可以使用 原始类型数组 例如
byte[]
- valhalla项目正计划支持原始类型
- 也就是说没有
-
运行时类型查询(eq或者instanceof)只适用于原始类型
- 比如
Pair<T>
和Pair<String>
是等价的,因为类型擦除 Pair<String> pair1 和 Pair<Date> pair2
pair1.getClass() 和 pair2.getClass() 是等价的都是返回Pair.class
- 比如
-
不能抛出也不能捕获泛型类实例
- 错误的示例:
public class Problem<T> extends Exception{}
public static <T extends Throwable> void doWork(){try{}catch(T t){}}
- 正确示例:
- 在异常声明中使用类型变量
public static <T extends Throwable> void doWork() throws T{.. catch(){throw t;}}
- 错误的示例:
-
参数化类型的数组不合法
- 例:
Pair<String>[] list = new Pair<String>[10];
- 因为擦除后 list是
Pair[]
类型, 能转成Object[]
这样就失去了泛型的作用 - 如果要使用的话最好直接使用集合 ArrayList:
ArrayList<Pair<String>>
安全又高效
1 2 3
Object[] array = list; array[0] = "hi";// 编译错误 array[0] = new Pair<Date>(); //通过数组存储的检测,但实际上类型错误了,所以禁止使用参数化类型的数组
- 例:
-
不能实例化类型变量(T)以及数组
- 非法
new T(){}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
public Pair(){ first = new T(); second = new T(); } //非法 T.class是不合法的 first = T.class.newInstance() //要实例化一个Pair<T>的对象就要如下: public static <T> Pair<T> initPair(Class<T> c){ try{ return new Pair<T>(c.newInstance(), c.newInstance()); }catch (Exception e){ return null; } } // 如下调用 Pair<String> pair = Pair.initPair(String.class); // 因为Class本身是泛型, String.class其实是Class<String>的实例 // 也不能实例化为一个数组 new T[5]
- 非法
-
泛型类的静态上下文中类型变量无效
- 不能在静态域中使用类型变量 如下:
- 如果这段代码能执行,那就可以声明一个 Singleton
共享随机数生成类, - 但是声明之后,类型擦除,就只剩下了Singleton类,并不能做对应的事情,所以禁止这样的写法
1 2 3 4
private static T first; // 错误 public static T getFirst(){ // 错误 return first; }
-
注意泛型擦除后的冲突
- 当类型擦除时,不能创建引发冲突的相关条件
- 例如 新实现一个类型变量约束的equals方法就会和Object原方法冲突 补救方法就是重命名该方法了
1 2 3 4 5
public class Pair<T>{ public boolean equals (T value){ return .. } }
泛型规范说明
- 要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化
- 以下代码就是非法的, GregorianCalendar 实现了两个接口,两个接口是Comparable接口的不同参数化,这是不允许的
1 2
class Calendar implements Comparable<Calendar>{} class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>{} // 错误
- 但是如下又是合法的
1 2
class Calendar implements Comparable{} class GregorianCalendar extends Calendar implements Comparable{}
- 很有可能是桥方法有关,不可能有两个一样的桥方法(因为两个接口其实是一个接口的不同参数化,桥方法的方法签名是一致的)
Tips
- Stream Optional结合泛型出现的极端问题 JDK bugs:嵌套泛型
- Stream bug
- Lambda 多继承bug
泛型类型的继承规则
例如 父子类: Human Student 那么 Pair
Pair 是继承(inherit)关系么,答案是否定的!!
|
|
为何
List<Object> list = Arrays.asList("1","2");
能通过编译
永远可以将参数化类型转换为一个原始类型, Pair
是原始类型Pair的一个子类型,转换成原始类型也会产生错误
相关测试类
|
|
泛型类可以扩展或实现其他的泛型类,就这一点而言,和普通类没有什么区别
- 例如 ArrayList
实现List 接口, 这意味着一个ArrayList 可以转换为List - 但是一个ArrayList
不是ArrayList 或者List .
- 但是一个ArrayList
通配符类型
-
Producer extends, Consumer super.
? extends
: 数据的提供方 执行 get 操作? super
: 数据的存储方 执行 set 操作
-
Tips
- 限定通配符总是包括自己
- 如果你既想存,又想取,那就别用通配符
- 不能同时声明泛型通配符上界和下界
<T>
可以看作<T extends Object>
<?>
可以看作<? extends Object>
注意
通配符的泛型约束一般是出现在基础库的API上(接口上, 方法上) 常见应用逻辑代码用的较少
子类 类型限定的通配符 extends
通配符上限 顾名思义,就是限定为该类及其子类
- 例如:
Pair<? extends Human>
表示任何Pair泛型类型并且他的类型变量要为Human的子类- 编写一个方法
public static void printMessage(Pair<Human> human){}
正如上面所说, Pair
类型的变量是不能放入这个方法的,因为泛型变量是没有继承关系, 这时候就可以使用这个通配符:
public static void printMessage(Pair<? extends Human>)
可以get不能set
|
|
注意此情况无法编译, 目前理解为编译期无法确认T的实际类型
|
|
基类 类型限定的通配符 super
通配符下限 顾名思义就是限定为父类, 通配符限定和类型变量限定十分相似, 但是可以指定一个超类型限定(supertype bound)
? super Student
这个通配符就限定为Student的所有超类型(super关键字已经十分准确的描述了这种关系)
带有超类型限定的通配符的行为和前者相反,可以为方法提供参数,但不能使用返回值即 可以 set 但是不能get
|
|
总结: 类定义上的泛型变量:
子类型限定: <? extends Human> 是限定了不能set,但是保证了get
超类型限定: <? super Student> 限定了不能正确get,但是保证了set.
无限定通配符
|
|
- 例如 这个hasNull()方法用来测试一个pair是否包含了指定的对象, 他不需要实际的类型.
通配符捕获
- 如果编写一个交换的方法
|
|
- 但是可以编写一个辅助方法
|
|
- swapHelper是一个泛型方法, 而swap不是, 它具有固定的Pair>类型的参数, 那么现在就可以这样写:
public static void swap(Pair<?> p){swapHelper(p);}
- 这种情况下, swapHelper方法的参数T捕获通配符, 它不知道是哪种类型的通配符,但是这是一个明确的类型 并且
<T>swapHelper
在T指出类型时,才有明确的含义 - 当然,这种情况下并不是一定要用通配符, 而且我们也实现了没有通配符的泛型方法
但是下面这个通配符类型出现在计算结果中间的示例
|
|
反射和泛型
JDK中Class类也泛型化了, 例如String.class实际上是
Class<String>
类的对象(事实上是唯一的对象)
类型参数十分有用, 这是因为他允许Class<T>
方法的返回类型更加具有针对性.
Class<T>
的方法就使用了类型参数
|
|
- newInstance方法返回一个示例, 这个实例所属的类由默认的构造器获得, 它的返回类型目前被声明为T, 其类型与
Class<T>
描述的类相同, 这样就免除了类型转换. - 如果给定的类型确实是T的一个子类型, cast方法就会返回一个现在声明为类型T的对象, 否则, 抛出一个BadCastException异常
- 如果这个类不是enum类或类型T的枚举值的数组, getEnumConstants方法将返回Null.
getConstructor
与getDeclaredConstructor
方法返回一个Constructor<T>
对象.Constructor类也加上了泛型, 方便newInstance方法有正确返回类型.
TODO 还要继续看书
|
|
Author Kuangcp
LastMod 2018-11-21