New Features in Java Versions 12 Through 17: What to add to Your Toolbox
Tech Insights
New Features in Java Versions 12 Through 17: What to add to Your Toolbox
New Features in Java Versions 12 Through 17: What to add to Your Toolbox
Tech Insights

New Features in Java Versions 12 Through 17: What to add to Your Toolbox

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: 

  1. \: to indicate the end of the line, so that a new line character is not introduced 
  2. \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
Mammadali Alizada
Senior Software Developer

Share