Bye Bye, Java 8!

Dr. Çağrı ATASEVEN
14 min readJul 3, 2023

If you’re a Selenium user, probably you’ve heard the declaration posted on Selenium’s official site:

“If it ain’t broke, don’t fix it” is a saying you may have heard, but sometimes it’s necessary to move on from our old favorites. re-announcing that Selenium will stop supporting Java 8 on September 30, 2023".

Admittedly, there are many Java 8 users, although it is old. And I’m sure this news upset them. I must admit that I am one of them :D I started using Selenium with Java 8 and I still use it in some of my projects without any problems.

But it seems it’s time to say goodbye to Java 8. Bidding farewell to this version, which has functioned seamlessly for many years, evokes a sense of nostalgia. In today’s rapidly evolving software landscape, there may not be much room for the outdated, but I found myself becoming emotional. Hence, I decided to pen this article as a tribute to Java 8, commemorating its lasting impact.

A quick look at Java History

Origins and Development at Sun Microsystems (1991–1995): Java was originally conceived by James Gosling, Patrick Naughton, Chris Warth, Ed Frank, and Mike Sheridan at Sun Microsystems (now Oracle Corporation) in 1991. The project, initially called “Oak,” aimed to develop a platform-independent language for consumer electronics. Oak was designed with the principles of simplicity, platform independence, and security in mind. [1]

Introduction of Java (1995): In 1995, Sun Microsystems officially released Java to the public. Initially, it was primarily used for programming applets, which were small, interactive programs embedded within web pages. Java applets gained popularity due to their ability to provide dynamic content and interactivity on the web. [1]

Platform Independence and “Write Once, Run Anywhere” (1996–1998): One of the key features of Java is its platform independence. Java programs are compiled into bytecode, which can run on any system with a Java Virtual Machine (JVM). This allows developers to write code once and run it on different platforms without the need for platform-specific modifications. The “Write Once, Run Anywhere” slogan became synonymous with Java. [2]

Introduction of Java 2 Platform (1998):
In 1998, Sun Microsystems released the Java 2 Platform, Standard Edition (J2SE), which was a major update to the Java platform. It introduced new features, improved performance, and enhanced APIs for developing desktop and server applications. J2SE included the Java Development Kit (JDK) and the Java Runtime Environment (JRE). [3]

Expansion of Java Ecosystem (late 1990s-early 2000s):
During this period, Java saw significant expansion in its ecosystem. Sun Microsystems introduced the Java Enterprise Edition (J2EE), which provided a platform for developing enterprise-level applications. Additionally, the Java Micro Edition (J2ME) was introduced for developing applications on mobile devices and embedded systems. [4]

Open Sourcing of Java (2006–2007):
In 2006, Sun Microsystems announced plans to open-source Java. This led to the creation of the OpenJDK project, which aimed to develop an open-source implementation of the Java platform. In 2007, Sun Microsystems released the majority of the Java platform under the GNU General Public License (GPL) as part of the OpenJDK project. [5]

Acquisition by Oracle (2010):
In 2010, Oracle Corporation acquired Sun Microsystems, including the Java technology. Oracle has since taken the lead in the development and stewardship of the Java platform.

Evolution of Java Versions and Features

Java has gone through several major releases, with each version introducing new features and improvements. Some notable versions include [6]:

Java SE 5 (released in 2004): Introduced significant language enhancements such as generics, annotations, and the enhanced for loop.
Java SE 7 (released in 2011): Introduced features like the try-with-resources statement, improved exception handling, and the diamond operator for type inference.
Java SE 8 (released in 2014): A major release that introduced lambda expressions, the Stream API, the Date and Time API, and default methods in interfaces.
Java SE 9 (released in 2017): Introducing modularity with the Java Platform Module System (JPMS), and improvements in performance, security, and language features.
Java SE 10–17 (released from 2018 to 2021): These versions introduced various incremental improvements, new language features, performance enhancements, and additional APIs.
Java SE 18 (released in 2022): Java 18 brings numerous performance, stability, and security improvements, along with nine enhancements that enhance developer productivity. These include features like Code Snippets in API Documentation, a Simple Web Server for prototyping, and preview features like Pattern Matching for Switch. [7]
Java SE 19 (released in 2022): Key updates include language improvements from the OpenJDK project, library enhancements for interoperability and leveraging vector instructions, and previews for Project Loom, which aims to simplify concurrent application development. [8]
Java SE 20 (released in 2023): The vector API allows for optimized vector computations on supported CPU architectures, virtual threads simplify concurrent application development, structured concurrency streamlines multithreaded programming, scoped values enable safe sharing of immutable data, record patterns enhance data deconstruction, and the foreign function and memory API facilitates interoperability with native code. These features have been incubated and refined across previous JDK versions, and their inclusion in JDK 20 reflects Oracle’s commitment to continuous improvement and innovation in the Java ecosystem. [9]

