Understanding Java 8’s features is a crucial skill because, by the beginning of 2021, more than 60% of professional developers will still be using it. In 2014, Java 8 was introduced, providing a huge number of new features. Our Java Training in Chennai makes you expert in Lambda Expressions in Java along with Functional Programming in Java, and Java Stream API.
Features that let Java programmers write in a functional programming language were included in these modifications. Lambda expressions were one of the largest additions.
Similar to methods, lambdas can be used outside of classes and do not require a name. As a result, they enable fully functioning programs and open the door for Java to provide more functional support. We’ll go through how to use lambda expressions with interfaces and guide you through the process today.
What are Lambda Expressions?
Since lambda expressions are anonymous functions, they don’t have names or identifiers. They are a parameter that can be supplied to another function. They include a parameter with an expression that refers to that parameter, along with a functional interface.
A basic lambda expression has the following syntax:
parameter → expression
The abstract method, which is a named but empty method within the paired functional interface, uses the expression as its code body.
Lambda expressions in Java exist outside of the scope of any object, unlike the majority of functions. They can therefore be handed around and called from anywhere in the application. Lambda expressions, in their most basic form, let functions behave like any other piece of data.
Use Cases of Java Lambda Expressions
To obtain the functionality of an anonymous class without the clumsy implementation, lambda expressions are employed. They work well for repeating straightforward actions that may be applied to several parts of the program, like adding two values without altering the input data.
Because of these characteristics, lambda is particularly helpful for Java’s functional programming paradigms. Before Java 8, Java had a difficult time finding tools that adhered to all the functional programming concepts.
There are 5 main principles of functional programming in Java:
Pure functions: Just the actions required to determine the output and operate independently from the state outside the function.
Immutability: Data is referred to rather than changed. Complex conditional behavior should not be used in functions. In general, a function should always return the same value, regardless of how many times it is called.
First-class functions: Functions are given first-class treatment, which means they are handled similarly to other values. Functions can be used to fill arrays, be passed as parameters, etc.
Higher-order functions: They can either take one or more functions as inputs or can return another function. They are necessary for functional programming to produce complicated behaviors.
Function Composition: To generate complicated functions, many small functions can be connected in a variety of ways. Whereas complex functions finish a full task, simple functions only finish a single step that may be shared by numerous tasks.
The Java principles of first-class functions, immutability, and pure functions are all made possible via lambda expressions.
Because they don’t rely on a specific class scope, lambda functions are pure. Since they only make a reference to the provided parameter and do not alter its value in any way to arrive at its outcome, they are immutable. Lastly, the fact that they can be handed to other methods anonymously makes them first-class functions.
Due to their class freedom, lambda expressions are also utilized in non-functional programming as callback functions and event listeners.
Writing a Lambda Expressions in Java
A single parameter is supplied to a lambda expression’s basic form, as we previously saw.
parameter → expression
Several parameters may also be included in a single lambda expression:
(parameter1, parameter2) → expression
A reference to the parameter is found in the expression segment, often known as the lambda body. The result of running the expression with the provided parameters is the value of the lambda expression.
For example:
import java.util.ArrayList;
public class main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<Integer>();
numbers.add(5);
numbers.add(9);
numbers.add(8);
numbers.add(1);
numbers.forEach( (n) -> { System.out.println(n); } );
}
}
The expression System.out.println accepts the parameter n as an argument (n). The print statement’s parameter n’s value is used to then execute the expression. For each number in the ArrayList, this is repeated while providing each element of the list as n to the lambda expression. The result of this expression is a printed list of the ArrayList’s elements, which looks like this: 5 9 8 1.
Body of Lambda Function
If enclosed in curly brackets, expressions on many lines may be included in the lambda function body.
For example:
(old state, newState) -> {
System.out.println(“Old state: ” + oldState);
System.out.println(“New state: ” + newState);
}
This enables the execution of more intricate expressions that contain code blocks as opposed to a single statement. By including a return statement inside the function body, lambda functions can also be made to return.
public static Addition getAddition() {
return (a, b) -> a + b;
}
Even Lambda has a return statement of its own:
(a, b) -> a + b;
The compiler takes the value of our return as a+b. The result from this syntax will be the same as that from the earlier example while being cleaner.
No matter how lengthy or intricate the expression becomes, keep in mind that lambda expressions need to output a consistent value right away. This means that an expression cannot wait for user input and cannot contain any conditional statements like if or while. No matter how many times the expression is executed, all of its code must have an immutable output.
Get fundamental knowledge of OOPs concepts in Java and make a better foundation by reading through our article.
Lambdas as Objects
Lambdas can be passed as parameters to other functions. Assume that we want to develop a greeting program that is flexible enough to include more greeting features in additional languages.
@FunctionalInterface
public interface Greeting {
void greet();
}
Here, the function greet(); is instantly called after the expression itself has been provided. From here, we can add more great functions for various languages that will replace the default ones and only print the right greeting.
Interfaces in Java
Java classes and interfaces are comparable. These are blueprints with procedures and variables. Interfaces, on the other hand, only contain abstract methods with signatures rather than actual implementations.
An implementing class must declare a set of characteristics or methods to use an interface. The interface specifies the features it must have but does not explain how to put them into practice.
As an illustration, a character in a video game might have an interface that has methods for all the things that the character must be able to accomplish. The interface specifies that all characters must have a move() method, but it is up to each character’s class to specify the direction and mode of movement (flight, running, sliding, etc.).
An interface’s syntax is as follows:
interface <interface_name> {
// declare constant fields
// declare methods that abstract
// by default.
}
Java classes can inherit from numerous classes thanks to interfaces because the one-class inheritance restriction does not apply to them. Since the interface by default lacks scope and values, it also aids in achieving complete abstraction.
An instance of these interfaces is expressed using lambda expressions. To use these interfaces before Java 8, we had to develop an inner anonymous class.
// functional interface before Java8
class Test
{
public static void main(String args[])
{
//Create an anonymous inner-class object
new Thread(new Runnable()
{
@Override
public void run() // anonymous class
{
System.out.println(“New thread created”);
}
}).start();
}
}
Functional Interfaces
Only functional interfaces, which have a single abstract method, can be implemented with lambda expressions. In the functional interface, the lambda expression serves as the abstract method’s body.
The compiler would be unable to determine which abstract method, if any, should utilize the lambda expression as its body if the interface contained multiple abstract methods. Predicates or Comparators are frequent illustrations of built-in functional interfaces.
The @FunctionalInterface annotation, which is optional, should always be added to the top of any functional interface.
The annotation is interpreted by Java as a limitation that the indicated interface can only have one abstract method. The compiler will issue a warning if there are many methods.
By using the annotation, you can make sure that any lambda expressions that invoke this interface won’t behave unexpectedly.
@FunctionalInterface
interface Square
{
int calculate(int x);
}
Want to know which is the best programming language for beginners? Check out our recent blog.
Default Methods in Interfaces
There is no restriction on default or static methods in functional interfaces, although there is a limit on abstract methods. Our interfaces can be fine-tuned to share various behaviors with descending classes using default or static methods.
Default method bodies may be seen inside interfaces. The most important component for extending a type’s functionality without tearing apart the implementing classes is default methods in interfaces.
Before Java 8, every class that implemented an interface would crash if a new method was added. To remedy that, each implementing class’ specific implementation of that method would need to be provided.
But occasionally a method only has one implementation, thus it is not necessary to include that implementation in every class. In such a situation, we can designate that method in the interface as a default and provide its implementation there.
public interface Vehicle {
void cleanVehicle();
default void startVehicle() {
System.out.println(“Vehicle is starting”);
}
}
StartVehicle() is the default approach in this case, whereas cleanVehicle() is abstract. Regardless of the class that implements it, startVehicle() always prints the same sentence. To avoid writing duplicate code, we may just use the default method since the behavior is not dependent on the class.
The Vehicle interface, which counts as a functional interface that can be used with lambda expressions, nevertheless only contains 1 abstract method, which is crucial.
Static Methods in Interfaces
Interface static methods are comparable to default methods but cannot be modified. When you want a method’s implementation to be fixed across all implementing classes, static methods are fantastic.
//functional interface
public interface Vehicle {
static void cleanVehicle(){
System.out.println(“I am cleaning vehicle”);
}
void repairVehicle();
}
To create the implementation specified in our interface, we can call cleanVehicle() in the Car class. The cleanVehicle() method was marked static, therefore if we attempt to @Override it, an error notice would appear.
The only abstract method we have is repairVehicle(), thus we can still use this interface in lambda expressions.
Java Stream API
For processing collections of items, Java Stream API in Java 8 is used. A stream is a group of items that can be used for a number of actions and pipelined to produce the desired result.
There are numerous approaches to building a stream instance from various sources. The ability to produce many instances from a single source is made possible by the fact that once created, the instance won’t change its source.
Empty Stream
If we create an empty stream, we should use the empty() method:
Stream<String> streamEmpty = Stream.empty();
To prevent streams without an element from returning null, we frequently utilize the empty() method when creating streams:
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}
Stream of Collection
Moreover, we may make a stream from any Collection (Collection, List, Set):
Collection<String> collection = Arrays.asList(“a”, “b”, “c”);
Stream<String> streamOfCollection = collection.stream();
Stream of Array
A stream may also originate from an array:
Stream<String> streamOfArray = Stream.of(“a”, “b”, “c”);
A stream can also be made from an array that already exists or from a portion of an array:
String[] arr = new String[] {“a”, “b”, “c”};
Stream<String> streamOfArrayFull = Arrays.stream(arr);
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3);
Stream.builder()
On the right part of the statement when the builder is used, the desired type must also be given; otherwise, the build() method will generate an instance of the StreamObject>:
Stream<String> streamBuilder =
Stream.<String> builder().add(“a”).add(“b”).add(“c”).build();
Stream.generate()
To create elements, the generate() function accepts a SupplierT>. Developers should specify the desired size because the output stream is endless; otherwise, the generate() method will keep running until it runs out of memory:
Stream<String> streamGenerated =
Stream.generate(() -> “element”).limit(10);
The code above generates ten strings with the value “element” in a sequence.
Stream.iterate()
Using the iterate() method is another option for producing an infinite stream:
Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20);
The first parameter of the iterate() method is the first element of the stream that results. The supplied function is applied to the preceding element before producing each subsequent element. The second element in the aforementioned case will be 42.
Stream Primitives
Java 8 gives users the option to build streams using the int, long, and double primitive types. Three new special interfaces were made: IntStream, LongStream, and DoubleStream because StreamT is a generic interface and there is no way to utilize primitives as a type parameter with generics. By eliminating superfluous auto-boxing when using the new interfaces, productivity can be increased:
IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);
From the first parameter to the second parameter, an ordered stream is produced using the range(int startInclusive, int endExclusive) method. The value of succeeding components is increased with a step size of 1. The result is only an upper bound of the sequence; it excludes the final component.
The only difference between this method and rangeClosed(int startInclusive, int endInclusive) is that the second element is included. These two techniques can be used to create any of the three different streams of primitives.
From Java 8, there are numerous methods for creating streams of primitives available in the Random class. As an illustration, the code that follows produces a DoubleStream with three elements:
Random random = new Random();
DoubleStream doubleStream = random.doubles(3);
Want to know the current job market for Certified Java Professionals? Read our blog for comprehensive knowledge.
Stream of Stream
With the help of the chars() function of the String class, we can also use a String as a source for generating a stream. We use IntStream to represent a stream of characters instead because JDK lacks an interface for CharStream.
IntStream streamOfChars = “abc”.chars();
The example that follows divides a String into substrings by a given RegEx:
Stream<String> streamOfString =
Pattern.compile(“, “).splitAsStream(“a, b, c”);
Stream of File
Moreover, we can create a StreamString> of a text file using the lines() method of the Java NIO class Files. Every text line becomes a component of the stream:
Path path = Paths.get(“C:file.txt”);
Stream<String> streamOfStrings = Files.lines(path);
Stream<String> streamWithCharset =
Files.lines(path, Charset.forName(“UTF-8”));
The lines() method’s arguments can include the Charset.
Referencing a Stream
If only preliminary operations are used, we can instantiate a stream and have a reference to it. A stream becomes unreachable when a terminal operation is carried out.
We will temporarily disregard the fact that chaining the order of the operations is the best practice to illustrate this. The following code is legitimate technically, despite its needless verbosity:
Stream<String> stream =
Stream.of(“a”, “b”, “c”).filter(element -> element.contains(“b”));
Optional<String> anyElement = stream.findAny();
The IllegalStateException will, however, be thrown if an attempt is made to reuse the same reference after calling the terminal operation.
Optional<String> firstElement = stream.findFirst();
Since the IllegalStateException is a RuntimeException, a compiler won’t flag an issue. So, it’s crucial to keep in mind that Java 8 streams cannot be recycled.
This kind of conduct makes sense. We didn’t develop streams to store items; rather, we built them to apply a finite sequence of functional operations to the source of elements.
Hence, the following changes need to be performed to make the prior code function properly:
List<String> elements =
Stream.of(“a”, “b”, “c”).filter(element -> element.contains(“b”))
.collect(Collectors.toList());
Optional<String> anyElement = elements.stream().findAny();
Optional<String> firstElement = elements.stream().findFirst();
Wrapping Up
A short piece of code that receives input and outputs a value is known as a lambda expression. Lambda expressions can be used directly in a method’s body and are similar to methods in that they don’t need a name.
A return statement may contain a lambda expression. When a lambda expression is used in a return statement, the method’s return type must be a functional interface.
The Stream API is a potent, yet easy-to-use set of utilities for handling the components in order. When properly used, it enables us to drastically minimize the amount of boilerplate code, produces more understandable programs, and increases the productivity of an app.
Get our Java Training in Chennai at SLA Institute to set yourself apart from the competition and show that you are an expert in Java development.