Oracle rolled out Java 17, the next long-term support release of Java in September 2021. Today we will focus on the Java new release features, which can be used in the daily development lifecycle. All features are generally available and enabled by default, except if they are labeled with one of the following: Preview – features are fully specified and implemented, but not yet considered to be final. They are considered to be almost complete, waiting for an additional round of real-world feedback. These features have to be explicitly enabled. Experimental – features are less stable and more likely to change. They also have to be explicitly enabled. Incubator – modules are non-final tools and APIs, and are distributed in separate modules. All the features I’ve singled out have been officially added to Java and are past their preview phases. Let’s take a look at 14 essential features from Java versions 12 to 17. Java 12 features 1. Java 12: File mismatch() method The method mismatch(Path, Path) compares two specified files and returns the index of the first byte where they differ or -1 if they don’t. The return value will be in the inclusive range of 0L up to the byte size of the smaller file or -1L if the files are identical. File relationships and their mismatch return values can be described as in the following table: Files RelationshipFiles.mismatch(Path, Path) Same File -1 (match) Copied File -1 (match) Different Files, same content -1 (match) Different Files, different content >0 (mismatch) Soft-linked -1 (match) Hard-linked -1 (match) Now, let’s take a look at two examples. In the first one, we’ll create two identical files and compare them, but in the second one we will create two different files with different content. @Test public void givenIdenticalFiles_thenShouldNotFindMismatch() throws IOException { Path filePath1 = Files.createTempFile("file1", ".txt"); Path filePath2 = Files.createTempFile("file2", ".txt"); Files.writeString(filePath1, "Java 12 Article"); Files.writeString(filePath2, "Java 12 Article"); long mismatch = Files.mismatch(filePath1, filePath2); // match assertEquals(-1, mismatch); } @Test public void givenDifferentFiles_thenShouldFindMismatch() throws IOException { Path filePath1 = Files.createTempFile("file1", ".txt"); Path filePath2 = Files.createTempFile("file2", ".txt"); Files.writeString(filePath1, "Java 12"); Files.writeString(filePath2, "Java 12 Post"); long mismatch = Files.mismatch(filePath1, filePath2); // mismatch -> return with byte size of smaller file assertEquals(7, mismatch); } 2. Java 12: Collectors.teeing() in Stream API It is a new static method teeing to java.util.stream.Collectors interface which allows to collect using two independent collectors and then merge their results using the supplied BiFunction. Every element passed to the resulting collector is processed by both downstream collectors. Then their results are merged into the final result using the specified merge function. Please note that this function helps in performing a certain task in a single step. We can already perform the given task in two steps if we do not use the teeing() function. It’s just a helper function to reduce verbosity. @Test public void givenArrayOfIntegerValues_thenCalculateMinAndMaxValue() { List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); Map<String, Optional<Integer>> collect = nums.stream().collect(Collectors.teeing( Collectors.minBy(Comparator.comparing(i -> i)), Collectors.maxBy(Comparator.comparing(i -> i)), (min, max) -> Map.of("min", min, "max", max))); assertEquals(1, collect.get("min").orElse(-1)); assertEquals(9, collect.get("max").orElse(-1)); } 3. Java 12: Compact Number Formatting CompactNumberFormat is a concrete subclass of NumberFormat that formats a decimal number in its compact form based on the patterns provided by a given locale. public static NumberFormat getCompactNumberInstance(Locale locale, NumberFormat.Style formatStyle) The locale parameter is responsible for providing proper format patterns. The format style can be either SHORT or LONG. For a better understanding of the format styles, let’s consider number 1000 in the US locale. The SHORT style would format it as “10K”, and the LONG one would do it as “10 thousand”. Now let’s take a look at an example that’ll take the number of likes under this article and compact it in two different styles: @Test public void givenNumber_thenFormatAsCompactValues() { NumberFormat likesShort = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT); likesShort.setMaximumFractionDigits(2); assertEquals("5.5K", likesShort.format(5500)); NumberFormat likesLong = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG); likesLong.setMaximumFractionDigits(2); assertEquals("5.5 thousand", likesLong.format(5500)); } We can also parse compact numbers into long patterns: @Test public void givenCompactValues_thenFormatAsNumber() throws ParseException { NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG); assertEquals(5500L, fmt.parse("5.5 thousand")); } 4. Java 12: Java Strings new Methods – indent() and transform() intent() method adjusts the indentation of each line based on the integer parameter. If the parameter is greater than zero, new spaces will be inserted at the beginning of each line. On the other hand, if the parameter is less than zero, it removes spaces from the beginning of each line. If a given line does not contain sufficient white space, then all leading white space characters are removed. @Test public void givenString_IntentStringWithNumberThatGreaterThanZero() { String text = """ Hi Symphony Solutions """;//First line does not have any space but in second one has 1 space in the beginning of line String newText = text.indent(1);//it adds space in each line,therefore length should be increased plus 2 assertEquals(text.length() + 2, newText.length()); } @Test public void givenString_IntentStringWithNumberThatLessThanZero() { String text = """ Hi Symphony Solutions """;//First line does not have any space but in second one has 1 space from beginning of line String newText = text.indent(-2); //it removes space in each line,therefore length should be decreased by 1 assertEquals(text.length() - 1, newText.length()); } Transform() method allows us to call a function on the given string. The function should expect a single String argument and produce an R result. Let’s look at an example where we will use transform() method to convert a CSV string to the list of strings: @Test public void givenString_thenConvertListOfStrings() { String text = "Hi,Symphony,Solutions"; List<String> strList = text.transform(s -> Arrays.asList(s.split(","))); assertEquals(strList.size(), 3); } Java 13 features 5. Java 13: FileSystems.newFileSystem() Method public static FileSystem newFileSystem(Path path, Map<String, ?> env) throws IOException Java FileSystems newFileSystem(URI uri, Map<String, ?> env) constructs a new file system that is identified by a URI. This method iterates over the FileSystemProvider installedProviders() providers to locate the provider that is identified by the URI URI getScheme() of the given URI. @Test public void givenFileName_thenCreateFileSystem() throws IOException { String zipFilename = "test.zip"; final Path path = Paths.get(zipFilename); final URI uri = URI.create("jar:file:" + path.toUri().getPath()); final Map<String, String> env = new HashMap<>(); env.put("create", "true"); try (FileSystem fileSystem = FileSystems.newFileSystem(uri, env)) { assertTrue(fileSystem.isOpen()); } } 6. Java 13: DOM and SAX Factories with Namespace Support New techniques for creating DOM and SAX factories that support Namespaces have been added. //java 13 onwards DocumentBuilder db = DocumentBuilderFactory.newDefaultNSInstance().newDocumentBuilder(); // before java 13 DocumentBuilderFactory dbf = DocumentBuilderFactory.newDefaultInstance(); dbf.setNamespaceAware(true); DocumentBuilder db = dbf.newDocumentBuilder(); Java 14 features 7. Java 14: Switch expressions Java 14 takes the switch to the next level. No longer a simple statement, it is now an expression, and so it can return a value! And by making this improvement we no longer have to think about break; Should one of your cases feed into a block of code, yield is used as the return statement of the Switch Expression, and we can assign this value to the corresponding variable. @Test public void givenString_thenTestSwitchWithExistingCaseValue() { String color = "Blue"; String adjacentColor = switch (color) { case "Blue", "Green" -> "yellow"; case "Red", "Purple" -> "blue"; case "Yellow", "Orange" -> "red"; default -> { System.out.println("The color could not be found."); yield "Unknown Color"; } }; assertEquals(adjacentColor, "yellow"); } 8. Java 14: Helpful NullPointerExceptions Previously, the stack trace for a NullPointerException didn’t have much of a story to tell, except that some value was null at a given line in a given file. Though useful, this information only suggested a line to debug instead of painting the whole picture for a developer to understand just by looking at the log. Now Java has made this easier by adding the capability to point out the name of the call that threw the exception, as well as the name of the null variable. int[] arr = null; arr[0] = 1; Earlier, on running this code, the log would say: Exception in thread "main" java.lang.NullPointerException at com.baeldung.MyClass.main(MyClass.java:27) But now, given the same scenario, the log might say: java.lang.NullPointerException: Cannot store to int array because "a" is null As we can see, now we know precisely which variable caused the exception. Java 15 features 9. Java 15: Text Blocks Starting from 2020, Java 15 finally supports text blocks! I am so excited by the fact that I no longer have to type dozens of “+” signs. I can simply do this instead: String json = """ "name":"Mammadali", "surname":"Alizada" """; Text blocks now have two new escape sequences: \: to indicate the end of the line, so that a new line character is not introduced \s: to indicate a single space String multiline = """ A quick brown fox jumps over a lazy dog; \ the lazy dog howls loudly."""; String json = """ "name":"Mammadali", "surname":"Alizada" """; In this example, we have improved readability of the sentence for the human eye, and no new line is added ->after “Alizada”.Finally, text blocks only blocks, you can’t use them in a single line. 10. Java 15: Hidden Classes A new feature being introduced in Java 15 is known as hidden classes. While most developers won’t find a direct benefit from them, anyone who works with dynamic bytecode or JVM languages will likely find them useful. The goal of hidden classes is to allow the runtime creation of classes that are not discoverable. This means they cannot be linked by other classes, nor can they be discovered via reflection. Classes such as these typically have a short lifecycle, and thus, hidden classes are designed to be efficient with both loading and unloading. Note that current versions of Java do allow for the creation of anonymous classes similar to hidden classes. However, they rely on the Unsafe API. Hidden classes have no such dependency. Java 16 features 11. Java 16: Pattern matching for instanceof Pattern Matching is a means to get rid of needless casting after an instanceof condition is met. JVM automatically casts the variable for us and assigns the result to the new binding variable. For example, we’re all familiar with this situation: if (o instanceof Car) { System.out.println(((Car) o).getModel()); } Which, of course, is necessary if you want to access the Car methods of o. That being said, it is also true that in the second line, there is no question that o is a Car – the instanceof has already confirmed that. So, with Pattern Matching, a small change is made: if (o instanceof Car c) { System.out.println(c.getModel()); } You can even use Pattern Matching in the same line as the instanceof: public boolean isHonda(Object o) { return o instanceof Car c && c.getModel().equals("Honda"); } 12. Java 16: Add Stream.toList Method The aim is to reduce the boilerplate with some commonly used Stream collectors, such as Collectors.toList and Collectors.toSet. List<String> integersAsString = Arrays.asList("1", "2", "3"); List<Integer> ints = integersAsString.stream().map(Integer::parseInt).collect(Collectors.toList()); //previous version List<Integer> intsEquivalent = integersAsString.stream().map(Integer::parseInt).toList(); //new version Java 17 features 13. Java 17: Sealed classes Sealed classes are somewhat similar to final classes. When sealed keyword is accompanied by permits keyword, this indicates that a class or an interface can be extended or implemented only by specified classes or interfaces. Using sealed classes allows developers to know all possible subtypes supported in the given format. Let’s take a look at a simple example: public sealed interface Expression permits Addition { double perform(double x, double y); } final class Addition implements Expression, Serializable { @Override public double perform(double x, double y) { return x + y; } } //This statement gives compile error, because Expression interface does not allow to Log subtype final class Log implements Expression { @Override public double perform(double x, double y) { return 0; } } 14. Java 17: Records Records are data-only classes that handle all the boilerplate code associated with POJOs. We can avoid all the unnecessary boilerplate code by using Records. Even though it doesn’t seem like a huge enhancement, it drastically increases developer productivity. That is to say, a POJO class like the following: public final class Person { private final String name; private final int age; private final String profession; public Person(String name, int age, String profession) { this.name = name; this.age = age; this.profession = profession; } public String name() { return name; } public int age() { return age; } public String profession() { return profession; } @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj == null || obj.getClass() != this.getClass()) return false; var that = (Person) obj; return Objects.equals(this.name, that.name) && this.age == that.age && Objects.equals(this.profession, that.profession); } @Override public int hashCode() { return Objects.hash(name, age, profession); } @Override public String toString() { return "Person[" + "name=" + name + ", " + "age=" + age + ", " + "profession=" + profession + ']'; } } Becomes this: public record Person(String name, int age, String profession) {} We no longer have to think about adding hashCode(), equals(), toString(). These are usually easily generated by a favorite IDE, but still for me this was a long-awaited feature! As you can see, Records are both final and immutable – there are no extending Records, and once a Record object is created, its fields cannot be changed. You are allowed to declare methods in a Record, both non-static and static, and implement interfaces just like with final classes. Conclusion Java remains the top programming language that is widely used by software programmers and is often recommended as the starting point for those just starting out in their careers. In this article, we have overviewed updates from Java 12 to Java 17. Of course, this doesn’t cover everything, but mostly the few that I found interesting. Hopefully, this gives you the gist of what they’re capable of doing. Looking at the improvements Java has delivered in one go, we can be sure that the Java platform is well-positioned for further development and growth in the cloud. So, if you want to become a software developer, you must learn Java to stay on track with the new developments. Mammadali has been working for Symphony Solutions since November 2021. He is currently living in Baku, Azerbaijan. Mammadali graduated from Azerbaijan State Oil and Industry university as Computer Engineering and till now has more than 5 years’ experience as developer. He’s both driven and self-motivated, and constantly experimenting with new technologies and techniques to became better professional in the future Mammadali Alizada Senior Software Developer