How do I use Objects.checkIndex() for safe index validation?

Objects.checkIndex, introduced in Java 11, is a utility method for safely validating an index against a given range. It simplifies index validation by throwing well-defined exceptions with meaningful error messages if the index is out of bounds.

Syntax

public static int checkIndex(int index, int length)
  • index: The index to check.
  • length: The upper bound (exclusive) of the valid index range (0 to length-1).

If the index is within bounds (index >= 0 and index < length), the method simply returns the index. Otherwise, it throws an IndexOutOfBoundsException with a clear and informative message.

Example Usage

The method can be helpful when working with arrays, lists, or other collections where you need to validate that an index is within the permissible range.

Example: Validating Array Index

package org.kodejava.util;

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        int indexToAccess = 3; // Index we want to validate

        try {
            // Validate the index
            Objects.checkIndex(indexToAccess, array.length);
            // If valid, safely access the array element
            System.out.println("Element at index " + indexToAccess + ": " + array[indexToAccess]);
        } catch (IndexOutOfBoundsException e) {
            System.err.println("Invalid index: " + e.getMessage());
        }
    }
}

Output (if indexToAccess = 3):

Element at index 3: 4

Output (if indexToAccess = 10, for example):

Invalid index: Index 10 out of bounds for length 5

When to Use

  • Use Objects.checkIndex when you expect to handle invalid index scenarios explicitly via exceptions instead of relying on implicit array or list behavior.
  • It provides better readable error messages compared to manually performing index checks and throwing custom exceptions.
  • It is typically used in contexts where throwing an IndexOutOfBoundsException is appropriate for invalid input.

Benefits

  • Simpler and cleaner code for index validation.
  • Automatically provides meaningful exception messages.
  • Ensures a uniform approach to index validation in Java codebases.

Notes

  • This method checks only one index at a time; use it in iterative or batch processing when validating multiple indices.
  • It is part of the java.util.Objects utility class and requires Java 11 or later.

How do I use String.indent() to format output text?

The String.indent(int n) method in Java is a useful tool for formatting multi-line strings by adjusting the amount of leading space (indentation) on each line. Introduced in Java 12, it performs the following for the specified n indentation:

  1. Positive n: Adds n spaces to the beginning of each line in the string.
  2. Negative n: Removes up to n leading spaces from each line.
  3. Zero n: Leaves the string unchanged (but trims blank lines at the start/end).

This is particularly useful for formatting text in structured views, like logs, JSON, XML, or pretty-printed output.

Example: Using String.indent to Format Output

Here is how you can use the String.indent() method:

package org.kodejava.lang;

public class StringIndentExample {
    public static void main(String[] args) {
        // Original multi-line string (no indentation)
        String text = "Line 1\nLine 2\nLine 3";

        // Adding an indent of 4 spaces
        String indented = text.indent(4);
        System.out.println("Indented by 4 spaces:\n" + indented);

        // Removing 2 leading spaces (negative indent)
        String negativeIndent = indented.indent(-2);
        System.out.println("Indented with -2 (removing spaces):\n" + negativeIndent);

        // Applying indent to empty lines
        String withEmptyLines = "Line 1\n\nLine 3";
        String indentedWithEmptyLines = withEmptyLines.indent(4);
        System.out.println("Handling empty lines:\n" + indentedWithEmptyLines);
    }
}

Explanation of Code:

  1. Adding Indentation: The first call to .indent(4) adds 4 spaces to each line.
  2. Removing Indentation: .indent(-2) deducts 2 spaces from the start of each line (if spaces exist).
  3. Empty Lines: When dealing with blank lines, indent maintains the level of indentation for such lines, adding or removing spaces as needed.

Output:

Indented by 4 spaces:
    Line 1
    Line 2
    Line 3

Indented with -2 (removing spaces):
  Line 1
  Line 2
  Line 3

Handling empty lines:
    Line 1

    Line 3

Notes:

  • Empty lines and their indentation are preserved.
  • Lines with no leading spaces are unaffected by negative indentation.
  • Leading and trailing full blank lines are removed.

