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.

How do I use sealed classes to control inheritance?

In Java, sealed classes are a feature introduced in Java 15 (as a preview and finalized in Java 17) that allows you to control inheritance by specifying which classes or interfaces can extend or implement a given class or interface. This makes your class hierarchy more predictable and easier to reason about.

Using sealed classes involves the following key concepts:

1. Declaration of a Sealed Class

A class can be declared as sealed, which means that only a specific set of classes (declared permits) can extend that class. Here’s the basic syntax:

public sealed class ParentClass permits ChildA, ChildB {
    // Class code
}

Here, only ChildA and ChildB (declared in permits) are allowed to extend ParentClass. This ensures complete control over the inheritance structure of your class.


2. The Role of Permitted Subclasses

Each subclass specified in the permits clause must do one of the following to complete the sealed hierarchy:

  • Declare itself as final (no further inheritance is allowed).
  • Declare itself as sealed (allowing further controlled inheritance).
  • Declare itself as non-sealed (allowing unrestricted inheritance).

Examples of each:

Final Subclass:

public final class ChildA extends ParentClass {
    // Class code
}

Sealed Subclass:

public sealed class ChildB extends ParentClass permits GrandChild {
    // Class code
}

public final class GrandChild extends ChildB {
    // Class code
}

Non-Sealed Subclass:

public non-sealed class ChildC extends ParentClass {
    // Class code
}

In the case of non-sealed, ChildC and its subclasses can be freely inherited, bypassing the restrictions of sealing.


3. Key Features and Benefits of Sealed Classes

  1. Ensure Complete Class Hierarchy Control:
    • By listing all allowed subclasses, you can restrict who can build upon your functionality.
    • Simplifies reasoning about the class hierarchy in complex systems.
  2. Improved Exhaustiveness Checking:
    • When used with instanceof or switch expressions, the compiler knows all the possible subclasses (because they’ve been explicitly listed).
    • For example, pattern matching with switch:
    public String process(ParentClass obj) {
         return switch (obj) {
             case ChildA a -> "ChildA";
             case ChildB b -> "ChildB";
             default -> throw new IllegalStateException("Unexpected value: " + obj);
         };
     }
    
  3. Enforces Encapsulation and API Design Consistency:
    • Encourages developers to think hard about which subclasses make sense.
  4. Useful for Modeling Closed Systems:
    • Great for scenarios where the possible subclasses represent a closed set of types, such as states in a state machine.

Example: Sealed Class for a Shape Hierarchy

Here is a practical example of using sealed classes in a geometric shape hierarchy:

public sealed class Shape permits Circle, Rectangle, Square {
    // Common shape fields and methods
}

public final class Circle extends Shape {
    // Circle-specific fields and methods
}

public final class Rectangle extends Shape {
    // Rectangle-specific fields and methods
}

public final class Square extends Shape {
    // Square-specific fields and methods
}

If someone tries to create a new subclass of Shape outside of those specified in permits, a compilation error will occur.


4. Rules and Restrictions

  • A sealed class must use the permits clause unless all permitted implementations are within the same file.
  • The permitted classes must extend the sealed class or implement the sealed interface.
  • Subclasses of sealed classes located in different packages must be public.
  • All permitted classes are resolved at compile time.

Summary

Java’s sealed classes provide you with a powerful tool to control inheritance in your programs by explicitly defining the classes that are allowed to extend or implement a particular class or interface. They make your code more robust, predictable, and maintainable by restricting which subclasses can exist in a hierarchy. Use them when you want tight control over a class hierarchy or when modeling scenarios with a limited set of possibilities.

How do I use enhanced instanceof pattern matching?

Enhanced instanceof pattern matching, introduced in Java 16 (as a preview feature) and finalized in Java 17, allows you to combine type checking with type casting, reducing boilerplate code and making it more concise and readable.

