现在,你已经知道如何创建带有元素的集合了。让我们直接(用它)来做些事情吧:打印它的内容。如果这看起来过于简单,请不要担心。与此同时,你将会遇到一大堆重要的概念。
Java集合有一个默认的toString
实现。但它的输出格式是固定的,而且并不总是你需要的:
>>> val list = listOf(1, 2, 3)
>>> println(list) // 1 调用toString()
[1, 2, 3]
想象一下,你需要元素用分号隔开,同时被括号隔开而不是使用默认的实现(就像这样):(1; 2; 3)
。为了解决这个问题,Java项目使用了第三方的库,例如Guava和Apache Commons,或者在项目内部重新实现这个逻辑。在Kotlin中,这个函数是标准库的一部分。
在这一章节,你将会自己实现这个函数。你将会没有使用Kotlin语法来简化函数声明,而是以一个直接的实现为开始。同时你将会以一个更具Kotlin味道的方式来重写这份代码。
下面的joinToString
函数把集合元素添加到了StringBuilder
中,在它们之间使用分隔符,在开头使用一个前缀,在末尾添加一个后缀:
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) // 1 在第一个元素之前不添加分隔符
result.append(element)
}
result.append(postfix)
return result.toString()
}
这个函数是支持泛型的:它对包含任意类型的集合有效。如你所见,泛型语法跟Java很相似。(更多关于泛型的详细讨论将会成为第9章的主题。)
让我们刻意的使用一下这个函数以验证它(的结果):
>>> val list = listOf(1, 2, 3)
>>> println(joinToString(list, "; ", "(", ")"))
(1; 2; 3)
这样的实现是很好的。你很可能留着它就这样子。我们将要集中讨论的是声明:你如何才能以更少的繁琐信息来调用这个函数呢?也许你能避免每次调用这个函数是都传递四个参数。让我们来看看你可以如何实现。
3.2.1 有名字的参数
我们即将解决的第一个问题关注的是函数调用的可读性。举个例子,看看接下来的函数调用joinToString()
:
joinToString(collection, " ", " ", ".")
你能够说出所有的这些字符串都对应什么样的参数吗?所有的元素都是用空白符或者逗号分隔开的吗?(如果)不看一眼这个函数的签名,这些问题很难回答。也许你记住它,或者你的IDE能够帮助你。但是调用代码却不明显(的告诉你函数签名)。
这个问题在有布尔标记位是特别的普遍。为了解决这个问题,一些Java编码风格推荐创建一个枚举类型而不是使用布尔类型。更有甚者强制你在注释中显式的指明参数名,正如String
参数的案例:
/* Java */
joinToString(collection, /* separator */ " ", /* prefix */ " ",
/* postfix */ ".");
但是用Kotlin,你可以做得更好:
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
当调用Kotlin编写的方法时,你看可以指定一些传递给函数的参数的名字,如果你在某个调用中指定了一个参数,你也应该为后续的所有参数指定名字来避免(造成)困惑。
TIP 提示
无需多言,IntelliJ IDEA能保持最新的够显式参数名,如果你重命名了被调函数的参数。你只需确保你使用了重命名或者改变签名动作而不是手动的编辑参数名。
WARNING 警告
不幸的是,当你调用Java编写的方法时,你不能使用命名参数,包括来自JDK的方法和Android框架的方法。当且仅当从Java8开始,在.class文件中存储参数名字成为一个可选的特性。Kotlin保持兼容Java6.结果是,编译器无法识别你的调用使用的参数名并与方法定义相匹配。
我们将在下文看到,只有当参数带有默认值的时候命名参数才是有效的。
3.2.2 默认参数值
另一个常见的Java问题是在某些类中重载方法过多。仅java.lang.Thread
就有八个构造函数!重载可以提供向后兼容,为API用户提供便捷,或者处于其他原因。但是最后的结果是相同的:重复。参数名和类型一次又一次的重复。即便你是一个好公民,你也必须在每一个重载中重复大部分的文档。与此同时,如果你调用了忽略某些参数的重载,那个参数使用了那个值变得不再一直清晰。
脚注7 http://mng.bz/1vKt
在Kotlin中,你通常可以避免创建重载,因为你可以在函数声明中指定参数的默认值。让我们使用这一特性来提升joinToString
函数。对于大多数情况,字符串可以用逗号分隔而不需任何的前缀或者后缀。所以让我们用这些值当做默认值:
fun <T> joinToString(
collection: Collection<T>,
separator: String = ", ", // 1 默认的参数值
prefix: String = "",
postfix: String = ""
): String
现在你也可以使用所有的参数来调用这个函数或者忽略它们:
>>> joinToString(list, ", ", "", "")
1, 2, 3
>>> joinToString(list)
1, 2, 3
>>> joinToString(list, "; ")
1; 2; 3
当使用常规调用语法是,你可以只忽略后面的参数。如果你使用命名参数,你可以忽略列表中的一些(其他)参数而仅仅指定你需要的那些(参数):
>>> joinToString(list, prefix = "# ")
# 1, 2, 3
NOTE 默认值和Java 考虑到Java并没有参数默认值的概念,当你在Java中调用带有参数默认值值的Kotlin函数时,你必须显式的指定所有参数的值。如果你经常需要从Java中调用一个函数并想让Java调用者易于使用,你可以用
@JvmOverloads
对它进行标注。它将指示编译器以从最后一个参数开始逐个忽略每一个参数的方式产生重载的Java函数。 举个例子,如果你用@JvmOverloads
标注了joinToString()
,以下的重载将会产生:
/* Java */ String joinToString(Collection<T> collection, String separator, String prefix, String postfix); String joinToString(Collection<T> collection, String separator, String prefix); String joinToString(Collection<T> collection, String separator); String joinToString(Collection<T> collection);
每一个重载(函数)都为忽略了签名的参数使用了默认值。
到目前为止,你已经编写了你的工具函数而不需要过多的关注上下文。毫无疑问,它必须是某个类的方法,同时你已经忽略了周围的类声明,对吧?事实上,Kotlin使得它不再是必要的。
3.2.3 摆脱静态工具类:顶层(top-level)函数和属性
我们都知道Java作为一个面向对象语言,要求代码写成类的方法。一般来说,这种方式可以很好的工作。但事实上,几乎每一个大型的项目都会以带有大量不属于任何单一个的类的职责不清的的代码结束。有时候,某个操作对两个同样重要的类的对象都有效。有时候,某个对象是主要的,但是你不想通过把操作添加为实例方法导致它的API变得臃肿。
结果,你以类不包含任何状态或作为静态方法的容器的实例方法结束代码。一个完美的例子就是JDK中的Collections
类。为了在你的代码中找出其他例子,查找以Util
作为命名的一部分的类。
在Kotlin中,你不需要创建所有这些无意义的类。相反,你可以把这些函数直接放在代码文件的最顶层而不需要在任何的类的内部。这样的函数依然是依然是声明在文件顶部的包的成员。同时如果你想要从其他包里调用它们,你依然需要导入它们。但是不必要的额外的嵌套层级不复存在了。
让我们把joinToString
函数直接放进strings
包内。用下面的内容创建一个叫join.kt
的文件:
package strings
fun joinToString(...): String { ... }
这份代码又如何运行呢?你知道,当你编译文件时,将会产生一些类,因为JVM只能在类中执行的代码。当你只用Kotlin时,这就是你所需要知道的全部(知识)。但是,如果你需要从Java中调用这样的一个函数,你必须理解它是如何被编译的。为了理解的更清晰一点,让我们来看看编译出同样的类的Java代码:
/* Java */
package strings;
public class JoinKt { // 对应前一个例子的文件名,join.kt
public static String joinToString(...) { ... }
}
你可以看到Kotlin编译器产生的类的名字跟包含函数的文件的名字一样。文件中所有的顶层函数都会被编译成这个类的静态方法。因此,从Java中调用这个方法跟调用其他静态方法一样简单:
/* Java */
import strings.JoinKt;
...
JoinKt.joinToString(list, ", ", "", "");
SIDEBAR 改变文件的类名 为了改变生成的包含Kotlin顶层函数的类的名字,你可以给文件添加
@JvmName
标注。把它放置在文件的开头位于包名之前的地方:
@file:JvmName("StringFunctions") // 1 指定类名的标注 package strings // 2 包声明跟在文件标注后面 fun joinToString(...): String { ... }
现在函数可以这样调用:
/* Java */ import strings.StringFunctions; StringFunctions.joinToString(list, ", ", "", "");
跟标注语法有关的详细讨论将会在后续的第十章进行。
顶层属性(TOP-LEVEL PROPERTIES)
就像函数那样,属性可以放在文件的顶层。保存类外部的单独的数据块并不常用,但依然非常有用。
举个例子,你可以使用var
属性来计算某个操作被执行的次数:
var opCount = 0 // 1 包级别的属性声明
fun performOperation() {
opCount++ // 2 改变属性的值
// ...
}
fun reportOperationCount() {
println("Operation performed $opCount times") // 3 读取属性的值
}
这样一个属性的值将会被存储在一个静态字段中。 顶层属性(Top-level properties)也允许你在你的代码中定义常量:
val UNIX_LINE_SEPARATOR = "\n"
默认的,顶层属性就像其他属性一样,以访问器(一个val
属性的读取函数或者一对读取函数/设置函数)的形式暴露给Java代码。如果你想把常量暴露给Java代码组委一个public static final
字段来让它的用法更加自然,你可以用const
修饰器标记它(原始类型属性是允许这样做的,String
类型也是):
const val UNIX_LINE_SEPARATOR = "\n"
这将得到跟以下等价的Java代码:
/* Java */
public static final String UNIX_LINE_SEPARATOR = "\n";
你已经把初始的工具函数joinToString
提升了很多。现在让我们看看如何让它变得更加易于使用。