val hashSet = hashSetOf(1, 2, 3, 4, 5) println(hashSet.javaClass) // class java.util.HashSet val linkedHashSet = linkedSetOf(1, 2, 3) println(linkedHashSet.javaClass) // class java.util.LinkedHashSet val arrayList = arrayListOf(1, 2, 3, 4, 5) println(arrayList.javaClass) // class java.util.ArrayList val list = listOf(1, 2, 3, 4, 5) println(list.javaClass) // class java.util.Arrays$ArrayList val hashMap = hashMapOf(1 to "a", 2 to "b", 3 to "c") println(hashMap.javaClass) // class java.util.HashMap 复制代码
通过上面这些函数就可以创建集合,通过自己 new 集合对象的方式也是可以的 , Kotlin 中在创建对象时省略了 new 关键字。虽然 Kotlin 采用的是 Java 集合类库,但是 Kotlin 提供了一些额外的扩展。
val list = listOf("小明", "丹尼", "李华") println("获取第一个元素 : ${list.first()}") // 获取第一个元素 : 小明 println("获取最后一个元素: ${list.last()}") // 获取最后一个元素: 李华 println("获取指定下标的元素: ${list[1]}") // 获取指定下标的元素: 丹尼 println("获取当中最大的一个元素: ${list.max()}") // 获取当中最大的一个元素: 李华 println("翻转这个集合 :${list.asReversed()}") // 翻转这个集合 :[李华, 丹尼, 小明] println("根据条件在集合中查找满足条件的元素 : ${list.find { it.startsWith("小") }}") // 根据条件在集合中查找满足条件的元素 : 小明 复制代码
在后面的部分会仔细探究他们的工作原来,以及这些在 Java 类中新增加的函数是从何而来。
这一节我们从一个例子开始,需求是得到一个集合的字符串展示形式,可以指定元素之间的分隔符号,前缀和后缀。先写一个最基本的函数。
fun <T> joinToString(collection: Collection<T> , separator: String , prefix: String , postfix: String): String { val result = StringBuilder(prefix) for ((index , element) in collection.withIndex()) { if (index > 0) { result.append(separator) } result.append(element) } result.append(postfix) return result.toString() } 复制代码
val list = listOf("小明", "丹尼", "李华") println(joinToString(list , "|" , "<" , ">")) // <小明|丹尼|李华> 复制代码
对 joinToString 函数的测试结果得到了我们的预期。接下来我们会用 Kotlin 支持的特性来改写这个函数,力求让它变得更简洁和实用。
命名参数是 Kotlin 的特性之一 ,可以解决可读性的问题 , 因为当你在调用这样一个 API : joinToString(Collection , "" , "" , "") 的时候。你很可能会搞不清楚每个位置的String类型的参数究竟意味着什么,只要参数的顺序传错了你就会得到一些奇怪的结果。为了避免这个问题你需要去看一下它的函数声明,来确定每个位置上的需要的是什么参数。
在 Kotlin 中可以通过命名参数来解决这个问题, 就是在调用一个函数传入参数的时候,可以显示的写上参数的名称,并且指定要传入的值赋值给那个参数。但是如果在调用一个函数时,指明了一个参数的名称时,为了避免混淆,这个参数之后的所有参数都需要标明名称了。 例如我对 prefix 参数标明了名称,那么必须在对之后的 postfix 和 separator 参数都标明名称。
val list = listOf("小明", "丹尼", "李华") println(joinToString(list , prefix = "<" , separator = "|" , postfix = ">")) // <小明|丹尼|李华> 复制代码
Java 的另一个普遍存在的问题是一些类的重载函数太多。这些重载,原本是为了向后兼容,方便这些API的使用者,又或者是出于别的原因,但导致的最终结果是一样的:重复。
在 Kotlin 中可以在声明函数的时候指定参数的默认值,这样可以避免创建重载函数。使用默认参数值对 joinToString 函数进行改写。
fun <T> joinToString(collection: Collection<T> , separator: String = ", " , prefix: String = "[" , postfix: String = "]"): String { val result = StringBuilder(prefix) for ((index , element) in collection.withIndex()) { if (index > 0) { result.append(separator) } result.append(element) } result.append(postfix) return result.toString() } 复制代码
val list = listOf("小明", "丹尼", "李华") println(joinToString(list)) // [小明, 丹尼, 李华] 复制代码
在对 joinToString 函数进行调用的时候我们只传入了一个 list 参数值。其他参数都使用了我们在声明函数时所指定的默认值。 注意!参数的默认值是被编码到被调用的函数中,而不是调用的地方。如果你改变了参数的默认值并重新编译这个函数,没有给参数重新赋值的调用者,将会开始使用新的默认值
@JvmOverloads fun <T> joinToString(collection: Collection<T> , separator: String = ", " , prefix: String = "[" , postfix: String = "]"): String { val result = StringBuilder(prefix) for ((index , element) in collection.withIndex()) { if (index > 0) { result.append(separator) } result.append(element) } result.append(postfix) return result.toString() } 复制代码
List<String> list = new ArrayList<>(); list.add("小明"); list.add("丹尼"); list.add("李华"); System.out.println(new KTDemo().joinToString(list)); // [小明, 丹尼, 李华] 复制代码
我相信绝大多数 Java 开发者都会在自己的,公司的,开源框架项目,或者是 JDK 中看到不少名称为 XXXUtils 或者 XXXs 的类。这些类存在的意义就是工作在一些不需要对象的地方。这样的类仅仅作为一堆静态函数的容器存在。看吧事实就是这样,并不是所有人都需要对象(object) (注意这里的对象指的是编程世界中的对象,而不是中文口语的那个对象,事实上在现实世界中人人都需要对象,不然人该有多孤单啊) 。
在 Kotlin 中根本酒不需要去创建这些无意义的类。相反,可以把这些函数直接放在代码文件的顶层 ,不用从属于任何类。这些放在文件顶层的函数任然是包内的成员,如果你需要从包外访问它,则需要 import 。
这里我们写了一个 join.kt 文件,直接将 joinToString 函数放在了文件内。在 Java 代码中调用这个函数 。
仔细观察可以发现 import static kt.demo.JoinKt.joinToString 这行代码,这说明了 join.kt 文件被编译成了一个类名为 JoinKt , joinToString 是其中的一个静态函数。当然这里你也可以这样写。
import kt.demo.JoinKt public class JavaClassDemo { @Test public void test1() { List<String> list = new ArrayList<>(); list.add("小明"); list.add("丹尼"); list.add("李华"); System.out.println(JoinKt.joinToString(list)); } } 复制代码
想要改变包含 Kotlin 顶层函数的编译生成的类名称,需要给这个 Kotlin 文件添加 @JvmName 的注解,将其放到这个文件的开头,为于包名的前面:
使用时就可以使用 JoinFunctions 这个名称。
和函数一样属性也可以被放到文件顶层。放在顶层的属性会被编译成一个静态字段。默认情况下顶层属性和其他任意属性是一样的,是通过访问器暴漏给使用者。为了方便使用,如果你想要把一个常量以 public static final 的属性暴漏给 Java 可以使用 const 来修饰它。
const val UNIX_LINE_SEPARATOR = "/n" public static final String UNIX_LINE_SEPARATOR = "/n"; // 这两行代码等同 复制代码
理论上来说扩展函数非常简单,就是一个类的成员函数,不过这个成员函数定义在了类的外面。如下图我们就为 String 定义了一个扩展函数用来获取字符串的最后一个字符。
fun String.lastChar(): Char = this.last() 复制代码
可以像调用类的普通成员去调用这个函数:
println("Kotlin".lastChar()) // n 复制代码
在上面这个例子中 ,String 就是接收者类型 。 "Kotlin" 字符串就是接收者对象。现在我们不需要修改 String 类的源码就为它增加了新的行为。不管 String 类是用 Java 、Kotlin,或者像 Groovy 的其他 JVM 语言编写的,只要他会编译为 Java 类,就可以为这个类添加自己的扩展。
一个扩展函数不会自动在整个项目范围内生效。如果你需要使用它需要进行导入。如果导入后发现了命名冲突可以使用 as 关键字来另外定义一个名称,这样对导入的类或者函数都是有效的。
import javax.persistence.Entity import org.hepeng.cornerstone.entity.Entity as E 复制代码
因为是静态函数,这样调用扩展函数就不会创建适配的对象或者任何运行时的额外开销。知道了这一点如何从 Java 中调用扩展对象就很简单了,无非就是调用这个静态函数罢了。
import kt.demo.StringsKt; public class JavaClassDemo { @Test public void test2() { String s = "kotlin"; System.out.println(StringsKt.lastChar(s)); // n } } 复制代码
在学习了以上这些知识后我们可以进一步改写 joinToString 函数了 :
@JvmOverloads fun <T> Collection<T>.joinToString(collection: Collection<T> , separator: String = ", " , prefix: String = "[" , postfix: String = "]"): String { val result = StringBuilder(prefix) for ((index , element) in collection.withIndex()) { if (index > 0) { result.append(separator) } result.append(element) } result.append(postfix) return result.toString() } 复制代码
在 Kotlin 中调用扩展函数 :
val list = listOf("小明", "丹尼", "李华") println(list.joinToString(separator = " @ ")) // [小明 @ 丹尼 @ 李华] 复制代码
扩展属性提供了一种方法,用来扩展类的 API ,可以用来访问属性,用的是属性语法而不是函数语法。尽管他们被称为属性,但是他们可以没有任何状态,因为没有合适的地方来存储它,不可能给现有的 Java 对象实例添加额外的字段。但有时短语法仍然是便于使用的。
声明一个扩展属性,这里必须显示的定义 getter 函数,因为没有对应的字段所以也不会存在默认的 getter 实现。同理初始化也是不可以的,因为没有地方存储值。
val String.lastChar: Char get() = this.last() 复制代码
这节内容会涉及到的语言特性:
使用函数来创建集合的时候可以传入任意个数的参数。
val list = listOf(1 , 2 , 3 , 4 , 5) 复制代码
在 Java 中的可变参数是通过 ... 声明的, 可以把任意个数的参数值打包到数组中传给函数。 Kotlin 的可变参数使用 vararg 声明。Kotlin 和 Java 之间另一给区别是,当需要传递的参数已经包装在数组中时,调用该函数的语法。在 Java 中可以按原样传递数组 ,而 Kotlin 则要求你显示的解包数组,以便每个数组元素在函数中能作为单独的参数来调用。从技术角度来讲这个功能被称为展开运算符,而使用的时候,不过是在参数前面放一个 * 。
fun main(args: Array<String>) { val list = listOf("args: " , *args) println(list) } 复制代码
在之前的内容中我写过一些这样的代码来创建一个 map 集合。在这行代码中 to 不是内置的结构,而是一种特殊的函数调用,被称为中缀调用。
在中缀调用中没有添加额外的分隔符,函数名称是直接放在目标对象名称和参数之间的。 第二行代码和第一行代码调用方式是等价的。
val hashMap = hashMapOf(1 to "a", 2 to "b", 3 to "c") 复制代码
val hashMap = hashMapOf(1.to("a"), 2.to("b"), 3.to("c")) 复制代码
infix fun String.join(s: String) = this.plus(" $s") 复制代码
println("hello" join "world") // hello world 复制代码
解构声明可以把一个对象解构成很多变量,这样会带来一些便利性。
val map = mapOf(1 to "One", 2 to "Two", 3 to "three") for ((key , value) in map) { println("key = $key , value = $value") } 复制代码
例如这里 (key , value) in map 就是一个解构声明
data class Cat(var name: String? , var color: String?) 复制代码
val cat = Cat(name = "小将" , color = "白色") val (name , color) = cat 复制代码
这里对 cat 也是一个解构声明
Kotlin 字符串和 Java 字符串完全相同。Kotlin 提供了一些有用的扩展函数,使得字符串使用起来更加方便。
Kotlin 中使用与 Java 完全相同的正则表达式语法。
val text = """ >Tell me and I forget. >Teach me and I remember. >Involve me and I learn. >(Benjamin Franklin) """ 复制代码
三重引号字符串中的内容不会被转义,它可以包含任何字符,将会保持原样。上面的字符串打印后会按照原样输出。
如果为了更好的表示这样的字符串,可以去掉缩进(左边距)。为此可以向字符串内容添加前缀,标记边距的结尾,然后调用 trimMargin 来删除每行中的前缀和前面的空格。
val text = """ >Tell me and I forget. >Teach me and I remember. >Involve me and I learn. >(Benjamin Franklin) """.trimMargin(">") 复制代码
许多开发人员认为,好代码的重要标准之一是减少重复代码,甚至还给这个原则起了个名字:不要重复你自己(DRY)。但是当你写 Java 代码的时候,有时候做到这点就不那么容易了。许多情况下可以抽取出多个方法,把长的函数分解成许多小的函数然后重用他们。但是这样可能会让代码更费解,因为你以一个包含许多小方法的类告终,而且他们之间没有明确的关系。可以更进一步将提取的函数组合成一个内部类,这样就可以保持结构,但是这种函数需要用到大量的样板代码。
Kotlin 提供了一个更整洁的方案: 可以在函数中嵌套这些提取的函数。这样既可以获得所需要得结构,也无需额外得语法开销。
data class User(var id:Int , var name: String , var address: String) fun saveUser(user: User) { if (user.name.isEmpty()) { throw IllegalArgumentException("Can't save user ${user.id}: empty Name") } if (user.address.isEmpty()) { throw IllegalArgumentException("Can't save user ${user.id}: empty Address") } // 保存到数据库 } 复制代码
data class User(var id:Int , var name: String , var address: String) fun saveUser(user: User) { fun validate(value: String , fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException("Can't save user ${user.id}: empty $fieldName") } } validate(user.name , "Name") validate(user.address , "Address") // 保存到数据库 } 复制代码
data class User(var id:Int , var name: String , var address: String) fun saveUser(user: User) { user.validateBeforeSave() // 保存到数据库 } fun User.validateBeforeSave() { fun validate(value: String , fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException("Can't save user $id: empty $fieldName") } } validate(name , "Name") validate(address , "Address") } 复制代码