Skip to main content

Java 8 Features with Examples: Lambdas, Streams, Optional, Default Methods and java.time

Every headline Java 8 feature with a short example you can run, the real output from a JDK 8, the compile errors you will meet, and the interview questions that come out of each one: lambdas, streams, Optional, default methods, java.time and functional interfaces.

Java 8 (March 2014) is the release that made Java a functional-style language as well as an object-oriented one. Five features carry almost all of it: lambda expressions (a short way to pass behaviour), streams (a pipeline for processing collections), Optional (a return type that says “there may be no value”), default methods (interfaces that can evolve without breaking implementers) and the java.time date and time API. Every example below was compiled and run on a real JDK 8, and the output you see is the output that JDK printed.
FeatureWhat it replacesOne-line example
Lambda expressionAnonymous inner classnames.sort((a, b) -> a.compareTo(b))
StreamHand-written loops that filter, map and collectwords.stream().distinct().map(String::toUpperCase).collect(toList())
OptionalReturning null and hoping the caller checksOptional.ofNullable(x).map(String::trim).orElse("none")
Default methodAbstract-only interfaces and helper classesdefault String greet() { ... }
java.timejava.util.Date and CalendarLocalDate.of(2026, 10, 20).plusMonths(1)
Tested against. Temurin JDK 1.8.0_504 for every demo and every transcript on this page. The runnable project is the java8-features module of the javademos repository; ./run.sh regenerates every output file quoted here. Nothing in it needs a newer JDK, because the point is to show what Java 8 itself can do.

The picture to keep in your head

Java 8 features fit together, which is why they arrived together. A lambda is the small piece of behaviour; a functional interface is the type that lets you hand it around; a stream is the machine that applies lambdas to data; Optional is how a method says “no result”; default methods are how the old collection interfaces gained stream() and forEach() without breaking every existing implementation.
A stream does nothing until the terminal operation runs source filter(…)lazy map(…)lazy collect(…)terminal: runs it all →→→ Dashed boxes are recipes, not work. One element at a time flows through the whole chain, so a short-circuiting terminal such as findFirst() can stop after the first match, as the output below shows.
The diagram is the whole stream model: intermediate operations only describe the pipeline, and the terminal operation pulls elements through it one at a time. Keep that in mind and most stream surprises stop being surprises.

Lambda expressions replace anonymous classes

Before Java 8, passing behaviour meant writing an anonymous inner class. A lambda says the same thing without the ceremony: a parameter list, an arrow, and a body. When the lambda only calls an existing method, a method reference (String::compareToIgnoreCase) is shorter still. The source is LambdaDemo.java:
names.sort((a, b) -> a.compareTo(b));               // lambda
names.sort(String::compareToIgnoreCase);              // method reference

int base = 10;
Function<Integer, Integer> addBase = x -> x + base;   // captures base
Real output from JDK 8, copied from 01-lambda.txt:
anonymous class : [Zoya, Meera, Ankur, Bhaskar]
lambda          : [Ankur, Bhaskar, Meera, Zoya]
method reference: [Ankur, Bhaskar, Meera, Zoya]
captured base   : 15
lambda this      : LambdaDemo
anonymous this   : LambdaDemo$2
The last two lines are the difference that matters in practice. Inside a lambda, this is the enclosing object (LambdaDemo); inside an anonymous class, this is the anonymous object itself (LambdaDemo$2). A lambda is not a smaller anonymous class; it has no identity of its own.
The capture rule, with the real compiler message. A lambda can read a local variable only if that variable is never reassigned. Mutate counter after the lambda captures it and JDK 8 refuses to compile (CaptureError.java, transcript in 07-compile-errors.txt):

error: local variables referenced from a lambda expression must be final or effectively final
Going deeper: functional interfaces and the four method-reference kinds
A lambda needs a target type, and that type must be a functional interface: an interface with exactly one abstract method. Runnable, Comparator and everything in java.util.function qualify. @FunctionalInterface is optional; it only makes the compiler check the rule for you. There are four kinds of method reference: static (Integer::parseInt), bound instance (prefix::startsWith), unbound instance (String::length) and constructor (ArrayList::new). They are worked through with output in the lambda expressions post, and the full java.util.function family is in the functional interfaces guide.

Streams process data as a pipeline

A stream is not a collection; it holds no data. It is a recipe for processing a source one element at a time. filter and map are intermediate operations (lazy); collect, count, findFirst and forEach are terminal operations (they run the pipeline). Source: StreamsDemo.java.
Stream<String> lazy = words.stream().filter(w -> { System.out.println("  filtering " + w); return w.length() > 5; });
System.out.println("pipeline built, nothing printed yet");
System.out.println("first match     : " + lazy.findFirst().get());
Output, from 02-streams.txt:
distinct+upper  : [STREAM, LAMBDA, OPTIONAL, DEFAULT, TIME]
sum of lengths  : 37
pipeline built, nothing printed yet
  filtering stream
