Unleashing the Power of Modern Java
Features and Capabilities for Everyday Programming
Since its inception, Java has come a long way, and its versatility has resulted in widespread adoption across diverse domains, including enterprise, mobile app development, embedded systems, and more. Java’s popularity can be attributed to its robustness, platform-independence, and support for cross-platform frameworks, which makes it a reliable choice for software development across different domains and platforms.
Photo by John Schnobrich on Unsplash
From functional programming constructs like lambdas and streams, to improved security and performance, each new release of Java continues to push the boundaries of what’s possible in modern programming.
Over the past few years, the JDK team has added a ton of features to Java, including new language constructs, improved libraries, and enhanced tools for development and debugging. Java went through a significant evolution with the release of Java 8, which brought transformational changes revolutionizing the way developers wrote Java code. latest stable version (Java 20), was released in march 2023 with several enhancements in terms of performance and security.
These features have made Java more powerful, flexible, and efficient than ever before, enabling developers to write cleaner, more concise code and deliver better software faster. Whether you’re working on a small project or a large-scale enterprise application, the latest advancements in Java have something to offer everyone.
In this article, we’ll explore some of the top features that have been added to Java in recent years, and how they are changing the way we write code.
01. Lambda expressions
Lambda expressions were introduced in Java 8 as a way to provide concise and functional programming constructs to Java. Prior to Java 8, anonymous inner classes were often used as a convenient way to define behaviors such as Event Handling, Callbacks, Adapters etc.
Lambda expressions are essentially anonymous functions that allow you to pass behavior as data. They are often used with functional interfaces, which are interfaces with a single abstract method.
In this case, the addActionListener method of the Button class expects an argument of type ActionListener, which is a functional interface with a single abstract method actionPerformed(ActionEvent e). The data type of the lambda expression’s parameter e is therefore inferred to be ActionEvent, which is the parameter type of the actionPerformed method.
Inner classes are not deprecated in Java, but they are not as commonly used as they once were because lambda expressions provide a more concise syntax and better performance in many cases.
Photo by Artturi Jalli on Unsplash
02.Functional interfaces
In a functional programming paradigm, functions are treated as first-class citizens, meaning they can be passed as arguments to other functions, returned as values, and stored in variables.
This style of programming is focused on the behavior of functions, rather than the state of objects. To support functional programming constructs, Java introduced functional interfaces in Java 8.
A functional interface is an interface with a single abstract method, which is used to represent a function. This makes it possible to use lambda expressions to pass behavior around, and treat functions as first-class citizens.
This allows Java to support some core features of functional programming not limited to,
01.Higher-order functions: Functions that take other functions as arguments or return functions as results.
02.Immutable data: Data that cannot be modified after it is created. This allows for safe sharing of data between functions.
03.Functional composition: The ability to combine multiple functions into a single function.
In this example, we create a Runnable instance using a lambda expression that defines the behavior of the run() method. We pass this lambda expression directly to the Thread constructor, without the need to create a separate class or method. When we start the thread, the lambda expression is executed in a separate thread, printing the message “Hello Runnable, Running now!” to the console.
Here are some of the functional interfaces introduced as a part of the java.util.function package.
01.Consumer: A functional interface with a single method accept(T t), which takes an argument of type T and returns no value. It is used to define a function that consumes a value and performs some operation on it.
02.Function: A functional interface with a single method apply(T t), which takes an argument of type T and returns a value of type R. It is used to define a function that transforms a value from one type to another.
03.Predicate: A functional interface with a single method test(T t), which takes an argument of type T and returns a boolean value. It is used to define a function that tests a value and returns a boolean result.
04.Supplier: A functional interface with a single method get(), which takes no arguments and returns a value. It is used to define a function that produces a value.
03.Optional <T>
Generally ‘null’ refers to the absence of a value. It is a special value that represents a variable or object reference that doesn’t refer to any object. Here are few reasons null is considered to bad in java.
- Trying to access a null object reference leads to a common runtime error called NullPointerException, which can be difficult to diagnose and fix.
- We often have to write extra code to check for null references, which can lead to cluttered and less readable code, and it can also add overhead to the application’s performance.
- Null can be ambiguous; if a method returns a null value, the caller can be confused as to whether it was intentional or if the method failed to execute correctly.
- Null can break contracts; null can break contracts when it violates the assumptions of interfaces or other parts of the program that rely on non-null values.
Optional <T> is introduced in java 8, as a clean and concise way of dealing with values that may be present or not, without relying on null references. An Optional instance can either contain a non-null value, or it can be empty, indicating the absence of a value.
A monad is a programming concept that provides a way to chain operations together in a functional way, while also handling errors and non-existent values. Optionals provides a way to handle non-existent values in a functional way, and it can be used to chain operations together through its map, flatMap, and filter methods.
map If a value is present, returns an Optional describing (as if by ofNullable) the result of applying the given mapping function to the value, otherwise returns an empty Optional.If the mapping function returns a null result then this method returns an empty Optional.
filter If a value is present, and the value matches the given predicate, returns an Optional describing the value, otherwise returns an empty Optional.
flatMap If a value is present, returns the result of applying the given
mapping function to the value (no extra wrapping with an another optional), otherwise returns an empty Optional.
Using Optional not only helps prevent NullPointerExceptions, but also makes code more readable and expressive, reduces boilerplate code, and allow developers to focus on business logic.
04.Stream <T>
This is a new feature introduced in Java 8 that enable processing collections of objects in a functional way. A Java Stream is a sequence of elements that can be any type of object, including primitive types, arrays, or even user-defined objects. It supports various operations to perform aggregate computations on those elements, such as filtering, mapping, reducing, and sorting.
Using Java Streams, you can write more concise and expressive code to perform complex data manipulations on collections. Java Streams make use of functional programming concepts, such as lambda expressions and method references, to enable a more functional style of programming. This allows you to write more expressive code by using functional interfaces and chaining together various operations to perform complex transformations on data.
Few key aspects that make streams preferred over traditional for-loops or iterators.
Abstraction: Streams provide a higher level of abstraction than for-loops, which can make it easier to reason about code and write correct code. Streams provide a set of composable operations that can be combined in different ways to achieve the desired result, without having to worry about low-level details like loop indices and array access.
Lazy Evaluation: Operations on a stream are only executed when they are needed. This allows you to process large datasets more efficiently as you may don’t need to load all the data into memory at once.
Streams can often express the same logic as a for-loop with less code, making the code more concise and easier to read. This is because streams are designed to operate on collections in a functional style, which emphasizes declarative expressions of what is being computed rather than imperative instructions for how to compute it.
Parallelism: Streams provide an easy way to parallelize operations on collections. By using parallel streams, you can take advantage of multi-core processors and speed up your code. Doing this can be much harder with a for-loop, which typically requires explicit management of threads and synchronization.
Stream operations and Usage
filter() is used to keep only the even numbers in the list, and then use the map() method to square each even number. We then collect the resulting elements into a new list using the collect() method.
Because of lazy evaluation, the skip() method is only called when it is needed to produce the output, which means that only the last two elements of the original stream are processed.
Because of parallel processing, the filtering operation can be performed on multiple threads, which can improve the performance of the code when dealing with large datasets.
We don’t need to specify the types of the stream, lambda expressions, or method references explicitly, because they can be inferred from the context based on the map declaration.
Type of the entrySet() method is Set
Inthe next post, we will explore some exciting Java language features introduced in recent versions, including those in the preview stage that may not have been widely adopted in the community yet. Stay tuned!
Author: Aravinda Liyanage