Java Table of Content




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *

Java Sealed Class

Sealed classes, introduced in Java 15 as a preview feature and later refined in subsequent releases, represent a middle ground between the two extremes of the class world: the completely open public class, and the completely closed final class.

What is a Sealed Class?

A sealed class restricts which other classes or interfaces can extend or implement it. It’s defined using the sealed, non-sealed, or permits modifier.

Basic Syntax:

public sealed class Shape 
    permits Circle, Rectangle, Triangle { }

In this example, Shape is a sealed class, and only Circle, Rectangle, and Triangle can extend it.

Using Sealed Classes:

Defining:

public sealed class Animal 
    permits Dog, Cat { }

public final class Dog extends Animal { }

public final class Cat extends Animal { }

Note: Classes extending a sealed class can be final, sealed, or non-sealed.

Benefits (Advantages):

  1. Controlled Hierarchies: You have greater control over how your class is used and extended. It’s particularly useful when you want to restrict subclasses for a specific domain problem.
  2. Pattern Matching: In conjunction with Java’s pattern matching features, sealed classes offer more robust compile-time checks. The compiler can verify if all permitted subclasses are covered in a pattern-matching block, allowing for exhaustive pattern matching.
  3. Maintainability: By restricting which classes can extend your class, you can avoid potential future complications when your codebase evolves or is refactored.

Drawbacks (Disadvantages):

  1. Flexibility: Sealed classes can reduce the flexibility for developers, especially if they want to extend a class in a way that wasn’t anticipated by the original developer.
  2. Complexity: For smaller projects or cases where exhaustive control isn’t needed, using sealed classes might introduce unnecessary complexity.

When to Use:

  1. Domain Modeling: When modeling a specific domain where you know all the possible subclasses, sealed classes can be beneficial. For example, modeling geometric shapes, AST nodes in compilers, or specific kinds of events in an event system.
  2. API Design: If you’re designing an API and want to ensure consumers only extend specific parts of your hierarchy, sealed classes can offer that control.
  3. Exhaustive Pattern Matching: When using pattern matching and you want to ensure all possible cases are handled.

When Not to Use:

  1. Open-ended Scenarios: If you believe there’s a chance that other developers might need to extend your class in ways you haven’t thought of, it’s best not to seal it.
  2. For Simplicity: For simple projects or basic class hierarchies where exhaustive control isn’t beneficial, adding sealed class restrictions might be overkill.

Conclusion:

Sealed classes are a powerful addition to the Java language, offering a level of control over inheritance that wasn’t previously available. Like all features, they have their pros and cons, and the decision to use them should be based on the specific needs of the project and the problems you’re trying to solve.

July 27, 2023

Java Record

What is a Record?

In Java, it’s common to have classes that exist solely to carry data, i.e., classes with fields, simple methods like getters, equals(), hashCode(), and toString(). Before records, defining such classes could be quite verbose.

A record is a restricted form of class that provides a concise way to define such classes. When you define a record, the Java compiler automatically implements several standard methods for you.

Characteristics of Records:

  1. Immutable: Records are implicitly final and their fields are also final. Thus, once a record is created, you cannot modify its state.
  2. No Subclassing: You cannot explicitly extend a record, and records cannot extend other classes.
  3. Auto-generated Standard Methods: The compiler automatically generates methods like equals(), hashCode(), and toString() based on the fields of the record.
  4. Explicit fields: The record’s header declares its fields, and these are the only fields allowed.
public record Point(int x, int y) { }

This simple declaration creates:

  • A record with two fields, x and y.
  • A constructor taking two parameters to initialize these fields.
  • Getters for these fields (x() and y()).
  • An equals() method that checks for equality based on the values of x and y.
  • A hashCode() method based on the values of x and y.
  • A toString() method that returns a string in the format Point[x=<value>, y=<value>].
public class TestRecords {
    public static void main(String[] args) {
        Point p1 = new Point(5, 10);
        Point p2 = new Point(5, 10);
        
        System.out.println(p1); // Prints: Point[x=5, y=10]
        System.out.println(p1.equals(p2)); // Prints: true
    }
}

