Blog Interview Experiences Sagarsoft

Interview Experience at Sagarsoft👨‍💻

Interview Experience at Sagarsoft

Table of Contents

1. Difference between ClassNotFoundException and NoClassDefFoundError

The difference between “Class Not Found” (often associated with ClassNotFoundException) and “NoClassDefFoundError” concerns how they manifest and are handled in Java, which is the language where these terms are most commonly used. Both are related to the Java ClassLoader system but occur at different times and under different circumstances during the execution of a program. Here’s a closer look:


  • When it Occurs: ClassNotFoundException is a checked exception that occurs when an application tries to load a class through its string name using methods like Class.forName(), ClassLoader.loadClass(), or when the is involved, but no definition for the class with the specified name can be found.
  • Cause: It typically happens when the class is not available in the classpath at runtime, which may be due to the class not being present, or the classpath is not correctly set up.
  • Handling: Because it’s a checked exception, ClassNotFoundException must be explicitly caught or declared to be thrown in the method signature. This allows developers to provide a more graceful handling of the situation, like fallback mechanisms or user-friendly error messages.
  • Common Scenarios: It’s often encountered in applications that rely on dynamic loading of classes, such as plug-in systems or web applications loading classes from various sources at runtime.


  • When it Occurs: NoClassDefFoundError is an error (not an exception) that occurs when the Java Virtual Machine (JVM) or a ClassLoader instance tries to load the definition of a class and no definition of the class could be found. It can occur after the class has been compiled by the Java compiler and an application tries to use it.
  • Cause: The most common cause is that the class in question was available at compile time but not at runtime. This discrepancy can arise from issues like failing to include necessary JAR files or libraries in the classpath when executing the program, changes in the classpath between compilation and execution, or issues with static initializers in a class.
  • Handling: Since it’s an Error, it indicates a serious problem that a reasonable application should not try to catch. It often points to a misconfiguration issue that needs to be resolved.
  • Common Scenarios: It’s typically seen in cases where the code was successfully compiled, but the required classes are missing during runtime due to classpath issues, or if a static initializer block or the initialization of a static variable throws an exception and does not complete normally.

Key Differences

  • Nature: ClassNotFoundException is a checked exception, signaling a condition that a well-written application should anticipate and recover from. NoClassDefFoundError, on the other hand, is an error, indicating a problem that is not expected to be caught under normal circumstances.
  • Cause and Occurrence: ClassNotFoundException is thrown when an application explicitly tries to load a class by name and it can’t be found. NoClassDefFoundError is thrown by the JVM when a class that was present at compile time is missing at runtime.
  • Handling: Checked exceptions like ClassNotFoundException require explicit handling in the code, either by catching the exception or declaring it in the method signature. NoClassDefFoundError, being an error, is typically not something you try to catch or handle; instead, you would resolve the underlying cause of the missing class.

Understanding these differences is crucial for Java developers, especially when dealing with dynamic class loading, external libraries, or complex application deployments.

Sure, I can explain the differences between NoClassDefFoundError, ClassNotFoundException, and the error message “Couldn’t find or load main class XYZ” in more detail.

  1. NoClassDefFoundError:
  • This error occurs at runtime when the Java Virtual Machine (JVM) cannot find the definition of a class that is being referenced by another class.
  • It typically happens when a class is present during compilation but is not available on the classpath at runtime.
  • Common causes include missing dependencies (JAR files) or incorrect classpath configuration.
  • For example, if your code references a third-party library class, and the corresponding JAR file is not included in the classpath, you might encounter a NoClassDefFoundError.
  1. ClassNotFoundException:
  • This exception is thrown when an application tries to load a class by its string name using methods like Class.forName(), ClassLoader.findSystemClass(), or ClassLoader.loadClass(), but the class cannot be found.
  • It occurs at runtime when the JVM cannot locate the specified class on the classpath.
  • Common causes include misspelled class names, incorrect package names, or incorrect classpath configuration.
  1. “Couldn’t find or load main class XYZ”:
  • This error message is displayed when the Java Runtime Environment (JRE) cannot find or load the main class specified when running a Java application.
  • It typically occurs due to one of the following reasons:
    a. Typo in the class name: If you misspelled the name of the class containing the main method, the JRE will not be able to locate it.
    b. Incorrect package specification: If the class containing the main method is inside a package, and you did not specify the fully qualified class name (including the package), the JRE will not be able to find it.
    c. Incorrect classpath: If the class or its dependencies are not present in the classpath, the JRE will not be able to load the class.

