How to Use Objects.requireNonNull() Effectively

The Objects.requireNonNull() method is a utility provided in Java to enforce that an object is not null during runtime. It is part of the java.util.Objects class starting from Java 7 and is commonly used for validating method parameters, ensuring that null values don’t propagate and cause unexpected NullPointerExceptions later.

Here’s a detailed explanation of how to use Objects.requireNonNull() effectively:


What It Does

Objects.requireNonNull() checks whether the provided reference is null. If it is null, it throws a NullPointerException. Optionally, you can provide a custom message to make the exception more meaningful.


Methods Available

There are three main variants of Objects.requireNonNull():

  1. public static <T> T requireNonNull(T obj)
    • Throws NullPointerException if obj is null.
  2. public static <T> T requireNonNull(T obj, String message)
    • Throws NullPointerException with the provided message if obj is null.
  3. public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) (Java 8 or later)
    • Defers the creation of the message via the Supplier, which is a performance-friendly option since the message is only computed if obj is null.

When to Use It

  1. To Validate Parameters
    Use Objects.requireNonNull() at the beginning of a method to validate parameters and catch null values early.

    public void setName(String name) {
       this.name = Objects.requireNonNull(name, "Name cannot be null!");
    }
    
  2. Before Using a Field in Code
    Validate fields that are expected to be non-null before operating on them.

    public void processData(Data data) {
       Objects.requireNonNull(data, "Data must not be null before processing.");
       // process the data
    }
    
  3. Constructor Argument Validation
    When writing constructors, validate inputs immediately to ensure that your object is consistently in a valid state.

    public Example(String id) {
       this.id = Objects.requireNonNull(id, "ID must not be null.");
    }
    
  4. To Prevent Nullable Logic Elsewhere in Code
    By enforcing non-null guarantees in one place (e.g., via method validation), null checks do not need to be repeated elsewhere in the codebase.


Best Practices

  1. Always Provide a Meaningful Message
    The message should indicate what went wrong, so developers can quickly pinpoint the issue.

    public void processFile(File file) {
       Objects.requireNonNull(file, "File parameter is required.");
    }
    
  2. Use a Supplier When the Message Is Expensive to Build
    If creating the message involves non-trivial operations, use the Supplier<String> version to only compute the message when it’s actually necessary:

    public void process(String input) {
       Objects.requireNonNull(input, () -> "Input cannot be null at " + LocalDateTime.now());
    }
    
  3. Avoid Overusing It
    Don’t use Objects.requireNonNull() unnecessarily, such as in places where null values are either acceptable or already handled by the program.

    // Not recommended - Avoid redundant requireNonNull()
    public String getNonNullValue(String value) {
       return Objects.requireNonNull(value, "Param cannot be null.");
    }
    
    // Instead, handle null where needed
    return (value == null) ? "Default" : value;
    
  4. In Lombok Constructors
    If using Lombok, you can reduce boilerplate code by annotating with @NonNull in the parameters, and Lombok will handle the validation using Objects.requireNonNull() under the hood.

    @Data
    public class Example {
       private final @NonNull String name;
    }
    
  5. Avoid Overhead
    Don’t use Objects.requireNonNull() in performance-critical sections of the code. For repetitive checks in such cases, consider earlier null validations.


Example

Here’s a complete example of how Objects.requireNonNull() works in practice:

package org.kodejava.util;

import java.util.Objects;

public class User {
    private final String username;

    public User(String username) {
        // Validate that the username is not null
        this.username = Objects.requireNonNull(username, "Username cannot be null.");
    }

    public void updateEmail(String email) {
        Objects.requireNonNull(email, "Email cannot be null.");
        System.out.println("Email updated to: " + email);
    }

    public String getUsername() {
        return username;
    }

    public static void main(String[] args) {
        try {
            User user = new User(null); // Throws NullPointerException with message
        } catch (NullPointerException e) {
            System.out.println(e.getMessage()); // Output: "Username cannot be null."
        }

        User user = new User("JohnDoe");

        try {
            user.updateEmail(null); // Throws NullPointerException with message
        } catch (NullPointerException e) {
            System.out.println(e.getMessage()); // Output: "Email cannot be null."
        }
    }
}

