Exploring Java 8: A Complete Feature Guide
Table of Contents
Introduction
- Overview of Java 8 Features
Lambda Expressions
What is a Lambda Expression?
Example 1: Traditional Way vs. Lambda Expression
Example 2: Filtering a List
Stream API
What is a Stream? (java.util.stream API)
Stream API Architecture Diagram
Internal Working of Stream API with Examples
Creating Streams
From Collections
From Arrays
Using Stream.of()
Stream Operations
Intermediate Operations
filter()
map()
distinct()
sorted()
Terminal Operations
forEach()
collect()
reduce()
count()
How Stream API Works
Stream Creation
Building the Pipeline
Triggering Execution
Processing and Collecting Results
Functional Interfaces
What is a Functional Interface?
Key Concepts of Functional Interfaces
Single Abstract Method (SAM)
@FunctionalInterface Annotation
Default and Static Methods
Predefined Functional Interfaces in Java
Predicate
Consumer
Function
Supplier
UnaryOperator
BinaryOperator
BiPredicate
BiConsumer
BiFunction
Primitive Specialisations
IntPredicate
IntConsumer
IntSupplier
IntFunction
ToIntFunction
IntUnaryOperator
IntBinaryOperator
Stream Methods with Functional Interfaces
forEach
filter
map
reduce
collect
anyMatch
allMatch
noneMatch
findFirst
findAny
sorted
peek
flatMap
distinct
limit
skip
reduce
toArray
count
max
min
Default Methods
What are Default Methods?
Why Use Default Methods?
Maintain Backward Compatibility
Support Multiple Inheritance
Reduce Boilerplate Code
Syntax of Default Methods
Example: Basic Usage of Default Methods
Defining an Interface with Default Method
Implementing the Interface
Using the Default Method
Overriding Default Methods
Testing the Override
Multiple Interfaces with Default Methods
Resolving Conflicts
Practical Use Case: Adding Methods to Interfaces
Advanced Features of Default Methods
Static Methods in Interfaces
Default Methods with Lambda Expressions
Comparing Java 7 and Java 8 Interfaces
Nashorn JavaScript Engine
Key Features of Nashorn
Using Nashorn
Executing JavaScript from Java
Invoking Java Methods from JavaScript
Accessing JavaScript Variables from Java
Interoperability between Java and JavaScript
Working with JSON
Using JavaScript Libraries
JavaScript Shell (jjs)
Nashorn Limitations
CompletableFuture
Why CompletableFuture?
Benefits of CompletableFuture
How to Create a CompletableFuture
Basic Creation
Using Factory Methods
Asynchronous Computations
runAsync()
supplyAsync()
Chaining Tasks
thenApply()
thenAccept()
thenRun()
thenCombine()
Exception Handling
exceptionally()
handle()
whenComplete()
Combining Multiple Futures
allOf()
anyOf()
Introduction : Overview of Java 8 Features -
Java 8 is a significant update that enhances Java programming with powerful new features. Here's a brief overview of what we'll explore step by step:
Lambda Expressions: Write shorter and more readable code for tasks like filtering and sorting.
Stream API: Efficiently process collections of data with operations like filtering, mapping, and reducing.
Functional Interfaces: Simplify function passing by using predefined or custom functional interfaces.
Method References: Simplify code by referring to methods directly.
Optional Class: Handle missing values more safely and concisely.
New Date and Time API: Work with dates and times using a more intuitive and flexible API.
Default Methods: Add new methods to interfaces without breaking existing implementations.
Nashorn JavaScript Engine: Execute JavaScript code within Java applications more efficiently.
CompletableFuture: Write asynchronous code more easily with improved handling of future results.
These features help you write cleaner and more efficient code, making Java 8 a significant improvement over previous versions. We'll cover each feature in detail, learning how they can make Java development more efficient and expressive.
1. Lambda Expression?
A lambda expression is a quick way to create methods (functions) without needing to write a full method implementation. Instead of creating a whole class with a method, you can use lambda expressions to define these methods in a shorter, more readable format.
In simple terms:
Lambda expressions let you write methods in a more compact way.
They are especially useful when working with functional interfaces, which have only one method to implement.
Example 1:
Instead of writing a lot of code to define a simple function, you can use a lambda expression to do it in one line.
Traditional Way:
new Thread(new Runnable() { @Override public void run() { System.out.println("Hello from Runnable"); } }).start();
Using Lambda Expression:
new Thread(() -> System.out.println("Hello from Runnable")).start();
Example 2: Filtering a List
If you want to filter a list of numbers to keep only the even numbers:
Using Anonymous Class:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
evenNumbers.add(number);
}
}
Using Lambda Expression with Streams:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(number -> number % 2 == 0)
.collect(Collectors.toList());
What is a Stream? (java.util.stream API**)**
A stream is a collection of elements that can be processed parallel or sequentially. Think of a stream as a pipeline that processes data. Streams allow you to perform complex data manipulation tasks like filtering, mapping, and reducing in a more readable and concise way compared to traditional loops.
Stream API Architecture Diagram
Here is a simplified architecture diagram for the Stream API:
Internal Working of Stream API with Examples
Creating Streams
From Collections: You can create a stream from collections like
List
,Set
, etc.List<String> names = Arrays.asList("Arjun", "Balaram", "chaitanya"); Stream<String> stream = names.stream();
From Arrays: You can create a stream from arrays.
String[] namesArray = {"Arjun", "Balaram", "chaitanya"}; Stream<String> stream = Arrays.stream(namesArray);
Using Stream.of(): You can create a stream from a set of values.
Stream<String> stream = Stream.of("Arjun", "Balaram", "chaitanya");
Stream Operations
Intermediate Operations: These operations transform the stream into another stream. They are lazy, meaning they are not executed until a terminal operation is invoked.
filter()
: Filters elements based on a condition.Stream<String> filteredStream = stream.filter(name -> name.startsWith("A"));
map()
: Transforms each element into another form.Stream<Integer> lengthStream = stream.map(String::length);
distinct()
: Removes duplicate elements.Stream<String> distinctStream = stream.distinct();
sorted()
: Sorts elements.Stream<String> sortedStream = stream.sorted();
Terminal Operations: These operations produce a result or a side effect. They trigger the processing of the stream.
forEach()
: Performs an action for each element.stream.forEach(name -> System.out.println(name));
collect()
: Collects the elements into a collection likeList
,Set
, orMap
.List<String> list = stream.collect(Collectors.toList());
reduce()
: Combines elements to produce a single result.Optional<String> concatenated = stream.reduce((s1, s2) -> s1 + s2);
count()
: Counts the number of elements.long count = stream.count();
How Stream API Works
Stream Creation:
- You start by creating a stream from a source. This can be done using methods like
stream()
on collections,Arrays.stream
()
, orStream.of()
.
- You start by creating a stream from a source. This can be done using methods like
Building the Pipeline:
- Apply intermediate operations to the stream. These operations are not executed immediately but are recorded to form a processing pipeline.
Triggering Execution:
- When a terminal operation is invoked, the stream pipeline is processed. The elements are passed through each intermediate operation in the pipeline.
Processing and Collecting Results:
- The terminal operation processes the elements according to the pipeline’s configuration and produces a result, such as a list, a single value, or a side effect.
What is a Functional Interface?
A functional interface in Java is an interface with only one abstract method. This makes it easy to represent instances of these interfaces using lambda expressions, method references, or constructor references. These features enable functional programming in Java, making the code more concise and readable.
Key Concepts of Functional Interfaces
Single Abstract Method (SAM):
A functional interface must have exactly one abstract method. This is the core method that defines the interface's behavior.
For example,
Runnable
is a functional interface with the single abstract methodrun()
.
@FunctionalInterface Annotation:
- This annotation is optional but recommended. It tells the compiler that the interface is intended to be a functional interface. If the interface does not meet the requirements (e.g., it has more than one abstract method), the compiler will generate an error.
@FunctionalInterface
public interface MyFunctionalInterface {
void execute(String message);
}
Default and Static Methods:
- Functional interfaces can also have default and static methods. These methods do not count towards the single abstract method requirement.
@FunctionalInterface
public interface MyFunctionalInterface {
void execute(String message);
default void print(String message) {
System.out.println("Printing: " + message);
}
static void display(String message) {
System.out.println("Displaying: " + message);
}
}
Predefined Functional Interfaces in Java
Java 8 provides many built-in functional interfaces in the java.util.function
package. Some commonly used ones include:
1. Predicate<T>
Explanation: Represents a boolean-valued function that tests an input. Commonly used for filtering or matching conditions.
Abstract Method:
boolean test(T t)
Default Methods:
and
,or
,negate
import java.util.function.Predicate;
public class PredicateExample {
public static void main(String[] args) {
Predicate<String> isLongerThanFive = s -> s.length() > 5;
Predicate<String> startsWithH = s -> s.startsWith("H");
System.out.println(isLongerThanFive.test("Hello")); // false
System.out.println(isLongerThanFive.test("Hello, World!")); // true
// Using default methods
Predicate<String> combined = isLongerThanFive.and(startsWithH);
System.out.println(combined.test("Hello, World!")); // true
}
}
2. Consumer<T>
Explanation: Represents an operation that accepts a single input argument and returns no result. Often used for performing actions like printing or logging.
Abstract Method:
void accept(T t)
Default Methods:
andThen
import java.util.function.Consumer;
public class ConsumerExample {
public static void main(String[] args) {
Consumer<String> print = s -> System.out.println(s);
Consumer<String> printUpperCase = s -> System.out.println(s.toUpperCase());
print.accept("Hello, World!"); // Hello, World!
// Using default methods
Consumer<String> combined = print.andThen(printUpperCase);
combined.accept("Hello, World!"); // Hello, World! \n HELLO, WORLD!
}
}
3. Function<T, R>
Explanation: Represents a function that accepts one argument and produces a result. Useful for mapping or transforming data.
Abstract Method:
R apply(T t)
Default Methods:
andThen
,compose
import java.util.function.Function;
public class FunctionExample {
public static void main(String[] args) {
Function<String, Integer> length = s -> s.length();
Function<Integer, String> toString = i -> "Length: " + i;
System.out.println(length.apply("Hello")); // 5
// Using default methods
Function<String, String> combined = length.andThen(toString);
System.out.println(combined.apply("Hello")); // Length: 5
}
}
4. Supplier<T>
Explanation: Represents a supplier of results that provides values without taking any input. Commonly used for lazy generation of values.
- Abstract Method:
T get()
import java.util.function.Supplier;
public class SupplierExample {
public static void main(String[] args) {
Supplier<String> helloSupplier = () -> "Hello, World!";
System.out.println(helloSupplier.get()); // Hello, World!
}
}
5. UnaryOperator<T>
Explanation: Represents an operation on a single operand that produces a result of the same type. Useful for operations like incrementing or squaring.
- Abstract Method:
T apply(T t)
import java.util.function.UnaryOperator;
public class UnaryOperatorExample {
public static void main(String[] args) {
UnaryOperator<Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
}
}
6. BinaryOperator<T>
Explanation: Represents an operation upon two operands of the same type, producing a result of the same type. Commonly used for combining values, like summing or multiplying.
- Abstract Method:
T apply(T t1, T t2)
import java.util.function.BinaryOperator;
public class BinaryOperatorExample {
public static void main(String[] args) {
BinaryOperator<Integer> sum = (a, b) -> a + b;
System.out.println(sum.apply(5, 3)); // 8
}
}
7. BiPredicate<T, U>
Explanation: Represents a boolean-valued function of two arguments. Useful for comparisons or matching conditions involving two values.
Abstract Method:
boolean test(T t, U u)
Default Methods:
and
,or
,negate
import java.util.function.BiPredicate;
public class BiPredicateExample {
public static void main(String[] args) {
BiPredicate<String, Integer> isLongerThan = (s, length) -> s.length() > length;
System.out.println(isLongerThan.test("Hello", 5)); // false
System.out.println(isLongerThan.test("Hello, World!", 5)); // true
// Using default methods
BiPredicate<String, Integer> startsWithH = (s, length) -> s.startsWith("H");
BiPredicate<String, Integer> combined = isLongerThan.and(startsWithH);
System.out.println(combined.test("Hello, World!", 5)); // true
}
}
8. BiConsumer<T, U>
Explanation: Represents an operation that accepts two input arguments and returns no result. Often used for operations involving two inputs, like printing paired values.
Abstract Method:
void accept(T t, U u)
Default Methods:
andThen
import java.util.function.BiConsumer;
public class BiConsumerExample {
public static void main(String[] args) {
BiConsumer<String, Integer> printStringAndLength = (s, length) -> System.out.println(s + " has length " + length);
printStringAndLength.accept("Hello", 5); // Hello has length 5
// Using default methods
BiConsumer<String, Integer> printUpperCase = (s, length) -> System.out.println(s.toUpperCase() + " has length " + length);
BiConsumer<String, Integer> combined = printStringAndLength.andThen(printUpperCase);
combined.accept("Hello", 5); // Hello has length 5 \n HELLO has length 5
}
}
9. BiFunction<T, U, R>
Explanation: Represents a function that accepts two arguments and produces a result. Useful for operations that combine or transform two inputs into a single output.
Abstract Method:
R apply(T t, U u)
Default Methods:
andThen
import java.util.function.BiFunction;
public class BiFunctionExample {
public static void main(String[] args) {
BiFunction<String, Integer, String> appendLength = (s, length) -> s + " has length " + length;
System.out.println(appendLength.apply("Hello", 5)); // Hello has length 5
// Using default methods
BiFunction<String, Integer, Integer> addLengths = (s, length) -> s.length() + length;
BiFunction<String, Integer, String> combined = appendLength.andThen(result -> "Result: " + result);
System.out.println(combined.apply("Hello", 5)); // Result: Hello has length 5
}
}
Primitive Specialisations
IntPredicate
Explanation: Represents a boolean-valued function that tests an int
value. Commonly used for filtering or matching integer conditions.
- Abstract Method:
boolean test(int value)
import java.util.function.IntPredicate;
public class IntPredicateExample {
public static void main(String[] args) {
IntPredicate isEven = x -> x % 2 == 0;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(5)); // false
}
}
IntConsumer
Explanation: Represents an operation that accepts a single int
argument and returns no result. Often used for performing actions like printing integer values.
Abstract Method:
void accept(int value)
Default Methods:
andThen
import java.util.function.IntConsumer;
public class IntConsumerExample {
public static void main(String[] args) {
IntConsumer printInt = x -> System.out.println(x);
printInt.accept(5); // 5
// Using default methods
IntConsumer printSquare = x -> System.out.println(x * x);
IntConsumer combined = printInt.andThen(printSquare);
combined.accept(5); // 5 \n 25
}
}
IntSupplier
Explanation: Represents a supplier of int
results that provides values without taking any input. Commonly used for lazy generation of integer values.
- Abstract Method:
int getAsInt()
import java.util.function.IntSupplier;
public class IntSupplierExample {
public static void main(String[] args) {
IntSupplier randomInt = () -> (int) (Math.random() * 100);
System.out.println(randomInt.getAsInt()); // Random int value
}
}
IntFunction<R>
Explanation: Represents a function that accepts an int
argument and produces a result of type R
. Useful for transforming an integer input into a different type.
- Abstract Method:
R apply(int value)
import java.util.function.IntFunction;
public class IntFunctionExample {
public static void main(String[] args) {
IntFunction<String> intToString = x -> "Number: " + x;
System.out.println(intToString.apply(5)); // Number: 5
}
}
ToIntFunction<T>
Explanation: Represents a function that accepts an argument of type T
and produces an int
result. Commonly used for extracting an integer property from an object.
- Abstract Method:
int applyAsInt(T value)
import java.util.function.ToIntFunction;
public class ToIntFunctionExample {
public static void main(String[] args) {
ToIntFunction<String> stringLength = s -> s.length();
System.out.println(stringLength.applyAsInt("Hello")); // 5
}
}
IntUnaryOperator
Explanation: Represents an operation on a single int
operand that produces an int
result. Useful for operations like incrementing or doubling an integer.
- Abstract Method:
int applyAsInt(int operand)
import java.util.function.IntUnaryOperator;
public class IntUnaryOperatorExample {
public static void main(String[] args) {
IntUnaryOperator increment = x -> x + 1;
System.out.println(increment.applyAsInt(5)); // 6
}
}
IntBinaryOperator
Explanation: Represents an operation upon two int
operands and produces an int
result. Commonly used for combining two integer values, like summing or multiplying.
- Abstract Method:
int applyAsInt(int left, int right)
import java.util.function.IntBinaryOperator;
public class IntBinaryOperatorExample {
public static void main(String[] args) {
IntBinaryOperator multiply = (a, b) -> a * b;
System.out.println(multiply.applyAsInt(5, 3)); // 15
}
}
Stream Methods with Functional Interfaces
forEach
Functional Interface Used:
Consumer<T>
Description: Performs an action for each element of the stream.
Explanation: The
forEach
method takes aConsumer<T>
, which is a functional interface that accepts a single input argument and returns no result. It iterates over each element and applies the provided action.Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.forEach(name -> System.out.println(name)); // Output: Alice, Bob, Charlie
filter
Functional Interface Used:
Predicate<T>
Description: Filters elements based on a condition.
Explanation: The
filter
method takes aPredicate<T>
, which tests each element against a condition and only includes elements that satisfy the predicate.Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); List<String> filteredNames = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList()); System.out.println(filteredNames); // Output: [Alice]
map
Functional Interface Used:
Function<T, R>
Description: Transforms each element to another type.
Explanation: The
map
method takes aFunction<T, R>
, which applies a function to each element and produces a new element of typeR
.Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); List<Integer> nameLengths = names.stream() .map(String::length) .collect(Collectors.toList()); System.out.println(nameLengths); // Output: [5, 3, 7]
reduce
Functional Interface Used:
BinaryOperator<T>
Description: Combines elements to produce a single result.
Explanation: The
reduce
method uses aBinaryOperator<T>
, which takes two elements and combines them into one. It accumulates elements from the stream into a single result.Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream() .reduce(0, (a, b) -> a + b); System.out.println(sum); // Output: 15
collect
Functional Interface Used:
Collector<T, A, R>
Description: Collects the elements of the stream into a collection or another result container.
Explanation: The
collect
method uses aCollector<T, A, R>
, which accumulates the elements into a final result, such as a list, set, or map.Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); List<String> upperCaseNames = names.stream() .map(String::toUpperCase) .collect(Collectors.toList()); System.out.println(upperCaseNames); // Output: [ALICE, BOB, CHARLIE]
anyMatch
Functional Interface Used:
Predicate<T>
Description: Checks if any elements match a condition.
Explanation: The
anyMatch
method uses aPredicate<T>
to test if at least one element in the stream satisfies the condition. If any element matches, it returnstrue
.Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); boolean hasNameStartingWithA = names.stream() .anyMatch(name -> name.startsWith("A")); System.out.println(hasNameStartingWithA); // Output: true
allMatch
Functional Interface Used:
Predicate<T>
Description: Checks if all elements match a condition.
Explanation: The
allMatch
method uses aPredicate<T>
to test if all elements in the stream satisfy the condition. It returnstrue
only if every element matches.Example:
List<Integer> numbers = Arrays.asList(2, 4, 6, 8); boolean allEven = numbers.stream() .allMatch(n -> n % 2 == 0); System.out.println(allEven); // Output: true
noneMatch
Functional Interface Used:
Predicate<T>
Description: Checks if no elements match a condition.
Explanation: The
noneMatch
method uses aPredicate<T>
to test if no elements in the stream satisfy the condition. It returnstrue
if all elements do not match.Example:
List<Integer> numbers = Arrays.asList(2, 4, 6, 8); boolean noneOdd = numbers.stream() .noneMatch(n -> n % 2 != 0); System.out.println(noneOdd); // Output: true
findFirst
Functional Interface Used: None
Description: Returns the first element in the stream.
Explanation: The
findFirst
method returns anOptional<T>
containing the first element of the stream, if available. It does not use a functional interface because it performs a simple retrieval.Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); Optional<String> firstName = names.stream() .findFirst(); firstName.ifPresent(System.out::println); // Output: Alice
findAny
Functional Interface Used: None
Description: Returns any element in the stream.
Explanation: The
findAny
method returns anOptional<T>
containing any element from the stream. It doesn’t require a functional interface as it returns an element without applying a condition.Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); Optional<String> anyName = names.stream() .findAny(); anyName.ifPresent(System.out::println); // Output: (Could be any element, e.g., Alice)
sorted
Functional Interface Used:
Comparator<T>
Description: Returns a stream with elements sorted.
Explanation: The
sorted
method uses aComparator<T>
to sort elements in natural order or according to a provided comparator.Example:
List<String> names = Arrays.asList("Charlie", "Alice", "Bob"); List<String> sortedNames = names.stream() .sorted() .collect(Collectors.toList()); System.out.println(sortedNames); // Output: [Alice, Bob, Charlie]
peek
Functional Interface Used:
Consumer<T>
Description: Performs an action for each element and returns the same stream.
Explanation: The
peek
method takes aConsumer<T>
to perform an action on each element of the stream, such as logging or debugging, and returns the stream itself for further operations.Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.stream() .peek(name -> System.out.println("Processing: " + name)) .map(String::toUpperCase) .forEach(System.out::println); // Output: Processing: Alice, ALICE // Processing: Bob, BOB // Processing: Charlie, CHARLIE
flatMap
Functional Interface Used:
Function<T, Stream<R>>
Description: Transforms each element to a stream and flattens the result.
Explanation: The
flatMap
method takes aFunction<T, Stream<R>>
that maps each element to a stream of new elements, then flattens these streams into a single stream.Example:
List<List<String>> namesNested = Arrays.asList( Arrays.asList("Alice", "Bob"), Arrays.asList("Charlie", "David"), Arrays.asList("Edward") ); List<String> namesFlatStream = namesNested.stream() .flatMap(List::stream) .collect(Collectors.toList()); System.out.println(namesFlatStream); // Output: [Alice, Bob, Charlie, David, Edward]
distinct
Functional Interface Used: None
Description: Returns a stream with distinct elements (i.e., removes duplicates).
Explanation: The
distinct
method removes duplicate elements from the stream without requiring any functional interface.Example:
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5); List<Integer> distinctNumbers = numbers.stream() .distinct() .collect(Collectors.toList()); System.out.println(distinctNumbers); // Output: [1, 2, 3, 4, 5]
limit
Functional Interface Used: None
Description: Limits the stream to a specified number of elements.
Explanation: The
limit
method takes an integer argument specifying the maximum number of elements to retain in the stream, and does not use a functional interface.Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); List<Integer> limitedNumbers = numbers.stream() .limit(3) .collect(Collectors.toList()); System.out.println(limitedNumbers); // Output: [1, 2, 3]
skip
Functional Interface Used: None
Description: Skips the first n elements of the stream.
Explanation: The
skip
method takes an integer argument specifying the number of elements to skip and does not use a functional interface.Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); List<Integer> skippedNumbers = numbers.stream() .skip(3) .collect(Collectors.toList()); System.out.println(skippedNumbers); // Output: [4, 5, 6]
reduce
Functional Interface Used:
BinaryOperator<T>
Description: Reduces the elements to a single result using a binary operator.
Explanation: The
reduce
method uses aBinaryOperator<T>
, which takes two elements and combines them into one. It accumulates elements from the stream into a single result.Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream() .reduce(0, (a, b) -> a + b); System.out.println(sum); // Output: 15
toArray
Functional Interface Used: None
Description: Converts the stream to an array.
Explanation: The
toArray
method converts the stream’s elements into an array. It does not use a functional interface, but provides methods to specify the array type.Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); String[] namesArray = names.stream() .toArray(String[]::new); System.out.println(Arrays.toString(namesArray)); // Output: [Alice, Bob, Charlie]
count
Functional Interface Used: None
Description: Counts the number of elements in the stream.
Explanation: The
count
method returns the number of elements in the stream as along
. It does not use a functional interface.Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); long count = names.stream() .count(); System.out.println(count); // Output: 3
max
Functional Interface Used:
Comparator<T>
Description: Finds the maximum element according to a comparator.
Explanation: The
max
method uses aComparator<T>
to determine the maximum element in the stream.Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> max = numbers.stream() .max(Integer::compareTo); max.ifPresent(System.out::println); // Output: 5
min
Functional Interface Used:
Comparator<T>
Description: Finds the minimum element according to a comparator.
Explanation: The
min
method uses aComparator<T>
to determine the minimum element in the stream.Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> min = numbers.stream() .min(Integer::compareTo); min.ifPresent(System.out::println); // Output: 1
Summary
Methods like
forEach
,peek
, andcollect
use functional interfaces such asConsumer<T>
andCollector<T, A, R>
.Methods like
filter
,anyMatch
,allMatch
, andnoneMatch
use thePredicate<T>
functional interface.Methods like
map
andflatMap
use theFunction<T, R>
interface.Methods like
reduce
,max
, andmin
useBinaryOperator<T>
andComparator<T>
respectively.Methods like
limit
,skip
,distinct
,findFirst
,findAny
,toArray
, andcount
do not use a functional interface.
Method References
There are four types of method references:
Reference to a static method
Reference to an instance method of a particular object
Reference to an instance method of an arbitrary object of a particular type
Reference to a constructor
1. Reference to a Static Method
Syntax:ClassName::staticMethodName
Example:
import java.util.function.BiFunction;
public class StaticMethodReferenceExample {
public static void main(String[] args) {
// Creating a method reference to the static method 'add' of the 'MethodReferences' class
BiFunction<Integer, Integer, Integer> adder = MethodReferences::add;
// Using the method reference to call the 'add' method
int result = adder.apply(10, 20);
// Printing the result
System.out.println("Sum: " + result);
}
}
class MethodReferences {
// Static method to add two integers
public static int add(int a, int b) {
return a + b;
}
}
Explanation:
In this example, we have a static method add
in the MethodReferences
class that takes two integers and returns their sum. We create a method reference to this static method using MethodReferences::add
and assign it to a BiFunction
functional interface. The apply
method of BiFunction
calls the add
method, resulting in the sum of 10 and 20 being printed.
2. Reference to an Instance Method of a Particular Object
Syntax:object::instanceMethodName
Example:
import java.util.function.Function;
public class InstanceMethodReferenceExample {
public static void main(String[] args) {
// Creating a string object
String str = "Hello, World!";
// Creating a method reference to the instance method 'toUpperCase' of the 'str' object
Function<String, String> toUpperCase = str::toUpperCase;
// Using the method reference to call the 'toUpperCase' method
System.out.println(toUpperCase.apply(str));
}
}
Explanation:
Here, we have a String
object str
with the value "Hello, World!". We create a method reference to the toUpperCase
instance method of this str
object using str::toUpperCase
. This method reference is assigned to a Function
functional interface. When we call the apply
method of Function
, it invokes the toUpperCase
method on the str
object, converting the string to uppercase.
3. Reference to an Instance Method of an Arbitrary Object of a Particular Type
Syntax:ClassName::instanceMethodName
Example:
import java.util.Arrays;
import java.util.List;
public class ArbitraryObjectMethodReferenceExample {
public static void main(String[] args) {
// Creating a list of names
List<String> names = Arrays.asList("John", "Paul", "George", "Ringo");
// Using method reference to the 'println' method of 'System.out' for each element of the list
names.forEach(System.out::println);
}
}
Explanation:
In this example, we have a list of names. We use the forEach
method of the List
interface, which accepts a Consumer
functional interface. We provide a method reference System.out::println
to the println
method of the System.out
object. This means that for each element in the list, the println
method will be called, printing each name to the console.
4. Reference to a Constructor
Syntax:ClassName::new
Example:
import java.util.function.Supplier;
public class ConstructorReferenceExample {
public static void main(String[] args) {
// Creating a method reference to the 'Person' class constructor
Supplier<Person> personSupplier = Person::new;
// Using the method reference to create a new instance of 'Person'
Person person = personSupplier.get();
// Printing the person object
System.out.println(person);
}
}
class Person {
String name;
// Constructor initializing the name field
public Person() {
this.name = "John Doe";
}
@Override
public String toString() {
return "Person{name='" + name + "'}";
}
}
Explanation:
In this example, we have a Person
class with a default constructor that initializes the name
field. We create a method reference to this constructor using Person::new
and assign it to a Supplier
functional interface. The get
method of Supplier
calls the constructor, creating a new Person
object. We then print this object, which invokes the toString
method to display the name.
Additional Concepts
Method References and Functional Interfaces
Method references work seamlessly with functional interfaces, which are interfaces with a single abstract method. Java provides several functional interfaces in the java.util.function
package, such as Function
, BiFunction
, Supplier
, Consumer
, and more. Method references can be used wherever these interfaces are expected, making the code more concise and readable.
Chaining Method References
You can chain method references to perform a sequence of operations. For example:
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
public class ChainingMethodReferenceExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("john", "paul", "george", "ringo");
// Using method reference chaining to process the names list
names.stream()
.map(String::toUpperCase) // Convert each name to uppercase
.forEach(System.out::println); // Print each name
}
}
Explanation:
In this example, we use a stream to process the list of names. The map
method converts each name to uppercase using the method reference String::toUpperCase
. The forEach
method then prints each name using the method reference System.out::println
.
Overloaded Methods and Method References
When dealing with overloaded methods, it's important to ensure that the functional interface's method signature matches exactly with the method reference.
Example:
import java.util.function.BiFunction;
import java.util.function.Function;
public class OverloadedMethodsExample {
public static void main(String[] args) {
// Method reference to an overloaded static method
BiFunction<String, String, Integer> compareStrings = OverloadedMethods::compare;
System.out.println(compareStrings.apply("apple", "banana"));
// Method reference to another overloaded static method
Function<String, Integer> getStringLength = OverloadedMethods::compare;
System.out.println(getStringLength.apply("apple"));
}
}
class OverloadedMethods {
// Overloaded static methods
public static int compare(String a, String b) {
return a.compareTo(b);
}
public static int compare(String a) {
return a.length();
}
}
Capturing Variables in Method References
Method references can capture variables from their enclosing scope, similar to lambda expressions.
Example:
import java.util.function.Function;
public class CapturingVariableExample {
public static void main(String[] args) {
String prefix = "Hello, ";
// Method reference that captures a variable from the enclosing scope
Function<String, String> greeter = prefix::concat;
// Using the method reference
System.out.println(greeter.apply("World!"));
}
}
Using Method References with Custom Functional Interfaces
You can define your own functional interfaces and use method references with them.
Example:
public class CustomFunctionalInterfaceExample {
public static void main(String[] args) {
// Method reference to an instance method
String str = "Custom functional interface example";
MyFunctionalInterface<String> toUpperCase = str::toUpperCase;
System.out.println(toUpperCase.apply());
// Method reference to a static method
MyBiFunction<Integer, Integer, Integer> adder = CustomFunctionalInterfaceExample::add;
System.out.println(adder.apply(10, 20));
}
public static int add(int a, int b) {
return a + b;
}
}
// Custom functional interface with no parameters
@FunctionalInterface
interface MyFunctionalInterface<T> {
T apply();
}
// Custom functional interface with two parameters
@FunctionalInterface
interface MyBiFunction<T, U, R> {
R apply(T t, U u);
}
Type Inference with Method References
Java's type inference can often determine the appropriate types for method references, making the code cleaner.
Example:
import java.util.function.BiFunction;
public class TypeInferenceExample {
public static void main(String[] args) {
// The types of the parameters are inferred from the context
BiFunction<String, String, String> concat = String::concat;
System.out.println(concat.apply("Hello, ", "World!"));
}
}
Method References in Streams and Collections
Method references are commonly used in conjunction with streams and collections to perform operations in a more concise manner.
Example:
import java.util.Arrays;
import java.util.List;
public class StreamMethodReferencesExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world", "method", "references");
// Using method references with streams
words.stream()
.map(String::toUpperCase)
.filter(word -> word.startsWith("M"))
.forEach(System.out::println);
}
}
Explanation:
In this example, the stream
method is used to create a stream from the list of words. The map
method converts each word to uppercase using the method reference String::toUpperCase
. The filter
method filters the words starting with "M". The forEach
method prints each word using the method reference System.out::println
.
What is Optional?
In Java, a common problem is dealing with null
values. If a variable is null
and you try to use it, it will cause a NullPointerException
. The Optional
class helps to avoid this by providing a way to represent a value that might be missing.
Why Use Optional
?
Avoid NullPointerException: Provides a safer alternative to returning
null
. Null checks can be error-prone and can lead toNullPointerException
.Expressive Code: Makes the presence or absence of a value explicit in the code. The intention behind an
Optional
return type is clear: it indicates that the value might be missing.Functional Style: Allows for more functional programming constructs, enabling more readable and declarative code using methods like
map()
,flatMap()
,filter()
, etc.
Creating an Optional
There are three main ways to create an Optional
object.
Empty Optional:
Optional<String> emptyOptional = Optional.empty(); System.out.println(emptyOptional); // Output: Optional.empty
- This creates an
Optional
that does not contain any value.
- This creates an
Optional with a Non-Null Value:
Optional<String> nonEmptyOptional = Optional.of("Hello, World!"); System.out.println(nonEmptyOptional); // Output: Optional[Hello, World!]
- This creates an
Optional
that contains the specified non-null value. If you passnull
toOptional.of()
, it will throw aNullPointerException
.
- This creates an
Optional with a Possibly Null Value:
Optional<String> nullableOptional = Optional.ofNullable(null); System.out.println(nullableOptional); // Output: Optional.empty
- This creates an
Optional
that can contain either a non-null value ornull
. If the value isnull
, it returns an emptyOptional
.
- This creates an
Checking for a Value
isPresent():
Optional<String> optional = Optional.ofNullable("Hello"); if (optional.isPresent()) { System.out.println("Value is present: " + optional.get()); // Output: Value is present: Hello } else { System.out.println("Value is absent"); }
- This checks if a value is present. If it is, you can get the value using
get()
. Otherwise, it prints "Value is absent".
- This checks if a value is present. If it is, you can get the value using
ifPresent():
optional.ifPresent(value -> System.out.println("Value is present: " + value)); // Output: Value is present: Hello
- This does the same check but in a more functional style. It executes the given action (printing the value) if a value is present.
Retrieving the Value
get():
Optional<String> optional = Optional.of("Hello"); String value = optional.get(); System.out.println(value); // Output: Hello
- This method returns the value if present. Be careful: if the
Optional
is empty, it throwsNoSuchElementException
.
- This method returns the value if present. Be careful: if the
orElse():
Optional<String> optional = Optional.ofNullable(null); String value = optional.orElse("Default Value"); System.out.println(value); // Output: Default Value
- This returns the value if present, otherwise returns the provided default value.
orElseGet():
Optional<String> optional = Optional.ofNullable(null); String value = optional.orElseGet(() -> "Default Value from Supplier"); System.out.println(value); // Output: Default Value from Supplier
- Similar to
orElse()
, but uses aSupplier
to generate the default value. TheSupplier
is only called if theOptional
is empty.
- Similar to
orElseThrow():
Optional<String> optional = Optional.ofNullable(null); try { String value = optional.orElseThrow(() -> new IllegalArgumentException("Value not present")); } catch (Exception e) { System.out.println(e.getMessage()); // Output: Value not present }
- Returns the value if present, otherwise throws an exception created by the provided
Supplier
.
- Returns the value if present, otherwise throws an exception created by the provided
Transforming the Value
map():
Optional<String> optional = Optional.of("hello"); Optional<String> upperCase = optional.map(String::toUpperCase); System.out.println(upperCase); // Output: Optional[HELLO]
- This applies a function to the value if present, transforming it. If the
Optional
is empty,map()
returns an emptyOptional
.
- This applies a function to the value if present, transforming it. If the
flatMap():
Optional<String> optional = Optional.of("hello"); Optional<Integer> length = optional.flatMap(value -> Optional.of(value.length())); System.out.println(length); // Output: Optional[5]
- Similar to
map()
, but the function itself returns anOptional
. This "flattens" the result, avoiding nestedOptional
objects.
- Similar to
Filtering the Value
filter():
Optional<String> optional = Optional.of("hello"); Optional<String> filtered = optional.filter(value -> value.startsWith("h")); System.out.println(filtered); // Output: Optional[hello] Optional<String> filteredAbsent = optional.filter(value -> value.startsWith("x")); System.out.println(filteredAbsent); // Output: Optional.empty
- Filters the value if present, only keeping it if it matches the given predicate. Otherwise, returns an empty
Optional
.
- Filters the value if present, only keeping it if it matches the given predicate. Otherwise, returns an empty
Practical Examples
Using
Optional
in a Method:public Optional<String> getUserEmail(User user) { return Optional.ofNullable(user.getEmail()); } // Assuming user.getEmail() returns "user@example.com" User user = new User("user@example.com"); Optional<String> email = getUserEmail(user); System.out.println(email); // Output: Optional[user@example.com]
- If the email is present, returns it wrapped in an
Optional
. If the email isnull
, returns an emptyOptional
.
- If the email is present, returns it wrapped in an
Using
Optional
with Streams:List<String> list = Arrays.asList("apple", "banana", "cherry"); Optional<String> firstWithB = list.stream() .filter(value -> value.startsWith("b")) .findFirst(); System.out.println(firstWithB); // Output: Optional[banana]
- Finds the first element in the list that starts with "b" and returns it wrapped in an
Optional
.
- Finds the first element in the list that starts with "b" and returns it wrapped in an
Avoiding
NullPointerException
:String name = Optional.ofNullable(getName()) .map(String::toUpperCase) .orElse("No Name Provided"); System.out.println(name); // Output: No Name Provided (if getName() returns null) or JOHN (if getName() returns "john")
- Transforms the name to uppercase if it is present. If the name is
null
, returns "No Name Provided".
- Transforms the name to uppercase if it is present. If the name is
Advanced Usage
Optional Chaining:
Optional<String> result = Optional.of("Java") .filter(value -> value.length() > 2) .map(String::toUpperCase); System.out.println(result); // Output: Optional[JAVA]
- Chains multiple
Optional
operations to process the value conditionally and transform it.
- Chains multiple
Combining Optionals:
Optional<String> optional1 = Optional.of("Hello"); Optional<String> optional2 = Optional.of("World"); Optional<String> combined = optional1.flatMap(value1 -> optional2.map(value2 -> value1 + " " + value2)); System.out.println(combined); // Output: Optional[Hello World]
- Combines two
Optional
values by usingflatMap()
andmap()
.
- Combines two
Utility Methods
equals() and hashCode():
Optional<String> optional1 = Optional.of("value"); Optional<String> optional2 = Optional.of("value"); System.out.println(optional1.equals(optional2)); // Output: true System.out.println(optional1.hashCode() == optional2.hashCode()); // Output: true
- Compares
Optional
objects and provides hash codes, ensuring they can be used in collections.
- Compares
toString():
Optional<String> optional = Optional.of("Hello"); System.out.println(optional.toString()); // Output: Optional[Hello]
- Provides a human-readable string representation of the
Optional
instance.
- Provides a human-readable string representation of the
Summary
Creation: Use
Optional.empty()
,Optional.of()
, orOptional.ofNullable()
to create anOptional
.Checking: Use
isPresent()
orifPresent()
to check if a value is present.Retrieving: Use
get()
,orElse()
,orElseGet()
, ororElseThrow()
to retrieve the value.Transforming: Use
map()
orflatMap()
to transform the value.Filtering: Use
filter()
to conditionally keep the value.
Key Classes in the New Date and Time API
LocalDate
LocalTime
LocalDateTime
ZonedDateTime
Period
Duration
Instant
DateTimeFormatter
LocalDate
LocalDate
represents a date without a time-zone in the ISO-8601 calendar system (e.g., 2007-12-03).
Creating LocalDate
Current Date:
LocalDate currentDate = LocalDate.now(); System.out.println(currentDate); // Output: Current date (e.g., 2024-07-18)
- Creates a
LocalDate
representing the current date.
- Creates a
Specific Date:
LocalDate specificDate = LocalDate.of(2020, Month.JANUARY, 1); System.out.println(specificDate); // Output: 2020-01-01
- Creates a
LocalDate
representing January 1, 2020.
- Creates a
Parsing a Date:
LocalDate parsedDate = LocalDate.parse("2020-01-01"); System.out.println(parsedDate); // Output: 2020-01-01
- Parses a
String
to create aLocalDate
.
- Parses a
Manipulating LocalDate
Add/Subtract Days, Months, Years:
LocalDate date = LocalDate.of(2020, Month.JANUARY, 1); LocalDate nextWeek = date.plusWeeks(1); LocalDate previousMonth = date.minusMonths(1); System.out.println(nextWeek); // Output: 2020-01-08 System.out.println(previousMonth); // Output: 2019-12-01
Get Day of Week/Month/Year:
DayOfWeek dayOfWeek = date.getDayOfWeek(); int dayOfMonth = date.getDayOfMonth(); int year = date.getYear(); System.out.println(dayOfWeek); // Output: WEDNESDAY System.out.println(dayOfMonth); // Output: 1 System.out.println(year); // Output: 2020
LocalTime
LocalTime
represents a time without a time-zone in the ISO-8601 calendar system (e.g., 10:15:30).
Creating LocalTime
Current Time:
LocalTime currentTime = LocalTime.now(); System.out.println(currentTime); // Output: Current time (e.g., 10:15:30)
Specific Time:
LocalTime specificTime = LocalTime.of(10, 15, 30); System.out.println(specificTime); // Output: 10:15:30
Parsing a Time:
LocalTime parsedTime = LocalTime.parse("10:15:30"); System.out.println(parsedTime); // Output: 10:15:30
Manipulating LocalTime
Add/Subtract Hours, Minutes, Seconds:
LocalTime time = LocalTime.of(10, 15, 30); LocalTime nextHour = time.plusHours(1); LocalTime previousMinute = time.minusMinutes(1); System.out.println(nextHour); // Output: 11:15:30 System.out.println(previousMinute); // Output: 10:14:30
Get Hour, Minute, Second:
int hour = time.getHour(); int minute = time.getMinute(); int second = time.getSecond(); System.out.println(hour); // Output: 10 System.out.println(minute); // Output: 15 System.out.println(second); // Output: 30
LocalDateTime
LocalDateTime
combines date and time, but still without a time-zone (e.g., 2007-12-03T10:15:30).
Creating LocalDateTime
Current Date and Time:
LocalDateTime currentDateTime = LocalDateTime.now(); System.out.println(currentDateTime); // Output: Current date and time (e.g., 2024-07-18T10:15:30)
Specific Date and Time:
LocalDateTime specificDateTime = LocalDateTime.of(2020, Month.JANUARY, 1, 10, 15, 30); System.out.println(specificDateTime); // Output: 2020-01-01T10:15:30
Parsing a Date and Time:
LocalDateTime parsedDateTime = LocalDateTime.parse("2020-01-01T10:15:30"); System.out.println(parsedDateTime); // Output: 2020-01-01T10:15:30
ZonedDateTime
ZonedDateTime
represents a date and time with a time-zone (e.g., 2007-12-03T10:15:30+01:00[Europe/Paris]).
Creating ZonedDateTime
Current Date and Time with Zone:
ZonedDateTime currentZonedDateTime = ZonedDateTime.now(); System.out.println(currentZonedDateTime); // Output: Current date and time with time-zone
Specific Date and Time with Zone:
ZonedDateTime specificZonedDateTime = ZonedDateTime.of(2020, Month.JANUARY, 1, 10, 15, 30, 0, ZoneId.of("Europe/Paris")); System.out.println(specificZonedDateTime); // Output: 2020-01-01T10:15:30+01:00[Europe/Paris]
Parsing a Date and Time with Zone:
ZonedDateTime parsedZonedDateTime = ZonedDateTime.parse("2020-01-01T10:15:30+01:00[Europe/Paris]"); System.out.println(parsedZonedDateTime); // Output: 2020-01-01T10:15:30+01:00[Europe/Paris]
Period
Period
represents a quantity of time in terms of years, months, and days (e.g., "2 years, 3 months and 4 days").
Creating Period
Between Two Dates:
LocalDate startDate = LocalDate.of(2020, Month.JANUARY, 1); LocalDate endDate = LocalDate.of(2022, Month.APRIL, 1); Period period = Period.between(startDate, endDate); System.out.println(period); // Output: P2Y3M
Specific Period:
Period twoYearsThreeMonths = Period.of(2, 3, 0); System.out.println(twoYearsThreeMonths); // Output: P2Y3M
Duration
Duration
represents a quantity of time in terms of seconds and nanoseconds (e.g., "34.5 seconds").
Creating Duration
Between Two Times:
LocalTime startTime = LocalTime.of(10, 15, 30); LocalTime endTime = LocalTime.of(12, 15, 30); Duration duration = Duration.between(startTime, endTime); System.out.println(duration); // Output: PT2H
Specific Duration:
Duration twoHours = Duration.ofHours(2); System.out.println(twoHours); // Output: PT2H
Instant
Instant
represents a point in time (e.g., "1970-01-01T00:00:00Z").
Creating Instant
Current Instant:
Instant currentInstant = Instant.now(); System.out.println(currentInstant); // Output: Current timestamp (e.g., 2024-07-18T08:15:30.00Z)
Specific Instant:
Instant specificInstant = Instant.ofEpochSecond(1629453600); System.out.println(specificInstant); // Output: 2021-08-20T00:00:00Z
DateTimeFormatter
DateTimeFormatter
is used to format and parse date-time objects.
Formatting and Parsing
Formatting:
LocalDate date = LocalDate.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); String formattedDate = date.format(formatter); System.out.println(formattedDate); // Output: 18/07/2024
Parsing:
String dateString = "18/07/2024"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); LocalDate parsedDate = LocalDate.parse(dateString, formatter); System.out.println(parsedDate); // Output: 2024-07-18
Summary
LocalDate, LocalTime, LocalDateTime: Used to represent date, time, and both date and time without a time-zone.
ZonedDateTime: Represents date and time with a time-zone.
Period, Duration: Used to represent a quantity of time in different units.
Instant: Represents a point in time.
DateTimeFormatter: Used for formatting and parsing date-time objects.
Default Methods?
Default methods are methods defined in interfaces with a default
keyword. They allow interfaces to have method implementations, not just method declarations. This feature was introduced in Java 8 to facilitate backward compatibility while enabling the addition of new methods to interfaces.
Why Use Default Methods?
Maintain Backward Compatibility:
- You can add new methods to interfaces without breaking existing implementations.
Support Multiple Inheritance:
- Allows interfaces to provide method implementations, enabling classes to inherit behavior from multiple interfaces.
Reduce Boilerplate Code:
- Promotes code reuse by providing default implementations of methods that can be shared across multiple classes.
Syntax of Default Methods
interface MyInterface {
// Abstract method
void abstractMethod();
// Default method
default void defaultMethod() {
System.out.println("This is a default method.");
}
}
Example: Basic Usage of Default Methods
Let's start with a basic example to understand how default methods work.
Defining an Interface with Default Method
interface Vehicle {
void start(); // Abstract method
default void stop() {
System.out.println("Vehicle is stopping.");
}
}
Here, Vehicle
has an abstract method start()
and a default method stop()
.
Implementing the Interface
class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car is starting.");
}
}
class Bike implements Vehicle {
@Override
public void start() {
System.out.println("Bike is starting.");
}
}
Car
andBike
classes implement theVehicle
interface and provide their implementations for thestart
method. They inherit the defaultstop
method.
Using the Default Method
public class Main {
public static void main(String[] args) {
Vehicle car = new Car();
car.start(); // Output: Car is starting.
car.stop(); // Output: Vehicle is stopping.
Vehicle bike = new Bike();
bike.start(); // Output: Bike is starting.
bike.stop(); // Output: Vehicle is stopping.
}
}
Overriding Default Methods
If a class needs a specific implementation of a default method, it can override the default method.
class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car is starting.");
}
@Override
public void stop() {
System.out.println("Car is stopping quickly.");
}
}
Now, the Car
class provides its own implementation of stop
.
Testing the Override
public class Main {
public static void main(String[] args) {
Vehicle car = new Car();
car.start(); // Output: Car is starting.
car.stop(); // Output: Car is stopping quickly.
}
}
Multiple Interfaces with Default Methods
Java 8 supports multiple inheritance of behavior through interfaces. Default methods in interfaces can be used together.
Two Interfaces with Default Methods
interface Engine {
default void start() {
System.out.println("Engine is starting.");
}
}
interface Vehicle {
default void start() {
System.out.println("Vehicle is starting.");
}
}
Engine
andVehicle
both have a defaultstart
method.
Implementing Both Interfaces
class Car implements Vehicle, Engine {
@Override
public void start() {
Vehicle.super.start();
Engine.super.start();
}
}
- The
Car
class overridesstart
to call bothVehicle
andEngine
default methods.
Testing Multiple Interface Inheritance
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.start();
// Output:
// Vehicle is starting.
// Engine is starting.
}
}
Resolving Conflicts
If two interfaces have conflicting default methods, the implementing class must resolve the conflict explicitly.
Interfaces with Conflicting Default Methods
interface Vehicle {
default void start() {
System.out.println("Vehicle is starting.");
}
}
interface Engine {
default void start() {
System.out.println("Engine is starting.");
}
}
Implementing with Conflict Resolution
class Car implements Vehicle, Engine {
@Override
public void start() {
Vehicle.super.start();
Engine.super.start();
}
}
- The
Car
class overridesstart
and explicitly callsVehicle.super.start()
andEngine.super.start()
.
Testing Conflict Resolution
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.start();
// Output:
// Vehicle is starting.
// Engine is starting.
}
}
Practical Use Case: Adding Methods to Interfaces
Default methods are particularly useful for evolving APIs without breaking existing code.
Initial Interface
interface Drawable {
void draw();
}
Adding a Default Method in Java 8
interface Drawable {
void draw();
default void resize() {
System.out.println("Resizing the drawable.");
}
}
Implementing the Interface
class Circle implements Drawable {
@Override
public void draw() {
System.out.println("Drawing a circle.");
}
}
class Rectangle implements Drawable {
@Override
public void draw() {
System.out.println("Drawing a rectangle.");
}
}
Using the Default Method
public class Main {
public static void main(String[] args) {
Drawable circle = new Circle();
circle.draw(); // Output: Drawing a circle.
circle.resize(); // Output: Resizing the drawable.
Drawable rectangle = new Rectangle();
rectangle.draw(); // Output: Drawing a rectangle.
rectangle.resize(); // Output: Resizing the drawable.
}
}
Advanced Features of Default Methods
Static Methods in Interfaces
- Interfaces can have static methods, which are not inherited by implementing classes.
interface MathOperations {
static int add(int a, int b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
int sum = MathOperations.add(5, 10);
System.out.println(sum); // Output: 15
}
}
Default Methods with Lambda Expressions
- Default methods can be overridden with lambda expressions in Java 8.
interface Greeting {
default void sayHello() {
System.out.println("Hello, World!");
}
}
public class Main {
public static void main(String[] args) {
Greeting greeting = () -> System.out.println("Hello from Lambda!");
greeting.sayHello();
}
}
Comparing Java 7 and Java 8 Interfaces
Feature | Java 7 | Java 8 |
Abstract Methods | Yes | Yes |
Default Methods | No | Yes |
Static Methods | No | Yes |
Backward Compatibility | Challenging (Adding methods breaks implementations) | Easy (Adding default methods doesn't break implementations) |
Multiple Inheritance of Behavior | Not possible | Possible through default methods |
Code Reuse | Limited | Enhanced with default methods |
Lambda Expressions | Not applicable | Can be used to implement default methods |
Summary
Default Methods: These are methods in interfaces with the
default
keyword, providing a default implementation.Backward Compatibility: You can add new methods to interfaces without breaking the existing code that implements the interface.
Multiple Inheritance: Classes can inherit behavior from multiple interfaces, making code more flexible and reusable.
Conflict Resolution: If two interfaces have conflicting default methods, the implementing class must resolve the conflict explicitly.
Static Methods: Interfaces can also contain static methods, which are not inherited by implementing classes.
Lambda Expressions: You can override default methods using lambda expressions for concise and readable code.
Nashorn JavaScript Engine
Nashorn is a JavaScript engine developed by Oracle, which is included in the Java Development Kit (JDK) starting from Java 8. It allows developers to execute JavaScript code from within Java applications. Nashorn provides improved performance and compatibility with the ECMAScript 5.1 specification.
Key Features of Nashorn
ECMAScript Compliance: Nashorn is designed to comply with the ECMAScript 5.1 specification, providing a modern JavaScript runtime.
Integration with Java: It allows for tight integration between Java and JavaScript, enabling developers to call Java code from JavaScript and vice versa.
Improved Performance: Nashorn uses the invokedynamic feature of the JVM to achieve better performance compared to its predecessor, Rhino.
JavaScript Shell: It provides a command-line tool (
jjs
) for executing JavaScript code.
Using Nashorn
Executing JavaScript from Java
To execute JavaScript code within a Java application, you can use the javax.script
package. The key classes involved are ScriptEngineManager
and ScriptEngine
.
Example: Basic JavaScript Execution
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class NashornExample {
public static void main(String[] args) {
// Create a ScriptEngineManager
ScriptEngineManager manager = new ScriptEngineManager();
// Get a Nashorn ScriptEngine
ScriptEngine engine = manager.getEngineByName("nashorn");
// Evaluate JavaScript code
try {
engine.eval("print('Hello, Nashorn');"); // Output: Hello, Nashorn
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Invoking Java Methods from JavaScript
Nashorn allows JavaScript code to call Java methods and access Java objects.
Example: Calling Java Methods
javaCopy codeimport javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class NashornExample {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
try {
engine.eval("var javaString = 'Hello from JavaScript';");
engine.eval("print(javaString);"); // Output: Hello from JavaScript
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Accessing JavaScript Variables from Java
You can also access and manipulate JavaScript variables from Java.
Example: Accessing JavaScript Variables
import javax.script.*;
public class NashornExample {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
try {
engine.eval("var number = 10;");
engine.eval("var greeting = 'Hello, Nashorn';");
// Access JavaScript variables from Java
int number = (int) engine.get("number");
String greeting = (String) engine.get("greeting");
System.out.println("Number: " + number); // Output: Number: 10
System.out.println("Greeting: " + greeting); // Output: Greeting: Hello, Nashorn
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Interoperability between Java and JavaScript
Nashorn makes it easy to work with Java objects in JavaScript and vice versa.
Example: Using Java Collections in JavaScript
import javax.script.*;
import java.util.*;
public class NashornExample {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
engine.put("list", list);
try {
engine.eval("list.forEach(function(item) { print(item); });");
// Output:
// Apple
// Banana
// Cherry
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Working with JSON
Nashorn makes it simple to parse and generate JSON data.
Example: Parsing JSON
import javax.script.*;
public class NashornExample {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
String jsonString = "{ \"name\": \"John\", \"age\": 30 }";
engine.put("jsonString", jsonString);
try {
engine.eval("var obj = JSON.parse(jsonString);");
engine.eval("print('Name: ' + obj.name);"); // Output: Name: John
engine.eval("print('Age: ' + obj.age);"); // Output: Age: 30
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Example: Generating JSON
import javax.script.*;
public class NashornExample {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
try {
engine.eval("var obj = { name: 'John', age: 30 };");
engine.eval("var jsonString = JSON.stringify(obj);");
String jsonString = (String) engine.get("jsonString");
System.out.println(jsonString); // Output: {"name":"John","age":30}
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
Using JavaScript Libraries
Nashorn can load and use JavaScript libraries, allowing for more complex scripting within Java applications.
Example: Loading and Using a JavaScript Library
import javax.script.*;
public class NashornExample {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
try {
engine.eval(new java.io.FileReader("path/to/library.js"));
engine.eval("var result = libraryFunction();");
System.out.println(engine.get("result"));
} catch (ScriptException | java.io.FileNotFoundException e) {
e.printStackTrace();
}
}
}
JavaScript Shell (jjs
)
The jjs
tool is a command-line utility for executing JavaScript code using the Nashorn engine. This tool can be useful for testing and quick scripting tasks.
Example: Running jjs
$ jjs
jjs> print('Hello, Nashorn');
Hello, Nashorn
You can also execute JavaScript files directly using jjs
.
$ jjs script.js
Nashorn Limitations
While Nashorn is powerful, it has some limitations:
Limited ECMAScript Support: Nashorn primarily supports ECMAScript 5.1, with some features from ECMAScript 6.
Performance: While faster than Rhino, Nashorn may still be slower than native Java code for some tasks.
Deprecation: Starting from JDK 11, Nashorn is deprecated, and it was removed in JDK 15.
Summary
Nashorn is a JavaScript engine introduced in Java 8 for executing JavaScript code within Java applications.
Key Features: ECMAScript 5.1 compliance, integration with Java, improved performance, and a JavaScript shell (
jjs
).Usage: Execute JavaScript code, call Java methods from JavaScript, access JavaScript variables from Java, and work with JSON.
Interoperability: Allows seamless interaction between Java and JavaScript.
Limitations: Limited ECMAScript support, performance considerations, and eventual deprecation in later Java versions.
Why CompletableFuture?
CompletableFuture
is introduced to help write asynchronous, non-blocking code. Asynchronous programming allows tasks to run in the background, so the main program can continue executing without waiting for these tasks to complete. This is useful for tasks like web requests, database queries, or any other long-running operations.
Benefits of CompletableFuture
:
Non-blocking: Main thread is not blocked while waiting for the task to complete.
Callback-based: You can specify actions to take once the task completes.
Flexible and Composable: Easily combine multiple asynchronous tasks.
How to Create a CompletableFuture
You can create a CompletableFuture
using different methods. Let's go through them step by step.
1. Basic Creation
You can create an incomplete CompletableFuture
and complete it manually.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
// Create an incomplete CompletableFuture
CompletableFuture<String> future = new CompletableFuture<>();
// Complete the future manually
future.complete("Hello, World!");
// Retrieve the result
System.out.println(future.join()); // Output: Hello, World!
}
}
2. Using Factory Methods
CompletableFuture
provides factory methods to create futures that are already completed or run tasks asynchronously.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
// Completed future with a value
CompletableFuture<String> completedFuture = CompletableFuture.completedFuture("Completed");
System.out.println(completedFuture.join()); // Output: Completed
// Run an asynchronous task
CompletableFuture<Void> asyncFuture = CompletableFuture.runAsync(() -> {
System.out.println("Running asynchronously");
});
asyncFuture.join(); // Wait for the task to complete
}
}
Asynchronous Computations
You can run tasks asynchronously using runAsync()
and supplyAsync()
.
runAsync()
Executes a Runnable
task asynchronously (a task that does not return a value).
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("Task executed asynchronously");
});
future.join(); // Wait for the task to complete
}
}
supplyAsync()
Executes a Supplier
task asynchronously (a task that returns a value).
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "Result of the asynchronous computation";
});
System.out.println(future.join()); // Output: Result of the asynchronous computation
}
}
Chaining Tasks
You can chain multiple tasks together using methods like thenApply()
, thenAccept()
, thenRun()
, and thenCombine()
.
thenApply()
Transforms the result of a future.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(result -> result + ", World!");
System.out.println(future.join()); // Output: Hello, World!
}
}
thenAccept()
Consumes the result of a future without returning a new result.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> "Hello")
.thenAccept(result -> System.out.println("Result: " + result)); // Output: Result: Hello
}
}
thenRun()
Runs a Runnable
task after the future completes.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> "Hello")
.thenRun(() -> System.out.println("Computation finished")); // Output: Computation finished
}
}
thenCombine()
Combines the results of two futures.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + " " + result2);
System.out.println(combinedFuture.join()); // Output: Hello World
}
}
Exception Handling
You can handle exceptions in CompletableFuture
using methods like exceptionally()
, handle()
, and whenComplete()
.
exceptionally()
Handles exceptions and provides a default value.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("Exception occurred");
}
return "Result";
}).exceptionally(ex -> "Default Value");
System.out.println(future.join()); // Output: Default Value
}
}
handle()
Handles both result and exception.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (true) {
throw new RuntimeException("Exception occurred");
}
return "Result";
}).handle((result, ex) -> {
if (ex != null) {
return "Default Value";
}
return result;
});
System.out.println(future.join()); // Output: Default Value
}
}
whenComplete()
Runs a callback after the future completes, handling both success and failure.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Result")
.whenComplete((result, ex) -> {
if (ex != null) {
System.out.println("Exception: " + ex);
} else {
System.out.println("Result: " + result); // Output: Result: Result
}
});
future.join();
}
}
Combining Multiple Futures
allOf()
Waits for all futures to complete.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(future1, future2);
combinedFuture.join(); // Wait for all to complete
System.out.println(future1.join() + " " + future2.join()); // Output: Hello World
}
}
anyOf()
Returns when any one of the futures completes.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Result 1";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Result 2");
CompletableFuture<Object> firstFinished = CompletableFuture.anyOf(future1, future2);
System.out.println(firstFinished.join()); // Output: Result 2
}
}
Summary
Asynchronous Execution: Use
runAsync()
andsupplyAsync()
to run tasks in the background.Chaining Tasks: Link tasks using methods like
thenApply()
,thenAccept()
,thenRun()
, andthenCombine()
.Exception Handling: Manage errors with
exceptionally()
,handle()
, andwhenComplete()
.Combining Futures: Use
allOf()
to wait for all tasks andanyOf()
to get the result of the first completed task.
CompletableFuture
makes it easier to write asynchronous code in Java, allowing you to handle background tasks more efficiently and cleanly.
Conclusion
Java 8 brings a wealth of new features that greatly enhance the language's capabilities. With the introduction of lambda expressions, the Stream API, default methods, and CompletableFuture, developers can write code that is more concise, readable, and efficient. The Nashorn JavaScript engine further expands Java's functionality by enabling seamless integration with JavaScript.
These features not only modernize the language but also ensure backward compatibility, making Java 8 a crucial update for both new and existing projects. By mastering these enhancements, developers can boost their productivity and create more robust applications.
As you delve deeper into these features, you'll discover that Java 8 provides a more versatile and powerful toolkit for addressing a wide array of programming challenges. Enjoy your journey with Java 8!......
Subscribe to my newsletter
Read articles from Bikash Nishank directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by