Here’s an example to illustrate the last point:

Suppose you have a class MyClass inside the package com.example, and you try to run it with the following command:

java MyClass

This command will result in the error “Couldn’t find or load main class MyClass” because the JRE is looking for the class MyClass in the default (unnamed) package. To run the class correctly, you need to specify the fully qualified class name:

java com.example.MyClass

Alternatively, if your class is part of a JAR file or a compiled project, you need to ensure that the classpath is configured correctly to include the appropriate directories or JAR files.

In summary, NoClassDefFoundError and ClassNotFoundException are runtime exceptions related to class loading issues, while “Couldn’t find or load main class XYZ” is an error message displayed when the JRE cannot locate the specified main class due to issues like typos, incorrect package names, or an incorrect classpath configuration.

2. What is the difference between transient and volatile?

In Java, both transient and volatile are keywords used for variable declarations, but they serve different purposes and are used in different contexts. Here’s a breakdown of the differences between the two:


  • Context: The transient keyword is used in the context of serialization. Serialization is the process of converting an object into a byte stream to save it to a file or send it over the network, and deserialization is the reverse process.
  • Purpose: When a variable is declared with the transient keyword, it tells the Java Virtual Machine (JVM) that this variable should not be serialized. In other words, the transient keyword marks a member variable of an object to be skipped from serialization.
  • Use Case: Use transient for variables that are either sensitive (like passwords) or that can be reconstructed with other data in the object and therefore do not need to be saved. An example could be a thread state or a database connection.


  • Context: The volatile keyword is used in the context of multi-threading, where multiple threads access and modify the same variable.
  • Purpose: Declaring a variable as volatile ensures that its value is always read from and written to the main memory, not just from the thread’s cache. This guarantees the visibility of changes to variables across threads.
  • Use Case: Use volatile for variables that may be updated by multiple threads and where it is important that the changes are immediately visible to other threads. It does not, however, prevent thread interference or solve synchronization problems on its own.

Key Differences

  • Application Domain: transient is used in the domain of serialization/deserialization, while volatile is used in the context of thread visibility and ordering.
  • Behavior: transient affects the persistence of variables (whether they are saved or ignored during serialization), whereas volatile affects the visibility and ordering of variable access across multiple threads.
  • Memory Consistency: volatile directly affects memory consistency guarantees, ensuring that any thread that reads a field will see the most recently written value. transient has no impact on memory consistency but rather on the state of an object as it is saved or transmitted.

In summary, while both transient and volatile keywords modify the default behavior of variables, they do so in entirely different contexts and for different reasons. transient is about skipping variables from serialization to ensure transient or sensitive data isn’t unnecessarily persisted, whereas volatile ensures changes to a variable are immediately visible to all threads, thus aiding in thread-safe operations.

3. Difference between Future and Callable

The difference between Callable and Future in Java’s concurrency framework is fundamental to understanding how tasks are executed asynchronously and how their results are managed and retrieved. Here’s a concise breakdown of each and their differences:


  • Nature: Callable is an interface representing a task that can be executed by another thread. It is similar to Runnable but with two key differences: it can return a result and throw a checked exception.
  • Method: The primary method in Callable is call(), which returns a value of type V and can throw an exception. Its signature is V call() throws Exception.
  • Purpose: Use Callable when you need to execute a task in a separate thread and retrieve the result of the computation, or when you need to handle exceptions that the task may throw.


  • Nature: Future represents the result of an asynchronous computation. It is not a task itself but rather a handle to the result of a task, which might not yet be available.
  • Methods: Future provides methods to check if the task is complete (isDone()), to wait for the task to complete and retrieve its result (get()), and to cancel the task (cancel()).
  • Purpose: Future is used to retrieve the result of a Callable (or a Runnable submitted to an executor service), check the status of the task, and cancel the task if necessary.

