Java Questions 1-10  «Prev  Next»

Casting, Resources, and Threads in Java SE 25

These ten questions review core Java concepts using Java SE 25. The answers distinguish language rules from design advice and update older interview material where the APIs or recommended practices have changed.

  1. What is downcasting in Java, and how can you check whether a cast is safe?

    Answer: Downcasting treats a reference of a broader type as a reference of a more specific compatible type. For example, a variable declared as Object can refer to a String. Casting that reference to String makes string-specific operations available through the resulting expression. The cast does not create a new object or change the existing object's class.

    A cast must be permitted by the compiler. For a checked reference cast, an incompatible non-null object causes ClassCastException at runtime. Casting null to a reference type produces null; subsequently invoking an instance method through that reference causes NullPointerException.

    Java SE 25 supports pattern matching for instanceof. A successful type test also introduces a variable of the tested type:

    public class CastExample {
        public static void main(String[] args) {
            Object value = "Java 25";
            if (value instanceof String text) {
                System.out.println(text.toUpperCase(java.util.Locale.ROOT));
            }
        }
    }

    The pattern does not match null. Inside the successful branch, text is a non-null String, so a separate explicit cast is unnecessary. When repeated casting is needed throughout an application, consider whether an interface or polymorphic method would express the operation more clearly. Type tests remain useful at boundaries where a value can legitimately have several types.

    Reference: Pattern matching with instanceof.

  2. What is the difference between cohesion and coupling?

    Answer: Cohesion concerns how closely the responsibilities inside a class, component, or module belong together. Coupling concerns how strongly one component depends on another. A common design goal is high cohesion within components and manageable coupling between them.

    For example, an InvoiceCalculator that calculates line totals, discounts, and invoice totals has related responsibilities. Adding email delivery, database connection management, and screen rendering gives that class several unrelated reasons to change. Moving those responsibilities to appropriate collaborators can make the design easier to understand and test.

    Coupling increases when one class relies on another class's implementation details, mutable internal state, or exact sequence of operations. A service that receives a PaymentGateway interface can depend on a stable contract while different implementations handle different payment providers. Encapsulation helps keep changes to an implementation from spreading to its callers.

    These are design tradeoffs, not a rule to eliminate every dependency or create an interface for every class. An application needs collaboration between components. Adding abstractions that have no clear purpose can make a small design harder to follow. In an interview, explain which responsibilities belong together, which dependencies are necessary, and how the design limits the effect of a likely change. Java SE 25 does not change these underlying principles.

  3. Is finalize() a reliable cleanup mechanism in Java SE 25?

    Answer: No. Object.finalize() is deprecated for removal, and finalization is unsuitable for timely or reliable cleanup. The historical rule is that the JVM automatically invokes a particular object's finalizer at most once. That does not guarantee an invocation before the program exits, and any invocation can be delayed indefinitely. Finalization can also be disabled.

    Do not use finalize() to ensure that a file, socket, or database connection is closed. Give resources an explicit lifetime instead. An object that owns a resource can implement AutoCloseable, allowing callers to use a try-with-resources statement to invoke its close() method when the block finishes.

    Garbage collection and resource cleanup solve different problems. Reclaiming Java heap memory does not provide a dependable schedule for releasing an external resource. Cleaner may support specialized fallback cleanup, but its execution is also not a substitute for explicit, timely closure. New application code should not introduce finalizers.

    Reference: Object.finalize().

  4. When should you use finally, and when should you use try-with-resources?

    Answer: Use finally for cleanup or state restoration that must happen when control leaves a try statement, whether the block completes normally, returns, or throws an exception. Releasing a lock acquired before the try block is a typical example. A finally block does not itself catch or handle the original exception.

    For resources implementing AutoCloseable, prefer try-with-resources. It expresses resource ownership directly and closes successfully initialized resources automatically. This example reads one line from a file supplied as a command-line argument:

    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    
    public class FirstLineExample {
        public static void main(String[] args) throws IOException {
            if (args.length != 1) {
                throw new IllegalArgumentException("Supply one file path");
            }
            try (var reader = Files.newBufferedReader(Path.of(args[0]))) {
                System.out.println(reader.readLine());
            }
        }
    }

    Resources are closed in reverse declaration order. If the body throws and closing also throws, the body exception remains the primary exception and the close exception is recorded as suppressed. This avoids a common problem in handwritten cleanup code.

    Avoid returning or throwing a new exception from finally merely to finish cleanup: doing so can replace a pending return value or exception. Also avoid claiming that finally always runs under every circumstance. Forced JVM termination, a process crash, or a block that never finishes can prevent execution.

    References: AutoCloseable and try-with-resources.

  5. What are Java assertions, and how are they enabled?

    Answer: An assertion checks an assumption made by the program, such as an internal invariant. When assertions are enabled and the condition is false, an assert statement throws AssertionError. An optional expression supplies diagnostic detail:

    public class AssertionsExample {
        public static void main(String[] args) {
            int result = -1;
            assert result >= 0 : "Unexpected negative result: " + result;
            System.out.println("Execution continued");
        }
    }

    Run this example with java -ea AssertionsExample to enable assertions. It fails at the assertion. With application assertions disabled, the assertion condition and detail expression are not evaluated, and the program prints its message. Assertions are normally disabled by default; they do not disappear from the class file merely because the application is deployed.

    Do not put essential work or required state changes inside an assertion expression. Otherwise the program can behave differently depending on the launch options. Assertions also should not be the only validation for user input or public API arguments. Use explicit checks and appropriate exceptions when a condition must be enforced in every execution. Assertions are useful for diagnosing programming mistakes, but they do not replace tests or ordinary error handling.

    References: The assert statement and Java launcher options.

  6. What does java -Dname=value do?

    Answer: The -D launcher option sets a Java system property for the launched JVM. Application code reads it with System.getProperty(). For example:

    public class PropertiesExample {
        public static void main(String[] args) {
            String mode = System.getProperty("app.mode", "development");
            System.out.println(mode);
        }
    }
    java -Dapp.mode=production PropertiesExample

    This prints production. Without the property, the supplied default is development. Put -Dapp.mode=production before the main class name. Arguments after the class name are application arguments passed to main, so moving the option there changes its meaning.

    A system property is not an operating-system environment variable. Use System.getenv() for environment variables. Setting a property also does not automatically read a .properties file; file loading is a separate operation. When combining file settings and launch settings, define which takes precedence instead of replacing the entire JVM property set.

    Use the exact property name and case expected by the application. A custom property such as app.mode has no effect until code reads it. Avoid putting credentials in command-line properties, where process inspection or logging may expose them. Quote values containing spaces according to the shell being used.

    References: The java command and System.getProperty().

  7. What does the javac -d option specify?

    Answer: The compiler's -d option selects the destination directory for generated class files. For a class in a named package, javac places the output beneath that directory using the package hierarchy.

    Save this source as Hello.java:

    package example;
    
    public class Hello {
        public static void main(String[] args) {
            System.out.println("Hello from Java 25");
        }
    }

    Compile and run it from the directory containing the source file:

    javac -d out Hello.java
    java -cp out example.Hello

    The generated file is out/example/Hello.class. The runtime class path identifies out, the root above the package directories. The launcher uses the fully qualified class name example.Hello, without a .class suffix.

    Distinguish lowercase javac -d from uppercase java -D: the first controls compiler output, while the second sets a runtime system property. Selecting an output directory also does not select a Java compatibility level. For cross-version compilation, options such as --release address a separate concern.

    Reference: The javac command.

  8. How can you create and run concurrent tasks in Java SE 25?

    Answer: The traditional approaches are to supply a Runnable to a Thread, or to subclass Thread and override run(). Implementing Runnable defines a task; it does not start a thread by itself. Calling start() schedules execution on the new thread. Calling run() directly is an ordinary method call on the current thread.

    Java SE 25 also provides builders for platform and virtual threads. Keeping the task separate from thread construction makes the choice explicit:

    public class ThreadsExample {
        public static void main(String[] args) throws InterruptedException {
            Runnable task = () -> System.out.println("Task completed");
    
            Thread platform = Thread.ofPlatform().start(task);
            Thread virtual = Thread.ofVirtual().start(task);
    
            platform.join();
            virtual.join();
        }
    }

    This prints the message twice; their execution order is unspecified. The joins wait for completion. A thread can be started only once. To execute the task again, create another thread or submit the task to an executor.

    For multiple tasks, an ExecutorService separates task submission from execution management. Executors.newVirtualThreadPerTaskExecutor() creates a new virtual thread for each submitted task and can be closed using try-with-resources. Virtual threads are especially useful for large numbers of tasks that spend time waiting for blocking I/O. They do not increase the available CPU capacity, and shared mutable data still needs appropriate synchronization. Virtual threads are daemon threads, so an application must arrange to wait for work that must finish before shutdown.

    References: Thread, Runnable, and Executors.

  9. What are the six states in Thread.State?

    Answer: Java exposes six thread lifecycle states through Thread.State:

    • NEW: the thread has been created but has not started.
    • RUNNABLE: the thread is executing in the JVM or is ready for execution while waiting for a resource such as a processor.
    • BLOCKED: the thread is waiting to acquire an intrinsic monitor lock, such as when entering a contended synchronized block.
    • WAITING: the thread is waiting indefinitely for another action, for example through untimed Object.wait() or Thread.join().
    • TIMED_WAITING: the thread is waiting for a bounded duration, for example during Thread.sleep() or a timed wait.
    • TERMINATED: the thread's execution has finished.

    There is no separate RUNNING enum constant. These JVM states are not a direct list of operating-system scheduling states. A thread can be in only one of them at a time, and a terminated thread cannot be restarted.

    getState() supplies a snapshot useful for diagnostics. Another thread can change state immediately afterward, so polling this value is not a reliable way to coordinate work. Use a completion mechanism such as join() or a future instead.

    Reference: Thread.State.

  10. How are thread priorities assigned, and do virtual threads behave differently?

    Answer: A platform thread created with a public Thread constructor normally inherits the creating thread's priority. The inherited value is subject to the thread group's maximum. A platform-thread builder can set a priority explicitly; otherwise it also inherits the parent's priority, with the same group limit.

    The priority range is Thread.MIN_PRIORITY through Thread.MAX_PRIORITY, numerically 1 through 10. Thread.NORM_PRIORITY is 5. It is incorrect to conclude that every new platform thread necessarily starts at 5: inheritance and explicit configuration matter.

    Virtual threads have a fixed priority of NORM_PRIORITY. Calling setPriority() with a valid value does not change a virtual thread's priority. Their scheduling model therefore cannot be controlled by assigning different thread priorities.

    Priority is not a correctness or ordering mechanism. A higher value does not promise that one task will finish first, and scheduler behavior differs across platforms. When task B depends on task A, express that dependency using completion and synchronization APIs, not priority changes. This also makes the intended relationship clearer to readers and more reliable across different execution environments.

    Reference: Thread priority.

SEMrush Software