Q71.

What is Java NIO?

Q72.

What is the difference between NIO and IO?

Q73.

Explain reflection performance impact.

Q74.

What is Java instrumentation?

Q75.

Explain Java modules system (JPMS).

Q76.

What is records feature in Java?

Q77.

Explain sealed classes in Java.

Sealed classes, introduced as a preview feature in Java 15 and 16 and standardized in Java 17, allow developers to restrict which classes or interfaces can extend or implement them. This mechanism provides more control over the inheritance hierarchy, making systems more robust and predictable by defining a finite set of known subtypes.

What are Sealed Classes?

A sealed class or interface can explicitly declare which other classes or interfaces are permitted to extend it or implement it. This is a significant step towards managing complexity in large codebases by preventing arbitrary extensions of a type. It enables a type to be both an abstract concept and a closed, enumerable set of implementations.

  • Restricted Inheritance: Only explicitly permitted subtypes can extend or implement the sealed type.
  • Increased Expressiveness: Clearly communicates the designer's intent about the precise boundaries of the type hierarchy.
  • Compiler Assistance: Allows the compiler to perform exhaustive checks in pattern matching with instanceof and switch expressions.
  • Enhanced Security and Robustness: Prevents unwanted or malicious extensions of core types and ensures all possible concrete types are known.

Syntax

To declare a sealed class or interface, you use the sealed modifier along with the permits clause, which lists the permitted direct subtypes. Each permitted subtype must be declared in the same module as the sealed type (or same package if in an unnamed module).

java
public abstract sealed class Shape permits Circle, Rectangle, Triangle {
    public abstract double area();
}

`permits` keyword

The permits keyword is crucial. It defines the exact set of classes that are allowed to extend or implement the sealed type. If no permits clause is specified, and all permitted subclasses are declared in the same compilation unit, the compiler can infer them. However, it's generally good practice to explicitly list them for clarity.

Rules for Permitted Subtypes

  • Location: Permitted subtypes must be in the same module as the sealed class or interface. If the sealed type is in the unnamed module, its permitted subtypes must be in the same package.
  • Direct Extension/Implementation: A permitted subtype must directly extend the sealed class or implement the sealed interface.
  • Modifier Declaration: Each permitted subtype must explicitly declare how it continues the sealing. It must be declared with one of the following modifiers:
  • - final: Prevents further extension of that subtype. The hierarchy ends here.
  • - sealed: Allows further restricted extension of that subtype. It effectively creates a sub-hierarchy that is also sealed.
  • - non-sealed: Allows unrestricted extension of that subtype. It breaks the seal, meaning any class can extend it.

Example Scenario: Geometric Shapes

Consider a scenario where you want to model different geometric shapes, but your application will only ever deal with a specific, limited set of shapes. Sealed classes are ideal for this, providing both abstraction and a finite set of implementations.

java
public abstract sealed class Shape permits Circle, Rectangle, Line {
    public abstract String getType();
}

// Circle is a final class, cannot be extended further
public final class Circle extends Shape {
    private double radius;
    public Circle(double radius) { this.radius = radius; }
    @Override
    public String getType() { return "Circle"; }
}

// Rectangle is a non-sealed class, can be extended by any class
public non-sealed class Rectangle extends Shape {
    private double width, height;
    public Rectangle(double width, double height) { this.width = width; this.height = height; }
    @Override
    public String getType() { return "Rectangle"; }
}

// Line is a sealed class, only specific subtypes can extend it
public sealed class Line extends Shape permits StraightLine, CurvedLine {
    private int length;
    public Line(int length) { this.length = length; }
    @Override
    public String getType() { return "Line"; }
}

public final class StraightLine extends Line {
    public StraightLine(int length) { super(length); }
    @Override
    public String getType() { return "Straight Line"; }
}

public final class CurvedLine extends Line {
    public CurvedLine(int length) { super(length); }
    @Override
    public String getType() { return "Curved Line"; }
}

Pattern Matching with `instanceof` and `switch`

Sealed classes work hand-in-hand with pattern matching for instanceof and switch expressions (introduced in Java 16 and enhanced in Java 17). The compiler can now know all possible direct subtypes of a sealed type, allowing it to perform exhaustive checks and often eliminate the need for a default clause in switch expressions.

java
public String describeShape(Shape shape) {
    return switch (shape) {
        case Circle c -> "A circle with radius " + c.radius();
        case Rectangle r -> "A rectangle with dimensions " + r.width() + "x" + r.height();
        case Line l -> "A line of length " + l.length();
        // No default case needed if all direct permitted subtypes of Shape are covered.
        // If Line had more permitted subtypes, they would need to be covered too or a default would be required for Line.
    };
}

Benefits

  • Strict Control over Inheritance: Precisely defines the boundaries of a class hierarchy.
  • Improved Readability and Maintainability: Developers immediately understand the closed nature of the type hierarchy.
  • Enhanced Compiler Safety: Allows for exhaustive switch expressions without a default case, catching missing cases at compile time.
  • Better Tooling Support: IDEs and static analysis tools can leverage this information for more accurate suggestions and warnings.
  • Foundation for Future Language Features: Sets the stage for more advanced pattern matching capabilities and data-oriented programming.