Customizing Records:

While records are meant to be simple data carriers, you can still customize them:

Custom Constructors:

You can define custom constructors, but they should call the canonical constructor:

public record Rectangle(int width, int height) {
    public Rectangle {
        if (width < 0 || height < 0) {
            throw new IllegalArgumentException("Dimensions should be positive");
        }
    }
}

Adding Methods:

You can add methods to a record:

public record Point(int x, int y) {
    public double distanceFromOrigin() {
        return Math.sqrt(x * x + y * y);
    }
}

Implementing Interfaces:

Records can implement interfaces:

public record NamedPoint(int x, int y, String name) implements Comparable<NamedPoint> {
    @Override
    public int compareTo(NamedPoint other) {
        return name.compareTo(other.name);
    }
}

Things to Remember:

Record components are final by default.

Records can’t be abstract.

Records can’t extend other classes.

You can’t declare instance fields in a record that aren’t record components.

Records are a significant step toward making Java more expressive and less verbose, especially for common coding patterns.

Caveats and Considerations for using Record with JPA:

Annotations: With records, you can’t annotate fields directly since they’re declared in the record header. If you need to add JPA annotations (like @Id, @GeneratedValue, etc.), they should be added to the accessor methods (getters) of the record.

Immutability: Records are inherently immutable, which means all their fields are final. This behavior aligns with the principle of immutability in entity design but can complicate scenarios where you want to change an entity’s state after it has been constructed.

NoArgs Constructor: JPA typically requires a noargs constructor, which records don’t provide out of the box. Hibernate’s (a popular JPA implementation) team has been working to support records, so expect improvements in this area. If your JPA provider doesn’t support records yet, consider using DTOs (Data Transfer Objects) alongside your entities.

Experimental Support: As mentioned earlier, while Spring Data started introducing experimental support for records, this might not be fully mature or cover all scenarios. Always refer to the latest Spring Data documentation and check for compatibility and best practices.

July 27, 2023

Java – Method Reference

Java method references are a shorthand notation of a lambda expression to call a method. They became available in Java 8, just like lambdas and streams. Method references allow for a cleaner and more readable way to refer to methods without executing them.

Static Method Reference

a reference to a static method is a type of method reference that allows you to reference and use a static method of a class as a functional interface. It is denoted by ClassName::staticMethodName. This type of method reference is useful when you want to pass a static method as an argument to a functional interface or use it in lambda expressions.

ClassName::staticMethodName

static void staticMethodReference() {
    System.out.println("\nstaticMethodReference...");

    // Using lambda expression
    Arrays.asList("apple", "banana", "orange").forEach(str -> System.out.println(str.toUpperCase()));

    // Using method reference
    Arrays.asList("apple", "banana", "orange").forEach(Java8MethodReference::convertToUpperCase);

    // using static method to print out
    users.forEach(System.out::println);

    System.out.println("staticMethodReference done!");
}

public static void convertToUpperCase(String str) {
    System.out.println(str.toUpperCase());
}

Instance Method Reference

a reference to an instance method is a type of method reference that allows you to reference and use an instance method of a particular object as a functional interface. It is denoted by instance::instanceMethodName. This type of method reference is useful when you want to pass an instance method as an argument to a functional interface or use it in lambda expressions.

instance::instanceMethodName

static void instanceMethodReference() {
    System.out.println("\ninstanceMethodReference...");

    List<String> fruits = Arrays.asList("apple", "banana", "orange");

    // Using lambda expression
    fruits.forEach(str -> {
        System.out.println("str: " + str + ",length: " + str.length());
    });

    Java8MethodReference java8MethodReference = new Java8MethodReference();

    // Using method reference
    fruits.forEach(java8MethodReference::printLength);

    System.out.println("instanceMethodReference done!");
}

public void printLength(String str) {
    System.out.println("str: " + str + ",length: " + str.length());
}

Constructor Method Reference

