Java 8 Key Features ( Lambdas, Steams API Default Methods in interfaces , Optional )


"Java has always been about delivering performance, security, and portability. With the innovations over the past few years, including modularity and better performance enhancements, Java continues to evolve and stay relevant in the rapidly changing software landscape." — James Gosling
Overview
🌐 Java Key Features 2014–2023👇🏻
├── 📦 Java 8 (2014)
│ ├── 🔄 Lambdas
│ ├── 🌊 Streams API
│ ├── 🕒 Date and Time API
│ └── 🔧 Default Methods in Interfaces
│ └── 🔹 Optional
Explanation of Lambdas in Java 8 (2014):
Java 8 came in hot with Lambdas, and let’s just say… it was a game changer. Before Lambdas, we had to write a lot of boilerplate code—like that one friend who always brings their entire suitcase on a weekend trip when they only need a toothbrush. But now, with Lambdas, it’s like a magic wand that shrinks all that extra baggage into a neat, compact form.
Lambdas let us pass around behavior as if it were a hot potato—super simple and super clean. Need to do something to a list of numbers? Just pass a Lambda to a stream and BOOM, it’s done. It's like going from a clunky old sedan to a slick sports car in terms of code style.
And guess what? They work with functional interfaces, those cool single-method interfaces that don't need to put on airs. Instead of writing verbose anonymous classes, you can just pass a Lambda, and it’s like “here’s my method, deal with it!” Simple, clear, and pretty darn elegant.
So in short: if you’re still writing out verbose anonymous classes in your Java code, it’s time to upgrade. Lambdas are here, and they’re all about efficiency and style.
Lambdas represent a function or method that can be used like an object. They allow you to:
Eliminate boilerplate code (like anonymous classes).
Pass behavior as a parameter to methods.
Write cleaner, more readable code.
Syntax And Examples
(parameters) -> expression
parameters: The input parameters of the function (can be zero or more).
->: The lambda operator, separating parameters from the body.
expression: The body of the lambda, where the logic happens.
Example 1: Without Lambdas (Using an Anonymous Class)
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = Arrays.asList("iheb", "Mohamed", "Aymen");
// Traditional anonymous class for iteration
names.forEach(new Consumer<String>() {
@Override
public void accept(String name) {
System.out.println(name);
}
});
}
}
Example 1 : With Lambdas
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = Arrays.asList("iheb", "Mohamed", "Aymen");
// Using lambda expression for iteration
names.forEach(name -> System.out.println(name));
}
}
In this version:
name -> System.out.println(name)
is a lambda expression.It takes one parameter (
name
) and runs theSystem.out.println(name)
statement.It replaces the need for an entire anonymous class.
Example 2 : Sorting with Lambdas
Lambdas also make sorting easier. Before Java 8, you would write :
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = Arrays.asList("iheb", "Mohamed", "Aymen");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
System.out.println(names);
}
}
With lambdas, you can simplify it to:
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = Arrays.asList("iheb", "Mohamed", "Aymen");
// Sorting with lambda
Collections.sort(names, (a, b) -> a.compareTo(b));
System.out.println(names);
}
}
Lambdas in Java 8 allow for more expressive and less verbose code, especially when working with functional interfaces. They simplify operations like sorting, iterating, and performing transformations on collections, making your code more modern and readable.
Explanation of Java Streams API
Oh, the Streams API in Java 8? Let’s just say it’s like upgrading from the Flintstones car to a Tesla. 🚗💨 Before Streams, we had to manually loop through collections, checking each item, deciding what to do, and pretending we weren't exhausted by the end of it. It was like eating a whole pizza... in one sitting... with no napkins.
Then came Streams, and suddenly we’re doing data manipulation with style and grace—like a ninja slicing through tasks with a katana. Need to filter, map, or sort? Just flow through the data with a few simple method calls, and voila! It’s clean, it’s declarative, and it feels like you’ve become one with the code. No more clunky for-loops or trying to remember which index you’re on—Streams let you focus on what you want to do, not how you do it.
It’s like switching from trying to juggle flaming torches to snapping your fingers and having everything fall into place perfectly. You can easily chain operations and handle big data like you’re working with a gentle stream, not a raging river.
So yeah, if you’re still looping manually, it’s time to take a dip in the cool, clean waters of the Streams API. It’ll make your life easier and your code look like poetry.
With Streams, you can:
Process data in a pipeline using functional-style operations.
Avoid writing complex loops and iterators.
Achieve parallel processing with minimal effort.
Syntax And Examples
Here’s how you can use the Streams API to work with a List of numbers, filtering, transforming, and collecting the results.
import java.util.*;
import java.util.stream.*;
public class StreamsExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Stream pipeline: filter even numbers, multiply by 2, and collect into a list
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // filter even numbers
.map(n -> n * 2) // multiply by 2
.collect(Collectors.toList()); // collect into a list
System.out.println(result); // Output: [4, 8, 12, 16, 20]
}
}
Explanation:
numbers.stream
()
: Converts the list into a Stream.filter(n -> n % 2 == 0)
: Filters the numbers, keeping only even numbers.map(n -> n * 2)
: Multiplies each remaining number by 2.collect(Collectors.toList())
: Collects the final Stream into a List.
Example 2: Using forEach
(Terminal Operation)
You can also use Streams to iterate over elements using forEach
.
import java.util.*;
import java.util.stream.*;
public class StreamsExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Use forEach to print each name
names.stream()
.forEach(name -> System.out.println(name));
}
}
Explanation:
forEach(name -> System.out.println(name))
: This terminal operation prints each name in the list.
Example 3: Using reduce
(Terminal Operation)
The reduce
operation allows you to combine elements of a Stream into a single result, such as calculating the sum or product of a list of numbers.
import java.util.*;
import java.util.stream.*;
public class StreamsExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Calculate the sum using reduce
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum); // Output: Sum: 15
}
}
Explanation:
reduce(0, (a, b) -> a + b)
: Thereduce
method combines all the elements into a single result, starting from 0 and applying the lambda(a, b) -> a + b
to add the elements.
Example 4: Parallel Stream
import java.util.*;
import java.util.stream.*;
public class StreamsExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Using parallelStream for parallel processing
List<Integer> result = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.collect(Collectors.toList());
System.out.println(result); // Output: [4, 8, 12, 16, 20]
}
}
Explanation:
parallelStream()
: Converts the list into a parallel stream, which processes the elements concurrently, improving performance for large datasets.
Explanation Optional
Ah, Optional—Java’s little safety net. It’s like that friend who always brings a backup charger when you forget yours. 😅
Before Optional, you'd often find yourself dealing with null values like a minefield: one wrong step, and boom, NullPointerException strikes! But then Optional swooped in like a superhero, offering a much safer and clearer way to handle the possibility that a value might not be there. It’s like saying, “Hey, I might have a value for you… or maybe not. But don’t worry, I’m going to let you know either way.”
Instead of returning null
and hoping the calling code doesn't trip over it, Optional is like, “I’m here, I’m holding your value—or not! But I’ll make it crystal clear so you don’t get blindsided.” You get methods like isPresent()
, ifPresent()
, or orElse()
—it’s like a toolbox for handling emptiness, making your code safer and more expressive.
So if you’re still tossing around null
like it’s nobody’s business, Optional is here to give you a much more elegant, error-free way to handle missing values. It's like checking your pockets before you leave the house: you’ll know exactly where you stand!
Optional<Type> optionalObject = Optional.of(value); // value is present
Optional<Type> optionalObject = Optional.empty(); // value is absent
Optional<Type> optionalObject = Optional.ofNullable(value); // value may or may not be present
Common Methods:
isPresent()
: Returnstrue
if a value is present, otherwisefalse
.ifPresent(Consumer<T> action)
: Executes the given action if a value is present.orElse(T other)
: Returns the value if present, otherwise returns the other value.orElseGet(Supplier<? extends T> other)
: Returns the value if present, otherwise invokes theSupplier
to return a value.
Example 1
import java.util.Optional;
public class OptionalExample {
public static void main(String[] args) {
// Example with Optional.of
Optional<String> name = Optional.of("John");
// Example with Optional.empty (no value)
Optional<String> emptyName = Optional.empty();
// Check if value is present and print it
name.ifPresent(n -> System.out.println("Name: " + n)); // Output: Name: John
// Use orElse() to provide default value when absent
String defaultName = emptyName.orElse("Default Name");
System.out.println("Name: " + defaultName); // Output: Name: Default Name
}
Example 2 :
import java.util.Optional;
public class OptionalExample2 {
// Method that may return null
public static String getUserEmail(String userId) {
if ("123".equals(userId)) {
return "user@example.com"; // Return an email for userId "123"
} else {
return null; // Return null for other userIds
}
}
public static void main(String[] args) {
// Using Optional.ofNullable to wrap the result of getUserEmail method
Optional<String> email = Optional.ofNullable(getUserEmail("123"));
// Check if the email is present and print it
email.ifPresent(e -> System.out.println("Email: " + e)); // Output: Email: user@example.com
// Using orElse to provide a default email if the result is null
String defaultEmail = Optional.ofNullable(getUserEmail("999")).orElse("default@example.com");
System.out.println("Email: " + defaultEmail); // Output: Email: default@example.com
// Using map to transform the value if present
Optional<String> upperCaseEmail = email.map(String::toUpperCase);
upperCaseEmail.ifPresent(e -> System.out.println("Uppercase Email: " + e)); // Output: Uppercase Email: USER@EXAMPLE.COM
// Using orElseThrow to throw an exception if the value is absent
try {
String userEmail = Optional.ofNullable(getUserEmail("999"))
.orElseThrow(() -> new IllegalArgumentException("Email not found"));
} catch (Exception e) {
System.out.println(e.getMessage()); // Output: Email not found
}
}
}
Explanation for exp2 :
getUserEmail(): This method returns an email for a specific userId, or
null
if the user is not found.Optional.ofNullable(): This wraps the result of
getUserEmail()
in anOptional
. If the result isnull
, an emptyOptional
is created.ifPresent(): This method checks if the value is present. If it's present, it executes the lambda expression to print the email.
orElse(): If the value inside the
Optional
isnull
, it returns a default value ("default@example.com" in this case).map(): If the value is present, it transforms it. In this case, it converts the email to uppercase.
orElseThrow(): If the
Optional
is empty, this method throws an exception. This is useful when you want to handle the absence of a value more explicitly.
Output :
Email: user@example.com
Email: default@example.com
Uppercase Email: USER@EXAMPLE.COM
Email not found
Default Methods in Interfaces
Ah and now, Default Methods—Java 8’s way of saying, “Hey, you can have your cake and eat it too.” 🍰 Before these magical little creatures arrived, interfaces were basically all talk and no action—only abstract methods, no implementation, just a promise of what could be, but never quite there. It was like hiring a contractor to build your house and they just handed you a blueprint with no tools.
Then Java 8 came along, and boom, Default Methods dropped in to give interfaces a new set of tools. Now, interfaces could have methods with actual code inside them—like, oh wow, that’s new! This means you can add new methods to interfaces without breaking any existing classes that implement them. It’s like slipping a shiny new gadget into your toolkit without accidentally blowing up your old stuff. 🚀
Imagine you’re building an interface for a bunch of classes, and one day you realize you need to add a method. But oh no—if you add a new abstract method, all the existing classes will throw a tantrum. Well, Default Methods are the calm, cool answer to that problem. You give the method a default implementation, and all your old classes are like, "Thanks, I’ll keep doing my thing, but if you ever need me to use this new method, I’m all set!"
So, thanks to Default Methods, interfaces got a whole lot smarter. It's like upgrading from a "no frills" rental car to one with all the bells and whistles, while still keeping your old, trusty ride.
Syntax And Examples
interface InterfaceName {
default void defaultMethod() {
// Default implementation
}
}
interface MyInterface {
// Default method
default void greet() {
System.out.println("Hello from default method!");
}
// Abstract method (must be implemented by classes)
void sayGoodbye();
}
class MyClass implements MyInterface {
@Override
public void sayGoodbye() {
System.out.println("Goodbye from implemented method!");
}
}
public class DefaultMethodExample {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.greet(); // Output: Hello from default method!
myClass.sayGoodbye(); // Output: Goodbye from implemented method!
}
}
Explanation of the Example :
MyInterface
defines agreet
method as a default method.The class
MyClass
implementsMyInterface
and provides its own implementation for the abstract methodsayGoodbye
.When we create an instance of
MyClass
, we can call the default methodgreet()
directly without overriding it, and we must implement the abstract methodsayGoodbye()
.
Subscribe to my newsletter
Read articles from iheb ben Temessek directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
