原文地址: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 类中加入了两个新的方法:joinchars. 第一个方法将任意数量的字符串,以特定分隔符拼接为单个字符串:

1
2
String.join(":", "foobar", "foo", "bar");
// => foobar:foo:bar

第二个方法 chars 为字符串对象中的所有字符创建一个流对象,因此你可以对这些字符进行流式操作:

1
2
3
4
5
6
7
"foobar:foo:bar"
.chars()
.distinct()
.mapToObj(c -> String.valueOf((char)c))
.sorted()
.collect(Collectors.joining());
// => :abfor

(译者注:chars 方法为 CharSequence 接口的 default 方法。 String 类实现了 CharSequence 接口。)

除了 String, 正则表达式的模式(Pattern)也能进行流式操作。与 String 中把所有字符逐一拆分到 stream 的方式不同,我们可以把字符串按特定的 pattern 进行拆分并创建 stream 对象,再基于此执行我们所需的操作。

1
2
3
4
5
6
Pattern.compile(":")
.splitAsStream("foobar:foo:bar")
.filter(s -> s.contains("bar"))
.sorted()
.collect(Collectors.joining(":"));
// => bar:foobar

译者注:上述操作实质上说的是通过不同的方式创建 stream 对象,因此等价于:

1
2
3
4
5
Stream.of("foobar:foo:bar".split(":"))
.filter(s -> s.contains("bar"))
.sorted().
collect(Collectors.joining(":"));
// => bar:foobar

此外,正则表达式的模式对象也能转化为断言(predicate)。这些断言能用于 stream 中过滤相应字符串:

1
2
3
4
5
Pattern pattern = Pattern.compile(".*@gmail\\.com");
Stream.of("bob@gmail.com", "alice@hotmail.com")
.filter(pattern.asPredicate()) // 注意此处的 asPredicate()
.count();
// => 1

上述模式可以判断以 @gmail.com 结尾的字符串,因此可以作为 Java 8 的 Predicate 对象来过滤 stream 中的 email 地址。

2. 处理数字

Java 8 新增了对无符号数的支持。

Java 中的数字均为带符号数,以 Integer 为例,一个 int 代表了 2³² 个二进制数的集合。其中最高位为正负符号位(0 为正,1 为负),因此可表达的最大正整数为 2³¹ - 1(从 0 开始)。通过 Integer.MAX_VALUE 可查看具体的值:

1
2
System.out.println(Integer.MAX_VALUE);      // 2147483647
System.out.println(Integer.MAX_VALUE + 1); // -2147483648

在 Java 8 中解析无符号 int:

1
2
3
4
long maxUnsignedInt = (1L << 32) - 1; // => 4294967295 (即 2³² - 1)
String string = String.valueOf(maxUnsignedInt);
int unsignedInt = Integer.parseUnsignedInt(string, 10);
String string2 = Integer.toUnsignedString(unsignedInt, 10);

如你所见,现在可以用 int 来表示 32 位二进制数的最大值,以及与 String 相互转换。这一点是原有的方法 parseInt 无法做到的:

1
2
3
4
5
6
try {
Integer.parseInt(string, 10); // string 为 4294967295
}
catch (NumberFormatException e) {
System.err.println("could not parse signed int of " + maxUnsignedInt);
}

由于超出了 signed int 的上限 2³¹ - 1,这段代码会在执行过程中抛出异常。

3. 数学运算

工具类 Math 已被增强用以处理数字溢出。

我们知道所有的数字类型都有最大值。那么当算术操作的结果与它的类型大小不符时会发生什么?

1
2
System.out.println(Integer.MAX_VALUE);      // 2147483647
System.out.println(Integer.MAX_VALUE + 1); // -2147483648

如你所见,上述的算术运算结果发生了整型溢出,很明显这个结果不是我们想要的。Java 8 中为严格的数学运算提供了支持,以便解决数字溢出的问题。

Math 类中新增了一系列以 exact 结尾的方法,如 addExact。这些方法会在遇到数字溢出的时候抛出 ArithmeticException

1
2
3
4
5
6
7
try {
Math.addExact(Integer.MAX_VALUE, 1);
}
catch (ArithmeticException e) {
System.err.println(e.getMessage());
// => integer overflow
}

在 long 转换成 int 的过程中,如果使用 toIntExact 也可能会抛出相同的异常:

1
2
3
4
5
6
7
try {
Math.toIntExact(Long.MAX_VALUE);
}
catch (ArithmeticException e) {
System.err.println(e.getMessage());
// => integer overflow
}

(译者注:由于具体的数据类型如 int 或 long 的大小并没有改变,因此能存储的范围并没有改变。区别在于抛出了异常,开发者可以得到通知并进行相应处理。)

4. 使用 Files

工具类 Files 作为 Java NIO 的一部分在 Java 7 发布。JDK 8 API 为其添加了额外的方法,允许我们在 Files 中使用函数式 stream. 让我们通过示例代码一探究竟:

列出文件