What about the Java 8?

Java 8 marked a significant milestone in the history of Java due to the introduction of several important features and improvements. To understand its significance, let’s compare it to some of the older versions of Java:

Lambda Expressions and Functional Programming:

One of the most influential additions in Java 8 was the support for lambda expressions. This feature allowed developers to write more concise and expressive code, promoting the adoption of functional programming principles. Prior to Java 8, achieving similar functionality required verbose anonymous inner classes, making the code more complex and harder to read.

Let's say, we have a list of names, and we want to iterate over the list and print each name. Next, we want to filter names starting with the letter ‘A’. Finally, we want to print the filtered names.

The Java code below can solve this problem.

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class LambdaExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave");

// Using an anonymous inner class to iterate and print each name
names.forEach(new MyConsumer());

// Using a separate class implementing Predicate to filter names starting with 'A'
names.stream()
.filter(new MyPredicate())
.forEach(new MyConsumer());
}

// Implementing a consumer to print each name
static class MyConsumer implements Consumer<String> {
@Override
public void accept(String name) {
System.out.println(name);
}
}

// Implementing a predicate to filter names starting with 'A'
static class MyPredicate implements Predicate<String> {
@Override
public boolean test(String name) {
return name.startsWith("A");
}
}
}

If we solve the same problem with the Lambda expression, the code will be as follows.

import java.util.Arrays;
import java.util.List;

public class LambdaExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave");

// Using Lambda expression to iterate and print each name
names.forEach(name -> System.out.println(name));

// Using Lambda expression with functional interface to filter names starting with 'A'
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(filteredName -> System.out.println(filteredName));
}
}

The problem is solved by using Lambda expressions and Lambda expressions allow us to define the behavior or action we want to perform on each element of the list or the condition we want to apply for filtering, without the need to create separate classes or write lengthy anonymous inner classes.

By using Lambda expressions, we can:

Eliminate the need for creating separate classes for simple actions or conditions, reducing code verbosity and improving readability.

Define the behavior or condition directly at the point of use, making the code more concise and localized.

Pass around behavior as if it were data, allowing us to treat code as a first-class citizen and enabling more flexible and modular programming.

In summary, Lambda expressions solve the problem of code verbosity and boilerplate when dealing with simple behaviors or conditions, allowing for more expressive and streamlined code. They make the code easier to read and maintain and promote the functional programming paradigm in Java.

Stream API:

The Stream API is closely related to lambda expressions and provides a powerful and declarative way to process data in collections. Streams allow you to perform operations such as filtering, mapping, and reducing elements in a collection with a concise and readable syntax.

Suppose we have a list of integers and we want to filter out the even numbers, double each number, and then find their sum.

we will start by creating a list of integers. Then, we create a stream from the list using the stream() method.

import java.util.Arrays;
import java.util.List;

public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(n -> n * 2)
.sum();

System.out.println("Sum of doubled even numbers: " + sum);
}
}
  1. filter(n -> n % 2 == 0): This operation filters out the even numbers from the stream, using a Lambda expression as the predicate.
  2. mapToInt(n -> n * 2): This operation doubles each remaining number in the stream, using a Lambda expression to perform the mapping.

Default Methods in Interfaces:

In previous versions of Java, once an interface was released, any changes to it would break compatibility with existing implementations. Java 8 introduced default methods in interfaces, which allowed the addition of new methods without breaking compatibility. This feature greatly facilitated the evolution of interfaces, making it easier to add new functionality to existing codebases.

interface Vehicle {
void start();
void stop();

default void honk() {
System.out.println("Honking the horn");
}
}

class Car implements Vehicle {
@Override
public void start() {
System.out.println("Starting the car");
}

@Override
public void stop() {
System.out.println("Stopping the car");
}
}

class Bike implements Vehicle {
@Override
public void start() {
System.out.println("Starting the bike");
}

@Override
public void stop() {
System.out.println("Stopping the bike");
}

@Override
public void honk() {
System.out.println("Bike horn sound");
}
}

public class DefaultMethodsExample {
public static void main(String[] args) {
Vehicle car = new Car();
car.start();
car.stop();
car.honk();

Vehicle bike = new Bike();
bike.start();
bike.stop();
bike.honk();
}
}

In the above example, we have an interface Vehicle that declares two abstract methods: start() and stop(). Additionally, the interface defines a default method honk() that provides a default implementation for honking the horn.

The Car class implements the Vehicle interface and provides its own implementations for the start() and stop() methods.

The Bike class also implements the Vehicle interface and overrides the start() and stop() methods. In addition, it overrides the honk() method to provide a different implementation specific to bikes.

