由Java泛型看Kotlin的泛型

参考链接:

《深入理解Java泛型》
《Kotlin 泛型》
《Kotlin中文文档》
《无界通配符》
《Kotin实战》泛型章节
《Effective Java》泛型章节

什么是泛型

泛型的目的是让我们写一套代码可以支持不同的数据类型,下面我们实现一个简单的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyList{

private int size;
private Object[] elements = new Object[10];

public void add(Object item) {
elements[size++]=item;
}

public Object get(int index) {
return elements[index];
}
}

我们实现的集合可以添加任意类型的数据(因为Java中Object是所有类的超类)

1
2
3
4
5
MyList myList = new MyList();
myList.add("A");
myList.add(1);
System.out.println(myList.get(0));
System.out.println((String)myList.get(1));

但是这样的代码有着很大的缺陷,因为需要类型的强制转换,这种转换错误是在编译期间发现不了的,在Java 1.5之前是没有泛型的,就是这样做的。泛型同样可以实现这个需求,但是通过面向对象的多态特性避免了类型强转,而且在编译期间就能发现类型问题。

1
2
3
4
5
6
7
8
9
10
11
class DataHolder<T>{
T item;

public void setData(T t) {
this.item=t;
}

public T getData() {
return this.item;
}
}

泛型类定义时只需要在类名后面加上类型参数即可,当然你也可以添加多个参数,类似于<K,V>,<T,E,K>等。这样我们就可以在类里面使用定义的类型参数。

Java的泛型

泛型方法

泛型不仅仅可以像上面一样作用于整个类,还可以作用于方法,泛型方法既可以存在于泛型类中,也可以存在于普通的类中。

如果使用泛型方法可以解决问题,那么应该尽量使用泛型方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TestClass {

/**
* 泛型方法
* @param e
*/
public <E> void printerInfo(E e) {
System.out.println(e);
}

/**
* 泛型方法
* @param id
* @param <E>
* @return
*/
public <E> E findViewById(E id){
return id;
}

public static void main(String[] args){
TestClass tc = new TestClass();
tc.printerInfo(1);
tc.printerInfo("AAAAA");
tc.printerInfo("0.13145");
}
}

我们看到我们是在一个泛型类里面定义了一个泛型方法printInfo。通过传入不同的数据类型,我们都可以打印出来。

与泛型类的定义一样,此处E可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。

泛型接口

Java泛型接口的定义和Java泛型类基本相同,下面是一个例子:

1
2
3
4
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
  • 泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。
1
2
3
4
5
6
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
  • 如果泛型接口传入类型参数时,实现该泛型接口的实现类,则所有使用泛型的地方都要替换成传入的实参类型。
1
2
3
4
5
6
class DataHolder implements Generator<String>{
@Override
public String next() {
return null;
}
}

Java泛型擦除

泛型类型不能显式地运用在运行时类型的操作当中,例如:转型、instanceof 和 new。因为在运行时,所有参数的类型信息都丢失了。类似下面的代码都是无法通过编译的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Erased<T> {
private final int SIZE = 100;
public static void f(Object arg) {
//编译不通过
if (arg instanceof T) {
}
//编译不通过
T var = new T();
//编译不通过
T[] array = new T[SIZE];
//编译不通过
T[] array = (T) new Object[SIZE];
}
}

编译器虽然会在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。

泛型参数将会被擦除到它的第一个边界(边界可以有多个,重用 extends 关键字,通过它能给与参数类型添加一个边界)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object。下面的例子中,可以把泛型参数T当作HasF类型来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface HasF {
void f();
}

public class Manipulator<T extends HasF> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}

extend关键字后后面的类型信息决定了泛型参数能保留的信息。Java类型擦除只会擦除到HasF类型。

由于Java泛型的类型擦除特点,我们难以判断参数的实际类型,可以通过一些手段来解决泛型的类型判断、类型转型、instanceof 和 new等问题。

