java8-增强API

大量的教程和文章涵盖了Java 8中最重要的变化,如lambda表达式和函数流。但是,JDK 8 API中的许多现有类已经通过有用的特性和方法得到了增强。

本文介绍了Java 8 API中的一些较小的更改 - 每个更改都使用易于理解的代码示例进行描述。让我们深入研究字符串,数字,算术和文件。

字符串切片

String类有两种新方法:join和chars。第一种方法将任意数量的字符串连接到具有给定分隔符的单个字符串:

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

现在不仅字符串而且正则表达式模式都受益于流。我们可以为任何模式拆分字符串,并创建一个要处理的流,而不是将字符串拆分为每个字符的流,如下例所示:

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

另外,可以将正则表达式模式转换为谓词。这些谓词可用于过滤字符串流:

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

上述模式接受任何以字符结尾的字符串,`@gmail.com然后用作Java 8Predicate`来过滤电子邮件地址流。

数字运算

Java 8为使用无符号数字添加了额外的支持。Java中的数字一直都是签名的。我们来看看Integer例如:

int最多可表示$2^{32}$个数字。Java中的数字默认为有符号的,因此最后一位二进制数字代表符号(0 =正数,1 =负数)。因此,int从小数零开始,最大正值符号为$2^{32}-1$。

您可以通过Integer.MAX_VALUE访问此值:

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

Java 8增加了对解析无符号整数的支持。让我们看看它是如何工作的:

1
2
3
4
long maxUnsignedInt = (1l << 32) - 1;
String string = String.valueOf(maxUnsignedInt);
int unsignedInt = Integer.parseUnsignedInt(string, 10);
String string2 = Integer.toUnsignedString(unsignedInt, 10);

正如您所看到的,现在可以将最大无符号数$2^{32}-1$解析为整数。您还可以将此数字转换回表示无符号数字的字符串。

以前这是不可能的,parseInt因为这个例子证明了:

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

该数字不能作为有符号整数解析,因为它超过了最大值$2^{32}-1$。

算术运算

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
}

尝试通过toIntExact方法将long转换为int时,可能会抛出相同的异常:

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

文件操作

实用程序类Files最初是作为Java NIO的一部分在Java 7中引入的。JDK 8 API增加了一些额外的方法,使我们能够将函数流与文件一起使用。让我们深入研究几个代码示例。

列出文件

Files.list方法流式的给出指定目录的所有路径,所以我们可以使用流操作,如filter与sorted在文件系统中的内容。

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);
}

上面的示例列出了当前工作目录的所有文件,然后将每个路径映射到它的字符串表示形式。然后对结果进行过滤,排序并最终连接成一个字符串。如果您还不熟悉函数流,则应阅读我的Java8-Stream教程

您可能已经注意到,流的创建被包装到try-with语句中。数据流实现了AutoCloseable,并且这里我们需要显式关闭数据流,因为它基于IO操作。

返回的流封装了DirectoryStream。如果需要及时处理文件系统资源,则应使用try-with-resources构造来确保在流操作完成后调用流的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方法接受三个参数:目录路径start是起始点,maxDepth定义要搜索的最大文件夹深度。第三个参数是匹配谓词并定义搜索逻辑。在上面的示例中,我们搜索所有JavaScript文件(文件名以.js结尾)。

我们可以使用Files.walk方法实现相同的行为。此方法不是传递搜索谓词,而是遍历任何文件。

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);
}

在此示例中,我们使用流操作filter来实现与上一示例中相同的行为。

读写文件

将文本文件读入内存并将字符串写入文本文件在Java 8中是一项简单的任务。不再需要操作ReaderWriterFiles.readAllLines方法将给定文件的所有行读入字符串列表。您只需修改此列表并通过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方法。此方法不是一次将所有行读入内存,而是通过函数式流逐个读取和流式传输每一行。

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);
}

如果您需要更细粒度的控制,您可以构建一个新的缓冲Reader

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

或者,如果您想要写入文件,只需构造一个缓冲Writer

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

缓冲Reader还可以访问函数式流。lines方法在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提供了三种简单的方法来读取文本文件的行,使文本文件处理非常方便。

不幸的是,你必须使用try / with语句显式关闭功能文件流,这使得代码样本仍然有点混乱。我原本期望功能流在调用类似count或者collect方法等终止操作时自动关闭。因为无论如何都无法在同一个流上调用终止操作两次。

翻译原文: https://winterbe.com/posts/2015/03/25/java8-examples-string-number-math-files/