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.
computeifabsent Example in Java map
Method Signature
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
- key — the key to look up
- mappingFunction — receives the key; its return value is inserted as the new value
- Returns — the existing value if the key was already present, or the newly computed value
- Side effect — if the function returns
null, no entry is inserted
Basic Example
import java.util.HashMap;
import java.util.Map;
public class ComputeIfAbsentExample {
public static void main(String[] args) {
Map<Integer, String> numbers = new HashMap<>();
numbers.put(1, "one");
numbers.put(2, "two");
numbers.put(3, "three");
System.out.println("Before: " + numbers);
// Key 4 is absent — value will be computed and inserted
numbers.computeIfAbsent(4, k -> "four");
// Key 5 is absent — lambda shorthand
numbers.computeIfAbsent(5, k -> "five");
// Key 1 already exists — no change
numbers.computeIfAbsent(1, k -> "ONE");
System.out.println("After: " + numbers);
}
}
Output:
Before: {1=one, 2=two, 3=three}
After: {1=one, 2=two, 3=three, 4=four, 5=five}
Key 1 remains "one" — computeIfAbsent() never overwrites existing values.
Common Pattern: Map of Lists
computeIfAbsent() is especially useful for building a Map<K, List<V>> — the classic "group by" pattern:
import java.util.*;
public class GroupByExample {
public static void main(String[] args) {
List<String> words = List.of("apple", "ant", "banana", "avocado", "blueberry");
Map<Character, List<String>> grouped = new HashMap<>();
for (String word : words) {
char firstLetter = word.charAt(0);
// Create the list if this letter hasn't been seen yet
grouped.computeIfAbsent(firstLetter, k -> new ArrayList<>()).add(word);
}
grouped.forEach((letter, list) ->
System.out.println(letter + ": " + list));
}
}
Output:
a: [apple, ant, avocado]
b: [banana, blueberry]
Without computeIfAbsent(), you'd need to check whether the list exists, create it if not, then add — three lines instead of one.
Before vs. After Java 8
Old approach:
if (!grouped.containsKey(firstLetter)) {
grouped.put(firstLetter, new ArrayList<>());
}
grouped.get(firstLetter).add(word);
With computeIfAbsent():
grouped.computeIfAbsent(firstLetter, k -> new ArrayList<>()).add(word);
Related Map Compute Methods
| Method | Behavior |
|---|---|
computeIfAbsent(k, fn) | Insert only if key is absent |
computeIfPresent(k, fn) | Update only if key exists |
compute(k, fn) | Always compute; handles both cases |
getOrDefault(k, default) | Read-only; returns default without inserting |
putIfAbsent(k, v) | Insert a fixed value if absent (no function) |
Memoization with computeIfAbsent()
computeIfAbsent() is perfect for caching computed results — only run the expensive computation once per key:
Map<Integer, Long> fibCache = new HashMap<>();
long fibonacci(int n) {
if (n <= 1) return n;
return fibCache.computeIfAbsent(n, k -> fibonacci(k - 1) + fibonacci(k - 2));
}
System.out.println(fibonacci(40)); // 102334155
The function is called at most once per key. On subsequent calls with the same key, the cached value is returned immediately. This turns an exponential recursion into a linear one.
Thread-Safe Use with ConcurrentHashMap
computeIfAbsent() is atomic on ConcurrentHashMap — the check and insertion happen as a single operation. This is important in concurrent environments where two threads might try to initialize the same key simultaneously:
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, List<String>> groupedResults = new ConcurrentHashMap<>();
// Safe to call from multiple threads simultaneously
groupedResults.computeIfAbsent("category-A", k -> new ArrayList<>()).add("item1");
groupedResults.computeIfAbsent("category-A", k -> new ArrayList<>()).add("item2");
With a regular HashMap, this pattern is not thread-safe — two threads could both call the mapping function and the second would overwrite the first's result. Always use ConcurrentHashMap for shared maps accessed from multiple threads.
computeIfAbsent() vs putIfAbsent()
Both insert a value only if the key is absent, but there's an important difference:
// putIfAbsent — the new ArrayList is ALWAYS created, even if the key exists
map.putIfAbsent(key, new ArrayList<>());
// computeIfAbsent — the lambda is ONLY called if the key is absent
map.computeIfAbsent(key, k -> new ArrayList<>());
| Method | When is value computed? | Takes a function? |
|---|---|---|
computeIfAbsent(k, fn) | Only if key absent | Yes — receives the key |
putIfAbsent(k, v) | Always (value evaluated before call) | No — fixed value |
For values that are cheap to create (like a constant string), putIfAbsent() is fine. For values that are expensive to create (like a new database connection or a populated list), always use computeIfAbsent() to avoid unnecessary work.
Summary
computeIfAbsent() shines when you need lazy initialization of map values — especially for Map<K, List<V>> or Map<K, Set<V>> grouping patterns and for memoization caches. Prefer it over putIfAbsent() when the value is expensive to create. Use it with ConcurrentHashMap for thread-safe lazy initialization.
Removing map entries by value (rather than by key) requires a slightly different approach than map.remove(key). Java provides clean, expressive ways to do this using the map's values collection.
The Setup
All examples below use this map:
import java.util.*;
Map<Integer, String> inventory = new HashMap<>();
inventory.put(1, "apple");
inventory.put(2, "banana");
inventory.put(3, "cherry");
inventory.put(4, "banana");
inventory.put(5, "date");
Goal: remove all entries where the value is "banana" (keys 2 and 4).
Method 1: removeIf() on values()
The cleanest, most expressive approach:
inventory.values().removeIf("banana"::equals);
System.out.println(inventory);
// {1=apple, 3=cherry, 5=date}
map.values() returns a live view of the map's values. Modifying it (including removing) directly modifies the underlying map. removeIf() accepts a predicate and removes all matching entries.
With a lambda:
inventory.values().removeIf(value -> value.equals("banana"));
Method 2: removeAll() on values()
Removes all entries matching any value in a given collection:
inventory.values().removeAll(Collections.singleton("banana"));
Collections.singleton("banana") creates a single-element set containing "banana". This is equivalent to removeIf("banana"::equals) but useful when you want to remove multiple distinct values at once:
// Remove all entries with value "banana" OR "cherry"
inventory.values().removeAll(Set.of("banana", "cherry"));
Method 3: entrySet() with removeIf()
Use this when your removal condition depends on both the key and the value:
// Remove all entries where the key is even AND the value starts with "b"
inventory.entrySet().removeIf(entry ->
entry.getKey() % 2 == 0 && entry.getValue().startsWith("b"));
This is the most flexible approach for complex conditions.
Method 4: Iterator (Pre-Java 8)
For environments that can't use lambdas:
Iterator<Map.Entry<Integer, String>> it = inventory.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, String> entry = it.next();
if ("banana".equals(entry.getValue())) {
it.remove(); // safe removal during iteration
}
}
Do not usemap.remove()inside a for-each loop — it will throwConcurrentModificationException. The iterator'sremove()method is the safe way to delete during iteration.
Comparison
| Method | Readable? | Complex conditions? | Java version |
|---|---|---|---|
values().removeIf() | Excellent | Value only | Java 8+ |
values().removeAll() | Good | Multiple values | Java 8+ |
entrySet().removeIf() | Good | Key + value | Java 8+ |
| Iterator | Verbose | Key + value | All |
Removing Null Values
To remove all entries where the value is null, the same removeIf() approach works — but use Objects.isNull() instead of .equals() to avoid a NullPointerException:
import java.util.Objects;
Map<String, String> config = new HashMap<>();
config.put("host", "localhost");
config.put("port", null);
config.put("user", "admin");
config.put("pass", null);
config.values().removeIf(Objects::isNull);
System.out.println(config); // {host=localhost, user=admin}
Calling "banana"::equals on a null value would throw a NullPointerException, so always use Objects::isNull or a null-safe predicate when nulls are possible.
Producing a New Filtered Map (Without Mutating the Original)
All the methods above modify the map in-place. If you want to keep the original and produce a new filtered map, use a stream:
Map<Integer, String> filtered = inventory.entrySet()
.stream()
.filter(entry -> !entry.getValue().equals("banana"))
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));
System.out.println(inventory); // original unchanged
System.out.println(filtered); // {1=apple, 3=cherry, 5=date}
This is the right approach when the original map must remain intact — for example, when it's a read-only view shared across multiple parts of your application.
Pitfall: Unmodifiable Maps
If your map was created with Map.of(), Map.copyOf(), or Collections.unmodifiableMap(), all the removal methods above will throw UnsupportedOperationException:
// This map is unmodifiable — removal will fail
Map<Integer, String> immutable = Map.of(1, "apple", 2, "banana");
immutable.values().removeIf("banana"::equals); // UnsupportedOperationException!
// Make a mutable copy first
Map<Integer, String> mutable = new HashMap<>(immutable);
mutable.values().removeIf("banana"::equals);
If you receive a map from an external source and don't know whether it's mutable, wrap it in new HashMap<>(map) before attempting any removal.
Summary
For most cases, values().removeIf(value -> ...) is the cleanest option. When you need to check both key and value, use entrySet().removeIf(entry -> ...). Use Objects::isNull when removing null values. If you need to preserve the original, produce a new filtered map with a stream instead of mutating. Avoid modifying the map inside a for-each loop — always use removeIf() or an explicit iterator.
There are a few ways to remove all entries from a Java Map. The right choice depends on whether other parts of your code hold a reference to the same map object, and whether thread safety matters.
Method 1: clear() — Empties the Existing Map Object
clear() removes all key-value pairs from the map in-place. The map object itself remains — it's the same reference, now empty. All implementations of the Map interface support it: HashMap, TreeMap, LinkedHashMap, ConcurrentHashMap, and others.
import java.util.HashMap;
import java.util.Map;
public class ClearMapExample {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.put(4, "four");
map.put(5, "five");
System.out.println("Before: " + map);
System.out.println("Size: " + map.size());
map.clear();
System.out.println("After: " + map);
System.out.println("Size: " + map.size());
}
}
Output:
Before: {1=one, 2=two, 3=three, 4=four, 5=five}
Size: 5
After: {}
Size: 0
After clear(), isEmpty() returns true and size() returns 0. The map is ready to be repopulated.
Method 2: Reassignment — Replace With a New Map
If the variable is local or only you hold a reference to it, replacing it with a new map is equally valid:
Map<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
// Replace with a fresh empty map
map = new HashMap<>();
System.out.println(map); // {}
Important caveat: If another variable also points to the original map, that variable still sees the old entries. The reassignment only changes this reference, not the underlying object.
Map<Integer, String> original = new HashMap<>();
original.put(1, "one");
Map<Integer, String> alias = original; // both point to same object
original = new HashMap<>(); // only original changes reference
System.out.println(alias); // {1=one} — alias still sees old data
System.out.println(original); // {} — new empty map
When in doubt, use clear() — it affects the shared object rather than just the local pointer.
clear() Across Map Implementations
The behavior of clear() is consistent across all standard Map implementations:
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
Map<String, Integer> hashMap = new HashMap<>();
Map<String, Integer> treeMap = new TreeMap<>();
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
hashMap.put("a", 1);
treeMap.put("b", 2);
linkedHashMap.put("c", 3);
concurrentMap.put("d", 4);
hashMap.clear();
treeMap.clear();
linkedHashMap.clear();
concurrentMap.clear();
System.out.println(hashMap.isEmpty()); // true
System.out.println(treeMap.isEmpty()); // true
System.out.println(linkedHashMap.isEmpty()); // true
System.out.println(concurrentMap.isEmpty()); // true
Thread Safety
If your map is accessed from multiple threads, the approach matters:
| Map Type | clear() Thread-Safe? | Notes |
|---|---|---|
HashMap | No | Requires external synchronization |
Collections.synchronizedMap() | Yes | Synchronized on the map object |
ConcurrentHashMap | Yes | Internally handles concurrent access |
TreeMap | No | Requires external synchronization |
For a ConcurrentHashMap, clear() is safe to call from multiple threads. For a regular HashMap, always synchronize externally if other threads might access the map during the clear:
synchronized (map) {
map.clear();
}
Checking if a Map is Already Empty
Use isEmpty() to check before operating on a map you expect to be empty — or to guard against redundant work:
if (!map.isEmpty()) {
map.clear();
}
// Or simply — clear() on an empty map is a no-op, safe to call unconditionally
map.clear();
Calling clear() on an already-empty map is safe and does nothing. There's no need to check first.
clear() vs. Iterating and Removing
Before Java had clear(), you might have removed entries one by one with an iterator. This is verbose and much slower:
// Old way — DO NOT do this
Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
it.next();
it.remove();
}
// Modern way — one method call
map.clear();
clear() is O(n) just like iterating, but its internal implementation is optimized and avoids iterator overhead.
Summary
Use clear() when you want to empty the existing map object in place — especially when other code holds a reference to the same map. Use reassignment (map = new HashMap<>()) only when you're certain no other variable shares a reference to the old map. For concurrent access, ConcurrentHashMap.clear() is safe without additional synchronization.
When to User BigInteger in Java
BigInteger is Java's class for representing arbitrarily large integers — numbers with no practical upper or lower limit. It lives in java.math and is the right tool whenever your values can overflow int or long.
Why int and long Aren't Always Enough
Java's primitive integer types have hard limits:
| Type | Min Value | Max Value |
|---|---|---|
int | -2,147,483,648 | 2,147,483,647 (~2.1 billion) |
long | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 (~9.2 quintillion) |
When a computation exceeds these bounds, the result silently wraps around — no exception, just a wrong answer:
int x = Integer.MAX_VALUE;
System.out.println(x + 1); // -2147483648 — silent overflow!
long y = Long.MAX_VALUE;
System.out.println(y + 1); // -9223372036854775808 — silent overflow!
BigInteger has no such limit. It allocates as much memory as needed to represent the value exactly.
Creating BigInteger Values
import java.math.BigInteger;
// From a long value
BigInteger a = BigInteger.valueOf(15);
// From a string — the only way for numbers larger than long
BigInteger huge = new BigInteger("123456789012345678901234567890");
// Built-in constants
BigInteger zero = BigInteger.ZERO;
BigInteger one = BigInteger.ONE;
BigInteger two = BigInteger.TWO;
BigInteger ten = BigInteger.TEN;
Arithmetic Operations
BigInteger is immutable — every operation returns a new object. Use the result, don't just call the method:
BigInteger a = BigInteger.valueOf(15);
BigInteger b = BigInteger.valueOf(4);
System.out.println(a.add(b)); // 19
System.out.println(a.subtract(b)); // 11
System.out.println(a.multiply(b)); // 60
System.out.println(a.divide(b)); // 3 (integer division)
System.out.println(a.remainder(b));// 3 (a % b)
System.out.println(a.pow(3)); // 3375 (15^3)
System.out.println(a.abs()); // 15
System.out.println(a.negate()); // -15
System.out.println(a.gcd(b)); // 1 (greatest common divisor)
Real-World Example: Large Factorial
Computing 50! overflows long at around 20!. BigInteger handles it without issue:
public static BigInteger factorial(int n) {
BigInteger result = BigInteger.ONE;
for (int i = 2; i <= n; i++) {
result = result.multiply(BigInteger.valueOf(i));
}
return result;
}
System.out.println(factorial(20));
// 2432902008176640000 — fits in long
System.out.println(factorial(50));
// 30414093201713378043612608166979581188299763898377856000000000000
// — impossible in long, exact in BigInteger
Comparison and Equality
Never use == to compare BigInteger values — it compares object references, not numeric values. Use compareTo() or equals():
BigInteger x = BigInteger.valueOf(100);
BigInteger y = BigInteger.valueOf(100);
BigInteger z = BigInteger.valueOf(200);
System.out.println(x == y); // false (different objects)
System.out.println(x.equals(y)); // true
System.out.println(x.compareTo(y)); // 0 (equal)
System.out.println(x.compareTo(z)); // -1 (x < z)
System.out.println(z.compareTo(x)); // 1 (z > x)
System.out.println(x.max(z)); // 200
System.out.println(x.min(z)); // 100
Converting Between Types
BigInteger big = BigInteger.valueOf(42);
int asInt = big.intValue(); // 42 (throws if value doesn't fit)
long asLong = big.longValue(); // 42L
double asDouble = big.doubleValue(); // 42.0 (may lose precision)
String asString = big.toString(); // "42"
String asHex = big.toString(16); // "2a" (hexadecimal)
String asBinary = big.toString(2); // "101010" (binary)
Use intValueExact() or longValueExact() (Java 8+) if you want an ArithmeticException instead of silent truncation when the value doesn't fit.
When to Use BigInteger
- Factorial and combinatorics — values grow faster than
longcan hold - Cryptographic algorithms — RSA, DSA, and Diffie-Hellman operate on numbers hundreds of digits long
- Financial calculations — when precision with very large sums is required (though
BigDecimalis better for fractions) - Competitive programming — problems that explicitly work with arbitrarily large integers
- Hash and ID generation — when encoding large identifiers that span more than 64 bits
Performance Consideration
BigInteger operations are significantly slower than primitive arithmetic — each operation involves heap allocation and multi-word arithmetic. For numbers that fit in long, always prefer long. Use BigInteger only when values genuinely exceed the long range or when exact arbitrarily-large arithmetic is required.
Summary
Use BigInteger when values can overflow long — factorials, cryptographic keys, and combinatorial counts are classic examples. Create values with BigInteger.valueOf() for numbers within long range, or the String constructor for larger values. Remember that BigInteger is immutable: every arithmetic method returns a new object. Compare with equals() or compareTo(), never with ==.
How to generate Radom Integer in Java between two numbers.
Java provides several ways to generate a random integer within a specified range. The right choice depends on your Java version, whether you're in a multithreaded environment, and whether cryptographic quality is required.
Method 1: Math.random()
Math.random() returns a double in the range [0.0, 1.0). To convert it to an integer in a custom range:
public static int randomWithMath(int min, int max) {
return (int) (min + Math.random() * (max - min + 1));
}
System.out.println(randomWithMath(35, 40)); // e.g. 37
System.out.println(randomWithMath(1, 100)); // e.g. 63
The formula min + random * (max - min + 1) scales the [0.0, 1.0) range to [min, max] inclusive. The + 1 ensures the maximum value is reachable (without it, the result is [min, max-1]).
Method 2: java.util.Random
The Random class provides more flexibility and reusability than Math.random():
import java.util.Random;
Random random = new Random();
// nextInt(bound) returns [0, bound) — add min to shift the range
int result = random.nextInt(max - min + 1) + min;
System.out.println(result); // e.g. 38
Using nextInt(bound) is cleaner than the nextDouble() approach because it works directly with integers — no casting needed.
// Full utility method
public static int randomWithRandom(int min, int max) {
Random random = new Random();
return random.nextInt(max - min + 1) + min;
}
System.out.println(randomWithRandom(35, 40)); // e.g. 36
Avoid creating a new Random() inside a tight loop — the same instance is fine to reuse. Creating many instances quickly can produce correlated values on some JVMs.
Method 3: ThreadLocalRandom — Recommended for Most Cases
Java 7 introduced ThreadLocalRandom as the preferred approach for most applications. It's faster than Random in concurrent code because each thread maintains its own generator — no contention:
import java.util.concurrent.ThreadLocalRandom;
// nextInt(min, maxExclusive) — note: upper bound is exclusive
int result = ThreadLocalRandom.current().nextInt(35, 41);
System.out.println(result); // e.g. 39
// General range method
public static int randomThreadLocal(int min, int max) {
return ThreadLocalRandom.current().nextInt(min, max + 1);
}
Note that ThreadLocalRandom.nextInt(min, max) has an exclusive upper bound — unlike Random.nextInt(bound) + min. Pass max + 1 to include the maximum value.
Method 4: SecureRandom — For Security-Sensitive Code
When randomness is used for security purposes (tokens, session IDs, OTPs), use SecureRandom instead. It uses a cryptographically strong algorithm:
import java.security.SecureRandom;
SecureRandom secureRandom = new SecureRandom();
int result = secureRandom.nextInt(max - min + 1) + min;
System.out.println(result);
SecureRandom is slower than the other options — only use it when you actually need cryptographic-quality randomness. For simulations, games, or general use, ThreadLocalRandom is the better choice.
Java 8+: IntStream for Multiple Random Integers
To generate multiple random integers at once, IntStream is convenient:
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
// Generate 10 random integers between 1 and 50
List<Integer> randoms = ThreadLocalRandom.current()
.ints(10, 1, 51) // 10 values, range [1, 51)
.boxed()
.collect(Collectors.toList());
System.out.println(randoms);
// e.g. [23, 7, 45, 12, 33, 5, 49, 18, 31, 2]
Comparison
| Method | Thread-Safe? | Speed | Use When |
|---|---|---|---|
Math.random() | Yes (synchronized) | Slow under contention | Simple one-off use |
java.util.Random | Yes (synchronized) | Slow under contention | Single-threaded code |
ThreadLocalRandom | Yes (no sharing) | Fast | Most applications, multithreaded code |
SecureRandom | Yes | Slow | Security: tokens, OTPs, session IDs |
Summary
For general-purpose random integers in a range, use ThreadLocalRandom.current().nextInt(min, max + 1) — it's the fastest option and safe under concurrent access. Use java.util.Random.nextInt(bound) + min for simple single-threaded code, and SecureRandom only when the randomness is used in a security context such as token generation.
How to List all modules in java using command
The Java Platform Module System (JPMS), introduced in Java 9, organizes the JDK itself into a set of named modules. You can list all available modules with a single command and filter them to find what you need.
The Command
java --list-modules
Run this in any terminal. It lists every module bundled in your JDK installation, one per line, with its version number.
Sample output (JDK 21):
java.base@21
java.compiler@21
java.datatransfer@21
java.desktop@21
java.instrument@21
java.logging@21
java.management@21
java.net.http@21
java.prefs@21
java.rmi@21
java.scripting@21
java.se@21
java.security.jgss@21
java.security.sasl@21
java.sql@21
java.sql.rowset@21
java.xml@21
java.xml.crypto@21
jdk.accessibility@21
jdk.attach@21
jdk.compiler@21
jdk.httpserver@21
jdk.jartool@21
jdk.javadoc@21
jdk.jconsole@21
jdk.jdeps@21
jdk.jdi@21
jdk.jfr@21
jdk.jlink@21
jdk.jpackage@21
jdk.jshell@21
jdk.management@21
jdk.net@21
jdk.sctp@21
jdk.security.auth@21
jdk.security.jgss@21
jdk.zipfs@21
...
Module Naming Conventions
| Prefix | Meaning | Examples |
|---|---|---|
java.* | Java SE specification modules — defined by the Java standard | java.base, java.sql, java.net.http |
jdk.* | JDK-specific modules — tools and implementation details, not part of the Java SE spec | jdk.compiler, jdk.jshell, jdk.jfr |
java.* modules are portable — they're available in any conforming Java SE implementation (OpenJDK, GraalVM, Amazon Corretto, etc.). jdk.* modules may differ between vendors.
Key Platform Modules to Know
| Module | What It Provides |
|---|---|
java.base | Core classes: java.lang, java.util, java.io, java.nio — always available, never needs to be declared |
java.sql | JDBC API: java.sql, javax.sql |
java.net.http | Modern HTTP client (Java 11+): java.net.http |
java.logging | JUL: java.util.logging |
java.desktop | AWT, Swing: java.awt, javax.swing |
jdk.compiler | The javac compiler API |
jdk.jshell | JShell REPL API |
jdk.jfr | Java Flight Recorder for profiling |
Filtering the Module List
The output can be long. Pipe through grep to find specific modules:
# Find all SQL-related modules
java --list-modules | grep sql
# Find all security modules
java --list-modules | grep security
# Count total modules
java --list-modules | wc -l
Describe a Single Module
To see what packages a specific module exports and what it requires:
java --describe-module java.sql
Output:
java.sql@21
exports java.sql
exports javax.sql
requires java.logging transitive
requires java.transaction.xa transitive
requires java.xml transitive
uses java.sql.Driver
This tells you which packages are publicly accessible from the module, and which other modules it depends on.
module-info.java — Declaring Your Own Module
In a modular application, each module declares its dependencies and exports in a module-info.java file at the source root:
module com.myapp.core {
requires java.sql; // depends on java.sql module
requires java.logging; // depends on java.logging module
exports com.myapp.api; // makes this package visible to other modules
}
Modules that aren't declared in requires are not accessible at runtime, even if their JARs are on the classpath. This enforces explicit dependency declarations and prevents accidental use of internal APIs.
Classpath vs Module Path
Most applications still use the traditional classpath rather than the module system. You don't need to use module-info.java to run Java 9+ applications — all platform modules remain accessible via the unnamed module when you launch normally with java -cp. The module system becomes relevant when you're building a modular application or creating a custom runtime image with jlink.
Summary
Run java --list-modules to see all modules in your JDK. Modules prefixed java.* are part of the Java SE specification; jdk.* modules are JDK-specific tools. Use java --describe-module <name> to inspect a module's exports and requirements. For modular applications, declare your module's dependencies in module-info.java.
What is package-info.java in Java
package-info.java is a special source file that lives in a Java package directory. It has two primary purposes: adding Javadoc documentation at the package level, and applying annotations to an entire package at once. It's optional, but it becomes increasingly valuable as codebases grow.
File Location and Structure
The file must be placed directly inside the package directory it describes — one file per package. It contains a package declaration, optionally preceded by a Javadoc comment and/or annotations:
/**
* Provides utility classes for parsing and validating user input.
*
* <p>All classes in this package are thread-safe unless stated otherwise.
* The primary entry point is {@link com.myapp.util.InputParser}.
*/
package com.myapp.util;
That's the entire file. No class declaration, no imports (unless needed for annotations). The package statement must be the only top-level declaration.
Use Case 1: Package-Level Javadoc
When you run javadoc, the comment in package-info.java becomes the description for that package in the generated HTML documentation. Without this file, the package page has no description.
A well-written package comment typically includes:
- What the package is for (one sentence)
- The main entry-point class(es) a new user should look at first
- Any thread-safety guarantees or invariants that apply across the package
- Links to related packages with
{@link}
/**
* Contains the HTTP client abstraction layer.
*
* <p>Use {@link com.myapp.http.HttpClient} as the primary entry point.
* All implementations in this package are non-blocking by default.
*
* @see com.myapp.auth Authentication utilities used by this package
*/
package com.myapp.http;
Use Case 2: Package-Level Annotations
Annotations placed in package-info.java apply to the entire package. A common use is marking an entire package as deprecated when you're retiring a legacy API:
@Deprecated(since = "2.0", forRemoval = true)
package com.myapp.legacy;
This causes IDEs and javac to show a deprecation warning for any code that imports from com.myapp.legacy.
Another common use is suppressing warnings across an entire package:
@SuppressWarnings("deprecation")
package com.myapp.migration;
Null Safety Annotations (JetBrains, Checker Framework)
Many teams use package-info.java to set a default null-safety policy for all classes in the package. For example, with JetBrains annotations:
@org.jetbrains.annotations.NonNullApi
package com.myapp.service;
Or with the Checker Framework's @DefaultQualifier. This marks every method parameter and return type in the package as non-null by default, so you only need to annotate the exceptions with @Nullable. It's much less verbose than annotating every individual element.
Rules and Constraints
- Exactly one
package-info.javaper package — you can't have two - The file must be in the same directory as the package's other
.javafiles - No class, interface, or enum declarations allowed in this file
- Import statements are allowed (required if annotations need them)
- The file compiles to
package-info.class— it's a real compilation artifact
package-info.java vs package.html
Before Java 5, the older way to document packages was a file called package.html — plain HTML that Javadoc would read. package-info.java replaced it:
| package-info.java | package.html (legacy) | |
|---|---|---|
| Introduced | Java 5 | Java 1.1 |
| Supports annotations | Yes | No |
| Compiled by javac | Yes | No |
| Used today | Preferred | Deprecated practice |
If your codebase has package.html files, they still work but you should migrate them to package-info.java to gain annotation support.
Generating Javadoc
Run the standard javadoc command — package-info.java is automatically picked up:
javadoc -d docs/ -sourcepath src/ com.myapp.util
Or with Maven:
mvn javadoc:javadoc
The package description appears at the top of the package's summary page in the generated documentation.
Summary
package-info.java serves two purposes: providing Javadoc for an entire package, and applying annotations to all classes in the package at once. It's optional for small projects but becomes valuable in larger codebases — especially for marking deprecated packages, setting default null-safety policies, and giving new developers a clear orientation to each package's purpose. One file per package, placed in the package's source directory.
What is @Deprecated annotation in Java
The @Deprecated annotation marks a method, class, constructor, field, or package as outdated — signaling to other developers that it should no longer be used and may be removed in a future version. It generates compiler warnings and IDE indicators whenever deprecated code is called.
Basic Usage
Apply @Deprecated directly above the element you want to mark:
public class Calculator {
@Deprecated
public int add(int a, int b) {
return a + b;
}
public int addValues(int a, int b) {
return a + b;
}
}
When another class calls calc.add(1, 2), the compiler emits a warning and most IDEs render the method name with strikethrough styling.
The since and forRemoval Attributes (Java 9+)
Java 9 added two optional attributes to @Deprecated that make the annotation more informative:
public class DataProcessor {
@Deprecated(since = "2.0", forRemoval = true)
public void processLegacy(String data) {
// old implementation
}
public void process(String data) {
// new implementation
}
}
| Attribute | Type | Meaning |
|---|---|---|
since | String | The version in which the element was deprecated |
forRemoval | boolean | true means the element will definitely be removed in a future release; false (default) means it's deprecated but not necessarily scheduled for removal |
When forRemoval = true, IDEs and tools can distinguish between "use with caution" and "stop using this immediately." The compiler also emits a stronger warning variant (removal category vs. deprecation category).
The @deprecated Javadoc Tag
Alongside the annotation, always add a @deprecated Javadoc tag that explains why the element is deprecated and what to use instead:
/**
* Computes the sum of two integers.
*
* @deprecated As of version 2.0, use {@link #addValues(int, int)} instead.
* This method does not handle integer overflow correctly.
*/
@Deprecated(since = "2.0", forRemoval = true)
public int add(int a, int b) {
return a + b;
}
The @deprecated tag (lowercase) is what appears in Javadoc HTML output. The @Deprecated annotation (uppercase) is what the compiler and tools read. Both serve different audiences — use both together.
Deprecating a Class
/**
* @deprecated Use {@link NewAuthService} instead. This class will be
* removed in version 4.0.
*/
@Deprecated(since = "3.0", forRemoval = true)
public class LegacyAuthService {
// ...
}
Deprecating a class does not automatically deprecate its methods — each element is deprecated independently. However, marking the class gives a clear signal that the entire API is being retired.
Deprecating a Constructor
public class Connection {
@Deprecated(since = "1.5")
public Connection(String url, String user, String password) {
// password passed as plain string — insecure
}
public Connection(String url, String user, char[] password) {
// char[] is cleared after use — preferred
}
}
When to Use @Deprecated
- You've written a better version of a method and want callers to migrate to it
- A class or API is being retired in an upcoming major version
- A security or correctness issue was found in the current implementation
- External library APIs that you're wrapping have been replaced
Don't use @Deprecated as a way to "soft delete" code you're too lazy to remove. If something should go, remove it. Reserve deprecation for situations where you need to maintain backward compatibility across a transition period.
Suppressing Deprecation Warnings
If you're intentionally calling deprecated code (e.g., during a migration), suppress the warning to keep build output clean:
@SuppressWarnings("deprecation")
public void migrationCode() {
legacyService.processLegacy(data); // intentional — being migrated
}
Only suppress when you have a deliberate reason. Blanket suppression at the class level hides all deprecation warnings, including ones you genuinely need to fix.
Summary
Use @Deprecated when you have a better alternative and want to give callers time to migrate. Always add since and forRemoval attributes (Java 9+) so callers understand the urgency, and pair the annotation with a @deprecated Javadoc tag that points to the replacement. The annotation generates compiler warnings and IDE indicators — use @SuppressWarnings("deprecation") only when you're intentionally calling deprecated code during a controlled migration.
Maven is the most widely used build tool for Java projects. It handles dependency management, compilation, testing, and packaging through a standardized project structure and a single configuration file: pom.xml. This guide walks through creating a project from scratch using Maven's quickstart archetype.
Prerequisites
- Java JDK installed (Java 8 or later)
- Maven installed — verify with
mvn --version
mvn --version
# Apache Maven 3.9.4
# Java version: 21.0.1, vendor: Eclipse Adoptium
If Maven isn't installed, download it from maven.apache.org or install it via brew install maven (macOS) or sudo apt install maven (Ubuntu).
Step 1: Generate the Project
Run the following command to generate a new project using the maven-archetype-quickstart template:
mvn archetype:generate \
-DgroupId=com.mycompany.app \
-DartifactId=my-java-app \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 \
-DinteractiveMode=false
The parameters:
| Parameter | Description |
|---|---|
groupId | Reverse-domain identifier for your organization (com.mycompany.app) |
artifactId | The project/module name — becomes the folder name |
archetypeArtifactId | The template to use — quickstart is the standard Java starter |
interactiveMode=false | Skips the interactive prompt; uses provided values directly |
Maven downloads the archetype metadata and creates a folder named my-java-app in the current directory.
Step 2: Project Structure
The generated structure follows Maven's standard layout:
my-java-app/
├── pom.xml
└── src/
├── main/
│ └── java/
│ └── com/mycompany/app/
│ └── App.java
└── test/
└── java/
└── com/mycompany/app/
└── AppTest.java
All production source code goes in src/main/java. All test code goes in src/test/java. This separation is enforced by Maven's build lifecycle.
Step 3: The Generated pom.xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-java-app</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Update maven.compiler.source and maven.compiler.target to match your Java version. To add dependencies, insert <dependency> blocks inside <dependencies>.
Step 4: Build the Project
cd my-java-app
mvn install
This runs the full build lifecycle: compile → test → package → install. The JAR is placed in target/my-java-app-1.0-SNAPSHOT.jar and also installed to your local Maven repository (~/.m2/repository).
Common lifecycle commands:
mvn compile # compile source code only
mvn test # compile and run tests
mvn package # compile, test, and create JAR/WAR
mvn install # package + install to local .m2 repo
mvn clean # delete the target/ directory
mvn clean install # clean then full build — most common
Importing Into an IDE
IntelliJ IDEA: File → Open → select the my-java-app folder. IntelliJ detects pom.xml and imports it as a Maven project automatically.
Eclipse: Generate Eclipse project files first, then import:
mvn eclipse:eclipse
In Eclipse: File → Import → Existing Projects into Workspace → navigate to the project folder.
VS Code: Open the folder. Install the "Extension Pack for Java" extension — it detects pom.xml and configures the project automatically.
Adding a Dependency
To add a library, add its <dependency> to pom.xml and run mvn install. Maven downloads it automatically from Maven Central:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
Find dependency coordinates at mvnrepository.com — search for the library and copy the <dependency> snippet.
Summary
Use mvn archetype:generate with the maven-archetype-quickstart template to scaffold a new Java project in seconds. The result is a standard Maven project structure with pom.xml, source and test directories, and a working build. Run mvn clean install to compile, test, and package your application. Open the pom.xml folder directly in IntelliJ or VS Code for automatic Maven integration.
Java logical operator short circuit with example
Java has two sets of logical operators for boolean expressions: the short-circuit operators (&& and ||) and the non-short-circuit operators (& and |). They produce the same boolean result, but differ in when they evaluate the right operand. Understanding this difference matters for both correctness and performance.
How Short-Circuit Evaluation Works
| Operator | Name | Short-circuits when… |
|---|---|---|
&& | Conditional AND | Left side is false — result is false regardless of right side |
|| | Conditional OR | Left side is true — result is true regardless of right side |
& | Logical AND | Never — always evaluates both sides |
| | Logical OR | Never — always evaluates both sides |
Demonstrating Short-Circuit with &&
public class ShortCircuitDemo {
static boolean isFalse() {
System.out.println("isFalse() called");
return false;
}
static boolean isTrue() {
System.out.println("isTrue() called");
return true;
}
public static void main(String[] args) {
System.out.println("--- Using && ---");
boolean result = isFalse() && isTrue();
System.out.println("Result: " + result);
System.out.println("--- Using & ---");
result = isFalse() & isTrue();
System.out.println("Result: " + result);
}
}
Output:
--- Using && ---
isFalse() called
Result: false
--- Using & ---
isFalse() called
isTrue() called
Result: false
With &&, once isFalse() returns false, Java skips isTrue() entirely — the overall result is already determined. With &, both methods are always called.
Demonstrating Short-Circuit with ||
System.out.println("--- Using || ---");
boolean result = isTrue() || isFalse();
System.out.println("Result: " + result);
System.out.println("--- Using | ---");
result = isTrue() | isFalse();
System.out.println("Result: " + result);
Output:
--- Using || ---
isTrue() called
Result: true
--- Using | ---
isTrue() called
isFalse() called
Result: true
With ||, once isTrue() returns true, the right side is skipped. With |, both sides always run.
Practical Benefit: Null Safety
The most common real-world use of short-circuit evaluation is guarding against NullPointerException:
String text = null;
// Safe — if text is null, the second condition is never evaluated
if (text != null && text.length() > 5) {
System.out.println("Long string: " + text);
}
// Unsafe with & — both sides always run; throws NullPointerException
if (text != null & text.length() > 5) { // NPE!
System.out.println("Long string: " + text);
}
This pattern — checking for null before calling methods on an object — is ubiquitous in Java. It only works correctly because && short-circuits.
Practical Benefit: Avoiding Expensive Operations
Place the cheaper or most-likely-to-fail condition on the left side so the expensive one is skipped when possible:
// isEnabled() is a cheap boolean field check
// fetchFromDatabase() is an expensive I/O call
if (isEnabled() && fetchFromDatabase(id) != null) {
process();
}
// If isEnabled() is false, fetchFromDatabase() is never called
When to Use Non-Short-Circuit Operators (& and |)
The non-short-circuit & and | are rarely used with booleans, but they have a valid use case: when you need both sides to execute for their side effects.
// Both increment operations must run — use & instead of &&
int x = 0, y = 0;
boolean result = (++x > 0) & (++y > 0);
System.out.println("x=" + x + " y=" + y); // x=1 y=1
// With &&, y might not be incremented:
x = 0; y = 0;
result = (++x > 5) && (++y > 0); // x=1 but y=0 — y not incremented
In practice, code with side effects inside conditions is hard to read. Prefer computing side effects before the condition rather than relying on &.
Summary
Use && and || (short-circuit) in almost all boolean conditions — they're safer (null guards work), more efficient (skip unnecessary work), and what Java developers expect. Use & and | only when you explicitly need both sides to always evaluate. Put cheaper or fail-fast conditions on the left side of && to maximize the performance benefit of skipping the right side.
Java's Map interface doesn't extend Iterable directly, so you can't use a for-each loop on the map itself. Instead, you iterate over one of its three collection views: entrySet(), keySet(), or values(). Here are five practical methods.
Setup — Sample Map
import java.util.*;
Map<Integer, String> countries = new HashMap<>();
countries.put(1, "USA");
countries.put(2, "Germany");
countries.put(3, "Japan");
countries.put(4, "Brazil");
countries.put(5, "India");
Method 1: entrySet() with For-Each Loop
The most common and readable approach. Each Map.Entry gives you direct access to both key and value without a second lookup:
for (Map.Entry<Integer, String> entry : countries.entrySet()) {
System.out.println(entry.getKey() + " → " + entry.getValue());
}
Output:
1 → USA
2 → Germany
3 → Japan
4 → Brazil
5 → India
This is the preferred approach when you need both key and value. It avoids the extra map.get(key) call that keySet() iteration requires.
Method 2: entrySet() with Iterator
Use an explicit Iterator when you need to remove entries during iteration — the only safe way to modify a map while iterating:
Iterator<Map.Entry<Integer, String>> iterator = countries.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, String> entry = iterator.next();
System.out.println(entry.getKey() + " → " + entry.getValue());
// Safe to remove — iterator.remove() doesn't throw ConcurrentModificationException
if (entry.getKey() == 3) {
iterator.remove();
}
}
Never call map.remove() inside a for-each loop — it throws ConcurrentModificationException. An explicit iterator's remove() is the safe alternative.
Method 3: keySet() with For-Each Loop
Iterate over keys, then look up each value separately:
for (Integer key : countries.keySet()) {
System.out.println(key + " → " + countries.get(key));
}
This works but performs an extra get() per entry. Use this when you only need the keys, or when the value lookup is intentional. For both key and value, entrySet() is more efficient.
Method 4: keySet() with Iterator
Iterator<Integer> keyIterator = countries.keySet().iterator();
while (keyIterator.hasNext()) {
Integer key = keyIterator.next();
System.out.println(key + " → " + countries.get(key));
}
Method 5: Java 8 forEach() — Most Concise
Java 8 added a forEach() method directly on Map that accepts a BiConsumer:
countries.forEach((key, value) ->
System.out.println(key + " → " + value));
This is the cleanest syntax for read-only iteration. It's internally backed by entrySet(), so performance is identical. You can't call iterator.remove() inside forEach() — use method 2 if you need to modify the map during iteration.
Iterating Values Only
When you only need the values (not the keys), iterate values():
for (String country : countries.values()) {
System.out.println(country);
}
Comparison
| Method | Access Key? | Access Value? | Remove During? | Java Version |
|---|---|---|---|---|
entrySet() for-each | Yes | Yes (direct) | No | 5+ |
entrySet() iterator | Yes | Yes (direct) | Yes | 5+ |
keySet() for-each | Yes | Via get() | No | 5+ |
keySet() iterator | Yes | Via get() | Yes | 5+ |
forEach() lambda | Yes | Yes (direct) | No | 8+ |
values() for-each | No | Yes | No | 5+ |
Which Method Should You Use?
- Default choice:
forEach((key, value) -> ...)— cleanest syntax, Java 8+ - Need to remove during iteration:
entrySet().iterator()withiterator.remove() - Keys only:
for (K key : map.keySet()) - Values only:
for (V value : map.values()) - Pre-Java 8:
entrySet()for-each
Summary
Iterate over a Map using one of its three views: entrySet() for both keys and values, keySet() for keys alone, or values() for values alone. For most modern code, map.forEach((k, v) -> ...) is the cleanest option. When you need to remove entries during iteration, use an explicit iterator — never map.remove() inside a for-each loop.
Java LinkedHashMap.removeEldestEntry method example
LinkedHashMap maintains a doubly-linked list across its entries, preserving insertion order (or access order, depending on configuration). Its protected removeEldestEntry() method lets you control automatic eviction — making it the simplest way to build a bounded cache in Java without any third-party library.
What Is removeEldestEntry()?
removeEldestEntry(Map.Entry<K,V> eldest) is a protected method called automatically after every put() or putAll(). If it returns true, LinkedHashMap removes the eldest (first) entry from the map. If it returns false, nothing is removed.
The default implementation always returns false — no automatic removal. You override it to add eviction logic.
Basic Example: Size-Bounded Map
Keep at most 4 entries — automatically evict the oldest when a 5th is added:
import java.util.*;
LinkedHashMap<Integer, String> boundedMap = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
return size() > 4;
}
};
boundedMap.put(0, "zero");
boundedMap.put(1, "one");
boundedMap.put(2, "two");
boundedMap.put(3, "three");
System.out.println("After 4 puts: " + boundedMap);
// {0=zero, 1=one, 2=two, 3=three}
boundedMap.put(4, "four"); // triggers eviction of key=0
System.out.println("After 5th put: " + boundedMap);
// {1=one, 2=two, 3=three, 4=four}
When the 5th entry is added, size() becomes 5, removeEldestEntry() returns true, and the entry with key 0 (the eldest) is automatically removed.
LRU Cache Using Access Order
For a true Least Recently Used (LRU) cache, construct LinkedHashMap with accessOrder = true. This moves accessed entries to the tail of the list, so the head always holds the least-recently-used entry:
int capacity = 3;
LinkedHashMap<String, String> lruCache = new LinkedHashMap<>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > capacity;
}
};
lruCache.put("a", "Apple");
lruCache.put("b", "Banana");
lruCache.put("c", "Cherry");
System.out.println(lruCache); // {a=Apple, b=Banana, c=Cherry}
lruCache.get("a"); // "a" accessed — moves to tail (most recent)
System.out.println(lruCache); // {b=Banana, c=Cherry, a=Apple}
lruCache.put("d", "Date"); // evicts "b" — least recently used
System.out.println(lruCache); // {c=Cherry, a=Apple, d=Date}
The LinkedHashMap(initialCapacity, loadFactor, accessOrder) constructor's third argument enables access-order mode. With accessOrder = true, every get() reorders the entry to the tail, so the head is always the LRU candidate for eviction.
Insertion Order vs Access Order
| Mode | How It Works | What Gets Evicted |
|---|---|---|
| Insertion order (default) | Entries stay in the order they were put() | The entry that was inserted first |
Access order (true) | Accessed entries move to the tail | The entry least recently accessed or inserted |
Reusable LRU Cache Class
import java.util.LinkedHashMap;
import java.util.Map;
public class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public LruCache(int maxSize) {
super(maxSize, 0.75f, true); // accessOrder = true
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
}
// Usage:
LruCache<Integer, String> cache = new LruCache<>(5);
cache.put(1, "Page 1");
cache.put(2, "Page 2");
cache.get(1); // "1" is now most recently used
System.out.println(cache.containsKey(1)); // true
Thread Safety
LinkedHashMap is not thread-safe. For a concurrent LRU cache, wrap it with Collections.synchronizedMap():
Map<String, String> syncCache = Collections.synchronizedMap(
new LruCache<>(100)
);
For high-concurrency workloads, consider ConcurrentHashMap with a manual size-bounding strategy, or a library like Caffeine which provides a lock-free LRU cache.
Summary
Override removeEldestEntry() in a LinkedHashMap subclass to automatically evict entries when the map exceeds a size limit. For a true LRU cache, construct LinkedHashMap with accessOrder = true so accesses move entries to the tail and the head always holds the least-recently-used entry. This pattern is clean, built into the JDK, and requires no external dependencies.
How to create sealed classes in Java
Java 17 finalised sealed classes (JEP 409), a language feature that gives you fine‑grained control over inheritance.
With sealed classes, you can declare a class or interface and explicitly list which types are allowed to extend or implement it.
This fills the gap between final (no subclasses) and completely open inheritance (any subclass allowed).
The Inheritance Dilemma
Before sealed classes, Java developers faced a choice:
finalclasses – too restrictive; you cannot extend them at all.- Open classes – anyone can extend them, which makes it hard to reason about a fixed set of subtypes.
Sealed classes introduce a third option: “only these specific classes may extend or implement this type.” This is invaluable when modelling a closed domain, such as:
- Shapes in a geometry library (
Circle,Rectangle,Triangle) - Payment statuses (
Success,Failure,Pending) - AST nodes in a compiler (
IfNode,WhileNode,AssignmentNode) - JSON value types (
JsonObject,JsonArray,JsonString, etc.)
Key Keywords and Their Roles
Understanding the interplay of sealed, permits, final, and non-sealed is essential:
| Keyword | Used On | Meaning |
|---|---|---|
sealed | Class or interface | Restricts which types may extend/implement it; must be followed by permits (unless subclasses are nested). |
permits | After sealed | Lists the permitted direct subclasses or implementors. |
final | Subclass | Cannot be extended further – the hierarchy ends here. |
sealed | Subclass | Is itself sealed and can have its own permitted subclasses (nested sealed hierarchy). |
non-sealed | Subclass | Re‑opens the subclass for arbitrary extension – the sealed constraint stops here. |
Basic Example: Sealed Class with permits
Let's define a sealed Shape class that allows only three specific subclasses:
public abstract sealed class Shape permits Circle, Rectangle, Triangle {
public abstract double area();
}
public final class Circle extends Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double area() { return Math.PI * radius * radius; }
}
public final class Rectangle extends Shape {
private final double width, height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
@Override
public double area() { return width * height; }
}
public final class Triangle extends Shape {
private final double base, height;
public Triangle(double base, double height) { this.base = base; this.height = height; }
@Override
public double area() { return 0.5 * base * height; }
}
Any attempt to create a fourth subclass (e.g., Hexagon) will be rejected by the compiler – the hierarchy is closed.
Omitting permits with Nested Subclasses
If all permitted subclasses are defined as nested classes inside the sealed class, you can omit the permits clause:
public abstract sealed class Operation {
public abstract int apply(int a, int b);
public static final class Add extends Operation {
@Override public int apply(int a, int b) { return a + b; }
}
public static final class Sub extends Operation {
@Override public int apply(int a, int b) { return a - b; }
}
public static final class Mul extends Operation {
@Override public int apply(int a, int b) { return a * b; }
}
}
This keeps the code self‑contained and clearly communicates that these are the only operations.
Sealed Interfaces and Records
Sealed interfaces work equally well and pair beautifully with records (which are implicitly final):
public sealed interface PaymentResult permits Success, Failure, Pending { }
public record Success(String transactionId) implements PaymentResult { }
public record Failure(String reason) implements PaymentResult { }
public record Pending(String reference) implements PaymentResult { }
Records are a natural choice here because each variant holds different data, and they are inherently immutable.
Pattern Matching with switch (Java 21+)
The real power of sealed classes shines when combined with pattern matching in switch expressions.
Because the compiler knows all permitted subtypes, it can verify exhaustiveness – no default case is needed:
double describeArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
// No default – compiler ensures all cases are covered
};
}
If you later add a new permitted subclass (say Hexagon) to Shape, every switch over Shape that lacks a Hexagon case will become a compile‑time error – catching the gap immediately.
Reopening a Branch with non-sealed
Sometimes you want to allow arbitrary extension for some part of the hierarchy. Mark a permitted subclass as non-sealed to reopen it:
public abstract sealed class Vehicle permits Car, Truck, SpecialVehicle { }
public final class Car extends Vehicle { }
public final class Truck extends Vehicle { }
public non-sealed class SpecialVehicle extends Vehicle { }
// Now anyone can extend SpecialVehicle:
public class Ambulance extends SpecialVehicle { }
public class FireTruck extends SpecialVehicle { }
This gives you a hybrid model: a closed top‑level family, with an open branch for extensibility where needed.
Important Rules and Constraints
- The sealed class and all its permitted subclasses must be in the same package or in the same module.
- Every permitted subclass must directly extend the sealed class (not a subclass of it).
- Each permitted subclass must be marked
final,sealed, ornon-sealed. - If the sealed class is an interface, the implementors must be
final,sealed, ornon-sealedclasses (records are implicitlyfinal). - You cannot use a sealed class with
enum– enums are already effectively a closed set of constants.
When to Use Sealed Classes vs Enums
| Feature | Sealed Classes | Enums |
|---|---|---|
| Number of instances | Many (unlimited) – each subclass can have many objects | Fixed set of singleton constants |
| State | Each subclass can have its own fields and behaviour | Enums can have fields, but they are effectively singletons |
| Pattern matching | Excellent – can deconstruct and handle different shapes | Limited to constant matching (switch on enum constants) |
| Use case | Modelling complex, state‑ful variants (e.g., AST nodes, JSON types) | Modelling fixed sets of named values (e.g., days of the week, status codes) |
Real‑World Use Cases
1. State Machines
public sealed interface State permits Idle, Running, Paused, Stopped { }
public record Idle() implements State { }
public record Running(long startTime) implements State { }
public record Paused(long elapsed) implements State { }
public record Stopped() implements State { }
2. Abstract Syntax Tree Nodes
public sealed interface Expr permits Const, Add, Mul, Var { }
public record Const(int value) implements Expr { }
public record Add(Expr left, Expr right) implements Expr { }
public record Mul(Expr left, Expr right) implements Expr { }
public record Var(String name) implements Expr { }
3. JSON Data Types
public sealed interface JsonValue permits JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonNull { }
public record JsonObject(Map<String, JsonValue> members) implements JsonValue { }
public record JsonArray(List<JsonValue> elements) implements JsonValue { }
public record JsonString(String value) implements JsonValue { }
// ... and so on
4. Payment Processing Results
We already saw the PaymentResult example – it's a perfect fit for sealed interfaces with records.
Best Practices
- Prefer
sealedinterfaces with records for data‑oriented hierarchies – they are concise and immutable. - Use
sealedclasses when you need shared state or behaviour that cannot be captured by records (e.g., mutable fields, helper methods). - Design for extensibility – if you anticipate that third‑party developers will need to extend a branch, mark that branch
non-sealed. - Combine with pattern matching to write safe, readable, and maintainable code that the compiler checks for completeness.
- Keep the permitted list small – a sealed hierarchy with more than 5–7 subtypes may indicate a design that could be simplified.
Summary
Sealed classes give you a powerful tool to declare closed type hierarchies while retaining the flexibility of object‑oriented design.
They turn runtime errors into compile‑time checks, especially when used with pattern matching.
Mark your root type with sealed, list your permitted subtypes, and choose final, sealed, or non-sealed for each one – depending on how open that branch should be.
Whether you are modelling a domain, building an interpreter, or handling responses from an external API, sealed classes help you express your intent clearly and let the compiler enforce your design decisions. Start using them today – they are available in Java 17 and later.
Happy coding!