类型判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 方法一:记录类型参数的Class对象,然后通过这个Class对象进行类型判断
* @param <T>
*/
class GenericType<T>{
Class<?> classType;

public GenericType(Class<?> type) {
classType=type;
}

//判断方法
public boolean isInstance(Object object) {
return classType.isInstance(object);
}
}

实例创建(new T())

泛型代码中不能new T()的原因有两个,一是因为擦除,不能确定类型;而是无法确定T是否包含无参构造函数。
为了避免这两个问题,我们使用显式的工厂模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 使用工厂方法来创建实例
*
* @param <T>
*/
interface Factory<T>{
T create();
}

class Creater<T>{
T instance;
public <F extends Factory<T>> T newInstance(F f) {
instance=f.create();
return instance;
}
}

class IntegerFactory implements Factory<Integer>{
@Override
public Integer create() {
Integer integer=new Integer(9);
return integer;
}
}

我们通过工厂模式+泛型方法来创建实例对象,上面代码中我们创建了一个IntegerFactory工厂,用来创建Integer实例,以后代码有变动的话,我们可以添加新的工厂类型即可。

调用代码如下:

1
2
Creater<Integer> creater=new Creater<>();
System.out.println(creater.newInstance(new IntegerFactory()));

创建泛型数组

一般不建议创建泛型数组。尽量使用ArrayList来代替泛型数组。但是在这里还是给出一种创建泛型数组的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class GenericArrayWithTypeToken<T> {
private T[] array;

@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz);
}

public void put(int index, T item) {
array[index] = item;
}

public T[] rep() {
return array;
}

public static void main(String[] args) {

}
}

这里我们使用的还是传参数类型,利用类型的newInstance方法创建实例的方式。

Java泛型通配符

我们前面提到过List和List之间没有继承关系,也就是说List不是List的子类型,这个和我们的直觉相违背。

例如我们定义了一个堆栈类(类似于集合,来存储数据)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Stack<E>{

private static final int DEFAULT_INITIAL_CAPACITY = 16;

private E[] elements;
private int size = 0;

public Stack(){
//假设这里创建出了E[]实例elements
}

public void push(E e){
//...
}

public E pop(){
//...
}

public boolean isEmpty(){
//...
}

}

此时我们给Stack类增加一个添加新集合进来的方法(pushAll)

1
2
3
4
5
public void pushAll(Iterable<E> src){
for(E e : src){
push(e);
}
}

到这个时候编译时通过的,但是使用起来却有很大的问题。

1
2
3
Stack<Number> stacks = new Stack<Number>();
Interable<Integer> itg = ....;
stacks.pushAll(itg);

此时你就会发现报错了,那是因为上面我们提到过void push(Number e)和void push(Integer e)是两个不同的泛型。

解决上面这个问题就需要使用到通配符。

上界通配符<? extends T>

1
2
3
4
5
public void pushAll(Iterable<? extends E> src){
for(E e : src){
push(e);
}
}

这样修改后上面代码就可以正确编译和执行了。

下界通配符<? super T>

接下来我们来编写一个popAll方法取出Stack集合中的多个元素,和上面的pushAll相对应。

1
2
3
4
5
public void popAll(Collection[E] dst){
while(!isEmpty()){
dst.add(pop());
}
}

同样的,上面的方法能正确得到编译,但是使用起来同样有问题。

1
2
3
Stack<Number> stacks = new Stack<Number>();
Collection<Object> oob = ....;
stacks.popAll(oob);

会发现,此时编译会报错,因为Collection不是Collection的子类型。此时popAll的输入参数类型不应该是“E的集合”而应该是“E的超类的集合”,修改如下:

1
2
3
4
5
public void popAll(Collection[? super E] dst){
while(!isEmpty()){
dst.add(pop());
}
}

总结上界和下界通配符何时用:

PECS:表示producer-exnteds, consumer-super

意思就是说如果参数类型表示一个T的生产者就使用<? exntends T>,如果表示一个T的消费者就使用<? super T>

无限通配符<?>

