How do I use Collectors.partitioningBy?

The Collectors.partitioningBy is a method in Java’s java.util.stream.Collectors class that is used to partition elements of a stream into two groups based on a predicate. It essentially creates a Map with a boolean key (true or false) and lists of elements as values. Here’s an explanation of how to use it effectively:

Syntax:

Collectors.partitioningBy(Predicate<? super T> predicate)

Description:

  1. Predicate: This is a functional interface that tests a condition on elements of the stream. Each element in the stream is evaluated against this condition.
  2. Result: The partitioningBy collector returns a Map with two entries:
    • Key true: Contains elements for which the predicate evaluates to true.
    • Key false: Contains elements for which the predicate evaluates to false.

Example:

Here’s an example usage of partitioningBy:

package org.kodejava.util.stream;

import java.util.*;
import java.util.stream.Collectors;

public class PartitioningExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Partition numbers into even and odd
        Map<Boolean, List<Integer>> partitions = numbers.stream()
                .collect(Collectors.partitioningBy(num -> num % 2 == 0));

        // Access partitions
        List<Integer> evens = partitions.get(true);  // Numbers divisible by 2 (even numbers)
        List<Integer> odds = partitions.get(false); // Numbers not divisible by 2 (odd numbers)

        System.out.println("Even Numbers: " + evens);
        System.out.println("Odd Numbers: " + odds);
    }
}

Output:

Even Numbers: [2, 4, 6, 8, 10]
Odd Numbers: [1, 3, 5, 7, 9]

Advanced Usage:

You can extend the functionality of partitioningBy by combining it with other collectors, such as Collectors.mapping or Collectors.counting.

Example: Count of elements in each partition

Map<Boolean, Long> partitionedCount = numbers.stream()
        .collect(Collectors.partitioningBy(num -> num % 2 == 0, Collectors.counting()));

System.out.println(partitionedCount);
// Output: {false=5, true=5}

In this example, instead of partitioning into lists, the partitioning is configured to count the number of elements in each group.


When to Use partitioningBy:

Use Collectors.partitioningBy when:

  • You need to classify a collection of items into two mutually exclusive groups.
  • The condition for classification is a boolean predicate.

It’s commonly used in scenarios like:

  • Splitting numbers into even and odd.
  • Categorizing people into adults and minors based on age.
  • Determining whether elements in a list satisfy a specific condition, e.g., “passing grade” or “failing grade.”

How do I collect stream results into an immutable collection?

In Java, you can use the Stream API’s Collectors to gather stream results into an immutable collection. Since Java 10, you can use Collectors.toUnmodifiableList(), Collectors.toUnmodifiableSet(), and other similar methods to collect the results into unmodifiable collections.

Here’s how you can collect the stream results into an immutable collection:

1. Immutable List

To collect the results of a stream into an immutable list:

package org.kodejava.util.stream;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        List<String> immutableList = Stream.of("apple", "banana", "cherry")
                .collect(Collectors.toUnmodifiableList());

        System.out.println(immutableList);

        // Attempting to modify the list will throw UnsupportedOperationException
        // immutableList.add("date"); // Throws UnsupportedOperationException
    }
}

2. Immutable Set

To collect the results into an immutable set:

package org.kodejava.util.stream;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        Set<String> immutableSet = Stream.of("apple", "banana", "cherry")
                .collect(Collectors.toUnmodifiableSet());

        System.out.println(immutableSet);

        // Attempting to modify the set will throw UnsupportedOperationException
        // immutableSet.add("date"); // Throws UnsupportedOperationException
    }
}

3. Immutable Map

To collect results into an immutable map:

package org.kodejava.util.stream;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        Map<String, Integer> immutableMap = Stream.of("apple", "banana", "cherry")
                .collect(Collectors.toUnmodifiableMap(
                        fruit -> fruit,         // Key mapper: the fruit itself
                        fruit -> fruit.length() // Value mapper: the length of the fruit name
                ));

        System.out.println(immutableMap);

        // Attempting to modify the map will throw UnsupportedOperationException
        // immutableMap.put("date", 4); // Throws UnsupportedOperationException
    }
}

