类(Classe)类 Vs 类型(Type)
首先给出两者维基百科上的定义
类:在面向对象编程中,类是一个可扩展的程序代码模板,用于创建对象,为状态(成员变量)和行为(成员函数或方法)的实现提供初始值。(en.wikipedia.org/wiki/Class_…
类型:类型就是数据的分类.....决定了该类型可能的值,以及在该类型的值上可以完成的操作。(en.wikipedia.org/wiki/Data_t…)
有些语言(尤其是Java)模糊了类型和类之间的界限。您甚至可以在教程、文档等中看到它们互换使用。虽然它们是相关的概念,但它们不是同义词。
在Kotlin中
var x: String
声明了一个可以保存String类实例的变量;但是,同样的类也可以用来声明可空类型
var x: String?
这意味着每个Koltin类(Class)都可以构造至少2种类型(Type).
具体到范型类,情况会更加复杂;请看如下代码:
ArrayList a = new ArrayList<Integer>();
ArrayList a是ArrayList类的一个实例。但是它的类型不是ArrayList,而是ArrayList<Integer>。ArrayList<Integer>和ArrayList<String>是同一个类(ArrayList)的实例,但它们显然不是同一个类型——它们保存着完全不同类型的数据!
ArrayList不是类型(Type),而是类(class)。因为它本身接受类型形参。当我们写ArrayList时,实际上是指ArrayList<E>,其中E是一个类型参数。
子类型
任何时候,如果要使用 类型A 的值,都能用 类型B 的值作为替换(当做 A 的值),称 B 是 A 的子类型。
从定义中可以看出,任何类型也是它自身的子类型。
把定义说的通俗一点就是 “小范围的类可以替换大范围的类” 。Int
是Number
的子类,是因为Int
所代表的数的集合范围是Number
所代表的子集。
“一个类型是否是另一个的子类型”?这个问题对于编译器来说很重要,因为每次给变量赋值或为函数传递实参时都要做这个检查。只有值的类型是变量的子类型时,才允许变量存储该值。
为什么要有范型
JDK 1.5开始引入Java泛型(generics)这个特性,该特性提供了编译时类型安全检测机制,允许程序员在编译时检测到非法的类型。
在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。
比如:没有泛型的情况下使用集合:
public static void noGeneric() {
// 编译正常通过,但是使用的时候可能转换处理出现问题
ArrayList arr = new ArrayList();
arr.add("加入一个字符串");
arr.add(1);
arr.add('a');
}
在 Java/Kotlin 中我们常用集合( List
、 Set
、 Map
等等)来存储数据,而在集合中可能存储各种类型的数据,现在我们有四种数据类型 Int
、 Float
、 Double
、 Number
,假设没有泛型,我们需要创建四个集合类来存储对应的数据。
class IntList{ }
class Floatlist{}
class DoubleList{}
class NumberList{}
如果有更多的类型,就需要创建更多的集合类来保存对应的数据,这显示是不可能的,而泛型是一个 "万能的类型匹配器",同时有能让编译器保证类型安全。
泛型将具体的类型( Int
、 Float
、 Double
等等)声明的时候使用符号来代替,使用的时候,才指定具体的类型。
协变,逆变与不变型
有2个类型A,B;如果B时A的字类型,后续我们简称A > B.
如果类型 A > 类型 B,经过一个变化 trans 后得到的 trans(A) 与 trans(B) 依旧满足 trans(A) > trans(B),那么称为协变。
逆变则刚好相反,如果类型 A > 类型 B,经过一个变化 trans 后得到的 trans(A) 与 trans(B) 满足 trans(B) > trans(A),称为逆变。
假设 泛型 类A 包含 类型参数T,即class A<T>
,而Type1
是Type2
的子类,如果A<Type1>
和A<Type2>
不存在父子关系,则称 类A 在类型参数上是不变型的。
Java 的数组是协变的,假如 A > B,那么有 A[] > B[],所以 B[] 可以赋值给 A[]。举个例子:
Number[] nums = new Integer[] {1, 2, 3};
nums[2] = 2.0;
以上代码可以正常编译,但是会引发运行时错误。因为错误的将Float数据放入到Int类型数组中;
Kotlin 和 Java 中的类都是不变型的。
这样会造成一些限制,辛辛苦苦抽象了一个方法,它接收一个List<CharSequence>
作为参数:handle(chars: List<CharSequence>)
,想当然地把List<String>
传递进入时,编译器会报错;
ArrayList<Number> numbers = new ArrayList<Integer>();
以上代码编译报错,如果这段代码编译通过,必然会造成运行时异常。
为了保证安全,所有Java/Kotlin中泛型是不支持型变。
协变应用场景
B 是 A 的子类,那么往ArrayList<A>中添加些c数据,是个很通用的场景;此时Collection<B>作为Collection<A> 的子类,这是一个非常自然而然的想法!但由于Java 中的类都是不变型,因此直接裸类型是无法编译通过的。
- Java 提供了<? extends T>使泛型拥有协变特性;
ArrayList addAll方法源码如下:
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
- 在 Kotlin 中关键字
out
表示协变
interface ProduceExtends<out T> {
val num: T // 用于只读属性
fun getItem(): T // 用于函数的返回值
}
逆变应用场景
有类型A,B, C;加入A>B>C,那么将符合条件的B数据,添加到ArrayList<A>或者ArrayList<B>是个很通用的场景;那么声明一种trans(C)类型,可以接受ArrayList<A>,ArrayList<B>类型(用于接收C类型数据),即可通用处理这种业务逻辑;此场景下需要使用逆变;
- 在 Java 中使用通配符
? super
表示逆变 - 在 Kotlin 中使用关键字
in
表示逆变
Koltin中Iterable 拓展方法filterTo
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
例如ArrayList forEach方法源码如下:
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
final Object[] es = elementData;
final int size = this.size;
for (int i = 0; modCount == expectedModCount && i < size; i++)
action.accept(elementAt(es, i));
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
协变应用缺陷
协变放宽了对数据类型的约束,但是放宽是有代价的,我们在花三秒钟思考一下,下面的代码是否可以正常编译。
// Koltin
val numbers: MutableList<out Number> = ArrayList<Int>()
numbers.add(1)
// Java
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(1)
调用 add()
方法会编译失败,虽然协变放宽了对数据类型的约束,可以接受 Number
或者 Number
子类型为对象的集合,但是代价是 无法添加元素,只能获取元素,因此协变只能作为生产者,向外提供数据。
为什么无法添加元素
因为 ?
表示未知类型,所以编译器也不知道会往集合中添加什么类型的数据,因此索性不允许往集合中添加元素。
但是如果想让上面的代码编译通过,想往集合中添加元素,这就需要用到逆变了。
逆变应用缺陷
现在我们将上面的代码简单修改一下,在花三秒钟思考一下是否可以正常编译
// Kotlin
val numbers: MutableList<in Number> = ArrayList<Number>()
numbers.add(100)
// Java
List<? super Number> numbers = new ArrayList<Number>();
numbers.add(100);
答案可以正常编译,逆变通配符 ? super Number
或者关键字 in
将继承关系颠倒过来,主要用来限制未知类型的子类型,在上面的例子中,编译器知道子类型是 Number
,因此只要是 Number
的子类都可以添加。
逆变可以往集合中添加元素,那么可以获取元素吗?我们花三秒钟时间思考一下,下面的代码是否可以正常编译。
// Kotlin
val numbers: MutableList<in Number> = ArrayList<Number>()
numbers.add(100)
numbers.get(0)
// Java
List<? super Number> numbers = new ArrayList<Number>();
numbers.add(100);
numbers.get(0);
无论调用 add()
方法还是调用 get()
方法,都可以正常编译通过,现在将上面的代码修改一下,思考一下是否可以正常编译通过。
// Kotlin
val numbers: MutableList<in Number> = ArrayList<Number>()
numbers.add(100)
val item: Int = numbers.get(0)
// Java
List<? super Number> numbers = new ArrayList<Number>();
numbers.add(100);
int item = numbers.get(0);
调用 get()
方法会编译失败,因为 numbers.get(0)
获取的的值是 Object
的类型,因此它不能直接赋值给 int
类型,逆变和协变一样,放宽了对数据类型的约束,但是代价是 不能按照泛型类型读取元素,也就是说往集合中添加 int
类型的数据,调用 get()
方法获取到的不是 int
类型的数据。
PECS
PECS(Producer-Extends, Consumer-Super)原则,即 ? extends
或者 out
作为生产者,? super
或者 in
作为消费者。遵循这个原则的好处是,可以在编译阶段保证代码安全,减少未知错误的发生。
协变只能读取数据,不能添加数据,所以只能作为生产者,向外提供数据,因此只能用来输出,不能用来输入。
逆变只能添加数据,不能按照泛型读取数据,所以只能作为消费者,因此只能用来输入,不能用来输出。