Advantages

  • Improved Readability: Instead of writing verbose null-checks, Objects.requireNonNull() provides clear intent with less code.
  • Centralized Null Handling: Enforces null-checking policy consistently.
  • Clear Debugging: The custom exception message pinpoints the issue.

Conclusion

Objects.requireNonNull() is a highly effective tool to enforce non-null constraints in your code. When combined with thoughtful custom messages or suppliers, it helps you write cleaner, safer, and more readable Java code.

How to Generate UUIDs in Java

In Java, you can generate universally unique identifiers (UUIDs) using the java.util.UUID class. Here’s how you can generate a UUID:

Example Code

package org.kodejava.util;

import java.util.UUID;

public class UUIDExample {
    public static void main(String[] args) {
        // Generate a random UUID
        UUID uuid = UUID.randomUUID();
        System.out.println("Generated UUID: " + uuid.toString());
    }
}

Explanation

  • The UUID.randomUUID() method generates a type-4 (pseudo-random) UUID.
  • The output will look something like: f47ac10b-58cc-4372-a567-0e02b2c3d479.
  • The toString() method converts the UUID object into its string representation.

Other UUID Options

If you want to specify your own inputs, you can use the UUID.fromString(String uuid) or create a UUID from specific values with UUID.nameUUIDFromBytes(byte[] bytes). For example:

package org.kodejava.util;

import java.util.UUID;

public class UUIDFromNameExample {
    public static void main(String[] args) {
        // Generate a UUID based on an input name
        UUID uuid = UUID.nameUUIDFromBytes("example.com".getBytes());
        System.out.println("Generated UUID from name: " + uuid.toString());
    }
}

Notes

  • UUIDs are useful for generating unique IDs in distributed systems, database keys, and more.
  • Version-4 (random) UUIDs are the most commonly used since they rely only on randomness and are highly unlikely to collide.

How to Install Java 21 and Set Up Your Development Environment

Here’s a step-by-step guide to install Java 21 and set up your development environment:

Step 1: Download and Install Java 21

  1. Download JDK 21:
    • Go to the official Oracle Java SE Downloads page or use Adoptium or OpenJDK for an open-source version.
    • Download the JDK 21 version suitable for your system (Windows, macOS, or Linux).
  2. Install Java 21:
    • Windows:
      • Run the installer file and follow the prompts.
    • macOS:
      • Use the .dmg package and follow the installation instructions.
    • Linux:
      • Extract the .tar.gz archive or use a package manager like apt or yum if supported by your Linux distribution.
      • Example for Ubuntu/Debian:
        sudo apt update
        sudo apt install openjdk-21-jdk
        

Step 2: Set JAVA_HOME and PATH

Once Java is installed, set the JAVA_HOME and add the binary folder to your PATH.

Windows:

  1. Open System Properties:
    • Press Win + S, search for “Environment Variables,” and click it.
  2. Add a JAVA_HOME variable:
    • Click New under System Variables.
    • Variable Name: JAVA_HOME
    • Variable Value: Path to the JDK installation directory (e.g., C:\Program Files\Java\jdk-21).
  3. Update the PATH variable:
    • Select the Path variable, click Edit, and add %JAVA_HOME%\bin.

macOS / Linux:

  1. Open your terminal and edit your shell configuration file (e.g., ~/.bashrc, ~/.zshrc, or ~/.bash_profile):
    export JAVA_HOME=/path/to/java/jdk-21
    export PATH=$JAVA_HOME/bin:$PATH
    
  2. Apply the changes:
    source ~/.bashrc
    # or
    source ~/.zshrc
    
  3. Verify the installation:
    java -version
    

Step 3: Set Up IntelliJ IDEA

  1. Download IntelliJ IDEA:
    • Visit the IntelliJ IDEA website and download the latest version.
    • Install the Ultimate Edition or the Community Edition, depending on your needs.
  2. Configure IntelliJ IDEA with Java 21:
    • Open IntelliJ IDEA and go to File > Project Structure > SDKs.
    • Click + to add a new JDK.
    • Navigate to the Java 21 installation folder and select it.
  3. Set the project’s JDK version:
    • Go to File > Project Structure > Modules and assign the JDK 21 to your project.

Step 4: Verify the Java Development Setup

  1. Create a sample application to test the setup:
    • Create a new Java project in IntelliJ.
    • Write a “Hello, World!” program:
      public class Main {
          public static void main(String[] args) {
              System.out.println("Hello, World!");
          }
      }
      
    • Run the program to ensure it works as expected.
  2. Confirm the Java version:
    • Run the following in the terminal:
    java -version
    
  3. IntelliJ’s terminal should point to Java 21.