Notes:

  • Unmodifiable vs Immutable: Collections created with Collectors.toUnmodifiableList(), Collectors.toUnmodifiableSet(), and Collectors.toUnmodifiableMap() are unmodifiable. While they cannot be changed (add, remove, replace), immutability might imply further guarantees (e.g., deeply immutable objects inside the collection, which this does not enforce).
  • Introduced in Java 10: toUnmodifiableList(), toUnmodifiableSet(), and toUnmodifiableMap() were introduced in Java 10. If you’re using Java 8 or Java 9, you’ll need a custom approach for creating immutable collections (like Collections.unmodifiableList).

In Java 8:

If you’re stuck on Java 8, you can achieve something similar using Collections.unmodifiableList() or other Collections.unmodifiableXxx methods:

package org.kodejava.util.stream;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        List<String> immutableList = Collections.unmodifiableList(
                Stream.of("apple", "banana", "cherry").collect(Collectors.toList())
        );

        System.out.println(immutableList);
        // immutableList.add("date"); // Throws UnsupportedOperationException
    }
}

This approach, however, wraps an existing modifiable collection, so try to update your project to take advantage of Java 10+ features.

How do I use Map.merge() to simplify counting logic?

The Map.merge method in Java is a convenient way to simplify various kinds of logic that require updating or modifying values in a map, such as counting occurrences. It works by letting you specify how to combine the old value (if it exists) and the new value (to be added). This is particularly useful for implementing counting logic more concisely.

Here’s how you can use Map.merge to count occurrences:

Key Idea

  • If the key doesn’t exist in the map, merge inserts it with the given value.
  • If the key already exists, merge uses the provided function (a BiFunction) to combine the existing value and the new value.

Example: Counting Word Occurrences in a String

package org.kodejava.util;

import java.util.HashMap;
import java.util.Map;

public class WordCounter {
    public static void main(String[] args) {
        String text = "apple banana apple orange banana apple";

        // Split the string into words
        String[] words = text.split(" ");

        // Map to store word counts
        Map<String, Integer> wordCounts = new HashMap<>();

        // Use Map.merge to simplify counting logic
        for (String word : words) {
            // Increment count for each word
            wordCounts.merge(word, 1, Integer::sum);
        }

        // Print the word counts
        System.out.println(wordCounts);
    }
}

Explanation of merge Usage

In the above example:

  1. wordCounts.merge(word, 1, Integer::sum);
    • word is the key.
    • 1 is the value to add (for each occurrence of the word).
    • Integer::sum is the combining function that adds the existing value (if present) and the new value.
      • If the word is already in the map, the count is increased by 1.
      • If the word is not in the map, it is added with an initial count of 1.

Advantages of Using Map.merge for Counting

  • Conciseness: Avoids the need for verbose if-else or containsKey checks.
  • Thread Safety: Works well in a thread-safe map (e.g., ConcurrentHashMap) without requiring additional synchronization.
  • Readability: The code is clear and easy to understand, as the counting logic is encapsulated in a single line.

Without Map.merge

To see why Map.merge simplifies the code, here’s how the same logic would look without it:

for (String word : words) {
    if (wordCounts.containsKey(word)) {
        wordCounts.put(word, wordCounts.get(word) + 1);
    } else {
        wordCounts.put(word, 1);
    }
}

As you can see, it’s more verbose and repetitive compared to using merge.


Other Use Cases for Map.merge

  1. Updating a map with custom logic:
    You can combine values in a way that suits your requirements, such as concatenating strings or appending to a list.

  2. Tracking multiple values:
    For example, storing a list of values associated with a key while avoiding null checks:

    map.merge(key, new ArrayList<>(List.of(value)), (oldList, newList) -> {
       oldList.addAll(newList);
       return oldList;
    });
    
  3. Combining maps:
    Merge entries from one map into another map using custom logic.


In summary, Map.merge helps to simplify and streamline your counting or updating logic by focusing on what to do with existing and new values, while handling key-insertion logic for you.

How do I use ConcurrentHashMap.computeIfAbsent safely?

To safely use ConcurrentHashMap.computeIfAbsent, it’s important to understand both its purpose and how to use it in a thread-safe manner.

