Java 8, released on March 18, 2014, brought a major shift in the way we write Java programs. It introduced several powerful features that made code more concise, expressive, and efficient. In this post, we’ll explore the key features of Java 8 and walk through examples to understand how they can be applied in real-world projects.
Java 8 features:--
Some of the important Java 8 features are,
- Lambda Expression
- static and default methods in interface
- Java Stream
- Functional Interfaces
- Java Time API
- Collection Improvement.
- Concurrency API improvement
- Java IO improvement
A lambda expression is an anonymous function, meaning it doesn’t have a name and doesn’t belong to any class. To create a lambda expression, we specify the input parameters on the left side of the lambda operator (->
) and place the expression or body on the right side of the operator.
Syntax,
(parameter_list) -> {function_body}
Example:--
//Java 7:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Before Java8, runnable run() method");
}
}).start();
//Java 8 way:
new Thread(() -> System.out.println("In Java8, Lambda expression !!") ).start();
The advantages of lambda expressions are:
-
They reduce the amount of code.
-
They support both sequential and parallel execution by passing behavior into methods using the Stream API.
-
When combined with the Stream API, lambda expressions can achieve higher efficiency (through parallel execution) for bulk operations on collections.
- static and default methods in interface
Before Java 8, interfaces were limited in functionality. They could only contain abstract methods, and every class implementing an interface was required to provide its own implementation for those methods. All methods were implicitly public and abstract, leaving no room for flexibility. Java 8 changed this by introducing default and static methods in interfaces. Default methods, in particular, were added to solve a long-standing problem: how to evolve an interface without breaking the classes that already implement it. With default methods, developers can now add new functionality directly to interfaces while still maintaining backward compatibility. This made interfaces far more powerful and flexible, especially when designing APIs.
Default method Example:-
In the below example, InterfaceEx interface has default method method1(), which is implemented in interface itself, no need to implement in the implementing classes.
interface InterfaceEx {
// This is a default method, no need to implement in the implementation classes.
default void method1(){
System.out.println("Java 8 default method");
}
// abstract method, need to implement in implementing classes
public void existingabstractMethod(String str);
}
public class JavaEx implements InterfaceEx {
// implementing abstract method
public void existingabstractMethod(String str){
System.out.println("Existing Java abstract method implementation");
}
public static void main(String[] args) {
JavaEx ex = new JavaEx();
//calling the default method of interface
ex.method1();
//calling the abstract method of interface
ex.existingabstractMethod("Java 8 features");
}
}
Output:--
Java 8 default method
Existing Java abstract method implementation
Static method in interfaces are similar to the default methods except that we cannot override these methods in the classes that implements these interfaces.
Static method example:--
interface InterfaceEx {
// This is a default method, no need to implement in the implementation classes.
default void method1(){
System.out.println("Java 8 default method");
}
static void staticMethod() {
System.out.println("Java 8 static method");
}
// abstract method, need to implement in implementing classes
public void existingabstractMethod(String str);
}
public class JavaEx implements InterfaceEx {
// implementing abstract method
public void existingabstractMethod(String str){
System.out.println("Existing Java abstract method implementation");
}
public static void main(String[] args) {
JavaEx ex = new JavaEx();
//calling the default method of interface
ex.method1();
// call static method using interface
InterfaceEx.staticMethod();
//calling the abstract method of interface
ex.existingabstractMethod("Java 8 features");
}
}
Output:--
Java 8 default method
Java 8 static method
Existing Java abstract method implementation.
The Stream API is used to process collections of objects. A stream is a sequence of elements that supports various methods, which can be pipelined to produce the desired result.
A stream does not store data and, in that sense, is not a data structure. It also never modifies the underlying data source.
In Java 8, a stream takes input from collections, arrays, or I/O channels and produces results based on the pipelined operations. Each intermediate operation is executed lazily and returns a stream, allowing multiple operations to be chained together. Terminal operations mark the end of the stream pipeline and produce the final result.
Different ways to build the Stream:--
1) Using Stream.of(val1, val2, val3….or arrayOfElements)
public class StreamApi {
public static void main(String[] args){
Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9);
stream.forEach(p -> System.out.println(p));
}
}
2) Using list.stream()
public class StreamEx {
public static void main(String[] args){
List<Integer> list = Arrays.asList(new Integer[]{2,3,4,6,7,9});
Stream<Integer> stream = list.stream();
stream.forEach(p -> System.out.println(p));
}
}
3) Using Stream.forEach() function
public class StreamEx {
public static void main(String[] args){
List<Integer> list = Arrays.asList(new Integer[]{2,3,4,6,7,9});
Stream<Integer> stream = list.stream();
stream.forEach(p -> System.out.println(p));
}
}
4) Using Stream.collect(Collectors.toList()) function - Convert Stream to List
public class StreamEx {
public static void main(String[] args){
List<Integer> list = Arrays.asList(new Integer[]{2,3,4,6,7,9});
Stream<Integer> stream = list.stream();
List<Integer> evenNumbersList = stream.filter(i -> i%2 ==
0).collect(Collectors.toList());
System.out.print(evenNumbersList);
}
}
5) Using Stream.toArray(EntryType[]::new) function - Convert Stream to array
public class StreamEx {
public static void main(String[] args){
List<Integer> list = Arrays.asList(new Integer[]{2,3,4,6,7,9});
Stream<Integer> stream = list.stream();
List<Integer> evenNumbersList = stream.filter(i -> i%2 ==
0).toArray(Integer[]::new);
System.out.print(evenNumbersList);
}
}
6) Intermediate filter() Operation
Filter accepts a predicate to filter all elements of the stream. This operation is intermediate which enables us to call another stream operation (e.g. forEach) on the result.
list.stream().filter((s) -> i%2 == 0)
.forEach(System.out::println);
Output:
2
4
7) Intermediate map() operation
The intermediate operation map converts each element into another object via the given function. The following example converts each string into an upper-cased string. But you can also use map to transform each object into another type.
names.stream().filter((s) -> s.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);
Output:
AMAR
ANKITA
8) Intermediate sorted() operation
Sorted is an intermediate operation which returns a sorted view of the stream.
names.stream().sorted()
.map(String::toUpperCase)
.forEach(System.out::println);
Output:
AMAR
KIRAN
A functional interface, introduced in Java 8, is an interface that has only a single abstract method. Conversely, any interface with exactly one abstract method is effectively a functional interface.
The @FunctionalInterface
annotation can be used explicitly to indicate that a given interface should be treated as a functional interface.
Example:-
@FunctionalInterface
interface FunctionalIntrface {
int operation(int a, int b);
}
If you try to add the second abstract method in the above code, will get compilation error.
@FunctionalInterface
interface FnInteface {
int operation(int a, int b);
String getEmplName(Long empId);
}
will get compilation error as,
Unexpected @FunctionalInterface annotation
@FunctionalInterface ^ FnInteface is not a functional interface
multiple non-overriding abstract methods found in interface FnInteface
Runnable is a great example of functional interface with single abstract method run().
Working with date, time, and time zones in Java has always been challenging. Prior to Java 8, there was no standard API for handling date and time consistently. One of the notable additions in Java 8 is the java.time
package, which streamlines working with time in Java.From the structure of the Java Time API, it is clear that it is designed to be easy to use. It includes sub-packages such as java.time.format
, which provides classes for printing and parsing dates and times, and java.time.zone
, which offers support for time zones and their rules.
The new Time API prefers enums over integer constants for months and days of the week, making code more readable and less error-prone. One particularly useful class is DateTimeFormatter
, which allows easy conversion between date-time objects and strings.
Reference :--
Java 8 Date – LocalDate, LocalDateTime, Instant
- Collection API Improvement
We have discussed the forEach()
method and the Stream API for collections. Several new methods have been added to the Collection API:
-
Iterator: Default method forEachRemaining(Consumer<? super E> action)
performs the given action for each remaining element until all elements have been processed or the action throws an exception.
-
Collection: Default method removeIf(Predicate<? super E> filter)
removes all elements of the collection that satisfy the given predicate.
-
Collection: Method spliterator()
returns a Spliterator
instance that can be used to traverse elements sequentially or in parallel.
-
Map: New methods replaceAll()
, compute()
, and merge()
provide more flexible operations on map entries.
-
HashMap: Performance improvements have been made for handling key collisions more efficiently.
- Concurrency API improvements
Some concurrent API enhancements in Java 8 are as,
- ConcurrentHashMap compute(), forEach(), forEachEntry(), forEachKey(), forEachValue(), merge(), reduce() and search() methods.
- CompletableFuture that may be explicitly completed (setting its value and status).
- Executors newWorkStealingPool() method to create a work-stealing thread pool using all available processors as its target parallelism level.
Some of IO improvements in Java 8 are:
- Files.list(Path dir) that returns a lazily populated Stream, the elements of which are the entries in the directory.
- Files.lines(Path path) that reads all lines from a file as a Stream.
- Files.find() that returns a Stream that is lazily populated with Path by searching for files in a file tree rooted at a given starting file.
- BufferedReader.lines() that return a Stream, the elements of which are lines read from this BufferedReader.
Thank you for visiting blog.
Related Posts:--