a reference to a constructor is a type of method reference that allows you to reference and use a constructor of a class as a functional interface. It is denoted by ClassName::new. This type of method reference is useful when you want to pass a constructor as an argument to a functional interface or use it in lambda expressions.

ClassName::new

static void constructorMethodReference() {
    System.out.println("\nconstructorMethodReference...");

    // Using lambda expression
    Supplier<User> personLambda = () -> User.builder().firstName("John").lastName("Doe").build();
    User johnFromLambda = personLambda.get();
    System.out.println("User from lambda: " + johnFromLambda);

    // Using method reference
    Supplier<User> personMethodRef = User::new;
    User johnFromMethodRef = personMethodRef.get();
    System.out.println("User from method reference: " + johnFromMethodRef);

    System.out.println("constructorMethodReference done!");
}

 

July 27, 2023

Java – Optional

Java Optional is a class that represents a value that may or may not be present. It is used to avoid NullPointerExceptions when dealing with nullable values.

Optional is a class introduced to handle the absence of a value in a more elegant and safe way. It is designed to avoid null pointer exceptions and make it clear when a value may be absent. An Optional object can either contain a non-null value or represent that no value is present.

Before Java 8, when a method could return a null value, developers had to explicitly check for null before performing any operations on the returned value. This often led to cluttered and error-prone code. With Optional, you can avoid explicit null checks and handle the absence of values in a more functional and streamlined manner.

The Optional class provides several methods to interact with and manipulate the contained value, such as get(), isPresent(), orElse(), orElseGet(), orElseThrow(), and more.

Here’s a simple example to demonstrate the use of Optional:

import java.util.Optional;

public class OptionalExample {
    public static void main(String[] args) {
        String name = "John"; // Change this to null to see the difference

        // Creating an Optional object from the value
        Optional<String> optionalName = Optional.ofNullable(name);

        // Checking if a value is present
        if (optionalName.isPresent()) {
            System.out.println("Name: " + optionalName.get());
        } else {
            System.out.println("Name is not present.");
        }

        // Using orElse to provide a default value if the value is absent
        String defaultName = optionalName.orElse("Unknown");
        System.out.println("Default Name: " + defaultName);

        // Using orElseGet to provide a default value using a supplier if the value is absent
        String otherDefaultName = optionalName.orElseGet(() -> "Anonymous");
        System.out.println("Other Default Name: " + otherDefaultName);
    }
}

Output when name = "John":

Name: John
Default Name: John
Other Default Name: John

Output when name = null:

Name is not present.
Default Name: Unknown
Other Default Name: Anonymous

In this example, we create an Optional object using the static ofNullable() method. If the value is present, we can retrieve it using the get() method. However, it is advisable to use the isPresent() method before calling get() to avoid a NoSuchElementException. Alternatively, you can use the orElse() method to provide a default value if the Optional object is empty.

The Optional class encourages better code design by making it explicit when a value may be absent and providing clear methods for handling both present and absent cases.

It’s essential to use Optional wisely and not overuse it. It is best suited for method return types and fields that might be absent. Avoid using Optional as a replacement for traditional null checks within method bodies.

Here are some of the benefits of using Java Optional:

  • It helps to avoid NullPointerExceptions.
  • It makes code more readable and maintainable.
  • It can be used to represent a variety of different scenarios, such as when a value is not yet known, when a value may be null, or when a value is optional.

isPresent()

The Optional class provides the isPresent() method, which is used to check whether an Optional object contains a nonnull value or is empty (contains a null value). The isPresent() method returns a boolean value indicating whether the Optional object holds a value.

static void isPresent() {
    System.out.println("isPresent...");

    String firstName = faker.name().firstName();
    String lastName = faker.name().lastName();
    String email = (firstName + lastName).toLowerCase() + "@gmail.com";

    User user = User.builder().firstName(firstName).lastName(lastName).email(email).phoneNumber(faker.phoneNumber().cellPhone()).build();

    System.out.println("User: " + user.toString());

    Optional<User> optUser = Optional.ofNullable(user);

    // Checking if the Optional object contains a value
    if (optUser.isPresent()) {
        System.out.println("User is present: " + optUser.get());
    } else {
        System.out.println("User is not present.");
    }

    System.out.println("isPresent done!");
}