Purpose of computeIfAbsent

computeIfAbsent is a method of ConcurrentHashMap that:

  1. Checks if the key exists in the map.
  2. If the key exists, it returns the associated value.
  3. If the key does not exist, it computes a value for the key using the provided function, inserts the computed value into the map, and returns the value.

This method is thread-safe, meaning:

  • It guarantees atomicity when checking for the key, computing the value, and inserting it into the map.
  • Multiple threads can safely call this method without introducing non-deterministic behavior or data race conditions.

Safe Usage Guidelines

  1. Avoid Side Effects in the Mapping Function:
    The computation function should not introduce side effects or interfere with the ConcurrentHashMap itself. Modifying the map inside the mapping function or depending on the external mutable shared state can lead to unexpected behavior.

    Example of unsafe behavior:

    map.computeIfAbsent(key, k -> {
       map.put(someOtherKey, someOtherValue);  // Modifies the map during compute
       return calculateValue(k);
    });
    

    Instead, the function should remain isolated and focus solely on deriving a value for the given key.

  2. Concurrency Is Handled For You:
    There’s no need for explicit synchronization or locking when using computeIfAbsent. The method ensures that the check and computation happen atomically for each key.

    Example:

    ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
    String value = map.computeIfAbsent("key", k -> "computedValue");
    
  3. Be Careful with Long/Expensive Computations:
    If the computation logic in computeIfAbsent is long-running or expensive, this can lead to contention or delays when multiple threads are trying to compute values for the same key. If you expect expensive computations:

    • Offload the computation to a dedicated service or background thread pool.
    • Return placeholders immediately if possible and fill them later.
  4. Guard Against Null Values:
    While ConcurrentHashMap does not allow null keys or values, the mapping function might inadvertently return a null value. This will result in a NullPointerException. Always ensure that the computation logic does not return null.

    Example check:

    map.computeIfAbsent("key", k -> {
       String result = computeValue(k);
       return (result != null) ? result : "defaultValue";
    });
    
  5. Avoid Recursive Dependencies:
    Do not create circular dependencies where computeIfAbsent recursively triggers a computation for the same key or related keys. This can cause a StackOverflowError.


Practical Example

Here’s a robust example:

package org.kodejava.util.concurrent;

import java.util.concurrent.ConcurrentHashMap;

public class ComputeIfAbsentExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // Safe and efficient usage of computeIfAbsent
        Integer value = map.computeIfAbsent("item", key -> {
            // Expensive or non-trivial computation can go here
            return key.length();  // Mapping key to length as the value
        });

        System.out.println("Value: " + value);  // Output: Value: 4
    }
}

Summary

Using ConcurrentHashMap.computeIfAbsent safely involves:

  • Avoiding side effects in the mapping function.
  • Being cautious with long or expensive computations.
  • Ensuring the mapping function does not return null.
  • Relinquishing explicit synchronization, as it’s already atomic.
  • Avoiding recursive or circular dependencies in value computation.

By adhering to these guidelines, you can leverage the method effectively, even in highly concurrent environments.

How do I use LinkedHashMap for predictable iteration order?

In Java, a LinkedHashMap is a subtype of HashMap that maintains a predictable iteration order. It uses a doubly linked list to store the entries in insertion order (or, optionally, access order). Here’s how you can use LinkedHashMap for predictable iteration order:

1. Maintaining Insertion Order

By default, a LinkedHashMap iterates its entries in the order they were inserted. This is useful when you want to retrieve elements in the same order you added them.

Here’s an example:

package org.kodejava.util;

import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapExample {
    public static void main(String[] args) {
        // Creating LinkedHashMap
        Map<String, Integer> map = new LinkedHashMap<>();

        // Adding elements (insertion order)
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);
        map.put("Four", 4);

        // Iterating through the map
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " => " + entry.getValue());
        }
    }
}

Output:

One => 1
Two => 2
Three => 3
Four => 4

In this example, the elements are iterated in the same order they were inserted.


2. Maintaining Access Order

