6.1 泛型(Generic Type)简介
通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么是自定义的类。
但是尤其在集合类的场景下,我们需要编写可以应用于多种类型的代码,我们最简单原始的做法是,针对每一种类型,写一套刻板的代码。
这样做,代码复用率会很低,抽象也没有做好。
在 jdk 5 中,Java引入了泛型。泛型,即“参数化类型”(Parameterized Type)。顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式,我们称之为类型参数,然后在使用时传入具体的类型(类型实参)。
我们知道,在数学中泛函是以函数为自变量的函数。类比的来理解,编程中的泛型就是以类型为变量的类型,即参数化类型。这样的变量参数就叫类型参数(Type Parameters)。
本章我们来一起学习一下Kotlin泛型的相关知识。
6.1.1 为什么要有类型参数
我们先来看下没有泛型之前,我们的集合类是怎样持有对象的。
在Java中,Object类是所有类的根类。为了集合类的通用性。我们把元素的类型定义为Object,当放入具体的类型的时候,再作强制类型转换。
这是一个示例代码:
class RawArrayList {
public int length = 0;
private Object[] elements;
public RawArrayList(int length) {
this.length = length;
this.elements = new Object[length];
}
public Object get(int index) {
return elements[index];
}
public void add(int index, Object element) {
elements[index] = element;
}
@Override
public String toString() {
return "RawArrayList{" +
"length=" + length +
", elements=" + Arrays.toString(elements) +
'}';
}
}
一个简单的测试代码如下:
public class RawTypeDemo {
public static void main(String[] args) {
RawArrayList rawArrayList = new RawArrayList(4);
rawArrayList.add(0, "a");
rawArrayList.add(1, "b");
System.out.println(rawArrayList);
String a = (String)rawArrayList.get(0);
System.out.println(a);
String b = (String)rawArrayList.get(1);
System.out.println(b);
rawArrayList.add(2, 200);
rawArrayList.add(3, 300);
System.out.println(rawArrayList);
int c = (int)rawArrayList.get(2);
int d = (int)rawArrayList.get(3);
System.out.println(c);
System.out.println(d);
// Exception in thread "main" java.lang.ClassCastException:
// java.lang.Integer cannot be cast to java.lang.String
String x = (String)rawArrayList.get(2);
System.out.println(x);
}
}
我们可以看出,在使用原生态类型(raw type)实现的集合类中,我们使用的是Object[]数组。这种实现方式,存在的问题有两个:
向集合中添加对象元素的时候,没有对元素的类型进行检查,也就是说,我们往集合中添加任意对象,编译器都不会报错。
当我们从集合中获取一个值的时候,我们不能都使用Object类型,需要进行强制类型转换。而这个转换过程由于在添加元素的时候没有作任何的类型的限制跟检查,所以容易出错。例如上面代码中的:
String a = (String)rawArrayList.get(0);
对于这行代码,编译时不会报错,但是运行时会抛出类型转换错误。
由于我们不能笼统地把集合类中所有的对象是视作Object,然后在使用的时候各自作强制类型转换。因此,我们引入了类型参数来解决这个类型安全使用的问题。
Java 中的泛型是在1.5 之后加入的,我们可以为类和方法分别定义泛型参数,比如说Java中的Map接口的定义:
public interface Map<K,V> {
...
boolean containsKey(Object key);
boolean containsValue(Object value);
V get(Object key);
V put(K key, V value);
V remove(Object key);
void putAll(Map<? extends K, ? extends V> m);
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
}
我们在Kotlin 中的写法基本一样:
public interface Map<K, out V> {
...
public fun containsKey(key: K): Boolean
public fun containsValue(value: @UnsafeVariance V): Boolean
public operator fun get(key: K): V?
@SinceKotlin("1.1")
@PlatformDependent
public fun getOrDefault(key: K, defaultValue: @UnsafeVariance V): V {
// See default implementation in JDK sources
return null as V
}
public val keys: Set<K>
public val values: Collection<V>
public val entries: Set<Map.Entry<K, V>>
}
public interface MutableMap<K, V> : Map<K, V> {
public fun put(key: K, value: V): V?
public fun remove(key: K): V?
public fun putAll(from: Map<out K, V>): Unit
...
}
比如,在实例化一个Map时,我们使用这个函数:
fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>
类型参数K,V是一个占位符,当泛型类型被实例化和使用时,它将被一个实际的类型参数所替代。
代码示例
>>> val map = mutableMapOf<Int,String>(1 to "a", 2 to "b", 3 to "c")
>>> map
{1=a, 2=b, 3=c}
>>> map.put(4,"c")
null
>>> map
{1=a, 2=b, 3=c, 4=c}
mutableMapOf<Int,String>表示参数化类型<K , V>分别是Int 和 String,这是泛型类型集合的实例化,在这里,放置K, V 的位置被具体的Int 和 String 类型所替代。
泛型主要是用来限制集合类持有的对象类型,这样使得类型更加安全。当我们在一个集合类里面放入了错误类型的对象,编译器就会报错:
>>> map.put("5","e")
error: type mismatch: inferred type is String but Int was expected
map.put("5","e")
^
Kotlin中有类型推断的功能,有些类型参数可以直接省略不写。上面的mapOf后面的类型参数可以省掉不写:
>>> val map = mutableMapOf(1 to "a", 2 to "b", 3 to "c")
>>> map
{1=a, 2=b, 3=c}
Java 和 Kotlin 的泛型实现,都是采用了运行时类型擦除的方式。也就是说,在运行时,这些类型参数的信息将会被擦除。Java 和 Kotlin 的泛型对于语法的约束是在编译期。