Optional: Tools to Enhance Development

  1. Maven/Gradle:
    • Set up Maven or Gradle build tools for dependency management.
  2. Version Control:
    • Install Git and set it up in IntelliJ.
  3. Extensions and Plugins:
    • Install helpful IntelliJ plugins like Lombok, Checkstyle, JRebel, or a Database tool.
  4. Docker:
    • If you’re working with containers, install Docker and configure IntelliJ’s Docker plugin.

You now have Java 21 and your development environment fully set up and configured!

How to Use StringBuilder for Efficient String Concatenation

In Java, using StringBuilder is a common way to handle efficient string concatenation, especially when working with loops or when you need to concatenate a large number of strings. Unlike String, which is immutable, StringBuilder is mutable and modifies its internal character array without creating new objects, hence improving performance.

Here’s how you can use StringBuilder for efficient string concatenation:

1. Creating a StringBuilder instance

You can create a new instance of StringBuilder using its constructor:

StringBuilder sb = new StringBuilder();

You can also initialize it with an existing string:

StringBuilder sb = new StringBuilder("Hello");

2. Appending Strings

Use the .append() method to concatenate strings:

StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
System.out.println(sb.toString()); // Output: "Hello World"

Here, the append() method modifies the existing StringBuilder instance.


3. Inserting Strings

To insert a string at a specific position, use the .insert() method:

StringBuilder sb = new StringBuilder("Hello World");
sb.insert(6, "Beautiful ");
System.out.println(sb.toString()); // Output: "Hello Beautiful World"

4. Replacing Part of the String

You can replace part of the string using .replace():

StringBuilder sb = new StringBuilder("Hello Java");
sb.replace(6, 10, "World");
System.out.println(sb.toString()); // Output: "Hello World"

5. Reversing the String

You can reverse the string using .reverse():

StringBuilder sb = new StringBuilder("abcd");
sb.reverse();
System.out.println(sb.toString()); // Output: "dcba"

6. Deleting Characters or Substrings

You can use .delete() or .deleteCharAt() to remove parts of the string:

StringBuilder sb = new StringBuilder("Hello World");
sb.delete(5, 11); // Remove characters from index 5 to 10
System.out.println(sb.toString()); // Output: "Hello"

sb.deleteCharAt(0); // Remove the character at index 0
System.out.println(sb.toString()); // Output: "ello"

7. Converting Back to a String

Once you are done building the string, convert it back to a String using .toString():

StringBuilder sb = new StringBuilder("Hello");
String result = sb.toString();
System.out.println(result); // Output: "Hello"

8. StringBuilder in Loops

It is particularly useful when appending strings in loops to avoid the overhead of creating multiple String instances:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
    sb.append("Number ").append(i).append(", ");
}
System.out.println(sb.toString());
// Output: "Number 0, Number 1, Number 2, Number 3, Number 4, "

Example: Complete Code

Here’s a complete example that combines multiple methods:

public class StringBuilderExample {
    public static void main(String[] args) {
        // Create a StringBuilder
        StringBuilder sb = new StringBuilder("Example");

        // Append strings
        sb.append(" of").append(" StringBuilder");

        // Insert a string
        sb.insert(8, " java");

        // Replace a substring
        sb.replace(0, 7, "Demo");

        // Delete part of the string
        sb.delete(5, 10);

        // Reverse the string
        sb.reverse();

        // Convert back to String
        System.out.println(sb.toString());
    }
}

Output:

redliuBgnirtS fo omeD

Performance Comparison: String vs StringBuilder

Here’s a quick comparison of the performance:

  • String: Creates a new object for each concatenation, which is inefficient in loops.
  • StringBuilder: Reuses the same object and modifies its internal buffer, which is much faster.

So, whenever you’re performing a lot of string manipulations, especially in loops, it’s highly recommended to use StringBuilder.

How to Format Strings Using String.format()

In Java, the String.format() method is a convenient way to create formatted strings using placeholders. It allows you to include values such as numbers or strings at specific positions in a string by using format specifiers. Here’s how you can use it:

Syntax

