How do I use List.of, Set.of, and Map.of factory methods?

The List.of, Set.of, and Map.of factory methods were introduced in Java 9 to create immutable collections in a simpler and more concise way. These methods directly create unmodifiable instances of List, Set, or Map.

Usage of List.of

The List.of method is used to create an unmodifiable List containing the provided elements. The key characteristics are:

  • The list is immutable, meaning you cannot modify its contents (e.g., add, remove, replace elements).
  • If you attempt to modify the list, a java.lang.UnsupportedOperationException will be thrown.

Key Points:

  1. It is null-safety aware, meaning null is not allowed as an element.
  2. The created list maintains the insertion order.

Example

List<String> names = List.of("Rosa", "John", "Mary", "Alice");
System.out.println(names); // Output: [Rosa, John, Mary, Alice]

names.add("Bob"); // Throws java.lang.UnsupportedOperationException

Note: Attempting to pass a null element will result in NullPointerException.


Usage of Set.of

The Set.of method creates an immutable Set containing the given elements. The key characteristics are:

  • The set is unmodifiable, so you cannot add or remove elements after creation.
  • It does not allow duplicate elements.
  • It does not allow null elements.

Key Points:

  1. A java.lang.IllegalArgumentException is thrown if duplicate elements are provided during creation.
  2. Since a Set does not guarantee order, the order of elements in the resulting set is not predictable.

Example

Set<String> items = Set.of("apple", "banana", "orange");
System.out.println(items); // Output: [apple, banana, orange] (order may vary)

items.add("kiwi"); // Throws java.lang.UnsupportedOperationException

Note: If duplicate elements are passed, an exception will be thrown:

Set.of("apple", "banana", "apple"); // Throws IllegalArgumentException

Usage of Map.of

The Map.of method creates an immutable Map with key-value pairs. The characteristics are:

  • The created map is unmodifiable.
  • Both null keys and null values are not allowed.
  • Duplicate keys are not allowed, and attempting to use duplicate keys will throw IllegalArgumentException.

Example

Map<String, Integer> map = Map.of("John", 25, "Mary", 30, "Alice", 27, "Rosa", 22);

System.out.println(map); 
// Output (order may vary): {John=25, Mary=30, Alice=27, Rosa=22}

map.put("Bob", 31); // Throws java.lang.UnsupportedOperationException

Note: Avoid duplicate keys. For example:

Map.of("Key1", 1, "Key2", 2, "Key1", 3); // Throws IllegalArgumentException

Advantages of List.of, Set.of, and Map.of

  1. Immutable collections help ensure thread safety without additional synchronization.
  2. Improved code conciseness compared to using Collections.unmodifiableList, Collections.unmodifiableSet, or Collections.unmodifiableMap.
  3. Simplified initialization with a clean and readable API.

For larger maps (more than 10 entries), use Map.ofEntries for better clarity:

Map<String, Integer> largeMap = Map.ofEntries(
    Map.entry("Alice", 27),
    Map.entry("John", 25),
    Map.entry("Mary", 30),
    Map.entry("Rosa", 22)
);

How do I use the RandomGenerator API introduced in JDK 17?

The RandomGenerator API, introduced in JDK 17, provides a simpler and more flexible way to work with random number generation by centralizing the different strategies for producing random numbers under a single interface (RandomGenerator). This allows developers to access multiple random number generation algorithms in a standardized manner. Additionally, it improves upon the legacy java.util.Random class.

Here’s how you can use the RandomGenerator API:

Key Classes/Interfaces in the RandomGenerator API

  • RandomGenerator (Interface): Defines methods for generating random values of different types (e.g., nextInt(), nextDouble(), etc.).
  • RandomGeneratorFactory (Class): Used to instantiate various implementations of the RandomGenerator interface.
  • SplittableRandom, ThreadLocalRandom, SecureRandom: Core implementations of RandomGenerator.
  • Random: Although part of the legacy API, it now implements RandomGenerator in JDK 17.

Code Example: Basic Usage with RandomGenerator

Here’s a basic example of how to use RandomGenerator:

package org.kodejava.util.random;

import java.util.random.RandomGenerator;

public class RandomGeneratorExample {
    public static void main(String[] args) {
        // Retrieve the default random generator
        RandomGenerator generator = RandomGenerator.getDefault();

        // Generate random numbers of various types
        int randomInt = generator.nextInt();          // Random integer
        double randomDouble = generator.nextDouble(); // Random double in [0.0, 1.0)
        long randomLong = generator.nextLong();       // Random long
        boolean randomBoolean = generator.nextBoolean(); // Random boolean

        // Print results
        System.out.println("Random integer: " + randomInt);
        System.out.println("Random double: " + randomDouble);
        System.out.println("Random long: " + randomLong);
        System.out.println("Random boolean: " + randomBoolean);

        // Generate a random integer within a range (0 to 100)
        int rangedInt = generator.nextInt(101);
        System.out.println("Random integer in range [0, 100]: " + rangedInt);
    }
}