无界通配符 意味着可以使用任何对象,因此使用它类似于使用原生类型。但它是有作用的,原生类型可以持有任何类型,而无界通配符修饰的容器持有的是某种具体的类型。

1
2
List list1;
List<?> list2;

编译器很少关心使用的是原生类型还是<?>,而在这种情况下,<?>可以被认为是一种装饰,但是它仍旧是很有价值的,因为,实际上,它是在声明:“想用Java的泛型来编写代码,但是我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。”

下面的示例展示了无界通配符的一个重要的应用。当你处理多个泛型参数的时,有时允许一个参数可以是任何的类型,同时为其他参数确定某种特定类型的能力会显得尤为重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static Map map1;
static Map<?, ?> map2;
static Map<String, ?> map3;

static void assign1(Map map) {
map1 = map;
}

static void assign2(Map<?, ?> map) {
map2 = map;
}

static void assign3(Map<String, ?> map) {
map3 = map;
}

assign1(new HashMap());
assign2(new HashMap());
// warning
// Unchecked assignment: 'java.util.HashMap' to 'java.util.Map<java.lang.String,?>'
assign3(new HashMap());
assign1(new HashMap<String, Integer>());
assign2(new HashMap<String, Integer>());
assign3(new HashMap<String, Integer>());

Kotlin的泛型

Kotlin的泛型语法和Java类似,在Kotlin中不存在通配符,但提供了两种方法:声明处类型变异(declaration-sitevariance), 以及类型投射(type projection)。

类型变异(协变)

我们上面提到下面的代码要正确执行,必须将pushAll的形参定义为Iterable<? extends E>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

class Stack<E>{

//...省略,请参考上面

}

public void pushAll(Iterable<E> src){
for(E e : src){
push(e);
}
}

Stack<Number> stacks = new Stack<Number>();
Interable<Integer> itg = ....;
stacks.pushAll(itg);

在Kotlin中我们有办法将这种情况告诉编译器. 这种技术称为声明处的类型变异(declaration-sitevariance)

我们可以对Stack的类型参数E添加注解, 来确保 Stack的成员函数只会返回E类型, 而绝不会消费E类型. 为了实现这个目的, 我们可以对E添加out修饰符:

1
2
3
4
5
6
7
8
9
10
class Stack<out E>{

//...省略,请参考上面
}

fun pushAll(src: Iterable<E>){
for(e in src){
push(e)
}
}

一般规则是: 当S类的类型参数E声明为out时, 那么在S的成员函数中,E类型只允许出现在输出位置, 这样的限制带来的回报就是, S

可以安全地用作I的父类型。

类型投射(逆变)

和类型变异类似的是反向类型变异(contravariant): 这个类型将只能被消费, 而不能被生产。

1
2
3
4
5
6
7
8
9
10
class Stack<in E>{

//...省略,请参考上面
}

fun popAll(dst: Collection[E]){
while(!isEmpty){
dst.add(pop())
}
}

声明点变型

但是我们上面的Stack类中我们既想要pushAll方法又要popAll方法,那么他既协变又得逆变,这个时候我们就得使用声明点变型。

声明点变型:在类声明的时候,指定变型修饰符,这些修饰符会应用到所有类被使用的地方;

1
2
3
public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R
}

Kotlin中支持使用点变型,允许在类型参数出现的具体位置指定变型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// (不变型) 从一个集合copy到另一个集合
fun <T> copyData(source: MutableList<T>, destination: MutableList<T>) {
for(item in source) {
destination += item
}
}

// 特定类型
fun <T : R, R> copyData2(source: MutableList<T>, destination: MutableList<R>) {
for (item in source) {
destination += item
}
}

// 使用点变型:给类型参数加上 变型修饰符 (out 投影)
fun <T> copyData3(source: MutableList<out T>, destination: MutableList<T>) {
for (item in source) {
destination += item
}
}

*星号投影

星号投影,用来表示不知道关于泛型实参的任何信息,跟Java的?问号通配符类似。