In the main() method, we create instances of Car and Bike and invoke the methods defined in the Vehicle interface, including the default method honk(). The default implementation is used when the implementing class doesn't provide its own implementation.

Long story short, default methods in interfaces allow for adding new functionality to existing interfaces without breaking backward compatibility with implementing classes. They provide a mechanism for extending interfaces without requiring modifications to all implementing classes.

Functional Interfaces:

Java 8 introduced the java.util.function package, which includes a set of functional interfaces such as Predicate, Function, Consumer, and Supplier. These interfaces enable the use of lambda expressions and provide a foundation for functional programming in Java.

In Java 8, a functional interface is an interface that contains only one abstract method. Functional interfaces are also known as single abstract method (SAM) interfaces. The concept of functional interfaces is closely related to lambda expressions and functional programming.

Let’s examine the example below:

@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}

public class FunctionalInterfaceExample {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b;
System.out.println("Addition: " + add.calculate(5, 3));

Calculator subtract = (a, b) -> a - b;
System.out.println("Subtraction: " + subtract.calculate(8, 4));

Calculator multiply = (a, b) -> a * b;
System.out.println("Multiplication: " + multiply.calculate(6, 2));
}
}

In that example, we define a functional interface called Calculator with a single abstract method calculate() that takes two integers as parameters and returns an integer result.

The @FunctionalInterface annotation is used to indicate that the interface is intended to be a functional interface. (While not mandatory) It helps prevent the accidental addition of more than one abstract method in the interface.

Within the main() method, we create instances of the Calculator functional interface using lambda expressions. Each instance represents a specific calculation operation such as addition, subtraction, and multiplication.

We then invoke the calculate() method on each instance and print the corresponding result.

Functional interfaces are essential in Java 8 because they provide the foundation for lambda expressions and enable the use of functional programming techniques. They allow us to treat functions as first-class citizens, facilitating the development of more concise and expressive code.

Date and Time API:

Prior to Java 8, working with dates and times in Java was often considered cumbersome. Java 8 introduced the new Date and Time API (java.time package), which provides a more comprehensive and intuitive way to handle date and time calculations, formatting, and parsing.

In Java 8, a new Date and Time API was introduced to address the limitations and issues of the existing java.util.Date and java.util.Calendar classes. The new Date and Time API, located in the java.time package, provides improved functionality, immutability, thread safety, and better support for date and time manipulation.

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DateTimeExample {
public static void main(String[] args) {
// Current date
LocalDate currentDate = LocalDate.now();
System.out.println("Current Date: " + currentDate);

// Current time
LocalTime currentTime = LocalTime.now();
System.out.println("Current Time: " + currentTime);

// Current date and time
LocalDateTime currentDateTime = LocalDateTime.now();
System.out.println("Current Date and Time: " + currentDateTime);

// Formatting and parsing dates
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
String formattedDate = currentDate.format(formatter);
System.out.println("Formatted Date: " + formattedDate);

LocalDate parsedDate = LocalDate.parse("30-09-2023", formatter);
System.out.println("Parsed Date: " + parsedDate);
}
}

In the example above, we demonstrate various aspects of the Date and Time API:

  1. We use the LocalDate.now() method to obtain the current date and store it in the currentDate variable.
  2. Similarly, we use the LocalTime.now() method to get the current time and store it in the currentTime variable.
  3. We combine both the current date and time using LocalDateTime.now() and store it in the currentDateTime variable.
  4. We create a DateTimeFormatter object using the pattern "dd-MM-yyyy" to format dates in the desired format.
  5. We format the currentDate using the format and storing the formatted date in the formattedDate variable.
  6. We parse the string “30–09–2023” (Last day of Java 8 with Selenium 😀) into a LocalDate object using the same format and store it in the parsedDate variable.

By utilizing the new Date and Time API in Java 8, we have access to a wide range of classes and methods for performing operations on dates, times, durations, and intervals. The API provides better precision, more flexibility, and improved readability compared to the older date and time classes.

Optional Class:

In Java 8, the Optional class was introduced to provide a more expressive way of dealing with potentially null values. It is designed to address the issues associated with NullPointerExceptions and to encourage better handling of null values in code.

The Optional class represents an object that may or may not contain a non-null value. It provides methods to perform operations on the underlying value without the need for explicit null checks. The main purpose of Optional is to make it clear that a value may be absent and to provide convenient methods to handle both cases.

import java.util.Optional;

