OptionalInt is a container that may or may not hold an int value. It's Java's way of explicitly representing the possibility of "no result" without using null or throwing exceptions — avoiding NullPointerException at the source.
It's part of a family of optional types in Java 8+:
| Class | Wraps |
|---|---|
OptionalInt | int |
OptionalLong | long |
OptionalDouble | double |
Optional<T> | Any object type |
The primitive-specific versions (OptionalInt, OptionalLong, OptionalDouble) exist for performance — they avoid boxing to wrapper types like Integer.
Core Methods
| Method | Description |
|---|---|
isPresent() | Returns true if a value is present |
getAsInt() | Returns the value; throws NoSuchElementException if empty |
orElse(int other) | Returns the value if present, otherwise other |
orElseGet(IntSupplier) | Returns the value if present, otherwise calls the supplier |
ifPresent(IntConsumer) | Runs an action if a value is present |
Basic Example
import java.util.Arrays;
import java.util.OptionalInt;
public class OptionalIntExample {
public static void main(String[] args) {
int[] numbers = {9, 10, 11, 12, 15, 25};
// reduce() returns OptionalInt because the array could be empty
OptionalInt first = Arrays.stream(numbers)
.reduce((left, right) -> left);
if (first.isPresent()) {
System.out.println("First element: " + first.getAsInt()); // 9
}
}
}
Using orElse() and orElseGet()
orElse() is cleaner than an if/else block for providing a default:
int[] numbers = {9, 10, 11, 12};
int[] empty = {};
OptionalInt result = Arrays.stream(numbers).filter(n -> n > 20).findFirst();
System.out.println(result.orElse(-1)); // -1 (no element > 20)
OptionalInt maxVal = Arrays.stream(empty).max();
System.out.println(maxVal.orElse(0)); // 0 (stream was empty)
OptionalInt from Stream Operations
Many IntStream terminal operations return OptionalInt because the stream may be empty:
int[] data = {3, 7, 2, 9, 4};
OptionalInt max = Arrays.stream(data).max();
OptionalInt min = Arrays.stream(data).min();
OptionalInt any = Arrays.stream(data).filter(n -> n > 5).findAny();
max.ifPresent(v -> System.out.println("Max: " + v)); // Max: 9
min.ifPresent(v -> System.out.println("Min: " + v)); // Min: 2
any.ifPresent(v -> System.out.println("Found: " + v)); // Found: 7
OptionalInt vs Optional<Integer>
You might wonder why not just use Optional<Integer>. The difference is boxing:
// OptionalInt — no boxing, primitive int stored directly
OptionalInt a = OptionalInt.of(42);
// Optional<Integer> — boxes int to Integer object
Optional<Integer> b = Optional.of(42);
For stream operations on int arrays and IntStream, use OptionalInt. For collections of Integer objects, use Optional<Integer>. Prefer OptionalInt whenever you're working with primitives to avoid unnecessary heap allocation.
isEmpty() — Java 11+
Java 11 added isEmpty() as the logical complement of isPresent():
OptionalInt result = Arrays.stream(new int[]{}).max();
if (result.isEmpty()) {
System.out.println("No values in stream"); // prints this
}
This is purely a readability improvement. result.isEmpty() is equivalent to !result.isPresent(). Use whichever reads more naturally in context — conditions like "if we got nothing, log a warning" read better with isEmpty().
stream() — Java 9+
OptionalInt.stream() returns an IntStream containing one value if present, or an empty stream if absent. This is useful for flatMapping:
int[] arrays = {3, 1, 4};
// Each OptionalInt gets converted to a 0-or-1-element stream
IntStream combined = Arrays.stream(arrays)
.filter(n -> n > 2)
.findFirst()
.stream(); // either stream of [3] or empty stream
combined.forEach(System.out::println); // 3
Common Pitfall: Calling getAsInt() Without Checking
The most frequent mistake with OptionalInt is calling getAsInt() unconditionally:
// WRONG — throws NoSuchElementException if stream is empty
int max = Arrays.stream(new int[]{}).max().getAsInt();
// CORRECT — always provide a fallback
int max = Arrays.stream(new int[]{}).max().orElse(Integer.MIN_VALUE);
// ALSO CORRECT — check first
OptionalInt maxOpt = Arrays.stream(new int[]{}).max();
if (maxOpt.isPresent()) {
System.out.println(maxOpt.getAsInt());
}
Only call getAsInt() when you have a guarantee the stream is non-empty, or after an isPresent() check.
When NOT to Use OptionalInt
While OptionalInt is useful as a method return type, avoid using it in these situations:
- As a method parameter — callers should pass an
intor use overloading; making callers wrap values inOptionalIntjust to pass them is awkward - As an instance field — use
nullor a sentinel value for nullable fields;OptionalIntfields add memory overhead with little benefit - Inside collections — a
List<OptionalInt>is almost always a design mistake; filter out missing values before collecting instead
OptionalInt is designed specifically for method return values where "no result" is a normal, expected outcome — like stream terminal operations on a potentially empty stream.
Summary
OptionalInt makes "no value" an explicit part of your API rather than something a caller has to guess at. Use orElse() for a concise default, isPresent() / isEmpty() for conditional logic, and avoid calling getAsInt() without a guard. Reserve it for method return values — not fields or parameters.
IntSummaryStatistics is a Java utility class that computes five statistics about a set of integers in a single pass: count, sum, min, max, and average. It's part of java.util and works naturally with Java 8 streams.
Instead of writing separate reductions for each statistic, summaryStatistics() gives you all five at once.
Getting IntSummaryStatistics from a Stream
import java.util.IntSummaryStatistics;
import java.util.stream.Stream;
public class IntSummaryStatisticsExample {
public static void main(String[] args) {
Stream<Integer> numStream = Stream.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numStream
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println("Count: " + stats.getCount()); // 5
System.out.println("Sum: " + stats.getSum()); // 15
System.out.println("Min: " + stats.getMin()); // 1
System.out.println("Max: " + stats.getMax()); // 5
System.out.println("Average: " + stats.getAverage()); // 3.0
}
}
Output:
Count: 5
Sum: 15
Min: 1
Max: 5
Average: 3.0
Adding More Values with accept()
IntSummaryStatistics is mutable — you can continue feeding it new values after the initial stream:
IntSummaryStatistics stats = Stream.of(1, 2, 3, 4, 5)
.mapToInt(Integer::intValue)
.summaryStatistics();
// Add a new value after the stream is consumed
stats.accept(10);
System.out.println("Count: " + stats.getCount()); // 6
System.out.println("Sum: " + stats.getSum()); // 25
System.out.println("Min: " + stats.getMin()); // 1
System.out.println("Max: " + stats.getMax()); // 10
System.out.println("Average: " + stats.getAverage()); // 4.166...
Using with an IntStream Directly
When you already have an IntStream (e.g., from an int[] array), you don't need mapToInt():
import java.util.Arrays;
int[] values = {10, 20, 30, 40, 50};
IntSummaryStatistics stats = Arrays.stream(values).summaryStatistics();
System.out.println(stats);
// IntSummaryStatistics{count=5, sum=150, min=10, average=30.000000, max=50}
Using collect() for Custom Aggregation
You can also use Collectors.summarizingInt() when collecting from an object stream:
import java.util.List;
import java.util.stream.Collectors;
List<String> words = List.of("apple", "fig", "banana", "kiwi");
IntSummaryStatistics lengthStats = words.stream()
.collect(Collectors.summarizingInt(String::length));
System.out.println("Shortest word length: " + lengthStats.getMin()); // 3
System.out.println("Longest word length: " + lengthStats.getMax()); // 6
System.out.println("Average word length: " + lengthStats.getAverage()); // 4.75
Available Methods
| Method | Return Type | Description |
|---|---|---|
getCount() | long | Number of values |
getSum() | long | Sum of all values |
getMin() | int | Minimum value |
getMax() | int | Maximum value |
getAverage() | double | Arithmetic mean |
accept(int) | void | Add a single value |
combine(other) | void | Merge another statistics object |
Merging Two Statistics Objects with combine()
combine() merges a second IntSummaryStatistics into the current one, updating all five fields atomically:
IntSummaryStatistics batch1 = Stream.of(1, 2, 3)
.mapToInt(Integer::intValue).summaryStatistics();
IntSummaryStatistics batch2 = Stream.of(4, 5, 6)
.mapToInt(Integer::intValue).summaryStatistics();
batch1.combine(batch2);
System.out.println("Count: " + batch1.getCount()); // 6
System.out.println("Sum: " + batch1.getSum()); // 21
System.out.println("Min: " + batch1.getMin()); // 1
System.out.println("Max: " + batch1.getMax()); // 6
System.out.println("Average: " + batch1.getAverage()); // 3.5
This is useful when you're processing data in batches — compute statistics per batch, then merge them all at the end.
Using with Parallel Streams
summaryStatistics() is safe to use with parallel streams. The stream framework handles merging partial results from each thread using the combine() method internally:
IntSummaryStatistics parallelStats = IntStream.range(1, 1_000_001)
.parallel()
.summaryStatistics();
System.out.println("Sum: " + parallelStats.getSum()); // 500000500000
System.out.println("Max: " + parallelStats.getMax()); // 1000000
You get the same result as a sequential stream — parallelism is handled transparently.
LongSummaryStatistics and DoubleSummaryStatistics
Java provides equivalent classes for the other primitive numeric types:
| Class | Stream type | getSum() returns |
|---|---|---|
IntSummaryStatistics | IntStream | long |
LongSummaryStatistics | LongStream | long |
DoubleSummaryStatistics | DoubleStream | double |
// LongSummaryStatistics — for large numbers that overflow int
LongSummaryStatistics longStats = LongStream.of(1_000_000L, 2_000_000L, 3_000_000L)
.summaryStatistics();
System.out.println("Sum: " + longStats.getSum()); // 6000000
// DoubleSummaryStatistics — for floating-point values
DoubleSummaryStatistics priceStats = DoubleStream.of(9.99, 24.50, 4.99)
.summaryStatistics();
System.out.println("Avg price: $" + priceStats.getAverage()); // $13.16
Note that IntSummaryStatistics.getSum() returns long even though the inputs are int — this prevents overflow when summing many large integers.
Real-World Example: Analyzing Order Totals
import java.util.*;
import java.util.stream.*;
List<Integer> orderTotals = Arrays.asList(
120, 45, 380, 95, 210, 67, 430, 28, 150, 300
);
IntSummaryStatistics stats = orderTotals.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println("Orders: " + stats.getCount());
System.out.println("Revenue: $" + stats.getSum());
System.out.println("Lowest: $" + stats.getMin());
System.out.println("Highest: $" + stats.getMax());
System.out.printf("Average: $%.2f%n", stats.getAverage());
// Orders: 10
// Revenue: $1825
// Lowest: $28
// Highest: $430
// Average: $182.50
Summary
IntSummaryStatistics is a clean, one-pass solution for computing common numeric statistics. Use it whenever you need more than one statistic from the same dataset — it's faster and cleaner than running separate stream operations. For large integers, use LongSummaryStatistics; for decimals, use DoubleSummaryStatistics.
computeIfPresent() is a Map method introduced in Java 8 that conditionally updates a value using a function — but only if the key already exists in the map. If the key is absent, nothing happens. It replaces the verbose containsKey() + put() pattern with a single, expressive call.
Method Signature
V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
- key — the key to look up
- remappingFunction — receives the key and current value; its return value becomes the new value
- Returns — the new value if key was present;
nullif absent or function returnednull - Side effect — if the function returns
null, the entry is removed from the map
Basic Example
import java.util.HashMap;
import java.util.Map;
public class ComputeIfPresentExample {
public static void main(String[] args) {
Map<String, Integer> prices = new HashMap<>();
prices.put("Sunglasses", 105);
prices.put("Watch", 1501);
prices.put("Wallet", 299);
System.out.println("Before: " + prices);
// "Watch" exists — price is doubled
prices.computeIfPresent("Watch", (key, value) -> value * 2);
// "Bag" doesn't exist — map is unchanged
prices.computeIfPresent("Bag", (key, value) -> value * 2);
System.out.println("After: " + prices);
// {Watch=3002, Sunglasses=105, Wallet=299}
}
}
Output:
Before: {Watch=1501, Sunglasses=105, Wallet=299}
After: {Watch=3002, Sunglasses=105, Wallet=299}
Removing an Entry by Returning null
If the remapping function returns null, the entry is deleted:
Map<String, Integer> stock = new HashMap<>();
stock.put("Apples", 10);
stock.put("Bananas", 5);
stock.put("Cherries", 20);
// Remove items with stock below 8
stock.computeIfPresent("Apples", (k, v) -> v < 8 ? null : v); // 10 >= 8, kept
stock.computeIfPresent("Bananas", (k, v) -> v < 8 ? null : v); // 5 < 8, removed
stock.computeIfPresent("Cherries", (k, v) -> v < 8 ? null : v); // 20 >= 8, kept
System.out.println(stock);
// {Apples=10, Cherries=20}
Before vs. After Java 8
Old approach — three lines, two map lookups:
if (prices.containsKey("Watch")) {
prices.put("Watch", prices.get("Watch") * 2);
}
Modern approach — one line, atomic:
prices.computeIfPresent("Watch", (k, v) -> v * 2);
Beyond being cleaner, the modern approach is also atomic on ConcurrentHashMap, which makes it correct under concurrent access — the old pattern is not.
Real-World Example: Updating a Frequency Map
Map<String, Integer> wordCount = new HashMap<>();
wordCount.put("java", 5);
wordCount.put("python", 3);
wordCount.put("kotlin", 7);
String[] wordsToUpdate = {"java", "rust", "python", "go"};
for (String word : wordsToUpdate) {
wordCount.computeIfPresent(word, (k, v) -> v + 1);
}
System.out.println(wordCount);
// {java=6, python=4, kotlin=7} — "rust" and "go" were NOT added
Real-World Example: Applying a Selective Discount
Map<String, Double> cart = new HashMap<>();
cart.put("Laptop", 999.99);
cart.put("Mouse", 29.99);
cart.put("Keyboard", 79.99);
cart.put("Monitor", 349.99);
List<String> saleItems = List.of("Mouse", "Keyboard", "Headphones");
for (String item : saleItems) {
// Apply 20% discount — "Headphones" not in cart, skipped
cart.computeIfPresent(item, (k, v) -> Math.round(v * 0.8 * 100.0) / 100.0);
}
cart.forEach((item, price) ->
System.out.printf("%-12s $%.2f%n", item, price));
// Laptop $999.99
// Mouse $23.99
// Keyboard $63.99
// Monitor $349.99
Thread-Safe Use with ConcurrentHashMap
computeIfPresent() is atomic on ConcurrentHashMap. The check and the update happen as a single operation — no other thread can modify the entry between them:
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("counter", 0);
// Safe to call from multiple threads concurrently
concurrentMap.computeIfPresent("counter", (k, v) -> v + 1);
The old containsKey() + put() pattern is not thread-safe — another thread can insert or remove the key between your two calls.
Related Map Compute Methods
| Method | When to Use |
|---|---|
computeIfPresent(k, fn) | Update an existing value; do nothing if key absent |
computeIfAbsent(k, fn) | Insert a new value; do nothing if key present |
compute(k, fn) | Always call fn; fn receives null if key absent |
merge(k, v, fn) | Combine a new value with an existing one, or insert if absent |
put(k, v) | Always set a value, regardless of whether key exists |
putIfAbsent(k, v) | Insert only if key absent; fixed value (no function) |
Summary
Use computeIfPresent() when you need to update an existing map entry based on its current value, without the boilerplate of a containsKey() check. It's cleaner, more expressive, and thread-safe when used with ConcurrentHashMap.
Before Java 8, interfaces were pure contracts — nothing but method signatures and constants. Adding a new method to a published interface was a nightmare: every single class that implemented it would break. Java 8 changed this with default methods — interface methods that have an actual implementation. You've been using them all along, perhaps without realizing it: List.forEach(), Collection.stream(), and Map.getOrDefault() are all default methods added to existing interfaces in Java 8.
List.sort() in Java 8. I thought, "Wait, List is an interface – how can it have a method with an implementation?" That's when I discovered default methods. I was working on a large codebase with dozens of custom collection implementations, and the Java team's ability to add stream() and forEach() without breaking any of our code blew my mind. This was one of those features that made me appreciate the thought that goes into language design.
Why This Feature Still Matters – A Personal Story
In 2016, I was maintaining a library that defined a Processor interface. Hundreds of projects used it. I wanted to add a processWithRetry() method – a common pattern in our codebase – but adding it to the interface would break every implementation. Without default methods, I would have had to create a whole new abstract class hierarchy or use a wrapper pattern. Both options were messy.
Thanks to default methods, I added the method with a default implementation (a simple retry loop) and every existing implementation automatically got the new capability. I didn't break anyone's code, and they could override it if they needed custom retry logic. This single feature saved us months of migration work.
The Problem They Solved
Imagine you maintain the java.util.Collection interface, used by thousands of classes across millions of projects. In 2014, Java wanted to add lambda support. That meant adding methods like forEach and stream() to Collection. With the old rules, this was impossible — adding any method to an interface was a breaking change.
Default methods solved this: the Java team added forEach() with a default implementation, so every existing Collection implementation instantly gained the method without any code changes. Backward compatibility maintained.
Syntax
Use the default keyword in front of the method definition inside an interface:
interface Greeter {
// Abstract method — must be implemented by classes
String getName();
// Default method — has implementation, can be overridden
default void greet() {
System.out.println("Hello, " + getName() + "!");
}
// Another default method that calls the first
default void greetFormally() {
System.out.println("Good day, " + getName() + ". How do you do?");
}
}
class FriendlyPerson implements Greeter {
@Override
public String getName() { return "Alice"; }
// Uses default greet() — no override needed
// But overrides greetFormally() for a custom version
@Override
public void greetFormally() {
System.out.println("Hey hey, " + getName() + "!");
}
}
FriendlyPerson person = new FriendlyPerson();
person.greet(); // Hello, Alice!
person.greetFormally(); // Hey hey, Alice!
A Real‑World Example
Here's a pattern common in real APIs — a core operation that subclasses must implement, wrapped in default methods that add useful behaviour:
interface Shape {
double area(); // Implementing class must provide this
default String describe() {
return String.format("This shape has an area of %.2f sq units", area());
}
default boolean isLargerThan(Shape other) {
return this.area() > other.area();
}
}
class Circle implements Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
@Override
public double area() { return Math.PI * radius * radius; }
}
class Square implements Shape {
private final double side;
Square(double side) { this.side = side; }
@Override
public double area() { return side * side; }
}
Circle c = new Circle(5);
Square s = new Square(8);
System.out.println(c.describe()); // This shape has an area of 78.54 sq units
System.out.println(c.isLargerThan(s)); // false (78.54 < 64)
calculatePrice() method and default methods for applyDiscount(), isEligibleForFreeShipping(), and getCurrency(). The implementers only had to write the core logic; everything else was derived. This drastically reduced code duplication across dozens of pricing strategies.
The Diamond Problem — When Two Interfaces Conflict
If a class implements two interfaces that both provide a default method with the same signature, Java forces you to resolve the conflict explicitly:
interface A {
default String hello() { return "Hello from A"; }
}
interface B {
default String hello() { return "Hello from B"; }
}
// Compile error if you don't override: class C inherits unrelated defaults
class C implements A, B {
@Override
public String hello() {
// Must choose one, or provide your own implementation
return A.super.hello(); // explicitly delegate to A's version
}
}
System.out.println(new C().hello()); // Hello from A
The special syntax InterfaceName.super.methodName() lets you explicitly call a specific interface's default method from the overriding class.
Comparable and Iterable (this was a custom collection). When Java 8 added forEach to Iterable and stream to Collection, I suddenly had conflicting defaults. The compiler error led me to this syntax. I now always use Interface.super.method() to explicitly disambiguate – it makes the code clearer for the next person.
All Interface Method Types in Java 8+
| Method Type | Keyword | Has body? | Can be overridden? | Called on |
|---|---|---|---|---|
| Abstract | (none) | ❌ No | Must be implemented | Instance |
| Default | default |
✅ Yes | ✅ Yes, optionally | Instance |
| Static | static |
✅ Yes | ❌ No (not inherited) | Interface name |
| Private (Java 9+) | private |
✅ Yes | ❌ No | Within interface only |
interface MathOps {
// Static — called as MathOps.square(5)
static int square(int n) { return n * n; }
// Default — inherited by implementing classes
default int cube(int n) { return n * n * n; }
// Private (Java 9+) — shared helper for default methods
private int pow(int base, int exp) {
int result = 1;
for (int i = 0; i < exp; i++) result *= base;
return result;
}
}
System.out.println(MathOps.square(4)); // 16 — called on interface directly
private methods in interfaces to share code between default methods. Before that, I had to duplicate logic. It makes interfaces much more maintainable when they have multiple default methods that share common logic.
When to Use Default Methods
- Evolving an existing interface — add new methods without breaking all existing implementors. This is the primary reason they were introduced.
-
Providing useful derived operations —
isLargerThan()anddescribe()in the Shape example above are derived fromarea()and save every implementing class from repeating the same logic. - Mixin‑style behaviour — composing behaviour from multiple interfaces, each contributing default methods. This is like multiple inheritance of behaviour.
-
Adding convenience methods — like
List.sort()in Java 8. It could have been a static utility method, but as a default method it's more discoverable.
When NOT to Use Default Methods
- When the method needs to maintain state – default methods can't hold instance fields. They only have access to other abstract methods and parameters. If you need state, use an abstract class.
- When there's no reasonable default implementation – if most implementers would override it, it's probably better as an abstract method.
- When you're just trying to avoid abstract class design – default methods are not a replacement for abstract classes; they serve different purposes.
Common Pitfalls I've Seen (and Made)
- Using default methods to add behaviour that depends on mutable state: Default methods can only access other methods in the interface. If you need to store data, you're out of luck.
- Assuming default methods are virtual: They are – they can be overridden. But if you're calling a default method from another default method, and it's overridden, the override will be called. This is expected, but I've seen confusion about this behaviour.
- Forgetting to resolve diamond conflicts: The compiler will catch this, but I've seen developers surprised by the error. The fix is simple – just override and delegate.
- Using default methods as a substitute for abstract class design: I've seen code where an interface had dozens of default methods, essentially becoming a de‑facto abstract class. This is a sign you might want to use an abstract class instead.
- Not documenting default method behaviour: Since the implementation is hidden, it's important to document what the default does and when implementers might want to override it.
How I Use Default Methods in Practice
I've developed a set of guidelines for when I use default methods:
- Always start with abstract methods. The core contract should be abstract. Default methods are for added convenience, not the core behaviour.
- Derive from the abstract methods. A default method should be implementable entirely by calling other abstract methods. This ensures that any implementer gets the behaviour for free.
-
Document override points. If I expect implementers to override a default method, I add a
@implNoteJavadoc to explain why and when. -
Use static methods for utilities. If a method doesn't need instance data and isn't designed to be overridden, I make it
static. -
Use private methods for shared logic. In Java 9+, I extract common code between default methods into
privatemethods to keep things DRY.
Default Methods vs Abstract Classes – A Decision Guide
| Feature | Abstract Class | Interface with Default Methods |
|---|---|---|
| State (instance fields) | ✅ Yes | ❌ No |
| Multiple inheritance | ❌ No (only one superclass) | ✅ Yes (multiple interfaces) |
| Constructors | ✅ Yes | ❌ No |
| Access modifiers | All | public (or private in Java 9+) |
| Best for | Shared state and behaviour across a class hierarchy | Defining contracts with optional convenience methods |
What This Feature Taught Me About Design
Default methods changed how I think about API design. Before Java 8, I was very careful about adding methods to interfaces – it was a one‑way door. With default methods, I have more freedom to evolve interfaces over time. I can start with a minimal contract and add convenience methods later without breaking existing code.
This has made me more willing to release APIs early and iterate. I now think of interfaces as "living documents" that can grow as I understand more about how they're used. Default methods are the tool that makes this possible.
Summary
Default methods let interfaces ship with ready‑to‑use implementations. They were introduced in Java 8 primarily to allow backward‑compatible evolution of the Java standard library — adding forEach, stream, and other methods to existing collection interfaces without breaking the world.
Key takeaways:
- Implementing classes inherit default methods automatically but can override them.
- When two interfaces provide the same default method, the implementing class must resolve the conflict with an explicit override using
Interface.super.method(). - Use default methods to share common derived behaviour across implementations, and to evolve interfaces without breaking backward compatibility.
- Use static methods in interfaces for utility functions that don't need to be overridden.
- Use private methods (Java 9+) to share code between default methods within an interface.
- Don't use default methods as a substitute for abstract classes – if you need state, use an abstract class.
This is one of those features that makes Java a pleasure to work with. It strikes the right balance between evolution and stability – and it's saved my team countless hours of migration work. Next time you add a method to an interface, think about whether it could be a default method – your users will thank you.
Happy coding – and may your interfaces always evolve gracefully!
Java Files.walk examples
List all the folders
public List getFolders(String path) throws IOException {
try (Stream walk = Files.walk(Paths.get(path))) {
List result = walk.filter(Files::isDirectory)
.map(x -> x.getFileName().toString())
.collect(Collectors.toList());
result.forEach(System.out::println);
return result;
}
}
List all files
public List getFiles(String path) throws IOException {
try (Stream walk = Files.walk(Paths.get(path))) {
List result = walk.filter(Files::isRegularFile)
.map(x -> x.getFileName().toString())
.collect(Collectors.toList());
result.forEach(System.out::println);
return result;
}
}
Files and Directories
public List getFoldersAndFiles(String path) throws IOException {
try (Stream walk = Files.walk(Paths.get(path))) {
List result = walk.map(x -> {
if (Files.isDirectory(x)) {
return x.getFileName().toString() + ":";
} else {
return x.getFileName().toString();
}
}).collect(Collectors.toList());
result.forEach(System.out::println);
return result;
}
}