100% found this document useful (1 vote)
133 views7 pages

Java Features: 8 to 17 Overview

The document summarizes the major new features introduced in Java versions 8 through 17. Some key features discussed include: - Java 8 included lambda expressions and streams API, method references, and default methods to support functional programming. - Subsequent versions added features like modules, private interface methods, and switch expressions. - Examples demonstrate using lambda expressions and streams to filter a list of cars, and method references for brevity. - Default methods allow adding implementation of interface methods to maintain backward compatibility. - Type annotations allow adding semantic meaning to types for validation.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
133 views7 pages

Java Features: 8 to 17 Overview

The document summarizes the major new features introduced in Java versions 8 through 17. Some key features discussed include: - Java 8 included lambda expressions and streams API, method references, and default methods to support functional programming. - Subsequent versions added features like modules, private interface methods, and switch expressions. - Examples demonstrate using lambda expressions and streams to filter a list of cars, and method references for brevity. - Default methods allow adding implementation of interface methods to maintain backward compatibility. - Type annotations allow adding semantic meaning to types for validation.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Java Features from Java 8 to Java 17

Trainers, A lot has changed in Java from 1995 until today. Java 8 was a revolutionary release that put
Java back on the pedestal of the best programming languages.

Let’s go through most of the changes in the Java language that happened from Java 8 in 2014 until
Java 17. I will share the main features between Java 8 and Java 17. The intention is to have a
reference for all features between Java 8 and Java 17 inclusively. I am also sharing examples of each
new feature in Java 8 for reference.

Java 8

The main changes of the Java 8 release were these:

Lambda Expression and Stream API


Method Reference
Default Methods
Type Annotations
Repeating Annotations
Method Parameter Reflection

Java 9

Java 9 introduced these main features:

Java Module System


Try-with-resources
Diamond Syntax with Inner Anonymous Classes
Private Interface Methods

Java 10

Local Variable Type Inference

Java 11

Local Variable Type in Lambda Expressions

Java 14

Switch Expressions
The yield Keyword

Java 15

Text Blocks

Java 16

Pattern Matching of instanceof


Records

Java 17

Sealed Classes
Lambda Expressions and Stream API

Java was always known for having a lot of boilerplate code. With the release of Java 8, this statement
became less valid. The stream API and lambda expressions are the new features that move us closer
to functional programming.

The below examples will show how we use lambdas and streams in different scenarios.

Scenario:

We own a car dealership business. To discard all the paperwork, we want to create a piece of
software that finds all currently available cars that have run less than 50,000 km.

Let us take a look at how we would implement a function for something like this in a naive way:

To implement the above scenario, we will create a static function that accepts a List of cars. It
should return a filtered list according to a specified condition.

Example: [Before Java 8]

