第十三讲 Kotlin泛型篇
前介
泛型,在 Java 中就是一个比较难的东西。我个人认为,它理解起来很简单,但是要想把它用好真的很难。看到别人用泛型巧夺天工的设计,我都湿了。

Java 泛型
讲解 Kotlin 泛型之前,先要将 Java 的泛型理解清楚,因为 Kotlin 的泛型本质还是 Java 泛型( Java 是 Kotlin 的爸爸)。

Java 的泛型有什么用呢?我个人认为:主要是减少代码上类型的转换,以及类型约束。
例如:我们常用的集合,试想想如果没有泛型。我们的
List集合会怎么定义呢?可能类似就要这样定义了StringList丶IntList丶ObjectList或只定义一个ObjectList(万物之祖宗OBJ),试想下若这样的设计给我们开发带来了多少的类型转换呢?
有了泛型,我们在定义类的时候或者方法的声明泛型,这样编译器就知道类型,让编译器做类型检测和强制类型转换。

Java 泛型擦除
前面说了 Java 泛型,本质是给编译器看的,让编译器帮你做强制转换,并将这个泛型擦除掉。这个过程俗称 泛型擦除。

public class TestJava {
public strictfp static void main(String[] args) {
/**
* 告诉编译器 strs 存的是一个 String 类型
*
* 编译器只会做2件事:
*
* 1. 只有检测到 add 不是一个 String 类型,就编译不通过
*
* 2. 只要有 取这个参数操作,帮我强制转换成 String
*/
List<String> strs = new ArrayList();
strs.add("我是 Kotlin");
strs.add("我是 java");
for (int i = 0; i < strs.size(); i++) {
/**
* 注意这里哦
*/
String s = strs.get(i);
System.out.println(s);
}
}
}
接下来看下,反编译后的代码。
public class TestJava {
public TestJava() {
}
public static strictfp void main(String[] args) {
List<String> strs = new ArrayList();
strs.add("我是 Kotlin");
strs.add("我是 java");
for(int i = 0; i < strs.size(); ++i) {
// 看到没,这里自动帮我们强制转换了
String s = (String)strs.get(i);
System.out.println(s);
}
}
}
看到了吧?在取值的时候,是做了强制类型转换的。有人又说,你不是说泛型会擦除吗?怎么反编译的 Java 还存在 List<String> 呢?
反编译的 Java 还保留了泛型,是因为 JDK 1.5 版本之后 class 文件的属性表中添加了 Signature 和 LocalVariableTypeTable 等属性来支持泛型识别的问题,这些信息可以在配置混淆的时候可以删除掉。

行,那我们看下 ByteCode ,让那些杠精死心。


上界通配符
说泛型之前,先理解下通配符。
Java 通配符其实是个 约定,这个约定是给编译器看的。Java 是强类型语言,变量类型的泛型参数也是区分类型的(注意现在说的是通配符和泛型是2个东西哦)。

请问下方代码报错吗?(友情提示:Integer 是 Number 的子类)
public class TestJava {
public strictfp static void main(String[] args) {
ArrayList<Integer> integers = null;
ArrayList<Number> numbers = null;
// 请问这句代码报错吗?
numbers = integers;
}
}
没错编译器不通过,因为编译器认为这是 2 种类型,直接报错。那有没有办法让 Java 的泛型类型也存在多态的关系呢?
是可以的哦,通配符 ? 的出现解决了这个问题。我们改下代码。
public class TestJava {
public strictfp static void main(String[] args) {
ArrayList<Integer> integers = null;
ArrayList<? extends Number> numbers = null;
// 请问这句代码报错吗?
numbers = integers;
}
}
定义改成 ArrayList<? extends Number>,意思我能接收一个属于 Number 子类的的泛型。
通过这样定义会让编译器不报错,但这样就会多一个约束。若通过上界修饰泛型,只能调用对应泛型类的返回泛型的方法(编译器约束:只能用不能改),啥意思呢?
举个例子:
public class TestJava {
public strictfp static void main(String[] args) {
ArrayList<Integer> integer = new ArrayList();
integer.add(1);
integer.add(2);
/**
* 赋值不会报错
*/
ArrayList<? extends Number> numbers = integer;
/**
* 调用 get 方法 不报错
*/
Number number = numbers.get(1);
/**
* 这里 编译不通过
*/
numbers.add(new Integer(3))
}
}
上面的代码,只能调用 ArrayList 的 get 方法,不能调用 add 方法。这下明白了吧,这次在回味我上面的那句话,只能调用对应泛型类的,返回泛型的方法,所以说类似 public E remove(int index) 也能调用(只能用不能改)。
思考问题,为何只能用不能改呢?其实这只是
Java处于安全考虑。
例如上面的例子,因为我通过上界通配符打开了一定的范围,代码角度考虑能保证返回的类型一定是Number的子类,根据多态我一定可以用Number类型的变量去接收。但是存储我就不能确定存的是什么类型了。

