TL;DR:
- 符号:
<Child extend Parent>
、<Parent super Child>
(这么写便于理解, 实际前面为?
号)- java中对于泛型 型变 只是一种约束
- 泛型 equal 类型安全的万能匹配 // 类似 “模板代码” 的技术
- 泛型在 1.5 才提出
- 不支持基本类型
- 不支持类型检测 (
instanceof List<A>
,instanceof T
)- 不能实例化类型参数和泛型数组
Java泛型使用类型擦除 (Type Erased) 实现, 所有工作都是编译器 compiler 做的, JVM 并不知道泛型的存在
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都会替换为他们的限定类型 (Object 替代了
T/?
, Parent 替换<? xxx Parent>
)- 会合成桥接方法来保持多态
- 为保持类型安全性,必要时会插入强制类型转换
ps: arrays in Java are covariant, generics are not, they are invariant
类型转换: 型变、不变、协变、逆变
e.g. 分页对象使用了范型
PageVO<T extends BaseVO>
一. 型变的定义1
- 类型转换: 将已知类型转换为另一种类型
- 型变 (variant, 又称变型) 是 Java 类型系统中 类型转换 的概念
- 不变 (invariant) 表示 Crate<Orange> 和 Crate<Fruit> 之间没有关系, 是不同对象
- 转换发生的场景: 泛型/数组/方法
下面主要讲泛型的型变, 准备实验类: Orange 类, 为 Fruit 类的子类,以泛型集合类 List<T>为例
static class Fruit { }
static class Orange extends Fruit { }
static class BloodOrange extends Orange { }
static class SweetOrange extends Orange { }
型变: 分为 逆变 和 协变 (与 不变 相对应), 用来描述类型转换后的继承关系
- 协变 (covariant) 表示 List<Orange> 是 List<? extends Fruit> 的子类型。上界 Fruit
- 逆变 (contravariant) 表示 List<Fruit> 是 List<? super Orange> 的子类型。下界 Orange
把对象想象成一颗从 Object 往下的树,能更好理解上下界
返回类型只能用协变(covariance),返回类型是接口或委托定义返回类型的子类,
参数类型只能用逆变(contravariance),参数类型是接口或委托定义的参数类型的父类。
- 生产者(Producer): 只能读取的对象; (没有消费者方法的对象)
- 生成者方法: 返回类型为T的方法 ( T getObject( ) )
- 消费者(Consumer): 只能写入的对象; (没有生产者方法的对象)
- 消费者方法: 参数带T的方法 ( void setObject( T t ) )
- 子类型(subtype) 不等于 子类(subclass)
- 子类一定是子类型,子类型不一定是子类
List<Orange>
是List<? extends Fruit>
的子类型,List<Orange>
不是List<? extends Fruit>
子类
二. 型变的目的
为了增加灵活性, 而做的约束;
对于协变 extends, 只 get;保证泛型类的类型安全;
对于逆变 super, 只 set;"get()"本身是类型安全的, 只是因为不确定返回的类型, 所以需要加以限制;
通过编译器, 限制泛型类上某些方法 ( producer method / consumer method ) 的调用;
三. 泛型协变 – PECS 原则 (全称 Producer Extends Consumer Super)
extends is for reading
super is for writing
- 定义
使用 extends 确定上界的只能是生产者,只能往外生产东西,取出的就是上界类型。不能往里塞东西。
使用 super 确定下界的只能做消费者,只能往里塞东西。取出的因为无法确定类型只能转成 Object 类型
- [extends get] 协变指定了上界 (限定父类,允许往下的子类), 限制调用 set 方法, 允许调用get来保证类型的安全
public class Test {
public static void main(String[] args) {
List<Orange> oranges = new ArrayList<Orange>();
oranges.add(new Orange());
producer(oranges);
}
static void producer(List<? extends Fruit> fruits) { // 提供了 1 个水果
Fruit f = fruits.get(0);
fruits.add(null); // 除了 null 之外, 都会 Compile error
// fruits.add(new Fruit());
// fruits.add(new Orange());
}
}
顺带说一下集合的设计, 可以注意到, 集合中只有
add(E e)
方法是泛型参数, 而remove(Obeject o)
和contains()
方法并不是, 为何要这样设计? 为何不把其余方法的参数类型也改为E?
其原因就是在于, 如果将 contains 和 remove 改为 E, 那么声明上界之后, 调用这两个方法会引发编译错误, 然而这两个方法均为类型安全方法, 自然不可声明为 E. add 作为很明显的写方法, 自然也需要用 E 作为参数类型
- [super set] 逆变限定了下界 (限定子类,允许往上的父类), 允许调用 set 方法, 并可以将objects赋值给它
public class Test {
public static void main(String[] args) {
List<Fruit> fruits = new ArrayList<Fruit>();
fruits.add(new Fruit());
consumer(fruits);
}
static void consumer(List<? super Orange> objs) { // 消费了 3 个橘子
objs.add(new Orange());
objs.add(new BloodOrange());
objs.add(new SweetOrange());
// 无法安全地读取, 完全可能包含 Orange 基类, 比如 Fruit 对象, 比如这里的 Object o 可以强转成 Fruit, 但不能直接获取成 Fruit
// Fruit f = objs.get(0); // wrong
Object o = objs.get(1); // right
}
}
对应协变的上界, 自然有逆变的下界, 使用
<? super >
组合来声明一个泛型的下界, 来表示可以 接收 本类型 或 其父类型
- 协变和逆变互为反方向,在协变中我们可以安全地从泛型类中读取(从一个方法中返回),而在逆变中我们可以安全地向泛型类中写入(传递给一个方法)
ps:
如果一个范型类ClassCase<T>
使用 extends, 就可以使用T get(Object o)
方法, 不能使用Object set(T t)
方法
如果一个范型类ClassCase<T>
使用 super, 就可以使用Object set(T t)
方法, 不能使用T get(Object o)
方法
ObjectService os = new ObjectService();
Function<? extends ObjectService, ?> funce;
Function<? super ObjectService, ?> funcs;
Function<ObjectService, ?> function = objectService -> null;
// 都能赋值成功
funce = function;
funcs = function;
// extends 无法接收入参 无解
funce.apply(os); // error
funcs.apply(os);
Function<?, ? extends ObjectService> funcie;
Function<?, ? super ObjectService> funcis;
Function<?, ObjectService> function1 = s -> os;
// 都能赋值成功
funcie = function1;
funcis = function1;
// super 无法返回对象 但可以变通: Object apply = funcis.apply(null);
ObjectService apply = funcis.apply(null); // error
ObjectService apply1 = funcie.apply(null);
上面的例子可以看出
- super 是一个功能性比 extends 稍逊的声明
- 实际上 super 就是
List<Object>
- super 只是为了限制入参, 也只能作用于入参声明, 不能作为返回体声明
- JDK 的 Collections 的 copy 方法是 PECS 最好的例子
// java.util.Collections#copy
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
四. 数组 & 方法 的协变 Covariance
顺带一提, 数组和方法都支持协变, 大多数人都知道, 只不过不知道名字而已
- 方法的协变很好理解
- 数组的协变, 应用较少, 也不推荐使用 (Effective Java中直接指出允许数组协变是Java的缺陷, 也是多用列表 List 而不用数组 Array 的原因之一)
Covariance 代表着越来越具体化 Specific
五. 总结
- 协变extends限定了通配符类型的上界, 可以称为上界通配符(Upper Bounds Wildcards),所以我们可以安全地从其中读取;(方便理解: 通过父类引用获取子类值)
- 比如我们在其他类使用时, 方法体里定义参数:
void get(Pair<? extends Number> p){ // 可以传入Pair<Integer>
方法体里面, p为Number类型引用, 实际类型以传入的为准(多态)
}
而如果是:
void get(Pair<Number> p){} // 不能传入Pair<Integer>
- java里, Pair<Integer>和Pair<Number>, 互相没有关系;
- 所以协变就是把泛型的T上限, 限定在了Number上; 使创造出来的Pair<? extends Number>和Pair<Integer>有继承关系, 前面一个是父类型; 协变不能set;
- 而super限定了通配符类型的下界,所以我们可以安全地向其中写入; 逆变不能get;
(方便理解: 子类属性比父类多, 父类属性更通用, 名义上子类通过父类属性赋值, 即通用属性赋值) - 生产与消费可以这样对应: Producer-Extends(协变get), Consumer-Super(逆变set)。
- btw, 都只是为了能传值取值; 协变规定了从父类型获取, 逆变规定了从子类型赋值;
限定类型
之前的部分, 大多是定义: class XX<T>{} 使用在方法上: add(XX<? extends Number> p){}
现在介绍用在类上的限定类型:
public class Pair<T extends Number> {}
- 一句话解释, 该泛型定义在类级别, 在使用时, 只能新建为Number或者Number的子类, 是一种约束;
Pair<Number> p1 = null;
Pair<Integer> p2 = null;
Pair<Double> p3 = null;
....所有的Number子类
- super不能用在class定义处,只能用在方法参数;
一. 无限定通配符
- <?>通配符:Pair<?>是所有Pair的超类
1. <?> 和 <? extends Object>的异同
- 首先, 使用到了 extends 所以只能 producer, 所以一般使用在方法入参上
public static void printListWildCard(List<? extends Object> list)
public static void printListWildCard(List<?> list)
List<Integer> li = Arrays.asList(1, 2, 3);
printListWildCard(li);
- 工作方式相同. 因为 Object 是 Java 所有对象的超类, 基本上所有的东西都扩展了 Object. 因此, 这个方法也会处理一个 Integer 类型的 List
也就是说, 大多数情况下 <?> 和 <? extends Object> 是同一个意思
<T extends Object & Foo>
will cause T to become Object
under erasure, whereas with <T extends Foo>
it will become Foo
under erasure.
- 区别
首先加个概念, 可重构类型
(reifiable 可具体化的, 可实现的) 是指那些在编译时 未被擦除
的类型, 不可重构类型
是会被擦除的类型
被擦除的类型, 在 运行时 表达的信息更少 (相比编译时)
比如 List<String>
和 Map<Integer,String>
就 不可重构
, 编译器会擦除它们的类型, 并将它们分别视为列表 List 和映射 Map, 所以会更严格
现在回到上面的例子: 无界通配符类型, 也就是 List<?>
以及 Map<?, ?>
是可重构的 (可以理解为 不严格的
)
List<? extends Object>
是不可重构. 虽然微妙, 但这是一个显著的区别.
<?> is reifiable while <? extend object> is not. The reason they did this is to make it easier to distinguish reifiable type. Anything that looks like <? extends something>,<T>,<Integer> are nonreifiable.
List someList = new ArrayList<>();
boolean instanceTest = someList instanceof List<?>; // true List<?> = List、List<Object>
boolean instanceTest = someList instanceof List<? extends Object>; // false ~ == List<Object>
二. 自限定类型(Self-Bounds Types)
Java中常用的自限定写法:
class Pair<T extends Base<T>> {
T element;
T get() {
return element;
}
void set(T t) {
element = t;
}
}
- class Subtype extends BasicHolder<Subtype> {}这样用,就构成自限定了。从定义上来说,它继承的父类的类型参数是它自己。
- 自限定的用法:父类作为一个泛型类或泛型接口,用子类的名字作为其类型参数。
- 这里再把Subtype抽象成类型参数T,刚好变成了T extends SelfBounded<T>这样的写法。
- 暂时埋个坑, 以后补
这种语法定义了一个基类,这个基类能够使用子类作为其参数、返回类型、作用域。
E: Element
K: Key
V: Value
N: Number
T: Type
关于 List
、List<?>
、List<Object>
的区别
容器类使用泛型的好处:
- 安全性:在对参数化类型的容器中放入了错误即不匹配的类型的时候,编译器将会强制性进行错误提示
- 便利性:当从容器中取出元素的时候不用自己手动将
Object
转换为元素的实际类型了,编译器将隐式地进行自动转换 - 表述性:带有类型实参的泛型即参数化类型,可以让人看到实参就知道里面的元素
E
都是什么类型
引用变量的类型 | 名称 | 可以接受的类型 | 能否添加元素 | 安全性 | 便利性 | 表述性 |
---|---|---|---|---|---|---|
List | 原始类型 | 任何对应 List<E> 的参数化类型 (包括 List<?> ) | 可添加任意类型的元素 | 无 | 无 | 无 |
List<?> | 通配符类型 | 以接受任何对应 List<E> 的参数化类型 (包括 List ) | 不能添加任何元素 | 有 | 无 | 有 |
List<Object> | 实际类型参数为 Object 的参数化类型 | 仅接受 List 和其本身类型 | 可添加任意类型元素 | 有 | 有 | 有 |
-
List<?> 缺点在于不能添加任何元素, 只能使用
-
泛型的子类型化的原则:
List<String>
类型是原生类型List
的一个子类型, 而不是参数化类型List<Object>
的子类型
“?” is inclusive of “Object” in the class hierarchy. You could say that String is a type of Object and Object is a type of ?. Not everything matches Object, but everything matches ?.
List<Object> object = Lists.newArrayList();
void testQuery(List<?> l) {}
testQuery(object); // compiles because any list will work
List<?> query = Lists.newArrayList();
void testObject(List<Object> l) {}
testObject(query); // fails because a ? might not be an Object
but,
List<?> query = Lists.newArrayList();
List<Object> hack = List<Object> query;
testObject(hack); // should work~
// void testObject(List<Object> l) {}
so,
// 编译时
List<String> -> List
List<Object> -> List
// 编码时
List<Any> 赋值 -> List<?> (代价是只能get了)
// 运行时
List<?> ≈ List / List<Object>
说到底, 只是一个编译时的规定
编译时和运行时
Java 有 编译期、运行期 两个阶段,
- 编译期: 源代码 在 编译器
- 运行期: 字节码 在 虚拟机
因为存在“编译后泛型擦除”, Java的泛型也被称为“假泛型”
-
泛型是JDK专门为编译器创造的语法糖,只在编译期,由编译器负责解析,虚拟机不知情
- 存入:普通类继承泛型类并给变量类型T赋值后,就能强制让编译器帮忙进行类型校验
- 取出:代码编译时,编译器底层会根据实际类型参数自动进行类型转换,无需程序员在外部手动强转
-
实际上,编译后的Class文件还是JDK1.5以前的样子,虚拟机看到的仍然是 Object
-
泛型有以下作用:
- 抽取代码模板:代码复用并且可以通过指定类型参数与编译器达成约定
- 类型校验:编译时阻止不匹配元素进入
Object[]
- 类型强转:根据泛型自动强转(多态,向下转型)
在大家开始慢慢觉得泛型只和编译器有关,但实际上泛型的成功离不开多态
代码模板的本质就是:用Object接收一切对象,用泛型+编译器限定特定对象,用多态支持类型强转。
泛型只是程序员和编译器的约定,程序员告诉编译器,我假定这个List只能存String,你帮我盯着点。对于存入的方法,如果不小心放错类型,就编译报错提醒我。对于取出的方法,编译时你根据我给的实际类型参数自动帮我类型转换吧
- 对象类型弄成了变量吗?
通过反编译大家也看到了,其实根本没有所谓的泛型类型T,底层还是Object
,所以当我们new一个ArrayList
时,JVM根本不会傻傻等着T被确定。T作为参数类型,只作用于编译阶段,用来限制存入和强转取出,JVM是感知不到的,它不关心这个对象将来是 ArrayList<Integer>
还是ArrayList<String>
,仍然还是按JDK1.4以前的做法,底层准备Object[]
,以便利用多态特性接收任意类型的对象。
更何况JVM实际new对象是在运行期,编译期的小把戏和它有什么关系?
所以对于泛型类本身来说,它的类型是确定的,就是Object
或Object[]
数组
- 为什么泛型不支持基本类型
JDK1.5 不仅引入泛型,还同时发布自动拆装箱特性, 在这之前 Object obj = 666;
是不能赋值的
但自动拆装箱特性, 也只是一个语法糖级别的特性, 也就是功能上支持了, 但语法层面还是不支持的; 碰上数组之类的, 赋值必然需要诶个拆箱, 带来性能损耗, 属于是既不允许也没必要
JAVA动态绑定机制
编译类型 or 运行类型
- 编译时类型由声明该变量时使用的类型决定, 编译时引用变量只能调用其编译类型所具有的方法
- 运行时类型由实际赋给该变量的对象决定
动态绑定机制是绑定运行类型
- 调用对象方法的时, 方法会和该对象的 内存地址/运行类型 绑定
- 调用对象属性时,没有动态绑定机制,哪里声明哪里使用
Java 数组是一个对象
https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.3.1
https://docs.oracle.com/javase/specs/jls/se8/html/jls-10.html
https://www.geeksforgeeks.org/array-primitive-type-object-java/
In the Java programming language, arrays are objects (§4.3.1), are dynamically created, and may be assigned to variables of type Object (§4.3.2). All methods of class Object may be invoked on an array.
int[] ints = {1, 1, 1, 1, 1, 11, 897, 1, 1, 1, 3, 4, 34, 3, 53, 53, 2, 3};
Arrays.parallelSort(ints);
// 编译错误, 因为该方法签名用的是范型 Arrays.sort(T[], Comparator<? super T> c); 由此可见 ints 没办法作为一个 对象[] 接收
Arrays.parallelSort(ints, Collections.reverseOrder());
Arrays.stream(ints).boxed().sorted(Collections.reverseOrder()).forEach(System.out::println);
https://www.jianshu.com/p/0c2948f7e656 ↩︎