You can configure a LinkedHashMap to maintain access order, which means it reorders entries based on the most recent access. To enable access order, you must use the constructor that takes a boolean parameter for accessOrder.

Here’s an example:

package org.kodejava.util;

import java.util.LinkedHashMap;
import java.util.Map;

public class AccessOrderExample {
    public static void main(String[] args) {
        // Creating LinkedHashMap with access-order
        Map<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);

        // Adding elements
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);

        // Accessing some elements
        map.get("One");  // Access "One"
        map.get("Three"); // Access "Three"

        // Iterating through the map
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " => " + entry.getValue());
        }
    }
}

Output:

Two => 2
One => 1
Three => 3

In this case:

  • Initially, the insertion order was One, Two, Three.
  • After accessing One and Three, they were moved to the end, making Two the first in the iteration order.

3. Removing the Oldest Entry with Access Order

If needed, you can use a LinkedHashMap in combination with its removeEldestEntry method to automatically remove the oldest entry (e.g., implementing a cache).

Here’s how:

package org.kodejava.util;

import java.util.LinkedHashMap;
import java.util.Map;

public class RemoveEldestExample {
    public static void main(String[] args) {
        // Create LinkedHashMap with override for removeEldestEntry
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>(3, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
                return size() > 3; // Remove oldest if size > 3
            }
        };

        // Adding elements
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);
        map.put("Four", 4); // "One" will be removed here

        // Accessing some elements
        map.get("Two");
        map.put("Five", 5); // "Three" will be removed here

        // Iterating through the map
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " => " + entry.getValue());
        }
    }
}

Output:

Four => 4
Two => 2
Five => 5

Explanation:

  1. The map was set to remove the eldest (first) entry when its size exceeds 3.
  2. When "Four" was added, "One" was removed because the size limit was exceeded.
  3. When "Five" was added, "Three" was removed, as it was now the eldest entry after accessing "Two".

Summary of Key Points:

  1. Insertion Order: By default, the iteration order matches the insertion order.
  2. Access Order: Can be enabled using the LinkedHashMap constructor with accessOrder = true.
  3. Custom Behavior: Override the removeEldestEntry method to create a fixed-size cache or similar functionality.

LinkedHashMap is handy when you need consistent iteration order (e.g., for caches, ordering-sensitive collections).

How do I use Collectors.groupingBy() with downstream collectors?

The Collectors.groupingBy is a powerful method in Java’s Stream API that allows grouping of elements in a stream based on a classification function, and it works well with downstream collectors. Here’s how you can use Collectors.groupingBy with downstream collectors effectively.


Syntax of Collectors.groupingBy with a Downstream Collector

The key method signature is:

Collectors.groupingBy(Classifier, Downstream)
  • Classifier: A function that determines how the elements are grouped (e.g., based on a key derived from the element).
  • Downstream Collector: The collector used to process the grouped elements further (e.g., counting, mapping, reducing, collecting to a list, etc.).

Example 1: Grouping Elements and Counting Them

To group elements based on a key and count the number of elements in each group:

Map<String, Long> result = items.stream()
    .collect(Collectors.groupingBy(
        item -> item.getCategory(), // Classifier
        Collectors.counting()       // Downstream collector
    ));
  • This produces a map where the key is the category, and the value is the count of items in that category.

Example 2: Group and Collect as a List

If you want to group the elements and collect them in lists:

Map<String, List<Item>> result = items.stream()
    .collect(Collectors.groupingBy(
        item -> item.getCategory(), // Classifier
        Collectors.toList()         // Downstream collector
    ));
  • Groups all elements into lists under their respective categories.

Example 3: Group and Use Summarizing Collector

To produce a statistical summary (e.g., count, sum, min, max, average) for each group:

Map<String, DoubleSummaryStatistics> result = items.stream()
    .collect(Collectors.groupingBy(
        item -> item.getCategory(), // Classifier
        Collectors.summarizingDouble(Item::getPrice) // Summarizing collector
    ));
  • This gives a map where each group has a DoubleSummaryStatistics object that includes the sum, count, min, max, and average for the prices in that group.

Example 4: Group and Reduce Values

To group elements and simultaneously reduce the values for each group:

Map<String, Optional<Item>> result = items.stream()
    .collect(Collectors.groupingBy(
        item -> item.getCategory(),                      // Classifier
        Collectors.reducing((item1, item2) -> 
            item1.getPrice() > item2.getPrice() ? item1 : item2) // Downstream: Find max price
    ));
  • This produces a map where each category has an Optional<Item> representing the item with the highest price.

Example 5: Multi-Level Grouping

You can nest multiple groupingBy collectors to perform hierarchical grouping:

Map<String, Map<String, List<Item>>> result = items.stream()
    .collect(Collectors.groupingBy(
        Item::getCategory,        // First-level group by category
        Collectors.groupingBy(Item::getType) // Second-level group by type
    ));
  • This creates a nested map where the first key is the category, and the value contains another map grouped by type.

Practical Example Walkthrough:

If you have a list of strings and want to:

  • Group them by their length.
  • Collect their counts using Collectors.counting().

Here’s how:

List<String> names = List.of("apple", "banana", "orange", "kiwi", "pear");

Map<Integer, Long> groupedCounts = names.stream()
    .collect(Collectors.groupingBy(
        String::length,       // Classifier: Group by string length
        Collectors.counting() // Downstream collector: Count elements
    ));

System.out.println(groupedCounts);
// Output: {4=2, 5=2, 6=1}

Key Points of Using Downstream Collectors:

  1. Flexibility: You can use different collectors (e.g., toList, toSet, counting, joining, etc.) to define how grouped elements are processed.
  2. Composition: Downstream collectors can be combined, nested, or customized using collectingAndThen or reducing.
  3. Extensibility: Custom Collector implementations can be used as downstream collectors for complex use cases.

This approach simplifies processing grouped data and eliminates the need for verbose loops or manual grouping logic.

How to Create a Custom Date Comparator in Java

To create a custom date comparator in Java, you can follow these steps:

1. Understand the Requirements

A date comparator is used to sort objects based on date values. For instance, consider a User class that has a Date field (e.g., ). We’ll compare and sort User instances by that date. birthDate

2. Define a Custom Comparator

In Java, you can create a Comparator by implementing the compare method or using lambda expressions along with the Comparator utility.

Example Code for Custom Date Comparator:

Here’s an example of creating a custom date comparator for sorting objects by date:

import java.util.*;
import java.util.stream.Stream;
import java.text.SimpleDateFormat;

public class CustomDateComparatorExample {

    public static void main(String[] args) throws Exception {

        // Sample date format and users with dates
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Stream<User> usersStream = Stream.of(
                new User("John", dateFormat.parse("1993-05-12")),
                new User("Rose", dateFormat.parse("1994-11-28")),
                new User("Adam", dateFormat.parse("1987-07-15"))
        );

        // Custom Date Comparator using Comparator.comparing
        usersStream
                .sorted(Comparator.comparing(User::getBirthDate))
                .forEach(System.out::println);
    }

    static class User {
        String name;
        Date birthDate;

        User(String name, Date birthDate) {
            this.name = name;
            this.birthDate = birthDate;
        }

        String getName() {
            return name;
        }

        Date getBirthDate() {
            return birthDate;
        }

        @Override
        public String toString() {
            return "User{" + "name='" + name + '\'' +
                    ", birthDate=" + birthDate + '}';
        }
    }
}

Explanation:

  1. Date Field ()birthDate:
    • Replaced age with a Date field () in the User class to sort based on dates. birthDate
  2. Custom Comparator:
    • Used Comparator.comparing() to directly compare the field. birthDate
    • It simplifies creating a comparator for a specific field, which in this case is a Date object.
  3. Sorted Stream:
    • The Stream.sorted() function is applied with our custom comparator. It ensures the stream of User objects is sorted.

Alternative: Manually Implement the Comparator

You can define the comparator manually for more control:

// Custom Comparator Implementation
Comparator<User> dateComparator = new Comparator<User>() {
    @Override
    public int compare(User u1, User u2) {
        return u1.getBirthDate().compareTo(u2.getBirthDate());
    }
};

// Usage
usersStream.sorted(dateComparator).forEach(System.out::println);