Using RandomGeneratorFactory for Choosing a Specific Algorithm

The RandomGeneratorFactory class allows you to select specific implementations of RandomGenerator. This can help you use different algorithms tailored to your use case.

Example:

package org.kodejava.util.random;

import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;

public class CustomRandomGeneratorExample {
    public static void main(String[] args) {
        // List all available random generator algorithms
        System.out.println("Available Random Generators:");
        RandomGeneratorFactory.all().forEach(factory -> {
            System.out.println("- " + factory.name());
        });

        // Create a specific random generator (e.g., `L64X128MixRandom`)
        RandomGenerator generator = RandomGeneratorFactory.of("L64X128MixRandom").create();

        // Generate random values
        System.out.println("Random value: " + generator.nextDouble());
    }
}

Notes

  1. Default Generator: RandomGenerator.getDefault() provides a default random number generator.
  2. Thread-Safety: Consider java.util.concurrent.ThreadLocalRandom for generating random numbers in a multithreaded context.
  3. Secure Random Numbers: Use java.security.SecureRandom for cryptographically secure random numbers.
  4. Performance: If you need high-performance statistically random values, explore generators like L128X256MixRandom.

Benefits of Using the RandomGenerator API

  • Multiple Algorithms: No need to rely solely on java.util.Random.
  • Improved Extensibility: Selecting different implementations for specific use cases is easier.
  • Consistency: Unified method signatures across implementations enable flexible and consistent code.

How do I use record with generics?

Using records with generics in Java allows you to create immutable data structures while also providing the benefits of generics, such as type safety and flexibility. Since Java 16, the record feature was introduced, and it can be combined with generics like other classes. Here’s an example of how to use records with generics.

Syntax

To define a record with generics, you specify the type parameter(s) in the record declaration just like in a class declaration.

public record MyRecord<T>(T value) { }

Examples

1. Basic Generic Record

A simple record that accepts a generic type parameter:

// Define record with a generic parameter T
public record Box<T>(T content) { }

Usage:

public class Main {
    public static void main(String[] args) {
        // Create a record with a String type
        Box<String> stringBox = new Box<>("Hello, generics!");
        System.out.println(stringBox.content()); // Output: Hello, generics!

        // Create a record with an Integer type
        Box<Integer> integerBox = new Box<>(123);
        System.out.println(integerBox.content()); // Output: 123
    }
}

2. Records with Multiple Generics

You can define records with multiple type parameters, just like generic classes:

// Define a record with two generic types
public record Pair<K, V>(K key, V value) { }

Usage:

public class Main {
    public static void main(String[] args) {
        // Create a Pair with String and Integer
        Pair<String, Integer> pair = new Pair<>("Age", 30);
        System.out.println(pair.key() + ": " + pair.value()); // Output: Age: 30

        // Create a Pair with two different types
        Pair<Double, String> anotherPair = new Pair<>(3.14, "Pi");
        System.out.println(anotherPair.key() + " -> " + anotherPair.value()); // Output: 3.14 -> Pi
    }
}

3. Generics with Constraints

Generic records can include bounded type parameters to restrict the types allowed:

// Generic type T is constrained to subclasses of Number
public record NumericBox<T extends Number>(T number) { }

Usage:

public class Main {
    public static void main(String[] args) {
        // Only Number or subclasses of Number are allowed
        NumericBox<Integer> intBox = new NumericBox<>(42);
        System.out.println(intBox.number()); // Output: 42

        NumericBox<Double> doubleBox = new NumericBox<>(3.14);
        System.out.println(doubleBox.number()); // Output: 3.14

        // Compiler error: String is not a subclass of Number
        // NumericBox<String> stringBox = new NumericBox<>("Not a number");
    }
}

4. Working with Wildcards

You can use wildcards in generic records when specifying their types:

public class Main {
    public static void main(String[] args) {
        // Using a wildcard
        Box<?> anyBox = new Box<>("Wildcard content");
        System.out.println(anyBox.content()); // Output: Wildcard content

        // Using bounded wildcards
        NumericBox<? extends Number> numBox = new NumericBox<>(42);
        System.out.println(numBox.number()); // Output: 42
    }
}

Benefits of Using Generics with Records

  1. Type Safety: With generics, the compiler ensures the record is used correctly for the intended type.
  2. Reusability: You can use the same record with different data types.
  3. Immutability: Records’ inherent immutability, coupled with generics, allows you to encapsulate type-safe, immutable data structures.

This approach works seamlessly with the other features of records, such as pattern matching and compact constructors. Let me know if you’d like more advanced scenarios!

How do I use String.strip(), isBlank() and lines() methods?

The String class in Java provides the methods strip(), isBlank(), and lines(), which were introduced in Java 11. These methods are useful for managing whitespaces, checking for blank strings, and processing multi-line strings.

1. strip()