String.format(String format, Object... args)
  • format: The format string with placeholders.
  • args: The arguments to replace the placeholders.

Common Format Specifiers

  • %s: String.
  • %d: Decimal integer.
  • %f: Floating-point number.
  • %c: Character.
  • %%: Literal % character.

You can combine these with width, precision, alignment, and other formatting options.


Examples

1. String Formatting

String name = "John";
int age = 30;
String formattedString = String.format("My name is %s and I am %d years old.", name, age);
System.out.println(formattedString);
// Output: My name is John and I am 30 years old.

2. Formatting Numbers

double price = 123.456789;
String formattedPrice = String.format("The price is %.2f.", price);
System.out.println(formattedPrice);
// Output: The price is 123.46.
  • %.2f: Limits the floating-point value to 2 decimal places.

3. Padding and Alignment

  • Right-aligned text:
String formattedString = String.format("%10s", "Java");
System.out.println(formattedString);
// Output: "      Java" (padded with spaces to the left, 10 characters in total)
  • Left-aligned text:
String formattedString = String.format("%-10s", "Java");
System.out.println(formattedString);
// Output: "Java      " (padded with spaces to the right, 10 characters in total)

4. Adding Leading Zeros

int number = 42;
String formattedNumber = String.format("%05d", number);
System.out.println(formattedNumber);
// Output: 00042

5. Formatting Multiple Values

String result = String.format("%s scored %d out of %d in the exam.", "Alice", 90, 100);
System.out.println(result);
// Output: Alice scored 90 out of 100 in the exam.

6. Escaping %

To include a literal % in the string, use %%.

String formattedString = String.format("Progress: %.2f%%", 85.123);
System.out.println(formattedString);
// Output: Progress: 85.12%

Notes

  1. Null Values: If a value in args is null, %s outputs the string "null".
  2. Exceptions: Make sure the placeholders match the number and type of arguments; otherwise, it may throw an exception (e.g., IllegalFormatException).

Formatted strings are especially useful when generating user-friendly messages or handling precise output formatting, such as in reporting systems or logs.

How to use the new API enhancements in java.nio.file in Java 17

Java 17 introduced several significant enhancements in the java.nio.file package, focusing on improving file system operations, security, and performance. Below is an explanation of the new APIs and available enhancements, with examples demonstrating how to use them.

Key API Enhancements in java.nio.file for Java 17

1. Files.mismatch()

The method Files.mismatch(Path, Path) was added to efficiently compare two files. It helps identify the position where two files differ or returns -1 if the files are identical.

Example:

package org.kodejava.nio;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class FilesMismatchExample {
    public static void main(String[] args) throws IOException {
        Path file1 = Path.of("file1.txt");
        Path file2 = Path.of("file2.txt");

        // Create sample files
        Files.writeString(file1, "Hello, world!");
        Files.writeString(file2, "Hello, Java!");

        long mismatchPosition = Files.mismatch(file1, file2);

        if (mismatchPosition == -1) {
            System.out.println("Files are identical.");
        } else {
            System.out.println("Files differ beginning at byte position: " + mismatchPosition);
        }
    }
}

Usage Notes:

  • This method is especially useful for large files where reading and comparing the entire contents manually would be inefficient.
  • For identical files, the method returns -1.

2. Files.copy() Enhancements

The Files.copy(InputStream in, Path target, CopyOption... options) method now supports the StandardCopyOption.REPLACE_EXISTING option to overwrite existing files directly.

Example:

package org.kodejava.nio;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public class FilesCopyExample {
    public static void main(String[] args) throws Exception {
        Path targetPath = Path.of("output.txt");

        try (InputStream inputStream = new ByteArrayInputStream("File content".getBytes())) {
            Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
        }
        System.out.println("File copied successfully to: " + targetPath);
    }
}

Usage Notes:

  • Prior to Java 17, replacing existing files required explicitly deleting the file first.
  • This enhancement simplifies file replacement logic.

3. Support for Hidden Files in Files.isHidden()

Java 17 improves the handling of hidden files for certain platforms where determining this attribute was inconsistent (e.g., macOS and Linux).

Example:

package org.kodejava.nio;

import java.nio.file.Files;
import java.nio.file.Path;

