十七、更多 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() | 对于FloatDouble,下一个整数值的上下限值。该类型保持不变,因此您必须添加.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(DoubleFloatIntLong)。常数EPI是自然对数和π(π)的底数。属性.sign返回一个数字(DoubleFloatIntLong)的符号,.ulp返回一个FloatDouble的最后一位的单位(这是两个数字之间的最小可测距离)。

日期和时间 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()隐式地使用它通过查询操作系统得到的时区(在我的例子中是欧洲/柏林)。

警告

DateSimpleDateFormat都是而不是线程安全的;您不能在不同的线程之间共享它们的实例。

有关所有这些日期和时间 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)的时间表示,而不考虑日期。

  • LocalDateTime

    LocalDateLocalTime的组合,可能用yyyy-MM-ddTHH:mm:ss来表示(T是字面意思)。

格式指示符yyyyHH等在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

请注意,尽管能够添加时区规范来进一步指定“现在”对应的时间,但该信息决不会以某种方式存储在日期和时间对象中。根据定义,本地日期和时间与时区无关。

我们可以解析字符串来获得LocalDateLocalTimeLocalDateTime的实例:

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)

我们可以定制自己的LocalDateLocalTimeLocalDateTime实例的字符串表示:

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

您可以使用LocalDateLocalTimeLocalDateTime实例执行时间运算:

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/格林威治时间的附加时间偏移。对于这样的偏移日期和时间,我们有两个类,OffsetTimeOffsetDateTime,您可以获得如下实例:

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)这样的操作用增加或减少给定的时间来构建一个新的实例。这适用于YearsMonthsWeeksDaysHoursMinutesSecondsNanos中的任何一种。

对于不同的时间段有不同的 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)

ZonedDateTimeLocalDateTime之间的连接通过

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()

持续时间和周期

持续时间是两个实例之间的物理时间跨度。周期与此类似,但只处理年、月和日,并考虑日历系统。有特殊的DurationPeriod类用于处理持续时间和周期:

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()

您可以对DurationPeriod类的实例执行算术计算:

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

您可以使用持续时间和周期向LocalDateLocalTimeLocalDateTimeZonedDateTimeInstant对象添加或从中减去时间量。

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本身提供了几个函数来调整时钟的使用。这两个特别有趣:

  1. 这是一个总是返回同一时刻的时钟。

  2. 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的所有功能,所以你可以应用过滤器、映射、折叠、分组以及我们在第 9 章中研究过的其他过程。如果需要真正的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包里找到。他们彼此关系密切。我们为MyClassKClass:

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" }!!

有了KProperty1KFunction实例,你可以做一些有趣的事情,比如发现它是私有的还是公共的,它是最终的还是开放的,或者一个属性是属于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 | 任何非数字[^0–9] | | \s | 空白字符 | | \s | 非白人角色 | | \w | 一个单词字符[A–Z _ A–Z _ 0–9] | | \W | 非文字字符[^\w] | | ^ | 一行的开始 | | $ | 一行的结尾 | | \b | 单词边界 | | \B | 非单词边界 | | xy | 一个 x 后面跟着一个 y | | x&#124;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 次nm次。 | | X?? | 不情愿的 | x 一次或者根本不要。 | | X*? |   | x 零次或更多次。 | | X+? |   | x 一次或多次。 | | X{n}? |   | x 正好是n次。 | | X{n,}? |   | X n次或更多次。 | | X{n,m}? |   | x 次nm次。 | | X?+ | 所有格 | x 一次或者根本不要。 | | X*+ |   | x 零次或更多次。 | | X++ |   | x 一次或多次。 | | X{n}+ |   | x 正好是n次。 | | X{n,}+ |   | X n次或更多次。 | | X{n,m}+ |   | x 次nm次。 |

确定匹配

要查看字符串是否匹配给定的正则表达式,可以使用以下函数:

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"