first match     : stream
reuse           : stream has already been operated upon or closed
groupingBy      : {long=[stream, lambda, optional, default], short=[time]}
Read the middle of that output carefully. “pipeline built, nothing printed yet” appears before any filtering, because building a pipeline does no work. Then findFirst() runs it, and only one element (“stream”) is ever filtered, because the first element already matched and the pipeline stopped. The reuse line is the other classic: a stream can be consumed once, and a second terminal call throws IllegalStateException.
Going deeper: where to learn the rest of the Stream API
flatMap, reduce, the Collectors family, primitive streams and parallel streams each deserve their own page. Start with the complete Streams API guide, then the Collectors cookbook. For practice, the 50 Stream API coding interview questions post works through the problems interviewers actually ask.

Optional says “there may be no value” in the type

Optional<T> is a container that holds either one value or nothing. Its job is to make a method’s contract visible: a method returning Optional<String> tells the caller to handle absence, where a method returning String that sometimes returns null does not. The one trap worth memorising is orElse versus orElseGet. Source: OptionalDemo.java.
Optional.of("x").orElse(expensive());                  // expensive() runs even though a value is present
Optional.of("x").orElseGet(OptionalDemo::expensive);   // expensive() is not called
present         : VALUE-1
absent          : none
orElse (value present):
  expensive() called
orElseGet (value present):
get() on empty  : No value present
Optional.of(null): NullPointerException
That is 03-optional.txt. orElse takes a value, so Java evaluates its argument before the call; “expensive() called” prints even though the Optional was full. orElseGet takes a Supplier, so the fallback is computed only when needed. The last two lines show the other beginner traps: get() on an empty Optional throws, and Optional.of(null) throws; use ofNullable when the value might be null. The Optional complete guide covers when not to use it at all (fields and method parameters, mainly).

Default methods let interfaces grow without breaking implementers

Before Java 8, adding a method to an interface broke every class that implemented it. Default methods fix that: the interface supplies a body, and implementers inherit it unless they override. That is how Collection gained stream() and Iterable gained forEach() in the same release. The price is the “diamond”: if a class inherits two defaults with the same signature, it must pick. Source: DefaultMethodsDemo.java.
static class Duck implements Walker, Swimmer {
    public String move() { return Walker.super.move() + " + " + Swimmer.super.move(); }
}
diamond resolved: walk + swim
default + static: Hello, Ankur
Output from 04-default-methods.txt. Delete the override in Duck and the compiler stops you (DiamondError.java):
broken/DiamondError.java:5: error: class C inherits unrelated defaults for move() from types A and B
    static class C implements A, B { }
           ^
1 error
Going deeper: default methods versus abstract classes
A default method can call the interface’s other abstract methods (see greet() calling name() above), but an interface still cannot hold instance state. That is the dividing line from an abstract class. The post on default methods and evolving interfaces goes through the resolution rules, and abstract class vs interface compares the two designs.

java.time replaces Date and Calendar

The old java.util.Date is mutable, has zero-based months, and mixes a date with a time zone it does not really carry. java.time (JSR 310) is immutable, has separate types for a date, a time, an instant and a zoned moment, and separates Period (calendar units) from Duration (exact time). Source: TimeDemo.java.
plusMonths      : 2026-11-20  (original still 2026-10-20)
end of month    : 2026-02-28
days between    : 292
Period          : P6Y7M21D
Duration        : PT2H15M
same instant NY : 2026-10-20 13:05 EDT
old Date month  : new java.util.Date(126, 9, 20) -> month index 9 (October is 9)
From 05-java-time.txt. Three things to notice: plusMonths returns a new object and leaves the original alone; January 31 plus one month lands on February 28 rather than throwing or overflowing into March; and the same instant (22:35 in Mumbai) is 13:05 in New York, because withZoneSameInstant keeps the moment and changes the clock. The last line is the legacy trap: Date counts months from zero, so October is 9. The Date and Calendar to java.time migration guide covers converting existing code.

What else shipped in Java 8

The five features above are the ones interviews and day-to-day code lean on, but the JDK 8 feature list on openjdk.org has 55 JEPs. A few more worth knowing by name:
Other Java 8 JEPs worth knowing (with numbers)
  • JEP 126 — Lambda Expressions and Virtual Extension Methods (the lambda and default-method work).
  • JEP 107 — Bulk Data Operations for Collections (the stream API).
  • JEP 150 — Date & Time API.
  • JEP 155 — Concurrency Updates (this is where CompletableFuture belongs; see the CompletableFuture, ExecutorService and ForkJoinPool guide).
  • JEP 135 — Base64 Encoding & Decoding.
  • JEP 120 — Repeating Annotations.
  • JEP 103 — Parallel Array Sorting.
  • JEP 122 — Remove the Permanent Generation (class metadata moved to native memory, Metaspace).
  • JEP 174 — Nashorn JavaScript Engine (later removed from the JDK by JEP 372 in Java 15).
  • JEP 180 — Handle Frequent HashMap Collisions with Balanced Trees (explained in HashMap internal working in Java).

