3.1、在Kotlin中创建集合
kotlin没有采用自己的集合类,采用的是标准的java集合类
示例
//创建集合
fun setCollection(){
val set= hashSetOf(1,7,53)
//创建list
val list= arrayListOf(1,7,53)
//创建map
//to不是一个特殊的结构,而是一个普通的函数
val map= hashMapOf(1 to "one",7 to "seven",53 to "fifty_three")
for(number in set){
println(number)
}
for( (index,number) in map){
println("$index,$number")
}
//查找对象所属的类型
println(set.javaClass)
println(list.javaClass)
println(map.javaClass)
//class java.util.HashSet
//class java.util.ArrayList
//class java.util.HashMap
//获取元素的最后一个元素
println("元素最后一个元素:${list.last()}")
println("元素最大值:${list.max()}")
}
3.2、让函数更好调用
命名参数
-
例子
//自定义打印toString 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() }
-
在调用一个Kotlin定义的函数时,可以显式地表明一些参数的名称,如果在调用一个函数时,指明了一个参数的名称,
为了避免混淆,它之后所遇参数都需要表明名称
println(joinToString(list,prefix = "&",postfix = "&",separator ="." ))
默认参数值
-
在
Kotlin
中,可以在声明函数的时候,指定参数的默认值 -
参数的默认值是
被编码到被调用的函数
中,而不是被调用的地方 -
例子
//自定义打印toString,默认参数值 fun <T> joinToString2(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中,不需要静态工具类,可以把函数直接放到代码文件的顶层,不用从属于任何的类
-
这些放在文件顶层的函数依然是包内的成员
-
属性也可以放到文件的顶层
-
顶层函数
-
声明joinToString()作为顶层函数
package 三.函数的定义与调用 //顶层函数,声明在类的外面 fun joinToString(name:String):String{ return "helloworld" } class Join { }
-
这会怎么运行呢?当编译这个文件的时候,会生成一些类,因为JVM只能执行类中的代码,当你在使用Kotlin的时候,知道这些就够了,如果需要从Java中来调用这些函数,就必须理解它将会怎样被编译,为了方便理解,我们来看一段代码,这里它会
编译成相同的类
package 三.函数的定义与调用; public class JoinKt { public static String joinToString(String name){ return "helloworld"; } }
-
可以看到Kotlin编译生成的类的名称,对应于包含函数的文件的名称。
这个文件中的所有顶层函数编译为这个类的静态函数
。因此,当从Java调用这个函数的时候,和调用任何其他静态函数一样非常简单public class JavaTest { public void test(){ JoinKt.joinToString("heloworld"); } }
-
所有顶层函数编译为这个类的静态函数
-
-
修改文件类名
-
要改变包含Kotlin顶层函数的生成的类的名称,需要为这个文件添加
@JvmName的注解
,将其放到这个文件的开头,位于包名的前面@file:JvmName("StringFunctions") package 三.函数的定义与调用 //顶层函数,声明在类的外面 fun joinToString(name:String):String{ return "helloworld" } class Join { }
-
-
在java中调用
public class JavaTest { public void test(){ StringFunctions.joinToString("heloworld"); } }
-
顶层属性
-
和函数一样,
属性也可以放到文件的顶层
在一个类的外面保存单独的数据片段虽然不常用,但还是有它的价值
-
代码
@file:JvmName("StringFunctions") package 三.函数的定义与调用 //顶层函数,声明在类的外面 fun joinToString(name:String):String{ return "helloworld" } //顶层属性,声明在类的外面 //const val opCount=0 var opCount=0 class Join { fun performOperation(){ //方法里面引用顶层属性 opCount++ println("当前count值:$opCount") } }
-
默认情况下,顶层属性和其他任意属性一样,是通过访问器暴露给Java使用的(如果是val就只有一个getter,如果是var就对应一对getter和setter)
-
const关键字
-
为了方便使用,如果你想要把一个常量以public static final的属性暴露给Java,可以用const来修饰(这个适用于所有的基本数据类型的属性,以及String类型)。(我的理解如果不用const修饰,那么会提供一个getter方法,而这种方式和在java中static中的方式就有点不一样了,java中的static final是没有getter方法的,所以用const来修饰,可能是去掉了getter这个方法,然后可以直接访问这个属性)
//顶层属性,声明在类的外面 const val opCount=0
const必须写在val前面,而且不能用var修饰,等同于:
public static final int opCount=0;
-
-
-
3.3、给别人的类添加方法:扩展函数和属性
导言
- 这个标题很明显了,是给已写好的类添加一些扩展的函数,在已有的基础上添加一些自己的函数或者属性
使用步骤
扩展函数定义在类的外面
- 函数声明格式:
- 在函数名称前面加要扩展的类,比如这里是扩展java的String类,在函数前面加String.,这个叫做接收者类型;
- this为接收者对象,用来调用扩展函数的对象
代码
-
package 三.函数的定义与调用 /** * 注意:扩展的函数,要定义在类的外面 */ fun String.lastChar():Char=this.get(this.length-1) /** * 扩展的类 */ class ExtendUtils { fun test(){ println("Kotlin".lastChar()) } }
String为接收者类型,“Kotlin”为接收者对象
-
自己写的扩展函数可以写在任何一个.kt文件中:
- 函数要声明在类的外面;
- 函数名称前面加了扩展类的类型String.
- 接收者对象this
这就相当于给String类增加了一个额外的函数,这个函数其他类可以直接使用,java类也可以直接使用
注意
在扩展函数中,可以直接访问被扩展的类的其他方法和属性
扩展函数并不允许你打破它的封装性,不能访问私有的或者是受保护的成员
导入和扩展函数
-
对于你定义的一个扩展函数,它不会自动地在整个项目范围内生效。相反,如果你要使用它,需要进行导入,就像其他任何的类或者函数一样
-
为了避免偶然性的命名冲突,Kotlin允许用和导入类一样的语法来导入单个的函数
import 三.函数的定义与调用.lastChar val c="Kotlin".lastChar()
-
使用
关键字as
来修改导入的类或者函数名称import 三.函数的定义与调用.lastChar as last val c="Kotlin".last()
当你在不同的包中,有一些重名的函数时,在导入时给它重命名就显得很有必要了,这样可以在同一个文件中去使用它们。在这种情况下,对于一般的类和函数,还有一个选择,可以选择用全名来指出这个类或者函数
从java中调用扩展函数
-
java调用的时候,kotlin文件ExtendUtils变成了ExtendUtilsKt,
多了一个Kt的后缀
,代码如下:ExtendUtilsKt.lastChar("哈哈");
作为扩展函数的工具函数
- 扩展函数无非就是静态函数的一个高效的语法糖,可以使用更具体的类型来作为接收者类型,而不是一个类
不可重写的扩展函数
扩展函数的静态性质决定了扩展函数不能被子类重写
Kotlin会把扩展函数作为静态函数对待
扩展属性
-
扩展属性提供了一种方法,用来扩展类的API,可以用来访问属性,用的是属性语法而不是函数的语法
-
例子
val String.lastChar:Char get()=get(length-1)
-
可以看到,和扩展函数一样,扩展属性也像接收者的一个普通的成员属性一样。
-
这里,必须定义getter函数,因为没有支持字段,因此没有默认的getter的实现,同理,初始化也不可以,因为没有地方存储初始值
-
如果在StringBuilder上定义一个相同的属性,可以置为var,因为StringBuilder的内容是可变的
//声明一个可变的扩展属性 var StringBuilder.lastChar:Char get()=get(length-1) set(value:Char){ this.setCharAt(length-1,value) }
//可以像访问使用成员属性一样访问它 println("Kotlin".lastChar()) val sb=StringBuilder("Kotlin?") sb.lastChar='!' println(sb) >> n Kotlin!
-
注意
当从Java中访问扩展属性的时候,
应该显式的调用它的getter函数
:StringUtilKt.getLastChar("Java")
3.4、处理集合:可变参数、中缀调用和库的支持
扩展java集合的api
-
本章前提,是基于Kotlin中的集合与Java的类相同,但对API做了扩展,可以看一个示例,用来获取列表中最后一个元素并找到数字集合中的最大值:
val strings:List<String> = listOf("first","second","fourteenth") println(strings.last()) val numbers:Collection<Int> = setOf(1,14,2) println(numbers.max()) fourteenth 14
-
为什么在Kotlin中能对集合有这么多丰富的操作,答案很明显了:因为被声明为了扩展函数。许多扩展函数都在Kotlin标准库中都有声明
可变参数:让函数支持任意数量的参数
-
当你在调用一个函数来创建列表的时候,可以传递任意个数的参数给它:
var list=listOf(2,3,5,7,11)
-
查看这个函数在库当中的声明,可以发现
fun listOf<T>(vararg values:T):List<T>{...}
-
Kotlin可变参数与java类似,但语法略有不同:Kotlin在该类型之后不再使用三个点,而是在
参数上使用vararg修饰符
-
展开运算符
- 在java中,可以按原样传递数组,而Kotlin则要求你显式的解包数组,以便每个数组元素在函数中能作为单独的参数来调用。从技术的角度来讲,这个功能被称为
展开运算符,使用的时候,在对应的参数前面加一个*
这个示例展示了,通过展开运算符,可以在单个调用中组合来自数组的值和某些固定值,这在Java中并不支持
- 在java中,可以按原样传递数组,而Kotlin则要求你显式的解包数组,以便每个数组元素在函数中能作为单独的参数来调用。从技术的角度来讲,这个功能被称为
键值对的处理:中缀调用和解构声明
-
可以使用mapOf函数创建map
val map=mapOf(1 to "one",7 to "seven",53 to "fifty-three")
-
单词to不是内置的结构,而是一种特殊的函数调用,被称为
中缀调用
-
在中缀调用中,没有添加额外的分割符,
函数名称是直接放在目标对象名称和参数之间的
,以下两种调用方式是等价的1.to("one")
一般to函数的调用
1 to “one”
使用中缀符号调用to函数
-
中缀调用可以与只有一个参数的函数一起使用
,无论是普通的函数还是扩展函数。要允许使用中缀符号调用函数,需要使用infix修饰符
来标记 下面是一个简单的to函数的声明
infix fun Any.to(other:Any)=Pair(this,other)
to函数会返回一个Pair类型的对象,Pair是Kotlin标准库中的类,它用来表示一对元素,Pair和to的声明都用到了泛型,简单起见,这里省略了泛型
可以直接用Pair的内容来初始化两个变量
val (number,name) = 1 to "one"
Kotlin允许一次声明多个变量 ,此技术成为解构声明
, 如图展示了它如何与Pair一起使用:-
也就是1 to “one”创建了一个Pair对象,然后用解构声明来展开了,分别赋值给了number和name变量
-
解构声明特征不止用于Pair,还可以使用map的key和value内容来初始化两个变量
for((index,element) in collection.withIndex()){ if(index>0) result.append(separator) result.append(element) }
-
to函数是一个扩展函数,可以创建一对任何元素,这意味着它是泛型接收者的扩展:可以使用 1 to “one”、"one" to 1、list to list.size()等写法
-
看看mapOf函数的声明
fun <K,V> mapOf(vararg values:Pair<K,V>):Map<K,V>
像listOf一样,mapOf接收可变数量的参数,只是参数为键值对
-
3.5、字符串和正则表达式的处理
Kotlin字符串和Java字符串完全相同
可以将在Kotlin代码中创建的字符串传递给任何Java函数,也可以把任何Kotlin标准库函数应用到从Java代码接收的字符串上,不用转换,不用创建附加的包装对象
Kotlin通过提供一系列有用的扩展函数,使标准Java字符串使用起来更加方便
它还隐藏了一些令人费解的函数,添加了一些更清晰易用的扩展
分割字符串
-
Kotlin提供了一些名为split的具有不同参数的重载的扩展函数,
用来承载正则表达式的值需要一个Regex类型
,而不是String,这样确保当有一个字符串传递给这些函数的时候,不会被当作正则表达式 -
这里用一个点号或者破折号来分割字符串
//显式地创建一个正则表达式 println("12.345-6.A".split("\\.|-".toRegex())) [12, 345, 6, A]
-
Kotlin使用与Java中完全相同的正则表达式语法
-
这里的模式匹配一个点(我们对它进行转义来表示我们指的是字面量,而不是通配符)或破折号
-
在Kotlin中,使用扩展函数toRegex将字符串转为正则表达式
-
-
对于一些简单的情况,就不需要使用正则表达式了,Kotlin中的split扩展函数的其他重载支持任意数量的纯文本字符串分隔符:
println("12.345-6.A".split(".","-"))
正则表达式和三重引号的字符串
-
例子:解析文件的完整路径名称到对应的组件:目录、文件名和扩展名。有两种方式实现
-
1、使用扩展函数处理字符串。Kotlin标准库中包含了一些可以用来获取在给定分隔符第一次(或最后一次)出现之前(或之后)的子字符串的函数
/** * 使用String的扩展函数来解析文件路径 */ fun parsePath(path: String) { //最后一个斜线之前的字符串 val directory = path.substringBeforeLast("/") //最后一个斜线之后的字符串 val fullName = path.substringAfterLast("/") val fileName = fullName.substringBeforeLast(".") val extension = fullName.substringAfterLast(".") println("Dir:$directory,name:$fileName,ext:$extension") }
path字符串中的最后一个斜线之前的部分,是目录的路径;点号之后的部分,是文件的扩展名;而文件名称,介于两者之间
解析字符串在Kotlin中变得更加容易,而且不需要使用正则表达式,这个功能非常强大
-
2、使用正则表达式
-
也可以使用Kotlin标准库中的正则表达式来实现
/** * 使用正则表达式来解析文件路径 */ fun parsePath2(path:String){ val regex="""(.+)/(.+)\.(.+)""".toRegex() val matchResult=regex.matchEntire(path) if(matchResult!=null){ //这里用到了解构声明 val (directory,filename,extension)=matchResult.destructured println("Dir:$directory,name:$filename,ext:$extension") } }
//使用正则表达式来解析文件路径 parsePath2("/User/yole/kotlin-book/chapter.adoc") - Dir:/User/yole/kotlin-book,name:chapter,ext:adoc
-
正则表达式写在一个三重引号的字符串中,在这样的字符串中,不需要对任何字符进行转义,包括反斜线
,所以可以用.而不是\.来表示点,正如写一个普通的字符串字面值一样 -
这个正则表达式将一个路径分为三个由斜线和点分割的组。这个.模式从字符串的一开始就进行匹配,所以第一组(.+)包含最后一个斜线之前的子串。这个子串包括所有前面的斜线,因为他们匹配“任何字符”的模式,同理,第二组包含最后一个点之前的子串,第三组包含剩余部分
-
多行三重引号的字符串
-
三重引号字符串的目的,不仅在于避免转义字符,而且使它可以包含任何字符,包含换行符,另外,它提供了一种更简单的方法,从而可以简单的把包含换行符的文本嵌入到程序中
-
例如,可以用ASCII码画点东西
//多行三重引号字符串 val kotlinLogo="""| // .|// .|/ \""" println(kotlinLogo.trimMargin("."))
-
一个三重引号的字符串可以包含换行,而不用专门的字符,比如\n,也不必转义字符\
-
因为多行字符串不支持转义序列,如果需要在字符串的内容中使用美元符号的字面量,则必须使用嵌入式表达式
,像这样:val price="""$ {'$'} 99.9"""
-
为了更好的格式化,
使用trimMargin函数
3.6、让你的代码更整洁:局部函数和扩展
局部函数
-
为了让代码更整洁,可以在函数中嵌套这些提取的函数,这样,既可以获得所需的结构,也无需额外的语法开销
-
例子
-
saveUser函数用于将user的信息保存到数据库,并且确保user对象包含有效数据
-
代码
/** * 带重复代码的函数 */ 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") } }
//带重复代码的函数 saveUser(User(1,"",""))
这里的重复代码很少,你可能不想要再类中的一个面面俱到的方法中,去验证用户字段的每一种特殊情况
-
但是,如果将验证代码放到局部函数中,可以摆脱重复,并保持清晰的代码结构,可以这样做
/** * 提取局部函数来避免重复 */ fun saveUser2(user:User){ //声明一个局部函数来验证所有字段 fun validate(user:User,value:String,fieldName:String){ if(value.isEmpty()){ throw IllegalArgumentException("Can't save user ${user.id}:empty $fieldName") } } //调用局部函数来验证特定字段 validate(user,user.name,"Name") validate(user,user.address,"Address") //保存user到数据库 }
-
局部函数,可以访问所在函数中的所有参数和变量,我们可以去掉User参数
/** * 在局部函数中访问外层函数的参数 */ fun saveUser3(user: User) { //现在不用在saveUser3函数中重复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") //保存user到数据库 }
-
继续改进,把验证逻辑放到User类的扩展函数中
package 三.函数的定义与调用 import java.lang.IllegalArgumentException /** * 提取逻辑到扩展函数 */ fun User.validateBeforeSave(){ fun validate(value:String,fieldName:String){ if (value.isEmpty()) { //可以直接访问User的属性 throw IllegalArgumentException("Can't save user $id:empty $fieldName") } } validate(name, "Name") validate(address, "Address") } class User(val id:Int,val name:String,val address:String) { }
/** * 保存用户信息 */ fun saveUser4(user:User){ //调用扩展函数 user.validateBeforeSave() //保存user到数据库 } saveUser4(User(1,"",""))
-
扩展函数也可以被声明为局部函数,但是深层嵌套的局部函数往往让人费解,因此,一般不建议使用多层嵌套
-