This is especially useful if you need more complex comparison logic (e.g., null handling or multi-level comparison).

Points to Remember:

  • Null Safety: Always handle null dates to avoid. Use Comparator.nullsFirst() or Comparator.nullsLast() when necessary. NullPointerException
    Comparator.comparing(User::getBirthDate, Comparator.nullsFirst(Date::compareTo));
    
  • Custom Date Format: Adjust the date format as needed using SimpleDateFormat, LocalDate, or other relevant classes from java.time.
    With this knowledge, you can tailor the comparator to fit any specific user-defined date or object sorting needs!

How to Use TreeMap for Sorted Key Access in Java

The TreeMap class in Java is part of the java.util package and provides an implementation of the Map interface that keeps its keys sorted in a natural order (according to ) or a custom order (defined by a Comparator, if provided during construction)Comparable. It’s commonly used when you need to access keys in sorted order efficiently.
Here’s a guide on how to use TreeMap for sorted key access in Java:

Key Features of TreeMap

  1. Maintains sorted order of keys.
  2. Implements the SortedMap and NavigableMap interfaces.
  3. Operates based on a Red-Black Tree, ensuring efficient sorting and lookup (O(log n) for most operations).

Basic Usage

Follow these steps to use TreeMap for sorted key access:

1. Create a TreeMap

You can create a TreeMap object with or without a custom comparator.

import java.util.*;

public class TreeMapExample {
    public static void main(String[] args) {
        // Natural ordering (keys must implement Comparable)
        TreeMap<Integer, String> treeMap = new TreeMap<>();

        // Custom comparator (e.g., descending order)
        TreeMap<Integer, String> customTreeMap = new TreeMap<>(Comparator.reverseOrder());
    }
}

2. Add Key-Value Pairs

Adding elements to a TreeMap is straightforward, using the put() method.

treeMap.put(3, "Three");
treeMap.put(1, "One");
treeMap.put(2, "Two");

The elements will automatically be stored in ascending order of keys.

3. Iterate Over Sorted Entries

The entries in the TreeMap can be accessed in sorted order.

for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
    System.out.println(entry.getKey() + " -> " + entry.getValue());
}

Output:

1 -> One
2 -> Two
3 -> Three

4. Access Specific Portions of the Map

The TreeMap provides powerful methods to access subsets of keys and values:

  • headMap(K toKey, boolean inclusive): Get keys less than a given key.
  • tailMap(K fromKey, boolean inclusive): Get keys greater than a given key.
  • subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive): Get keys in a given range.

Example:

System.out.println("Keys less than 3: " + treeMap.headMap(3).keySet());
System.out.println("Keys greater than or equal to 2: " + treeMap.tailMap(2).keySet());
System.out.println("Keys between 1 (inclusive) and 3 (exclusive): " 
                   + treeMap.subMap(1, true, 3, false).keySet());

Output:

Keys less than 3: [1, 2]
Keys greater than or equal to 2: [2, 3]
Keys between 1 (inclusive) and 3 (exclusive): [1, 2]

5. Use NavigableMap Methods

The TreeMap also implements the NavigableMap interface, offering methods for navigation:

  • firstKey() / lastKey(): Get the smallest/largest key.
  • lowerKey(key) / higherKey(key): Get the keys just below/above a given key.
  • floorKey(key) / ceilingKey(key): Get keys less than/greater than or equal to the given key.

Example:

System.out.println("First key: " + treeMap.firstKey());
System.out.println("Last key: " + treeMap.lastKey());
System.out.println("Key just below 3: " + treeMap.lowerKey(3));
System.out.println("Key just above 2: " + treeMap.higherKey(2));

Output:

First key: 1
Last key: 3
Key just below 3: 2
Key just above 2: 3

6. Remove Items

You can remove specific entries using the remove(key) method.

treeMap.remove(2); // Removes the key "2"
System.out.println(treeMap);

Output:

{1=One, 3=Three}

Example: Full Program

package org.kodejava.util;

import java.util.*;

