DEV Community

Ruby Valappil
Ruby Valappil

Posted on • Originally published at Medium on

A Quick Look at The Programming Features Introduced between Java 1.8 and Java 17

Features that make your Java code adhere to modern standards

Photo by Reka Illyes on Unsplash

Java went real quick from 1.8 to 17 and this year version 18 is slated for release. In this article, we will try to cover the features that were released from the 1.8th version till the 17th.

This is going to be a lengthy list, so let’s not spend too much time on setting up the stage.

Please do note that the below list doesn’t cover all the features and changes that were introduced in each release but the ones that would be most helpful in typical programming cases and makes programming more interesting and easy.

Java 8

Lambda:

Lambda expressions let us use functionality as a method argument. Let’s take a look at the code before Java 8 when we had to create an anonymous class to implement a simple interface.

Thread t = new Thread( new Runnable() {
 public  void run() {
  System.out.println("Start thread");
 }
});
Enter fullscreen mode Exit fullscreen mode

Using lambda expressions, we can express the instance of a class in a single line

Thread t1 = new Thread(() -> {
 System.out.println("Start 1st thread");
});
Enter fullscreen mode Exit fullscreen mode

Functional Interface:

A functional Interface is an Interface that has only one abstract method. Additionally, the Interface can have a default and a static method but only one abstract method.

We have covered these features in length in a previous article, please refer to that article for examples.

Stream

Stream is an interface in the java.util.stream package that provides ways to perform sequential and parallel operations on collections.

stream() method of Collection Interface returns back a stream of elements of the given collection which are of type Stream.

The stream interface supports many operations that are needed to filter or map or find an aggregate result of the elements in a stream.

In the example below, we call stream() method on a List and then pass a Predicate (Functional Interface) to the anyMatch() method that returns a boolean.

List<String> colors = new ArrayList<>(Arrays._asList_("Green","Yellow","Red"));

boolean isTrue = colors.stream().anyMatch(x -> x.equals("Red"));

System.out.println(isTrue);
Enter fullscreen mode Exit fullscreen mode

Optional

Optional creates a container object which may or may not contain a non-null value. Using Optional we can check whether a value is non-null (present) or not, we use isPresent() to validate that.

In the sample code below, we use findAny() method of Stream Interface which returns back an Optional object. Based on the value of the Optional instance we can execute the corresponding logic.

To perform a similar operation like below in the Java versions before 8, we would have to perform an explicit null check on the String.

List<String> colors = new ArrayList<>(Arrays._asList_("Green","Yellow","Red"));

Optional<String> color = colors.stream().filter(x -> x.equals("Black")).findAny();

System.out.println(color.isPresent()? color.get() : "Not Found");
Enter fullscreen mode Exit fullscreen mode

Changes in java.util.concurrent package:

Interface CompletionStage and Class CompletableFuture and CompletionException were introduced in Java 8.

CompletableFuture helps to perform asynchronous tasks that are much more flexible and enhanced than the Future object that existed before Java 8.

Since we have already covered the concurrent package and Future in a previous article, we will not get into analyzing a sample code in the current article.

Method References (::)

Double colons can be used to invoke an existing method instead of using lambda expressions.

Method References can be used to refer to static methods and instance methods. Let’s check an example where we refer to a static method.

First, let’s implement a method invocation using a lambda expression,

public class Main {

public static void main(String[] args) {
 List<Integer> locList = **new** ArrayList<>(Arrays._asList_(1,2,5,6));
 boolean isFound = locList.stream().anyMatch(l -> _isEqual_(l));
 System.out.println(isFound);
}

public static <T> boolean isEqual(T a) {
Predicate<T> isEqual = (x) -> x.equals(5);
return isEqual.test(a);
}
}
Enter fullscreen mode Exit fullscreen mode

isEqual() is a static method in class Main so instead of using a lambda expression to invoke the method, we can simply use the method reference to get the job done.

public class Main {

public  static  void main(String[] args) {
 List<Integer> locList = new ArrayList<>(Arrays._asList_(1,2,5,6));
 boolean isFound = locList.stream().anyMatch(Main::_isEqual_);
 System.out.println(isFound);
}

public static <T> boolean isEqual(T a) {
 Predicate<T> isEqual = (x) -> x.equals(5);
 return isEqual.test(a);
}
}
Enter fullscreen mode Exit fullscreen mode

Java 9

Stream.ofNullable

Since Java 8, we could create a stream of elements using Stream.of(Object… values), from Java 9 we can use Stream.ofNullable on a single element to create a stream or return an empty stream.

List<Location> locList = **new** ArrayList<>(Arrays._asList_(loc, loc1, loc2));
Stream._ofNullable_(locList);
Enter fullscreen mode Exit fullscreen mode

Private Methods in Interface

Java 8 introduced the concepts of default methods in Interface. If there is more than one default method that wants to share the same logic then that common logic can be implemented using private methods.

Java Modules

Java module deserves an article of its own but let’s take a quick look at what it is. Java module is a technique by which packages and their related resources can be grouped together. A module contains a module descriptor that would specify the module name, dependent modules, packages that are available for other modules, etc.

One of the end goals is to reduce the size of our application at runtime. If we are not using GUI, we need not add that module as a dependency in our application.

Java 10

local variable type inference

From Java 10 onwards we can initialize non-null local variables and assign them to type var. The type of the variable is decided from the context.

Sample usage is shown below,