When to Use:

  • To format multiline strings cleanly.
  • To produce human-readable or formatted output in tools or commands (e.g., logging, text processing).

How do I use Stream.toList() instead of collect(Collectors.toList())?

In Java 16, a convenient method Stream.toList() was introduced to simplify collecting elements of a Stream into a List. It provides a more concise alternative to collect(Collectors.toList()), which was used in older versions of Java.

Key Differences

  • Stream.toList() produces an immutable list, whereas collect(Collectors.toList()) produces a mutable list.
  • Stream.toList() guarantees immutability, meaning the resulting list cannot be structurally modified (additions, deletions, updates).
  • collect(Collectors.toList()) does not enforce immutability. It typically returns an ArrayList.

How to Replace collect(Collectors.toList()) with Stream.toList()

If you want to update your code to use Stream.toList() (introduced in Java 16), here’s how you can do it.

Using collect(Collectors.toList()) (Old Style):

package org.kodejava.util.stream;

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

public class Main {
    public static void main(String[] args) {
        List<String> result = Stream.of("a", "b", "c")
                                    .collect(Collectors.toList());
        System.out.println(result);
    }
}

Using Stream.toList() (New Style):

package org.kodejava.util.stream;

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

public class Main {
    public static void main(String[] args) {
        List<String> result = Stream.of("a", "b", "c")
                                    .toList(); // Simpler, concise, and immutable
        System.out.println(result);
    }
}

How to Modify Your Code:

  1. Replace .collect(Collectors.toList()) with .toList().
  2. Ensure your code works well with an immutable list because Stream.toList() returns a list that does not allow structural modifications.

Example Comparison:

Immutable List with Stream.toList():

List<String> result = Stream.of("a", "b", "c").toList();
result.add("d"); // Throws UnsupportedOperationException

Mutable List with collect(Collectors.toList()):

List<String> result = Stream.of("a", "b", "c").collect(Collectors.toList());
result.add("d"); // Works fine

Compatibility Note

  • If you are using Java 16 or above, prefer Stream.toList() for conciseness and immutability.
  • If you need a mutable list (e.g., you want to add or remove elements later), stick to collect(Collectors.toList()).

When to Use Each

  • Use Stream.toList() when immutability is preferred or sufficient.
  • Use collect(Collectors.toList()) when you need a list you can modify after creation.

How do I use Predicate.not() in Streams?

To use Predicate.not() in streams, you take advantage of its ability to negate an existing predicate. This can be helpful in filter operations where you want to filter out elements that match a given condition instead of including them.

Here’s how you can use Predicate.not in streams:

Basic Explanation

  1. What it does: The Predicate.not() method is a static method (added in Java 11) that creates a predicate that negates the specified predicate. Instead of writing complex logic for negation, you can directly use Predicate.not() for cleaner and more readable code.

  2. Use Case in Streams: When working with Java Streams, you often use .filter() to include elements that satisfy a condition. If you want to exclude elements that satisfy a condition, you can use Predicate.not().


Example: Using Predicate.not() in a Stream

package org.kodejava.util.function;

import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class PredicateNotExample {
    public static void main(String[] args) {
        // Define a list of integers
        List<Integer> numbers = List.of(5, 15, 8, 25, 3, 12);

        // Define a predicate to filter numbers greater than 10
        Predicate<Integer> isGreaterThan10 = number -> number > 10;

        // Use Predicate.not() to filter numbers NOT greater than 10
        List<Integer> filteredNumbers = numbers.stream()
                .filter(Predicate.not(isGreaterThan10))
                .collect(Collectors.toList());

        // Print the filtered list
        System.out.println(filteredNumbers); // Output: [5, 8, 3]
    }
}

Breakdown of the Code:

  1. Define Predicate
    A predicate is defined (isGreaterThan10) to test if a number is greater than 10.

  2. Stream Filtering

    • Using .stream() to process the list of numbers.
    • .filter(Predicate.not(isGreaterThan10)) negates the predicate, effectively including numbers less than or equal to 10.
  3. Collect Results
    The result is collected using .collect(Collectors.toList()).