public class HiddenFileExample {
    public static void main(String[] args) throws Exception {
        Path filePath = Path.of(".hiddenFile");
        Files.createFile(filePath);

        if (Files.isHidden(filePath)) {
            System.out.println(filePath + " is a hidden file.");
        } else {
            System.out.println(filePath + " is not a hidden file.");
        }
    }
}

4. File Permission Enhancements on Unix-like Systems

Java 17 improves security and performance for managing file permissions using PosixFilePermissions.

Example:

package org.kodejava.nio;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;

public class FilePermissionExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("example.txt");
        Files.createFile(path);

        Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-r--r--");
        Files.setPosixFilePermissions(path, permissions);

        System.out.println("File permissions: " + Files.getPosixFilePermissions(path));
    }
}

Usage Note:

  • This improvement provides more robust support for file permissions on Unix-like operating systems.

Summary Table of Changes

Enhancement Description Java Version
Files.mismatch() Compares two files to find the first mismatch position or confirms equality Java 17
Enhanced Files.copy() Overwrite files without manually deleting them Java 17
Improved Files.isHidden() Better cross-platform handling of hidden files Java 17
File Permission Enhancements Improved security and performance on Unix-like systems Java 17

These enhancements improve efficiency, accessibility, and usability when working with file system operations. You can start using them to simplify your file-handling logic in Java applications.

How to compile and run Java 17 code using command line

To compile and run Java 17 code using the command line, follow these steps:


1. Install Java 17

  • Ensure that Java 17 is installed on your system.
  • Run the following command to check the installed Java version:
java -version

If Java 17 is not installed, download and install it from the official Oracle website or use OpenJDK.


2. Write Your Java Code

  • Create a Java file with the .java extension. For example, create a file named HelloWorld.java with the following content:
public class HelloWorld {
   public static void main(String[] args) {
       System.out.println("Hello, World!");
   }
}

3. Open Command Line

  • Open a terminal (on Linux/Mac) or Command Prompt/PowerShell (on Windows).

4. Navigate to the Directory

  • Go to the directory where the .java file is located using the cd command. For example:
cd /path/to/your/code

5. Compile the Java File

  • Use the javac command to compile the .java file into bytecode. The javac compiler will create a .class file.
javac HelloWorld.java
  • If there are no errors, you’ll see a file named HelloWorld.class in your directory.

6. Run the Compiled Java File

  • Execute the compiled .class file using the java command (without the .class extension):
java HelloWorld
  • You should see the following output:
Hello, World!

7. (Optional) Use Java 17 Specific Features

  • Java 17 brought several new features such as sealed classes, pattern matching for switch, and more. Make sure your code uses features specific to Java 17 to fully utilize it.

Common Troubleshooting

  1. 'javac' is not recognized as an internal or external command:
    • Ensure Java is added to your system’s PATH environment variable. Refer to your operating system’s documentation to add the Java bin directory to the PATH.
  2. Specify Java Version (if multiple versions are installed):
    • Use the full path to the desired Java version for compilation and execution:
/path/to/java17/bin/javac HelloWorld.java
/path/to/java17/bin/java HelloWorld

With these steps, your Java 17 code should successfully compile and run from the command line.

How to use helpful NullPointerExceptions in Java 17

In Java 14, along with the -XX:+ShowCodeDetailsInExceptionMessages feature, Helpful NullPointerExceptions were introduced. This feature provides detailed and precise messages when a NullPointerException (NPE) occurs. It is available starting from Java 14 as a preview feature and was enabled by default (no longer requiring the JVM flag) starting with Java 16. This behavior continues in Java 17.

These enhancements tell you exactly which object reference was null, making debugging easier compared to the default NPE messages.


Steps to Use Helpful NullPointerExceptions in Java 17

  1. Ensure Java 17 is Installed
    • Verify that the installed JDK version is Java 17 or newer. Use:
    java -version
    
  2. By Default, It’s Enabled
    • Starting from Java 16, Helpful NullPointerExceptions are enabled by default, so no additional JVM flag or setup is required.
  3. Run Your Application
    • If your code throws a NullPointerException, the detailed message will be generated.
  4. How It Works
    • When a NullPointerException is thrown, the JVM will now include details in the exception’s message about the null reference that caused the problem.

Example

Code Example

package org.kodejava.basic;

public class NullPointerDemo {
   public static void main(String[] args) {
      String str = null;
      System.out.println(str.toLowerCase()); // Will throw a NullPointerException
   }
}