Common mistakes with Java 8 features

Reusing a stream
A stream is single-use. Calling a second terminal operation on it throws IllegalStateException: stream has already been operated upon or closed, as the output above shows. Keep the source collection and call stream() again, or use a Supplier<Stream>.
Writing orElse(expensiveCall())
The argument is evaluated whether or not the Optional is empty. Use orElseGet(() -> expensiveCall()) when the fallback costs anything (a database hit, an object construction, a log line you did not want).
Calling Optional.get() without checking
It throws NoSuchElementException: No value present on an empty Optional, which is the same failure as a null dereference with a different name. Prefer orElse, orElseGet, orElseThrow, map or ifPresent.
Mutating a captured local variable
The compiler rejects it (“must be final or effectively final”). The usual workarounds are a one-element array or an AtomicInteger, but if you reach for them, ask whether a stream reduce or collect says the same thing without hidden state.
Using parallelStream() as a free speed-up
Parallel streams run on the shared common ForkJoinPool. For small collections or blocking work (I/O, locks) they are often slower or starve other work. Measure before switching.
Still using Date for new code
Zero-based months and mutability make Date and Calendar bug-prone. Use java.time types, and convert at the edges only where a legacy API demands a Date.

Common interview questions on Java 8

What is a functional interface?
An interface with exactly one abstract method (default, static and Object methods do not count). It is the target type for lambdas and method references. @FunctionalInterface is optional and only makes the compiler enforce the rule.
How does a lambda differ from an anonymous class?
Inside a lambda, this is the enclosing instance; inside an anonymous class it is the anonymous object (the output above prints LambdaDemo versus LambdaDemo$2). A lambda also needs a functional-interface target type and does not introduce a new scope for names.
What is the difference between intermediate and terminal operations?
Intermediate operations (filter, map, sorted) return a new stream and are lazy. Terminal operations (collect, count, forEach, findFirst) produce a result or side effect and trigger the whole pipeline. Nothing runs until a terminal operation is called.
What is the difference between map and flatMap?
map turns each element into exactly one element. flatMap turns each element into a stream and flattens those streams into one, which is what you want for “list of lists” or “each order has many items”.
Why can a stream not be reused?
A stream is a one-shot view over a source. After a terminal operation it is marked consumed and any further use throws IllegalStateException.
orElse versus orElseGet?
orElse(x) always evaluates x; orElseGet(supplier) calls the supplier only when the Optional is empty. The output above shows it: with a value present, only orElse ran the expensive call.
What happens when a class inherits two default methods with the same signature?
It does not compile unless the class overrides the method. Inside the override, InterfaceName.super.method() picks a specific default, as Duck does above.
Why should Optional not be used as a field or method parameter?
It was designed as a return type. As a field it is not serializable and adds an object per field; as a parameter it forces callers to wrap values. Use overloads, or a plain nullable parameter documented as such.
Why is java.time better than Date?
Its types are immutable (thread-safe by construction), months are 1-based, and it separates date, time, instant and zone. plusMonths returns a new object, as the output above shows.
More practice questions on the collections side are in the Top 40 Java Collections interview questions, and on threading in the Top 40 Java Concurrency interview questions.

FAQ

What are the main features of Java 8?
Lambda expressions and method references, the Stream API, Optional, default and static methods in interfaces, the java.time date and time API, and CompletableFuture, plus smaller additions such as Base64 and repeating annotations.
What replaced PermGen in Java 8?
JEP 122 removed the permanent generation; class metadata now lives in native memory in a region called Metaspace.
Can I use var or records in Java 8?
No. var arrived in Java 10, and records, text blocks and sealed classes came later. See the Java 11 and Java 17 feature guides.
Is a lambda just syntax sugar for an anonymous class?
No. The behaviour of this differs, and the compiler does not generate a class file per lambda. The mechanism is shown in the lambda expressions post.
Should I still learn Java 8 features?
Yes. Later releases build on them (streams, Optional and lambdas appear in nearly every modern Java codebase), and interviews still ask about them first.
Should you stay on Java 8? Only if something forces you to. Every feature on this page still works on newer JDKs, and the later releases add records, switch expressions, virtual threads and more. The step up is documented in Java 11 features (and what changed from Java 8), Java 17 features, Java 21 features and the Java 21 to 25 LTS guide.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.