Plenty of tutorials and articles cover the most important changes in Java 8 like lambda expressions and functional streams. But furthermore many existing classes have been enhanced in the JDK 8 API with useful features and methods.
This article covers some of those smaller changes in the Java 8 API – each described with easily understood code samples. Let’s take a deeper look into Strings, Numbers, Math and Files.
Slicing Strings
Two new methods are available on the String class: join
and chars
. The first method joins any number of strings into a single string with the given delimiter:
String.join(":", "foobar", "foo", "bar");
// => foobar:foo:bar
Copy the code
The second method chars
creates a stream for all characters of the string, so you can use stream operations upon those characters:
"foobar:foo:bar"
.chars()
.distinct()
.mapToObj(c -> String.valueOf((char)c))
.sorted()
.collect(Collectors.joining());
// => :abfor
Copy the code
Not only strings but also regex patterns now benefit from streams. Instead of splitting strings into streams for each character we can split strings for any pattern and create a stream to work upon as shown in this example:
Pattern.compile(":")
.splitAsStream("foobar:foo:bar")
.filter(s -> s.contains("bar"))
.sorted()
.collect(Collectors.joining(":"));
// => bar:foobar
Copy the code
Additionally regex patterns can be converted into predicates. Those predicates can for example be used to filter a stream of strings:
Pattern pattern = Pattern.compile(".*@gmail\\.com"); Stream.of("[email protected]", "[email protected]") .filter(pattern.asPredicate()) .count(); / / = > 1Copy the code
The above pattern accepts any string which ends with @gmail.com
and is then used as a Java 8 Predicate
to filter a stream of email addresses.
Crunching Numbers
Java 8 adds additional support for working with unsigned numbers. Numbers in Java had always been signed. Let’s look at Integer
for example:
Numbers in Java are per default signed, so the last binary digit represents the sign (0 = positive, Thus the maximum positive signed int is 2³¹ -1 starting with the decimal zero.
You can access this value via Integer.MAX_VALUE
:
System.out.println(Integer.MAX_VALUE); // 2147483647 System.out.println(Integer.MAX_VALUE + 1); / / - 2147483648Copy the code
Java 8 adds support for parsing unsigned ints. Let’s see how this works:
long maxUnsignedInt = (1l << 32) - 1;
String string = String.valueOf(maxUnsignedInt);
int unsignedInt = Integer.parseUnsignedInt(string, 10);
String string2 = Integer.toUnsignedString(unsignedInt, 10);
Copy the code
As you can see it’s now possible to parse the maximum possible unsigned number 2³² – 1 into an integer. And you can also convert this number back into a string representing the unsigned number.
This wasn’t possible before with parseInt
as this example demonstrates:
try {
Integer.parseInt(string, 10);
}
catch (NumberFormatException e) {
System.err.println("could not parse signed int of " + maxUnsignedInt);
}
Copy the code
The number is not parseable as a signed int because it exceeds The maximum of 2³¹ -1.
Do the Math
The utility class Math
has been enhanced by a couple of new methods for handling number overflows. What does that mean? We’ve already seen that all number types have a maximum value. So what happens when the result of an arithmetic operation doesn’t fit into its size?
System.out.println(Integer.MAX_VALUE); // 2147483647 System.out.println(Integer.MAX_VALUE + 1); / / - 2147483648Copy the code
As you can see a so called integer overflow happens which is normally not the desired behavior.
Java 8 adds support for strict math to handle this problem. Math
has been extended by a couple of methods who all ends with exact
, e.g. addExact
. Those methods handle overflows properly by throwing an ArithmeticException
when the result of the operation doesn’t fit into the number type:
try {
Math.addExact(Integer.MAX_VALUE, 1);
}
catch (ArithmeticException e) {
System.err.println(e.getMessage());
// => integer overflow
}
Copy the code
The same exception might be thrown when trying to convert longs to int via toIntExact
:
try {
Math.toIntExact(Long.MAX_VALUE);
}
catch (ArithmeticException e) {
System.err.println(e.getMessage());
// => integer overflow
}
Copy the code
Working with Files
The utility class Files
was first introduced in Java 7 as part of Java NIO. The JDK 8 API adds a couple of additional methods which enables us to use functional streams with files. Let’s deep-dive into a couple of code samples.
Listing files
The method Files.list
streams all paths for a given directory, so we can use stream operations like filter
and sorted
upon the contents of the file system.
try (Stream stream = Files.list(Paths.get(""))) { String joined = stream .map(String::valueOf) .filter(path -> ! path.startsWith(".")) .sorted() .collect(Collectors.joining("; ")); System.out.println("List: " + joined); }Copy the code
The above example lists all files for the current working directory, then maps each path to it’s string representation. The result is then filtered, sorted and finally joined into a string. If you’re not yet familiar with functional streams you should read my Java 8 Stream Tutorial.
You might have noticed that the creation of the stream is wrapped into a try/with statement. Streams implement AutoCloseable
and in this case we really have to close the stream explicitly since it’s backed by IO operations.
The returned stream encapsulates a DirectoryStream. If timely disposal of file system resources is required, the try-with-resources construct should be used to ensure that the stream’s close method is invoked after the stream operations are completed.
Finding files
The next example demonstrates how to find files in a directory or it’s sub-directories.
Path start = Paths.get("");
int maxDepth = 5;
try (Stream 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);
}
Copy the code
The method find accepts three arguments: The directory path start is the initial starting point and maxDepth defines the maximum folder depth to be searched. The third argument is a matching predicate and defines the search logic. In the above example we search for all JavaScript files (filename ends with .js).
We can achieve the same behavior by utilizing the method Files.walk
. Instead of passing a search predicate this method just walks over any file.
Path start = Paths.get("");
int maxDepth = 5;
try (Stream 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);
}
Copy the code
In this example we use the stream operation filter
to achieve the same behavior as in the previous example.
Reading and writing files
Reading text files into memory and writing strings into a text file in Java 8 is finally a simple task. No messing around with readers and writers. The method Files.readAllLines
reads all lines of a given file into a list of strings. You can simply modify this list and write the lines into another file via Files.write
:
List lines = Files.readAllLines(Paths.get("res/nashorn1.js")); lines.add("print('foobar');" ); Files.write(Paths.get("res/nashorn1-modified.js"), lines);Copy the code
Please keep in mind that those methods are not very memory-efficient because the whole file will be read into memory. The larger the file the more heap-size will be used.
As an memory-efficient alternative you could use the method Files.lines
. Instead of reading all lines into memory at once, this method reads and streams each line one by one via functional streams.
try (Stream stream = Files.lines(Paths.get("res/nashorn1.js"))) {
stream
.filter(line -> line.contains("print"))
.map(String::trim)
.forEach(System.out::println);
}
Copy the code
If you need more fine-grained control you can instead construct a new buffered reader:
Path path = Paths.get("res/nashorn1.js");
try (BufferedReader reader = Files.newBufferedReader(path)) {
System.out.println(reader.readLine());
}
Copy the code
Or in case you want to write to a file simply construct a buffered writer instead:
Path path = Paths.get("res/output.js"); try (BufferedWriter writer = Files.newBufferedWriter(path)) { writer.write("print('Hello World');" ); }Copy the code
Buffered readers also have access to functional streams. The method lines
construct a functional stream upon all lines denoted by the buffered reader:
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);
}
Copy the code
So as you can see Java 8 provides three simple ways to read the lines of a text file, making text file handling quite convenient.
Unfortunately you have to close functional file streams explicitly with try/with statements which makes the code samples still kinda cluttered. I would have expected that functional streams auto-close when calling a terminal operation like count or collect since you cannot call terminal operations twice on the same stream anyway.
I hope you’ve enjoyed this article. All code samples are hosted on GitHub along with plenty of other code snippets from all the Java 8 articles of my blog. If this post was kinda useful to you feel free to star the repo and follow me on Twitter.
Keep on coding!