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.

How to Use Pattern Matching with instanceof in Java 17

Pattern matching with the instanceof operator was introduced in Java 16 (as a preview feature) and became a standard feature in Java 17. It simplifies the process of type casting when checking an object’s type, making the code shorter and more readable.

Here’s how you can use pattern matching with instanceof in Java 17:

Syntax

With pattern matching, you can directly declare a local variable while checking the type with instanceof. If the condition is true, the variable is automatically cast to the specified type, and you can use it without explicit casting.

if (object instanceof Type variableName) {
   // Use variableName, which is already cast to Type
}

Key Features:

  1. Type Checking and Casting in One Step: No need for an explicit cast.
  2. Shorter Code: Reduces boilerplate.
  3. Available Within Scope: The variable is accessible only within the scope of the if block where the condition is evaluated as true.
  4. Guarded Pattern (Available in Java 20+ – Preview): Introduced later, allowing additional conditions within instanceof.

Example 1: Basic Usage

package org.kodejava.basic;

public class PatternMatchingExample {
   public static void main(String[] args) {
      Object obj = "Hello, Java 17!";

      if (obj instanceof String str) {
         // Type already checked and cast to `String`
         System.out.println("String length: " + str.length());
      } else {
         System.out.println("Not a string.");
      }
   }
}

Explanation:

  • The variable str is declared and automatically cast to String in the same instanceof statement.
  • Within the if block, you can directly use str as it is guaranteed to be a String.

Example 2: Pattern Matching in Loops

package org.kodejava.basic;

import java.util.List;

public class PatternMatchingExample {
   public static void main(String[] args) {
      List<Object> objects = List.of("Java", 42, 3.14, "Pattern Matching");

      for (Object obj : objects) {
         if (obj instanceof String str) {
            System.out.println("Found a String: " + str.toUpperCase());
         } else if (obj instanceof Integer num) {
            System.out.println("Found an Integer: " + (num * 2));
         } else if (obj instanceof Double decimal) {
            System.out.println("Found a Double: " + (decimal + 1));
         } else {
            System.out.println("Unknown type: " + obj);
         }
      }
   }
}

Output:

Found a String: JAVA
Found an Integer: 84
Found a Double: 4.14
Found a String: PATTERN MATCHING

Example 3: Combining && Conditions

You can combine pattern matching with additional conditions:

package org.kodejava.basic;

public class PatternMatchingExample {
   public static void main(String[] args) {
      Object obj = "Hello";

      if (obj instanceof String str && str.length() > 5) {
         System.out.println("String is longer than 5 characters: " + str);
      } else {
         System.out.println("Not a long string (or not a string at all).");
      }
   }
}

Notes:

  1. Scope of Variable:
    The variable introduced inside the instanceof is only accessible inside the block where the condition is true. For example:

    if (obj instanceof String str) {
       System.out.println(str); // str is available here
    }
    // System.out.println(str); // ERROR: str not available here
    
  2. Null Safety:
    If the object being matched is null, the instanceof check will return false, so you don’t have to handle nulls manually.

Benefits:

  • Simplifies code structure.
  • Eliminates the need for verbose casting.
  • Improves readability and reduces errors associated with unnecessary manual typecasting.

Pattern matching with instanceof is now widely used in modern Java. Make sure you’re using JDK 17 or later to take advantage of this feature!

How to use switch expressions in Java 17

In Java 17, switch expressions provide a more concise and streamlined way to handle conditional logic. This feature was introduced in Java 12 as a preview and made a standard feature in Java 14. Java 17, being a Long-Term Support version, includes this feature as well.

Let me guide you through how to use them.


What Are Switch Expressions?

Switch expressions allow you to:

  1. Return values directly from a switch block (as an expression).
  2. Use concise syntax with the arrow -> syntax.
  3. Prevent fall-through by removing the need for explicit break statements.
  4. Handle multiple case labels compactly.

Switch Expression Syntax

Basic Syntax

switch (expression) {
    case value1 -> result1;
    case value2 -> result2;
    default -> defaultResult;
}
  1. Use -> for expression forms.
  2. A default case is mandatory unless all possible values are handled.
  3. The switch expression evaluates to a single value, which can be assigned to a variable.