Conclusion

Sealed classes in Java provide a powerful mechanism for creating more robust and predictable type hierarchies. By explicitly declaring permitted subtypes, developers gain fine-grained control over inheritance, leading to safer code, clearer intentions, and improved support for modern language features like pattern matching. They are an excellent addition for designing APIs where you want to allow polymorphism but within a well-defined and controlled set of implementations.

Q78.

What is virtual threads (Project Loom)?

Q79.

Explain memory leaks in Java.

A memory leak in Java occurs when objects are no longer needed by the application but are still referenced, preventing the garbage collector from reclaiming the memory they occupy. This leads to increased memory consumption, reduced performance, and eventually OutOfMemoryErrors.

What is a Memory Leak?

In Java, memory management is largely handled by the Garbage Collector (GC). The GC automatically identifies and reclaims memory occupied by objects that are no longer reachable (referenced) by the running application. A memory leak arises when an object becomes 'logically' unreachable from the application's perspective (it's no longer used or needed) but remains 'technically' reachable because some reference still points to it, thus preventing the GC from freeing its memory.

Common Causes of Memory Leaks in Java

Unreferenced Objects in Static Collections

Static fields and collections have a lifecycle that matches the application's lifecycle. If objects are added to a static collection (e.g., HashMap, ArrayList) and not explicitly removed, they will persist in memory even if they are no longer needed. Since static collections are root references, any objects they hold will never be garbage collected.

java
import java.util.ArrayList;
import java.util.List;

public class LeakyClass {
    private static final List<byte[]> data = new ArrayList<>();

    public void addData() {
        // This byte array will never be garbage collected because 'data' is static
        // and keeps a strong reference to it.
        data.add(new byte[1024 * 1024]); // Adds 1MB byte array
    }
}

Incorrect `equals()` and `hashCode()` Implementations

When custom objects are used as keys in hash-based collections like HashMap or HashSet, the equals() and hashCode() methods must be correctly implemented and consistent. If an object's hashCode() changes after it's been inserted into a HashMap, subsequent attempts to retrieve or remove it may fail, leaving the object stuck in the map and thus leaking memory.

java
import java.util.HashMap;
import java.util.Map;

public class Key {
    private int id;
    private String name;

    public Key(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // Omitting hashCode() or providing an inconsistent one can cause leaks
    // For example, if name changes but hashCode relies on it, the object
    // becomes 'lost' in the map.

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Key key = (Key) o;
        return id == key.id && name.equals(key.name);
    }

    // Missing or bad hashCode()
    // @Override
    // public int hashCode() {
    //    return Objects.hash(id, name);
    // }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Map<Key, String> map = new HashMap<>();
        Key key = new Key(1, "initial");
        map.put(key, "Value 1");

        // Now change the key's state which impacts hashCode but not equals
        key.setName("changed");

        // The original key object is still in the map, but we can't find it
        // using the new state, effectively leading to a leak if new keys
        // with the same ID but different 'name' are added later.
        System.out.println(map.get(key)); // Prints null if hashCode changes
    }
}

Unclosed Resources

Resources like InputStream, OutputStream, Connection, Statement, ResultSet, etc., require explicit closing. Failing to close these resources can not only lead to resource exhaustion (file handles, network sockets) but also potentially hold references to objects that would otherwise be garbage collected.

java
import java.io.FileInputStream;
import java.io.IOException;

public class ResourceLeak {
    public void readFile(String path) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(path);
            // ... read data ...
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // Resource is not closed here, potential leak and resource exhaustion
            // fis.close(); // This line is missing
        }
    }
    
    public void readFileCorrectly(String path) {
        try (FileInputStream fis = new FileInputStream(path)) { // try-with-resources
            // ... read data ...
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Inner Classes and Anonymous Classes

Non-static inner classes implicitly hold a strong reference to their enclosing outer class instance. If an instance of a non-static inner class (or an anonymous inner class, or a lambda expression that captures this) outlives the outer class instance that created it, the outer class instance (and potentially its entire object graph) cannot be garbage collected, leading to a memory leak.

Detecting and Preventing Memory Leaks

  • Profiling Tools: Use tools like JConsole, VisualVM, JProfiler, YourKit, or Eclipse Memory Analyzer (MAT) to analyze heap dumps and track object references.
  • Code Reviews: Peer reviews can help identify potential leak points, especially around static collections, resource management, and inner classes.
  • Weak References: For caching or listener patterns, consider using WeakHashMap or WeakReference to allow objects to be garbage collected when only weak references remain.
  • try-with-resources: Always use try-with-resources for AutoCloseable resources to ensure they are properly closed.
  • Clear Static Collections: Ensure static collections are cleared or their elements removed when no longer needed, especially during application shutdown or context reloads in servers.
  • Override equals() and hashCode() Correctly: Follow the contract for these methods when using custom objects in hash-based collections.

Conclusion

Memory leaks, while not as common in Java as in languages with manual memory management, can still significantly impact application stability and performance. Understanding the common causes and employing diligent coding practices, coupled with effective profiling tools, is crucial for building robust and memory-efficient Java applications.

Q80.

How to profile Java applications?