美文网首页
Kotlin精点

Kotlin精点

作者: MoShengLive | 来源:发表于2025-03-14 18:07 被阅读0次

一 基础点

密封类与枚举类

package com,a5lwork6.section5
@0class Success(val message: String) : Result()8class Failure(val error: Error) : Result()
sealed class Result
fun onResult(result: Result){
④
when (result){
is success -> println("${result}输出成功消息:${result.message}"、is Failure -> println("${result)输出失败消息:${result.error.message)"//else->不再需要
fun main(args: Array<String>) {
val result1 = Success("数据更新成功")
onResult (result1)
val result2 = Failure(Error("主键重复,插入数据失败"))onResult(result2)

上述代码第①行是声明一个密封类 Result,使用 sealed 修饰。密封类本身就是抽象的不需要 abstract 修饰,一定也是 open 的,密封类不能实例化。代码第②行和第③行都是声明密封类的子类,但是 Success 和 Failure 的内部结构是不同的,Success 有一个字符串属性
message,而 Failure 有一个 Error 类型属性。
代码第④行使用 when 结果判定密封类实例,注意不再需要 else 结构
提示
密封类与枚举类的区别:密封类子类与枚举类常量成员是对应的,密封类子类可以有不同的内部结构,子类可以有多个构造函数,可以创建多个不同的实例。而枚举类常量成员的构造函数是固定的,由枚举类定义好的,每一个枚举类常量只能创建一个实例。
密封类的子类还可以写成嵌套类形式,这是Kotlin1.1之前版本的密封类规然可以使用这些形式。示例代码

接口

使用接口

比抽象类更加抽象的是接口,接口中主要应该包含抽象函数和抽象属性,但是根据需要可以有具体函数和具体属性。

提示

接口和抽象类都可以有抽象函数和抽象属性,也可以有具体函数和具体属性。那么接口和抽象类有什么区别?接口不能维护一个对象状态,而抽象类可以,因为维护一个对象状态需要支持字段,而接口中无论是具体属性还是抽象属性,后面都没有支持字段。

接口概念

其实 13.1.1 节的抽象类 Figure 可以更加抽象,即 Figure 接口,虽然接口中可以有函数和属性,也有具体函数和具体属性,但接口不保存状态。将13.1.1节中的几何图形类改成接口后,类图如图 13-2 所示。
13.2.2 接口声明和实现
在 Kotlin 中接口声明使用的关键字是 interface,声明接口 Figure 示例代码如下:
第13章 抽象类与接口

//代码文件:chapter13/src/com/a5lwork6/section2/s3/AB.kt
package com.a5lwork6.section2.s3.
class AB : Any(), InterfaceA, InterfaceB { ①
override fun methodc(){}
override fun methodA(){}
override fun methodB(){}

上述代码第①行是声明 AB 类,其中继承 Any 类实现了两个接口。注意先声明继承父类,并指定调用父类的哪个构造函数,然后再是声明的接口,它们之间使用逗号(,)分隔在 AB 类中的代码第②行是 methodB0函数,这个函数既实现了 InterfaceA 又实现了InterfaceB .
13.2.4 接口继承
Kotin 语言中允许接口和接口之间继承。由于接口中的函数都是抽象函数,所以继承之后也不需要做什么,因此接口之间的继承要比类之间的继承简单得多。如图 13-4 所示,其中 InterfaceB 继承了 InterfaceA,在InterfaceB 中还重写了InterfaceA 中的 methodB()函数ABC 是 InterfaceB 接口的实现类,从图 13-4 中可见,ABC需要实现 InterfaceA 和 InterfaceB接口中的所有函数。


image.png

图 13-4 接口继承类图
接口 InterfaceA 和 InterfaceB 代码如下:

//代码文件:chapter13/src/com/a5lwork6/section2/s4/InterfaceA.kt
Package com.a5lwork6.section2.s4
interface InterfaceA {
fun methodA()
methodB()
}

第3篇 函数式编程

14.2.2 函数字面量
函数类型可以声明的变量就是函数字面量,那么函数类型变量能够接收什么数据呢即函数字面量如何表示的问题,函数字面量可以有三种表示:
。函数引用。引用到一个已经定义好的、有名字的函数。它可以作为函数字面量。匿名函数。没有名字的函数即匿名函数,它也可以作为函数字面量。
。Lambda 表达式。Lambda表达式是一种匿名函数,可以作为函数字面量,示例代码如下:

//代码文件:chapter14/src/com/a51work6/section2/ch14.2.2.kt
package com.a5lwork6.section2
fun calculate(opr: Char):(Int, Int)-> Int {
//加法函数
fun add(a: Int, b: Int): Int {
return a+ b
//减法函数fun sub(a: Int, b: Int): Int{
return a-b
val result:(Int, Int)-> Int =
when (opr){
'+'-> ::add
1-' -> ::sub
中→{
//乘法匿名函数
fun(a: Int, b: Int): Int {
return (a * b)
0@
6
val f3 = calclprintin(f3 (10,val f4 = calcprintIn(f4(10
上述代码第①行和是两个局部函数,它们的给 result 变量。代码第赋值给 result 变量。代个函数类型获得lfl= cald代码用 fl 函代码巢不再赘,
14.23
若可以扎的 calculate 函再介绍一个函类
//代码文件:(
package com.
fun getArea(ty
var returnl
when (type
"rect"
reti
else
else ->{ a,b->(a / b)} //除法Lambda 表达式④
return result
fun main(args: Array<string>){val f1 = calculate('+')
println(f1(10,5)) //调用 f1 变量
val f2 = calculate('-')println (f2(10, 5))
println(f2(10,5))

2.使用尾随 Lambda 表达式
[ambda表达式可以作为函数的参数传递,如果[ambda 表达式很长,就会影响程风可读性。如果一个函数的最后一个参数是Lambda 表达式,那么这个 Lambda 表达式部放在函数括号之后。示例代码如下:

fun calculatePrintl(funN:(Int,Int)->Int){//参数是函数类型//使用 funN 参数
printin("${funN(10, 5)}")
}
//打印计算结果函数
//ch14.3.2.kt中的 calculatePrint
fun calculatePrint(n1: Int,
n2: Int,opr: Char,funN:(Int,Int)->Int){//最后一个参数是函数类型②
println("${n1} ${opr} ${n2} = ${funN (n1, n2)}")
}
fun main(args: Array<String>) {
calculatePrint(10,5,'+',{a,b ->a +b ))//标准形式
calculatePrint(10,5,'-'){a,b ->a -b)//尾随Lambda 表达式形式 ③
calculatePrint1({ a,b->a +b})//标准形式
calculatePrint1(){a,b->a +b)//尾随Lambda 表达式形式④
是函发码第(量替

第14 章 函数式编程基石--高阶函数和 Lambda 表达式

return@label 10 ⑤
a + b
//调用 ambda 表达式 add
println(add())//10

上述代码第①行是使用了 forEach 函数,它后面的 Lambda 表达式如果使用代码第②行if(it =10) return -1 语句,则会返回最近的函数,即 sum 函数,不是则返回Lambda 表达式forEach。为了返回 Lambda 表达式则需要在 return 语句后面加上标签,见代码第③行,@forEach 是隐式声明标签,标签名是 Lambda 表达式所在函数名(forEach)。也可以为Lambda 表达式声明显式标签,代码第④行 label@是 Lambda 表达式显式声明标签,代码第⑤行是使用显式标签。
提示
forEach 是集合、数组或区间的函数,它后面是一个 Lambda表达式,集合、数组或区间对象调用 forEach 函数时,会将它们的每一个元素传递给 Lambda 表达式并执行。

在 Kotlin 中一个凼数参数被声明为非空类型时,也可以接收可空类型的参数,但是如果实际参数真的为空,可能会导致比较严重的问题。因此需要在参数传递之前判断可空线数是否为非空,示例代码如下:

//代码文件:chapter14/src/com/a5lwork6/section5/ch14.5.2.kt
package com.a5lwork6.section5
fun square(num: Int): Int = num * num ①
fun main(args: Array<string>){val n1: Int?= 10 //null ②
//自己进行非空判断
if (n1 != null){ ③
printIn(square(n1)) ④
}

上述代码第①行是声明一个函数 square,参数是非空整数类型,该函数实现一个整数的平方运算。代码第②行是声明一个可空整数类型(Int?)变量 n1,代码第③行是判断 n1是否为非空,如果非空才调用,见代码第④行。
自己判断一个对象非空比较麻烦。在 Kotlin 中任何对象都可以使用一个 let 函数,let函数后面尾随一个 [ambda 表达式,在对象非空时执行 Lambda 表达式中的代码,为空时则不执行。
示例代码如下:

nl?.let {n -> println(square(n))}
nl?.let { println(square(it))

这两行代码都是使用 let 函数进行调用,效果是一样的,当n1 非空时执行 Lambda 表达式中的代码,如果n1为空则不执行。n1?.let {println(square(it)}语句省略了参数声明,使用隐式参数 it 替代参数 n。
14.5.3 使用 with 和 apply 函数
当需要对一个对象设置多个属性或调用多个函数时,可以使用 with 或 apply 函数。与let函数类似,Kotlin 中所有对象都可以使用这两个函数。
示例代码如下:

//代码文件:chapter14/src/com/a51work6/section5/ch14.5.3.kt
第3篇 函数式编程
package com.a5lwork6.section5
import java.awt.Borderlayout
import javax.swing.JButton
import javax.swing.Jrrame
import javax.swing.Jlabel
class MyFrame(title: string): Jprame(title){
init {//创建标签
val label = JLabel ("abel")
//创建 Button1
val button1 = JButton() ①
buttonl.text = "Button1"
button1.toolripText = "Button1"
//注册事件监听器,监听 Button1 单击事件
buttonl.addActionlistener {label.text ="单击Button1"}②
//创建 Button2
val button2 = JButton().apply {③
text = "Button2"
toolTipText = "Button2"
//注册事件监听器,监听 Button2 单击事件
addActionlistener {label.text ="单击Button2"
//添加 Button2 到内容面板
contentPane.add (this, BorderLayout .SOUTH)④
}
with(contentPane){⑤
//添加标签到内容面板
add (label, Borderlayout .NORTH)
//添加 Button1 到内容面板
add (button1, BorderLayout .CENTER)
println(height)
printin(this.width)⑥
}
//设置窗口大小
setsize(350, 120)//设置窗口可见
fun main (args: Array<string>)//创建 Frame 对象MyFrame ("MyFrame" )

上述代码是 Swing 的窗口,Swing 是 Java 的用户图形界面介绍,swing 将会在第 22绍,本示例中图形界面组件的技术细节暂不讨论。代码第①行和第国行分别创建两7囊对象,其中代码第①行~第②行是创建并调用 Buonl 的属性和函数,这是传统的做法楼于多次调用同一个对象的属性或函数,可以使用 wih 或 apply 函数,代码第⑧行 第④行是创建并调用 Butom2 的属性和函数,其中使用 apply 函数,apply 函数后面尾随一个(mbd 表达式,需要调用的属性和函数被放到Lambda 表达式中,Lambda 表达式中省略了对象名 buton2,例如 text="Buton2"表达式说明调用的 buton2 的 text 属性,aply 函数中若想引用当前对象可以使用 this 关键字,例如 contentPane.add(this, BorderLayout.sOUTH)中的 this。apply 函数是有返回值的,它的返回值就是当前对象。
如果不需要返回值可以使用 with 函数,with 函数与 apply 函数类似。代码第⑤行~第®行是使用 with 函数,with 函数后面也尾随一个Lambda 表达式,需要调用的属性和函数被放到 Lambda 表达式中,with 函数中若想引用当前对象也是使用 this 关键字。
本章小结
本章主要介绍了高阶函数和 Lambda 表达式,读者需要理解函数式编程特点。熟悉高阶函数和Lambda 表达式特点。掌握 Lambda 表达式标准语法,了解 Lambda 表达式的几个简写方式以及尾随Lambda 表达式、熟悉闭包等内容。了解什么是内联函数以及自定义内联函数,熟悉使用 let、with和 apply 等内联函数的使用场景。

第15章 泛型

以上分别对3种类型的两个参数进行了比较,声明了类似的3个函数,那么是否可以个函数使之能够比较3种类型呢?合并后的代码如下:

oivate fun <T> isEquals(a: T, b: T): Boolean {return (a == b}

在函数名 isEquals 前面添加<T>就是泛型函数了,<T>是声明类型参数,T是类型参数,藏中参数类型也被声明为T,在调用函数时T会被实际的类型替代。
提示
泛型中的类型参数,可以是任何大写或小写的英文字母,一般情况下使用字母T、E、K和 U等大写英文字母。

调用泛型函数代码如下:

fun main(args: Array<string>){
printin(isEquals(1, 5))
printin(isEquals(1.0, 5.0))
}

isEquals(1,5)调用函数时将类型参数T替换为 Int 类型,而 isEquals(1.0,5.0)调用函数时将类型参数T 替换为 Double 类型。
15.1.2 多类型参数
上一节泛型函数示例只是使用了一种类型参数,事实上可以同时声明使用多个类型参数,它们之间用逗号“,”分隔,示例如下:
fun <T, U> addRectangle(a: T, b: u): Boolean {...}
类型参数不仅可以声明函数参数类型,还可以声明函数的返回类型,示例代码如下:
fun <r, U> rectangleEquals (a: T, b: U): U {...}
15.1.3 泛型约束
事实上在 15.11 节声明的 fun <T> isEquals(a:T,b:T):Boolean 函数还有一点问题,这是因为并不是所有的类型参数T 都具有“可比性”,必须限定T的类型,如果只是数字类型比较可以限定为 Number,因为 In 和 Double 等数字类型都继承了 Number,是 Number的子类型。声明类型参数时在T后面添加冒号(:)和限定类型,这种表示方式称为“泛型约束”,泛型约束主要应用于泛型函数和泛型类的声明。
示例代码如下:

//代码文件:chapter15/src/com/a51work6/section1/ch15.1.3.kt
package com.a5lwork6.section1
private fun <r : Number> isEquals(a:T, b: T): Boolean { ①
return (a == b)
}
fun main(args: Array<string>){
printin(isEquals(1, 5))//false
println(isEquals(1.0,1.0)) //true
}

上述代码第①行是声明泛型函数,其中<T:Number>是带有约束的类型参数。代码②行是比较两个 Int 整数是否相等,代码第③行是比较两个 Double 浮点数是否相等。代码第①行的 isEquals 函数只能比较 Number 类型的参数,不能比较 String 等其他据类型。为此也可以将类型参数限定为 Comparable<T>接口类型,所有可比较的对象都实现 Comparable<T>接口,Comparable<T>本身也是泛型类型。
修改代码如下:

//代码文件:chapter15/src/com/a51work6/section1/ch15.1.3.kt
package com.a51work6.section1
import java.util.*
fun <T : Comparable<T>> isEquals(a:T,b: T): Boolean {return (a == b)
fun main(args: Array<String>){
println(isEquals(1, 5))//false
printin(isEquals(1.0,1.0)) //true
println(isEquals("a", "a")) //true
val dl = Date()
val d2 = Date()
println(isEquals(d1, d2))//true

代码第①行是比较两个字符串是否相等,代码第②行是比较两个日期

集合
image.png

17.2.2 forEachIndexed
使用 forEach 函数无法返回元素的索引,如果既想返回集合元素,又想返回集合元索引,则可以使用 forEachIndexed 函数,forEachIndexed 适用于 Collection 集合和数组。示例代码如下:

//代码文件:chapter17/src/com/a51work6/section2/ch17.2.2.kt
package com.a5lwork6.section2
fun main(args: Array<String>){
val strArray = arrayOf("张三","李四","王五","董六")//创建字符串数组
val set= setof(1,3,34,54,75)//创建 Set 集合
print1n("-----遍历数组-----"

下面通过一个示例介绍一下 map 函数使用,示例代码如下:

/代码文件:chapter17/src/com/a51work6/section3/ch17.3.2.kt
package com.a5lwork6.section3
fun main(args: Array<string>){
users.filter { it.name.startsWith("t", ignorecase = true) )①
.map ( it.name )②
.forEach{ printin(it)}③

输出结果:

Tony
Tom

上述代码使用 flter 函数和 map 函数对集合进行操作,过程如图 17-3 所示,代码第①行使用 filter 函数过滤,只有两元素,元素类型是 User 对象。代码第②行使用 map 函数对集合进行变换,it.name 是变换表达式,将计算的结果放到一个新的List 集合中,从图 17-3可见,新的集合元素变成了字符串,这就是 map 函数变换的结果。


image.png

[图片上传中...(image.png-3eaa2e-1742030068190-0)]

17.3.3 reduce
聚合操作会将 Collection 集合或数组中的数据聚合起来输出单个数据,聚合操作中最基础的是归纳函数 reduce,reduce 函数会将集合或数组的元素按照指定的算法积累叠加起来,最后输出一个数据。
下面通过一个示例介绍一下 reduce 函数的使用,示例代码如下:

//代码文件:chapter17/src/com/a51work6/section3/song.ktPackage com.a5lwork6
data class Song(val title: string, val durationInSeconds: Int) ①
//测试使用
val songs= listof(Song("Speak to Me", 90),②
Song("Breathe",163),
Song("on he Run", 216),
Song("rime", 421),
Song("The Great Gig in the sky", 276),
Song("Money", 382),
Song("us and Them", 462),
Song("Any Color You Like", 205),
Song("Brain Damage",228),
Song("Eclipse", 123)
//代码文件:chapter17/src/com/a5iwork6/section3/ch17.3.3.kt
package com.a5lwork6.section3
import com.a5lwork6.songs
fun main(args: Array<String>){
//计算所有歌曲播放时长之和
val durations = songs.map { it.durationInSeconds }③
.reduce { acc, i -> ④
acc + i
}
println(durations)//输出:2566
}

为了测试首先声明了一个数据类 Song,见代码第①行。代码第②行声明 songs 的属性它是保存 Song 对象的 List 集合。
代码第③行是调用 map 函数变换 songs 集合的数据,返回歌曲时长(durationInSeconds的 List 集合。代码第④行调用 reduce 函数计算时长,其中 acc 参数是上次累积的计算结果i为当前元素,acc+i表达式是进行累加。这里表达式是关键,根据自己的需要这个表达是不同的。

17.4 聚合函数

聚合函数虽然 173 节已经介绍了一些基础函数,但对集合和数组的操作还有很多函数,下面再分别介绍一下常用的函数。
首先介绍聚合函数,常用的聚合函数除了reduce 外还有 11个,如表 17-1 所示


image.png
image.png

函数17.5
常围的过滤函数除了 fter 还有 14个,如表 17-2 所示:


image.png
17.6 映射函数·
常用的映射函数除了 map 外还有3个,如表 17-3 所示。
image.png
17.7 排序函数

常用的排序函数有5个,如表 17-4 所示。


image.png
18.3.3 多catch 代码块

如果 try 代码块中有很多语句发生异常,而且发生的异常种类又很多,那么可以在i后面跟随多个 catch 代码块。多个 catch 代码块语法如下:

try{
//可能会发生异常的语句
}catch(e :Throwable)
//处理异常e
} catch(e : rhrowable){
//处理异常e
} catch(e : Throwable){
//处理异常e
}

在多个 catch 代码的情况下,当一个 catch 代码块捕获到一个异常时,其他的 catch 代码块就不再进行匹配。
注意
当捕获的多个异常类之间存在父子关系时,捕获异常顺序与catch 代码块的顺序有关。一般先捕获子类,后捕获父类,否则子类捕获不到。
注意
try-catch 不仅可以嵌套在 try 代码块中,还可以嵌套在 catch代码块或 finally 代码块中,finally 代码块后面会详细介绍。tny-catch 嵌套会使程序流程变得复杂,如果能用多 catch 捕获异常,尽量不要使用 try-catch 嵌套。要梳理好程序的流程再考虑 try-catch 嵌套的必要性。

18.4释放资源

有时在 try-catch 语句中会占用一些非 Java 虚拟机资源,如打开文件、网络连接、打开数据库连接和使用数据结果集等,这些资源并非Kotin 资源,不能通过Java虚拟机的垃圾收集器回收,需要程序员释
18.4.2 自动资源管理
15.4.1 节使用 fnaly 代码块释放资源会导致程序代码大量增加,一个 @nndy 代码块行建比正常执行的程序还要多、在Kotin 中使用Java7之后提供的自动资源管m(AmommtBeomee Management)技术,可以替代 fnaly 代码块,优化代码结构,提高程序可还性以示例代码如下:

代码文件:chapter18/src/com/a5lwork6/section4/ch18.4.2,kt
package com,a5lwork6.section4.
import java.io.*
import java.text.ParseException
import java.text.SimpleDateFormatimport java.util.*
fun main(args: Array<string>){
val date = readDate()
println("读取的日期 ="+ date)
private fun readDate(): Date? {
//自动资源管理
try {
FileInputStream("readme.txt").use { fileis ->InputStreamReader(fileis).use {isr ->BufferedReader(isr).use { br->

//读取文件中的一行数据val str = br.readLine() ?: return null
val df = SimpleDateFormat("yyyy-MM-dd")
return df.parse(str)
)catch (e: FileNotFoundException){
printin("处理 FileNotFoundException...")
e.printstackTrace()
}catch (e: IoException){
printin("处理 IoException...")
e.printstackTrace()
}catch (e: ParseException){
println("处理 ParseException...")
e.printstackTrace()
return null

上述代码第①行~第③行是调用输入流 use 函数进行嵌套,这就是自动资源管理技,采用了自动资源管理后不再需要 finaly 代码块,不需要自己关闭这些资源,释放过程交!了 Java 虚拟机。
注 意
所有可以自动管理的资源需要实现 Java 中的 AutoCloseable接口,上述代码中三个输入流 FileInputStreamInputStreamReader 和 BufferedReader 都实现了 Java 中的AutoCloseable 接口,这些资源对象都可以使用 use 函数管理资源。

18.5 throw 与显式抛出异常“

本节之前读者接触到的异常都是由系统生成的,当异常发生时,系统会生成一个异常对象,并将其抛出。但也可以通过 throw 语句显式抛出异常,语法格式如下:

throw Throwable 或其子类的实例

所有 Throwable 或其子类的实例都可以通过 throw 语句抛出。显式抛出异常的作用有很多,例如不想某些异常传给上层调用者,可以捕获之后重显式抛出另外一种异常给调用者。
提示
Java 中一个函数要抛出异常,需要在函数后使用 throws 语句显式声明。而 Kotlin 中没有 throws 关键字,也不需要显式声明抛出异常。
本章小结
本章介绍了 Kotlin 异常处理机制,其中包括Kotlin 异常类继承层次、捕获异常、释放资源及throw 使用等。读者需要重点掌握捕获异常处理。

线程

19.1.3 主线程
Kotlin 程序至少会有一个线程,这就是主线程,程序启动后由 Java 虚拟机创建主线程程序结束时由 Java 虚拟机停止主线程。主线程负责管理子线程,即子线程的启动、挂起停止等操作。图 19-2展示了进程、主线程和子线程的关系,其中主线程负责管理子线程
即子线程的启动、挂起、停止等操作。


image.png

19.2 创建线程
在Java 中线程类是 Thread,Kotlin 中的线程也是使用了 Java 中ad类。在 Java 中创建一个线程比较麻烦,而在 Kotlin 中非常简单,使用tread 函数就可以,thread 函数定义如下:

fun thread(
start: Boolean = true,
isDaemon: Boolean =false,
contextClassLoader: ClassLoader?= null,name: String? = null,priority: Int =-1,
block:()->Unit
): Thread

thread 函数的返回类型是 Thread 类,函数中 start 参数在创建完成线程后马上启动,在Java 中启动线程需要另外调用 start 函数;isDaemon 参数是守护线程,守护线程是一种在后台长期运行的线程,守护线程主要提供一些后台服务,它的生命周期与Java 虚拟机一样长;contextClassLoader 参数是类加载器,用来加载一些资源等:name 参数是指定线程名,如果不指定线程名,系统会分配一个线程名;priority 参数是设置线程优先级:block参数是线程体,即线程要执行的核心代码。
提示
主线程中执行入口是 main(args:Array<String>)函数,这里可以控制程序的流程、管理其他的子线程等。子线程执行入口是线程体,子线程相关代码都是在线程体中编写的。
下面看一个具体示例代码如下:

//代码文件:chapter19/src/com/a5lwork6/section2/ch19.2.kt
package com.a5lwork6.section2
import java.lang.Math.random
import java.lang.Thread.currentThread
import java.lang.Thread.sleep
import kotlin.concurrent.thread
//编写执行线程代码
fun run(){
for (i in 0..9){
//打印次数和线程的名字
println("第${i}次执行- ${currentThread().name}")③
//随机生成休眠时间
val sleepTime =(1000 * random()).toLong()W
//线程休眠
sleep(sleeprime)
//线程执行结束
printin("执行完成!"+ currentThread().name)
fun main(args: Array<String>) {
//创建线程 1
thread {⑥
run ()
}
//创建线程2
thread(name = "MyThread"){ ⑦
run ()
}
}

2)就绪状态
当主线程调用新建线程的 start(函数后,它就进入就绪状态(Runnable)。此时的线程尚未真正开始执行线程体,它必须等待 CPU 的调度。
3)运行状态
CPU 调度就绪状态的线程,线程进入运行状态(Running),处于运行状态的线程独占CPU,执行完成线程体。
4)阻塞状态
由于某种原因运行状态的线程会进入不可运行状态,即阻塞状态(Blocked)。Java 鹿拟机系统不能执行处于阻塞状态的线程,即使 CPU 空闲,也不能执行该线程。如下几个因会导致线程进入阻塞状态:
。当前线程调用 sleep 函数,进入休眠状态;
。被其他线程调用了 ioin 函数,等待其他线程结束;
。发出 IO 请求,等待 IO 操作完成期间;
当前线程调用 wait 函数。
处于阻塞状态可以重新回到就绪状态,如休眠结束、其他线程加入、IO 操作完成和调用 notify 或 notifyAll 唤醒 wait 线程。
5)死亡状态
线程执行完成线程体后,就会进入死亡状态(Dead),线程进入死亡状态有可能是正常执行完成进入,也可能是由于发生异常而进入的。


image.png
fun main(args; Array<string>){
//创建线程t1
val tl = thread (
//一直循环,直到满足条件再停止线程
while (command !="exit"){②
//线程开始工作
//TODO
printin("下载中...")
//线程体眠
Thread.sleep(10000)
}
//线程执行结束
println("执行完成!")
}
command = readiine()!!//接收从键盘输入的字符串③
}

上述代码第①行是设置一个结束变量。代码第②行是在子线程的线程体中判断用户输是否为 exit 字符串,如果是则结束循环,否则进行循环,结束循环就结束了线程体,就停止了。
代码第③行的 readLine 函数接收从键盘输入的字符串。测试时需要注意:在控制台输it,然后按 Enter 键,如图 19-4 所示。
提示
控制线程停止不要使用Thread 提供的 stop 函数,不推荐使用这个函数,这个函教有时会引发严重的系统故障,类似的还有suspend 和 resume 挂起函数。控制线程停止推荐的做法就是采用本例的结束变量方式。
本章小结
本章介绍了 Kotlin 线程技术。首先是介绍了线程相关的一些概念,然后介绍了如何创建子线程、线程状态和线程管理等内容,其中创建线程和线程管理是学习的重点。

提示
底层API 和高级 AP! 的包名中都有 experimental,这表明目前协程 API 还是实验性的,在未来有可能还会有一些变化。
20.2.2 创建支持 kotlinx.coroutines 的项目
由于 kotlinx.coroutines 提供了高级 API,使用起来比标准库中的底层 AP 要简单得多本节重点介绍使用 kotlinx.coroutines 实现协程编程。kotlinx.coroutines 不属于Kotlin 标准库需要额外配置项目依赖关系,因此需要创建 IntelliJ IDEA 与 Gradle 项目,项目创建完成后再打开 build.gradle 文件,添加依赖关系,具体内容如下:

group 'com.5lwork6'
version '1.0-SNAPSHOT'
apply plugin:'java'
apply plugin:'kotlin'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile "org.jetbrains,kotlin:kotlin-stdlib-jre8:skotlin_version"
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.19.3' ①
testCompile group: "junit', name: 'junit', version: '4.12'
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
}

上述代码第行的 compile 'orgjetbrains kodinx kotinx coroutines-coret0. 19.3'刚刚添加的依赖关系,另外,还需要检查代码第②行的 cxtkodin_veirsion 是否为Kotlin最版本。

20.2.3 第一个协程程序
协程是轻量级的线程,因此协程也是由主线程管理的,如果主线程结束那么游结束了。下面看看第一个协程示例:

//代码文件:chapter20/src/main/kotlin/com/a5lwork6/section2/ch20.2..3.kt
package com.a5lwork6.section2
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch

import java.lang.Math.random
import java.lang.Thread.sleep
fun main(args: Array<String>){
launch {//启动一个协程 ①
for(i in 0..9){
//打印协程执行次数
println("子协程执行第${i}次")
//随机生成挂起时间
val sleeprime =(1000 * random()).toLong()
//协程挂起
delay(sleepTime)②
}
print1n("子协程执行结束。")
}
leep(10000L)//主线程休眠,保持其他线程处于活动状态 ③
println("主协程结束。")
}

上述代码第①行的 launch 函数创建并启动一个协程,类似于线程的 thread 函数。代码第②行的 delay 函数是挂起协程,类似于线程的 sleep 函数,但不同的是 delay 函数不会阻塞线程,而 sleep 函数会阻塞线程。代码第③行是让主线程休眠 10s,如果这里主线程不休眠,主线程就直接结束了,其他的线程或协程没有机会运行。
20.2.4 launch 函数与 Job 对象

在上一节的示例中用到的 launch 函数是非常重要的,它的定义如下:

fun launch(
context: CoroutineContext = DefaultDispatcher,
start: CoroutineStart = Coroutinestart.DEFAULT
block: suspend CoroutineScope.()-> Unit
):Job

context 参数是协程上下文对象默认在 DefaultDispatcher 的参数,DefaultDispatcher 通常是一个公共线程池。start 参数设置协程启动,block 参数是协程体,类似于线程体,协程执行的核心代码在此编写,在协程体中执行的函数应该都是挂起函数,例如 delay 函数就是挂起函数。
launch 函数的返回是一个Job 对象,Job 是协程要执行的任务,可以将 Job对象看作协程本身,所有对协程的操作都是通过了Job 对象完成的,协程的状态和生命周期都是通过 Job反映出来的。
提示
使用 kotinx.coroutines 框架,开发人员不需要直接创建协程对象,而是使用 Job 对象。
Job 对象中常用的属性和函数如下:
。isActive 属性:判断Job 是否处于活动状态。
。isCompleted属性:判断Job 是否处于完成状态
。isCancelled 属性:判断Job 是否处于取消状态
。start 函数:开始 Job。
。cancel 函数:取消 Job。
。join 函数:使当前协程处于等待状态,直到 Job 完成,join 是一个挂起函数,只能
在协程体或其他的挂起函数中调用。

//代码文件:chapter20/src/main/kotlin/com/a5lwork6/section2/ch20.2.4.kt
package com.a5lwork6.section2
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import java.lang.Math.random

import java.lang.Thread.sleep
fun main (args: Array<String>){
val job = launch {①
//启动一个协程
for (i in 0..9){
//打印协程执行次数
println("子协程执行第${i}次")
//随机生成挂起时间
val sleepTime =(1000 * random()).toLong()
//协程挂起
delay(sleeprime)
}
Println("子协程执行结束。")
}
printIn(job.isActive)//true ②
printIn(job.isCompleted) //false ③
sleep(10000L)//主线程休眠,保持其他线程处于活动状态
println("主协程结束。")
printIn(job.isCompleted) //true ④
}

上述代码第①行调用 launch 函数创建并开始一个协程,然后返回 Job 对象赋值给 job变量.代码第②行是判断 job 是否处于活动状态。代码第③行是判断 job 是否处于完状态,由于协程还没有执行完成,因此这里返回 flse。代码第④行是返回true。
20.2.5 runBlocking 函数
为了保持其他线程处于活动状态,前面两节的示例中都使用了 sleep 函数。sleep 函数我程提供的函数,最好不要在协程中使用,应该使用协程自己的 delay 函数,但delay是挂起函数,必须在协程体或其他的挂起函数中使用。
修改20.23 节示例代码如下:

代码文件:chapter20/src/main/kotlin/com/a5lwork6/section2/ch20.2.5.kt
package com.a5lwork6.section2
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking
import java.lang.Math.random
fun main(args: Array<String>)= runBlocking<Unit>{①
val job = launch {
//启动一个协程
for (i in 0..9){
//打印协程执行次数
println("子协程执行第${i}次")
//随机生成挂起时间
val sleepTime =(1000 *random()).toLong()
//协程挂起
delay(sleepTime)
}
println("子协程执行结束。")
}
delay(10000L)//主协程挂起②
println("主协程结束。")
}

上述代码第ⓘ行是将 main 代码放到 runBlocking 函数中,runBlocking 函数也是启动并创建一个协程,可以与顶层函数一起使用。代码第②行是使用 dclay 函数挂起主协程。

20.3 协程生命周期

协程的生命周期是通过Job 的几种状态体现的,如图 20-1 所示,Job 协程有 6种状态。
1)新建状态
新建状态主要是通过 launch 函数创建协程对象,它仅仅是一个空的协程对象。
2)活动状态
新建协程调用 start 函数后,它就进入活动状态。launch 函数通过 start 参数判断是否启动协程。处于活动状态的协程会执行协程体。
3)正在完成状态
正在完成状态是一个瞬间过渡状态,从活动状态进入到已完成状态时经历的中间状态。
4)已完成状态
协程成功执行完协程体,就会进入已完成状态,这是最终状态,说明这个协程已经
停止。
5)正在取消状态
在活动状态或正在完成状态时,如果调用了 cancel函数则会进入已取消状态,在此之前要先进入正在取消状态,正在取消状态也是一个瞬间过渡状态。
6)已取消状态
在新建状态、活动状态或正在完成状态时,如果调用了 cancel 函数最终都会是已取消状态,只是新建状态没有经历正在取消状态,而直接是已取消状态。已取消状态是最终状态,说明这个协程已经停止。


image.png

Job 状态可以通过Job 的 isActive、isCompleted 和 isCanceled 属性判断而知,

//代码文件:chapter20/src/com/a51work6/section4/ch20.4.2.kt
package com.a5lwork6.section4
import kotlinx.coroutines .experimental.delay
import kotlinx.coroutines.experimental.runBlocking
import kotlinx,coroutines.experimental. withrimeout
suspend fun run(name: string)(
//启动一个协程
for (i in 0..9) {
//打印协程执行次数
println("子协程${name)执行第${1)次")
//随机生成挂起时间
val sleeprime =(1000 * Math.random()).torong()
//协程挂起
delay(sleeprime)
}
println("子协程${name)执行结束。")
}
fun main(args: Array<String>)= runBlocking<Unit>{
//启动一个协程1
withrimeout(2000L){ ①
run ("job1")
}
println("主协程结束。")
}

执行结果如下:

子协程 job1 执行第0次
子协程job1 执行第1次
子协程 job1 执行第2 次
子协程job1 执行第 3次
子协程 job1 执行第4 次
Exception in"thread "main" kotlinx.coroutines.experimental.Timeout-CancellationException: Timed out waiting for 2000 MILLISECONDS
at kotlinx.coroutines.experimental.Scheduledkt.rimeoutCancellation.Exception(scheduled.kt:185)
at
....

上述代码第①行调用 withTimeout 函数,设置超时时间为 2s,超过 2s抛出异常,需要执行的协程体放到winTimeout{...}中,wiimeout 也会创建并启动一个协程,但它返回的是Job 对象。
20.4.3 取消协程
协程体结束后,协程进入完成状态,协程就停止了。但是有些业务比较复杂,例如开发一个下载程序,每隔一段时间执行一次下载任务,下载任务一般会由子协程执行,目挂起一段时间再执行。这个下载子协程中会有一个死循环,但是为了能够停止子协程,可以调用 cancel 函数或 cancelAndJoin 函数取消协程。

Kotlin调用Java

在 Kotin 中调用 Java 函数式接口非常简洁,形式是“接口名{....}”
示例代码如下:

//Java ft码文件: chapter21/src/main/java/com/a5lwork6/section2/calculable.javpackage com.a5lwork6.section2;
//可计算接口
@FunctionalInterfacepublic interface Calculable {①
//计算两个 int 数值
int calculateInt(int a, int b);
}
//Kotlin代码文件:chapter21/src/main/kotlin/com/a5lwork6/section2/ch21.2.4.kt
package com.a5lwork6.section2
fun main(args: array<string>){
val nl = 10
val n2=5
//实现加法计算 calculable 对象
val fl= Calculable {a,b->a+b}②
//实现减法计算 calculable 对象
val f2 =Calculable {a,b ->a-b}③
//调用 calculateInt 函数进行加法计算
printin("Sn1 + $n2 = ${f1.calculateInt(n1, n2)}")④
//调用 calculateInt 函数进行减法计算
println("$nl - $n2 = ${f2.calculateInt(n1, n2)}") ⑤
}

上述代码第①行是声明一个函数式接口 Calculable,它只有一个抽象函数 calculateInt.代码第②行和第③行是在 Kotlin 中实现 Calculable 接口,并实例化它,其中的 Lambda 表达式{a,b>a+b }和{a,b->a-b}是对抽象函数 calculateInt 的实现。代码第④行和第⑤行是调用函数 calculatelnt。
21.3 Java 调用 Kotlin
21.3.1 访问 Kotlin 属性
Kotlin的一个属性对应Java中的一个私有字段、一个 setter 甬数和一个 getter 函数,如果是只读属性则没有 setter 函数。那么 Java 访间 Kotin 的属性是通过这些 getter函数和setter函数。
示例代码如下:

/kotlin 代码文件: chapter2l/src/main/kotlin/eom/a51work6/section3/user.kt
package com.a5lwork6.section3
data class User(var name: String, var password! $tring)①
java 代码文件: chapter21/src/main/java/com/a51work6/section3/ch21 3 1.java
package com.a5lwork6.section3;
public class Ch21 3 1 {
public static void main(string[] args) {
User user = new User("rom","12345");②
System.out,println(user.getName()); //Tom ③ 
user.setpassword("54321");④
System,out.printIn(user.getpassword());//54321 ⑤
}
}

上述代码第①行是声明 Kotlin 数据类,其中有两个属性,var 声明的属性会生成 setter 和getter函数,如果 val 声明的属性是只读的,只生成 getter 函数。代码第②行是实例化 User 对象,代码第③行是读取 name 属性,代码第④行是为属性password赋值,代码第⑤行是读取 password 属性。
21.3.2 访问包级别成员
在同一个 Kotlin 文件中,那些顶层属性和函数(包括顶层扩展属性和函数)都不隶属个类,但它们隶属于该 Kotlin 文件中定义的包。在 Java 中访问它们时,把它们当成静态成员。
示例代码如下:

//代码文件:chapter21/src/main/kotlin/com/a51work6/section3/s2/ch21.3.2.kt①
@file:JvmName("PackageLevelDemo" )②
package com.a51work6.section3.s2
//顶层函数
fun rectangleArea(width: Double, height; Double): Double (③
val area = width * height
return area
}
//顶层属性
val area =100.0 ④
//Java 代码文件:chapter21/src/main/java/com/a5lwork6/section3/ch21_3 2. java
package com.a5lwork6.section3;
//import com.a5lwork6.section3.s1.ch21_3_2kt;
import com.a5lwork6.section3.s1.PackageLevelDemo;
public class Ch21_3_2(
public static void main(string[] args) {
//访问顶层函数
//Double area = ch21 3 2Kt.rectangleArea(320.0,480.0);⑤
Double area = PackageLevelDemo.rectangleArea(320.0,480.0);⑥
System.out.println (area);
//访问顶层属性
//System.out.printIn(Ch21_3_2Kt,getArea());⑦
System.out.println(PackageLevelDemo.getArea());⑧
}
}

上述代码第①行~第④行是一个 Kotlin 源代码文件,文件名 ch21.3.2.kt 中声明了一个顶层函数(见代码第③行)和一个顶层属性(见代码第④行)。ch21.3.2.kt 文件编译之后生成 Ch21_3_2Kt.class 文件,因为点(.)字符不能构成 Java 类名,编译器会将其替换为下画线(_),所以在 Java 中访问 ch21.3.2.kt 对应的类名是 Ch21_3_2Kt,见代码第⑤行和第⑦行。
如果觉得 Ch21_3_2Kt这样的类名不友好,但还不想修改 Kotlin 源文件名,那么可以在 Kotin 源文件中使用@JvmName 注解,指定生成的文件名,见代码第②行的@file:JvmName("PackageLevelDemo"),其中 PackageLevelDemo 是生成之后的类名。那么在Java 中使用PackageLevelDemo 类名就可以访问 ch21.3.2.kt文件中的顶层函数和属性了,见代码第⑥行和第⑦行。
21.3.3 实例字段、静态字段和静态函数
Java 语言中所有的变量和函数都被封装到一个类中,类中包括实例函数、实例字段、静态字段和静态函数。Java 实例函数就是 Kotlin 类中声明的函数,而 Java 中的实体静态字段和静态函数,Kotlin 也是支持的。
注意
Java 中的字段在很多资料中被翻译为成员变量,而java 中的备数在很多资料中被翻译为方法,为了与Kotin 中的相关概念的译相同,本书中将Java 中的成员变量翻译为字段,java 中的方法翻译为函数。
1.实例字段

如果需要以 Java 实例字段形式(即:实例名.字段名)访问 Kotlin 中的属性,则需要

该属性前加@JvmFicld 注解,表明该属性被当作Java 中的字段使用,可见性相同。另外#巡初始化 (lateinit)属性在 Java 中被当作字段使用,可见性相同。示例代码如下:

//Kotlin代码文件:chapter21/src/main/kotlin/com/a51work6/section3/s3/person.kt
package com.a5lwork6.section3.s3
import java.util.*
class Person {

//名字
@JvmField
var name = "Tony" ①
/1年龄
var age = 18
//出生日期
lateinit var birthDate: Date ②
//Java代码文件:chapter21/src/main/java/com/a51work6/section3/Ch21_3_3.java
package com.a5lwork6.section3;
import com.a5lwork6.section3.s3.Person,

public class ch21_3_3 {
public static void main(string[] args) {
Person p = new Person();
  System.out.println(p.name);//Tony ③
System.out.println(p.birthpate); //null ④
}
}

上述代码第①行使用@JvmField 注解声明 name 属性,代码第②行声明延迟属性birthDate。代码第③行和代码第行是访问字段。
2.静态字段
如果需要以 Java 静态字段形式(即:类名,字段名)访问 Kotlin 中的属性,可以有两种实现方式:
(1)属性声明为顶层属性,Java中将所有的顶层成员(属性和函数)都认为是静态的具体访问方式在 21.3.2节已经介绍了,这里不再赘述;
(2)在 Kotlin 的声明对象和伴生对象中定义属性,这些属性需要使用@JvmField 注解lateinit 或 const 来修饰。

//代码件:chapter21/src/main/kotlin/com/a5lwork6/section3/s3/ch21.3.3.kt
@file:JvmName("staticFieldDemo") ①
package com.a5lwork6.section3.s3
import java.util.*
object Singleton { //singleton 声明对象
@JvmField
val x= 10  ②
lateinit var birthDate: Date
}
class Account { //Account 伴生对象
companion object {
const val interestRate=0.018 ④
}
}
const val MAX COUNT = 500 ⑤

//Java代码文件:chapter21/src/main/java/com/a51work6/section3/ch21
//访问静态字段
System.out.println(Singleton.x); //10 ⑥
Singleton.birthDate = new Date()
System.out.printIn(Account.interestRate);//0.018
System.out.printIn(StaticrieldDemo.MAX COUNT); //500 ⑦

上述代码第①行设置生成之后的文件名为 StaticFieldDemo。代码第②行是@JvmFieldま聲 Šingleon 对象的x属性。代码第⑧行是声明延迟属性 binhDac。代码第④行声明伴对象的inerestRate 属性是 const 常量类型。代码第⑤行是声明顶层常量 MAX_COUNT。
代码第⑥行~第⑦行是在 Java 中访问静态字段。
#######3. 静态函数
如果需要以 Java 静态函数形式(即:类名.函数名)访问 Kotin 中的函数,可以有两种实现方式:
(1)函数声明为顶层函数,这种访问方式在 21.3.2 节已经介绍了,这里不再赘述。
(2)在 Kotlin 的声明对象和伴生对象中定义函数,这些函数需要使用@JvmStatic 来
修饰。
示例代码如下:

//代码文件:chapter21/src/main/kotlin/com/a5lwork6/section3/s3/ch21.3.3.kt
@file:JvmName("staticFieldDemo" )
package com.a5lwork6.section3.s3

import java.util.*
object singleton { //singleton 声明对象
@JvmField
val x= 10
lateinit var birthDate: Date
@JvmStatic
fun displayX(){①
println (x)
}
}
class Account {
companion object{ //account 伴生对象
const val interestRate=0.018
@JvmStatic
②fun interestBy(amt: Double): Double {②
return interestRate * amt
}
}
}
const val MAX_COUNT = 500
//Java 代码文件:chapter21/sro/main/java/com/a51work6/section3/ch21_3_3. java
//访问静态函数
Singleton.displayX(); ③
Account.interestBy(5000);④

上述代码第①行@JvmStatie 注解 Singleton 对象中的 displayX 函数,代码第②行@JvmStatic 注解伴生对象中的 iinterestBy 函数。代码第③行和第④行是调用静态函数。
21.3.4 可见性
Java 和 Kotlin 都有4种可见性,但是除了 public 可完全兼容外,其他的可见性都是有所区别的。为了便于比较,首先介绍一下Java 可见性。Java 可见性有:私有、包私有、保护和公有。具体规则如表 21-5 所示。


image.png

将表 21-5 与 Kotlin 可见性修饰符使用规则表(见表 11-1)对照,可知 Kotlin 中没有默认包私有可见性,而 Java 中没有内部可见性。详细的解释说明如下。1)Kotlin 私有可见性
由于 Kotlin 私有可见性可以声明类中成员,也可以声明顶层成员。那么映射到 Java分为两种情况:
(1)Kotlin 类中私有成员映射到 Java 类中私有实例成员:

(2)Kotlin 中私有顶层成员映射到 Java 中私有静态成员
2)Kotlin 内部可见性
由于 Java 中没有内部可见性,那么 Kotlin 内部可见性映射为 Java 公有可见性
3)Kotlin 保护可见性
Kotlin 保护可见性映射为 Java 保护可见性
4)Kotlin 公有可见性
Kotlin 公有可见性映射为 Java 公有可见性。
注 意
Kotlin 中内部可见性类成员会生成比较复杂的函数名字,在IDE工具中存在这个函数语法,但是编译无法通过。这源自于 Kotlin内部可见性与 Java 可见性的兼容问题,事实上目前在 Kotlin的这个版本上 Java 不能访问 Kotlin 内部可见性类成员,但可以访问内部可见性的类和内部可见性的顶层成员。

21.3.5 生成重载函数
Kotlin 的函数参数可以设置默认值,看起来像多个函数重载一样。但 Java 中并不支持参数默认值,只能支持全部参数函数。为了解决这个问题,可以在 Kotlin 函数前使用@JvmOverloads 注解,Kotlin 编译器会生成多个重载函数。@JvmOverloads 注解的函数可以是构造函数、成员函数和顶层函数,但不能是抽象函数。
示例代码如下:

IO
image.png

所有的输入形式都抽象为输入流,所有的输出形式都抽象为输出流,它们与设备无关。
22.1.2 Java 流类继承层次
以字节为单位的流称为字节流,以字符为单位的流称为字符流。Java 提供4个顶层抽,两个字节流抽象类:InputStream 和 OutputStream;两个字符流抽象类:Reader 和Writer .
1.字节输入流
字节输入流的根类是 InputStream,如图 22-2 所示,它有很多子类,这些类的说明如表22-1 所示。


image.png

2.字节输出流
字节输出流的根类是OupuSuream,如图 22-3 所示,它有很多子类,这些类的说明如表 22-2 所示。


image.png
3.字符输入流
字符输入流的根类是Reader,这类流以16位的Unicode编码表示的字符为基本处理单位。如图 22-4 所示,它有很多子类,这些类的说明如表 22-3 所示。
image.png
image.png
4.字符输出流
字符输出流的根类是 Writer,这类流以 16 位的 Unicode编码表示的字符为基本处理单。如图 22-5 所示,它有很多子类,这些类的说明如表 22-4所示。
image.png

22.2字节流
要想掌握字节流的 API,首先要熟悉它的两个抽象类:InputSteam 和OuputStream,并了解它们有哪些主要的函数。
22.2.1 InputStream 抽象类
[npusuream是字节输入流的根类,影响着字节输入流的行为,Kotlin为Iputstream类定义了很多扩展函数和属性,下面主要介绍这些扩展函数。
(1)返回字节缓冲区输入流:

fun InputStream.buffered(bufferSize:Int=DEFAULT BUFFER SIZE //缓存区大小):BufferedInputStream

(2)返回字符缓冲区输入流,charset是字符集,默认是 UTF-8:

fun InputStream.bufferedReader(
charset:Charset =Charsets.UTF 8 //字符集):BufferedReader

(3)从输入流中复制数据到输出流,返回复制的字节数:

fun Inputstream.copyTo (out: OutputStream,
bufferSize: Int = DEFAULT BUFFER SIZE):Long

(4)将字节输入流转换为字符输入流 InputStreamReader,charset 是字符集,默认是UTF-8:

fun InputStream.reader(
charset:Charset= Charsets.UTr 8):InputStreamReader

22.2.2 OutputStream 抽象类
OutputStream 是字节输出流的根类,影响着字节输出流的行为。Kotlin 为 OutputStream类定义了很多扩展函数和属性,下面主要介绍这些扩展函数。
(1)返回字节缓冲区输出流:

fun OutputStream.buffered(
bufferSize: Int = DEFAULT BUFFER SIZE
):BufferedOutputStream

(2)返回字符缓冲区输出流,charset 是字符集,默认是 UTF-8.

fun OutputStream.bufferedWriter(
charset:Charset=Charsets.UTF 8
):BufferedWriter

22.2.3 案例:文件复制
前面介绍了 Kodin 中字节流的常用扩展函数和属性,下面通过一个案例熟悉一下它。用。该案例实现了文件复制,数据源是文件,所以用到了文件输人流FileInputStream,数据目的地也是文件,所以用到了文件输出流 FileOutputStream.FlelnputStream 和 FileOutputStream 都属于底层流,在实际开发时为了提高效率可!缓冲流BuferedInputStream 和 BuferedOutputStream,使用字节缓冲流后就内置了二区,第一次调用 read 函数时尽可能多地从数据源读取数据到缓冲区,后续再用 read数时先观察缓冲区中是否有数据,如果有则读取缓冲区中的数据,如果没有再将数据中的数据读入到缓冲区,这样可以减少直接读取数据源的次数。通过输出流调用 write写入数据时,也先将数据写入到缓冲区,缓冲区满了之后再写入数据目的地,这样可减少直接对数据目的地的写入次数。使用了缓冲字节流可以减少 I/O 操作次数,提效率。

//代码文件:chapter22/src/com/a51work6/section2/ch22.2.3.kt
package com.a5lwork6.section2
import java.io.FileInputStream
import java.io.FileOutputStream
fun main(args: Array<String>){

FileInputstream("./restDir/src.zip").use { fis ->
FileOutputstream("./TestDir/subDir/src,zip").use { fos ->
//创建字节缓冲输入流
val bis = fis.buffered()//创建字节缓冲输出流
val bos = fos.buffered()
//复制到输出流
bis.copyTo (bos)
Println("复制完成")
}
}
}

字符流22.3
上一节介绍了字节流,本节详细介绍一下字符流的 API。要想掌握字符流的 API,首先要熟悉它的两个抽象类:Reader 和 Writer,并了解它们有哪些主要的函数。
22.3.1 Reader 抽象类
Reader 是字符输入流的根类,它定义了很多函数,影响着字符输入流的行为。Kotlin为 Reader 类定义了很多扩展函数和属性,下面主要介绍这些扩展函数。

(1)返回字符缓冲区输入流:

fun keader.buffered(
bufferSize: Int = DEFAULT BUFFER SIZE):BufferedReader

(2)从输入流中复制数据到输出流,返回复制的字符数:

fun Reader.copyTo(
out: Writer,
buffersize: Int = DEFAULT BUFFER SIZE):Jong

(3)遍历输入流中的每一行数据,对每一行数据进行处理,完成之后关闭流:

fun Reader,forEachLine(action:(String) -> Unit)

(4)读取输入流中的数据到一个 List 集合,每一个行数据是一个元素,完成之后关闭流:

fun Reader .readlines(): List<string>

(5)读取输入流中的数据到字符串中:

fun Reader,readrext(): string

22.3.2 Writer 抽象类
Writer 是字符输出流的根类,它定义了很多函数,影响着字符输出流的行为。

fun writer.buffered(
buffersize: Int = DEFAULT BUFFER SIZE
):BufferedWriter

22.3.3 案例:文件复制
前面两节介绍了字符流常用的函数,下面通过一个案例熟悉一下它们的使用。该案实现了文件复制,数据源是文件,所以用到了文件输入流 FileReader,数据目的地也是文件,所以用到了文件输出流 FileWriter。
FileReader 和 FileWriter 都属于底层流,在实际开发时为了提高效率可以使用缓冲流BuferedReader 和 BufferedWriter.
下面通过文本文件复制的案例介绍一下如何使用字符流,该案例是将当前项目下TestDir 目录中的 JButtonGroup.html 文件复制到 TestDir 下的 subDir 目录中。代码如下:

//代码文件:chapter22/src/com/a51work6/section3/ch22.3.3.kt
package com.a5lwork6.section3
import java.io.FileReader
import java.io.FileWriter
fun main(args: Array<string>){
FileReader("./TestDir/JButtonGroup.html").use { fis ->
FileWriter("./restDir/subDir/JButtonGroup.html").use { fos ->
//创建字符缓冲输入流
val bis = fis.buffered()
//创建字符缓冲输出流
val bos = fos.buffered()
//复制到输出流
bis .copyTo (bos)
println("复制完成")
}
}
}

上述代码与 22.2.3 节的代码非常相似,只是将文件输入流改为 FileReader,文件输出【改为Filewriter,还将文件换成了文本文件。

22.4 文件管理
在 Kotlin 中如果只是对文件进行操作,可以不直接使用文件流。Kotlin 在 Java 文件类 File 的基础上增加了很多扩展函数和属性,对字符串的操作变得非常简单。

22.4.1 File 类扩展函数
File 类可以表示一个文件也可以表示一个目录。Kotlin 提供的 File 扩展函数和属性有很多,这里重点介绍几个常用的函数。
(1)读取文件全部内容,返回字节数组:

fun File.readBytes(): ByteArray

(2)读取文件全部内容,返回字符串,所以只能是文本文件,默认字符是 UTF-8:

fun File.readText(charset: Charset = Charsets.UrF_8): String

(3)写入字节数组到文件中:

fun File.writeBytes(array: ByteArray)

(4)写入字符串到文件,只能是文本文件,默认字符是 UTF-8:

fun File.writeText(text: String,charset:Charset =Charsets.UTF_8)

(5)遍历文件中每一行数据,对每一行数据进行处理,只能是文本文件:

fun File.forEachLine(charset: Charset = Charsets.UTF_8,action:(line: String)-> Unit)

(6)读取文件中的数据到一个List集合,每一个行数据是一个元素,只能是文本文件:

fun File.readLines(charset: Charset = Charsets.UTF_8): List<String>

(7)复制到目标文件,target参数是目标文件,overwrite 参数选择是否覆盖目标文件:

fun File.copyTo (
target: File,
overwrite: Boolean = false,
bufferSize: Int = DEFAULT_BUFFER_SIZE): File

(8)遍历文件目录和内容,direction 是遍历的方向:

fun File.walk(
direction: FilewalkDirection = FilewalkDirection.TOP_DOWN):FileTreeWalk

(9)按自下而上的顺序遍历文件目录和内容:

fun rile.walkBottomUp(): FileTreeWalk

(10)按自上而下的顺序遍历文件目录和内容:

fun rile.walkTopDown(): FileTreeWalk

22.4.2 案例:读取目录文件

//代码文件:chapter22/src/com/a51work6/section4/ch22.4.2.kt
package com.a5lwork6.section4.
import java.io.File

fun main(args: Array<String>) {
File("./TestDir/")
.walk()
.filter ( it.isFile }
.filter l it.extension == "html" }
.forEach {
printin (it)
}
Socket
image.png

服务器端监听某个端口是否有连接请求,此时服务器端程序处于阻塞状态,直到客户端向服务器端发出连接请求,服务器端接收客户端请求,服务器会响应请求并处理请求,然后将结果应答给客户端,这样就会建立连接。一旦连接建立起来,通过 Socket 可以获得输入输出流对象。借助于输入输出流对象就可以实现服务器与客户端的通信,最后不要忘记关闭 Socket 和释放一些资源(包括关闭输入输出流)。
23.2.3 Socket类
java.net 包为TCP Socket 编程提供了两个核心类:Socket 和 ServerSocket,分别用来表示双向连接的客户端和服务器端。
本节先介绍一下 Socket 类,Socket 类常用的构造函数有:
Socket(adress: InetAddress!, por: Int):创建 Socket 对象,并指定远程主机卫地址
和端口号;
Socket(address: InetAddress!, port: Int, localAddr: InetAddress!, localport: Int:创建Socket 对象,并指定远程主机 IP 地址、端口号以及本机的 ⅢP 地址(localAddr)和端口号(localPort);
Socket(host: String!,port: Int):创建 Socket 对象,并指定远程主机名和端口号,I地址为 null,null 表示回送地址,即 127.0.0.1;
Socket(host: String!, port: Int, localAddr: InetAddress!, localPort: Int):创建Socket 对象,并指定远程主机、端口号以及本机的IP地址(localAddr)和端口号(localPort)。bost为主机名,P 地址为 null,null 表示回送地址,即 127.0.0.1。
提示
本书中“数据类型!”表示“平台类型”,String!表示 String 或者String?。平台类型在第 21 章已经介绍过了。
Socket 其他的常用函数和属性有:
getInputStream0)函数:通过此 Socket 返回输入流对象
getOutputStream()函数:通过此 Socket 返回输出流对象
port:Int 属性:返回 Socket 连接到的远程端口。
localPort 属性:返回 Socket 绑定到的本地端口。
inetAddress 属性:返回 Socket 连接的地址。
localAddress 属性:返回 Socket 绑定的本地地址。
isClosed 属性:判断返回 Socket 是否处于关闭状态。
isConnected 属性:判断返回 Socket 是否处于连接状态
close0)函数:关闭 Socket。
注意
Socket 与流所占用的资源类似,不能通过 Java 虚拟机的垃圾收集器回收,需要程序员释放。释放的方法有两种,一种是可以在 finally 代码块调用 close()函数关闭 Socket,释放流所占用的资源。另一种是通过自动资源管理技术释放资源,Socket和 ServerSocket 都实现了 AutoCloseable 接口,所以 Kotlin中可以使用 use 函数。
####### 23.2.4 ServerSocket类
ServerSocket 类常用的构造函数有:
ServerSocket(port: Int,maxQueue: Int)。创建绑定到特定端口的服务器 Socket。maxQueue 设置连接请求的最大队列长度,如果队列满时,则拒绝该连接。默认值是 50。
ServerSocket(port: Int)。创建绑定到特定端口的服务器 Socket。连接请求的最大队列长度是 50。
ServerSocket 其他的常用函数和属性有:
。getInputStream()函数:通过此 Socket 返回输入流对象。
getOutputStream()函数:通过此 Socket 返回输出流对象
。isClosed 属性:返回 Socket 是否处于关闭状态。
。isConnected 属性:返回 Socket 是否处于连接状态。
。accept0)函数:侦听并接收到 Socket 的连接。此函数在建立连接之前一直是阻塞状态。
ServerSocket 类本身不能直接获得 I/O 流对象,而是通过 accept()函数返回 Socket 对象,通过 Socket 对象取得 I/O 流对象,进行网络通信。此外,ServerSocket 也实现了AutoCloseable接口,通过自动资源管理技术关闭 ServerSocket。
23.2.5 案例:文件上传工具

基于 TCP Socket 的编程比较复杂,先从一个简单的文件上传工具案例介绍 TCP Socket编程的基本流程。上传过程是一个单向 Socket 通信过程,如图 23-5 所示,客户端通过文件输入流读取文件,然后从 Socket 获得输出流写入数据,写入数据完成则上传成功,客户端任务完成。服务器端从 Socket 获得输入流,然后写入文件输出流,写入数据完成则上传成功,服务器端任务完成。


image.png
com.a5lwork6.section2
import java.io.BufferedInputstream
import java.io.FileOutputstream
import java.net.Serversocket
fun main (args: Array<string>){
println("服务器端运行...")
ServerSocket(8080).use { server ->
server.accept().use { socket ->
BufferedInputstream(socket.getInputstream()).use { sin ->FileOutputstream("./TestDir/subDir/coco2dxcplus.jpg").use{ fout ->
sin.copyTo(fout)
print1n("接收完成!")
}
}
}
}
}

提示
由于当前线程是主线程,所以 server.accept()会阻塞主线程阻塞主线程是不明智的,如果是在一个图形界面的应用程序阻塞主线程会导致无法进行任何的界面操作,就是常见的“卡现象,所以最好是把 server.accept()语句放到子线程中。

import java.net.Socket
fun main(args: Array<string>) {
println("客户端运行...")
Socket("127.0.0.1",8080).use { socket ->
BufferedoutputStream(socket.getoutputstream()).use { sout ->FileInput  Stream("./TestDir/coco2dxcplus.jpg").use { fin ->fin.copyTo (sout)println("上传成功!")
}
}
}
}

上述代码第①行的 Socket("127.0.0.1",8080)是创建 Socket,指定远程主机的 IP 地址和端口号。代码第②行的 socket.getOutputStream()是从 socket 对象获得输出流。
23.3 UDP Socket 低层次网络编程
UDP(用户数据报协议)就像日常生活中的邮件投递,不能保证
可靠地寄到目的地。UDP 是无连接的,对系统资源的要求较少,UDP
可能丢包且不保证数据顺序。但是对于网络游戏和在线视频等要求传
输快、实时性高、质量可稍差一点的数据传输,UDP 还是非常不错的。UDP Socket 网络编程比 TCP Socket 编程简单得多,UDP 是无连接协议,不需要像 TCP一样监听端口且建立连接,才能进行通信。

23.3.1 DatagramSocket 类
java.net 包中提供了两个类 DatagramSocket 和 DatagramPacket,它们用来支持UDP 迪信。这一节先介绍一下 DatagramSocket 类,DatagramSocket 用于在程序之间建立传送数排报的通信连接。

先来看一下 DatagramSocket 常用的构造函数:
DatagramSocket()。创建数据报 DatagramSocket 对象,并将其绑定到本地主机上体任何可用的端口。
DatagramSocket(port: Int)。创建数据报 DatagramSocket 对象,并将其绑定到本地主机上的指定端口。
, palagramSocket(port: Int, laddr: InetAddress!)。创建数据报 DatagramSocket 对象,并将其绑定到指定的本地地址。
DatagramSocket 其他的常用函数和属性有:
. send(p: DatagramPacket!)。发送数据报包
receive(p: DatagramPacket!)。接收数据报包
port属性。返回 DatagramSocket 连接到的远程端口。
。localPort 属性。返回 DatagramSocket绑定到的本地端口。
。inetAddress 属性。返回 DatagramSocket 连接的地址。
,localAddress 属性。返回 DatagramSocket 绑定的本地地址
,isClosed 属性。返回 DatagramSocket 是否处于关闭状态。
。val isConnected:Boolean 属性。返回 DatagramSocket 是否处于连接状态。
。close0函数。关闭 Socket。
DatagramSocket 也实现了 AutoCloseable 接口,通过自动资源管理技术关闭DatagramSocket.
23.3.2 DatagramPacket类
DatagramPacket 用来表示数据报包,是数据传输的载体。DatagramPacket 实现无连接数据包投递服务,投递数据包仅根据该包中的信息从一台机器的路由到另一台机器的路由从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达,
不保证包都能到达目的地。下面看一下 DatagramPacket 的构造函数:DatagramPacket(buf: ByteArray!,length: Int)。构造数据报包,其中 buf是包数据。length 是接收包数据的长度。DatagramPacket(buf; ByteArray!, length: Int, address: InetAddress!, port: Int).构造数报包,包发送到指定主机上的指定端口号。
DatagramPacket(buf: ByteArray!,offset: Int, length: Int)。构造数据报包,其中 offset是 buf字节数组的偏移量。
DatagramPacket(buf: ByteArray!, offset: Int, length: Int, address: InetAddress!, portInt)。构造数据报包,包发送到指定主机上的指定端口号。
DatagramPacket 常用属性如下:。address。返回发往或接收该数据报包相关的主机IP 地址,属性类型是 InetAddress
data。返回数据报包中的数据,属性类型是 ByteArray。
length。返回发送或接收到的数据的长度,属性类型是 Int.
ofset。返回发送或接收到的数据的偏移量,属性类型是 Int。
。port。返回发往或接收该数据报包相关的主机的端口号,属性类型是Int.

23.3.3 案例:文件上传工具
使用 UDP Socket将 23.2.5 节的文件上传工具重新实现一下。案例服务器端 UploadServer 代码如下:

//代码文件:chapter23/src/com/a51work6/section3/Uploadserver.kt
package com.a5lwork6.section3 
fun main(args: array<string>){
printin("服务器端运行...")
DatagramSocket(8080).use { socket ->FileOutputStream("./restDir/subDir/coco2dxcplus .jpg") .use f fout-BufferedOutputStream(fout).use { out ->

//准备一个缓冲区
val buffer = ByteArray(1024)
//循环接收数据报包
while (true) {
//创建数据报包对象,用来接收数据
val packet = DatagramPacket (buffer, buffer.size)
//接收数据报包
socket.receive(packet)
//接收数据长度
val len = packet.length
if(len == 3){
//获得结束标志
val flag = string(buffer, 0,3)
//判断结束标志,如果是 bye 则结束接收
if (flag =- "bye"){
break
}
}
//写入数据到文件输出流
out.write(buffer,0, len)
}
println("接收完成!")
}

与TCPSocket 不同,UDP Socket 无法知道哪些数据包是最后一个,因此需要发送一个特殊的数据包,包中包含了一些特殊标志。代码第③行~第④行是取出并判断这个
标志。案例客户端 UploadClient 代码如下:

1/代码文件:chapter23/src/com/a5lwork6/section3/uploaaclient.kt
package com.a5lwork6.section3
fun main(args: Array<string>){
println("客户端运行...")

DatagramSocket().use { socket ->
FileInputStream("./TestDir/coco2dxcplus.jpg").use { fin ->BufferedInputStream(fin).use { input ->
//创建远程主机 IP 地址对象
val address = InetAddress.getByName("localhost")
//准备一个缓冲区
val buffer = ByteArray(1024)
//首次从文件流中读取数据
var len = input.read(buffer)
while(len!=-1){
//创建数据报包对象
val packet = DatagramPacket (buffer, len, address,8080)
//发送数据报包
socket.send(packet)
//再次从文件流中读取数据
len = input.read(buffer)
}
//创建数据报对象
val packet=DatagramPacket ("bye".toByteArray(),3, address,8080)
//发送结束标志
socket.send (packet)
println("上传完成!")
}
}
}
}

23.4.2 使用第三方 JSON 库

于目前Kotlin 官方没有提供 JSON 编码和解码所需要的类库,所以需要使用第三方JSON库,笔者推荐Klaxon库,Klaxon库全部是由Kotlin代码编写的,最重要的是不是依赖其他第三方库,支持 Gradle配置很容易添加到现有项目中。读者很容易在https://github.com/.cbeust/klaxon 查看帮助文档和下载源代码。
添加Klaxon 库到现有项县中、这需婴创建 lntcll] IDEA+Gradle 项目,项目创建完成后再打开build.gradle 文件,修改文件内容如下;

group 'com.5lwork6.
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version ='1.1.51'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:skotlin version"
}
}
apply plugin: 'java"
apply plugin: "kotlin'
sourceCompatibility= 1.8
repositories {
mavenCentral()
jcenter()
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile 'com.beust:klaxon: 2.0.0'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
23.5 访问互联网资源“

Kotlin 可以通过使用 Java 的 java.net.URL 类进行高层次网络编程,通过 URL类访问互联网资源。使用 URL 进行网络编程不需要对协议本身有太多的了解,相对而言是比较简单的。
23.5.1 URL的概念
互联网资源是通过URL指定的,URL是 Uniform ResourceLocator 的简称,翻译过来是“一致资源定位器”,但人们都习惯 URL这个简称。
URL 组成格式如下:

协议名://资源名

“协议名”指明获取资源所使用的传输协议,如 http、fp、gopher 和 fle 等,“资源名”则应该是资源的完整地址,包括主机名、端口号、文件名或文件内部的一个引用。
######## 23.5.4 使用 HttpURLConnection 发送 GET 请求
由于 URL 类只能发送 HTTP/HTTPS 的 GET 函数请求,如果想要发送其他的情况或者对网络请求有更深入的控制时,可以使用 HttpURLConnection 类型。示例代码如下:

//代码文件:chapter23/src/main/kotlin/com/a5lwork6/section5/ch23.5.4.kt
package com.a5lwork6.section5
import java.net.HttpURLConnection
import java.net.URL
//web 服务网址
private val urlString="http://www.5lwork6.com/service/mynotes/Webservicephp?"+
"email=<换成你在 51work6.com注册时填写的邮箱>stype=JSoN&action=query"
fun main(args: Array<String>){
var conn: HttpURLConnection?=null
try {
conn = URL (urlString).openConnection() as HttpURLconnection 
conn.connect()
conn.inputStream.use { input ->
val data = input.bufferedReader() .readText()
printin (data)
}
} catch (e: Exception) 
e.printStackTrace()
I finally {
conn?.disconnect()
}
}

提示
发送 GET 请求时发送给服务器的参数是放在 URL 的“?”之后,参数采用键值对形式,例如:第①行的 URL 中 type=JSON是一个参数,type 是参数名,JSON是参数名,服务器端会根据参数名获得参数值。多个参数之间用“&”分隔,例如 typeJSON&action=query 就是两个参数。
23.5.5 使用 HttpURLConnection 发送 POST 请求
HttpURLConnection 也可以发送 HTTP/HTTPS 的 POST 请求,下面介绍如何使用HttpURLConnection 发送 POST 请求
示例代码如下:

//代码文件:chapter23/src/main/kotlin/com/a5lwork6/section5/ch23.5.5.kt
package com.a5lwork6.section5
import java.io.DataOutputStream
import java.net.HttpURLConnection
import java.net.URL
private val urlString="http://www.5lwork6.com/service/mynotes/webServicephp"
fun main(args: Array<String>){
var conn: HttpURLConnection?= null
try {
conn = URL(urlString).openConnection() as HttpURLConnection
conn.requestMethod ="POST" //POST 请求
conn.doOutput = true

//POST 请求参数
val param = String.format("email=%s&type=%s&action=%s"
"tonyteste5lwork6.com","JSON", "query")④
//设置参数
DataOutputStream(conn.outputStream).use { dstream ->dStream.writeBytes(param)
}
conn.connect()
conn.inputStream.use { input ->
val data = input.bufferedReader() .readText ()
println(data)
}
}catch (e: Exception){
e.printstackrrace()
}finally {
conn?.disconnect()
}
}
Kotin 与 Java swing 图形用户界面编程

布局管理器(LayoutManager)。下面将围绕这些概念展开。

Swing 类层次结构

容器和组件构成了 Swing 的主要内容,下面分别介绍一下 Swing 中的容器和组件类层结构。图24-3是Swing 容器类层次结构,Swing 容器类主要有JWindow、JFrame 和JDialog,其他不带 “J”开头的都是 AWT 提供的类,在 Swing 中大部分类都是以 “J”开头。


image.png

图24-4 是Swing 组件类层次结构,Swing所有组件继承自JComponent,JComponen(间接继承自AWT 的java.awt.Component 类。Swing 组件很多,这里不一一解释了,在后面学习过程中会重点介绍一些组件。


image.png
//Kotlin代码文件:chapter24/src/main/kotlin/com/a5lwork6/seetion2/swingDemol.kt
package com.a5lwork6.section2
import javax.swing.JFrame
import javax.swing.JLabel
fun main(args: Array<string>){
//创建窗口对象
val frame = JFrame("MyFrame" )

//创建 Label
val label = ulabel("Hello Swing! ")
//获得窗口的内容面板
val pane = frame.contentPane
//添加 Labe1 到内容面板
pane.add (label)
//设置窗口大小
frame.setsize(300, 300)
//设置窗口可见
frame.isVisible = true

上述代码第①行使用 JFrame 的 JFrame(title: String!)构造函数创建 JFrame 对象,title是设置创建的标题。默认情况下 JFrame 是没有大小且不可见的,因此创建JFrame 对象后还需要设置大小和可见,代码第⑤行是设置窗口大小,代码第行是设置窗口的可见。
创建好窗口后,就需要将其中的组件添加进来,代码第②行是创建标签对象,构造函数中的字符串参数是标签要显示的文本。创建好组件之后需要把它添加到窗口的内容面板上,代码第③行是获得窗口的内容面板,它是 Container 容器类型。代码第④行调用容器的add 函数将组件添加到窗口上。


image.png
image.png

2.继承 JFrame 方式
继承 JFrame 方式就是编写一个继承 JFrame 的子类,在构造函数中初始化窗口,添加窗口所需要的组件。
自定义窗口代码如下:

//Kotlin代码文件:chapter24/src/main/kotlin/com/a51work6/section2/MyFrame.kt
package com.a5lwork6.section2
import javax.swing.JLabel
import javax.swing.JFrame

class MyFrame(title: String): JFrame(title){
init {
//创建 Labe1
val label = JLabel("Hello swing!")
//获得窗口的内容面板
val pane = contentPane
//添加 Labe1 到内容面板
pane.add(label)
//设置窗口大小

setsize(300, 300)
//设置窗口可见
isVisible = true
}
}

提示
创建 JFrame 方式适合于小项目、代码量少、窗口不多、组件少的情况。继承 JFrame 的方式适合于大项目,可以针对不同界面自定义一个 Frame 类,属性可以在构造函数中进行设置;缺点是需要有效地管理很多类文件。
24.3 事件处理模型
图形界面的组件要响应用户操作,就必须添加事件处理机制。Swing采用 AWT的事件处理模型进行事件处理。在事件处理的过程中
涉及三个要素:
(1)事件。是用户对界面的操作,在Java 中事件被封装称为事件类java.awt.AWTEvent
及其子类,例如按钮单击事件类是java.awt.event.ActionEvente
(2)事件源。是事件发生的场所,就是各个组件,例如按钮单击事件的事件源是按钮(Button).
(3)事件处理者。是事件处理程序,在Java 中事件处理者是实现特定接口的事件对象。在事件处理模型中最重要的是事件处理者,它根据事件(假设XXXEvent 事件)的不同会实现不同的接口,这些接口命名为XXXListener,所以事件处理者也称为事件监听智最后事件源通过 addxxXListener 函数添加事件监听,监听 XXXEvent 事件。各种事件和对应的监听器接口如表 24-1 所示。


image.png

同会实现不同的接口,这些接口命名为XXXListener,所以事件处理者也称为事件监听智最后事件源通过 addxxXListener 函数添加事件监听,监听 XXXEvent 事件。各种事件和对应的监听器接口如表 24-1 所示。

事件处理者可以是实现了 XXXListener 接口的任何形式,即一般类、内部类、对象表达式和 Lambda 表达式等。如果 XXXListener 接口只有一个抽象函数,事件处理者还可以是Lambda 表达式。为了方便访问窗口中的组件,往往使用内部类、对象表达式和 Lambda表达式。
24.3.1 内部类和对象表达式处理事件
内部类和对象表达式能够方便地访问窗口中的组件,本节先介绍内部类和对象表达实现的事件监听器

24.3.3 使用适配器
事件监听器都是接口,在 Kodin 接口中定义的抽象函数必须全部实现,哪怕对某些丽(#不关心,也要给一对空的大括号表示实现。例如 WindowListener 是窗口事件(WindowEvent)监听器接口,为了在窗口中接收到窗口事件,需要在窗口中注册WindowListener事件监听器,

this.addWindowListener(object : WindowListener {
override fun windowActivated(e: WindowEvent){
override fun windowClosed(e: WindowEvent){}
override fun windowClosing(e: WindowEvent){//退出系统
System.exit(0)
override fun windowDeactivated(e: WindowEvent){}
override fun windowDeiconified(e: windowEvent){}
override fun windowIconified(e: WindowEvent){}
override fun windowOpened(e:WindowEvent){}
})

实现 WindowListener 接口需要实现提供它的7个函数,很多情况下只需要实现其中两个函数,本例是关闭窗口只需要实现代码第①行的 windowClosing 函数,其他的函数并不关心,但是也必须给出空的实现。这样的代码看起来很臃肿,为此 AWT 提供了一些与监听器相配套的适配器。监听器是接口,命名采用 XXXListener,而适配器是类,命名采用 XXX Adapter。在使用时通过继承事件所对应的适配器类覆盖所需要的函数,无关函数不需要实现。
采用适配器注册接收窗口事件代码如下:

this.addwindowListener(object :WindowAdapter(){override fun windowClosing(e: WindowEvent){//退出系统
System.exit(0)
}
})

可见上述代码非常简洁。事件适配器提供了一种简单的实现监听器的手段,可以缩短程序代码。但是,由于 Kotlin 的单一继承机制,当需要多种监听器或此类已有父类时,就无法采用事件适配器了。
并非所有的监听器接口都有对应的适配器类,一般定义了多个函数的监听器接口,例如 WindowListener 有多个函数对应多种不同的窗口事件时,才需要配套的适配器,主要的适配器如下:
。ComponentAdapter。组件适配器
。ContainerAdapter。容器适配器
。FocusAdapter。焦点适配器。
。KeyAdapter。键盘适配器。
MouseAdapter。鼠标适配器。
。MouseMotionAdapter。鼠标运动适配器。
WindowAdapter。窗口适配器

24.4布局管理

在Swing 图形用户界面中,容器内的所有组件布局都是由布局管理器管理的。布局管理器负责组件的排列顺序、大小、位置以及当窗口移动或调整大小后组件如何变化等。

Swing提供了7种布局管理器,包括FlowLayout、BorderLayout,GridLayout、BoxLayout,CardLayout、SpringLayout 和GridBagLayout,其中最基础的是 FlowLayout、BorderLayou、GridLayout布局管理器。下面重点介绍这三种布局。
24.4.1 FlowLayout 布局
FlowLayout布局摆放组件的规律是:从上到下、从左到右进行摆放组件,如果容器足宽,第一个组件先添加到容器中第一行的最左边,后续的组件依次添加到上一个组件的右边,如果当前行已摆放不下该组件,则摆放到下一行的最左边。FlowLayout主要的构造函数如下:
FlowLayout(align: Int,hgap: Int,vgap:Int)。创建一个 FlowLayout 对象,它具有指定的对齐方式及指定的水平和垂直间隙,hgap 参数是组件之间的水平间隙,vgap 参数是组件之间的垂直间隙,单位是像素。
FlowLayout(align:Int)。创建一个 FlowLayout 对象,指定的对齐方式,默认的水平和垂直间隙是5个像素。
FlowLayout()。创建一个 FlowLayout 对象,它是居中对齐的,默认的水平和垂直间隙是5个像素。
上述参数align是对齐方式,它是通过FlowLayout的常量指定的,这些常量说明如下:FlowLayout.CENTER。指示每一行组件都应该是居中的。
FlowLayout.LEADING。指示每一行组件都应该与容器方向的开始边对齐,例如
对于从左到右的方向,则与左边对齐。
·FlowLayout.LEFT。指示每一行组件都应该是左对齐的。
。FlowLayout.RIGHT。指示每一行组件都应该是右对齐的。HowLayou.TRAILING。指示每行组件都应该与容器方向的结束边对齐,例如,对于从左到右的方向,则与右边对齐。


image.png

24.4.2 BorderLayout 布局
BorderLayout 布局是窗口的默认布局管理器,24.3 节的示例就是采用 BorderLayout 布局实现的。

BorderLayout 是JWindow、JFrame 和 JDialog 的默认布局管理器。BorderLayout 布局管理器把容器分成5个区域:北、、东、西和中。BorderLayout 布局如图 24-10 所示,每个区域只能放置一个组件。

BorderLayout 主要的构造函数如下:
·BorderLayout( hgap:Int,vgap:Int)。创建一个BorderLayout 对象,指定水平和垂直间隙,hgap 参数是组件之间的水平间隙,vgap 参数是组件之间的垂直间隙,单位是像素。
BorderLayout()。创建一个 BorderLayout 对象,组件之间没有间隙。BorderLayout 布局有5个区域,为此 BorderLayout 中定义了5个约束常量,
说明如下:·BorderLayout.CENTER。中间区域的布局约束(容器中央)。
BorderLayout.EAST。东区域的布局约束(容器右边)。
BorderLayout.NORTH。北区域的布局约束(容器顶部)
BorderLayout.SOUTH。南区域的布局约束(容器底部)。
BorderLayout.WEST。西区域的布局约束(容器左边)。


image.png

24.4.3 GridLayout 布局
GidLayout 布局以网格形式对组件进行摆放,容器被分成大小相等的矩形,一个矩形放置一个组件。
GridLayout布局主要的构造函数如下:
。GridLayout():创建具有默认值的 GridLayout 对象,即每个组件占据一行一列。
。GridLayout(rows: Int, cols: Int):创建具有指定行数和列数的 GridLayout 对象。。GridLayout(rows:Int,cols:Int,hgap:Int, vgap:Int):创建具有指定行数和列数的GridLayout 对象,并指定水平和垂直间隙。


image.png

24.4.4 不使用布局管理器
如果要开发的图形用户界面应用不考虑跨平台,也不考虑动态布局,窗口大小不变的,狐么布局管理器就失去了意义。容器也可以不设置布局管理器,那么此时的布局是由开发员自己管理的。
组件有三个与布局有关的函数 setLocation、setSize 和 setBounds。在设置了布局管理的容器中,这几个函数是不起作用的,不设置布局管理时它们才会起作用。
这三个函数的说明如下:
setLocation(x:Int,y:Int)函数是设置组件的位置;
setSize(width: Int, height: Int)函数是设置组件的大小;
setBounds(x: Int, y: Int ,width: Int, height: In)函数是设置组件的大小和位置。


image.png
//kotlin(00y0chapter2a/sre/ma1n/kot1in/eom/a51work6/8e3t1on2/53/u
package com.a5iworks,seetion4,s4
import javax.awing.JButton
import javax,awing .JFrame
import javax,awing.JLabel
import javax.swing.SwingConstants
class Myframe(title:String) : JFrame(titie)(
init {
//设置窗口大小不变
isResizable = false
//不设置布局管理器
layout = null

//创建标签
val label = JLabel("Label")
//设置标签的位置和大小
label.setBounds(89,13,100,30)
//设置标签文本水平居中
label.horizontalAlignment = SwingConstants.CENTER
//添加标签到内容面板
contentPane,add(label)
//创建 Button1
val button1 = JButton("Button1")
//设置 Button1 的位置和大小
button1.setBounds(89,59,100,30)
//添加 Button1 到内容面板
contentPane.add(button1)

//创建 Button2
val button2 = JButton("Button2")
//设置 Button2 的位置
button2.setLocation(89,102)
//设置 Button2 的大小
button2.setsSize(100,30)
//添加 Button2 到内容面板
contentPane.add (button2)

//设置窗口大小
setSize(300,200)
设置度口可见
isVisible = true
注册事件监听器,监听 Button2 单击事件
button2,addActionListener(label,text = "Hello swing!” )
//注册事件监听器,监听 Button1 单击事件
buttonl.addActionListener ( label,text = "Hgllo World!n !)
}
}
24.5 Swing 组件

Swing 所有组件都继承自JComponent,主要有文本处理、按钮、标签、列表、面板、组合框、滚动条、滚动面板、菜单、表格和树等组件。下面介绍一下常用的组件。
提示
资源文件是放在字节码文件夹中的文件,可通过 XXX.classjava.getResource()函数获得它运行时的绝对路径

MySql

提示
mysql -h localhost -u root -p命令的参数说明如下:
-h:是要连接的服务器主机名或IP 地址,可以是远程的一个服
务器主机,也可以是-hlocalhost的形式(没有空格)。
-u:是服务器要验证的用户名,这个用户一定是数据库中存在的,并且具有连接服务器的权限,也可以是-uroot 的形式(没有空格 )。
-p:是与上面用户对应的密码,也可以直接输入密码-p1234512345 是 root 密码。
所以 mysql -h localhost -u root -p命令也可以替换为 mysq-hlocalhost -uroot -p12345.
25.3 使用 Exposed 框架
本章的重点是介绍数据库编程,因此重点介绍Exposed 框架的使用。Exposed 是Kotlin DSL 轻量级的 SOL框架,Exposed 是基于 JDBC(Java Database Connectivity)技术实现的。使用 JDBC 比较麻烦,要先建立数据连接、编写 SOL 语句、操作数据库,最后要关闭数据库。而使用 Exposed 的开发人员不需要关心数据库连接或关闭等与业务无关的问题,只需要关注数据操作。
25.3.1 配置项目
为了能够在项目中使用 Exposed 框架,需要创建 IntelliJ IDEA 与Gradle 项目,项目创建完成后再打开 build.gradle 文件,修改文件内容如下:

version 1.0-SNAPSHOT'
buildscript {
ext.kotlin_version ='1.1.60'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'java'
apply plugin: 'kotlin'
sourceCompatibility = 1.8
repositories {
mavenCentral()
maven (
url "https://dl.bintray.com/kotlin/exposed"
}
}
image.png

25.4.2 配置 SQL 日志
为了更好地调试,往往需要参看 SQL DSL 执行过程生成的 SQL 语句,特别是那些复杂的査询语句。Exposed 框架提供了 SOL 日志工具类 SqlLogger,使用时需要进行一些酤置。打开 build.gradle 文件,修改 dependencies 内容如下:

dependencies
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "org.jetbrains.exposed:exposed:0.9.1'
compile("mysgl:mysgl-connector-java:5.1 .6")
compile 'org.slf4j:slf4j-api:1.7.25'
compile 'org.slf4j:slf4j-simple:1.7.25'
compile "org.jetbrains.kotlinx:kotlinx-html-jvm:0.6.8"
testCompile group:'junit', name:'junit', version:'4.12'
}

在dependencies添加代码第①行和第②行,这是因为SqlLogger依赖于slf4j日志框架在程序代码中的transaction...}还需要添加如下语句:

transaction {
logger.addLogger(stdoutSqlLogger)
}

注意

logger.addLogger(StdOutSqlLogger)语句应该是 transaction{...}
中的第一条语句。

反射

反射(Renection)是程序的自我分析能力,通过反射可以确定类中有哪些函数以及扇性。反射机制在一般的应用开发中很少使用,主要用于框架开发。
Kotlin 语言本身提供了反射 API,也可以通过调用 Java 语言反射 API实现反射。通过
反射机制能够动态读取一个类的信息;能够在运行时动态加载类,而不是在编译期。反射可以应用于框架开发,它能够从配置文件中读取配置信息动态加载类、调用函数和调用属性等。

26.1 Kotlin 反射 API

Kotlin 反射 API 主要来自于 kotlin.reflect、kotlin.reflect.full 和kotlin.reflect.jvm 包。其中 kotlin.reflect 和 kotlin.reflect.full 是主要的Kotlin 反射 API,而 kotlin.reflect.jvm 包主要用于 Kotlin 反射和 Java 反射的互操作。kotlin.reflect 包是 Kotlin 反射核心 API,它的类图如图 26-1 所示,它们都是接口,详细说明如下:

·KClass。表示一个具有反射功能的类。
。KParameter。表示一个具有反射功能的可传递给函数或属性的参数。
。KCallable。表示具有反射功能的可调用实体,包括属性和函数,它的直接子接口有KProperty 和KFunction.
。KFunction。表示一个具有反射功能的函数。
。KProperty。表示一个具有反射功能的属性,它有很多子接口。KProperty0、KPropery1
和 KProperty2 后面的数字表示接收者作为参数的个数。
KMutableProperty。表示一个具有反射功能的使用 var 声明的属性KMutableProperty0、KMutableProperty1和KMutableProperty2 后面的数字含义间KProperty.


image.png

提示
Kotlin 反射 API 所需要的运行时组件来自于独立的 kotlin-reflect.jar 文件,在 Android 等移动平台上为了减少应用程序包的大小,应用程序包在默认情况下不包含 kotlin-reflect.jar 文件。如果要在应用中使用反射功能,则需要额外添加kotlin-reflect.jar 文件至应用程序包中,并添加 kotlin-reflect.jar到项目的类路径。

注解

在源代码中嵌入一些补充信息,这种补充信息称为注解(Annotation)。注解并不能改。运行的结果,不会影响程序运行的性能。有的注解在编译时会给用户提示或警告,注解在运行时读写字节码文件信息。
提示
使用注解对代码的实现功能设有任何影响。程序员即便是不知道注解,也完全可以编写 Kotlin 程序代码。对此不感兴趣的读者可以跳过本章的内容。
Kotlin 中的注解本质上是一种接口类型。Kotlin 标准库中提供一些基本注解和元注解,基本注解会影响编译器的行为,如@JvmName、@JvmField、@JvmStatic、@JvmOverloads和@Throws 等,这些基本注解主要用于 Kotlin 与 Java 的混合编程中。元注解Meta Annotation)是负责注解其他的注解,自定义注解会用到元注解。
27.1 元注解
Kotlin 元注解有4个,其中包括@Target、@Retention、@Repeatable
@MustBeDocumented,它们都位于 kotlin.annotation 包中。元注解是
骐他注解进行说明的注解,当自定义一个新的注解类型时,其中可用元注解。这一节先介绍一下这几种元注解的含义,下一节在自定义注解中详细介绍它们的使用。

  1. @Target
    @Target 适用于目标注解,对应注解类是 kotlin.annotation.Target,它用来指定一个新注解的适用目标。@Target 注解有一个 allowedTargets 属性,该属性用来设置适用目标、allowedTargets 是 kotlin.annotation.AnnotationTarget 枚举类型的数组,AnnotationTarget 描述Kotlin 代码中可以被注解的元素类型,它有15个枚举常量,如表 27-1 所示。


    image.png
  2. @Retention
    @Retention 适用于保留期注解,对应注解类是 kotlin.annotation.Retention,它用来指定一个新注解的有效范围,@Retention 注解有一个 value 属性,该属性用来设置保留期,value是 kotlin.annotation.AnnotationRetention 枚举类型,AnnotationRetention 描述注解保留期种类,它有3个常量,如表 27-2所示。


    image.png
  3. @Repeatable
    @Repeatable适用于可重复注解,对应注解类是kotlin.annotation.Repeatable,它允许在相同的程序元素中重复注解,可重复的注解必须使用@Repeatable进行注解。
    4.@MustBeDocumented
    @MustBeDocumented 适用于文档注解,对应注解类是 kotlin.annotation.MustBeDocumented,该注解可以修饰代码元素(类、接口、函数和属性等),文档生成工具可以提取这些注解信息。
    27.2 自定义注解
    与基本注解不同,在一般的应用开发中不会直接使用元注解,在开发框架或生成工具时会用到一些自定义注解,元注解是自定义注解的组成要素。

27.2.1 声明注解
声明自定义注解可以使用 annotation 关键字实现,最简单形式的注解示例代码如下:

annotation class Marker

上述代码声明一个 Marker 注解,annotation 声明一个注解类型,注解的可见性有共有的、内部的和私有的,不能是保护的。
Marker 注解中不包含任何的成员,这种注解称为标记注解(Marked Annotation),标记注解属于基本注解。注解也可以有成员属性,通过构造函数初始化成员属性。示例代码如下:

annotation class MyAnnotationl(val value: string)

代码中(val value:String)是声明注解 MyAnnotation1 的构造函数,构造函数参数类塱只能是基本数据类型、字符串、类类型(KClass)、枚举、数组和其他的注解类型。另外,构造函数(val value:Sting)同时也声明了注解 MyAnnotation1 有一个成员属性
"le,成员属性的可见性只能是公有的。
注解成员属性也可以有默认值,示例代码如下:

annotation class MyAnnotation2 (val value: String ="注解信息", val count: Int = 20)

27.2.3 注解目标声明
27.2.3 节案例代码的第③行和第⑤行都用到@set:MemberAnnotation 形式的注解,其中set 是声明 MemberAnnotation 注解应用在 name 成员属性 setter 访问器上,事实上一个 name成员属性有很多可以被注解的元素,这些元素有:属性本身、getter 访问器、setter 访问器和支持字段。
Kotlin 中使用某个注解进行修饰时,当多个元素都有被修饰的可能时,可以使用注解目标声明指定注解目标声明指定注解修饰的具体元素,27.2.2节案例中的 get(见代码第③行和第⑤行)就是注解目标声明。Kotlin 的注解目标列表如表 27-3 所示。


image.png

27.2.4 案例:读取运行时注解信息
注解是为工具读取信息而准备的。有些工具可以读取源代码文件中的注解信息;有的可以读取字节码文件中的注解信息;有的可以在运行时读取注解信息。但是读取这些注解信息的代码都是一样的,区别只在于自定义注解中@Retention 的保留期不同。
读取注解信息所需要的反射 API 是 KClass 类的 findAnnotation 函数。读取运行时Sudent 类中注解信息代码如下:

//代码文件:chapter27/src/com/a51work6/section2/ch27.2.3.kt
package com.a5lwork6.section2

import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.findAnnotation
fun main(args: Array<string>) {
val clz = student::class
//读取类注解见
val ann = clz.findAnnotation<MyAnnotation>()
println("类${clz.simpleName},注解描述:${ann?.description ?:"无"}")③

//读取成员函数的注解信息
println("--读取成员函数的注解信息--")
clz.declaredFunctions.forEach {
val ann = it.findAnnotation<MemberAnnotation>()
println("函数s{it.name),注解描述:${ann?.description ?:"无"}")
}
//读取属性的注解信息
println("--读取属性的注解信息--")
clz.declaredMemberproperties .forEach {
val ann = it.findAnnotation<MemberAnnotation>()
printIn(" 属性s(it.name),注解描述;$lann?.description ?:"无"}")
}
}

相关文章

网友评论

      本文标题:Kotlin精点

      本文链接:https://www.haomeiwen.com/subject/eghimjtx.html