内容简介
泛型,在 Java 中个人认为是一个比较难的东西。它理解起来很简单,但是要想把它用好真的很难。
每当我看到别人用泛型来完成巧夺天工的设计,我都很是羡慕。
Java 泛型
讲解 Kotlin
泛型之前,先要将 Java
的泛型理解清楚,因为 Kotlin
的本质还是 Java
( Java
是 Kotlin
的爸爸)。
Java
的泛型有什么用呢?我个人认为:主要是减少代码上类型的转换,以及类型约束,在编译前期将错误暴露出来。
例如:我们常用的集合,试想想如果没有泛型。我们的
List
集合会怎么定义呢?可能类似就要这样定义了StringList
丶IntList
或者定义一个ObjectList
(万物之祖宗OBJ
),试想下若这样的设计给我们开发带来了多少的类型转换与不便呢?
有了泛型,我们在定义类或方法时只需声明泛型,这样编译器就知道类型,让编译器做类型检测和类型转换。
前面说了 Java
泛型,我们了解到泛型是给编译器看的,让编译器在编译的时候帮你做强制转换。
其实在最后定义的泛型是会被清理掉的,这个过程俗称 泛型擦除
。
为何要擦除掉呢?其实在 Java 初期认为 C 中的泛型没啥用,就没有设计。但后续发现没泛型有点麻烦,后续版本就设计了泛型。但是为了兼容以前的版本,用了这种折中的方案 (个人猜想)。
public class TestJava {
public strictfp static void main(String[] args) {
/**
* 告诉编译器 strs 存的是一个 String 类型
*
* 编译器只会做2件事:
*
* 1. 只有检测到 add 不是一个 String 类型,就编译不通过
*
* 2. 只要有 get 这个参数操作,帮我强制转换成 String
*/
List 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 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
呢?
其实反编译的 Java
还保留了泛型,是因为 JDK1.5
版本之后 class
文件的属性表中添加了 Signature
和 LocalVariableTypeTable
等属性来支持泛型识别的问题,这些信息可以在配置混淆的时候可以删除掉。
行,那我们看下 ByteCode
,让那些杠精死心。
通配符
Java
通配符其实是个 约定
,这个约定是给编译器看的。 Java
是强类型语言,变量类型的泛型参数也是区分类型的(注意我现在说的是通配符和泛型是2个东西哦)。
请问下方代码报错吗?(友情提示: Integer
是 Number
的子类)
public class TestJava {
public strictfp static void main(String[] args) {
ArrayList integers = null;
ArrayList numbers = null;
// 请问这句代码报错吗?
numbers = integers;
}
}
没错编译器不通过,因为编译器认为这是 2
种类型,直接报错。
上方的代码编译不通过,那有没有办法让 Java
的泛型类型也存在多态的关系呢?
是可以的哦,通配符 ?
的出现解决了这个问题。我们改下代码。
public class TestJava {
public strictfp static void main(String[] args) {
ArrayList integers = null;
ArrayList extends Number> numbers = null;
// 请问这句代码报错吗?
numbers = integers;
}
}
定义改成 ArrayList
,意思我能接收一个属于 Number
子类的的泛型。
通过这样定义会让编译器不报错,但这样就会多一个约束。
若通过上界通配符修饰泛型,只能调用返回泛型类型的方法(编译器约束:只能用不能改),啥意思呢?
举个例子:
public class TestJava {
public strictfp static void main(String[] args) {
ArrayList 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
方法。
这下明白了吧,在细细品味味我上面的那句话 只能调用返回泛型类型的方法。
所以说类似 publicE remove(intindex)
也能调用(只能用不能改)。
思考问题,为何只能用不能改呢?其实这是
Java
处于安全考虑。例如上面的例子,因为我通过上界通配符打开了一定的范围,代码角度考虑能保证返回的类型一定是
Number
的子类,根据多态我一定可以用Number
类型的变量去接收。但是存储我就不能确定存的是什么类型了。
例如:
返回 List
集合中的最大值。想想我不能为所有的 Int
丶 Double
丶 Long
都写重载一个方法吧?
应用上界通配符,我们只需要定义一个方法。
public class TestJava {
public strictfp static void main(String[] args) {
ArrayList integer = new ArrayList();
integer.add(1);
integer.add(2);
/**
* 可以 Integer 的
*/
System.out.println(max(integer));
ArrayList 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 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());
}
}
上面代码明白了吧?我们一定要把 通配符
和 泛型声明 区分开,它们是 2
个不同的东西。 T
泛型声明可以告诉编译器类型检测和帮我强制转换。通配符能限定条件,只能用还是只能改。
类上可以声明泛型,方法上也是可以的哦。还记得讲解 上界通配符
时候,获取一个数字集合中的最大值吗?我当时返回的都是 Double
类型,当时只是想说明只能用不了改的 上界通配符
,实际开发我们并不这样写。
public class TestJava {
public static T max(List 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 = new ArrayList();
integer.add(1);
integer.add(2);
/**
* 可以 Integer 的,并且自动转化成 Integer 了
*/
Integer max = max(integer);
System.out.println(max);
ArrayList longs = new ArrayList();
longs.add(3L);
longs.add(4L);
/**
* 可以计算 Long 的,并且类型也正确
*/
Long max2 = max(longs);
System.out.println(max2);
}
}
Kotlin 泛型
理解了 Java
泛型, Kotlin
就更加的轻车熟路了。 Kotlin
产物也是 class
文件,所以泛型也是会被擦除的。
Kotlin
定义通配符和 Java
定义方式不一样,功能都一样,请看下方代码。
fun main() {
/**
* 使用 out 定义上界通配符
* 等价于 extends Number>
*/
val ints: MutableList = mutableListOf()
/**
* 可以返回
*/
val number = ints.get(0)
/**
* 修改报错
*/
ints.add(0)
/**
* 使用 out 定义下界通配符
* 等价于 super Long>
*/
val longs: MutableList = mutableListOf()
/**
* 使用 返回的是 Any 类型,也就是 Object
*/
val l = longs.get(0)
/**
* 修改不报错,成功
*/
longs.add(0)
}
其实和 Java
一样的,只不过定义的关键词不一样了。
/**
* Java 是
* Kotlin 是
*/
class Test{
}
总结
其实 Java
通配符中还存在只写 ?
情况,在 Kotlin
中对应的是 *
,由于用处太少了,大家自行查阅资料吧。其实理解了通配符约束和泛型,在使用起来就简单多了。但是想要把泛型用在项目上还需要很多设计上的积累,理解容易得心应手真的很难。
推荐阅读
--END--
识别二维码,关注我们