Introduction to Java 8 and Modern Java Language Features
Course: Core Java Training
Topic: Introduction to Java 8 and Modern Java Language Features
Course Link: Core Java Training
Demo Registration: Register for a Course Demo
Website: JustAcademy Official Website
1. Introduction to Java 8
Java 8 is a major release of the Java programming language and platform that introduced several features to make code more concise, readable, reusable, and efficient. Released in March 2014, Java 8 introduced functional programming capabilities while maintaining Java's object-oriented programming model.
Before Java 8, developers frequently used anonymous inner classes to implement interfaces, traditional loops to process collections, and older classes to handle dates and times. Java 8 introduced lambda expressions, the Stream API, functional interfaces, method references, default methods in interfaces, the Optional class, and the modern Date and Time API.
These features help developers express what a program should accomplish with less repetitive code. They are widely used in Java applications, enterprise development, data processing, and backend programming.
2. Learning Objectives
- Understand the importance of Java 8 in Java development.
- Identify the major language and library features introduced in Java 8.
- Understand lambda expressions and functional interfaces.
- Use method references to simplify code.
- Process collections with the Stream API.
- Understand default and static methods in interfaces.
- Handle potentially missing values with Optional.
- Work with dates and times using the java.time package.
- Explore collection enhancements and modern coding practices.
3. Why Was Java 8 Introduced?
Java has traditionally emphasized object-oriented programming, in which programs are organized around classes and objects. Although this approach remains important, some tasks involving collections, data filtering, sorting, and event handling can require substantial repetitive code.
Java 8 introduced functional programming features that allow developers to represent behavior more concisely and combine operations into readable pipelines.
Limitations of Older Java Code
- Anonymous inner classes often require lengthy implementations for simple operations.
- Processing collections with loops can create repetitive code.
- Interfaces could not provide ordinary default implementations before Java 8.
- Older date and time classes had usability and thread-safety limitations.
- Representing potentially absent values often relied on null checks.
How Java 8 Improves Development
| Older Approach |
Java 8 Improvement |
| Anonymous inner classes for simple behavior |
Lambda expressions |
| Long collection-processing loops |
Stream API operations |
| Repeated method-call lambda bodies |
Method references |
| Interfaces containing only abstract methods and constants |
Default and static methods in interfaces |
| Null values handled through repeated checks |
Optional for representing potentially absent values |
| Older date and time classes |
Modern java.time API |
4. Major Features Introduced in Java 8
Java 8 includes language improvements, new APIs, and enhancements to existing libraries.
| Feature |
Description |
Main Benefit |
| Lambda Expressions |
Represent behavior using compact expressions. |
Less repetitive code. |
| Functional Interfaces |
Interfaces with one abstract method. |
Provide target types for lambdas. |
| Method References |
Refer directly to existing methods. |
Improve readability. |
| Stream API |
Process sequences of data through operations. |
Declarative collection processing. |
| Default Methods |
Provide implementations inside interfaces. |
Extend interfaces compatibly. |
| Static Interface Methods |
Define utility methods inside interfaces. |
Keep related behavior together. |
| Optional |
Represent a value that may be absent. |
Make absence explicit. |
| Date and Time API |
Modern classes for dates, times, and time zones. |
Clearer date and time handling. |
| Collection Enhancements |
Methods such as forEach, removeIf, and replaceAll. |
Simplify common operations. |
| Repeating Annotations |
Apply the same annotation type multiple times. |
More flexible metadata. |
| Type Annotations |
Apply annotations to type usages. |
Support stronger type checking tools. |
| CompletableFuture |
Compose asynchronous computations. |
More flexible asynchronous programming. |
5. Understanding Lambda Expressions
A lambda expression is a concise way to represent an implementation of a functional interface. It allows behavior to be passed as an argument without creating a separate named class or writing a full anonymous inner class.
Lambda expressions are one of the most important features introduced in Java 8.
Basic Syntax
(parameters) -> expression
A lambda expression contains a parameter list, the arrow operator ->, and either an expression or a block of statements.
Example 1: A Simple Lambda Expression
public class LambdaExample {
public static void main(String[] args) {
Runnable task = () -> System.out.println("Hello Java 8!");
task.run();
}
}
Output:
Hello Java 8!
Explanation: The lambda expression provides the implementation of the run() method declared by the Runnable interface. Calling task.run() executes the supplied behavior on the current thread.
Example 2: Lambda with Parameters
public class LambdaAddition {
public static void main(String[] args) {
Calculator calculator = (a, b) -> a + b;
System.out.println(calculator.add(10, 20));
}
}
interface Calculator {
int add(int a, int b);
}
Output:
30
Explanation: The lambda expression implements the single abstract method of the Calculator interface and returns the sum of two integers.
Advantages of Lambda Expressions
- Reduce boilerplate code.
- Make short implementations easier to read.
- Support functional-style programming.
- Work naturally with collection methods and streams.
- Allow behavior to be passed to reusable methods.
6. Functional Interfaces
A functional interface is an interface that has exactly one abstract method. It can contain default methods and static methods in addition to that abstract method.
Functional interfaces provide the target types required by lambda expressions and method references.
Example
@FunctionalInterface
interface Greeting {
void sayHello();
}
public class FunctionalInterfaceExample {
public static void main(String[] args) {
Greeting greeting =
() -> System.out.println("Welcome to Java 8!");
greeting.sayHello();
}
}
Output:
Welcome to Java 8!
The @FunctionalInterface annotation is optional, but it helps the compiler detect accidental changes that would introduce another abstract method.
Common Built-in Functional Interfaces
| Interface |
Purpose |
Example |
Predicate<T> |
Tests a condition and returns a boolean. |
Check whether a number is positive. |
Function<T,R> |
Transforms a value of type T into type R. |
Convert a name to uppercase. |
Consumer<T> |
Accepts a value and performs an action without returning a result. |
Print a name. |
Supplier<T> |
Provides a value without accepting an input argument. |
Generate a message. |
UnaryOperator<T> |
Accepts and returns the same type. |
Increase an integer. |
BinaryOperator<T> |
Combines two values of the same type into one value of that type. |
Add two integers. |
Example Using Predicate
import java.util.function.Predicate;
public class PredicateExample {
public static void main(String[] args) {
Predicate isEven = number -> number % 2 == 0;
System.out.println(isEven.test(10));
System.out.println(isEven.test(7));
}
}
Output:
true
false
7. Method References
A method reference is a compact way to refer to an existing method when a lambda expression would simply call that method. The double-colon operator :: is used to create method references.
Example
import java.util.Arrays;
import java.util.List;
public class MethodReferenceExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Amit", "Neha", "Rahul");
names.forEach(System.out::println);
}
}
Output:
Amit
Neha
Rahul
Explanation: The expression System.out::println refers to the existing println method instead of writing a lambda that calls it for every name.
Types of Method References
| Type |
Example |
Meaning |
| Static method reference |
Integer::parseInt |
References a static method. |
| Instance method reference on a particular object |
System.out::println |
References a method on a specific object. |
| Instance method reference on an arbitrary object of a type |
String::toUpperCase |
References an instance method for an object supplied as an argument. |
| Constructor reference |
ArrayList::new |
References a constructor. |
8. Introduction to the Stream API
The Stream API allows developers to process sequences of elements using operations such as filtering, mapping, sorting, and reducing. It was introduced in Java 8 and works with collections and other data sources.
A stream does not normally store data itself. Instead, it processes elements from a source, such as a list or set, through a sequence of operations.
Example: Filtering Even Numbers
import java.util.Arrays;
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers =
Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
numbers.stream()
.filter(number -> number % 2 == 0)
.forEach(System.out::println);
}
}
Output:
2
4
6
8
Important Stream Operations
| Operation |
Purpose |
Example |
filter() |
Selects elements matching a condition. |
filter(n -> n > 5) |
map() |
Transforms each element. |
map(String::toUpperCase) |
sorted() |
Sorts elements. |
sorted() |
distinct() |
Removes duplicate elements. |
distinct() |
limit() |
Restricts the number of elements. |
limit(3) |
count() |
Counts elements. |
count() |
reduce() |
Combines elements into a result. |
reduce(0, Integer::sum) |
collect() |
Accumulates stream elements into a result container. |
collect(Collectors.toList()) |
Example: Filtering and Transforming Data
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamProcessingExample {
public static void main(String[] args) {
List<String> names =
Arrays.asList("amit", "neha", "arjun", "riya");
List<String> result = names.stream()
.filter(name -> name.startsWith("a"))
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(result);
}
}
Output:
[AMIT, ARJUN]
Explanation: The stream first filters names that start with the letter a, converts the matching names to uppercase, and collects the results into a list.
9. Intermediate and Terminal Stream Operations
Stream operations are commonly divided into intermediate and terminal operations.
Intermediate Operations
Intermediate operations return another stream and can be chained together. They are generally lazy, meaning their processing begins when a terminal operation is invoked.
Examples include:
filter()
map()
sorted()
distinct()
limit()
Terminal Operations
Terminal operations produce a result or a side effect and trigger the processing of the stream pipeline.
Examples include:
forEach()
collect()
count()
reduce()
anyMatch()
A stream should generally be consumed only once. If another processing pipeline is required, create a new stream from the source.
10. Default Methods in Interfaces
Before Java 8, interfaces could declare abstract methods but could not provide ordinary default implementations. Java 8 introduced default methods, which allow interfaces to include method implementations using the default keyword.
This feature helps library designers add functionality to existing interfaces while maintaining compatibility with existing implementations in many cases.
Example
interface Vehicle {
void start();
default void stop() {
System.out.println("Vehicle stopped.");
}
}
public class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car started.");
}
public static void main(String[] args) {
Car car = new Car();
car.start();
car.stop();
}
}
Output:
Car started.
Vehicle stopped.
Explanation: The Car class implements the abstract start() method but inherits the default implementation of stop().
11. Static Methods in Interfaces
Java 8 also allows interfaces to define static methods. These methods are associated with the interface itself and are called using the interface name.
Example
interface CalculatorUtility {
static int add(int a, int b) {
return a + b;
}
}
public class StaticInterfaceMethodExample {
public static void main(String[] args) {
int result = CalculatorUtility.add(10, 20);
System.out.println(result);
}
}
Output:
30
Explanation: The static method is called directly using CalculatorUtility.add(). It is not inherited as an instance method by implementing classes.
12. Understanding Optional
The Optional class, introduced in Java 8, represents a value that may or may not be present. It can make the possibility of a missing result explicit and reduce some accidental null-handling errors.
Optional is particularly useful for method return values when an expected result might be absent. It does not eliminate all null-related problems, and it should not be used automatically for every field or method parameter.
Example
import java.util.Optional;
public class OptionalExample {
public static void main(String[] args) {
Optional<String> name = Optional.of("Rahul");
System.out.println(name.orElse("Guest"));
Optional<String> emptyName = Optional.empty();
System.out.println(emptyName.orElse("Guest"));
}
}
Output:
Rahul
Guest
Common Optional Methods
| Method |
Purpose |
Optional.of(value) |
Creates an Optional containing a non-null value. |
Optional.ofNullable(value) |
Creates an Optional that may be empty if the supplied value is null. |
Optional.empty() |
Creates an empty Optional. |
isPresent() |
Checks whether a value is present. |
ifPresent(action) |
Runs an action when a value is present. |
orElse(defaultValue) |
Returns the value or a fallback value. |
orElseGet(supplier) |
Obtains a fallback value from a supplier when empty. |
map(function) |
Transforms a present value. |
13. Java 8 Date and Time API
Java 8 introduced the java.time package, which provides a more modern approach to handling dates, times, durations, and time zones.
Many classes in this API are immutable and thread-safe, making date and time operations easier to understand and maintain.
Important Classes
| Class |
Purpose |
Example |
LocalDate |
Represents a date without a time zone. |
2026-10-26 |
LocalTime |
Represents a time without a date or time zone. |
10:30:00 |
LocalDateTime |
Represents a date and time without a time zone. |
2026-10-26T10:30:00 |
ZonedDateTime |
Represents a date and time with a time zone. |
Date and time in Asia/Kolkata. |
Period |
Represents a date-based amount of time. |
Two months. |
Duration |
Represents a time-based amount of time. |
Two hours. |
DateTimeFormatter |
Formats and parses date and time values. |
26-10-2026 |
Example: Working with Dates and Times
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateTimeExample {
public static void main(String[] args) {
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("dd-MM-yyyy");
System.out.println(date.format(formatter));
System.out.println(time);
System.out.println(dateTime);
}
}
Explanation: The program obtains the current date, time, and date-time. The formatted date uses the pattern dd-MM-yyyy. Actual output depends on the machine's clock and environment.
14. Enhancements to the Collections Framework
Java 8 added several methods to existing collection interfaces and classes. These methods make it easier to perform operations on lists, sets, and maps.
Important Collection Methods
| Method |
Purpose |
forEach() |
Performs an action for each element. |
removeIf() |
Removes elements matching a predicate. |
replaceAll() |
Replaces list elements using a supplied operator. |
sort() |
Sorts a list using a comparator. |
getOrDefault() |
Returns a map value or a default when the key is absent. |
computeIfAbsent() |
Computes and stores a value when a key has no mapping. |
merge() |
Combines a supplied value with an existing map value. |
Example: Removing Elements from a List
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CollectionEnhancementExample {
public static void main(String[] args) {
List<Integer> numbers =
new ArrayList<>(Arrays.asList(10, 15, 20, 25, 30));
numbers.removeIf(number -> number < 20);
System.out.println(numbers);
}
}
Output:
[20, 25, 30]
15. Parallel Streams
Java 8 introduced support for parallel stream processing through the Stream API. A parallel stream can divide certain operations across multiple threads, potentially improving performance for suitable workloads.
Example
import java.util.Arrays;
import java.util.List;
public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> numbers =
Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.parallelStream()
.forEach(System.out::println);
}
}
Important: The output order is not guaranteed. Parallel streams are not automatically faster than sequential streams. Performance depends on data size, operation cost, workload characteristics, and thread coordination overhead.
When to Consider Parallel Streams
- Processing sufficiently large collections.
- Performing computationally intensive operations that can be divided safely.
- Using operations that do not rely on shared mutable state.
- Benchmarking shows a measurable improvement over sequential processing.
16. CompletableFuture and Asynchronous Programming
CompletableFuture was introduced in Java 8 to support asynchronous computations and the composition of dependent tasks. It allows developers to run work asynchronously, transform results, combine tasks, and handle errors.
Example
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> result =
future.thenApply(message -> message + " Java 8");
System.out.println(result.join());
}
}
Output:
Hello Java 8
Explanation: supplyAsync() starts an asynchronous computation, and thenApply() transforms its result. The join() method waits for the result and returns it when available.
17. Repeating Annotations and Type Annotations
Repeating Annotations
Java 8 allows the same annotation type to be applied multiple times to a declaration when that annotation is designed to support repetition using @Repeatable.
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Roles.class)
@interface Role {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@interface Roles {
Role[] value();
}
@Role("Admin")
@Role("Editor")
class UserAccount {
}
This example defines a repeatable annotation that can assign multiple role labels to the same class. The annotations themselves do not implement access control; application code must interpret them appropriately.
Type Annotations
Java 8 allows annotations to be applied in type-use positions, such as generic type arguments and type declarations. These annotations can support tools that perform additional checks on code.
import java.util.List;
class TypeAnnotationExample {
private List<@Deprecated String> names;
}
This is a simplified illustration of annotation placement. In production code, a custom annotation designed for type use is generally more useful than applying @Deprecated to a type argument.
18. Traditional Java vs Java 8: Practical Comparison
Traditional Anonymous Inner Class
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Task executed");
}
};
task.run();
Java 8 Lambda Expression
Runnable task = () -> System.out.println("Task executed");
task.run();
The lambda version expresses the same behavior with less code. This is especially useful when implementing simple functional interfaces.
Traditional Collection Processing
List<Integer> numbers =
Arrays.asList(1, 2, 3, 4, 5, 6);
for (Integer number : numbers) {
if (number % 2 == 0) {
System.out.println(number);
}
}
Java 8 Stream Processing
List<Integer> numbers =
Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream()
.filter(number -> number % 2 == 0)
.forEach(System.out::println);
Both approaches print even numbers. The Stream API separates the filtering rule from the action performed on each matching element.
19. Best Practices for Using Java 8 Features
- Use lambda expressions when they make behavior clearer than anonymous inner classes.
- Keep lambda bodies short and easy to understand.
- Use method references when they express the intended behavior more clearly.
- Build stream pipelines from small, meaningful operations.
- Avoid modifying shared mutable state inside stream operations.
- Do not assume parallel streams will always improve performance.
- Use Optional mainly to represent potentially absent return values.
- Prefer immutable date and time objects from
java.time for modern date handling.
- Use default methods thoughtfully when evolving interfaces.
- Test exception handling and boundary conditions.
- Check the required Java version before using language or library features.
20. Practical Mini Project: Student Marks Analyzer
Objective: Create a small Java application that processes student marks using Java 8 lambda expressions, streams, and method references.
Requirements
- Store student marks in a list.
- Display all marks.
- Filter marks greater than or equal to 60.
- Convert matching marks into a separate list.
- Calculate the number of passing marks.
- Calculate the average mark.
Sample Solution
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StudentMarksAnalyzer {
public static void main(String[] args) {
List<Integer> marks =
Arrays.asList(45, 78, 90, 55, 66, 82);
System.out.println("All marks:");
marks.forEach(System.out::println);
List<Integer> passingMarks = marks.stream()
.filter(mark -> mark >= 60)
.collect(Collectors.toList());
long passCount = passingMarks.size();
double average = marks.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
System.out.println("Passing marks: " + passingMarks);
System.out.println("Number of passes: " + passCount);
System.out.println("Average marks: " + average);
}
}
Expected Output
All marks:
45
78
90
55
66
82
Passing marks: [78, 90, 66, 82]
Number of passes: 4
Average marks: 69.33333333333333
Concepts Practiced
- List collections.
- Lambda expressions.
- Method references.
- Stream filtering.
- Collection of stream results.
- Primitive streams and average calculation.
- OptionalDouble fallback handling through
orElse().
21. Interview Questions and Answers
Q1. What is Java 8?
Java 8 is a major Java release that introduced features such as lambda expressions, the Stream API, functional interfaces, default methods, Optional, and the modern Date and Time API.
Q2. What is the main purpose of lambda expressions?
Lambda expressions provide a concise way to implement functional interfaces and pass behavior as an argument.
Q3. What is a functional interface?
A functional interface has exactly one abstract method. It may also contain default and static methods.
Q4. What is the difference between a lambda expression and a method reference?
A lambda expression defines behavior directly, while a method reference refers to an existing method that matches the required functional interface.
Q5. What is the Stream API?
The Stream API supports declarative processing of sequences of elements using operations such as filtering, mapping, sorting, and reduction.
Q6. What is the difference between intermediate and terminal operations?
Intermediate operations return another stream and are generally lazy. Terminal operations produce a result or side effect and trigger pipeline processing.
Q7. What is a default method in an interface?
A default method is an interface method with an implementation declared using the default keyword.
Q8. What is Optional in Java 8?
Optional represents a value that may or may not be present and provides methods for working with absent values more explicitly.
Q9. Why was the java.time package introduced?
It provides a modern API for representing and manipulating dates, times, durations, periods, and time zones.
Q10. What is a method reference?
A method reference uses the :: operator to refer to an existing method or constructor when a compatible functional interface is expected.
Q11. What is the difference between stream() and parallelStream()?
stream() creates a sequential stream, while parallelStream() creates a stream that can process operations in parallel. Parallel execution is not guaranteed to be faster.
Q12. What is CompletableFuture?
CompletableFuture supports asynchronous computations and allows developers to compose dependent tasks and handle their results.
Q13. Can an interface contain static methods in Java 8?
Yes. Java 8 allows interfaces to declare static methods, which are called using the interface name.
Q14. What is the purpose of Predicate?
Predicate<T> represents a condition that accepts an input and returns a boolean result. It is commonly used with stream filtering.
Q15. What is the advantage of Java 8 for collection processing?
Lambda expressions, streams, and enhanced collection methods reduce repetitive code and make data transformations easier to express.
22. Quick Revision Checklist
- Understand Java 8 and its major improvements.
- Learn lambda expression syntax and functional interfaces.
- Practice Predicate, Function, Consumer, and Supplier.
- Use method references with the
:: operator.
- Understand stream pipelines and common operations.
- Differentiate intermediate and terminal operations.
- Use default and static methods in interfaces.
- Represent potentially absent values with Optional where appropriate.
- Use LocalDate, LocalTime, and LocalDateTime.
- Practice collection enhancements and CompletableFuture.
- Understand the trade-offs of parallel streams.
- Verify Java version compatibility before compiling examples.
23. Conclusion
Java 8 introduced important language features and APIs that changed how developers write modern Java applications. Lambda expressions and functional interfaces make behavior easier to represent, while method references and the Stream API simplify collection processing.
Default methods help evolve interfaces, Optional supports clearer handling of absent values, and the java.time package provides a modern approach to date and time operations. CompletableFuture also enables developers to compose asynchronous tasks.
Mastering these features builds a strong foundation for modern Core Java programming and prepares developers to work with contemporary Java frameworks and backend applications.
24. Additional Learning Resources
To continue learning, explore the Core Java Training course at JustAcademy, visit the JustAcademy website, or register for a course demo.