public class TreeMapExample {
    public static void main(String[] args) {
        // Create a TreeMap
        TreeMap<Integer, String> treeMap = new TreeMap<>();

        // Add elements
        treeMap.put(3, "Three");
        treeMap.put(1, "One");
        treeMap.put(2, "Two");

        // Iterate over TreeMap
        System.out.println("TreeMap in ascending order:");
        for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }

        // Access portions of the map
        System.out.println("Keys less than 2: " + treeMap.headMap(2).keySet());
        System.out.println("Keys greater than or equal to 2: " + treeMap.tailMap(2).keySet());

        // Use NavigableMap methods
        System.out.println("First key: " + treeMap.firstKey());
        System.out.println("Last key: " + treeMap.lastKey());
    }
}

Output:

TreeMap in ascending order:
1 -> One
2 -> Two
3 -> Three
Keys less than 2: [1]
Keys greater than or equal to 2: [2, 3]
First key: 1
Last key: 3

Things to Remember

  1. Keys must be Comparable or you must provide a Comparator during construction.
  2. Null keys are not allowed in TreeMap, but null values are permitted.
  3. Use TreeMap when you need sorted access; otherwise, HashMap is a better choice for performance.

How to Encode and Decode URLs in Java

In Java, you can encode and decode URLs using the java.net.URLEncoder and java.net.URLDecoder classes. These classes handle encoding and decoding in compliance with the application/x-www-form-urlencoded MIME type.
Here’s how you can encode and decode URLs:

Code Example

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.net.URLDecoder;

public class URLEncoderDecoderExample {

    public static void main(String[] args) {
        try {
            // The String to be encoded
            String url = "https://example.com/query?name=John Doe&age=25";

            // Encoding URL
            String encodedUrl = URLEncoder.encode(url, "UTF-8");
            System.out.println("Encoded URL: " + encodedUrl);

            // Decoding URL
            String decodedUrl = URLDecoder.decode(encodedUrl, "UTF-8");
            System.out.println("Decoded URL: " + decodedUrl);

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace(); // Handle exception if unsupported encoding is provided
        }
    }
}

Explanation:

  1. Encoding:
    • The URLEncoder.encode() method encodes special characters in the URL to make it safe for transmission over the network.
    • UTF-8 is typically used as the charset.
  2. Decoding:
    • The URLDecoder.decode() method decodes the string back to its original format.

Sample Output:

If the input is:

https://example.com/query?name=John Doe&age=25

After encoding:

https%3A%2F%2Fexample.com%2Fquery%3Fname%3DJohn+Doe%26age%3D25

After decoding:

https://example.com/query?name=John Doe&age=25

Notes:

  • Replace spaces with + in the encoded string. This is because spaces are not typically allowed in URLs, and encoding replaces them.
  • Use "UTF-8" because it’s the most widely used and supports all Unicode characters.

How to Resolve a Domain Name in Java

Here are common ways to resolve domain names in Java, from simplest to more advanced use cases.

Basic A/AAAA record lookup (IPv4/IPv6)

  • Uses the system resolver and OS DNS settings.
  • Returns all IPs (both IPv4 and IPv6 where available).
import java.net.InetAddress;
import java.net.UnknownHostException;

public class DnsLookup {
    public static void main(String[] args) {
        String host = "example.com";
        try {
            InetAddress[] addresses = InetAddress.getAllByName(host);
            for (InetAddress addr : addresses) {
                System.out.println(addr.getHostAddress());
            }
        } catch (UnknownHostException e) {
            System.err.println("DNS lookup failed: " + e.getMessage());
        }
    }
}

Notes for InetAddress:

  • No direct per-call timeout configuration (it relies on OS resolver timeouts).
  • Caching is controlled by security properties:
    • -Dnetworkaddress.cache.ttl=60 (seconds; -1 = forever; default often JVM-dependent)
    • -Dnetworkaddress.cache.negative.ttl=10
  • Prefer IPv6: -Djava.net.preferIPv6Addresses=true

Asynchronous lookups

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.CompletableFuture;

public class AsyncDns {
    public static CompletableFuture<InetAddress[]> resolve(String host) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return InetAddress.getAllByName(host);
            } catch (UnknownHostException e) {
                throw new RuntimeException(e);
            }
        });
    }
}