The output

isPresent...
User: User(id=0, firstName=Natalie, lastName=Braun, email=nataliebraun@gmail.com, phoneNumber=975.490.2241)
User is present: User(id=0, firstName=Natalie, lastName=Braun, email=nataliebraun@gmail.com, phoneNumber=975.490.2241)
isPresent done!

ifPresentOrElse()
The Optional class introduced the ifPresentOrElse() method. This method provides a concise way to perform an action if the Optional object contains a value, and an alternative action if the Optional object is empty.

static void ifPresentOrElse() {
    System.out.println("ifPresentOrElse...");

    String firstName = faker.name().firstName();
    String lastName = faker.name().lastName();
    String email = (firstName + lastName).toLowerCase() + "@gmail.com";

    User user = User.builder().firstName(firstName).lastName(lastName).email(email).phoneNumber(faker.phoneNumber().cellPhone()).build();

    System.out.println("User: " + user.toString());

    Optional<User> optUser = Optional.ofNullable(user);

    optUser.ifPresentOrElse(u -> {
        System.out.println("user is present");
        System.out.println("User: " + u.toString());
    }, () -> {
        System.out.println("else");
    });

    user = null;
    optUser = Optional.ofNullable(user);

    optUser.ifPresentOrElse(u -> {
        System.out.println("user is present");
    }, () -> {
        System.out.println("user is null");
    });

    System.out.println("ifPresentOrElse done!");
}

ofElse()

static void orElse() {
    System.out.println("orElse...");

    String firstName = faker.name().firstName();
    String lastName = faker.name().lastName();
    String email = (firstName + lastName).toLowerCase() + "@gmail.com";

    User user = User.builder().firstName(firstName).lastName(lastName).email(email).phoneNumber(faker.phoneNumber().cellPhone()).build();

    System.out.println("User: " + user.toString());

    Optional<User> optUser = Optional.ofNullable(user);

    User u = optUser.orElse(User.builder().firstName("Test").build());

    System.out.println("User firstName: " + u.getFirstName());

    user = null;
    optUser = Optional.ofNullable(user);

    u = optUser.orElse(User.builder().firstName("Test").build());

    System.out.println("User firstName: " + u.getFirstName());

    System.out.println("orElse done!");
}

filter()

The Optional class provides the filter() method, which is used to conditionally process the value contained within the Optional. The filter() method checks if the Optional contains a value that matches a given predicate (a condition), and if so, it returns the same Optional object. If the Optional is empty (contains a null value) or the predicate evaluates to false for the value, the filter() method returns an empty Optional.

static void filter() {
    System.out.println("filter...");

    String firstName = faker.name().firstName();
    String lastName = faker.name().lastName();
    String email = (firstName + lastName).toLowerCase() + "@gmail.com";

    User user = User.builder().firstName(firstName).lastName(lastName).email(email).phoneNumber(faker.phoneNumber().cellPhone()).build();

    System.out.println("User: " + user.toString());

    user = null;

    Optional<User> optUser = Optional.ofNullable(user);

    optUser = optUser.filter(u -> {
        System.out.println("running filter if value is not null");
        return u.getFirstName().length() > 0;
    });

    optUser.ifPresent(u -> {
        System.out.println("User firstName: " + u.getFirstName());
    });

    System.out.println("filter done!");
}

map()

static void map() {
    System.out.println("map...");

    String firstName = faker.name().firstName();
    String lastName = faker.name().lastName();
    String email = (firstName + lastName).toLowerCase() + "@gmail.com";

    User user = User.builder().firstName(firstName).lastName(lastName).email(email).phoneNumber(faker.phoneNumber().cellPhone()).build();

    System.out.println("User: " + user.toString());

    user = null;

    Optional<User> optUser = Optional.ofNullable(user);

    Optional<String> optEmail = optUser.map(u -> {
        System.out.println("running map if value is not null");
        return u.getEmail();
    });

    optEmail.ifPresent(em -> {
        System.out.println("User email: " + em);
    });

    System.out.println("map done!");
}

 