Output

Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.toLowerCase()" because "str" is null

If you use field/method chaining, the message will identify exactly which part caused the NPE.

Example with Field Access

class Person { 
    Address address; 
}

class Address { 
    String city; 
}

public class HelpfulNPEExample { 
    public static void main(String[] args) { 
        Person person = new Person(); 
        System.out.println(person.address.city); // Accessing null property
    }
}

Detailed Output

Exception in thread "main" java.lang.NullPointerException: 
Cannot read field "city" because "person.address" is null

Enabling or Disabling (Optional)

Helpful NullPointerExceptions can be disabled using the following JVM argument:

-XX:-ShowCodeDetailsInExceptionMessages

To enable explicitly (though it’s enabled by default in Java 17+):

-XX:+ShowCodeDetailsInExceptionMessages

Add this argument when running your application:

java -XX:+ShowCodeDetailsInExceptionMessages YourMainClass

Benefits of Helpful NullPointerExceptions

  1. Faster Debugging: You no longer need to search manually for which variable or reference is null.
  2. Enhanced Error Information: Pinpoints the exact null reference, which is especially useful in complex codebases.
  3. Productivity Increase: Saves time during troubleshooting and debugging.

Java 17 users benefit from this feature out-of-the-box, making it a significant enhancement for clean and error-free development.

How to create records in Java 17 for immutable data models

In Java 17, you can use the record feature to create immutable data models. Records are a new type of class in Java designed specifically to hold immutable data. Using records simplifies creating classes that are essentially data carriers. Here’s a step-by-step guide on how to create and use records in Java 17:

What is a Record?

A record is a special kind of class in Java introduced in Java 14 (as a preview) and became stable in Java 16+. It:

  • Is designed for immutability
  • Automatically generates boilerplate code like getters, equals(), hashCode(), and toString()

Syntax of a Record

Declaring a record is simple. Here’s the syntax:

public record RecordName(datatype field1, datatype field2, ...) {}

Key Features of Records

  1. Records automatically:
    • Generate getter methods for fields (no need for get prefix – field name itself is used).
    • Override toString(), hashCode(), and equals().
  2. Records are immutable (fields cannot be changed after initialization).

  3. Records can include custom methods.
  4. Records cannot extend other classes (inheritance is not allowed) but can implement interfaces.

An Example: Immutable Data Model with Records

package org.kodejava.basic;

public record Person(String name, int age) {
    // Custom constructor (optional)
    public Person {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }

    // Example of adding a custom method
    public String greet() {
        return "Hello, my name is " + name + " and I am " + age + " years old.";
    }
}

How to Use Records

You use a record just like any other class:

package org.kodejava.basic;

public class Main {
    public static void main(String[] args) {
        // Create a record instance
        Person person = new Person("John Doe", 30);

        // Access fields using getters
        System.out.println("Name: " + person.name());
        System.out.println("Age: " + person.age());

        // Use a custom method
        System.out.println(person.greet());

        // Immutability tested
        // person.name = "New Name"; // Compilation error because fields are final
    }
}

Output:

Name: John Doe
Age: 30
Hello, my name is John Doe and I am 30 years old.

Advantages of Using Records

  1. Less Boilerplate Code: You don’t need to write getters, setters, constructors, or methods like toString() and hashCode().
  2. Thread-Safety: Records are immutable, making them easy to use in concurrent environments.
  3. Better Readability: The succinct syntax improves code readability.

Restrictions of Records

  1. Records are final — you cannot extend them.
  2. Fields in a record are also final and cannot be changed.
  3. Records themselves cannot be mutable.

When Should You Use Records?

You should use records when:

  • You need a simple data model to hold immutable data.
  • You want to avoid the verbosity of writing boilerplate code for fields and methods (getters, toString(), etc.).

For mutable data, traditional classes or other patterns should be used instead of records.

How to use sealed classes for better type safety in Java 17

Java 17 introduced sealed classes as part of the enhancements to the type system. Sealed classes allow developers to explicitly control which classes can extend or implement a class or interface, thereby achieving better type safety and making it easier to design domain-specific hierarchies. Here’s a guide on how to use sealed classes effectively:


What are Sealed Classes?