注意:MutableList<*> 和 MutableList<Any?> 不一样

  • MutableList<Any?>包含的是任何类型的元素。
  • MutableList<*>是包含某种特定类型元素,但不知是哪个类型,所以不能写入但可读取。

评论

Ajax Android AndroidStudio Animation Anroid Studio AppBarLayout Babel Banner Buffer Bulma ByteBuffer C++ C11 C89 C99 CDN CMYK COM1 COM2 CSS Camera Raw, 直方图 Chrome Class ContentProvider CoordinatorLayout C语言 DML DOM Dagger Dagger2 Darktable Demo Document DownloadManage ES2015 ESLint Element Error Exception Extensions File FileProvider Flow Fresco GCC Git GitHub GitLab Gradle Groovy HTML5 Handler HandlerThread Hexo Hybrid I/O IDEA IO ImageMagick IntelliJ Intellij Interpolator JCenter JNI JS Java JavaScript JsBridge Kotlin Lab Lambda Lifecycle Lint Linux Looper MQTT MVC MVP Maven MessageQueue Modbus Momentum MySQL NDK NIO NexT Next Nodejs ObjectAnimator Oracle VM Permission PhotoShop Physics Python RGB RS-232 RTU Remote-SSH Retrofit Runnable RxAndroid RxJava SE0 SSH Spring SpringBoot Statubar Style Task Theme Thread Tkinter UI UIKit UML VM virtualBox VS Code VUE ValueAnimator ViewPropertyAnimator Vue Vue.js Web Web前端 Workbench api apk bookmark by关键字 cli compileOnly computed css c语言 databases demo hexo hotfix html iOS icarus implementation init jQuery javascript launchModel logo merge methods mvp offset photos pug query rxjava2 scss servlet shell svg tkinter tomcat transition unicode utf-8 vector virtual box vscode watch webpack 七牛 下载 中介者模式 串口 临潼石榴 主题 书签 事件 享元模式 仓库 代理模式 位运算 依赖注入 修改,tables 光和色 内存 内核 内部分享 函数 函数式编程 分支 分析 创建 删除 动画 单例模式 压缩图片 发布 可空性 合并 同向性 后期 启动模式 命令 命令模式 响应式 响应式编程 图层 图床 图片压缩 图片处理 图片轮播 地球 域名 基础 增加 备忘录模式 外观模式 多线程 大爆炸 天气APP 太白山 头文件 奇点 字符串 字符集 存储引擎 宇宙 宏定义 实践 属性 属性动画 岐山擀面皮 岐山肉臊子 岐山香醋 工具 工厂模式 年终总结 开发技巧 异常 弱引用 恒星 打包 技巧 指令 指针 插件 插值 摄影 操作系统 攻略 故事 数据库 数据类型 数组 文件 新功能 旅行 旋转木马 时序图 时空 时间简史 曲线 杂谈 权限 枚举 架构 查询 标准库 标签选择器 样式 核心 框架 案例 桥接模式 检测工具 模块化 模板 模板引擎 模板方法模式 油泼辣子 泛型 洛川苹果 浅色状态栏 渲染 源码 源码分析 瀑布流 热修复 版本 版本控制 状态栏 状态模式 生活 留言板 相册 相对论 眉县猕猴桃 知识点 码云 磁盘 科学 笔记 策略模式 类图 系统,发行版, GNU 索引 组件 组合模式 绑定 结构 结构体 编码 网易云信 网格布局 网站广播 网站通知 网络 美化 联合 脚手架 膨胀的宇宙 自定义 自定义View 自定义插件 蒙版 虚拟 虚拟机 补码 补齐 表单 表达式 装饰模式 西安 观察者模式 规范 视图 视频 解耦器模式 设计 设计原则 设计模式 访问者模式 语法 责任链模式 贪吃蛇 转换 软件工程 软引用 运算符 迭代子模式 适配器模式 选择器 通信 通道 配置 链表 锐化 错误 键盘 闭包 降噪 陕西地方特产 面向对象 项目优化 项目构建 黑洞
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×