Github Code Reference

July 24, 2023

Java – CompletableFuture

CompletableFuture was introduced in Java 8 and is part of the java.util.concurrent package. It represents a future result of an asynchronous computation—a way to write nonblocking asynchronous code in Java. The main advantage of using CompletableFuture is its ability to chain multiple asynchronous operations, handle exceptions, combine results, and apply transformations without blocking the execution thread.

CompletableFuture.supplyAsync()

The CompletableFuture.supplyAsync() method is a handy tool in Java’s arsenal for asynchronous programming. It provides a way to execute asynchronous computations that produce a result. This method is used to initiate an asynchronous computation and return a CompletableFuture that will be completed with the result of that computation. The computation itself is specified as a Supplier<T>, where T is the type of the resulting value.

static void runSupplyAsync() {
    System.out.println("runSupplyAsync...");
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        // Simulate some long-running task
        try {
            System.out.println("supplyAsync sleeping...");
            Thread.sleep(1500);
            System.out.println("supplyAsync done sleeping!");
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
        return "Result";
    });

    // This will print immediately
    System.out.println("Waiting for the result...");

    String result = future.join();
    System.out.println("return result: " + result);

    System.out.println("runSupplyAsync done!");
}

Output

runSupplyAsync...
Waiting for the result...
supplyAsync sleeping...
supplyAsync done sleeping!
return result: Result
runSupplyAsync done!
static void runSupplyAsyncThen() {
    System.out.println("runSupplyAsyncThen...");
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        // Simulate some long-running task
        try {
            System.out.println("supplyAsync sleeping...");
            Thread.sleep(1500);
            System.out.println("supplyAsync done sleeping!");
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
        return "Result";
    }).thenApply(result -> {
        System.out.println("thenApply, result=" + result);
        return result + result;
    }).thenApply(result -> {
        System.out.println("thenApply, result=" + result);
        return result + result + result;
    });

    future.thenAccept(result -> {
        System.out.println("thenAccept, result=" + result);
    });

    // This will print immediately
    System.out.println("Waiting for the result...");

    String result = future.join();
    System.out.println("return result: " + result);

    System.out.println("runSupplyAsyncThen done!");
}

Completable.runAsync()

While CompletableFuture.supplyAsync() is designed to initiate asynchronous computations that produce a result, there are times when you want to perform an action that doesn’t return anything. That’s where CompletableFuture.runAsync() comes in.

This method is used to run a Runnable task asynchronously. A Runnable doesn’t return a result. Hence, the CompletableFuture returned by runAsync() is of type Void.

static void runAsync() {
    System.out.println("runAsync...");
    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
        // Simulate some long-running task
        try {
            System.out.println("runAsync sleeping...");
            Thread.sleep(1500);
            System.out.println("runAsync done sleeping!");
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    });

    // This will block until the future is completed (i.e., the Runnable has executed)
    future.join();

    System.out.println("runAsync done!");

}
static void runWithCustomExecutor() {
    System.out.println("runWithCustomExecutor...");

    Executor executor = Executors.newFixedThreadPool(2);

    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
        // Simulate some long-running task
        try {
            System.out.println("runAsync sleeping...");
            Thread.sleep(1500);
            System.out.println("runAsync done sleeping!");
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }, executor);

    // This will block until the future is completed (i.e., the Runnable has executed)
    future.join();

    System.out.println("runWithCustomExecutor done!");

}

 

Key Takeaways:

CompletableFuture.supplyAsync() initiates a nonblocking, asynchronous computation.

By default, tasks execute in the ForkJoinPool.commonPool(), but you can provide your custom Executor.

It provides a more modern, fluent, and readable approach to asynchronous programming in Java, especially when combined with other methods of CompletableFuture for chaining, combining, and handling results.

 

July 27, 2021