例如:
我要做一个寻找 List 集合,所有数字的最大值并返回。想想我不能为所有的 Int 丶 Double 丶 Long 都写重载一个方法吧?
通过分析,我们只用不改集合,因此我们就可以定义这样的一个方法。
public class TestJava {
public strictfp static void main(String[] args) {
ArrayList<Integer> integer = new ArrayList();
integer.add(1);
integer.add(2);
/**
* 可以 Integer 的
*/
System.out.println(max(integer));
ArrayList<Long> longs = new ArrayList();
longs.add(3L);
longs.add(4L);
/**
* 可以计算过 long 的
*/
System.out.println(max(longs));
}
/**
* 用 double 是考虑所有类型
*/
public static double max(List<? extends Number> datas) {
double max = datas.get(0).doubleValue();
for (int i = 1; i < datas.size(); i++) {
if (max < datas.get(i).doubleValue()) {
max = datas.get(i).doubleValue();
}
}
return max;
}
}
当然上面的代码,返回值是
double,我是为了考虑精度不要损失的问题,其实需求应该是我传入的是Long的就返回Long类型的吗,若传入的是Integer的就返回Integer类型。这里会多一个方法泛型定义,我们后面讲来完善这个代码。
下界通配符
有天堂就有地狱,有上界就用下界。下界通配符和上界通配符相反。
上界:子类泛型参数变量,能赋值给通过上界修饰符修饰父类泛型的泛型变量(只能用,不能改)。
下界:父类泛型参数变量,能赋值给通过下界修饰符修饰子类泛型的泛型变量(不能用,能改)。
上面 2 句话是不是懵逼了?的确当时我也懵逼!

不明白?那我们就看个图。

回想下在讲上界的时候,为啥能调用
get方法,不能用add方法。下界的时候怎么也能调用get方法啊,也能调用add方法啊?注意下界调用的get方法返回的是Object,这里说的不能用是说返回不了真正的类型。因为鬼知道接收的是一个什么样的父类类型,但是编译器知道它祖宗一定是Object。

其实功能就是根据特性来定制的,下界通配符就是 不能用,能改 。说实话感觉 下界通配符 的一直没想到一个特别好的例子,感觉 下界通配符 用的好少。
泛型声明
记住上面说的 ? 代表的是通配符,它的功能是约束只能使用或者只能修改。而泛型声明和通配符,根本就是 2 个东西,他们没有任何关系。泛型声明 T 一般是说在声明类的时候或者方法的时候告诉编译器,你要强制转换的类型。