Key Differences

  • Role in Concurrency: Callable is about defining a task that returns a result and potentially throws an exception. Future is about managing the outcome of an asynchronous task, providing mechanisms to check if the task is complete, to wait for its completion, and to retrieve its result.
  • Return Value: Callable tasks return a result of type V when their call() method is executed. Future represents the lifecycle of a task and provides a way to retrieve the result once the task has completed.
  • Usage: You submit a Callable task to an executor service, which returns a Future. You then use the Future to manage and retrieve the result of the Callable task. In contrast, Future does not define or execute tasks by itself.

In essence, Callable and Future are complementary concepts in Java’s concurrency model. Callable defines tasks that can be executed asynchronously and return a result, while Future provides a way to track the progress of these tasks, cancel them if needed, and retrieve their results once they are completed.

4. In Java how many ways you can create threads?

In Java, creating threads can be achieved in several ways, reflecting the flexibility and robustness of its concurrency model. Here are the primary methods to create and manage threads:

1. Extending the Thread Class

You can create a new thread by extending the Thread class and overriding its run() method. After creating an instance of your class, you start the thread by calling its start() method.

class MyThread extends Thread {
    public void run() {
        // Task to execute in this thread

MyThread t = new MyThread();

2. Implementing the Runnable Interface

Another way is by implementing the Runnable interface and passing an instance of the implementing class to a Thread object. This method is more flexible than extending Thread, as it allows your class to extend another class.

class MyRunnable implements Runnable {
    public void run() {
        // Task to execute in this thread

Thread t = new Thread(new MyRunnable());

3. Implementing the Callable Interface

Callable works similarly to Runnable, but it can return a result and throw a checked exception. Callable tasks need to be submitted to an ExecutorService, which returns a Future object that can be used to retrieve the computation result and manage the task’s state.

ExecutorService executor = Executors.newCachedThreadPool();
Callable<Integer> task = () -> {
    // Compute a result
    return 42;
Future<Integer> future = executor.submit(task);

4. Using an ExecutorService

The ExecutorService interface provides a higher-level replacement for working directly with threads. It manages a pool of threads for you. You submit Runnable or Callable tasks to the executor, which executes them as threads become available.

ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
    // Task to execute
executor.shutdown(); // Always remember to shut down the executor when done

5. Using Parallel Streams (Java 8+)

Introduced in Java 8, parallel streams allow you to perform operations in parallel on collections. This method abstracts away direct thread management, using the ForkJoinPool behind-the-scenes.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNumbers = numbers.parallelStream()
                                       .map(n -> n * n)

6. Using CompletableFuture (Java 8+)

CompletableFuture provides a more flexible and functional-style way to handle asynchronous programming. You can process results, chain multiple tasks, combine them, and handle errors in a declarative way.

CompletableFuture.supplyAsync(() -> {
    // Task that returns a value
    return "Hello, World!";
}).thenAccept(result -> {

Each of these methods serves different use cases and offers various levels of control and convenience. Understanding when and how to use them is key to effectively leveraging concurrency in Java applications.

5. What is the Covariant return type?💡

In this example, the upgrade() method in Car overrides the upgrade() method in Vehicle. The return type of the upgrade() method in Car is Car, which is a subtype of Vehicle, thus demonstrating the concept of covariant return types.

Covariant return types make method overriding more powerful and expressive by allowing more specific return types in subclasses, which can lead to more intuitive and easier-to-maintain codebases.

In this example, the upgrade() method in Car overrides the upgrade() method in Vehicle. The return type of the upgrade() method in Car is Car, which is a subtype of Vehicle, thus demonstrating the concept of covariant return types.

Covariant return types make method overriding more powerful and expressive by allowing more specific return types in subclasses, which can lead to more intuitive and easier-to-maintain codebases.

Covariant return type refers to a feature in object-oriented programming languages where an overridden method in a subclass is allowed to return a type that is a subtype of the return type declared in the method of the superclass.

This capability enhances polymorphism, allowing for more specific and refined return types in subclass methods without breaking the inheritance hierarchy or the principle of substitutability, where instances of a subclass can be used wherever instances of a superclass are expected.

Example in Java

class Vehicle {
    Vehicle upgrade() {
        // Logic to upgrade the vehicle
        return new Vehicle();

class Car extends Vehicle {
    Car upgrade() {
        // Specific logic to upgrade a car
        return new Car(); // This return type is a subtype of Vehicle

In this example, the upgrade() method in Car overrides the upgrade() method in Vehicle. The return type of the upgrade() method in Car is Car, which is a subtype of Vehicle, thus demonstrating the concept of covariant return types.

Covariant return types make method overriding more powerful and expressive by allowing more specific return types in subclasses, which can lead to more intuitive and easier-to-maintain codebases.

6. Why is String immutable in Java?

Java’s design choice to make strings immutable is rooted in efficiency, security, and simplicity considerations. Here’s a detailed breakdown of the reasons:

  1. Security: Strings are used extensively in Java for network connections, file paths, database connections, and so forth. Immutable strings help avoid security issues like accidental or intentional changes in critical data. For example, if a string were mutable, a connection string could be altered after being checked, leading to security breaches.
  2. Synchronization and Concurrency: Immutable objects are naturally thread-safe, meaning that they can be shared between multiple threads without the need for synchronization. In a multithreaded application, this property removes a significant burden of handling synchronization for strings, making the code safer and simpler to write.
  3. Caching and Performance: Since strings are immutable, their hashcodes can be cached, enhancing performance significantly, especially when strings are used as keys in a HashMap or HashSet. The hashcode needs to be calculated only once, at the time of creation and can be reused, making operations like searching and insertion in collections very efficient.
  4. Class Loading: Java uses strings to represent class names. The immutability of strings ensures that the correct class is loaded by the ClassLoader.
  5. String Pool: Java maintains a pool of strings, where it reuses objects instead of creating new ones when a string is declared. This process is known as string interning. Immutable strings are essential for the reliability of string pooling, as it ensure that a string’s value remains constant throughout its lifespan in the pool, allowing for safe sharing of string literals across the application.
  6. Memory Efficiency: String immutability aids in memory efficiency due to the sharing of string literals. Since literals are stored in the string pool, two strings with the same content will point to the same memory location, reducing memory overhead.
  7. Simplicity and Reliability: Immutable objects are simpler to understand and use. They can’t be changed once they’re created, which makes it easier to reason about their state. This immutability reduces the likelihood of errors, especially in complex, multithreaded applications.

While immutability has these advantages, it also means that operations like concatenation, which may seem simple, can be less efficient if not handled correctly (since every operation that modifies a string results in a new string).

However, Java provides classes like StringBuilder and StringBuffer for situations where mutable strings are needed, balancing the benefits of immutability with the flexibility of mutable sequences of characters.

7. Can you write a Constructor in the Abstract class?

Yes, you can define a constructor in an abstract class. In many object-oriented programming languages, abstract classes are those that cannot be instantiated on their own; they require subclasses to provide implementations for their abstract methods. However, an abstract class can still have a constructor.

Here’s why you might want to include a constructor in an abstract class:

  • Initialization: Even though an abstract class cannot be instantiated, its constructor can be called from a subclass. This is useful for performing initialization tasks that are common to all subclasses, thereby avoiding code duplication.
  • Setting Up State: If the abstract class contains fields that need to be initialized to specific values, a constructor in the abstract class can be used to set up this state. Subclasses can then call the superclass constructor explicitly (if the language requires or allows it) to ensure this initialization happens.

Here is an example in Java:

public abstract class Animal {
    protected String name;

    public Animal(String name) { = name;

    public abstract void makeSound();

public class Dog extends Animal {

    public Dog(String name) {
        super(name); // Call the constructor of the abstract class, Animal

    public void makeSound() {
        System.out.println(name + " says: Bark!");

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Buddy");
        dog.makeSound(); // Outputs: Buddy says: Bark!

In this example, the Animal class is abstract and includes a constructor that initializes the name field. Subclasses like Dog must call the super constructor (super(name)) to ensure the name field is properly initialized when an instance of Dog is created.

This demonstrates how constructors in abstract classes can be used to set up a state that is common across all subclasses.

8. How can you override the static method in Java?🎯

In Java, overriding a static method isn’t possible in the same way that instance methods can be overridden. This is due to the nature of static methods, which are bound to the class level rather than the instance level.

However, you can declare a static method with the same signature in a subclass, but it is not considered an override but rather method hiding.

This means that the method to be called is determined at compile time, based on the reference type, not at runtime based on the actual object type. ☝️

Thus, the subclass’s static method does not override the superclass’s method; instead, it hides it.

class SuperClass {
    static void test() {
        System.out.println("Static method in SuperClass");
class SubClass extends SuperClass {
    static void test() {
        System.out.println("Static method in SubClass");
public class TestHiding {
    public static void main(String[] args) {
        SuperClass.test(); // Prints "Static method in SuperClass"
        SubClass.test();   // Prints "Static method in SubClass"
        SuperClass obj = new SubClass();
        obj.test(); // Prints "Static method in SuperClass"
class SuperClass {
    static void test() {
        System.out.println("Static method in SuperClass");
class SubClass extends SuperClass {
    static void test() {
        System.out.println("Static method in SubClass");
public class TestHiding {
    public static void main(String[] args) {
        SuperClass.test(); // Prints "Static method in SuperClass"
        SubClass.test();   // Prints "Static method in SubClass"
        SuperClass obj = new SubClass();
        obj.test(); // Prints "Static method in SuperClass"

9. What is lambda express in Java 8?

In Java 8, lambda expressions were introduced as a significant feature to provide a clear and concise way to represent one method interface using an expression. Lambda expressions are used primarily to define the implementation of a functional interface (an interface with a single abstract method, also known as SAM interface) in a more concise way. They are particularly useful in the context of collection operations and when working with streams.

A lambda expression consists of a list of parameters, an arrow (->), and a body. For example:

(parameters) -> expression


(parameters) -> { statements; }

Here are some key points about lambda expressions:

  • Simplicity: They allow for writing less verbose code, especially when implementing functional interfaces directly in place, such as in the case of anonymous inner classes.
  • Functional Programming: Lambda expressions bring elements of functional programming to Java, making operations on collections more straightforward and efficient by using stream API.
  • Use with Collections: They are often used with the Collections framework, providing a powerful way to process, filter, and map data elements.
  • Thread Operations: Lambda expressions work well with new APIs for concurrent operations, simplifying the syntax for working with threads by using APIs like the ExecutorService.

For example, consider a simple operation of printing all elements of a list before and with lambda expressions:

Without lambda (using anonymous inner class):

List<String> list = Arrays.asList("A", "B", "C", "D");
Collections.sort(list, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);

With lambda:

List<String> list = Arrays.asList("A", "B", "C", "D");
Collections.sort(list, (s1, s2) -> s1.compareTo(s2));

Lambda expressions make the code more readable and concise, reducing the boilerplate code associated with the use of anonymous classes. They have become a fundamental part of writing Java code, especially when working with modern APIs and frameworks that leverage functional programming concepts.

10. What are terminal and intermediate operations in Stream API?

In Java’s Stream API, operations on streams are classified into intermediate and terminal operations. Understanding the distinction between these two types of operations is crucial for effective stream processing.

Intermediate Operations

Intermediate operations are operations that transform a stream into another stream. These operations are lazy, meaning they do not start processing the stream elements until a terminal operation is invoked. Examples of intermediate operations include filter, map, flatMap, sorted, etc. Because they return a stream, you can chain multiple intermediate operations one after the other.

List<String> myList = Arrays.asList("apple", "banana", "cherry", "date");
      .filter(s -> s.startsWith("b"))
      .map(String::toUpperCase) // Intermediate operations

Terminal Operations

Terminal operations, on the other hand, produce a result or a side effect. Once a terminal operation is invoked on a stream, it triggers the execution of all lazy operations that were previously called. After a terminal operation is performed, the stream can no longer be used. Examples of terminal operations include forEach, collect, reduce, findFirst, anyMatch, etc.

List<String> myList = Arrays.asList("apple", "banana", "cherry", "date");
List<String> filteredList =
                                  .filter(s -> s.startsWith("b"))
                                  .collect(Collectors.toList()); // Terminal operation

In this example, collect is a terminal operation that triggers the execution of the filter and map operations and collects the result into a new list.

Terminal operations are essential for producing a final outcome from the stream operations pipeline, whether it be a list, a sum, a count, a boolean, or any other form of a single value or a collection. They are what ultimately bring the stream processing to life, allowing you to obtain tangible results from your stream transformations.

11. What is a functional interface?

In Java, a functional interface is an interface that contains exactly one abstract method. These interfaces are used as the basis for lambda expressions in Java 8 and beyond, enabling a more functional programming style within the object-oriented Java language. Despite having only one abstract method, a functional interface can have multiple default or static methods. 👓

Functional interfaces are crucial for working with lambda expressions because they provide the type context that allows a lambda expression to match the interface’s abstract method. This compatibility with lambda expressions makes functional interfaces a key part of the Java API, especially in the context of stream operations and event listeners, among other scenarios.

Here’s an example of a functional interface:

public interface SimpleFunctionalInterface {
    void execute();

The @FunctionalInterface annotation is optional but recommended because it makes your intention clear and allows the compiler to generate an error if the annotated interface does not meet the criteria of a functional interface (exactly one abstract method).

A functional interface can then be implemented using a lambda expression like so:

SimpleFunctionalInterface sfi = () -> System.out.println("Executing");
sfi.execute(); // Outputs: Executing

The Java standard library includes several functional interfaces in the java.util.function package, such as Predicate<T>, Function<T,R>, Consumer<T>, and Supplier<T>. These interfaces are designed to be used extensively with the Stream API and other areas of Java that benefit from a functional programming approach.

12. What default method in Java 8?

In Java 8, the concept of default methods was introduced in interfaces. A default method is a method in an interface that has an implementation. Before Java 8, interfaces could only have abstract methods, which means the methods could not have a body. The introduction of default methods allowed developers to add new methods to interfaces without breaking the existing implementations of these interfaces.

Default methods help in evolving interfaces over time without compromising the compatibility with classes that were implemented before the addition of new methods to the interface. This feature is particularly useful for enhancing the Java Collections Framework and other library interfaces with new functionalities.

Here’s how you can define a default method in an interface:

public interface MyInterface {
    // Abstract method
    void abstractMethod();
    // Default method with an implementation
    default void defaultMethod() {
        System.out.println("This is a default method.");

In this example, abstractMethod is a traditional abstract method that does not have a body and must be implemented by any class that implements MyInterface. On the other hand, defaultMethod has an implementation directly in the interface, so implementing classes do not need to provide an implementation for this method (though they can choose to override it).

Here’s an example of a class implementing MyInterface without providing its own implementation of defaultMethod:

public class MyClass implements MyInterface {
    public void abstractMethod() {
        System.out.println("Implementing abstractMethod");
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        myClass.abstractMethod(); // Outputs: Implementing abstractMethod
        myClass.defaultMethod();  // Outputs: This is a default method.

Default methods were a significant enhancement in Java 8, allowing for more flexible interface designs and backward compatibility with older versions of interfaces.

13. Difference between put and patch methods?

In the context of HTTP and RESTful web services, the PUT and PATCH methods are both used to update resources, but they differ in how they handle the update process:


  • Idempotency: The PUT method is idempotent, meaning that multiple identical requests should have the same effect as a single request. This property is crucial for the reliability of web systems, allowing, for example, a client to retry a request without worrying about causing unintended effects.
  • Complete Update: PUT requests replace the entire resource with the new data provided in the request. If the new data is partial, missing values may be interpreted as being set to their defaults or null, potentially resulting in the removal of unspecified fields.
  • Usage Scenario: Use PUT when you want to update a resource entirely. For instance, if you have a user resource, a PUT request might replace all the user’s data, such as email, name, and password.


  • Idempotency: The PATCH method can be idempotent but is not required to be. The idempotency of a PATCH request depends on how the server implements it and the nature of the change being requested.
  • Partial Update: Unlike PUT, PATCH is used for making partial updates to a resource. This means that you only send the data that you want to update, without touching the rest of the resource’s data.
  • Usage Scenario: Use PATCH when you want to update part of a resource. For example, if you only want to update a user’s email address without affecting any other information, a PATCH request is appropriate.


  • PUT is used for replacing a resource entirely. It is idempotent, meaning repeated requests will have the same effect as one request.
  • PATCH is used for partial updates to a resource. It can be idempotent but isn’t necessarily so, depending on how it’s implemented by the server.

Choosing between PUT and PATCH depends on the specific requirements of the operation you’re performing—whether you need to update the entire resource or just a part of it.

14. Difference between the @Controller and @RestController in Spring Boot?

In Spring Boot, the distinction between @Controller and @RestController annotations is primarily related to the type of applications they are used in and how they handle the response body. Both are used to mark a class as a Spring MVC controller, but they serve slightly different purposes.


  • Usage: Traditionally used in Spring MVC applications to build web pages. Controllers annotated with @Controller are components that can handle HTTP requests and generate a view (HTML) response.
  • Response Handling: Methods in a class annotated with @Controller typically return a String or a ModelAndView object that represents the name of the view to be rendered. To respond with a ResponseBody (data) instead of a view (HTML page), you must annotate the method with @ResponseBody.
  • Example Use Case: Best suited for applications that serve HTML content.


  • Usage: Introduced in Spring 4 to simplify the creation of RESTful web services. A class annotated with @RestController is a controller that handles HTTP requests, but it’s specifically designed for REST APIs.
  • Response Handling: All handler methods in a class annotated with @RestController assume @ResponseBody semantics by default, meaning the return value of each method is written directly to the HTTP response body, not interpreted as a view name. It eliminates the need to annotate each method with @ResponseBody.
  • Example Use Case: Best suited for service endpoints that return JSON or XML responses, typical in REST APIs.

Key Differences

  • @Controller is used for traditional web applications where the controller’s role is to return a view (like JSP, Thymeleaf).
  • @RestController combines @Controller and @ResponseBody and is suited for developing RESTful web services where the controller always returns a data response (JSON/XML) directly to the client.

In summary, the choice between @Controller and @RestController depends on what you’re building: if your application is designed to serve HTML views, use @Controller; if you’re building a REST API that returns data (like JSON or XML), use @RestController.

15. Can we send JSON in @Controller in Spring Boot?

Yes, in Spring Boot, you can send JSON from a method in a class annotated with @Controller. To do this, you need to use the @ResponseBody annotation on the handler method. This annotation indicates that the return value of the method should be bound to the web response body. It effectively turns a method into a @Controller-annotated class into a RESTful method, similar to how methods in a @RestController-annotated class behave by default.

Here is a simple example demonstrating how to send JSON from a @Controller:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RequestMapping;
public class MyController {
    public MyResponse sayHello() {
        return new MyResponse("Hello, World!");
    static class MyResponse {
        private String message;
        public MyResponse(String message) {
            this.message = message;
        // Getters and setters
        public String getMessage() {
            return message;
        public void setMessage(String message) {
            this.message = message;

In this example, the @GetMapping annotated sayHello method will return a JSON representation of the MyResponse object. The @ResponseBody annotation is what enables the serialization of the return object into JSON and sending it in the HTTP response body.

This method allows you to selectively create methods within a @Controller that return data directly to the client, while still having other methods that return views. It provides flexibility when building applications that serve both traditional web pages and also need to expose some API endpoints returning JSON or other data formats.

16. What use of accept and content-type headers in HTTP requests?

The Accept and Content-Type headers in HTTP requests play crucial roles in content negotiation between a client (such as a web browser or API client) and a server. They are used to determine the format of the data being exchanged. Understanding these headers is important for developing web applications and APIs that can serve and accept data in various formats.

Accept Header

  • Purpose: The Accept header is used by the client to specify the media types that the client is willing to receive from the server. This header is part of the request sent to the server.
  • Functionality: It informs the server of the types of content the client can correctly process, allowing the server to select a compatible content format to send in the response. If the server cannot serve any of the media types specified in the Accept header, it can respond with a 406 Not Acceptable status code.
  • Example: If a client can handle JSON and XML, it might include the following Accept header in its request: Accept: application/json, application/xml. This tells the server that it can respond with either JSON or XML.

Content-Type Header

  • Purpose: The Content-Type header is used to specify the media type of the body of the request or response. In a request, it tells the server what the data actually is (and therefore how the server should parse it). In a response, it tells the client what the data actually is (and therefore how the client should parse it).
  • Functionality: It ensures that both the sender and receiver of the HTTP request or response know how to correctly interpret the content of the message body.
  • Example: If a client is sending JSON data in the body of a POST request, it would include a Content-Type: application/json header to inform the server that the data in the body is in JSON format.


  • Accept header is used in HTTP requests to tell the server what content types the client can handle. It allows the client to specify preferences for the response’s media type.
  • Content-Type header is used in both requests and responses to specify the exact media type of the data in the body. In requests, it tells the server how to parse the incoming data, and in responses, it tells the client how to parse the data.

Together, these headers help in smoothly facilitating the content negotiation process, making sure that data is both sent and received in formats that are understood by both parties.

17. What is the difference between the @Query param and @Request param?

In the context of building web applications with frameworks that use annotations to handle HTTP requests, such as Spring Boot for Java, the @RequestParam and @QueryParam annotations (or their equivalents, since @QueryParam is specifically from JAX-RS, and in Spring it’s also @RequestParam) are often used to extract query parameters from the URL. However, there seems to be a little mix-up in the terminology based on the context of your question, so I’ll clarify based on both Spring Framework (Spring Boot) and JAX-RS (Java API for RESTful Web Services), which are common contexts where such questions arise.

Spring Framework (Spring Boot)

  • @RequestParam: Used in Spring MVC or Spring Boot to bind parameter values from the query string to a method parameter in your controller. It’s commonly used in web applications to handle HTTP GET requests where the parameters are appended to the URL. Example:
  public ResponseEntity<String> getItems(@RequestParam String category) {
      // Implementation here

In this example, @RequestParam is used to extract the “category” parameter from the query string.

JAX-RS (Java API for RESTful Web Services)

  • @QueryParam: Equivalent to Spring’s @RequestParam, but used in the JAX-RS framework to bind query parameter values from the URL to your resource method parameters. Example:
  public String getItems(@QueryParam("category") String category) {
      // Implementation here

Here, @QueryParam serves the same purpose as @RequestParam in Spring, but it’s specific to JAX-RS.


Both @RequestParam (Spring) and @QueryParam (JAX-RS) are used to extract query parameters from the URL. The primary difference lies in the framework in which they are used. Your choice between them depends on whether you are developing your application with Spring (Spring MVC/Spring Boot) or a JAX-RS-based framework (like Jersey or RESTEasy).

In both cases, these annotations allow server-side methods to access data sent by the client within the URL query string, facilitating the development of dynamic web applications that can respond to varied client requests.

18. Can you change the location of application.yml file in String boot application?

Yes, you can change the location of the application.yml (or file in a Spring Boot application. By default, Spring Boot looks for configuration files in certain locations inside your project (like src/main/resources) and also allows external configurations. If you want to place your application.yml file outside these default locations, you have several options to specify its new location:

1. Command Line Argument

You can specify the location of the application.yml file when you run your application with a command line argument:

java -jar your-application.jar --spring.config.location=file:/path/to/config/application.yml

This tells Spring Boot to look for the configuration file at the specified path.

2. Environment Variable

You can set the SPRING_CONFIG_LOCATION environment variable to point to your config file:

export SPRING_CONFIG_LOCATION=file:/path/to/config/application.yml

Then run your application normally.

3. Programmatically

You can customize the config file location programmatically by modifying the SpringApplication initialization in your main method. This is more complex and less flexible than the other methods.

4. Spring Boot’s Config Data API

Spring Boot 2.4 introduced a new Config Data API that offers more flexibility in how and from where configuration is loaded. You can use this API to specify custom locations by setting the spring.config.import property either in a default application.yml file or as a command line argument:

java -jar your-application.jar --spring.config.import=file:/path/to/config/application.yml

Or in an file located in one of the default locations:


This approach allows Spring Boot to include the external configuration file in its environment.

When specifying a custom location, be mindful of how Spring Boot merges configurations from different sources. Properties defined in files that are read later override those defined in earlier files. This allows for default properties to be overridden by external configurations when necessary.

Choosing the best method depends on your deployment environment and specific requirements, such as whether the configuration path is fixed or needs to be flexible across different environments.



About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over