Sealed classes restrict which other classes or interfaces can extend or implement them. By using sealed classes, you can:

  1. Define a closed hierarchy of types where only a fixed set of subtypes is allowed.
  2. Ensure better maintainability and readability of your type hierarchy.
  3. Provide exhaustive handling for these types with features like switch statements.

The syntax revolves around the sealed, non-sealed, and final keywords.


Declaring and Using Sealed Classes

1. Declaration

To declare a sealed class:

  • Use the sealed modifier.
  • Specify the permitted subclasses with the permits clause.
package org.kodejava.basic;

public sealed class Shape permits Circle, Rectangle, Square {
   // Common properties and methods for all shapes
}

In this example:

  • Shape is the sealed class.
  • Only Circle, Rectangle, and Square are allowed to extend Shape.

2. Permitted Subclasses

Every subclass permitted by the sealed class must opt for one of the following:

  • final: The subclass cannot be further extended.
  • non-sealed: The subclass can be extended by any other class.
  • sealed: The subclass restricts its hierarchy further with permits.

Examples:

package org.kodejava.basic;

// Final subclass (cannot have further subclasses)
public final class Circle extends Shape {
   double radius;

   public Circle(double radius) {
      this.radius = radius;
   }
}
package org.kodejava.basic;

// Sealed subclass with its own permitted subclasses
public sealed class Rectangle extends Shape permits RoundedRectangle {
   double width, height;

   public Rectangle(double width, double height) {
      this.width = width;
      this.height = height;
   }
}
package org.kodejava.basic;

// Non-sealed subclass (can have arbitrary subclasses)
public non-sealed class Square extends Shape {
   double side;

   public Square(double side) {
      this.side = side;
   }
}
package org.kodejava.basic;

// Permitted subclass of Rectangle
public final class RoundedRectangle extends Rectangle {
    double cornerRadius;

    public RoundedRectangle(double width, double height, double cornerRadius) {
        super(width, height);
        this.cornerRadius = cornerRadius;
    }
}

Benefits of Sealed Classes

  1. Closed Type Hierarchies
    Sealed classes provide an explicit way to define and restrict type hierarchies, avoiding unintended subclasses.

  2. Exhaustiveness in switch Statements
    When all subclasses of a sealed class are known, the compiler ensures exhaustiveness in switch expressions. This helps eliminate the possibility of missing a case.

    Example:

    public double calculateArea(Shape shape) {
        return switch (shape) {
            case Circle c -> Math.PI * c.radius * c.radius;
            case RoundedRectangle rr ->
                    rr.width * rr.height - (4 - Math.PI) * rr.cornerRadius * rr.cornerRadius / 4;
            case Rectangle r -> r.width * r.height;
            case Square s -> s.side * s.side;
            default -> throw new IllegalStateException("Unexpected value: " + shape);
        };
    }
    

    If you later add a new subclass to Shape, the compiler will generate an error until you update the switch statement accordingly.

  3. Immutability and Security
    By marking direct subclasses as final or controlling further inheritance (e.g., via sealed vs. non-sealed), you ensure immutability in specific contexts and prevent unintended behavior caused by subclassing.


Practical Use Cases for Sealed Classes

  1. Domain Modelling
    Example: A sealed class Payment can have subclasses for CreditCardPayment, BankTransfer, and CryptoPayment.

    public sealed class Payment permits CreditCardPayment, BankTransfer, CryptoPayment {
       // Common payment attributes
    }
    
    public final class CreditCardPayment extends Payment {
       // Credit card specific fields
    }
    
    public final class BankTransfer extends Payment {
       // Bank transfer specific fields
    }
    
    public final class CryptoPayment extends Payment {
       // Crypto payment specific fields
    }
    
  2. Compiler Assistance for Type-Safe Code
    The sealed hierarchy ensures that when you process these types (e.g., with switch or polymorphic methods), the compiler helps enforce exhaustive handling.


Key Points to Remember

  • You must list all permitted subclasses explicitly using the permits clause.
  • All subclasses of a sealed class must be declared in the same module or package as the sealed class (enhanced encapsulation).
  • sealed, non-sealed, and final define the inheritance relation for permitted subclasses.

Summary

Sealed classes are a powerful tool in Java 17 for creating controlled and predictable type hierarchies. They help enforce constraints at compile-time, reduce runtime errors, and assist developers in creating clean, maintainable, and type-safe code. Use them effectively to create robust domain models and application logic.