public class Sample {

public  static  void main(String[] args) {

var x = 10;
var y = 11;
System.out.println(x + y);

var obj = new TryVar();
System.out.println(obj.getName());

var c; //Illegal, Compiler will throw an error -> "Cannot use 'var' on variable without initializer"
}
}

class TryVar {
 String name;

 public String getName() {
  return "my name";
 }
}
Enter fullscreen mode Exit fullscreen mode

Java 11

new method in Optional

Under the Java 8 features, we learned how to use Optional to replace explicit null check using Optional.isPresent() method.

Since Java 11, we can do a similar check using Optional.isEmpty() method which also returns a boolean based on it is null or non-null;

Java 12

Collectors.teeing()

teeing() returns a Collector that is a composite of two downstream collectors.

Let’s check an example. In this example, we will use a class named Location which has two variables x and y. In the teeing example, we will fetch the number of records that have the accepted value for x and the accepted value for y. (Logic wouldn’t make any sense but let’s just focus on how teeing works 😝 )

//Create three instances of Location
Location loc = new Location(2.2, 3.6);
Location loc1 = new Location(2.4, 3.3);
Location loc2 = new Location(2.3, 3.2);

// Add the reference variables to an ArrayList
List<Location> locList = **new** ArrayList<>(Arrays._asList_(loc, loc1, loc2));

//Count records that has correct x, Count the records that has correct y and create a new Location object  
var result = locList.stream().collect(Collectors._teeing_(Collectors._filtering_(Location :: isXFound, Collectors._counting_()),Collectors._filtering_(Location :: isYFound, Collectors._counting_()), Location :: new ));

//Print the Result
System.out.println("Records with correct X coordinate " + result.getX() + " :: Records with correct Y coordinate " + result.getY());
Enter fullscreen mode Exit fullscreen mode

Class Location looks like below,

Java 13

Text Blocks

Text Blocks minimize the Java syntax that is needed to represent a multi-line string.

It could be used in place of any string that is conventionally added within double-quotes.

Before Text Blocks, if we had to print a multi-line string, we would need to use delimiters, concatenations, etc.

For example, the below code would give us the complete string in a single line

System.out.print("Hey There "

+ "What's up?? "

+ "How was your vacation?? "

+ ";)");
Enter fullscreen mode Exit fullscreen mode

output

Hey There What's up?? How was your vacation?? ;)
Enter fullscreen mode Exit fullscreen mode

To print them in the next line we will have to modify the above code to the one given below,

System.out.print("Hey There \n"

+ "What's up?? \n"

+ "How was your vacation?? \n"

+ ";)");
Enter fullscreen mode Exit fullscreen mode

Using Text Blocks we can rewrite the above code to the one given below,

System.out.print("""

Hey There

What's up??

How was your vacation??

;)

""");
Enter fullscreen mode Exit fullscreen mode

Text Blocks can also be used in place of standard strings.

For example, both strings shown below have the same meaning

_//Text Block  
printMsg_ ("""
Print This!! """);

// String in Double Quotes
_printMsg_("Print this!");
Enter fullscreen mode Exit fullscreen mode

Java 14

Records

Records is a restricted form of class that is ideal for POJOs. A standard data-carrier-class would have a few private fields along with constructors and getters/setters.

Let’s check an example of a simple data-carrier class that has two members,

public  class Location {
double x;
double y;

public Location(double x, double y) {
this.x = x;
this.y = y;
}

public double getX() {
 return x;
}

public double getY() {
 return y;
}
}
Enter fullscreen mode Exit fullscreen mode

We can rewrite the above class using a Record using the below-given code

record NewLocation( **double** x, **double** y) {}
Enter fullscreen mode Exit fullscreen mode

A Record will acquire the getters and constructors at runtime. It also gets the equals(), hashcode() and toString() methods.

Java 15

Sealed Class / Interface

sealed is a modifier we can add to a Class and Interface. A sealed class can decide the subclasses that can extend it and stop other classes from doing so.

To declare a Class / Interface as sealed, we need to add the “sealed” modifier and then add extends or implements, if applicable, followed by “permits” clause. “permits” is followed by the name of the subclasses that are allowed to extend it.

Other conditions are that, the subclass that’s extending a sealed class can be 1. final — which stops it from further extensions,

  1. sealed — which lets the sealed subclass define it’s own permitted subclasses,
  2. non-sealed — let’s any arbtrary class extend it.

For example, if Location is a sealed class, then we can declare it as,

public  sealed  class Location permits SubLocation
Enter fullscreen mode Exit fullscreen mode

and the Sublocation class could be declared as,

public  final  class SubLocation extends Location{}
Enter fullscreen mode Exit fullscreen mode

or

public  sealed  class SubLocation extends Location permits AnotherClass{}
Enter fullscreen mode Exit fullscreen mode

or

public non-sealed class SubLocation extends Location{}
Enter fullscreen mode Exit fullscreen mode

Java 16

Stream.toList()

From Java 16, we can collect the result from stream directly to a list instead of using Collectors.toList().

List<Integer> collectedList = locList.stream().filter( l -> l >=2).collect(Collectors._toList_());
Enter fullscreen mode Exit fullscreen mode

The above code will yield the same result as the one given below

List<Integer> collectedListNew = locList.stream().filter( l -> l >=2).toList();
Enter fullscreen mode Exit fullscreen mode

Java 17

Pattern matching for switch

Instead of just using constant values in switch cases we can now use patterns as case labels.


Discussion (0)