Kotlin 安卓开发学习手册(五)
十七、更多 API
本章收集了一些你可以在应用中使用的 API。首先,我们有用于数学计算的数学 API。对于日期和时间处理,包括不同时间表示之间的转换,以及解析和格式化日期和时间,我们描述了日期和时间 API。对于输入和输出,Android 归结为文件处理,我们给出了输入和输出 API 的概述。为了动态获取类成员信息,使用了反射 API 这不是面向对象的突出部分,但在某些情况下会有帮助,所以我们包括了一篇关于反射的论文。正则表达式提供了一个非常强大的方法来研究和操作字符串中的模式,所以我们用一个正则表达式结构的概览来结束这一章。
数学应用编程接口
Kotlin 允许你从包java.lang中导入包Math
import java.lang.Math
这可以像一个单例对象一样使用,并且有很多数学函数,比如sin()、cos()、tan()等等。您可以在 Java API 文档中查找它们。Kotlin 在kotlin.math包中提供了其中一些的副本,所以在大多数情况下你可以不使用java.lang导入。例如,正弦函数是作为kotlin.math包中的一个类外函数提供的,所以要使用它,您可以编写
import kotlin.math.sin
...
val x = sin(1.562)
许多其他功能也是如此。表 17-1 包括一个非详尽的列表。要获得完整的列表,请参阅网站上的 Kotlin 官方文档。
表 17-1。
Kotlin 数学函数
|功能
|
描述
|
| --- | --- |
| sin()、cos()、tan() | 正弦、余弦和正切函数。与Math.sin()、Math.cos()和Math.tan()相同,但另外允许一个Float作为自变量。 |
| asin()、acos()、atan()、atan2() | 反正弦、反余弦和反正切函数。函数atan2()接受两个对应于(x, y)坐标的参数。与Math.asin()、Math.acos()、Math.atan()和Math.atan2()相同,但另外允许Float s 作为自变量。 |
| sinh()、cosh()、tanh() | 双曲正弦、余弦和正切函数。与Math.sinh()、Math.cosh()和Math.tanh()相同,但另外允许一个Float作为自变量。 |
| asinh()、acosh()、atanh() | 反双曲正弦、余弦和正切函数。与Math.asinh()、Math.acosh()和Math.atanh()相同,但另外允许一个Float作为自变量。 |
| abs() | 一个数的绝对值。 |
| floor(),ceil() | 对于Float或Double,下一个整数值的上下限值。该类型保持不变,因此您必须添加.toInt()或.toLong()来将其转换为整数类型。与Math.floor()和Math.ceil()相同,但另外允许一个Float作为自变量。 |
| round() | 向上舍入到最接近的整数。该类型保持不变,因此您必须添加.toInt()或.toLong()来将其转换为整数类型。与Math.round()相同,但另外允许一个Float作为参数。 |
| exp(),log() | 指数函数和对数。与Math.exp()和Math.log()相同,但另外允许一个Float作为参数。 |
| pow() | xy幂函数(两个参数)。与Math.pow()相同,但另外允许一个Float作为参数。 |
| sqrt() | 平方根。与Math.sqrt()相同,但另外允许一个Float作为参数。 |
| min(),max() | 两个数的最小值和最大值。 |
| sign() | 符号函数。负值返回-1.0,0.0 返回 0.0,正数返回 1.0。与Math.sign()相同,但另外允许一个Float作为参数。 |
同一个包kotlin.math包含几个扩展属性。例如,你可以写
import kotlin.math.absoluteValue
...
val x:Double = -3.5
val y = x.absoluteValue // -> 3.5
这种扩展的完整列表包括用于数字绝对值的.absoluteValue(Double、Float、Int或Long)。常数E和PI是自然对数和π(π)的底数。属性.sign返回一个数字(Double、Float、Int或Long)的符号,.ulp返回一个Float或Double的最后一位的单位(这是两个数字之间的最小可测距离)。
日期和时间 API,API 级别 25 或更低
Kotlin 没有单独的日期和时间 API,这就是为什么在 Kotlin 文档中找不到任何关于如何处理日期和时间的信息。但是,您可以使用 Java 中的日期和时间 API,它包含在 Android 中,可由 Kotlin 访问。
注意
Java 8 中的日期和时间 API 发生了很大的变化。Android API 最高 25 版本不使用 Java 8,但后来的 API 版本使用;这就是为什么我们需要描述两个日期和时间 API。本节适用于所有 Android API 级别,因此引用了较旧的 Java 7 日期和时间 API。
从 Java 第 7 版借用的日期和时间 API 以下列表达式为中心:
import java.util.Date
import java.util.GregorianCalendar
import java.text.SimpleDateFormat
val timeMillis:Long = System.currentTimeMillis()
val d = Date()
val cal = GregorianCalendar()
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
第一个,System.currentTimeMillis(),表达了绝对时间的观念。更准确地说,这是自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。这是低级信息,通常用作数据库条目的可靠时间戳。在性能测量过程中,您还会看到它对程序部分的快速计时:
val t1 = System.currentTimeMillis()
...
Log.d("LOG", "Calculation took " +
(System.currentTimeMillis() - t1) + "ms")
Date类是绝对时间的一个薄薄的包装。它将其表示为一个对象,并提供一个简单的toString()实现,以人类可读的格式输出时间:
import java.util.Date
...
val d = Date() // current time
Log.d("LOG", d.toString())
// -> s.th. like
// Sun Jan 13 10:12:26 GMT+01:00 2019
一个Date实例给出了从 1970-01-01 00:00:00 UTC 到它的当前值所经过的毫秒数。要获得那个数字——它是一个Long类型的数字——使用它的time属性:
import java.util.Date
...
val d = Date() // current time
val tm = d.time // ms since 1970-01-01T00:00:00 UTC
类给了我们工具来处理月、周、时区、一天中的时间、一小时中的分钟、一分钟中的秒钟以及所有这些东西。
import java.util.Date
import java.util.Calendar
import java.util.GregorianCalendar
import java.util.TimeZone
...
val cal = GregorianCalendar()
// <- will hold the current time
cal.timeZone = TimeZone.getTimeZone("US/Hawaii")
// Note: TimeZone.getAvailableIDs().forEach {
// Log.e("LOG","!!! " + it) }
// shows a list
// Set to current time
cal.time = Date()
// Set to 2018-02-01T13:27:44
cal.set(2018, Calendar.FEBRUARY, 1, 13, 27 ,44)
val month = cal.get(Calendar.MONTH)
val hour = cal.get(Calendar.HOUR_OF_DAY)
SimpleDateFormat类帮助我们生成人类可读的日期和时间的字符串表示,并允许我们将这样的字符串表示转换回Date实例:
import java.util.Date
import java.text.SimpleDateFormat
import java.util.Locale
...
val d = Date() // now
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm")
Log.d("LOG", sdf.format(d)) // -> 2019-01-13 13:41
val loc = Locale("en")
val sdf2 = SimpleDateFormat("yyyy-MMM-dd HH:mm", loc)
Log.d("LOG", sdf2.format(d)) // -> 2019-Jan-13 13:41
val d2:Date = sdf.parse("2018-12-12 17:13")
Log.d("LOG", d2.toString())
// -> Wed Dec 12 17:13:00 GMT+01:00 2018
这些示例使用通过查询操作系统检索的时区。您也可以在SimpleDateFormat对象上设置时区,如下所示:
import java.text.SimpleDateFormat
import java.util.Date
import java.util.TimeZone
...
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm")
sdf.timeZone = TimeZone.getTimeZone("US/Hawaii")
val d:Date = sdf.parse("2018-12-12 17:13")
Log.d("LOG", d.toString())
// -> Thu Dec 13 04:13:00 GMT+01:00 2018
顺便说一下,Date.toString()隐式地使用它通过查询操作系统得到的时区(在我的例子中是欧洲/柏林)。
警告
Date和SimpleDateFormat都是而不是线程安全的;您不能在不同的线程之间共享它们的实例。
有关所有这些日期和时间 API 接口和类以及相关接口和类的详细信息,请参考 Oracle 的 Java 文档。确保不要使用高于 7 的 Java 版本的文档。我们将在下一节讨论与 Java 8 相关的日期和时间 API。
日期和时间 API,API 级别 26 或更高
注意
本节针对从 26 (Android 8.0)开始的 Android API 级别,因此指的是 Java 8 日期和时间 API。
从 Android API level 26 (Android 8.0)开始,一些新的日期和时间相关的接口和类可用。您可以继续使用上一节中描述的旧 API,但是新 API 包含一些我们在本节中概述的改进。
注意
截至 2019 年初,使用 API 级别 26 或更高的设备数量并不是很高。在开始开发 API 水平超过 25 的产品之前,您应该咨询一下发行调查。
只有在模块的build.gradle文件中将minSdkVersion设置为 26 或更大时,才能使用新的 API:
android {
...
defaultConfig {
...
minSdkVersion 26
...
}
...
}
新的接口和类驻留在包java.time中。对于本节的其余部分,我们通常省略相应的导入。
当地日期和时间
本地日期和时间从观察者的上下文中得到描述,并且基本上使用来自java.time包的以下类:
-
LocalDate这个类对应于格式
yyyy-MM-dd(例如 2018-11-27)的日期表示,并且不考虑一天中的时间。 -
LocalTime这个类对应于格式
HH:mm:ss(例如 21:27:55)的时间表示,而不考虑日期。 -
LocalDateTimeLocalDate和LocalTime的组合,可能用yyyy-MM-ddTHH:mm:ss来表示(T是字面意思)。
格式指示符yyyy、HH等在java.time.DateTimeFormatter的 API 文档中有描述。
这三者都包含生成对象实例的工厂方法。这包括获取当前日期和时间:
import java.time.*
// current day in the default time zone
val ld1 : LocalDate = LocalDate.now()
// "Now" corresponds to different days in different
// time zones. The following allows us to specify a
// different time zone
val z2 = ZoneId.of("UTC+01")
val ld2 : LocalDate = LocalDate.now(z2)
val ld3 = LocalDate.of(2018, Month.MARCH, 27)
val ld4 = LocalDate.of(2018, 3, 27) // the same
val lt1 : LocalTime = LocalTime.now()
val lt2 = LocalTime.now(z2) // different time zone
val lt3 = LocalTime.of(23, 27, 55) // 23:27:55
val ldt1 = LocalDateTime.now()
val ldt2 = LocalDateTime.now(z2)
val ldt3 = LocalDateTime.of(2018, Month.APRIL, 23, 23, 44, 12)
// <- 2018-04-23T23:44:12
请注意,尽管能够添加时区规范来进一步指定“现在”对应的时间,但该信息决不会以某种方式存储在日期和时间对象中。根据定义,本地日期和时间与时区无关。
我们可以解析字符串来获得LocalDate、LocalTime和LocalDateTime的实例:
import java.time.*
import java.time.format.*
// Parse ISO-8601
val ld1 = LocalDate.parse("2019-02-13")
// Parse other formats. For the format specification,
// see API documentation of class DateTimeFormatter.
val formatter1 = DateTimeFormatter.ofPattern("yyyy MM dd")
val ld2 = LocalDate.parse("2019 02 13", formatter1)
val lt1 = LocalTime.parse("21:17:23")
val lt2 = LocalTime.parse("21:17:23.3734")
val formatter2 = DateTimeFormatter.ofPattern("HH|mm|ss")
val lt3 = LocalTime.parse("21|17|23", formatter2)
val ldt1 = LocalDateTime.parse("2019-02-13T21:17:23")
val ldt2 = LocalDateTime.parse("2019-02-13T21:17:23.3734")
val formatter3 = DateTimeFormatter.ofPattern("yyyy.MM.dd.HH.mm.ss")
val ldt3 = LocalTime.parse("2019.04.23.17.45.23", formatter3)
我们可以定制自己的LocalDate、LocalTime和LocalDateTime实例的字符串表示:
import android.util.Log
import java.time.*
import java.time.format.*
val s1 = LocalDate.now().format(
DateTimeFormatter.ofPattern("yyyy|MM|dd"))
Log.d("LOG","s1 = ${s1}") // -> 2019|01|14
val s2 = LocalDate.now().format(
DateTimeFormatter.ISO_LOCAL_DATE)
Log.d("LOG","s2 = ${s2}") // -> 2019-01-14
val s3 = LocalTime.now().format(
DateTimeFormatter.ofPattern("HH mm ss"))
Log.d("LOG","s3 = ${s3}") // -> 14 46 20
val s4 = LocalTime.now().format(
DateTimeFormatter.ISO_LOCAL_TIME)
Log.d("LOG","s4 = ${s4}") // 14:46:20.503
val s5 = LocalDateTime.now().format(
DateTimeFormatter.ofPattern(
"yyyy MM dd - HH mm ss"))
Log.d("LOG","s5 = ${s5}") // -> 2019 01 14 - 14 46 20
val s6 = LocalDateTime.now().format(
DateTimeFormatter.ISO_LOCAL_DATE_TIME)
Log.d("LOG","s6 = ${s6}") // -> 2019-01-14T14:46:20.505
您可以使用LocalDate、LocalTime和LocalDateTime实例执行时间运算:
import java.time.*
import java.time.temporal.*
val ld = LocalDate.now()
val lt = LocalTime.now()
val ldt = LocalDateTime.now()
val ld2 = ld.minusDays(7L)
val ld3 = ld.plusWeeks(2L)
val ld4 = ld.with(ChronoField.MONTH_OF_YEAR, 11L)
val lt2 = lt.plus(Duration.of(2L, ChronoUnit.SECONDS))
val lt3 = lt.plusSeconds(2L) // same
val ldt2 = ldt.plusWeeks(2L).minusHours(2L)
从LocalDateTime我们可以计算自 1970-01-01:00:00:00 UTC 以来经过的秒数,类似于旧 API 中的System.currentTimeMillis()函数:
import java.time.*
val ldt : LocalDateTime = ...
val secs = ldt.toEpochSecond(ZoneOffset.of("+01:00"))
注意,要获得纪元秒,更好的解决方案是取一个ZonedDateTime。我们稍后将讨论分区日期和时间。
瞬间
瞬间是时间线上的瞬间点。在需要唯一的绝对时间戳的情况下使用它,例如,在数据库中注册事件等。精确的定义有点复杂;关于介绍,请阅读java.time.Instant的 API 文档。
例如,您可以通过查询系统时钟、指定自 1970-01-01T00:00:00Z 以来经过的时间、解析时间字符串或从其他日期和时间对象中获取一个Instant:
import java.time.*
val inz1 = Instant.now() // default time zone
// Specify time zone
val inz2 = Instant.now(Clock.system(
ZoneId.of("America/Buenos_Aires")))
val secondsSince1970 : Long = 1_000_000_000L
val nanoAdjustment : Long = 300_000_000 // 300ms
val inz3 = Instant.ofEpochSecond(
secondsSince1970, nanoAdjustment)
// "Z" is UTC ("Zulu" time)
val inz4 = Instant.parse("2018-01-23T23:33:14.513Z")
// Uniform converter, for the ZonedDateTime class
// see below
val inz5 = Instant.from(ZonedDateTime.parse("2019-02-13T21:17:23+01:00[Europe/Paris]"))
偏移日期和时间
偏移日期和时间类似于Instant s,加上 UTC/格林威治时间的附加时间偏移。对于这样的偏移日期和时间,我们有两个类,OffsetTime和OffsetDateTime,您可以获得如下实例:
import java.time.*
import java.time.format.DateTimeFormatter
// Get now ------------------------------------------
// System clock, default time zone
val ot1 = OffsetTime.now()
val odt1 = OffsetDateTime.now()
// Use a different clock
val clock:Clock = ...
val ot2 = OffsetTime.now(clock)
val odt2 = OffsetDateTime.now(clock)
// Use a different time zone
val ot3 = OffsetTime.now(
ZoneId.of("America/Buenos_Aires"))
val odt3 = OffsetDateTime.now(
ZoneId.of("America/Buenos_Aires"))
// From time details --------------------------------
val ot4 = OffsetTime.of(23, 17, 3, 500_000_000,
ZoneOffset.of("-02:00"))
val odt4 = OffsetDateTime.of(
1985, 4, 23, // 19685-04-23
23, 17, 3, 500_000_000, // 23:17:03.5
ZoneOffset.of("+02:00"))
// Parsed -------------------------------------------
val ot5 = OffsetTime.parse("16:15:30+01:00")
val odt5 = OffsetDateTime.parse("2007-12-03T17:15:30-08:00")
val ot6 = OffsetTime.parse("16 15 +00:00",
DateTimeFormatter.ofPattern("HH mm XXX"))
val odt6 = OffsetDateTime.parse("20181115 - 231644 +02:00",
DateTimeFormatter.ofPattern("yyyyMMdd - HHmmss XXX"))
// From other objects -------------------------------
val lt = LocalTime.parse("16:14:27.235")
val ld = LocalDate.parse("2018-05-24")
val inz = Instant.parse("2018-01-23T23:33:14.513Z")
val ot7 = OffsetTime.of(lt, ZoneOffset.of("+02:00"))
val odt7 = OffsetDateTime.of(ld, lt, ZoneOffset.of("+02:00"))
val ot8 = OffsetTime.ofInstant(inz, ZoneId.of("America/Buenos_Aires"))
val odt8 = OffsetDateTime.ofInstant(inz, ZoneId.of("America/Buenos_Aires"))
val zdt = ZonedDateTime.of( // see below
2018, 2, 27, // 2018-02-27
23, 27, 33, 0, // 23:27:33.0
ZoneId.of("Pacific/Tahiti"))
val odt9 = zdt.toOffsetDateTime()
// uniform converter
val ot10 = OffsetTime.from(zdt)
val odt10 = OffsetDateTime.from(zdt)
使用偏移日期和时间,您可以使用与本地日期和时间基本相同的方式进行运算和格式化。此外,对于转换操作,我们有
import java.time.*
val ot = OffsetTime.parse("16:15:30+01:00")
val lt : LocalTime = ot.toLocalTime()
val odt = OffsetDateTime.parse("2007-12-03T17:15:30-08:00")
val ldt : LocalDateTime = odt.toLocalDateTime()
val lt2 : LocalTime = odt.toLocalTime()
val ld2 : LocalDate = odt.toLocalDate()
val ot2 : OffsetTime = odt.toOffsetTime()
val zdt : ZonedDateTime = odt.toZonedDateTime()
// see below for class ZonedDateTime
分区日期和时间
如果我们不关心用户的位置,本地日期和时间是很好的。如果我们让世界各地的不同实体、用户、计算机或设备输入日期和时间,我们需要添加时区信息。这就是类ZonedDateTime的用途。
注意,这与带有固定时间偏移信息的日期和时间不同,与OffsetDateTime的情况不同。时区包括夏令时等需要考虑的因素。
与LocalDateTime类似,ZonedDateTime现在有了获得的工厂方法:
import java.time.*
// Get "now" using the system clock and the default
// time zone from your operating system.
val zdt1 = ZonedDateTime.now()
// Get "now" using a time zone. To list all available
// predefined zone IDs, try
// Log.d("LOG", ZoneId.getAvailableZoneIds().
// joinToString { it + "\n" })
val z2 = ZoneId.of("UTC+01")
val zdt2 = ZonedDateTime.now(z2)
// Get "now" using an instance of Clock
val clock3 = Clock.systemUTC()
val zdt3 = ZonedDateTime.now(clock3)
我们还可以使用详细的时间信息获得一个ZonedDateTime,并解析时间戳的字符串表示以获得一个ZonedDateTime:
import java.time.*
val z4 = ZoneId.of("Pacific/Tahiti")
val zdt4 = ZonedDateTime.of(
2018, 2, 27, // 2018-02-27
23, 27, 33, 0, // 23:27:33.0
z4)
// The 7th par is nanoseconds, so for
// 23:27:33.5 you have to enter
// 500_000_000 here
val localDate = LocalDate.parse("2018-02-27")
val localTime = LocalTime.parse("23:44:55")
val zdt5 = ZonedDateTime.of(localDate, localTime,
ZoneId.of("America/Buenos_Aires"))
val ldt = LocalDateTime.parse("2018-02-27T23:44:55.3")
val zdt6 = ZonedDateTime.of(ldt,
ZoneId.of("America/Buenos_Aires"))
val inz = Instant.parse("2018-01-23T23:33:14.513Z")
val zdt7 = ZonedDateTime.ofInstant(inz,
ZoneId.of("America/Buenos_Aires"))
val zdt8 = ZonedDateTime.parse(
"2018-01-23T23:33:14Z[America/Buenos_Aires]")
一个ZonedDateTime允许像plusWeeks(weeks:Long)和minusDays(days:Long)这样的操作用增加或减少给定的时间来构建一个新的实例。这适用于Years、Months、Weeks、Days、Hours、Minutes、Seconds或Nanos中的任何一种。
对于不同的时间段有不同的 getter 函数:getYear()、getMonth()、getMonthValue()、getDayOfMonth()、getHour()、getMinute()、getSecond()和getNano(),以及其他一些函数。要得到时区,写getZone()。
要解析日期和时间字符串并将ZonedDateTime转换为字符串,请编写:
import java.time.*
import java.time.format.DateTimeFormatter
val zdt1 = ZonedDateTime.parse(
"2007-12-03T10:15:30+01:00[Europe/Paris]")
val formatter = DateTimeFormatter.ofPattern(
"HH:mm:ss.SSS")
// See DateTimeFormatter API docs for more options
val str = zdt1.format(formatter)
ZonedDateTime和LocalDateTime之间的连接通过
import java.time.*
val ldt = LocalDateTime.parse("2018-02-27T23:44:55.3")
val zdt = ZonedDateTime.of(ldt,
ZoneId.of("America/Buenos_Aires"))
val ldt2 = zdt.toLocalTime()
持续时间和周期
持续时间是两个实例之间的物理时间跨度。周期与此类似,但只处理年、月和日,并考虑日历系统。有特殊的Duration和Period类用于处理持续时间和周期:
import java.time.*
import java.time.temporal.ChronoUnit
val ldt1 = LocalDateTime.parse("2018-01-23T17:23:00")
val ldt2 = LocalDateTime.parse("2018-01-24T16:13:10")
val ldt3 = LocalDateTime.parse("2020-01-24T16:13:10")
// Getting a duration: ------------------------------
val d1 = Duration.between(ldt1, ldt2)
// Note: this works also for Instant and ZonedDateTime
// objects
val d2 = Duration.of(27L, ChronoUnit.HOURS) // 27hours
val d3 = Duration.ZERO.
plusDays(3L).
plusHours(4L).
minusMinutes(78L)
val d4 = Duration.parse("P2DT3H4M")
// <- 2 days, 3 hours, 4 minutes
// For more specifiers, see the API documentation
// of Duration.parse()
// Getting a period: --------------------------------
val ld1 = LocalDate.parse("2018-04-23")
val ld2 = LocalDate.parse("2018-08-16")
val p1 = Period.between(ld1, ld2)
// Note, end date not inclusive
val p2 = Period.of(2, 3, -1)
// <- 2 years + 3 months - 1 day
val p3 = Period.parse("P1Y2M-3D")
// <- 1 year + 2 months - 3 days
// For more specifiers, see the API documentation
// of Period.parse()
您可以对Duration或Period类的实例执行算术计算:
import java.time.*
// Duration operations: ------------------------------
val d = Duration.parse("P2DT3H4M")
// <- 2 days, 3 hours, 4 minutes
val d2 = d.plusDays(3L)
// also: .minusDays(33L)
// or .plusHours(2L) or .minusHours(1L)
// or .plusMinutes(77L) or .minusMinutes(7L)
// or .plusSeconds(23L) or .minusSeconds(5L)
// or .plusMillis(11L) or .minusMillis(55L)
// or .plusNanos(1000L) or .minusNanos(5_000_000L)
val d3 = d.abs() // make positive
val d4 = d.negated() // swap sign
val d5 = d.multipliedBy(3L) // three times as long
val d6 = d.dividedBy(2L) // half as long
// Period operations: --------------------------------
val p = Period.of(2, 3, -1)
// <- 2 years + 3 months - 1 day
val p2 = p.normalized()
// <- possibly adjusts the year to make the month lie
// inside [-11;+11]
val p3 = p.negated()
val p4 = p.minusYears(11L)
// also: .plusYears(3L)
// or .minusMonths(4L) or .plusMonths(2L)
// or .minusDays(40L) or .plusDays(5L)
val p5 = p.multipliedBy(5) // 5 times as long
您可以使用持续时间和周期向LocalDate、LocalTime、LocalDateTime、ZonedDateTime和Instant对象添加或从中减去时间量。
import java.time.*
val d = Duration.parse("P2DT3H4M")
val p = Period.of(2, 3, -1)
// <- 2 years + 3 months - 1 day
val ld = LocalDate.parse("2018-04-23")
val lt = LocalTime.parse("17:13:12")
val ldt = LocalDateTime.of(ld, lt)
val zdt = ZonedDateTime.parse(
"2007-12-03T10:15:30+01:00[Europe/Paris]")
val inz = Instant.parse("2018-01-23T23:33:14.513Z")
// ---- Using a LocalDate
val ld2 = ld.plus(p) // or .minus(p)
// val ld3 = ld.plus(d) // -> exception
// val ld4 = ld.minus(d) // -> exception
// ---- Using a LocalTime
val lt2 = lt.plus(d) // or .minus(d)
// val lt3 = lt.minus(p) // -> exception
// val lt4 = lt.plus(p) // -> exception
// ---- Using a LocalDateTime
val ldt2 = ldt.plus(d) // or .minus(d)
val ldt3 = ldt.plus(p) // or .minus(p)
// ---- Using a ZonedDateTime
val zdt2 = zdt.plus(d) // or .minus(d)
val zdt3 = zdt.plus(p) // or .minus(p)
// ---- Using an Instant
val inz2 = inz.plus(d) // or .minus(d)
// val inz3 = inz.minus(p) // -> exception
// val inz4 = inz.plus(p) // -> exception
请注意,有些操作是不允许的,会导致异常。这些在前面的清单中被注释掉了。例外的原因是时间概念中可能的精度损失或不匹配。有关详细信息,请参见 API 文档。
时钟
一个Clock位于日期和时间 API 的深处。对于许多(如果不是大多数)应用,您可以很好地处理本地日期和时间、偏移和分区日期和时间以及瞬间。对于测试和特殊情况,可能有必要调整时钟使用以使变为:
import java.time.*
val clock : Clock = ...
val ldt = LocalDateTime.now(clock)
val zdt = ZonedDateTime.now(clock)
val inz = Instant.now(clock)
除了覆盖抽象的Clock类,Clock本身提供了几个函数来调整时钟的使用。这两个特别有趣:
-
这是一个总是返回同一时刻的时钟。
-
Clock.offset(baseClock:Clock, offsetDuration:Duration):返回一个新的时钟,该时钟是从基础时钟加上指定的持续时间得到的。
然而,如果您重写了时钟,您必须至少实现来自Clock基类的抽象函数。下面是一个时钟的例子,它总是返回相同的时刻,并且不关心时区:
import java.time.*
val myClock = object : Clock() {
override fun withZone(zone: ZoneId?): Clock {
// Supposed to return a copy of this clock
// with a different time zone
return this
}
override fun getZone(): ZoneId {
// Supposed to return the zone ID
return ZoneId.of("Z")
}
override fun instant(): Instant {
// This is the engine of the clock. It must
// provide an Instant
return Instant.parse("2018-01-23T23:33:14Z")
}
}
... use myClock
练习 1
创建一个时钟ClockTwiceAsFast,用构造函数从 UTC 系统时钟获取时间。在此之后,时钟应该运行两倍的速度。忽略区域信息。要证明它正在以预期的方式运行,请使用
import java.time.*
val myClock = ClockTwiceAsFast()
Log.d("LOG", LocalDateTime.now(myClock).format(
DateTimeFormatter.ISO_LOCAL_DATE_TIME))
Thread.sleep(1000L)
Log.d("LOG", LocalDateTime.now(myClock).format(
DateTimeFormatter.ISO_LOCAL_DATE_TIME))
输入和输出
在 Android 环境中,你可能不会经常使用输入和输出。你的应用的用户看不到一个控制台println("Hello World")会打印到那里,而且你的应用产生的任何日志都不应该被最终用户看到。此外,为了保存和读取任何类型的数据,您可以使用内置的数据库。
话虽如此,但如果您绝对需要,您仍然可以读取和写入文件以进行输入和输出。在 Android 中,最好使用位于指定文件系统空间中的文件,这些文件可以被你的应用访问。你通过写作做到这一点
import java.io.File
// We are inside an Activity or other Context!
val dataDir:File = getFilesDir()
尽管这个清单中的类命名为File,但是dataDir对应于一个目录,而不是狭义的数据文件。本节的其余部分假设您已经预先考虑了代码片段val dataDir = getFilesDir()。
Kotlin 的文件处理严重依赖于 Java 接口和类,并为一些 Java 类添加了扩展。还有几个类外函数在包kotlin.io中定义。不用导入kotlin.io;它是默认导入的,因此这个包中的所有类扩展都是默认启用的。
创建一些测试文件
为了有一些文件让您开始试验 I/O API,请运行以下命令一次:
dataDir.resolve("a.txt").takeIf{ !it.exists() }.appendText("Hello World A")
dataDir.resolve("b.txt").takeIf{ !it.exists() }.appendText("Hello World B")
File(dataDir,"dir1").mkdirs()dataDir.resolve("dir1").resolve("a.txt").
takeIf{ !it.exists() }.appendText("Hello World dir1-A")
我们稍后将讨论这些功能。
文件名
为了获得最大的互操作性,您应该将文件名限制为仅包含 A–Z、A–Z、0–9、_、-和。同样,为了表明文件file位于目录dir中,写下dir/file。要指定文件系统的根目录,请使用/。
注意
斜线(/)是 Android 上的文件系统分隔符。其他操作系统使用不同的分隔符。如果你想真正精通多种语言,你可以写"dir" + File.separator + "file".运行时引擎将为它工作的操作系统选择合适的分隔符。
要对给定目录中的文件fileName进行寻址,您可以使用
val someDir:File = ...
val file:File = someDir.resolve("fileName")
它适用于真实的文件和子目录。
列出目录
要列出应用文件存储中的文件,请写入
dataDir.walk().maxDepth(1).forEach { file ->
Log.d("LOG", file.toString())
}
这显示了数据目录的直接内容。如果您运行前面的小准备代码,日志输出将如下所示:
/data/user/0/multipi.pspaeth.de.multipi/files
/data/user/0/multipi.pspaeth.de.multipi/files/instant-run
/data/user/0/multipi.pspaeth.de.multipi/files/a.txt
/data/user/0/multipi.pspaeth.de.multipi/files/b.txt
/data/user/0/multipi.pspaeth.de.multipi/files/dirs1
/data/user/0/multipi.pspaeth.de.multipi/files/dir1
multipi.pspaeth.de.multipi恰好是我运行代码的示例应用,在第二行中,instant-run属于默认安装的 Android 目录。当然,您可以将walk()应用到任何其他目录,只要确保您拥有适当的文件系统访问权限。maxDepth(1)将遍历限制在目录的直接子目录。省略它将递归遍历所有内容,包括目录中的文件、目录中的文件、目录中的文件等等。
walk()和maxDepth()都返回类FileTreeWalk的一个实例。这个类是一个Sequence,模仿了Iterable的所有功能,所以你可以应用过滤器、映射、折叠、分组以及我们在第九章中研究过的其他过程。如果需要真正的Iterable也可以写asIterable()(一个Sequence本身不继承Iterable)。
注意
Sequence接口存在的原因是序列可能会被迭代多次,而对于Iterable的实现来说却不是这样。
例如,要递归列出dataDir中的所有真实文件,忽略目录,您可以应用如下过滤器:
dataDir.walk().filter { it.isFile() }.forEach {
file ->
Log.d("LOG", file.toString())
}
您可以使用相同的过滤程序仅列出具有特定结尾的文件:
dataDir.walk().filter { it.endsWith(".txt") }.
forEach {
file ->
Log.d("LOG", file.toString())
}
还有一个函数startsWith("someString")查看文件名是否以某个字符串开头。您还可以根据正则表达式检查名称:
dataDir.walk().filter {
it.name.matches(".*invoice\\d\\d.*\\.txt")
}.forEach {
file ->
Log.d("LOG", file.toString())
}
这将匹配任何文件名包含添加了两个数字的invoice,并以.txt结尾的文件。
写入文件
要向文件中写入或追加文本,可以使用
val file = dataDir.resolve("a.txt")
// or any other file
// Write to the file
file.writeText("In the house, there was no light")
// Append to the file
file.appendText("\nIn the house, there was no light")
注意writeText(text:String)和appendText(text:String)使用 UTF-8 字符集。如果需要不同的字符集,可以添加一个java.nio.charset.Charset的实例作为第二个参数:writeText( "...", Charsets.ISO_8859_1 ) (Charsets 是一个 Kotlin 类:kotlin.text.Charsets)。
为了获得更低的级别,也可以将原始字节从ByteArray写入文件:
val file = dataDir.resolve("a.txt")
val bytes = byteArrayOf(27, 34, 13, 47, 50)
// Write to the file
file.writeBytes(bytes)
// Append to the file
file.appendBytes(bytes)
注意
如果您需要对大型文件或许多细粒度文件操作进行繁重的文件处理,Kotlin 提供了更多的扩展可以帮助您,您还可以使用大量的 Java 文件处理类和方法。因为在 Android 上你有一个内置的快速数据库来处理这样的用例,我不认为你会经常使用这样特殊的文件处理,但是你可以自由地探索 Kotlin 和 Java 文档。
从文件中读取
要从文件中读取,您必须决定是要将整个文件读入内存,是要逐行读取文本文件,还是要逐块读取包含二进制数据的文件。
要将一个中等大小的文本文件作为一个整体读入一个属性,请这样写(我们再次假设您从本章开始就运行了那个小的准备程序):
val file = dataDir.resolve("a.txt")
val fileText:String = file.readText()
这里使用了 UTF 8 字符集。要读取不同字符集的文件,请添加一个参数:
val file = dataDir.resolve("a.txt")
val fileText:String = file.readText(
Charsets.ISO_8859_1)
如果您没有文本文件,但是有一些原始字节数据的文件,要从文件中读取字节,请使用以下命令:
val file = dataDir.resolve("a.txt")
val fileBytes:ByteArray = file.readBytes()
将文本文件作为一个整体读入属性对于小的文本文件来说当然是有意义的。要处理较大的文本文件,您也可以逐行读取它们:
val file = dataDir.resolve("a.txt")
val allLines = file.readLines()
allLines.forEach { ln ->
// do something with the line (a String)
}
文档说你不应该对大文件这样做。在内部,文件被读入一个包含所有行的大列表。不过,多达 100,000 行的文件实际上不会造成问题。如果您的目标是从 API 级别 26 开始的 Android 设备,还有一种更有效的方法将行读入流:
val file = dataDir.resolve("a.txt")
// Only API level > 25
file.bufferedReader.use {
it.lines().forEach { ln ->
// do something with the line (a String)
}
}
这一次没有使用列表;lambda 函数准确接收当前读取的行。use是文件系统资源在使用后正确关闭所必需的。
按块读取二进制数据文件有助于处理大型二进制文件:
import java.io.File
...
val file = dataDir.resolve("a.txt")
// Buffer size implementation dependent
file.forEachBlock{ buffer:ByteArray, bytesRead:Int ->
// do something with the buffer
}
// Or, if you want to prescribe the buffer size
file.forEachBlock(512) { buffer, bytesRead ->
// do something with the buffer
}
删除文件
删除你写的文件或目录
import java.io.File
...
val file:File = ...
val wasDeleted:Boolean = file.delete()
这对文件和目录都有效;但是,该目录不得包含任何文件。要删除一个目录及其所有内容,包括其他目录,您可以使用以下命令:
import java.io.File
...
val file:File = ...
val wasDeleted:Boolean = file.deleteRecursively()
如果在删除内容时发生了任何事情,例如,由于缺少访问权限而无法删除某个文件,那么您将得到一个部分删除的文件结构。也可以在应用中处理文件,并在应用终止时请求自动删除:
import java.io.File
...
val file:File = ...
file.deleteOnExit()
如果你的应用中有几个deleteOnExit(),删除会以相反的顺序进行。注意,对于普通的delete()调用,对目录也可以这样做,但是它们必须是空的。
使用临时文件
如果你需要临时文件,它更容易使用
import java.io.File
...
val prefix = "tmpFile"
val suffix = ".tmp"
val tmpFile:File = File.createTempFile(prefix, suffix)
tmpFile.deleteOnExit()
... use tmpFile
与手动创建临时文件相比。
这将使用由您的操作系统提供的目录,特别是临时文件,它将通过在文件名中添加一些随机但唯一的字符来确保该文件不存在。对于前缀和后缀,您可以使用您想要的,但前缀必须至少有三个字符长。如果您使用null作为后缀,默认情况下会使用.tmp。
如果您想为临时文件提供自己的目录,只需添加一个表示该目录的File作为createTempFile()的第三个参数。
更多文件操作
使用我们已经知道的函数复制一个文件是相对容易的:file2.writeBytes( file1.readBytes() )。还有一个库函数,使它更有表现力,还增加了一些选项:
import java.io.File
...
val file1:File = ...
val file2:File = ...
f1.copyTo(f2) // f2 must not exist
f1.copyTo(f2, true) // overwrite if necessary
// To fine-tune performance, you can tweak the
// buffer size
f1.copyTo(f2, bufferSize = 4096)
copyTo()函数返回目标文件。
另一个标准库函数提供了递归复制完整目录(包括所有子目录及其文件)的能力:
import java.io.File
...
val dir1:File = ...
val dir2:File = ...
f1.copyRecursively(f2) // f2 must not exist
f1.copyRecursively(f2, true) // overwrite if necessary
// To fine-tune error handling, you can add a handler.
// Otherwise an IOException gets thrown.
f1.copyRecursively(f2, onError = {
file:File, ioException:IOException ->
// do something.
// What to do now? Just skip this file, or
// terminate the complete function?
OnErrorAction.SKIP // or .TERMINATE
})
重命名文件是通过
import java.io.File
...
val file1:File = ...
val file2:File = ...
file1.renameTo(file2)
File类有更多的函数告诉我们文件的细节:
import java.io.File
import java.util.Date
...
val file = dataDir.resolve("a.txt")
val log = { msg:String -> Log.d("LOG", msg) }
log("Name: " + file.name)
log("The file exists: " + file.exists())
log("You can read the file: " + file.canRead())
log("You can write to the file: " + file.canWrite())
log("Is a directory: " + file.isDirectory())
log("Is a real file: " + file.isFile())
log("Last modified: " + Date(file.lastModified()))
log("Length: " + file.length())
注意
如果你需要更多的细节,java.nio包包含了更多的类和函数,这些类和函数提供了关于文件的更多信息。
读取 URL
文件 API 包含非常方便的函数来读取互联网 URL 的内容。只管写
import java.net.URL
import kotlin.concurrent.thread
thread {
val contents:String =
URL("http://www.example.com/something.txt").
readText()
val isoContents =
URL("http://www.example.com/something.txt").
readText(Charsets.ISO_8859_1)
val img:ByteArray =
URL("http://www.example.com/somepic.jpg").
readBytes()
}
注意
在 Android 上,您必须请求互联网访问权限才能工作。在AndroidManifest.xml文件的manifest元素内添加<uses-permission android:name = "android.permission.INTERNET"/>。
在 Android 上,这必须在后台线程中运行。这就是为什么我将读取操作包装在一个thread{ }结构中。这很容易,但在一个严肃的应用中,你应该使用 Android 的一个真正的后台执行功能,例如一个IntentService。这意味着要做更多的工作。更多细节请参考 Android 文档。
这只是一种非常简单的访问互联网资源的方式。要获得更多选项,请使用专用软件,例如 Apache HttpClient 库。
使用反射
反射就是将类视为对象。这怎么可能呢?我们了解到对象是类的实例。不过,我们也了解到,对象是可识别的单元,它们通过属性来描述某些东西,并提供了使用函数对属性进行操作的方法。
诀窍是:类也是可识别的单元,如果你想描述它们,你需要解释它们的属性和功能的本质。反射就是这样:类是描述它们所引用的类的属性和功能的对象。此外,我们还可以动态地查找一个类实现的接口,以及可能的超类。
注意
Kotlin 反射不是标准库的一部分。你必须加上
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
(一行)到你的应用模块的build.gradle文件的依赖部分。
我们从一个简单的类开始,它扩展了一些基类,实现了一些任意的接口,此外还有一个构造函数、两个属性和一个函数:
import android.util.Log
open class MyBase(val baseProp:Int)
class MyClass(var prop1:Int) :
java.io.Serializable, MyBase(13) {
var prop2:String
get() = "Hi"
set(value) { /* ignore */ }
init {
Log.d("LOG", "Hello from init")
}
fun function(i:Int):Int {
return prop1 * i
}
}
val instance = MyClass(42)
我们首先注意到有一个用于描述类对象(不是类实例)的Class类。是从包java.lang来的。然而,与 Java 相比,Kotlin 表现出一些特性,这使得 Kotlin 有必要拥有自己的类 class。它叫KClass,你可以在kotlin.reflect包里找到。他们彼此关系密切。我们为MyClass取KClass:
val clazz = MyClass::class
// We can also get it from instances
val clazz = instance::class
如果您需要的话,仍然可以从这里获得 Java 类:val javaClass = clazz.java
一旦我们有了一个KClass对象,我们就可以自省这个类,这让我们可以显示构造函数、属性和函数:
import android.util.Log
import kotlin.reflect.*
import kotlin.reflect.full.*
Log.d("LOG", "**** constructors")
clazz.constructors.forEach { c ->
Log.d("LOG", c.toString())
}
// show only our own properties
Log.d("LOG", "**** declaredMemberProperties") clazz.declaredMemberProperties.forEach { p ->
Log.d("LOG", p.toString())
}
// show also inherited properties
Log.d("LOG", "**** memberProperties")
clazz.memberProperties.forEach { p ->
Log.d("LOG", p.toString())
}
// show only our own functions
Log.d("LOG", "**** declaredFunctions")
clazz.declaredFunctions.forEach { f ->
Log.d("LOG", f.toString())
}
// show also inherited functions
Log.d("LOG", "**** functions")
clazz.functions.forEach { f ->
Log.d("LOG", f.toString())
}
如果使用查找过滤器,我们可以获得特定的属性或功能:
val p1: KProperty1<out MyClass, Any?> =
clazz.declaredMemberProperties.find {
it.name == "prop1" }!!
val f1: KFunction<*> =
clazz.declaredFunctions.find {
it.name == "function" }!!
有了KProperty1和KFunction实例,你可以做一些有趣的事情,比如发现它是私有的还是公共的,它是最终的还是开放的,或者一个属性是属于cons还是属于lateinit。对于函数,我们可以确定参数类型和返回类型,等等。请参考这些类的 API 文档来查看所有细节。
我们可以调用实际实例的函数,或者从实际实例中获取和设置属性:
...
val instance = MyClass(42)
val p1: KProperty1<out MyClass, Any?>? =
clazz.declaredMemberProperties.find {
it.name == "prop1" }!!
val p1Mutable: KMutableProperty1<out MyClass, Any?> =
p1 as KMutableProperty1
// getting
val prop1Val = p1.getter.call(instance)
// setting
p1Mutable.setter.call(instance, 55)
// invoking
val f1: KFunction<*> =
clazz.declaredFunctions.find {
it.name == "function" }!!
val res = f1.call(instance, 44) as Int
我们可以获取该类继承的超类和接口:
// Only directly declared superclasses and interfaces
clazz.superclasses.forEach { sc ->
Log.d("LOG", sc.toString())
}
// All superclasses and interfaces
clazz.allSuperclasses.forEach { sc ->
Log.d("LOG", sc.toString())
}
要动态创建实例,我们必须区分无参数构造函数和带参数的构造函数:
val clazz : KClass = ...
// If we have a no-arg primary constructor
val instance1 = clazz.createInstance()
// Otherwise for the primary constructor
val instance2 = clazz.primaryConstructor?.call(
[parameters]
)
// Otherwise
val instance3 = clazz.constructors.
find { [criterion] }!!.
call( [parameters] )
警告
不要错误地将反射在普通类、属性和函数使用上的改进视为对所有属性和函数访问使用反射是编写程序的更好方式的标志。使用反射,你会得到相当大的性能下降,你会失去表达性和简洁性,你还会发展出某种程度上的“围绕”面向对象。小心使用反射。
正则表达式
正则表达式试图给出以下问题的答案:
-
字符串是否包含某种字符模式?例如,我们想知道字符串
invoice 2018-01-01-A4536是否包含以A开头的子串。或者同一个字符串是否包含任何日期yyyy-MM-dd。我们想在这里变得非常多才多艺;模式应该允许我们指定字符类,如字母、小写字母、大写字母、数字、字符枚举、空格、重复等等。 -
如何以模式的形式在分隔符处分割字符串?例如,我们有一个字符串
A37 | Q8 | 156-WE,我们想在|处对其进行分割,以得到一个字符串数组[ "A37 ", " Q8 ", " 156-WE" ]。对于拆分标记,还应该可以指定一个更长的字符串或模式。 -
给定一个模式,我们如何从一个字符串中提取某些子字符串?例如,我们有一个字符串
The invoice numbers are X-23725, X-7368 and X-71885,我们想提取所有的发票号码X-<some digits>来得到一个数组[ "X-23725", "X-7368", "X-71885" ]。 -
如何用其他字符串替换一个字符串中的某些模式?例如,我们有一个字符串
For version v1.7 it is possible, ... another advantage of version v.1.7 is that ...,我们想用LEOPARD替换所有出现的v<digit>.<digit>。
模式
在讨论如何实现正则表达式操作之前,我们先研究一下可用于这些操作的模式。模式是具有正则表达式结构的字符串,如表 17-2 所示。您可以在普通字符串中输入带有转义反斜杠()的模式:因此模式^\w{3}(开头三个单词字符)必须作为^\\w{3}输入。您可以使用 raw 字符串来避免转义:
val patStr = "^\\w{3}$" // exactly 3 word chars
val patStr2 = """^\w{3}$""" // the same
注表 17-2 并不详尽;它显示了最常用的结构。要获得完整的参考资料,请查阅java.util.regex.Pattern的 Java API 文档。
表 17-2。
正则表达式模式
|建造
|
比赛
|
| --- | --- |
| x | 任何字符 x |
| \\ | 反斜杠字符\ |
| \X | 文字 X,如果 X 代表模式构造 |
| \n | 换行符 |
| \r | 回车符 |
| [abc] | a、b 或 c 中的任何一个 |
| [^abc] | 除了 a、b 或 c 之外的任何东西 |
| [A-Z] | 介于 A 和 Z 之间的任何东西 |
| [0-9a-z] | 0 和 9 之间或 a 和 z 之间的任何值 |
| . | 任何字符 |
| \d | 任何数字[0–9] |
| \D | 任何非数字[⁰–9] |
| \s | 空白字符 |
| \s | 非白人角色 |
| \w | 一个单词字符[A–Z _ A–Z _ 0–9] |
| \W | 非文字字符[^\w] |
| ^ | 一行的开始 |
| $ | 一行的结尾 |
| \b | 单词边界 |
| \B | 非单词边界 |
| xy | 一个 x 后面跟着一个 y |
| x|y | 不是 x 就是 y |
| (p) | 任何子模式 p 作为一个组 |
量词用于声明模式结构的重复。量词有三种类型:
-
Greedy :在模式匹配期间,模式将尽可能多地消耗字符串,而不会阻碍后续的模式部分。
-
*勉强:*在模式匹配期间,模式将只消耗必要的字符串。
-
*所有格:*在模式匹配过程中,模式会消耗尽可能多的字符串,而不考虑后续的模式部分。
贪婪和不情愿的量词用得最多,而所有格量词或多或少只适用于特殊情况。为了理解其中的区别,请考虑输入字符串012345abcde和模式\d+.*。这里的*表示零次或多次贪婪,+表示一次或多次贪婪。如果我们执行匹配,\d+将消耗尽可能多的数字(即所有数字,012345)。作为任何字符匹配器的.*将匹配剩余的abcde。如果我们使用不情愿的模式\d+?.*?,那么\d+?将匹配尽可能多的数字。因为凭借+的\d+?匹配器乐于一个数字出现一次,并且.*?匹配器能够匹配任意数量的字符,所以\d+?将乐于匹配0,并且.*?匹配器将消耗剩余的12345abcde。
不太重要的所有格量词的功能最好用输入字符串012345abcde和所有格模式.*+de来描述。这里的.*+匹配器能够将字符串从头到尾匹配一遍。因为它不关心模式的其余部分,它会消耗所有字符。然而,de需要已经消耗的弦部分de;因此,它没有匹配的内容,整个正则表达式匹配将会失败。量词列于表 17-3 中。
表 17-3。
正则表达式量词
|建造
|
类型
|
比赛
|
| --- | --- | --- |
| X? | 贪婪的 | x 一次或者根本不要。 |
| X* | | x 零次或更多次。 |
| X+ | | x 一次或多次。 |
| X{n} | | x 正好是n次。 |
| X{n,} | | X n次或更多次。 |
| X{n,m} | | x 次n到m次。 |
| X?? | 不情愿的 | x 一次或者根本不要。 |
| X*? | | x 零次或更多次。 |
| X+? | | x 一次或多次。 |
| X{n}? | | x 正好是n次。 |
| X{n,}? | | X n次或更多次。 |
| X{n,m}? | | x 次n到m次。 |
| X?+ | 所有格 | x 一次或者根本不要。 |
| X*+ | | x 零次或更多次。 |
| X++ | | x 一次或多次。 |
| X{n}+ | | x 正好是n次。 |
| X{n,}+ | | X n次或更多次。 |
| X{n,m}+ | | x 次n到m次。 |
确定匹配
要查看字符串是否匹配给定的正则表达式,可以使用以下函数:
val re = Regex("^\\w{3}$") // exactly 3 word chars
val matches1 = "Hello".matches(re) // -> false
val matches2 = "abc".matches(re) // -> true
练习 2
写一个字符串扩展函数,允许我们写
"Hello" % ".*ll.*"
代替
"Hello".matches(Regex(".*ll.*"))
提示:运算符%写成.rem()。
Regex类具有允许指定一个或多个选项的构造函数:
Regex(pattern:String, option:RegexOption)
Regex(pattern:String, options:Set<RegexOption>)
RegexOption是一个包含以下成员的enum class(完整列表见 API 文档):
-
IGNORE_CASE:使用它来执行不区分大小写的匹配。 -
DOT_MATCHES_ALL:如果你想让.图案也包含换行符,使用此选项。 -
MULTILINE:如果你想让^和$考虑换行符,使用这个。 -
COMMENTS:允许正则表达式模式下的注释。
如果添加了RegexOption.COMMENTS标志,就可以向正则表达式模式添加注释。如果正则表达式更复杂,这是非常宝贵的。举例来说,请考虑以下情况:
val re1 = Regex("^A(/|_)\\d{4}$")
// This is the same:
val ENDS = "$"
val re2 = Regex("""
^ # begins with
A # an "A"
(/|_) # a "/" or a "_"
\d{4} # 4 digits
$ENDS # ends here
""", RegexOption.COMMENTS)
(忽略多空格警告。)我们必须在这里添加笨拙的val ENDS = "$",以避免$导致的字符串插值。您可以看到空格被忽略(如果您需要在模式中包含空格,请使用\s)并且#开始一行注释。
拆分字符串
将正则表达式周围的字符串拆分为您编写的分隔符
val re = Regex("\\|")
// <- use "\" escape to get a "|" as a literal
val s = "ABC|12345|_0_1"
val split: List<String> = s.split(re)
// -> "ABC", "12345", "_0_1"
// limit to at most 37 splits
val split37 = s.split(re, limit = 37)
注意
为了将一个包含换行符的大字符串拆分成多个行,出于性能原因,您可能不想使用正则表达式。使用lines()函数要简单得多,它可以应用于任何字符串:val s = "big string... "; s.lines().forEach { ln -> ... }
提取子字符串
在字符串中寻找模式并实际提取它们是通过Regex类的函数实现的:
// a number pattern
val re = Regex("""
-? # possibly a "-"
\d+ # one or more digits
(
\. # a dot
\d+ # one or more digits
)? # possibly
""", RegexOption.COMMENTS)
val s = "x = 37.5, y = 3.14, z = -100.0"
val firstNumber:MatchResult? = re.find(s)
// start at a certain index instead:
// val firstNumber = re.find(s, 5)
val notFound = firstNumber == null
firstNumber?.run {
val num = groupValues[0]
// do something with num...
}
val allNumbers:Sequence<MatchResult> = re.findAll(s)
allNumbers.forEach { mr ->
val num = mr.groupValues[0]
// do something with num...
}
如果我们想将每个模式匹配分配给一个本地属性,这是没问题的。然而,还有更多:我们可以获得匹配的组,它们属于由( )对定义的子模式。考虑稍微重写的数字匹配器:
val re = Regex("""
(
(
-? # possibly a "-"
\d+ # one or more digits
)
(
\. # a dot
(
\d+ # one or more digits
)
)? # possibly
)
""", RegexOption.COMMENTS)
它仍然匹配相同的模式,但是通过不同的( )组引入了子模式。如果我们将这种模式应用于一个数字,例如,3.14,为了便于说明,我们可以添加相应的组,这样我们就得到了((-3)(.(14)))。这样的群体很容易在MatchResult中独立解决:
// The pattern from the last listing compressed
val re = Regex("""((-?\d+)(\.(\d+))?)""")
val s = "x = 37.5, y = 3.14, z = -100.0"
val firstNumber:MatchResult? = re.find(s)
val notFound = firstNumber == null
firstNumber?.run {
val (num, nf, f1, f2) = destructured
// <- "37.5", "37", ".5", "5"
// the same:
// val num = groupValues[1]
// val nf = groupValues[2]
// val f1 = groupValues[3]
// val f2 = groupValues[4]
val wholeMatch = groupValues[0] // 37.5
// ...
}
val allNumbers:Sequence<MatchResult> = re.findAll(s)
allNumbers.forEach { mr ->
val (num, nf, f1, f2) = mr.destructured
// the same:
// val num = mr.groupValues[1]
// val nf = mr.groupValues[2]
// val f1 = mr.groupValues[3]
// val f2 = mr.groupValues[4]
val wholeMatch = mr.groupValues[0]
// ... wholeMatch is: 37.5, 3.14 or -100.0
// ... num is: 37.5, 3.14 or -100.0
// ... nf is: 37, 3, -100
// ... f1 is: .5, .14, .0
// ... f2 is 5, 14, 0
}
您可以看到,在MatchResult实例的groupValues属性中,索引 0 元素总是引用整个匹配,而所有其他索引都引用( )组。destructured属性从第一个( )组开始。只是因为我们添加了一个包含一切的大包围( ),所以destructured的第一个成员包含了与groupValues[0]相同的字符串。
警告
属性虽然易于使用,但最多只能处理十个组。属性groupValues可能是无限的。
取代
替换字符串中的模式类似于查找模式。我们有一个函数replaceFirst(),它只替换模式的第一次出现,还有一个函数replace(),它替换所有出现的模式:
// again the number pattern:
val re = Regex("""((-?\d+)(\.(\d+))?)""")
val s = "x = 37.5, y = 3.14, z = -100.0"
// replace the first number by 22.22
val s2 = re.replaceFirst(s, "22.22")
// -> "x = 22.22, y = 3.14, z = -100.0"
// replace all numbers by 22.22
val s3 = re.replace(s, "22.22")
// -> "x = 22.22, y = 22.22, z = 22.22"
不过,这两个替换函数还有更多功能。用 lambda 函数替换第二个参数,我们可以在替换过程中施展真正的魔法(仅针对replace();对于replaceFirst(),使用适当的等效物):
// again the number pattern:
val re = Regex("""((-?\d+)(\.(\d+))?)""")
val s = "x = 37.5, y = 3.14, z = -100.0"
// double all numbers
val s2 = re.replace(s, { mr:MatchResult ->
val theNum = mr.groupValues[1].toDouble()
(theNum * 2).toString() // <- replacement
})
// -> "x = 75.0, y = 6.28, z = -200.0"
// zero all fractions
val s3 = re.replace(s, { mr:MatchResult ->
val (num, nf, f1, f2) = mr.destructured
nf + ".0" // <- replacement
})
// -> "x = 37.0, y = 3.0, z = -100.0"
十八、并行工作:多线程
现代计算机和现代智能手机有几个能够并行工作的 CPU。你可能会想到几个应用同时运行,但是并发性并不简单;你可以让几个“演员”在一个应用中并行工作,显著加快程序的执行速度。我故意说“演员”,因为简单地说几个 CPU 并行工作只涵盖了故事的一部分。事实上,软件开发人员更喜欢把*线程、*看作是潜在的可以彼此独立运行的程序序列。哪个 CPU 实际运行一个线程是留给操作系统管理的进程调度。我们采用线程概念,并从操作系统进程处理和硬件执行内部抽象出来。
在一个应用中,几个线程同时运行通常被称为多线程。多年来,多线程一直是 Java 的重要组成部分,您可以在包java.lang和java.util.concurrent以及子包中找到 Java 的相关接口和类。这些也包含在 Kotlin for Android 中。然而,Kotlin 对多线程有自己的想法,并引入了一种叫做协程的技术。你可以使用这两种特性,在这一章中我们将讨论这两种特性。
Java 方式的基本多线程
无需任何进一步的准备,当你启动一个 Kotlin(或 Java)应用时,程序就会在主线程中运行。但是,您可以在主线程运行的同时定义并启动其他线程。
注意
在 Android 开发环境中,Kotlin 可以自动使用 Java 多线程类。
Java 中最重要的多线程相关类是java.util.Thread。您可以使用它的构造函数创建一个线程,但是 Kotlin 有一个简化线程创建的函数:thread()。它的概要是这样的:
fun thread(
start: Boolean = true,
isDaemon: Boolean = false,
contextClassLoader: ClassLoader? = null,
name: String? = null,
priority: Int = -1,
block: () -> Unit
)
例如,您可以按如下方式使用它:
val thr:Thread = thread(start = true) {
... do something ...
}
thread()函数使用以下特征创建一个Thread:
-
如果没有显式地指定
start参数来读取false,那么Thread.start()函数会在线程创建后立即被调用。 -
如果将
isDaemon设置为true,当主线程完成工作时,正在运行的线程不会阻止运行时引擎关闭。然而,在 Android 环境中,当系统决定关闭或暂停一个应用时,非守护化线程不会使应用继续活跃,所以这个标志对 Android 没有明显的影响。 -
如果您希望线程使用不同于系统类装入器的类装入器,那么指定一个单独的类装入器是一项高级功能。在本书中,我们不讨论类加载问题;通常,在 Android 环境中,您可以安全地忽略类加载问题。
-
如果出现问题,为线程指定单独的名称有助于故障排除。线程的名称可以显示在日志文件中。
-
指定优先级给了系统一个提示,告诉它一个线程相对于其他线程应该如何优先。数值范围从
Thread.MIN_PRIORITY到Thread.MAX_PRIORITY。默认值为Thread.NORM_PRIORITY。对于您的第一次实验,您不必关心这个值。 -
block包含线程运行时执行的语句。无论block做什么,运行多长时间,thread()函数总是立即退出。
Android 应用最基本的线程示例可能是这样的(请记住,作为最后一个调用参数的函数可以放在括号外):
// inside an activity:
override fun onCreate(savedInstanceState: Bundle?) {
...
thread {
while(true) {
Thread.sleep(1000L)
Log.e("LOG", Date().toString())
}
}
}
对于您的实验,您可以使用我们在前面章节中开发的NumberGuess示例应用。这个线程开始一个无限循环(while( true ){ }),每次迭代休眠 1000 毫秒,然后将当前日期和时间写入日志控制台。thread()函数返回Thread实例,所以如果我们以后需要用线程做更多的事情,我们也可以写
val thr:Thread = thread {
while(true) {
Thread.sleep(1000L)
Log.e("LOG", Date().toString())
}
}
由于缺省值start = true,线程立即在后台开始工作。然而,如果你想自己开始线程,你写
val thr = thread(start = false) {
while(true) {
Thread.sleep(1000L)
Log.e("LOG", Date().toString())
}
}
...
thr.start()
到目前为止,这听起来很容易,不是吗?不过,我们在本书后面的章节中讨论多线程是有原因的。考虑以下示例:
val l = mutableListOf(1,2,3)
var i = 0
thread {
while(true) {
Thread.sleep(10L)
i++
if(i % 2 == 0) { l.add(i) }
else { l.remove(l.first()) }
}
}
thread {
while(true) {
Thread.sleep(1000L)
Log.e("LOG", l.joinToString())
}
}
这里我们让一个线程每 10 毫秒改变一个列表,另一个线程将列表打印到日志控制台。
一旦你开始这样做,在你的应用崩溃之前应该不会超过几毫秒。发生了什么事?日志上写着(缩写):
2018-12-29 09:40:52.570 14961-14983/
android.kotlin.book.numberguess
E/AndroidRuntime: FATAL EXCEPTION: Thread-5
Process: android.kotlin.book.numberguess, PID: 14961
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.next(...)
at ...CollectionsKt.joinTo(...)
at ...CollectionsKt.joinToString(...)
at ...CollectionsKt.joinToString...
at ...MainActivity$onCreate$2.invoke...
at ...MainActivity$onCreate$2.invoke...
at ...ThreadsKt....run()
重要的部分是在java.util.ConcurrentModificationException和java.util.ArrayList$Itr.next(...)的两条线。后者表示当我们遍历列表时发生了一些事情。这个迭代需要为joinToString()函数构造字符串。主要线索来自异常名:
ConcurrentModificationException
它基本上是说,当另一个线程修改一个列表时,我们正在遍历它,这就是问题所在:如果我们让几个线程同时修改一个列表的结构并遍历它,我们就会有一个列表数据不一致的问题。
当我们谈论多线程时,出现的另一个问题是我们需要找到一种聪明的方法来同步线程。例如,一个线程需要等待另一个线程完成一些工作,然后才能开始运行。
这两个问题——数据一致性和同步——使多线程成为一种艺术,直到现在还没有找到最终的通用解决方案。这就是为什么,关于多线程,新的想法不断诞生,几种方法同时存在,所有这些方法相对于其他方法都有各自的优点和缺点。
在讨论 Java 和 Kotlin 遵循的高级方法之前,我们先完成对 Java 的基本多线程解决方案的研究,这样我们就对问题领域有了一个了解。如果我们再次考虑并发修改异常的例子,如果我们能够避免多个线程同时在一个共享列表上工作,不是更好吗?这是可能的,我们可以这样做的方法是将相关的代码示例包装在synchronized(){ }块中,如下所示:
val l = mutableListOf(1,2,3)
var i = 0
thread {
while(true) {
Thread.sleep(10L)
i++
synchronized(l) {
if(i % 2 == 0) { l.add(i) }
else { l.remove(l.first()) }
}
}
}
thread {
while(true) {
Thread.sleep(1000L)
synchronized(l) {
Log.e("LOG", l.joinToString())
}
}
}
在这里,所有访问列表的线程中的synchronized(l)块确保当另一个线程在同一列表的任何其他synchronized块中时,没有访问列表的线程可以进入synchronized中的代码。相反,最先到达的线程让所有其他线程等待,直到它完成了它的synchronized块。
也可以向synchronized指令添加更多参数。只是用逗号分隔的列表,如
synchronized(l1, l2) {
...
}
其中同步确保让多个线程在l1和l2上工作是安全的。
我们仍然需要一种方法让一个线程等待另一个线程完成它的工作。为此目的,join指令存在。假设您想要实现以下目标:
val l = mutableListOf(1,2,3)
var i = 0
val thr1 = thread {
for(i in 1..100) {
l.add(i)
Thread.sleep(10)
}
}
thread {
// Here we want to wait until thread thr1 is done.
// How can this be achieved?
...
Log.e("LOG", l.joinToString())
}
现在,告诉第二个线程显式地等待线程thr1通过thr1.join()完成它的工作:
val l = mutableListOf(1,2,3)
var i = 0
val thr1 = thread {
...
}
thread {
thr1.join()
Log.e("LOG", l.joinToString())
}
现在,thr1.join()之后的指令仅在线程thr1完成其工作后开始。
表 18-1 中列出了这些关键字和函数,以及 Java 方式的基本多线程的更有趣的函数和构造。
表 18-1
Java 方式的基本多线程
|构造/功能
|
描述
|
| --- | --- |
| thread(...) | 创建并可能启动一个线程。参数包括:start:施工后立即开始螺纹。默认:true。isDaemon:如果true,当主线程完成其工作时,正在运行的线程不会阻止运行时引擎关闭。在 Android 中没有影响。默认:false。contextClassLoader:指定不同的类加载器。默认为null,表示系统类加载器。对于 Android,你通常使用默认设置。name:线程的名称。显示在日志文件中。默认:使用带有连续编号的默认字符串。priority:指定一个优先级给系统一个提示,告诉它一个线程相对于其他线程的优先级。可能值:在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间,默认为Thread.NORM_PRIORITY。block:包含线程的代码。如果你不需要任何特殊的参数,你只需要写thread { [thread_code] }。 |
| synchronized( object1, object2, ...) { } | 只有当当前没有其他线程在参数列表中至少有一个相同对象的synchronized块中执行时,才会进入{ }块。否则,线程将处于等待状态,直到其他相关的synchronized模块完成它们的工作。 |
| Thread.sleep(millis: Long) | 让当前线程等待指定的毫秒数。可以被中断,在这种情况下,语句立即终止,并抛出一个InterruptedException。 |
| Thread.sleep(millis: Long, nanos:Int) | 与Thread.sleep(Long)相同,但使该功能额外休眠nanos纳秒。 |
| thread.join() | 让当前线程等待,直到线程thread完成其工作。 |
| thread.interrupt() | 当前线程中断线程thread。被中断的线程被终止并抛出一个InterruptedException。被中断的线程必须支持中断。它通过调用像Thread.sleep()这样的可中断方法或者通过定期检查自己的Thread.interrupted标志来查看它是否应该退出。 |
| @Volatile var varName = ... | 仅适用于类或对象属性。将支持字段(属性后面的数据)标记为volatile。运行时引擎(Java 虚拟机)确保 volatile 变量的更新立即传达给所有线程。否则在这种情况下线程间的状态可能会不一致。与synchronized块相比,性能开销更小。 |
| Any.wait() | 仅从synchronized块内部。挂起同步,以便其他线程可以继续工作。同时,它让这个线程等待一段不确定的时间,直到notify()或notifyAll()被调用。 |
| Any.wait( timeout:Long ) | 与wait()相同,但最多等待指定的毫秒数。 |
| Any.wait( timeout:Long, nanos:Int ) | 与wait()相同,但最多等待指定的毫秒和纳秒数。 |
| Any.notify() | 仅从synchronized块内部。唤醒一个正在等待的线程。一旦当前线程离开它的synchronized块,等待线程就开始工作。 |
| Any.notifyAll() | 仅从synchronized块内部。唤醒所有等待的线程。一旦当前线程离开它的synchronized块,等待线程就开始工作。 |
对于类java.lang.Thread的所有其他函数,请参考 API 文档。
Java 方式的高级多线程
将synchronized块和join函数分散在你的代码中会带来几个问题:首先,它使你的代码难以理解;对于重要的程序来说,理解多线程状态处理绝非易事。第二,拥有几个线程和synchronized块可能会导致死锁:一些线程 A 等待线程 B,而线程 B 正在等待线程 A。第三,编写太多的join函数来收集线程的计算结果可能会导致太多的线程只是等待,从而削弱多线程的优势。第四,使用synchronized块进行任何集合处理也可能导致太多线程等待。
在 Java 发展历史的某个时刻,引入了高级的多线程结构,即java.util.concurrent包和子包中的接口和类。在本节中,我们并不要求完整,而是涵盖了其中的一些构造,因为它们也包含在 Kotlin 中,您可以根据自己的意愿任意使用它们。
特殊并发集合
仅仅为了适当的并发可访问性,或者为了线程安全,而将任何列表或集合访问封装到synchronized块中,会给人一种不满意的感觉。如果集合和地图对您的应用很重要,那么考虑多线程似乎不值得。幸运的是,java.util.concurrency包包含一些列表、集合和映射实现,有助于避免将所有东西都放入synchronized块中。
-
CopyOnWriteArrayList:一个列表实现,其中任何变异操作都发生在完整列表的一个新副本上。同时,任何迭代都精确地使用迭代器创建时的列表状态,所以不会发生ConcurrentModificationException。复制完整的列表代价很高,所以这种实现通常只在读操作远远多于写操作的情况下有用。然而,在这种情况下,线程安全不需要synchronized块。 -
CopyOnWriteArraySet:集合实现,其中任何变异操作都发生在完整集合的新副本上。我们之前针对CopyOnWriteArrayList所说的也适用于CopyOnWriteArraySet实例。 -
ConcurrentLinkedDeque:线程安全的Deque,其中迭代操作是弱一致的,这意味着读取元素反映了迭代器创建时或创建后某个时间点的队列状态。没有ConcurrentModificationException会被扔。 -
ConcurrentLinkedQueue:线程安全的Queue实现。前面针对ConcurrentLinkedDeque所做的关于线程安全的讨论也适用于这个类。不会扔出ConcurrentModificationException。 -
ConcurrentSkipListSet:线程安全的Set实现。迭代操作是弱一致的,意味着读元素反映了集合在迭代器创建时或创建后的状态。没有ConcurrentModificationException会被扔。除了 API 文档建议的类型规范,元素必须实现Comparable接口。 -
ConcurrentSkipListMap:线程安全的Map实现。迭代操作是弱一致的,这意味着读取元素反映了迭代器创建时或创建后某个时刻的映射状态。没有ConcurrentModificationException会被扔。除了 API 文档建议的类型规范,这些键必须实现Comparable接口。
锁
在本章前面的“Java 方式的基本多线程”一节中,我们了解到synchronized块确保不同的线程不能同时处理不同的程序部分:
val obj = ...
thread {
synchronized(obj) {
... synchronized code
}
}
这样的synchronized块是一个语言构造;然而,我们可以通过使用如下的lock对象以更加面向对象的方式实现同样的事情:
import java.util.concurrent.lock.*
...
val lock:Lock = ...
...
lock.lock()
try {
... synchronized code
} finally {
lock.unlock()
}
更准确地说,synchronized在所谓的重入锁中有它的对等物,相应的锁类相应地读为ReentrantLock。因此,在前面的代码中,我们将使用
val lock:Lock = ReentrantLock()
作为一个Lock实现。
名称重入锁来自锁被同一个线程多次获取的能力,因此当线程已经通过lock.lock()获取锁并试图在unlock()发生之前再次获取同一个锁时,它不会陷入等待状态。
与synchronized相比,Lock有更多的选择。例如,使用Lock可以避免在当前线程最近进入中断状态时试图锁定,或者在等待锁定时试图锁定。这可以通过写作来实现
val lock:Lock = ReentrantLock()
...
try {
lock.lockInterruptibly()
} catch(e: InterruptedException) {
... do things if we were interrupted
return
}
try {
... synchronized code
} finally {
lock.unlock()
}
您还可以首先检查锁,看它是现在被获取还是在实际被获取之前的一段时间内被获取。相应的代码如下所示
val lock:Lock = ReentrantLock()
...
if(lock.tryLock()) {
try {
... synchronized code
} finally {
lock.unlock()
}
} else {
... no lock acquired
... do other things
}
或者在等待特定时间量的变体中:
...
if(lock.tryLock(time:Long, unit:TimeUnit)) {
// lock was acquired within that time span
...
} else {
...
}
一个不同的锁接口叫做ReadWriteLock。与普通的Lock相比,它能够区分读写操作。在几个线程能够以只读方式使用变量而没有任何问题的情况下,这可能是有帮助的,而写操作必须阻止读操作,此外还必须被限制到单个线程。相应的实现读作ReentrantReadWriteLock。它的使用细节可以在 API 文档中找到。
原子变量类型
考虑以下示例:
class Counter {
var c = 0
fun increment() { c++ }
fun decrement() { c-- }
}
因为运行时引擎(Java 虚拟机 JVM)在内部将c++分解为(1)获取c的值,(2)增加我们刚刚检索到的值,以及(3)将更改后的值写回到c,所以可能会发生以下情况:
Thread-A calls increment
Thread-B calls decrement
Thread-A retrieves c
Thread-B retrieves c
Thread-A increments its version of c
Thread-A updates c, c is now +1
Thread-B decrements its version of c
Thread-B updates c, c is now -1
线程 A 的工作因此完全丢失了。这种效应通常被称为螺纹干涉。
我们在上一节中看到,通过synchronized进行同步有助于:
class Counter {
var c = 0
fun increment() { synchronized(c){ c++ } }
fun decrement() { synchronized(c){ c-- } }
}
凭借synchronized对c的更新现在不再受其他线程的影响。然而,我们可能有不同的解决方案。如果我们有一个以原子方式处理修改和检索的变量类型,并且没有其他线程干扰和破坏一致性的机会,我们可以减少 ?? 带来的开销。这样的原子数据类型确实存在,它们被称为AtomicInteger、AtomicLong和AtomicBoolean。都是来自java.util.concurrent.atomic包。
使用一个AtomicInteger我们可以去掉synchronized块。对于Counter类的解决方案将如下所示:
import java.util.concurrent.atomic.*
...
class Counter {
var c:AtomicInteger = AtomicInteger(0)
fun increment() { c.incrementAndGet() }
fun decrement() { c.decrementAndGet() }
}
注意
包java.util.concurrent.atomic有一些特殊用例的原子类型。如果你有兴趣的话,可以看看文档。
遗嘱执行人、期货和可赎回权
在java.util.concurrent包中,你会发现一些在更高层次上处理多线程的接口和类。下面的列表显示了对高级多线程很重要的主要接口和类。
-
Callable这是可以被另一个线程调用并返回结果的东西。
-
Runnable这个不在
java.util.concurrent包里,在java.lang包里。它是可以被调用的东西,可能被另一个线程调用。不返回任何结果。 -
Executors这是一个重要的实用程序类,用于获取
ExecutorService和ScheduledExecutorService实现。 -
ExecutorService这是一个对象接口,允许调用
Runnable或Callable并收集它们的结果。 -
ScheduledExecutorService这是一个对象接口,允许调用
Runnable或Callable并收集它们的结果。调用在一段延迟之后发生,或者以重复的方式发生。 -
Future这是一个可以用来从
Callable.获取结果的对象 -
ScheduledFuture这是一个可以用来从提交给
ScheduledExecutorService的Callable中获取结果的对象。
这些接口和类的主要使用模式如下:
-
从单例对象
Executors中使用一个以new开始的函数来获得一个ExecutorService或ScheduledExecutorService。将其保存在属性中;出于我们的目的,我们称之为srvc或schedSrvc。 -
对于需要同时完成的注册任务,对于
srvc使用以invoke或submit开头的任何功能,对于schedSrvc使用以schedule开头的任何功能。 -
等待终止,由来自
ExecutorService或ScheduledExecutorService的合适函数发出信号,或者由您在上一步骤中可能收到的Future或ScheduledFuture发出信号。
如您所见,这些接口、类和函数主要编排线程及其计算结果。他们不控制共享数据的使用;为此,您需要遵循前面几节中介绍的技术。
作为一个例子,我们开发了一个计算π的多线程程序。想法很简单:从[0; 1]×[0; 1]平面获得一对随机数。计算到原点的距离,并计算距离小于1.0和距离大于等于1.0的点的数量。称所有点的个数n和四分之一单位圆内的点数p。因为一个[0; 1] × [0; 1]平面的面积是1.0,但四分之一单位圆内区域的面积是π /4,所以我们有
= π /4或π = 4 · (见图 18-1 )。
图 18-1
圆周率计算
注意
这肯定不是计算π的最聪明的方法,但它很容易理解,并且您可以轻松地在多个线程之间分配工作负载。
在 Android Studio 中,启动一个新的应用,并按照第一章中为您的第一个 Kotlin 应用所述进行操作,相应地重命名应用和包。对于活动,使用以下元素创建布局:
图 18-2
Pi 用户界面
-
任何标签,如图 18-2 所示。
-
在
Processors标签旁边有一个 ID 为@+id/procs的TextView。 -
在
Iterations标签旁边有一个 ID 为@+id/iters的EditText。添加属性android:text="1000000"。 -
在
Threads标签旁边有一个 ID 为@+id/threads的EditText。添加属性android:text="4"。 -
在
Cumul Iters标签旁边有一个 ID 为@+id/cumulIters的TextView。 -
在
Current Pi标签旁边有一个 ID 为@+id/pi的TextView。 -
在
Calc Time标签旁边有一个 ID 为@+id/calcTime的TextView。 -
一个带有文本
CALC和属性android:onClick="calc"的Button。 -
一个带有文本
RESET和属性android:onClick="reset"的Button。
我们把设计的细节留给你。对于实际的计算,列表中显示的视图 id 和 onClick 处理程序很重要。计算并不太复杂,所以我们在活动类中做所有的事情。对于更复杂的项目,您应该将计算外包给一个或多个专门的计算类。在我们的例子中,让活动类阅读
class MainActivity : AppCompatActivity() {
var points = 0L
var insideCircle = 0L
var totalIters = 0L
override
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
savedInstanceState?.run {
points = getLong("points")
insideCircle = getLong("insideCircle")
totalIters = getLong("totalIter")
}
val cores = Runtime.getRuntime().
availableProcessors()
procs.setText(cores.toString())
}
override
fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
outState?.run {
putLong("points",points)
putLong("insideCircle",insideCircle)
putLong("totalIter", totalIters)
report()
}
}
fun calc(v:View) {
val t1 = System.currentTimeMillis()
val nThreads = threads.text.toString().
takeIf { it != "" }?.toInt()?:1
val itersNum = iters.text.toString().
takeIf { it != "" }?.toInt()?:10000
val itersPerThread = itersNum / nThreads
val srvc = Executors.newFixedThreadPool(nThreads)
val callables = (1..nThreads).map {
object : Callable<Pair<Int,Int>> {
override fun call(): Pair<Int, Int> {
var i = 0
var p = 0
(1..itersPerThread).forEach {
val x = Math.random()
val y = Math.random()
val r = x*x + y*y
i++
if(r < 1.0) p++
}
return Pair(i, p)
}
}
}
val futures = srvc.invokeAll(callables)
futures.forEach{ f ->
val p = f.get()
points += p.first
insideCircle += p.second
}
val t2 = System.currentTimeMillis()
calcTime.setText((t2-t1).toString())
report()
}
fun reset(v:View) {
points = 0
insideCircle = 0
report()
}
private fun report() {
cumulIters.setText(points.toString())
if(points > 0) {
val pipi = 1.0 * insideCircle / points * 4
pi.setText(pipi.toString())
} else {
pi.setText("")
}
}
}
其特点如下:
-
该类将
points中的总点数、insideCircle中的四分之一单位圆内的点数和totalIters中的总迭代次数作为状态。 -
在
onSaveInstanceState()和onCreate()中,我们确保当 Android 决定暂停应用时,状态得到保存和恢复。 -
同样在
onCreate()中,我们确定设备拥有的 CPU 数量,并将其写入用户界面。 -
在
reset()中,算法被重新初始化。 -
在
report()里面,我们根据前面的公式计算π,并写入用户界面。 -
多线程发生在
calc()内部。我们从用户界面读取要使用的线程数和迭代数,在线程间平均分配迭代数,从Executors获取线程池,定义并注册计算算法,最终从所有线程收集结果。 -
在
calc()结束时,我们确定计算所需的时间,并将其写入用户界面。
您可以试验一下线程和迭代次数,看看多线程的影响。在大多数设备上,您应该可以看到在一个线程和两个或更多线程上运行的明显区别。顺便说一下,随着数字的累积,多次按下 CALC 按钮可以提高计算π的精度。
练习 1
实施多线程π计算应用,如本节所述。
Kotlin·科特雷普
Kotlin 对如何处理多线程有自己的想法。它使用了在旧的计算机语言中已经存在了一段时间的概念,协程。这里实现的思想是编写函数,这些函数可以在内部程序流中的某些位置挂起,稍后再恢复。这是以一种非抢先的方式发生的,这意味着在以多线程方式运行程序的过程中,程序流上下文不会被操作系统切换,而是被语言构造、库调用或两者切换。
缺省情况下,Kotlin 中不包含协程。要安装它们,打开“app”模块的build.gradle文件并添加到“dependencies”部分:
implementation
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.0'
(一行)。
在我们继续讨论 Kotlin 的协程之前,我们首先提供一个扩展的词汇表,帮助您习惯协程编程。您可以快速浏览它,或者现在完全跳过它,稍后再回来,因为在这个列表之后,我们会给出对协程更全面的介绍。
- 协程作用域:任何协程功能都在协程作用域内运行。作用域就像多线程集合周围的括号,作用域可以有一个定义作用域层次结构的父作用域。作用域层次的根既可以是
GlobalScope也可以通过函数runBlocking { }获得,其中在{ }块中输入了一个新的阻塞作用域。这里的阻塞意味着runBlocking()调用只有在所有包含的作用域完成它们的工作后才结束。因为CoroutineScope是一个接口,所以您也可以定义任何类来产生协程作用域。一个非常突出的例子是让一个活动也代表一个协程作用域:
class MyActivity : AppCompatActivity(),
CoroutineScope by MainScope() {
...
override fun onDestroy() {
super.onDestroy()
cancel() // CoroutineScope.cancel
}
...
}
CoroutineScope by MainScope()指的是委托:任何CoroutineScope函数都被转发给MainScope的一个实例,这是一个专门为用户界面活动定制的作用域。在这里,由于作用域层次结构根上的cancel(),层次结构中任何当前活动的作用域都会在活动的onDestroy()中关闭。
-
协程上下文:
CoroutineContext对象是与协程范围相关联的数据容器。它包含协程作用域正常工作所必需的对象。在范围层次结构中没有任何进一步干预的情况下,范围子级从它们的父级继承上下文。 -
全局范围:
GlobalScope是一个单例对象,其生存期由应用整体决定。虽然使用全局作用域作为最重要的协程的基础很有诱惑力,但为了正确构建应用的多线程方面,通常不建议您使用它。请使用专用的协程生成器。 -
*作业:*作业是一个可能在自己的线程中运行的任务。
-
*协程构建器:*这些是以阻塞或非阻塞方式启动协程的函数。建筑商的例子有:
-
runBlocking():定义并运行一个阻塞协程。如果在协程作用域中使用了runBlocking(),那么由runBlocking{ }定义的块就会产生一个新的子作用域。 -
launch():定义并运行一个非阻塞的协程。它不能在协程范围之外使用。它立即返回一个Job对象,并在后台运行它的块。 -
async():定义并运行一个返回值的非阻塞协程。它不能在协程范围之外使用。它立即返回一个Deferred对象,并在后台运行它的块。 -
coroutineScope():除了一个新的Job对象之外,这将创建一个新的范围,其上下文继承自外部范围。它调用新范围中的指定块。 -
supervisorScope():这创建了一个带有SupervisorJob的新作用域。它调用新范围中的指定块。主管作业是一些特殊的作业,它们可以彼此独立地失败。
-
它们都期望 lambda 函数作为最后一个参数,作为要运行的指令块。因为这样的参数可以在括号外注明,所以您最常使用它们,就像在launch { ..instructions.. }中一样。
-
*加入:*如果得到调用属性:
val job = launch{ ... }中launch()的结果,可以稍后调用job.join()来阻止程序执行,直到作业完成。 -
*暂停功能:*关键字
suspend是 Kotlin 语言中唯一一个与协程相关的关键字。所有其他协程材质都可以通过协程库获得。您需要将suspend添加到您希望从作用域内部调用并且希望能够与协程一起使用的函数中。例如,suspend fun theFun() { }。将suspend视为协程范围转发器。 -
取消:一旦你有了一个
Job对象,你可以在它上面调用cancel()来发出取消作业的信号。注意,作业通常不会立即退出工作,所以您必须通过join()调用等待实际终止,或者使用cancelAndJoin()。您必须确保您想要取消的协同程序是可取消的。为了达到这个目的,你可以让代码调用一个类似于yield()的挂起函数,或者你可以定期检查你在协程作用域中自动拥有的isActive属性。 -
*超时:*为您使用的语句块显式指定超时
withTimeout(3000L) { // milliseconds ... code here }
一旦达到超时限制,就会抛出一个TimeoutCancellationException,它是CancellationException的子类,如果不使用定制的异常处理程序,就会被忽略。withTimeout()的一个变体读作withTimeoutOrNull();如果没有发生超时,它不抛出TimeoutCancellationException,而是返回其块中最后一个表达式的值,否则返回null:
-
*协程异常处理程序:*协程对于如何处理异常有自己的想法。例如,如果您没有在协程上下文中提供单独的
CoroutineExceptionHandler,当取消一个作业时,会抛出一个CancellationException,但是这个被忽略了。如果您需要在作业被取消后执行清理操作,您仍然可以将代码包装在一个try { } finally { }块中:runBlocking { val job = launch { try { ... do work } finally { ... cleanup if canceled } } ... job.cancelAndJoin() } -
*延迟:*使用
delay(timeMillis)指定临时暂停。这里的 API 文档提到了非阻塞挂起,因为运行在协程后面的线程实际上被允许做其他工作。延迟后,程序流程可以继续执行delay函数后面的指令。 -
屏蔽 : 你用
runBlocking { ... }启动
{ }块内语句的阻塞执行。您通常在程序的 main 函数中应用它,以获得可用于协程的第一个协程作用域。 -
协程调度程序:
CoroutineDispatcher的实例是协程上下文的一部分,您可以在每个作用域的属性coroutineContext中找到。runBlocking { val ctx:CoroutineContext = coroutineContext ... }
val res = withTimeoutOrNull(3000L) {
... code here
"Done."
}
// -> res is null if timeout happened
调度程序控制协程在哪个线程中运行。这可能是一个特定的线程、一个线程池,或者调用者的线程(直到第一个暂停点),如果使用了一个不受限制的调度程序(不是用于一般用途)。
-
*结构化并发:*这描述了反映在由
{ ... }构造的层次结构所描述的结构中的作业并发特征的依赖性。Kotlin 的协同程序强烈支持建立并发的结构化并发风格。 -
*通道:*通道提供了一种在协程之间传递数据流的方法。从 Kotlin 版本 1.3 开始,这个 API 被认为是实验性的。查看官方文档以了解该 API 的当前状态。
-
*Actor:*Actor 既是协程启动器,也是通道端点。从 Kotlin 版本 1.3 开始,这个 API 被认为是实验性的。查看官方文档以了解该 API 的当前状态。
以下段落概述了基本和高级协程使用模式。
基本协程
关于协程,需要知道的最重要的事情是,在使用多线程的协程方式之前,我们需要一个协程作用域。为了简单起见,如果我们有这样一个结构就好了:
openScope {
// Scope now automatically available
...
}
Kotlin 知道如何通过接收器的功能来做到这一点。例如,看看与协程相关的函数runBlocking()。在源代码中,您会发现:
fun <T> runBlocking(context: CoroutineContext =
EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T): T
{
// code to run block in a blocking thread
...
}
在block: suspend CoroutineScope.() -> T中,你可以看到块在扩展CoroutineScope的对象内部运行。这样的CoroutineScope是一个带有类型CoroutineContext的val名为coroutineContext的接口。有关上下文的详细信息,请参见下文。
警告
接口可以有val s。我们在面向对象的介绍性章节中没有提到这个特性,在运行时引擎的发展过程中,它主要是因为技术原因而引入的。在这里,它被用来简化协程处理。但是不鼓励在应用的接口中使用val s 和var s,因为变量通常属于实现方面,而不属于声明方面,这正是接口的用途。小心使用接口中的变量!
如果我们已经在协程中运行,您可以选择使用现有的作用域,或者生成新的作用域,如下所示:
-
runBlocking { ... }这进入了一个新的阻塞范围。这里的阻塞意味着
runBlocking()调用只有在{ ... }lambda 中的所有活动完成工作后才会返回。runBlocking()可以从协程作用域内部和外部启动,尽管不鼓励在协程作用域内部使用它。在这两种情况下,都会创建一个新的上下文,其中包括使用当前正在运行的线程来执行作业。 -
runBlocking(context:CoroutineContext) { ... }这与
runBlocking()相同,但具有由参数给出的基本上下文。 -
GlobalScope不鼓励使用这种方法。如果您希望使用与应用本身及其生命周期相关的范围,请使用这个 singleton 对象。例如,您可以使用
GlobalScope.launch{ ... }或GlobalScope.async{ ... }。通常你应该从runBlocking{ ... }开始。不显式地使用GlobalScope可以改善你的应用的结构。 -
coroutineScope { ... }这创建了一个新的协程作用域,它从外部协程作用域继承上下文;也就是说,
coroutineScope()被调用的范围。但是,它会覆盖作业,并使用从其 lambda 函数参数的内容({ ... }的内容)派生的自己的作业。此函数只能从作用域内部调用。使用coroutineScope()是结构化并发的一个突出例子:一旦{ ... }中的任何一个孩子失败,所有其他的孩子也会失败,最终整个coroutineScope()都会失败。 -
supervisorScope { ... }这与
coroutineScope()相同,但是让其子作用域彼此独立运行。特别是,如果任何一个子项被取消,其他子项和主管范围不会被取消。 -
launch { ... }这定义了后台作业。当由
{ ... }lambda 定义的后台作业开始在后台工作时,launch()调用立即返回。launch()返回类Job的一个实例。您可以使用Job中的join()功能等待作业完成。 -
async { ... }这与
launch()相同,但允许后台作业产生结果。为此,launch()返回类Deferred的一个实例。您可以使用它的await()函数来检索结果;当然,这意味着等待作业完成。 -
Implement CoroutineScope在您的任何类中,您都可以实现类
CoroutineScope:class MyClass : CoroutineScope { ... }。这种方法的问题是,因为CoroutineScope只是一个接口,我们需要通过用可感知的对象填充协程上下文来实现协程功能。一个简单的方法是使用委托:class MyClass : CoroutineScope by MainScope() { ... },它将所有协程构建器委托给一个MainScope对象。这一点对于用户界面特别有用。一旦这样做了,我们就可以在MyClass内部的任何地方自由地使用launch()和async()这样的构建器,以及cancel()这样的控制函数。
launch()函数有几个默认参数。其完整的概要如下:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
您可以使用context参数来设置上下文名称,例如,如
launch(context = coroutineContext +
CoroutineName("CoRou1")) {
...
)
start参数可以用来调整协程启动的方式。详见 API 文档(在 Android Studio 中输入“CoroutineStart”,然后按 Ctrl+B)。
async()函数的默认参数与launch()相同,因此您也可以调整async()的启动特性。
考虑下面的示例代码。对于 Android,你可以在活动的onCreate()函数中直接测试它。
runBlocking {
// This starts in the current thread.
// We are now inside a coroutine scope. This means
// we have a
// val coroutineContext:CoroutineContext
// for the context. The runBlocking() ends
// after all work is done.
Log.d("LOG", "1\. Started inside runBlocking()")
coroutineScope {
Log.d("LOG", "2\. coroutineScope()")
delay(500L)
Log.d("LOG", "3\. coroutineScope()")
coroutineScope {
Log.d("LOG", "4\. coroutineScope() II")
// If you add this, both coroutineScope()
// fail and runBlocking() prematurely ends:
// throw CancellationException("4.")
// Also, because runBlocking transports the
// exception to the outside world, (15.)
// below will not be reached.
}
Log.d("LOG", "5\. inner done")
}
val job1 = launch {
// This runs in the background, so
// (8.) happens before (7.)
Log.d("LOG", "6\. inside launch()")
delay(500)
Log.d("LOG", "7\. done with launch()")
}
Log.d("LOG", "8\. returned from launch()")
val deferr1 = async {
// This runs in the background as well, but it
// returns something
Log.d("LOG", "9\. inside async()")
delay(500)
Log.d("LOG", "10\. done with async()")
"Result"
}
Log.d("LOG", "11\. returned from async()")
job1.join()
Log.d("LOG", "12\. launch finish")
val res1 = deferr1.await()
Log.d("LOG", "13\. async finish")
Log.d("LOG", "14\. End of runBlocking()")
}
Log.d("LOG", "15\. Returned from runBlocking()")
它具有以下特点。
- 运行代码时,日志将显示如下内容:
1\. Started inside runBlocking()
2\. coroutineScope()
3\. coroutineScope() - 0.5secs later
4\. coroutineScope() II
5\. inner done
8\. returned from launch()
11\. returned from async()
6\. inside launch()
9\. inside async()
7\. done with launch()
10\. done with async()
12\. launch finish
13\. async finish
14\. End of runBlocking()
15\. Returned from runBlocking()
项目 6、9、7 和 10 可能以不同的顺序显示,因为它们属于后台处理。
-
外部的
runBlocking()在协程作用域层次结构中引入了一个根。 -
只有当它的所有子进程都完成了它们的工作或者它们的工作被取消时,这个
runBlocking()才会返回。 -
如果抛出了一个
CancellationException(取消对throw的注释以查看其发生的情况),它将在作用域层次结构中向上传输,因此不会到达 15。 -
async()和launch()都引入了异步性(并发性);他们立即返回,而他们的{ ... }lambda 在后台工作。 -
job1.join()和deferr1.await()同步后台作业;两者都等待相应的作业完成。
协程上下文
一个CoroutineContext将协程作用域的状态保存为一组上下文元素。虽然CoroutineContext没有实现您通常在这种情况下使用的普通的Set、List或Map接口,但是您仍然可以通过这些方法之一获得它的元素。
-
coroutineContext[Job]这将检索包含组成协程的指令的
Job实例。 -
coroutineContext[CoroutineName]可选地,这检索协程的名称。您可以通过
coroutineContext + CoroutineName("MyFancyCoroutine")将名称指定为协程构建器(如launch()或async())调用的第一个参数。 -
coroutineContext[CoroutineExceptionHandler]这是一个可选的专用异常处理程序。我们稍后将讨论异常。
-
coroutineContext[ContinuationInterceptor]这个内部项保存了一个对象,该对象负责在一个协程被挂起并恢复工作后,正确地继续它的工作。
尽管像runBlocking()、launch()或async()这样的作用域构建器会产生一个新的协程上下文,该上下文被转发给从内部调用的其他协程函数,但是您可以通过使用
withContext(context: CoroutineContext) {
...
}
作为一个参数,你可以自由构建自己的上下文,或者使用+来改变当前上下文的专用元素。例如,临时设置您编写的协程名称
... we are inside a coroutine scope
withContext(context = coroutineContext +
CoroutineName("TmpName")) {
... here we have a scope with a tweaked context
}
同样,您可以改变或重新定义其他上下文元素。
一个delay()做什么
乍一看,delay(timeMillis:Long)函数与 Java 中使用并发的基本Thread.sleep(millis:Long)函数有相同的用法:让程序流等待一段时间,然后才能继续执行delay()或sleep()语句之后的指令。然而,这两者之间有一个主要的区别:函数Thread.sleep()实际上阻塞了当前线程,让其他线程去做它们的工作,而delay()调用一个挂起函数,它不阻塞当前线程,而是在指定的时间过去后调度程序流的恢复。
从一个用例的角度来看,您使用这两者的目的是一样的:仅在指定的时间过去之后继续程序流。然而,知道对于协程来说,线程不会被阻塞,有助于调整并发性以获得最大的稳定性和性能。
什么是暂停功能?
挂起函数是这样一种函数,它可能会也可能不会立即执行,或者一旦调用开始就被挂起,然后最终结束。它不会阻塞一个线程,即使它或它的一部分被挂起。
从编码的角度来看,如果从协程中提取函数,您必须使自己的函数可挂起:
runBlocking {
...
launch {
...
}
}
转换为
runBlocking {
...
doLaunch()
}
suspend fun doLaunch() {
launch {
...
}
}
在内部,suspend关键字导致为协程上下文添加一个隐藏参数。
等待工作
将工作分派给几个并发执行的协程是故事的一部分。首先,如果协程计算了一些东西,在协程完成它们的工作之后,我们需要确保在继续程序流程之前,我们能够收集到结果。其次,我们必须确保程序作为一个大的状态机处于一致的状态,然后我们才能在协程完成后继续做更多的工作。这里我们说的是结果采集和协同或同步。
对于同步,为了确保一个Job或一个Deferred已经完成了它的工作,使用join(),如
val job = launch { ... }
val deferr = async { ... }
job.join() // suspend until job finished
deferr.join() // suspend until deferr finished
我们也可以对Deferred这样做,因为它是Job的子类。在这两种情况下,它确保作业的所有协程子作业也完成了它们的工作。然而,对于Deferred,我们实际上想要得到计算的结果,这将我们引向协程结果收集。你通过写作做到这一点
val deferr1 = async { ... }
val deferr2 = async { ... }
val deferr1Res = deferr1.await()
val deferr2Res = deferr2.await()
再次,await()函数调用暂停程序流,直到Deferred完成它们的工作。再一次,async乔布斯的子女也将完成他们的工作。
对于Deferred,还有一个函数getCompleted(),您可以使用它来获得一个已经计算好的结果:
val deferr1Res = deferr1.getCompleted()
不过,在这里您必须确保Deferred确实完成了它的计算,否则您将得到一个IllegalStateException。你可以读取isCompleted属性来检查一个Deferred或者一个Job是否已经完成。
在具有父子关系的协同程序的分层设置中,协同程序库确保子进程将在父进程退出之前完成工作,因此我们不必编写
runBlocking {
val job1 = launch {
}
job1.join() // unnecessary!
}
加入将自动发生。
取消协程
要取消任何作业,调用Job或Deferred对象上的cancel()功能。
val job = launch { ... }
val deferr = async { ... }
...
job.cancel() // or deferr.cancel()
取消并不意味着作业立即停止工作。相反,它被标记并在可行的时间停止工作。
-
在取消的作业中,任何挂起函数的调用都将导致作业结束执行。一个例子是
delay()函数中的delay();,将进行取消检查,如果作业被取消,作业将立即退出。 -
如果没有挂起函数调用或调用次数不够,您可以使用
yield()函数启动这样的取消检查。 -
在您的代码中,您可以定期检查
isActive属性是否给出了false。如果是这种情况,您知道作业已被取消,您可以完成作业执行。
因为取消通常不会导致任务立即终止,所以您必须附加一个join():
val job = launch { ... }
...
job.cancel()
job.join()
另一种选择是使用
val job = launch { ... }
...
job.cancelAndJoin()
结合了这两者。
在本章后面的“异常处理”一节中,我们将讨论取消对协程作用域层次结构的影响。
超时设定
您可以通过以下方式为协程内部的指令指定超时
withTimeout(1000L) { // milliseconds
...
}
如果达到超时限制,就会抛出一个TimeoutCancellationException(CancellationException的子类),或者
val res = withTimeoutOrNull(1000L) { // milliseconds
...
[result expression]
}
它不会抛出异常,而是在经过的时间超过给定时间时将null赋给结果。由于 Kotlin 惯用的?:操作符用于null值处理,我们也可以抛出自己的异常,如
withTimeoutOrNull(1000L) { // milliseconds
...
"OK"
} ?: throw Exception("Timeout Exception")
分配器
协程调度程序实际上告诉作业在哪里以及如何运行。更准确地说,它描述了协程在哪个线程中运行,以及如何创建或查找线程(例如,从线程池中)。您可以通过以下方式获得当前的调度程序
coroutineContext[ContinuationInterceptor]
如果您不想使用像launch()或async()这样的构建器所使用的缺省值,您可以显式地指定一个调度器。记住,我们可以给launch()或async()上下文作为第一个参数。如果我们有一个调度员,那么,我们可以写
val myDispatcher = ...
runBlocking {
val job = launch(coroutineContext + myDispatcher) {
...
}
job.join()
}
您不必自己开发这样的调度程序,因为有些调度程序是由协程库提供的:
-
Dispatchers.Default如果上下文不包含调度程序,这是默认的调度程序。它使用至少有两个线程的线程池,最大线程数是当前设备拥有的 CPU 数减去
1。但是,您可以通过在应用的早期(在构建任何协程之前)编写System.setProperty( "kotlinx.coroutines.default.parallelism", 12 )来覆盖这个数字。 -
Dispatchers.MainThis is a dispatcher tied to user interface processing. For Android, if you want to use the main dispatcher, you must add library
kotlinx-coroutinesandroidto the dependencies section insidebuild.gradle. If you route your coroutines structure likeclass MyClass : CoroutineScope by MainScope() { ... }Dispatchers.Main会自动使用。 -
Dispatchers.IO这是一个专门为阻塞 IO 功能定制的调度程序。它类似于
Dispatchers.Default调度程序,但是如果需要的话,会创建多达64个线程。 -
newSingleThreadContext("MyThreadName)"这将启动一个专用的新线程。您应该通过在最后应用
close()来结束使用它,或者将由newSingleThreadContext()函数调用返回的实例存储在某个全局位置以便重用。 -
Dispatchers.Unconfined这不是一般用途。非受限调度程序是一种使用周围上下文线程的调度程序,直到调用第一个挂起函数。它从线程中第一个被使用的挂起函数开始恢复。
异常处理
在协程执行期间,我们基本上有三种异常,除非采取进一步的预防措施,否则将会发生以下情况:
-
对于
CancellationException异常和launch():记住取消异常发生在对Job元素显式调用cancel()时。如果一个CancellationException被抛出,它将导致当前协程的退出,但不会导致任何一个父协程的退出;他们只会忽略它。层次的根协程同样会忽略异常,因此在协程机制之外,这样的异常不会被检测到。 -
对于
CancellationException异常和async():除了launch(),通过调用Deferred元素上的cancel()来取消Deferred作业不会导致异常被忽略。相反,我们必须对异常做出反应,这将出现在await()函数中。 -
对于
TimeoutCancellationException异常:如果withTimeout( timeMillis:Long ) { ... }超时,抛出TimeoutCancellationException。这是CancellationException的一个子类,没有特殊处理,所以正常的取消异常也适用于超时。 -
*任何其他异常:*正常异常会导致协程层次结构中任何正在运行的作业立即退出,并且也会被根协程抛出。例如,如果您希望出现这样的异常,您必须将一个根
runBlocking()包装到一个 try-catch 子句中。当然,您可以在作业中添加 try-catch 子句,以便尽早捕获这种异常。
要了解取消异常会发生什么,以及它是如何在协程层次结构中传播的,请尝试下面的代码:
var l1:Job? = null
var l11:Job? = null
var l111:Job? = null
runBlocking {
Log.d("LOG", "A")
l1 = launch {
Log.d("LOG", "B")
l11 = launch {
Log.d("LOG", "C")
delay(1000L)
Log.d("LOG", "D")
l111 = launch {
Log.d("LOG", "E")
delay(1000L)
Log.d("LOG", "F")
delay(1000L)
Log.d("LOG", "G")
}
delay(2500L)
Log.d("LOG", "H")
}
delay(1000L)
Log.d("LOG", "I")
}
Log.d("LOG", "X1")
delay(1500L)
Log.d("LOG", "X2")
l111?.cancel()
Log.d("LOG", "X3")
}
如果运行此命令,日志记录将如下所示:
10:05:31.295: A
10:05:31.295: X1
10:05:31.299: B
10:05:31.301: C
10:05:32.300: I
10:05:32.302: D
10:05:32.302: E
10:05:32.796: X2
10:05:32.796: X3
10:05:34.802: H
我们观察到以下特征:
-
runBlocking()不向外界转发取消异常。因此,这个异常是一个有点“预期”的异常。 -
标签
X1在A之后立即到达。这并不奇怪,因为所有的launch()调用都会导致后台处理。 -
标签
B和C在A之后不久到达,因为除了后台处理启动之外,没有指定延迟。 -
标签
I在1秒后到达,因为delay(1000L)就在它前面。这时候标签C后的延迟已经差不多过去了。几毫秒后到达D和E。 -
当标签
E到达时,在X1之后的延迟还没有完全过去,但是半秒钟后X2到达,我们在作业l111上触发取消。那时我们正处于E后的delay(1000L)中间。 -
由于取消,
E后的延迟立即退出,作业l111提前退出。标签F和G因此永远不会被触及。 -
l111的父协同程序继续它们的工作,它们只是忽略了作业l111的取消。这就是为什么稍后标签H到达。 -
标签
X3出现在H之前。我们知道runBlocking()继续它的工作,而任何非取消的孩子仍然在运行。作业l111被取消,但是作业l11和l1都没有被取消,所以H和I都到达。
如果在后一个示例中用l11.cancel()替换l111.cancel(),将产生以下输出:
11:40:35.893: A
11:40:35.894: X1
11:40:35.894: B
11:40:35.896: C
11:40:36.896: I
11:40:36.898: D
11:40:36.899: E
11:40:37.394: X2
11:40:37.395: X3
这里我们可以看到父作业l11和其子作业(作业l111)都被取消了;标签F、G和H永远也到不了。
练习 2
在前面的例子中,删除了cancel()语句,并在标签E之后的delay()中添加了一个0.5秒的超时。你还能指望什么Will测井与cancel()语句的测井不同?
如果您希望确保一段代码不能被取消,尽管它包含挂起的函数调用,您可以将它包装到一个特殊的新上下文中:
...
withContext(NonCancellable) {
// uncancellable code here
...
}
...
如果您需要定制异常处理,可以使用构建器调用显式注册一个ExceptionHandler:
val handler = CoroutineExceptionHandler {
_, exception ->
Log.e("LOG", "Caught $exception")
}
runBlocking(handler) {
...
}
或者
val handler = ...
runBlocking {
...
launch(coroutineContext + handler) {
...
}
}
注意,尽管以大写字母开头,但CoroutineExceptionHandler()实际上是一个函数调用。如果你想写一个处理异常的类,也有一个使用相同名字CoroutineExceptionHandler的接口。
这种异常处理程序只处理协程不会捕获的异常。我们知道对于launch()作业来说,CancellationException不会在协程层次结构中向上传送;在这种情况下,对于这种特殊的异常类型,也不会调用异常处理程序。
如果您不想要所有那些异常传播的东西,您可以使用一个主管作业,如
// we are inside a coroutine scope
val supervisor = SupervisorJob()
withContext(coroutineContext + supervisor) {
// the coroutines hierarchy here
...
}
或者你使用一个supervisor范围:
// we are inside a coroutine scope
supervisorScope {
// the coroutines hierarchy here
...
}
一个管理程序导致所有协程相互独立地处理它们的异常。然而,没有孩子会比父母活得长。