Reverse DNS (PTR)

  • Basic: addr.getHostName() may trigger reverse lookup (can be slow or cached).
InetAddress addr = InetAddress.getByName("93.184.216.34");
String reverse = addr.getHostName(); // may do a PTR lookup

Query specific DNS record types (MX, TXT, SRV, PTR) or specific DNS servers

Option 1: JNDI DNS (built-in, configurable)

import javax.naming.directory.*;
import javax.naming.*;
import java.util.Hashtable;

public class JndiDns {
    public static void main(String[] args) throws NamingException {
        String domain = "example.com";
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
        // Use specific DNS server(s) (optional)
        env.put(Context.PROVIDER_URL, "dns://8.8.8.8 dns://1.1.1.1");
        // Timeouts in milliseconds (optional)
        env.put("com.sun.jndi.dns.timeout.initial", "2000");
        env.put("com.sun.jndi.dns.timeout.retries", "1");

        DirContext ctx = new InitialDirContext(env);
        Attributes attrs = ctx.getAttributes(domain, new String[] {"MX", "TXT", "A"});
        Attribute mx = attrs.get("MX");
        if (mx != null) {
            for (int i = 0; i < mx.size(); i++) System.out.println("MX: " + mx.get(i));
        }
        Attribute txt = attrs.get("TXT");
        if (txt != null) {
            for (int i = 0; i < txt.size(); i++) System.out.println("TXT: " + txt.get(i));
        }
        Attribute a = attrs.get("A");
        if (a != null) {
            for (int i = 0; i < a.size(); i++) System.out.println("A: " + a.get(i));
        }
    }
}

Notes:

  • JNDI DNS supports MX, TXT, SRV, CNAME, PTR, etc.
  • You can set specific DNS servers via PROVIDER_URL.

Option 2: Use a dedicated DNS library (e.g., dnsjava)

  • Recommended for fine control, timeouts, EDNS, DNSSEC (if needed), or custom resolvers.

Maven Dependency:

<dependency>
    <groupId>dnsjava</groupId>
    <artifactId>dnsjava</artifactId>
    <version>3.6.3</version>
    <type>bundle</type>
</dependency>

Lookup A/AAAA with custom resolver and timeout:

import org.xbill.DNS.*;

public class DnsJavaExample {
    public static void main(String[] args) throws Exception {
        String domain = "example.com";
        Resolver resolver = new SimpleResolver("8.8.8.8");
        resolver.setTimeout(Duration.ofSeconds(2));
        Name name = Name.fromString(domain + ".");
        Record[] records = new Lookup(name, Type.A).run();
        if (records != null) {
            for (Record r : records) System.out.println(r.rdataToString());
        }
    }
}

SRV/TXT example:

import org.xbill.DNS.*;

Name srvName = Name.fromString("_sip._tcp.example.com.");
Record[] srv = new Lookup(srvName, Type.SRV).run();
if (srv != null) {
    for (Record r : srv) System.out.println(r.rdataToString());
}

Name txtName = Name.fromString("example.com.");
Record[] txt = new Lookup(txtName, Type.TXT).run();
if (txt != null) {
    for (Record r : txt) System.out.println(r.rdataToString());
}

Spring/Jakarta usage example (service component)

import org.springframework.stereotype.Service;
import java.net.InetAddress;

@Service
public class DnsService {
    public String[] resolve(String host) {
        try {
            return java.util.Arrays.stream(InetAddress.getAllByName(host))
                    .map(InetAddress::getHostAddress)
                    .toArray(String[]::new);
        } catch (Exception e) {
            return new String[0];
        }
    }
}

Practical tips

  • Retry logic: DNS failures are often transient. Consider simple retries with backoff when appropriate.
  • Validate input: Ensure the host is a valid hostname to avoid unnecessary exceptions.
  • Respect caching: Tune networkaddress.cache.ttl for your runtime environment to balance freshness and performance.
  • Split-horizon DNS: In containerized/cloud setups, behavior may differ between environments. Test where it runs.
  • Don’t hardcode IPs unless necessary; prefer hostnames to benefit from DNS-based failover.