原文地址:Java 8 API by Example: Strings, Numbers, Math and Files
作者: winterbe
关于 Java 8 变化的文章中,绝大部分内容都聚焦于 Lambda 表达式、函数式接口与 Stream. 但除此之外,在 JDK 8 中还有很多类被增强,加入了许多有用的特性和方法。
这篇文章将会涵盖 Java 8 API 中改动较小的部分:String, Numbers, Math 以及 Files, 辅以简单易懂的示例代码。
1. 字符串切片
String 类中加入了两个新的方法:join
和 chars
. 第一个方法将任意数量的字符串,以特定分隔符拼接为单个字符串:
1 | String.join(":", "foobar", "foo", "bar"); |
第二个方法 chars
为字符串对象中的所有字符创建一个流对象,因此你可以对这些字符进行流式操作:
1 | "foobar:foo:bar" |
(译者注:chars
方法为 CharSequence
接口的 default 方法。 String 类实现了 CharSequence 接口。)
除了 String, 正则表达式的模式(Pattern)也能进行流式操作。与 String 中把所有字符逐一拆分到 stream 的方式不同,我们可以把字符串按特定的 pattern 进行拆分并创建 stream 对象,再基于此执行我们所需的操作。
1 | Pattern.compile(":") |
译者注:上述操作实质上说的是通过不同的方式创建 stream 对象,因此等价于:
1 | Stream.of("foobar:foo:bar".split(":")) |
此外,正则表达式的模式对象也能转化为断言(predicate)。这些断言能用于 stream 中过滤相应字符串:
1 | Pattern pattern = Pattern.compile(".*@gmail\\.com"); |
上述模式可以判断以 @gmail.com
结尾的字符串,因此可以作为 Java 8 的 Predicate
对象来过滤 stream 中的 email 地址。
2. 处理数字
Java 8 新增了对无符号数的支持。
Java 中的数字均为带符号数,以 Integer
为例,一个 int
代表了 2³² 个二进制数的集合。其中最高位为正负符号位(0 为正,1 为负),因此可表达的最大正整数为 2³¹ - 1(从 0 开始)。通过 Integer.MAX_VALUE
可查看具体的值:
1 | System.out.println(Integer.MAX_VALUE); // 2147483647 |
在 Java 8 中解析无符号 int:
1 | long maxUnsignedInt = (1L << 32) - 1; // => 4294967295 (即 2³² - 1) |
如你所见,现在可以用 int
来表示 32 位二进制数的最大值,以及与 String 相互转换。这一点是原有的方法 parseInt
无法做到的:
1 | try { |
由于超出了 signed int 的上限 2³¹ - 1,这段代码会在执行过程中抛出异常。
3. 数学运算
工具类 Math
已被增强用以处理数字溢出。
我们知道所有的数字类型都有最大值。那么当算术操作的结果与它的类型大小不符时会发生什么?
1 | System.out.println(Integer.MAX_VALUE); // 2147483647 |
如你所见,上述的算术运算结果发生了整型溢出,很明显这个结果不是我们想要的。Java 8 中为严格的数学运算提供了支持,以便解决数字溢出的问题。
Math
类中新增了一系列以 exact
结尾的方法,如 addExact
。这些方法会在遇到数字溢出的时候抛出 ArithmeticException
:
1 | try { |
在 long 转换成 int 的过程中,如果使用 toIntExact
也可能会抛出相同的异常:
1 | try { |
(译者注:由于具体的数据类型如 int 或 long 的大小并没有改变,因此能存储的范围并没有改变。区别在于抛出了异常,开发者可以得到通知并进行相应处理。)
4. 使用 Files
工具类 Files
作为 Java NIO 的一部分在 Java 7 发布。JDK 8 API 为其添加了额外的方法,允许我们在 Files 中使用函数式 stream. 让我们通过示例代码一探究竟:
列出文件
Files.list
方法将给定目录的所有 Path
对象转换成一个 stream 对象,因此我们可以使用 filter
和 sorted
等流相关的操作。
1 | try (Stream<Path> stream = Files.list(Paths.get(""))) { |
上述代码的执行流程为:列出当前工作目录的所有文件、将相应的 path 对象转换为 string, 过滤与排序后并最终合并成一个字符串。如果你对函数式的流式操作并不熟悉,可以参考我的文章 Java 8 Stream Tutorial.
你应该已经注意到了,stream 的创建被包含在一个 try/with 声明中。Stream
接口实现了 AutoCloseable
接口,在本例中我们必须显式地关闭流,因为这背后涉及到 IO 操作。
返回的 stream 封装了一个
DirectoryStream
对象。如果文件系统的资源必须及时释放,那么我们应当使用 try-with-resource 结构来确保 stream 操作结束后,它的close
方法会被调用。
查找文件
下列示例展示了如何在一个目录及其子目录中查找文件:
1 | Path start = Paths.get(""); |
find
方法接收三个参数: path 对象 start
为查找的起点,maxDepth
定义了查找的最大深度,第三个参数为一个用于检验的 predicate, 定义了搜索的具体逻辑。因此该示例的执行效果为查找当前目录下所有 JavaScript 文件(以 .js
结尾的文件)。
我们也可以使用 Files.walk
达到相同效果。该方法不需要传入 predicate 即可遍历所有文件,我们可以通过 stream 的 filter
来过滤文件名:
1 | Path start = Paths.get(""); |
读写文件
文本文件的读写操作在 Java 8 中终于变成一项简单的任务,而无需使用让人混乱的 reader 类和 writer 类。使用 Files.readAllLines
即可将一个文本文件的内容读取至一个 string 列表。你可以修改这个列表的内容并将其的每行内容通过 Files.write
写入到另一个文件:
1 | List<String> lines = Files.readAllLines(Paths.get("res/nashorn1.js")); |
必须牢记的是,这些操作从内存的角度来说并不是非常高效,因为会将整个文件加载到内存中,文件越大,占用的堆内存越多。
为了高效使用内存,你可以改为使用 Files.lines
方法。不同于之前的操作会一次性地读取文件的所有内容,这个方法每次只会读取一行内容并加入到 stream 中。
1 | try (Stream<String> stream = Files.lines(Paths.get("res/nashorn1.js"))) { |
如果你需要更精细的控制来读取文件,可以改为使用 BufferedReader
:
1 | Path path = Paths.get("res/nashorn1.js"); |
或者通过 BufferedWriter
来将内容写入到文件中:
1 | Path path = Paths.get("res/output.js"); |
BufferedReader
也能使用函数式流,它的 lines
方法会基于 buffered reader 中所有行的内容构建一个函数式流对象。
1 | Path path = Paths.get("res/nashorn1.js"); |
通过以上示例,可见 Java 8 提供了三个简单的方法去读取文本文件(Files.readAllLines
, Files.lines
, BufferedReader.lines
),使得文件的处理更加简便。
不幸的是,你必须使用 try/with 语句来显式地关闭文件的函数式流对象,这多少使得代码看起来有点奇怪。我希望的是函数式流在调用终点型操作(如 count
或 collect
等)后可以自动关闭,毕竟你不能在同一个流对象中执行两次终点型操作。
希望你喜欢这篇文章。所有示例代码都托管在 github 上,包括我博客中其他关于 Java 8 的文章。如果你觉得这篇文章多少有些作用,欢迎你为我的项目点颗星或在 Twitter 上关注我。
Keep on coding!