Why Use Predicate.not()?

  • Improved Readability: Instead of writing a negation explicitly like x -> !isGreaterThan10.test(x), you can use Predicate.not(isGreaterThan10) for better readability.

  • Reusability: Predicate.not() can work for any predicate, making it easier to reuse your existing predicates in multiple ways.

  • Less Prone to Errors: Writing custom negation logic in lambdas may lead to errors or make the code harder to understand. Predicate.not() makes intent clear and reduces the chance of mistakes.


Notes:

  • The Predicate.not() method was introduced in Java 11. Ensure you are using Java 11 or later to use it.
  • You can apply this with any kind of predicate—numerical, string-based, or custom objects.

How do I use Collectors::teeing in Streams?

In Java, Collectors::teeing is a feature introduced in Java 12 that allows you to collect elements of a stream using two different collectors and then combine the results using a BiFunction.

Syntax:

static <T, R1, R2, R> Collector<T, ?, R> teeing(Collector<? super T, ?, R1> downstream1,
                                                Collector<? super T, ?, R2> downstream2,
                                                BiFunction<? super R1, ? super R2, R> merger)

Concept:

  1. downstream1: The first collector for processing input elements.
  2. downstream2: The second collector for processing input elements.
  3. merger: A BiFunction that merges the results of the two collectors.

The teeing collector is useful when you want to process a stream in two different ways simultaneously and combine the results.


Example 1: Calculate the Average and Sum of a Stream

Here’s how you can calculate both the sum and average of a list of integers simultaneously:

package org.kodejava.util.stream;

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

public class TeeingExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);

        var result = numbers.stream()
                .collect(Collectors.teeing(
                        Collectors.summingInt(i -> i),             // Collector 1: Sum
                        Collectors.averagingInt(i -> i),           // Collector 2: Average
                        (sum, avg) -> "Sum: " + sum + ", Avg: " + avg // Merger
                ));

        System.out.println(result); // Output: Sum: 15, Avg: 3.0
    }
}

Example 2: Get Statistics (Min and Max) from a Stream

You can create a single step operation to compute the minimum and maximum values of a stream:

package org.kodejava.util.stream;

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

public class TeeingStatsExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(3, 5, 7, 2, 8);

        var stats = numbers.stream()
                .collect(Collectors.teeing(
                        Collectors.minBy(Integer::compareTo),     // Collector 1: Get Min
                        Collectors.maxBy(Integer::compareTo),     // Collector 2: Get Max
                        (min, max) -> new int[]{min.orElse(-1), max.orElse(-1)} // Merge into an array
                ));

        System.out.println("Min: " + stats[0] + ", Max: " + stats[1]);
        // Output: Min: 2, Max: 8
    }
}

How It Works:

  • Two collectors (downstream1 and downstream2) collect the stream elements independently. For example, the first collector might compute the sum, while the second computes the average.
  • Once the stream has been fully processed, the results from both collectors are passed to the merger, which applies a transformation or combination of the two results.

Example 3: Concatenate Strings and Count Elements Simultaneously

Here’s how you can process a stream of strings to count the number of elements and also concatenate them into a single string:

package org.kodejava.util.stream;

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

public class TeeingStringExample {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie");

        var result = names.stream()
                .collect(Collectors.teeing(
                        Collectors.joining(", "),        // Collector 1: Concatenate strings
                        Collectors.counting(),           // Collector 2: Count elements
                        (joined, count) -> joined + " (Total: " + count + ")" // Merge
                ));

        System.out.println(result);  // Output: Alice, Bob, Charlie (Total: 3)
    }
}

Key Points:

  1. Stream Processing: The stream elements are processed only once but collected using two different collectors.
  2. Merger Function: The merger combines both results into a final result of your choice.
  3. Utility: Collectors::teeing is very useful when you need to perform dual aggregations in one pass over the data.

Now you’re ready to use Collectors::teeing for combining results in your streams!

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.