public class OptionalExample {
public static void main(String[] args) {
String name = "John Doe";
Optional<String> optionalName = Optional.ofNullable(name);

// Checking if a value is present
if (optionalName.isPresent()) {
System.out.println("Name is present: " + optionalName.get());
} else {
System.out.println("Name is absent");
}

// Performing an action if a value is present
optionalName.ifPresent(n -> System.out.println("Length of name: " + n.length()));

// Getting a default value if a value is absent
String defaultValue = optionalName.orElse("Unknown");
System.out.println("Default value: " + defaultValue);

// Throwing an exception if a value is absent
try {
String value = optionalName.orElseThrow(() -> new RuntimeException("Value is absent"));
System.out.println("Value: " + value);
} catch (RuntimeException e) {
System.out.println("Exception: " + e.getMessage());
}
}
}

We create an Optional object called optionalName using the Optional.ofNullable() method, which wraps the name variable. If name is not null, the optionalName will contain its value; otherwise, it will be empty.

By using the Optional class, we can write more concise and readable code that handles null values in a more explicit and controlled manner. It promotes better programming practices by encouraging developers to handle both the presence and absence of values, reducing the chances of encountering null pointer exceptions.

Parallel Operations:

In Java 8, the Stream API introduced parallel operations, which allows for the efficient processing of large data sets by leveraging multi-core processors. Parallel operations enable the stream to be processed in parallel, dividing the workload among multiple threads and potentially speeding up the execution.

To perform parallel operations in Java 8, you can simply invoke the parallel() method on a stream to indicate that the operations should be executed in parallel. Let's do an example to illustrate parallel operations using the Stream API:

import java.util.Arrays;

public class ParallelOperationsExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Sequential stream processing
int sumSequential = Arrays.stream(numbers)
.sum();
System.out.println("Sequential sum: " + sumSequential);

// Parallel stream processing
int sumParallel = Arrays.stream(numbers)
.parallel()
.sum();
System.out.println("Parallel sum: " + sumParallel);
}
}

we have an array of numbers. We first perform the sum operation sequentially by using Arrays.stream() and invoking the sum() method. This processes the elements in a sequential manner, resulting in the sum of all the numbers.

Next, we modify the stream to operate in parallel by calling the parallel() method before invoking sum(). This enables the stream to be processed concurrently by multiple threads.

Sequential sum: 55
Parallel sum: 55

The output shows that both sequential and parallel processing result in the same sum of 55. However, the parallel sum may be computed faster due to the parallel execution of the stream.

Nashorn JavaScript Engine:

Java 8 included a new JavaScript engine called Nashorn, which allowed developers to embed and execute JavaScript code within Java applications. Nashorn provided improved performance compared to the older Rhino JavaScript engine.

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class NashornExample {
public static void main(String[] args) {
// Create a script engine manager
ScriptEngineManager manager = new ScriptEngineManager();

// Get the Nashorn script engine
ScriptEngine engine = manager.getEngineByName("nashorn");

try {
// Evaluate JavaScript code
engine.eval("var message = 'Bye bye Java 8!'; print(message);");
} catch (ScriptException e) {
e.printStackTrace();
}
}
}

The JavaScript code var message = 'Bye bye Java 8!'; print(message); assigns a string value to the variable message and then prints it using the print() function.

When you run this code, it will output the following in the console:

Bye bye Java 8!

Conclusion

We wouldn’t be exaggerating if we consider it revolutionary that a programming language makes such big and important changes in a version. Maybe that’s why Java 8 has been the favorite version used by millions of people for many years without any problems. However, with Java 8, we have come to the end of our journey. At least for Selenium users.

So the question is: Which Java version should I go with?

Selenium’s official announcement continues with the following sentences:

“September 30, 2023, is also the end of active support for Java 11. However, we want to take a cautious and conservative path forward, and not force our users to make the big jump from Java 8 to Java 17, as we understand the community might need longer to move to that version. We will revisit this topic in the future to announce the plan to support Java 17 as a minimum version.”

After Java 8, it seems that the most logical version, for now, will be Java 17. I hope Java 17 is as enjoyable as Java 8. 😀

Resources

[1] https://www.oracle.com/a/ocom/docs/dc/ww-brief-history-java-infographic.pdf

[2] Java: The Complete Reference” by Herbert Schild

[3] Core Java Volume I — Fundamentals” by Cay S. Horstmann and Gary Cornell

[4] “Java Enterprise in a Nutshell” by Jim Farley

[5] “Open Source Development with CVS” by Karl Fogel

[6] Java in a Nutshell” by Benjamin J. Evans and David Flanagan

[7] https://www.oracle.com/news/announcement/oracle-releases-java-18-2022-03-22/

[8] https://www.oracle.com/news/announcement/oracle-releases-java-19-2022-09-20/

[9] https://www.infoworld.com/article/3676699/jdk-20-the-new-features-in-java-20.html

--

--

Dr. Çağrı ATASEVEN

Cagri Ataseven is ISTQB Certified Software Test Automation Engineer, Also, he has a Ph.D. in mathematics.