Examples

1. Assigning a Value with Switch Expression

package org.kodejava.basic;

public class SwitchExpressionExample {
    public static void main(String[] args) {
        String day = "MONDAY";

        int dayNumber = switch (day) {
            case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" -> 1;
            case "SATURDAY", "SUNDAY" -> 2;
            default -> throw new IllegalArgumentException("Invalid day: " + day);
        };

        System.out.println("Day Group: " + dayNumber);
    }
}
  • Explanation:
    • Multiple case labels like "MONDAY", "TUESDAY" are handled via commas.
    • Default throws an exception if the input doesn’t match any case.

2. Block Syntax with yield

For cases where a more complex computation is needed, you can use a code block and yield to return a value.

package org.kodejava.basic;

public class SwitchExpressionWithYieldExample {
    public static void main(String[] args) {
        String grade = "B";

        String message = switch (grade) {
            case "A" -> "Excellent!";
            case "B" -> {
                int score = 85;
                yield "Good job! Your score is " + score;
            }
            case "C" -> "Passed.";
            default -> {
                yield "Invalid grade.";
            }
        };

        System.out.println(message);
    }
}
  • Explanation:
    • Use {} to enclose a block, and yield to specify the value to return.

3. Enhanced Switch with Enums

Switch expressions work great with enums, promoting type safety and readability.

package org.kodejava.basic;

public class SwitchWithEnums {
    enum Day {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
    }

    public static void main(String[] args) {
        Day today = Day.FRIDAY;

        String dayType = switch (today) {
            case SATURDAY, SUNDAY -> "Weekend";
            case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday";
        };

        System.out.println("Today is a " + dayType);
    }
}
  • Explanation:
    • Using enums eliminates the need for default because all cases are covered.

4. Using Switch in a Method

You can use switch expressions for cleaner and more concise methods.

package org.kodejava.basic;

public class SwitchInMethodExample {
    public static void main(String[] args) {
        System.out.println(getSeason(3)); // Output: Spring
    }

    static String getSeason(int month) {
        return switch (month) {
            case 12, 1, 2 -> "Winter";
            case 3, 4, 5 -> "Spring";
            case 6, 7, 8 -> "Summer";
            case 9, 10, 11 -> "Autumn";
            default -> throw new IllegalArgumentException("Invalid month: " + month);
        };
    }
}
  • Explanation:
    • No break is needed, as the return value is implicit in switch expressions.

Key Features to Remember

  1. No need for break statements.
  2. Use -> for one-liner cases.
  3. Use yield to return values from block-style cases.
  4. Works well with var for type inference.

Switch expressions simplify many patterns while keeping your code readable and concise!

How to leverage Java 10’s Thread-Local Handshakes for performance tuning

Java 10 introduced Thread-Local Handshakes as a powerful feature in the JVM, designed to improve the performance and responsiveness of applications, especially in scenarios involving thread-based operations and garbage collection.

Here’s what you need to know about Thread-Local Handshakes and how to leverage them for performance tuning:


1. What are Thread-Local Handshakes?

Thread-Local Handshakes allow a thread to execute a callback function locally without stopping all threads. This contrasts with traditional global safepoints in Java, where all threads must come to a safe state before any work can be done, such as garbage collection or code deoptimization.

In other words:

  • A handshake is a mechanism to perform operations on a subset of threads (or even individual threads) without stopping the entire JVM.
  • This is useful for operations that don’t require a global JVM safepoint, improving responsiveness and reducing latency.

2. Benefits of Thread-Local Handshakes

  • Avoids Global Safepoints: Operations can target some threads or a single thread, meaning other threads continue their work unaffected.
  • Reduces Latency: No need to pause all threads, improving performance for multithreaded applications.
  • Fine-Grained Control: Perform thread-specific tasks like flushing thread-specific memory buffers, deoptimizing code for just one thread, or collecting specific thread-local objects without interrupting the entire JVM.

3. Use Cases

