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!