Files.list 方法将给定目录的所有 Path 对象转换成一个 stream 对象,因此我们可以使用 filtersorted 等流相关的操作。

1
2
3
4
5
6
7
8
try (Stream<Path> stream = Files.list(Paths.get(""))) {
String joined = stream
.map(String::valueOf)
.filter(path -> !path.startsWith("."))
.sorted()
.collect(Collectors.joining("; "));
System.out.println("List: " + joined);
}

上述代码的执行流程为:列出当前工作目录的所有文件、将相应的 path 对象转换为 string, 过滤与排序后并最终合并成一个字符串。如果你对函数式的流式操作并不熟悉,可以参考我的文章 Java 8 Stream Tutorial.

你应该已经注意到了,stream 的创建被包含在一个 try/with 声明中。Stream 接口实现了 AutoCloseable 接口,在本例中我们必须显式地关闭流,因为这背后涉及到 IO 操作。

返回的 stream 封装了一个 DirectoryStream 对象。如果文件系统的资源必须及时释放,那么我们应当使用 try-with-resource 结构来确保 stream 操作结束后,它的 close 方法会被调用。

查找文件

下列示例展示了如何在一个目录及其子目录中查找文件:

1
2
3
4
5
6
7
8
9
10
Path start = Paths.get("");
int maxDepth = 5;
try (Stream<Path> stream = Files.find(start, maxDepth, (path, attr) ->
String.valueOf(path).endsWith(".js"))) {
String joined = stream
.sorted()
.map(String::valueOf)
.collect(Collectors.joining("; "));
System.out.println("Found: " + joined);
}

find 方法接收三个参数: path 对象 start 为查找的起点,maxDepth 定义了查找的最大深度,第三个参数为一个用于检验的 predicate, 定义了搜索的具体逻辑。因此该示例的执行效果为查找当前目录下所有 JavaScript 文件(以 .js 结尾的文件)。

我们也可以使用 Files.walk 达到相同效果。该方法不需要传入 predicate 即可遍历所有文件,我们可以通过 stream 的 filter 来过滤文件名:

1
2
3
4
5
6
7
8
9
10
Path start = Paths.get("");
int maxDepth = 5;
try (Stream<Path> stream = Files.walk(start, maxDepth)) {
String joined = stream
.map(String::valueOf)
.filter(path -> path.endsWith(".js"))
.sorted()
.collect(Collectors.joining("; "));
System.out.println("walk(): " + joined);
}

读写文件

文本文件的读写操作在 Java 8 中终于变成一项简单的任务,而无需使用让人混乱的 reader 类和 writer 类。使用 Files.readAllLines 即可将一个文本文件的内容读取至一个 string 列表。你可以修改这个列表的内容并将其的每行内容通过 Files.write 写入到另一个文件:

1
2
3
List<String> lines = Files.readAllLines(Paths.get("res/nashorn1.js"));
lines.add("print('foobar');");
Files.write(Paths.get("res/nashorn1-modified.js"), lines);

必须牢记的是,这些操作从内存的角度来说并不是非常高效,因为会将整个文件加载到内存中,文件越大,占用的堆内存越多。

为了高效使用内存,你可以改为使用 Files.lines 方法。不同于之前的操作会一次性地读取文件的所有内容,这个方法每次只会读取一行内容并加入到 stream 中。

1
2
3
4
5
6
try (Stream<String> stream = Files.lines(Paths.get("res/nashorn1.js"))) {
stream
.filter(line -> line.contains("print"))
.map(String::trim)
.forEach(System.out::println);
}

如果你需要更精细的控制来读取文件,可以改为使用 BufferedReader:

1
2
3
4
Path path = Paths.get("res/nashorn1.js");
try (BufferedReader reader = Files.newBufferedReader(path)) {
System.out.println(reader.readLine());
}

或者通过 BufferedWriter 来将内容写入到文件中:

1
2
3
4
Path path = Paths.get("res/output.js");
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
writer.write("print('Hello World');");
}

BufferedReader 也能使用函数式流,它的 lines 方法会基于 buffered reader 中所有行的内容构建一个函数式流对象。

1
2
3
4
5
6
7
8
Path path = Paths.get("res/nashorn1.js");
try (BufferedReader reader = Files.newBufferedReader(path)) {
long countPrints = reader
.lines()
.filter(line -> line.contains("print"))
.count();
System.out.println(countPrints);
}

通过以上示例,可见 Java 8 提供了三个简单的方法去读取文本文件(Files.readAllLines, Files.lines, BufferedReader.lines),使得文件的处理更加简便。

不幸的是,你必须使用 try/with 语句来显式地关闭文件的函数式流对象,这多少使得代码看起来有点奇怪。我希望的是函数式流在调用终点型操作(如 countcollect 等)后可以自动关闭,毕竟你不能在同一个流对象中执行两次终点型操作。

希望你喜欢这篇文章。所有示例代码都托管在 github 上,包括我博客中其他关于 Java 8 的文章。如果你觉得这篇文章多少有些作用,欢迎你为我的项目点颗星或在 Twitter 上关注我。

Keep on coding!