The strip() method removes leading and trailing whitespaces from a string. Unlike trim(), it uses Unicode-aware whitespace handling, making it more robust for international characters.

Example:

public class StringStripExample {
    public static void main(String[] args) {
        String str = " \u2009Hello World  "; // Unicode whitespace
        System.out.println(str.strip());      // Outputs: "Hello World"
        System.out.println(str.stripLeading()); // Removes leading spaces: "Hello World  "
        System.out.println(str.stripTrailing()); // Removes trailing spaces: " \u2009Hello World"
    }
}

Key Point:

  • strip() differs from trim() in that it removes all Unicode whitespace, not just ASCII spaces.

2. isBlank()

The isBlank() method checks whether a string is empty or contains only whitespaces. This includes Unicode whitespace and helps to quickly validate string content.

Example:

public class StringIsBlankExample {
    public static void main(String[] args) {
        String empty = "   "; // Contains whitespaces
        System.out.println(empty.isBlank()); // Outputs: true

        String nonBlank = "Hello";
        System.out.println(nonBlank.isBlank()); // Outputs: false

        String unicodeSpace = "\u2009"; // Unicode whitespace
        System.out.println(unicodeSpace.isBlank()); // Outputs: true
    }
}

Key Point:

  • isBlank() is stronger than isEmpty() because it treats strings with only whitespace as blank, whereas isEmpty() considers only an empty string ("").

3. lines()

The lines() method breaks a multi-line string into a stream of lines, using the platform’s line terminator (e.g., \n or \r\n) to split the string.

Example:

public class StringLinesExample {
    public static void main(String[] args) {
        String multiLineString = "Hello\nWorld\nJava 11";

        // Use 'lines()' to split the multi-line string
        multiLineString.lines().forEach(System.out::println);

        // Output:
        // Hello
        // World
        // Java 11
    }
}

Key Points:

  • lines() splits the string into lines and returns a Stream<String>.
  • It can be combined with stream operations like filter(), map(), and forEach().

Combining Methods for Common Use Cases

Here’s how you can combine them:

Trim and Check Blank:

public class StringExample {
    public static void main(String[] args) {
        String input = "   ";
        if (input.strip().isBlank()) {
            System.out.println("Input is blank or empty!");
        } else {
            System.out.println("Input: " + input.strip());
        }
    }
}

Processing Multi-Line Strings:

public class MultiLineExample {
    public static void main(String[] args) {
        String text = "  Line 1  \n  Line 2  \n  Line 3  ";

        text.lines()
            .map(String::strip) // Clean up each line
            .forEach(System.out::println);

        // Output:
        // Line 1
        // Line 2
        // Line 3
    }
}

Summary of Functionalities:

  • strip(): Removes leading/trailing Unicode whitespace.
  • isBlank(): Checks if a string is empty or only whitespace.
  • lines(): Processes multi-line strings by splitting them into lines.

How do I use compact constructors in records?

Compact constructors in records are a concise way to initialize and validate fields of a record in Java. Unlike standard constructors, compact constructors omit the parameter list, as they operate directly on the record’s declared components.

Here’s how to use compact constructors in records:

1. Syntax:

A compact constructor is declared without parentheses after the constructor name. Inside the constructor body, you can initialize or validate fields of the record.

Example:

public record Person(String name, int age) {
    // Compact constructor
    public Person {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name cannot be null or blank");
        }
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
}

2. How It Works:

  • The compact constructor implicitly takes all the components of the record as parameters.
  • You can directly access these fields without explicitly specifying them as parameters since they are already “properties” of the record.
  • Unlike regular constructors, compact constructors aim to reduce boilerplate validations and/or additional initialization.

In the above example:

  • name and age are automatically initialized in the generated constructor, but the compact constructor validates them before allowing that to happen.
  • If the validations fail, the constructor throws exceptions.

3. Special Notes:

  • The Java compiler ensures that the compact constructor assigns values to all record components before the constructor completes execution.
  • You cannot directly modify the record components since they are implicitly final. What you can do is validate or make use of the incoming values.

4. Why Use Compact Constructors?

Compact constructors are helpful when:

  1. You need to validate fields during record initialization.
  2. You want to add extra logic (like logging) to the generated canonical constructor of the record.
  3. You want to reduce boilerplate by avoiding redundant parameter declarations.

5. Example with Additional Fields:

If the record has additional fields beyond what is declared in the record header, those can also be initialized in the compact constructor:

public record Rectangle(int length, int width) {
    private static final int MINIMUM_SIZE = 1;

    public Rectangle {
        // Validation
        if (length < MINIMUM_SIZE || width < MINIMUM_SIZE) {
            throw new IllegalArgumentException("Dimensions must be at least " + MINIMUM_SIZE);
        }
    }
}

Summary:

Compact constructors in records:

  • Automatically operate on the fields declared as record components.
  • Reduce boilerplate for constructor declarations.
  • Are ideal for validation and pre-processing logic.
  • Must initialize or ensure that all components are correctly set before completing.