Here’s how you can use enhanced instanceof pattern matching:

  1. Basic Usage:
    Instead of separately checking if an object is an instance of a class and then casting it, you can do both in one step using the pattern matching feature. The syntax is:

    if (obj instanceof Type variableName) {
       // variableName is automatically cast to Type
    }
    

    Example:

    Object obj = "Hello, Java!";
    
    if (obj instanceof String str) { // This checks and casts obj to String
       System.out.println("String length: " + str.length());
    } else {
       System.out.println("Not a string.");
    }
    

    This eliminates the need for explicit type casting.

  2. Combine with Logical Operators:
    You can combine the pattern matching with additional conditions using logical operators like && or ||.

    Example:

    Object obj = "Patterns in Java";
    
    if (obj instanceof String str && str.length() > 10) {
       System.out.println("String is longer than 10 characters: " + str);
    } else {
       System.out.println("String is too short or not a string at all.");
    }
    

    In this case, the str variable is only in scope if both conditions are true.

  3. Scope of the Pattern Variable:

    • The pattern variable (e.g., str in the examples above) is only accessible within the block where the pattern matching is true.
    • Outside of the if block, the variable doesn’t exist.
  4. Negating with !instanceof:
    Pattern matching itself cannot be negated directly (no “not instanceof”), but you can invert the condition like this:

    if (!(obj instanceof String)) {
       System.out.println("Not a string.");
    }
    
  5. Using Pattern Matching in switch:
    Starting from Java 17 (as a preview) and improved in later versions, you can use pattern matching in switch statements for more powerful expressions. For example:

    Object obj = "Java 17";
    
    switch (obj) {
       case String str && str.length() > 5 -> System.out.println("Long string: " + str);
       case String str -> System.out.println("Short string: " + str);
       default -> System.out.println("Not a string.");
    }
    

    This allows a combination of pattern matching and conditionals directly within switch.


Benefits of Enhanced instanceof Pattern Matching

  • Reduction of Boilerplate Code: By avoiding explicit casting and declaring new variables.
  • Improved Readability: Simplifies conditional checks by combining the instance check and cast in one step.
  • Type Safety: Provides better compile-time safety for the variables you use after a cast.

Recap of the Code Features

From the files you’ve referenced:

  1. PatternMatchingExample.java demonstrates simple pattern matching with instanceof, where the type check and assignment are done in one step.
  2. PatternMatchingExampleCombine.java shows combining pattern matching with additional conditions (e.g., &&).

Both examples illustrate the practical and concise approach to type checking and casting introduced via enhanced instanceof pattern matching.

How do I use Optional for cleaner null checks?

Using Optional in Java can help streamline and simplify null checks, avoiding potential NullPointerException issues and making the code more readable and elegant. Optional is particularly useful when you want to express the possibility of an absent value explicitly in the API and handle such scenarios gracefully.

Here’s how you can use Optional for cleaner null checks:


1. Creating an Optional

You can create an Optional object to wrap either a non-null or null value.

Optional<String> optionalValue = Optional.of("example"); // Non-null value
Optional<String> emptyOptional = Optional.empty();       // Explicit empty optional
Optional<String> nullableOptional = Optional.ofNullable(null); // Can be null

2. Using isPresent() for Checks

Instead of if (value != null), you can use isPresent() to check for a value’s presence:

Optional<String> optionalValue = Optional.ofNullable("example");
if (optionalValue.isPresent()) {
    System.out.println("Value is present: " + optionalValue.get());
}

3. Using ifPresent() for Action

If you want to perform some operation only if a value is present, you can use ifPresent():

optionalValue.ifPresent(value -> System.out.println("Found: " + value));

This eliminates the need for explicit if checks.


4. Provide a Default Value with orElse()

You can supply a default value to use if the Optional is empty:

String result = optionalValue.orElse("Default Value");
System.out.println(result);

5. Lazy Default Value with orElseGet()

To defer the computation of the default value:

String result = optionalValue.orElseGet(() -> "Generated Default");
System.out.println(result);

6. Throw an Exception if Absent with orElseThrow()

You can ensure an exception is thrown when the value is absent:

String value = optionalValue.orElseThrow(() -> new IllegalArgumentException("Value is missing!"));

7. Transforming the Value with map()

Use map() to apply a transformation function to the contained value, without needing to check for null:

Optional<Integer> length = optionalValue.map(String::length);
length.ifPresent(len -> System.out.println("Length: " + len));

8. Chained Operations with flatMap()

If the transformation itself returns an Optional, use flatMap() to avoid nesting:

Optional<String> toUpperCaseOptional = optionalValue.flatMap(value -> Optional.of(value.toUpperCase()));
toUpperCaseOptional.ifPresent(System.out::println);

9. Filtering Values

You can filter the value based on a condition:

optionalValue.filter(value -> value.length() > 5)
             .ifPresent(value -> System.out.println("Value with sufficient length: " + value));

10. Combining Operations

Combine operations like map, filter, and orElse to handle cases cleanly in a pipeline:

String finalValue = optionalValue
                        .map(String::toUpperCase)
                        .filter(value -> value.startsWith("EX"))
                        .orElse("Default Result");
System.out.println(finalValue);

Common Use Cases:

  • Avoid nullable parameters in methods by using Optional.
  • Indicate that a return value may or may not be present, eliminating null checks on the client side.
  • Use in streams to safely process values.

By following these practices with Optional, you can reduce boilerplate code and improve the overall clarity of null safety in Java applications.