3. 数值和日期

本章涵盖了Scala中的数值类型,以及Java 8引入的日期和时间API的使用方法。

在Scala中,ByteShortIntLongChar 类型被称为整数类型,因为它们由整数或数字表示。整数类型以及DoubleFloat组成了Scala中的数值类型。与被称为“非数值类型”的UnitBoolean一样,这些数值类型都继承AnyVal特质(trait)。在Scala页面( https://oreil.ly/C7Id7 )关于统一类型的讨论中,这9种类型都被称为预定义值类型,并且不能为空。

预定义值类型与AnyValAny(以及Nothing)的关系如图3-1所示:

  • 所有的数值类型都继承自AnyVal

  • Scala类层次结构中的所有其他类型都继承自AnyRef

图3-1:所有预定义的数值类型都继承自 AnyVal

如表3-1所示,Scala的数值类型与Java中对应的基本类型有着相同的数值范围。

表3-1:Scala中数值类型的数值范围

类型
描述
范围

Char

16位无符号Unicode字符

0~65535

Byte

8位有符号整数

-128~127

Short

16位有符号整数

-32768~32767

Int

32位有符号整数

-2147483648~2147483647

Long

64位有符号整数

-2^63~2^63-1

Float

32位IEEE 754单精度浮点数

见下文

Double

64位IEEE 754双精度浮点数

见下文

除此之外,Boolean可以为true或者false

如果想知道数据范围的精确值,但手头没有这本书,可以在Scala REPL中查看:

除了这些基本的数值类型之外,BigIntBigDecimal在本章后续部分都会讲到。

数值常量中的下划线

Scala 2.13中引入了在数值常量中使用下划线的功能:

带下划线的数值常量可以使用在任何常用的地方:

目前带下划线的数值常量不能使用的一个地方是从String转换为数值类型:

复数

除了Scala自带的math类库,如果你需要其他更强大的数学运算能力,可以了解一下Spire( https://typelevel.org/spire ),它包含了Rational、Complex和Real等更高级的数学概念。

日期和时间

本章最后几节介绍了Java 8引入的日期和时间API,并展示了如何使用LocalDateLocalTimeLocalDateTimeInstantZonedDateTime等新类。

3.1 从字符串到数值

问题

你想把一个字符串转换成一个Scala的数值类型。

解决方案

可以在String上使用 to* 方法:

需要注意的是,这些方法可能会抛出Java的NumberFormatException

因此,使用to*Option方法也是一个不错的选择,它的作用是在转换成功时返回Some,转换失败时返回None

BigIntBigDecimal实例也可以通过字符串创建,并且也有可能抛出NumberFormatException

处理基数和根

如果想要处理十进制以外的计算,使用Scala的toInt方法是不支持传入进制的参数的。此时可以使用java.lang.Integer类的parseInt方法,例如:

当然,参考2.11节,也可以使用Scala中的自定义扩展方法来解决这个问题:

讨论

如果用过Java把字符串转换成数值类型,那么一定会对NumberFormatException很熟悉。然而,Scala中并没有检查型异常,所以可以用其他方式来处理这个异常。

首先,没有必要在一个Scala方法上声明可能抛出的异常,因此下面的方法定义是合法的:

编写一个纯函数

然而,在函数式编程(FP)中,最好永远不要定义上述的方法。由于可能会抛出异常,所以这种方法可能会造成调用方的代码短路。(如果不希望其他开发人员定义这种方法给你使用,你最好也不要对其他开发人员这样做。)相反,纯函数 总是 返回其签名所显示的类型。因此,在FP中,可以这样写这个函数:

该函数的返回值类型是Option[Int],这意味着如果给它一个 “10”,它将返回一个Some(10),如果给它一个 “Yo”,它将返回一个None。此函数相当于上述解决办法中的toIntOption,该方法在Scala2.13中引入。

简化makeInt方法 -- TODO 松鼠栏

虽然上述代码 makeInt(s: String): Option[Int] 已经比较完美了,但是还可以进一步精简,如下所示:

这两种不同定义的makeInt方法都会返回 Some[Int] 或者 None

如果更加喜欢Try的返回值类型,可以这样做:

使用Try的优势是当程序发生异常,可以从返回的Failure对象中获取具体的异常:

声明方法的异常

如果期望声明方法会抛出异常,可以通过 @throws 注解来标注方法:

如果在Java代码调用这个方法,这个注解式异常声明是必须的。22.7小节说明了这点。

另见

  • 24.6小节,更详细讲解了OptionSomeNone的使用。

3.2 数值类型转换

问题

你想把一个数值类型转换成另一个数值类型,比如把Int转成DoubleDouble转成Int、或者涉及到BigIntBigDecimal的转换。

解决方案

数值类型通常可以使用一系列 to* 方法进行类型转换,包括toBytetoChartoDoubletoFloattoInttoLongtoShort方法。这些方法由RichDoubleRichIntRichFloat等类添加到基本数值类型中,且自动被类Scala.Predef所引入。

在关于Scala统一类型的页面( https://oreil.ly/C7Id7 ),数值类型可以很容易按照图3-2所示的方向进行转换。

图3-2:数值类型简单类型转换方向

一些简单的转换示例:

当按照图示转换方向时,操作很简单。当然也可以按照相反的方向进行转换,如下所示:

然而,按照相反的方向进行转换时,需要注意所产生的一些问题:

因此,在尝试使用这些转换方法时,最好仔细检查一下转换是否合法:

注意Double类型没有以下检查方法:

另外请注意,当使用这些方法时,如果Double含有非零小数部分,Int/Short/Byte的测试将会失败:

asInstanceOf

根据使用需求,也可以按照类型转换方向使用asInstanceOf方法:

讨论

由于所有数值类型都是类(而不是基本类型),所以BigIntBigDecimal的使用方式也类似。以下示例展示了它们如何跟数值类型一起使用。

BigInt

BigInt的构造函数含有9种不同的重载方式,包括IntLongString等入参:

BigInt也包含 isValid*to* 方法来协助进行数值类型的转换:

  • isValidByte、toByte

  • isValidChar、toChar

  • isValidDouble、toDouble

  • isValidFloat、toFloat

  • isValidInt、toInt

  • isValidLong、toLong

  • isValidShort、toShort

BigDecimal

同样,BigDecimal构造函数也含有多种不同的重载方式,包括如下:

BigDecimal同样包含 isValid*to* 方法。它还包含 to*Exact 方法,使用方式如下:

请参考Scaladoc查看更多关于 BigInthttps://oreil.ly/PGdIE )和 BigDecimalhttps://oreil.ly/E6Lsk )的使用方法。

3.3 覆盖默认的数值类型

问题

当你使用隐式类型声明一个变量时,Scala会根据变量的具体数值自动地把数值类型赋值给这个变量,并且可以覆盖其默认的类型。

解决方案

如果把1赋给一个变量,并且没有显示声明类型,Scala会把它的类型设置为Int

因此,如果想显示声明类型,可以这样:

虽然这种样式更让人喜欢,但在表达式末尾指定类型也是合法的:

对于long、double和float,也可以使用以下声明方式:

通过在数字前面加一个前导0x0X来定义一个十六进制值,并且可以把这个值保存为Int或者Long类型:

讨论

在创建对象实例时,了解这种定义变量的方式是很有帮助的,其基本语法如下:

当你需要在一个类中初始化 var 字段时,这个语法非常有用:

可以看到,在初始化变量时,可以使用下划线作为占位符。这在创建类成员变量时是可以的,但是在其他情况下,如在方法中定义变量是无效的。对于数值类型,这也不算一个问题,可以赋值为0。但是对于其他类型,可以采用这种方式:

但通常的警告是:不要使用null值。最好使用Option/Some/None模式,在一些优秀的Scala库和框架中,如Play Framework,这种模式非常常见。在24.5小节和24.6小节,对这种方式进行了更深入的讨论。


类型归属

在一些不常见的情形需要使用类型归属(type ascription),Stack Overflow “What Is the Purpose of Type Ascriptions in Scala?”( https://oreil.ly/y9eQz )上有个例子讲解了把String向上转成Object的好处,实现方式如下:

如上所示,这个技巧和本节很类似。向上转换(upcast)也被称为类型归属,Scala官方文档( https://oreil.ly/eWyge )对其的描述如下:

“类型归属是一种发生在编译时,为了满足类型检查器的类型的向上转换。这种情况并不常见,但确实会发生。其中最常见的一种场景是向一个接受变参(varargs)的方法传入一个Seq类,将 _* 的类型进行归属。”


3.4 替代++和−−

问题

你希望像其他语言里那样使用 ++ 来递增 或者使用 -- 来递减一个变量,但是Scala里没有这样的操作符。

解决方案

由于声明为val的字段是不可变的,他们不能递增或递减,但是声明为varInt类型变量是可以通过+=和-=方法来修改的:

同样,可以通过类似的方法对变量进行乘法和除法操作:

如果想在一个被声明为val的字段上调用此方法,会得到一个编译时错误:

讨论

这种方式的另外一个优势就是可以在除Int以外的其他类型上调用这些操作方法,例如在DoubleFloat类上:


注意:上面提到,+=、-=、*=和/=并不是操作符,而是方法。这种使用“操作符”来作为方法名的实现方式是Scala里常见的做法。例如,Actors就是通过库而非语言本身实现的。


3.5 浮点数的比较

问题

你需要比较两个浮点数的值,然而,和其他语言一样,两个应该相等的浮点数有可能实际上是不相等的。

解决方案

刚开始接触浮点数时,可以知道0.10.1等于0.2

但是0.10.2并不精确等于0.3

这个微小的误差使浮点数比较成了一个问题:

因此,可以定义能容忍一定误差的浮点数比较的函数。下面用近似相等的方法来进行展示:

可以这样使用该方法:

讨论

在上述解决方法中 @targetName 注解是可选的,但是推荐在使用符号的方法中使用:

  • 有助于提高跟其他不支持使用符号方法名的语言之间的互操作性。

  • 这使得堆栈分析变得更容易,因为其中使用的是注解提供的方法名,而不是方法的符号名。

  • 注解中的名称也会在文档中标注为符号名称的一个可选的别名。

扩展方法

在8.9节中所示,可以在Double类上定义一个扩展方法。假设可容忍的误差是0.5,可以这样定义扩展方法:

还可以使用条件判断的方式:

在任何时候,都可以在两个Double值上进行使用:

这会让代码更具有可读性。然而,当硬编码容忍误差的值时,做好将该值定义为给定x的百分比:

或者,将容忍误差值(tolerance)定义为方法入参:

最后,可以这样使用该扩展方法:

另见

  • “What Every Computer Scientist Should Know About Floating-Point Arith‐metic”( https://oreil.ly/K52zn )。

  • 维基百科的浮点数精度算法页面的“Accuracy problems”部分( https://oreil.ly/PgxrB )。

  • 维基百科的任意精度数算法页面( https://oreil.ly/PPdkg )。

3.6 处理大数

问题

你正在编写一个需要处理非常大的整数或者浮点数的程序。

解决方案

如果 LongDouble 无法满足“大”的需求,可以使用Scala的 BigIntBigDecimal 类:

BigIntBigDecimal 是Java BigIntegerBigDecimal 类的封装,并且支持Scala普通数值类型的所有操作符:

可以把它们转换成其他数值类型:

为避免错误,可以在转换前测试下是否能够转换:

BigInt也可以转换成Array[Byte]:

讨论

在使用BigInt或者BigDecimal之前,可以检查下LongDouble能处理的最小和最大值:

根据需要,也可以使用普通数值类型的PositiveInfinityNegativeInfinity

BigDecimal经常在货币中使用 -- TODO 松鼠栏

BigDecimal通常用于表示货币,因为它提供了对舍入行为的控制。如之前的章节所示,$0.10$0.20不能精确表示$0.30

但是BigDecimal避免了这个问题:

尽管如此,在BigDecimal构造函数中使用Double可能还会产生一些问题:

因此,推荐在BigDecimal构造函数中使用String来获取准确的结果:

正如Joshua Bloch在 Effective JavaAddison Wesley )中所说:“使用BigDecimal、int或long进行货币计算”。

另见

  • Baeldung的“BigDecimal and BigInteger in Java”( https://oreil.ly/3pdrS )含有大量的使用细节。

  • 如果想要将这些数据类型存储到数据库,这些页面可能会提供一些帮助:

    • Stack Overflow的”How to Insert BigInteger in Prepared Statement Java”( https://oreil.ly/kdn73

    • Stack Overflow的“Store BigInteger into MySql” ( https://oreil.ly/5lQgk

  • Stack Overflow的“Unpredictability of the BigDecimal(double) Constructor”( https://oreil.ly/62ZSQ )讨论了在Java中将 double 传递给 BigDecimal 的问题。

3.7 生成随机数

问题

你需要创建随机数,例如在测试一个应用程序、运行仿真系统以及其他许多情况下。

解决方案

使用Scala的scala.util.Random生成随机数,下面列出一些使用示例:

当在nextInt方法上传入一个最大值,方法将会返回一个0(包括)到给定的值(不包括)之间的随机数。比如,给定100将会返回一个0到99之间的随机数。

讨论

这一小节将会展示一些Random类的常用方式。

生成随机长度的range

Scala中创建一个随机长度的range,这在测试时会非常有用,也十分方便:

当然,Range可以按需转换成集合序列:

也可以通过for/yield循环来修改这些数值:

可能会产生如下的序列:

生成固定长度的随机数

可以创建一个长度已知的序列,序列中的值是随机生成的:

可能会产生如下包含5个随机数的序列:

同样,使用 nextFloatnextDouble 方法也类似:

打乱序列的元素

另一个常见的需求是“随机化”一个现有的序列。因此,可以使用Random类中的shuffle方法:

从序列中随机取出一个元素

已知一个存在的序列,想要从中随机获取一个元素,可以这样做:

下面列出一些使用getRandomElement方法的示例:

3.8 格式化数值和金额

问题

你想对数值或者金额的小数位数或逗号进行格式化,特别是在输出时。

解决方案

对于基本数值的格式化,可以使用 f 字符串插值器。对于其他需求,比如添加逗号、本地化输出和货币处理,可以使用java.text.NumberFormat的相关实例:

NumberFormat实例也可以自定义地区(locales)。

f 字符串插值器

在2.4节讨论了 f 字符串插值器的相关内容,可以对简单的数值进行格式化:

更多使用这个技巧的例子:

如果更偏爱显式调用字符串中的format方法,可以像下面一样写代码:

逗号、本地化和整数

如果想格式化整数数值,比如像美国在整数中添加逗号,可以使用NumberFormat类的getIntegerInstance

由于本书作者所处的地区靠近美国的科罗拉多州丹佛附近,所以示例中的输出添加了逗号。getIntegerInstance也支持传入自定义的Locale类( https://oreil.ly/lYaAj )参数:

逗号、本地化和浮点数

可以通过getInstance获取一个formatter用来处理浮点数:

也可以设置自定义的locale参数:

货币

对于货币金额的输出,可以使用getCurrencyInstance返回的formatter。默认使用美元的格式输出:

使用Locale格式化为国际货币的格式:

如果不使用Currency类,getCurrencyInstance也可以格式化BigDecimal。下面使用默认的美元格式输出:

使用自定义locale的示例:

自定义格式化模式

可以使用DecimalFormat类创建自定义的格式化模式。只需要随便创建一个pattern,然后使用format方法作用在一个数值上,下面列出一些示例:

更多格式化模式字符可以参考Java的DecimalFormat类( https://oreil.ly/nvJda )。还有一个值得注意的是,通常情况下最好不用直接创建DecimalFormat的实例,可以使用NumberFormat类。

本地化

java.util.Locale类有三个构造函数:

它还包括比如CANADA、CHINA、FRANCE、GERMANY、JAPAN、UK、US等很多地区的静态实例。对于一些Locale不支持的国家和语言,可以使用语言或者语言/国家的字符串来表示。比如,根据Oracle’s JDK 10和JRE 10所支持的地区页面( https://oreil.ly/fjQXp ),India地区可以这样表示:

更多使用示例:

下面的例子展示了如何使用India地区:

对于NumberFormat类中的所有get*Instance方法,可以设置一个默认的locale:

讨论

本章使用Java中的类库来处理输出金额和格式化数值,当然,关于金额的处理依赖于项目的实际需求。在我的咨询生涯里,看到大多数公司采用Java的BigDecimal类来处理金额,还有一些公司基于BigDecimal创建了自定义的金额处理类。

3.9 新的日期和时间

问题

你需要使用Java8新引入的日期和时间API。

解决方案

使用Java8的API,你可以创建新的date、time和date/time值。表3-2提供了一些新类的描述(参考 java.time 的Javadoc( https://oreil.ly/7T8dh )),并且所有这些类兼容ISO-8601 日历系统。

表3-2:Java8常用的Date和Time类

类名
描述

LocalDate

不包含时区的日期类型,比如 2007-12-03。

LocalTime

不包含时区的时间类型,比如 10:15:30。

LocalDateTime

不包含时区的日期-时间类型,比如 2007-12-03T10:15:30。

ZonedDateTime

包含时区的日期-时间类型,比如 2007-12-03T10:15:30+01:00 Europe/Paris。

Instant

为时间轴上的单一瞬时点建模。这可能用于记录应用程序中事件的时间戳。

创建新的date/time实例:

  • 使用这些类中的now方法创建表示当前时刻的实例。

  • 使用这些类中的of方法创建表示过去或者未来时刻的实例。

now

使用API中新类的now方法,可以创建表示当前日期和时间的实例:

这些方法的结果展示了每种类型中存储的数据。

过去或未来

使用API中新类的of方法,可以创建表示过去或者未来的日期和时间的实例。比如,下面使用of方法创建java.time.LocalDate类( https://oreil.ly/qXo7f )的实例:

注意使用LocalDate,一月使用1代表,而不是0

java.time.LocalDatehttps://oreil.ly/TNNWb )含有5个 of* 方法,包含如下:

下面刻意通过异常的方式来展示下合法的分钟和小时:

java.time.LocalDateTimehttps://oreil.ly/eHIH4 )含有9个 of* 方法,包含如下:

java.time.ZonedDateTimehttps://oreil.ly/nmoY3 )含有7个 of* 方法,包含如下:

下面使用ZonedDateTime类的第二个方法做个示例:

顺便提一嘴,上面示例中java.time.ZoneIdhttps://oreil.ly/h7A9T )还有一些其他值:

最后,java.time.Instanthttps://oreil.ly/aThjs )含有3个 of* 方法:

Instant是一个很不错的类,比如它能够计算两个实例之间的时间差:

3.10 计算两个日期的差值

问题

你需要确定两个日期之间的差值。

解决办法

如果需要确定两个日期之间天数的差值,java.time.temporal.ChronoUnit类( https://oreil.ly/ydsbP )中的DAYS枚举是一个最简单的解决方案:

如果需要确定两个日期之间年数月数的差值,可以使用ChronoUnit类中的YEARSMONTHS枚举:

使用上面相同的LocalDate实例值,也可以结合Period类进行使用,但请注意ChronoUnitPeriod方法之间的输出有显著差异:

讨论

ChronoUnitbetween方法含有两个Temporal类的入参:

因此,方法支持所有Temporal的子类,包括Instant、LocalDate、LocalDateTime、LocalTime、ZonedDateTime等等。下面是使用LocalDateTime的示例:

ChronoUnit类还包括很多其他的枚举类型,包括CENTURIES、DECADES、HOURS、MICROS、MILLIS、SECONDS、WEEKS、YEARS等等,用来处理世纪、数十年、小时、微秒、毫秒、秒、周、年等等。

3.11 格式化日期

问题

你需要按照一定的格式打印日期。

解决办法

使用java.time.format.DateTimeFormatter类( https://oreil.ly/qWHWM )。它提供了3种打印日期和时间的格式:

  • 内置的格式

  • 本地化的格式

  • 自定义的格式

内置的格式

DateTimeFormatter提供了15种内置的格式可供使用。下面结合LocalDate进行演示:

其他的日期格式比如:

本地化的格式

使用DateTimeFormatter的静态方法来创建本地化的格式:

  • ofLocalizedDate

  • ofLocalizedTime

  • ofLocalizedDateTime

当创建本地化日期时,也可以使用java.time.format.FormatStylehttps://oreil.ly/K50kH )中的4个枚举:

  • SHORT

  • MEDIUM

  • LONG

  • FULL

下面结合LocalDateFormatStyle.FULL来展示下ofLocalizedDate的使用:

使用同样的技巧,4种FormatStyle的输出看起来像这样:

自定义的格式

可以使用DateTimeFormatter类的ofPattern方法创建自定义的字符串模式。下面例举了一些示例:

下面是一些常用的字符串模式:

接下来展示如何格式化LocalTime类:

使用LocalDateTime类,可以同时格式化日期和时间:

更详细的内置格式以及合法的字符串模式(pattern),请参考DateTimeFormatter类( https://oreil.ly/qWHWM )。

3.12 从字符串到日期

问题

你需要将字符串转换成Java8引入的日期/时间类型。

解决方案

如果字符串是一个合法的格式,那么直接使用相关类的parse方法。否则,需要创建一个自定义的formatter来格式化该字符串。

LocalDate

下面例子使用java.time.LocalDate默认的格式化方法:

如果向parse方法中传入了错误的时间格式字符串,将会抛出异常:

为了满足不同格式字符串的转换,需要创建一个与之对应的formatter

LocalTime

下面例子使用java.time.LocalTime默认的格式化方法:

请注意,每个字段都需要一个前导0

接下来的示例展示了使用formatter的一些方法:

LocalDateTime

下面例子使用java.time.LocalDateTime默认的格式化方法:

接下来的示例展示了使用formatter的一些方法:

Instant

java.time.Instant类只有一个parse方法,且需要传入一个正确的日期/时间格式的字符串:

ZonedDateTime

下面例子使用java.time.ZonedDateTime默认的格式化方法:

接下来的示例展示了使用formatter的一些方法:

请注意,传入不正确的日期(或格式不正确的日期)将引会抛出异常:

最后更新于

这有帮助吗?