Here are some scenarios where Thread-Local Handshakes can be beneficial:

  • Garbage Collection
    Garbage collectors rely on safepoints to pause threads while managing memory. Thread-Local Handshakes can isolate such operations to only the threads that need it, reducing pause times and improving application throughput.

  • Code Deoptimization
    This happens during just-in-time (JIT) compilation when compiled code needs to revert to interpreted mode. Utilizing handshakes allows deoptimization to occur on specific threads, minimizing the impact on other threads.

  • Thread-Specific Profiling and Debugging
    A developer or monitoring agent can perform profiling or diagnostic tasks on a single thread without disturbing other threads.

  • Thread-Specific Resource Cleanup
    Thread-local data structures can be cleaned up or flushed for specific threads, optimally managing system resources.


4. How Thread-Local Handshakes Work Internally

Thread-Local Handshakes introduce thread-specific “safepoints.” When a request is initiated:

  1. The JVM signals specific threads to execute a callback function (like releasing resources or processing pending tasks).
  2. Unlike global safepoints, only the targeted thread(s) pause and execute the operation.
  3. Once the operation is complete, the thread resumes execution.

This makes operations more granular and non-blocking at the JVM level.


5. Leveraging Thread-Local Handshakes in Performance Tuning

Although Thread-Local Handshakes are implemented at the JVM level, you can indirectly leverage them for performance tuning in the following ways:

  1. Tuning for Garbage Collection
    If you’re using a garbage collector like G1GC or ZGC, you can reduce garbage collection pauses since these collectors take advantage of handshakes to avoid halting all threads during certain operations.

    • How to Monitor: Use tools like Java Mission Control (JMC), VisualVM, or JVM logging to monitor GC pause times and ensure thread-local synchronization is being effectively utilized.

    Relevant JVM Options:

    • -XX:+UseG1GC (or any GC of choice) to enable advanced garbage collection strategies.
    • Use -Xlog:gc to monitor GC logs and observe pauses.
  2. Reducing Latency in Thread-Sensitive Applications
    If your application uses many threads (e.g., for handling requests or background tasks), Thread-Local Handshakes reduce overall latency by targeting specific threads instead of pausing all threads unnecessarily.

    Best practices:

    • Profile your application for thread contention and safepoints using tools like Async Profiler or JFR (Java Flight Recorder).
    • Optimize thread management through thread pools (using ForkJoinPool, ThreadPoolExecutor, etc.) to prevent thread starvation and maximize throughput.
  3. Tuning Thread-Specific Tasks
    For tasks that manipulate thread-local data or thread-specific settings:

    • Optimize performance by ensuring the work is allocated to specific threads that need operations (e.g., specific callbacks).
    • Reduce contention by designing operations that leverage locality (thread-local memory, caches, etc.).

6. Practical Tips for Developers

While Thread-Local Handshakes are managed by the JVM, the following tips help you align your code and architecture to take full advantage:

  1. Choose Modern JVMs: Use JDK 10 or later for applications where fine-grained thread optimization matters. Newer garbage collectors like ZGC or Shenandoah optimize handshakes even further.

  2. Monitor Safepoints and Utilization:

    • Safepoint statistics can be enabled using -XX:+PrintSafepointStatistics to understand how your threads interact with JVM-managed resources.
    • Use tools like JFR to detect safepoint delays or thread-local handshake activity.
  3. Minimize Global Syncs in Application Code:
    • Avoid global thread synchronization where possible.
    • Use thread-local structures (e.g., ThreadLocal API) for thread-scoped data.
  4. Benchmark Your Application:
    Profile how your code interacts with the JVM and threads. Use tools like JMH (Java Microbenchmark Harness) for thread and synchronization benchmarking.


7. Example: Monitoring Thread Safepoints

java -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 -XX:+LogVMOutput -XX:LogFile=safepoints.log -jar YourApp.jar

This will output safepoint-related logs, showing where Thread-Local Handshakes may improve performance by reducing pauses.


8. Conclusion

Thread-Local Handshakes represent an evolutionary step in how the JVM manages thread interactions, replacing costly global operations with thread-targeted approaches. While you may not directly invoke or control handshakes, you can optimize your application and JVM configuration to reap their benefits:

  • Select JVM options and garbage collection strategies that leverage handshakes.
  • Profile and diagnose thread safepoints to find opportunities for performance tuning.

These adjustments ensure better efficiency, reduced latency, and improved performance in multithreaded applications.