public class LambdaExpressions {

public static List<Car> findCarsOldWay(List<Car> cars) {

List<Car> selectedCars = new ArrayList<>();

for (Car car : cars) {

if ([Link] < 50000) {

[Link](car);

return selectedCars;

Let us look at how we would implement the above scenario using a Stream and a Lambda
Expression.

Example:

public class LambdaExpressions {

public static List<Car> findCarsUsingLambda(List<Car> cars) {

return [Link]().filter(car -> [Link] < 50000)

.collect([Link]());
}

Explanation:

We need to transfer the list of cars into a stream by calling the stream() method. Inside the filter()
method, we are setting our condition. We are evaluating every entry against the desired condition.
We are keeping only those entries that have less than 50,000 kilometers. The last thing we must do is
wrap it up into a list.

Method Reference

A method reference allows us to call functions in classes using a special syntax [::]. There are four
kinds of method references:

Reference to a static method


Reference to an instance method on an object
Reference to an instance method on a type
Reference to a constructor

We will use the same scenario of owning a car dealership shop and want to print out all the cars in
the shop. For that, we will use a method reference.

Example using the standard method call:

public class MethodReference {

List<String> withoutMethodReference =

[Link]().map(car -> [Link]())

.collect([Link]());

We are using a lambda expression to call the toString() method on each car.

Example Using a Method Reference for the above scenario

public class MethodReference {

List<String> methodReference = [Link]().map(Car::toString)

.collect([Link]());

We are, again, using a lambda expression, but now we call the toString() method by method
reference. We can see how it is more concise and easier to read.
Default Methods

Suppose we have a simple method log(String message) that prints log messages on invocation. We
wanted to provide message timestamps so that logs are easily searchable. We don’t want our clients
to break after we introduce this change. We will do this using a default method implementation on
an interface.

Default method implementation is the feature that allows us to create a fallback implementation of
an interface method.

In the below example, we have created a simple interface with just one method and implemented it
in LoggingImplementation class.

Example:

public class DefaultMethods {

public interface Logging {

void log(String message);

public class LoggingImplementation implements Logging {

@Override

public void log(String message) {

[Link](message);

Next, we will add a new method inside the interface. The method accepts the second argument,
called date, which represents the timestamp.

public class DefaultMethods {

public interface Logging {

void log(String message);

void log(String message, Date date);


}

We have added a new method but have yet to implement it inside all client classes. The compiler will
fail with the exception:
Class 'LoggingImplementation' must either be declared abstract
or implement abstract method 'log(String, Date)' in 'Logging'`.

After adding a new method inside the interface, our compiler threw exceptions. We will solve this
using the default method implementation for the new method.

Let us look at how to create a default method implementation:

public class DefaultMethods {

public interface Logging {

void log(String message);

default void log(String message, Date date) {

[Link]([Link]() + ": " + message);

Putting the default keyword allows us to add the implementation of the method inside the interface.
Now, our LoggingImplementation class does not fail with a compiler error even though we didn’t
implement this new method inside of it.

Type Annotations

Type annotations are one more feature introduced in Java 8. Even though we had annotations
available before, now we can use them wherever we use a type. This means that we can use them on
the following:

a local variable definition


constructor calls
type casting
generics
throw clauses

Tools like IDEs can then read these annotations and show warnings or errors based on the
annotations.
Local Variable Definition

Example to ensure that our local variable doesn’t end up as a null value:

public class TypeAnnotations {

public static void main(String[] args) {

@NotNull String userName = args[0];

We are using annotation on the local variable definition here. A compile-time annotation processor
could now read the @NotNull annotation and throw an error when the string is null.

Constructor Call

Example to make sure that we cannot create an empty ArrayList:

public class TypeAnnotations {

public static void main(String[] args) {

List<String> request =

new @NotEmpty ArrayList<>([Link](args).collect(

[Link]()));

This is the perfect example of how to use type annotations on a constructor. Again, an annotation
processor can evaluate the annotation and check if the array list is not empty.

Generic Type

One of our requirements is that each email has to be in the format <name>@<company>.com. If we
use type annotations, we can do it easily:

Example:

public class TypeAnnotations {

public static void main(String[] args) {

List<@Email String> emails;

}
}

This is a definition of a list of email addresses. We use @Email annotation that ensures that every
record inside this list is in the desired format.

A tool could use reflection to evaluate the annotation and check that each element in the list is a
valid email address.

Common questions

Powered by AI

The introduction of lambda expressions in Java 8 marked a significant shift towards functional programming in the Java language. Prior to Java 8, Java was often criticized for having verbose and boilerplate code. Lambda expressions allow for more concise and readable code by enabling programmers to write inline code blocks in a succinct manner. Using lambda expressions, developers can leverage the Stream API to perform operations on collections such as filtering, mapping, and reducing in a more efficient and less error-prone way. For example, instead of looping over a collection to filter its elements, developers can simply use a stream pipeline with lambda expressions to achieve the same result, which simplifies the logic and improves code readability .

Java 8 type annotations are particularly useful in scenarios where there is a need for additional validation or constraints on types that do not fit within the traditional type system. For example, using type annotations, developers can ensure that a local variable does not end up as null by using a @NotNull annotation. This adds a layer of compile-time validation that can help prevent null-pointer exceptions. Similarly, type annotations can be used on constructor calls, such as ensuring a list is not empty with @NotEmpty, which can prevent runtime issues related to unexpected empty collections. Annotations on generics, such as @Email for email addresses, allow for validation of data formats, which helps maintain data integrity. These annotations improve code reliability by enabling tools like IDEs and compilers to perform additional checks and provide warnings or errors based on the annotations, reducing potential runtime errors .

Local variable type inference, introduced in Java 10 with the 'var' keyword, impacts code readability by allowing developers to write more concise code without explicitly declaring the types of local variables. This can make code more readable by reducing verbosity, as the actual type of the variable is inferred by the compiler based on the assigned expression. However, while it enhances readability, it maintains type safety because the inferred type is still checked at compile-time, ensuring that type constraints are respected. Nevertheless, excessive use of 'var' may obscure the variable type, potentially making code harder to understand for humans if not used judiciously, especially in cases where the inferred type isn’t immediately obvious .

The Java Module System, introduced in Java 9, plays a crucial role in improving application architecture by providing a more effective means to encapsulate code and define clear module boundaries. It allows developers to specify which parts of a module are accessible to other modules and which parts are internal. This encapsulation prevents unwanted dependencies and promotes more maintainable and scalable codebases. The module system also enhances build and deployment efficiency by enabling greater control over classpath configuration and reducing the application footprint by including only necessary modules. Furthermore, it improves security by limiting the accessibility of internal APIs only to trusted modules. This modular approach helps manage dependencies more effectively and fosters better software design practices .

Default methods introduced in Java 8 facilitate backwards compatibility by allowing new methods to be added to interfaces without breaking existing client code. Before default methods, adding a method to an interface required all implementing classes to define that method, which could lead to substantial refactoring if the interface had widespread implementations. With default methods, developers can provide a default implementation for a method directly in the interface, enabling existing classes to inherit this default behavior without requiring changes. This feature enables the evolution of interfaces over time—such as adding new behaviors or improving existing functionalities—while maintaining compatibility with older codebases that use those interfaces .

Records, introduced in Java 16, provide a more efficient way to model immutable data objects by automating common boilerplate code associated with Java classes. In traditional Java classes, defining a data carrier class with fields necessitates manually writing constructors, getters, equals, hashCode, and toString methods. Records simplify this process by automatically generating these methods upon declaration, thus reducing boilerplate and potential errors. The concise syntax makes records ideal for modeling simple data structures with immutable fields, encouraging a functional style and reducing unintended side-effects from mutable state. This improves code readability and maintenance by focusing solely on the data essence, making records highly suitable for data transfer objects and applications following a value-based design .

Java 14 introduced switch expressions to simplify control flow statements and enhance their expressiveness. Traditional switch statements required a cumbersome syntax with a risk of fall-through errors if break statements were inadvertently omitted. Switch expressions, on the other hand, allow for more concise and less error-prone code by enabling developers to switch over values to produce a result. The addition of the yield keyword within switch expressions is significant because it allows for the return of a value from a switch block, eliminating the need for a supporting variable outside the switch to capture the result. This feature aligns with Java's move toward more functional-style constructs, making code more streamlined and reducing boilerplate .

Pattern matching for instanceof, introduced in Java 16, significantly enhances developer productivity by streamlining the common task of type-checking and casting. Traditional code required verbose constructs where a type-check using instanceof is immediately followed by an explicit cast. Pattern matching simplifies this by combining these steps: once a variable is checked to be an instance of a particular class, it can be directly and safely accessed as that class type within a test block. This reduces boilerplate code, minimizes potential errors (such as a missing or incorrect cast), and leads to cleaner and more readable code by allowing developers to focus on the logic rather than redundant operations .

Sealed classes, introduced in Java 17, provide a way to enforce strict control over class hierarchies and inheritance, enhancing the ability to maintain a well-defined and predictable class structure. A sealed class explicitly restricts which classes can extend it through a permits clause that lists the allowed subclasses. This restriction ensures that the class hierarchy is tightly controlled and prevents unauthorized or unintended subclassing, which can help maintain invariants and implement domain models more securely. The feature aligns with Java’s goals of enhancing software component modularity and readability by clearly defining possible implementations of a class, making the system behavior clearer and reducing the chances of extension-related errors .

Method references in Java 8 offer a more succinct and readable alternative to lambda expressions for calling methods. While lambda expressions are concise compared to prior approaches, method references can reduce the verbosity even further by eliminating the need to explicitly specify the method parameters in the lambda expression. For example, instead of writing 'car -> car.toString()', a method reference allows you to write 'Car::toString', which directly refers to the method. This improves readability and clarity by reducing the syntactic clutter in cases where the lambda expression is solely invoking an existing method. Method references provide four types of references, adding versatility in how methods can be referenced in different contexts .

You might also like