ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 泛型 [TOC] **泛型是一种编译时的安全检测机制,它允许在定义类、接口、方法时使用类型参数,声明的类型参数在使用时用具体的类型来替换**。 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。在本章我们将对泛型进行详细讲解。 **参考文章**: [泛型](https://www.kancloud.cn/alex_wsc/android_kotlin/1318308) ## 泛型定义 现实生活中,我们在整理物品时,会把各种各样的物品放在一个收纳盒中,如剪刀、火机、梳子、头绳等,这个收纳盒相当于是一个容器,该容器中可以放各种物品。如果有一个空的收纳盒,想要知道收纳盒中放什么物品,只有当收纳盒中放了物品之后才会知道放的是什么物品。收纳盒这个容器的概念与JDK系统中的List集合比较类似,List集合也是一个容器,这个容器中也可以放置各种数据类型,如String、Int、Object等。如果想要知道List集合中存放的是什么类型的对象,则必须在该集合存放完对象之后才能知道。 当创建一个空的List集合时,调用List集合中的add()方法,该方法中传递的参数是一个“某种类型”的对象,由于这个“某种类型”是一个不确定的类型,因此可以通过泛型来表示。泛型即“参数化类型”,就是将具体的类型变成参数化类型,在声明一个泛型时,传递的是一个类型形参,在调用时传递的是一个类型实参。 * 当定义泛型时,泛型是在类型名之后、主构造函数之前用尖括号“<>”括起来的大写字母类型参数。 * 当定义泛型类型变量时,可以完整地写明类型参数,如果编译器可以自动推断类型参数,则可以省略类型参数。 ``` class ArrayList<E> ``` 上述代码中**E表示某种类型,定义时是一个未知类型**,称为**类型形参**。E这个类型类似一个参数,当创建ArrayList实例时,需要传入具体的类型。具体示例代码如下: ``` var list1 = arrayListOf<String>("aa","bb","cc") var list2 = arrayListOf<Long>(111,222,333) var list3 = arrayListOf<Int>(1,2) ``` 上述代码中传入的参数String、Long、Int都是类型实参。 接下来我们通过一个例子来了解可以省略的泛型类型参数,具体示例代码如下: ``` class Box<T>(t: T) { var value = t } ``` 上述代码中T表示泛型的类型参数,如果要创建这个类的实例,则需要提供具体的类型参数。具体示例代码如下: ``` val box: Box<Int> = Box<Int>(1) ``` **如果类型参数可以推断出来,则可以省略类型参数**。具体示例代码如下: ``` val box = Box(1) //参数1 是Int 类型 ``` 由于传递到构造函数中的参数是1,这个参数是一个Int类型的参数,编译器可以推断出泛型的类型是Int类型,因此在创建类的实例时可以省略类型参数。 ## 泛型:类型安全的利刃 众所周知,Java 1.5引入泛型。那么我们来思考一个问题,为什么Java一开始没有引入泛型,而1.5版本却引入泛型?先来看一个场景: ``` List stringList = new ArrayList(); stringList.add(new Double(2.0)); String str = (String)stringList.get(0); 执行结果: >>> java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.String at javat.Rectangle.main(Rectangle.java:29) ``` 因为ArrayList底层是用一个Object类型的数组实现的,这种实现虽然能让ArrayList变得更通用,但也会带来弊端。比如上面例子中,我们不小心向原本应作为String类型的List中添加了一个Double类型的对象,理想的情况下编译器应该能够提示错误,但事实上这段代码能编译通过,在运行时却会报错。这是一个非常糟糕的体验,我们真正需要的是在代码编译的时候就能发现错误,而不是让错误的代码发布到生产环境中。这便是泛型诞生的一个重要的原因。有了泛型后,我们可以这么做: ``` List<String> stringList = new ArrayList<String>(); stringList.add(new Double(2.0)); //编译时报错,add(java.lang.String)无法适配add (java.lang.Double) ``` 利用泛型代码在编译时期就能发现错误,防止在真正运行的时候出现ClassCastException。当然,泛型除了能帮助我们在编译时期进行类型检查外,还有很多其他好处,比如自动类型转换。 继续来看第一段代码,在获取List中的值的时候,我们进行了以下操作: ``` String str = (String)stringList.get(0); ``` 是不是感觉异常的烦琐,明明知道里面存的是String类型的值,取值的时候还要进行类型强制转换。但有了泛型之后,就可以利用下面这种方式实现: ``` List<String> stringList = new ArrayList<String>(); stringList.add("test"); String str = stringList.get(0); ``` 有了泛型之后,不仅在编译的时候能进行类型检查,在运行时还会自动进行类型转换。而且通过引入泛型,增强上述功能的同时并没有增加代码的冗余性。比如我们无须为声明一个类型安全的List而去创建StringList、DoubleList等类,只需在声明List的同时指定参数类型即可。 总的来说,泛型有以下几点优势: * 类型检查,能在编译时就帮你检查出错误; * 更加语义化,比如我们声明一个`List<String>`,便可以知道里面存储的是String对象,而不是其他对象; * 自动类型转换,获取数据时不需要进行类型强制转换; * 能写出更加通用化的代码。 ## 如何在Kotlin中使用泛型 假设现在我们有一个需求,定义一个find方法,传入一个对象,若列表中存在该对象,则返回该对象,不存在则返回空。由于原有的集合类不存在这个方法,所以可以定义一个新的集合类,同样也要声明泛型。我们可以这么做: ``` class SmartList<T> : ArrayList<T> () { fun find(t: T): T? { val index = super.indexOf(t) return if (index >= 0) super.get(index) else null } } fun main(args: Array<String>) { val smartList = SmartList<String>() smartList.add("one") println(smartList.find("one"))//输出one println(smartList.find("two").isNullOrEmpty())//输出true } ``` 发现,Kotlin定义泛型类的方式与我们在Java中所看到的类似。另外泛型类同样还可以继承另一个类,这样我们就可以使用ArrayList中的属性和方法了。 当然,除了定义一个新的泛型集合类外,我们还可以利用扩展函数来实现这种需求。由于扩展函数支持泛型的情况,所以我们可以这么做: ``` fun <T> ArrayList<T>.find(t: T): T? { val index = this.indexOf(t) return if (index >= 0) this.get(index) else null } fun main(args: Array<String>) { val arrayList = ArrayList<String>() arrayList.add("one") println(arrayList.find("one"))//输出one println(arrayList.find("two").isNullOrEmpty())//输出true } ``` 利用扩展函数这种方式也非常简洁,所以,当你只是需要对一个集合扩展功能的时候,使用扩展函数非常合适。 ### 使用泛型时是否需要主动指定类型? 在Kotlin中,以下的方式不被允许: ``` val arrayList = ArrayList() ``` 而在Java中却可以这么做,这主要是因为泛型是Java 1.5版本才引入的,而集合类在Java早期版本中就已经有了。各种系统中已经存在大量的类似代码: ``` List list = new ArrayList(); ``` 所以,为了保证兼容老版本的代码,Java允许声明没有具体类型参数的泛型类。而Kotlin是基于Java 6版本的,一开始就有泛型,不存在需要兼容老版本代码的问题。所以,**当你声明一个空列表时,Kotlin需要你显式地声明具体的类型参数。当然,因为Kotlin具有类型推导的能力,所以以下这种方式也是可行的**: ``` val arrayList = arrayListOf("one", "two") ``` 总的来说,使用泛型可以让我们的代码变得更加通用化,更加灵活。但有时过于通用灵活并不是一个好的选择,比如现在我们创建一个类型,只允许添加指定类型的对象。接下来我们就来看看如何在Kotlin中约束类型参数。