/**
* 定义人的接口
*/
public interface People {
void play();
void eat();
void sleep();
}
/**
* 男人
*/
public static class Man implements People {
@Override
public void play() {
System.out.println("玩LOL");
}
@Override
public void eat() {
System.out.println("吃辣条");
}
@Override
public void sleep() {
System.out.println("打呼噜");
}
}
/**
* 女人
*/
public static class Woman implements People {
@Override
public void play() {
System.out.println("玩QQ炫舞");
}
@Override
public void eat() {
System.out.println("啥都吃");
}
@Override
public void sleep() {
System.out.println("抱娃娃");
}
}
/**
* 创建代理人的 代理类
*/
public static class PeopleProxy<T extends People> implements People {
private T people;
public PeopleProxy(T people) {
this.people = people;
}
/**
* 加入修改的方法
*
* @param people
*/
public void modifyPeople(T people) {
this.people = people;
}
public T getPeople() {
return people;
}
@Override
public void play() {
people.play();
}
@Override
public void eat() {
people.eat();
}
@Override
public void sleep() {
people.sleep();
}
}
其实例子就是静态代理设计模式,代理类上声明泛型 T 加了约束,传入的泛型必须是 People 的子类(也就是说能代理所有 People 的子类)。
接下来我们就可以根据业务需求,可以加一定 上界 或者 下界 约束条件。
public class TestJava {
public static void main(String[] args) {
/**
* 这时候业务逻辑来了,我根据一些信息判断出了他的性别,我们可以确定后续我们只是使用不修改
*
* 所以通过 上界进行约束
*
*/
PeopleProxy<? extends People> proxy = null;
if ("穿的是超短裙吗?") {
proxy = new PeopleProxy(new Woman());
} else {
proxy = new PeopleProxy(new Man());
}
/**
* 尝试调用 getPeople 方法
*
* 编译器通过
*/
People people = proxy.getPeople();
/**
* 尝试调用 modifyPeople 修改的方法
*
* 编译器报错
*/
proxy.modifyPeople(new Man());
}
}
上面代码明白了吧?一定要把 ? 通配符 和 T 区分开,2 个不同的东西。T 泛型声明可以告诉编译器类型检测和帮我强制转换。通配符能限定条件,只能用还是只能改。
类上可以声明泛型,方法上也是可以的哦。还记得讲解 上界通配符 时候,获取一个数字集合中的最大值吗?我当时返回的都是 Double 类型,当时只是想说明只能用不了改的 上界通配符,实际开发我们并不这样写。
public class TestJava {
public static <T extends Number> T max(List<T> datas) {
T max = datas.get(0);
for (int i = 0; i < datas.size(); i++) {
if (max.doubleValue() < datas.get(i).doubleValue()) {
max = datas.get(i);
}
}
return max;
}
public strictfp static void main(String[] args) {
ArrayList<Integer> integer = new ArrayList();
integer.add(1);
integer.add(2);
/**
* 可以 Integer 的,并且自动转化成 Integer 了
*/
Integer max = max(integer);
System.out.println(max);
ArrayList<Long> longs = new ArrayList();
longs.add(3L);
longs.add(4L);
/**
* 可以计算 Long 的,并且类型也正确
*/
Long max2 = max(longs);
System.out.println(max2);
}
}
Kotlin 泛型
理解了 Java 泛型,Kotlin 就更加的轻车熟路了。Kotlin 产物也是 class 文件,所以泛型也是会被擦除的。
Kotlin 的通配符
Kotlin 定义通配符和 Java 定义方式不一样,功能都一样,请看下方代码。
fun main() {
/**
* 使用 out 定义上界通配符
* <out Number> 等价于 <? extends Number>
*/
val ints: MutableList<out Number> = mutableListOf()
/**
* 可以返回
*/
val number = ints.get(0)
/**
* 修改报错
*/
ints.add(0)
/**
* 使用 out 定义下界通配符
* <in Long> 等价于 <? super Long>
*/
val longs: MutableList<in Long> = mutableListOf()
/**
* 使用 返回的是 Any 类型,也就是 Object
*/
val l = longs.get(0)
/**
* 修改不报错,成功
*/
longs.add(0)
}
Kotlin 定义泛型
其实和 Java 一样的,只不过定义的关键词不一样了。
/**
* Java 是 <T extends Number>
* Kotlin 是 <T : Number>
*/
class Test<T : Number>{
}
总结
其实 Java 通配符中还存在只写 ? 不确定上下界的情况,在 Kotlin 中对应的是 *,由于用处太少了,大家自行查阅资料吧。其实理解了通配符约束和泛型,在理解就简单多了。但是想要把泛型用在项目上还需要很多设计上的积累,理解容易得心应手真的很难。
总感觉举的例子不好,大家有更好的例子吗?