Java Questions
About Lesson

String vs StringBuffer in Java

When it comes to making frequent updates to a string, using StringBuffer is preferred over String. In Java, String objects are immutable, meaning once a String object is created, it cannot be altered. Any modification, such as appending, deleting, or inserting characters, will create a new String object, leading to increased memory usage and slower performance due to the constant creation of new objects.

On the other hand, StringBuffer is mutable, allowing modifications without creating new objects. This makes StringBuffer more efficient in scenarios where the string undergoes numerous changes, such as in loops or heavy concatenation operations. StringBuffer provides synchronized methods for modifying the buffer, making it thread-safe.

Here’s an example demonstrating the difference:

public class StringVsStringBuffer {
    public static void main(String[] args) {
        // Using String
        String str = "Hello";
        for (int i = 0; i < 5; i++) {
            str += " World";
        }
        System.out.println("String result: " + str);

        // Using StringBuffer
        StringBuffer stringBuffer = new StringBuffer("Hello");
        for (int i = 0; i < 5; i++) {
            stringBuffer.append(" World");
        }
        System.out.println("StringBuffer result: " + stringBuffer.toString());
    }
}

In the above example, the String approach creates new String objects for each concatenation, while StringBuffer modifies the existing buffer, making it more efficient.

Singleton Pattern in Java

A Singleton in Java ensures that a class has only one instance and provides a global point of access to it. To create a robust Singleton class that counters all breakable conditions like reflection, serialization, and multithreading, follow these steps:

  1. Make the constructor private.
  2. Create a static instance of the class.
  3. Provide a public static method to access the instance.
  4. Use synchronization for thread safety.
  5. Prevent cloning.
  6. Prevent creation of a second instance during deserialization.

Here’s an implementation:

import java.io.Serializable;

public class Singleton implements Serializable, Cloneable {
    private static final long serialVersionUID = 1L;

    // private static instance
    private static volatile Singleton instance;

    // private constructor
    private Singleton() {
        if (instance != null) {
            throw new IllegalStateException("Already initialized.");
        }
    }

    // public method to provide access to the instance
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    // Prevent cloning
    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("Cloning of this singleton is not allowed");
    }

    // Prevent creation of a new instance during deserialization
    protected Object readResolve() {
        return getInstance();
    }
}

This implementation ensures that only one instance of Singleton is created, and it handles serialization, cloning, and reflection vulnerabilities.

Auto Configuration in Java

Auto Configuration is a feature in Spring Boot that automatically configures Spring applications based on the jar dependencies added in the project. It eliminates the need for manual configurations, making development faster and easier.

Spring Boot’s auto-configuration mechanism works by scanning the classpath and checking for the presence of various libraries and their versions. Based on these observations, it configures beans and settings automatically. This is particularly useful in creating production-ready applications quickly, as it provides sensible defaults and configurations out-of-the-box.

Example of auto-configuration:

  • If spring-boot-starter-web is on the classpath, Spring Boot auto-configures a web server.
  • If spring-boot-starter-data-jpa is found, it configures JPA-related beans and settings.

You can see auto-configuration in action in a simple Spring Boot application with the @SpringBootApplication annotation, which enables auto-configuration among other things.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MySpringBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(MySpringBootApplication.class, args);
    }
}

In this example, Spring Boot scans for configurations and applies them automatically, reducing the need for manual bean definitions.

@Primary vs @Qualifier

In Spring, @Primary and @Qualifier annotations are used to resolve the ambiguity when multiple beans of the same type are available.

  • @Primary: When multiple beans of the same type are present, and one of them is marked with @Primary, that bean is preferred by Spring during autowiring. It serves as a default choice.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    @Primary
    public MyService primaryService() {
        return new PrimaryServiceImpl();
    }

    @Bean
    public MyService secondaryService() {
        return new SecondaryServiceImpl();
    }
}
  • @Qualifier: It provides more fine-grained control over which bean should be autowired when multiple candidates are available. You specify the bean name explicitly.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class MyComponent {
    private final MyService myService;

    @Autowired
    public MyComponent(@Qualifier("secondaryService") MyService myService) {
        this.myService = myService;
    }
}

In the above example, @Primary sets PrimaryServiceImpl as the default bean, while @Qualifier explicitly selects SecondaryServiceImpl.

Idempotent

Idempotent is a concept that describes an operation that produces the same result no matter how many times it is performed. In other words, making multiple identical requests has the same effect as making a single request.

In HTTP methods, GET, PUT, and DELETE are idempotent, whereas POST is not. Idempotency is crucial in distributed systems and APIs to ensure consistency and reliability.

For example, if a client sends a DELETE request to remove a resource, the server should delete the resource if it exists. If the client repeats the DELETE request, the server should return the same response, indicating the resource has been deleted.

Example in Java:

public class IdempotentOperation {
    private static Set<String> items = new HashSet<>();

    public static void main(String[] args) {
        addItem("item1");
        addItem("item1"); // Duplicate addition
        addItem("item2");
        System.out.println(items); // Output: [item1, item2]
    }

    public static void addItem(String item) {
        items.add(item); // Adding the same item multiple times has no additional effect
    }
}

In the above code, adding the same item multiple times has no additional effect, demonstrating idempotency.

Class Loader in Java

A class loader in Java is a part of the Java Runtime Environment (JRE) that dynamically loads Java classes into the Java Virtual Machine (JVM). It is responsible for finding and loading class files at runtime, as opposed to compile time. Class loaders follow a delegation hierarchy, where each class loader delegates the loading request to its parent before attempting to load the class itself.

Types of Class Loaders:

  1. Bootstrap Class Loader: Loads core Java classes (e.g., java.lang.*) from the Java Runtime Environment’s lib directory.
  2. Extension Class Loader: Loads classes from the ext directory or any other directories specified by the java.ext.dirs system property.
  3. Application (or System) Class Loader: Loads classes from the application’s classpath, which includes the classes defined by the user.

Example demonstrating class loading:

public class ClassLoaderExample {
    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoaderExample.class.getClassLoader();

        System.out.println("Application ClassLoader: " + classLoader);
        System.out.println("Extension ClassLoader: " + classLoader.getParent());
        System.out.println("Bootstrap ClassLoader: " + classLoader.getParent().getParent()); // Will return null
    }
}

In this example, the ClassLoaderExample class is loaded by the Application ClassLoader, which is a child of the Extension ClassLoader, which in turn is a child of the Bootstrap ClassLoader.

Heap Memory and Stack Memory in Java

In Java, memory management is divided into two main regions: Heap Memory and Stack Memory.

  • Heap Memory: Used for dynamic memory allocation for Java objects and JRE classes at runtime. Objects created in heap memory are globally accessible and are managed by the Garbage Collector to reclaim memory once objects are no longer in use. Heap memory is divided into generations: Young Generation, Old Generation, and Permanent Generation (Metaspace in Java 8 and later).
  • Stack Memory: Used for static memory allocation and executing threads. It contains primitive values and references to objects in heap memory. Each thread has its own stack, which is LIFO (Last In, First Out). Stack memory is limited and faster compared to heap memory.

Example illustrating heap and stack memory:

public class MemoryExample {
    public static void main(String[] args) {
        int a = 10; // Stored in stack
        MemoryExample obj = new MemoryExample(); // obj reference stored in stack, actual object in heap
        obj.method();
    }

    public void

 method() {
        int b = 20; // Stored in stack
        String str = new String("Hello"); // str reference in stack, "Hello" object in heap
    }
}

In the above example, primitive variables a and b are stored in stack memory, while objects like MemoryExample and String are stored in heap memory.

@Configuration and @Bean

In Spring, @Configuration and @Bean annotations are used for defining beans and their configurations in a Spring application.

  • @Configuration: Indicates that the class can be used by the Spring IoC container as a source of bean definitions. It is a specialization of @Component, allowing classes to be autodetected through classpath scanning.
  • @Bean: Indicates that a method produces a bean to be managed by the Spring container. It is used within @Configuration classes to define beans.

Example:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }

    @Bean
    public MyRepository myRepository() {
        return new MyRepositoryImpl();
    }
}

In this example, AppConfig is a configuration class, and myService and myRepository are beans defined using the @Bean annotation.

Why String is Immutable in Java

String in Java is immutable for several reasons:

  1. Security: Immutable objects are inherently thread-safe and can be shared among multiple threads without synchronization issues. This is crucial for sensitive data like usernames and passwords.
  2. String Pool: String immutability allows for the implementation of the string pool, which reduces memory usage by storing only one copy of each literal string.
  3. Caching: Since the value of an immutable object cannot change, it can be cached without worrying about it being altered.
  4. Performance: Immutable strings are more efficient because the same instance can be reused without creating new objects for every modification.

Example illustrating immutability:

public class StringImmutability {
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = "Hello";

        // str1 and str2 refer to the same object in the string pool
        System.out.println(str1 == str2); // Output: true

        str1 = str1.concat(" World");
        System.out.println(str1); // Output: Hello World
        System.out.println(str2); // Output: Hello
    }
}

In this example, modifying str1 creates a new string, leaving str2 unchanged, demonstrating immutability.

Creating an Immutable Class in Java

An immutable class in Java is a class whose instances cannot be modified once created. To create an immutable class:

  1. Declare the class as final.
  2. Make all fields private and final.
  3. Do not provide setters.
  4. Initialize all fields via constructor.
  5. Provide only getters for accessing the fields.

Example:

public final class ImmutablePerson {
    private final String name;
    private final int age;

    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

In this example, ImmutablePerson is immutable because its fields are final and private, and there are no setters to modify them after construction.

How HashMap Works Internally

A HashMap in Java is a data structure that provides a way to store key-value pairs. It uses a hash table for efficient storage and retrieval of data. Here’s how it works internally:

  1. Hashing: When a key-value pair is inserted, the key is hashed using the hashCode() method to determine the bucket index.
  2. Buckets: The HashMap has an array of buckets, where each bucket can hold multiple key-value pairs using a linked list or tree structure (from Java 8 onwards).
  3. Collision Handling: When two keys hash to the same index, a collision occurs. The HashMap handles collisions by storing entries in a linked list or tree at the same bucket index.
  4. Load Factor and Resizing: The load factor determines when to resize the HashMap. When the number of entries exceeds the threshold (load factor * number of buckets), the HashMap resizes by doubling the number of buckets and rehashing all entries.
  5. Retrieval: To retrieve a value, the key is hashed to find the correct bucket. Then, the bucket is searched for the key using equals() method.

Example:

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);

        System.out.println(map.get("two")); // Output: 2
    }
}

In this example, keys are hashed to determine their bucket index, and values are stored and retrieved efficiently.

Garbage Collector in Java

The Garbage Collector (GC) in Java is responsible for automatic memory management. It reclaims memory by identifying and disposing of objects that are no longer in use. The GC performs the following steps:

  1. Marking: The GC identifies which objects are reachable and which are not by traversing object references starting from root objects (e.g., local variables, static fields).
  2. Sweeping: The GC reclaims memory occupied by unreachable objects, making it available for new objects.
  3. Compacting: The GC compacts the memory by moving reachable objects together, reducing fragmentation and freeing up contiguous memory space.

Java provides several types of garbage collectors, such as:

  • Serial GC: A single-threaded collector suitable for small applications.
  • Parallel GC: Uses multiple threads for parallel garbage collection.
  • CMS (Concurrent Mark-Sweep) GC: Low-pause collector that works concurrently with application threads.
  • G1 (Garbage-First) GC: Designed for large heap sizes and provides predictable pause times.

Example to demonstrate GC:

public class GarbageCollectorExample {
    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            String str = new String("Hello" + i);
        }
        System.gc(); // Suggests the JVM to perform garbage collection
    }
}

In this example, creating many String objects and then suggesting garbage collection demonstrates how the GC can reclaim memory.

Checked vs Unchecked Exceptions

In Java, exceptions are categorized into checked and unchecked exceptions:

  • Checked Exceptions: These are exceptions that are checked at compile-time. They must be either caught or declared in the method signature using the throws keyword. Examples include IOException, SQLException.
import java.io.*;

public class CheckedExceptionExample {
    public static void main(String[] args) {
        try {
            FileReader file = new FileReader("test.txt");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}
  • Unchecked Exceptions: These are exceptions that are not checked at compile-time but at runtime. They do not need to be declared or caught explicitly. Examples include NullPointerException, ArrayIndexOutOfBoundsException.
public class UncheckedExceptionExample {
    public static void main(String[] args) {
        int[] arr = new int[5];
        System.out.println(arr[10]); // This will throw ArrayIndexOutOfBoundsException
    }
}

In these examples, FileNotFoundException is a checked exception, while ArrayIndexOutOfBoundsException is an unchecked exception.

Throw vs Throws

In Java, throw and throws are used to handle exceptions, but they serve different purposes:

  • throw: Used to explicitly throw an exception from a method or a block of code. It can be used to throw both checked and unchecked exceptions.
public class ThrowExample {
    public static void main(String[] args) {
        try {
            validateAge(15);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void validateAge(int age) {
        if (age < 18) {
            throw new IllegalArgumentException("Age must be at least 18");
        }
    }
}
  • throws: Used in the method signature to declare that a method can throw one or more exceptions. It is mandatory for methods that throw checked exceptions but optional for unchecked exceptions.
import java.io.*;

public class ThrowsExample {
    public static void main(String[] args) {
        try {
            readFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void readFile() throws IOException {
        FileReader file = new FileReader("test.txt");
    }
}

In the examples, throw is used to explicitly throw an exception, while throws is used to declare exceptions that a method may throw.

Aggregation and Composition

Aggregation and Composition are two types of association relationships in object-oriented programming:

  • Aggregation: Represents a “has-a” relationship where the child object can exist independently of the parent. It is a weaker form of association. For example, a Library has Books, but Books can exist without the `Library’
class Book {
    private String title;
    // Constructor, getters, and setters
}

class Library {
    private List<Book> books;
    // Constructor, getters, and setters
}
  • Composition: Represents a “contains-a” relationship where the child object’s lifecycle is dependent on the parent. It is a stronger form of association. For example, a House has Rooms, and Rooms cannot exist without the House.
class Room {
    private String name;
    // Constructor, getters, and setters
}

class House {
    private List<Room> rooms;
    // Constructor, getters, and setters
}

In these examples, Book can exist independently of Library, but Room cannot exist without House.

Adding Elements to a Collection with No Duplicates and Preserved Insertion Order

To add elements to a collection where duplicates are not allowed and insertion order is preserved, you should use LinkedHashSet. It combines the behavior of a HashSet (no duplicates) and a LinkedHashMap (preserved insertion order).

Example:

import java.util.LinkedHashSet;

public class LinkedHashSetExample {
    public static void main(String[] args) {
        LinkedHashSet<String> set = new LinkedHashSet<>();
        set.add("one");
        set.add("two");
        set.add("three");
        set.add("one"); // Duplicate, won't be added

        System.out.println(set); // Output: [one, two, three]
    }
}

In this example, LinkedHashSet maintains the insertion order and does not allow duplicate elements.

Limit vs Skip

In Java Streams, limit and skip are intermediate operations that allow slicing of streams:

  • limit: Limits the number of elements in the stream to the specified count.
import java.util.stream.Stream;

public class LimitExample {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5)
              .limit(3)
              .forEach(System.out::println); // Output: 1, 2, 3
    }
}
  • skip: Skips the first n elements of the stream.
import java.util.stream.Stream;

public class SkipExample {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5)
              .skip(2)
              .forEach(System.out::println); // Output: 3, 4, 5
    }
}

In these examples, limit restricts the number of elements to 3, and skip skips the first 2 elements.

Stream vs Parallel Stream

In Java, Stream and Parallel Stream are used for processing sequences of elements:

  • Stream: Sequential processing of elements. Operations are performed on one element at a time.
import java.util.Arrays;
import java.util.List;

public class StreamExample {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.stream()
            .map(x -> x * x)
            .forEach(System.out::println); // Output: 1, 4, 9, 16, 25
    }
}
  • Parallel Stream: Parallel processing of elements. Operations are divided into multiple threads for faster execution.
import java.util.Arrays;
import java.util.List;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        list.parallelStream()
            .map(x -> x * x)
            .forEach(System.out::println); // Output may vary in order
    }
}

In these examples, Stream processes elements sequentially, while Parallel Stream processes them in parallel.

@Controller Annotation in Spring

The @Controller annotation in Spring is used to mark a class as a Spring MVC controller, which handles HTTP requests and returns views.

Example:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/home")
public class HomeController {
    @GetMapping
    public String home() {
        return "home"; // Returns the view name "home"
    }
}

To create a controller without using @Controller, you can extend AbstractController or SimpleFormController and override their methods. However, this approach is less common and more cumbersome than using annotations.

Example without @Controller:

import org.springframework.web.servlet.mvc.AbstractController;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HomeController extends AbstractController {
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
        return new ModelAndView("home");
    }
}

Filtering Loans with Incomplete Status Using Java 8 Features

To filter out loans with an incomplete status using Java 8 features, you can use the Stream API along with filter and collect methods.

Example:

import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;

class Loan {
    private String status;
    private double amount;

    public Loan(String status, double amount) {
        this.status = status;
        this.amount = amount;
    }

    public String getStatus() {
        return status;
    }

    @Override
    public String toString() {
        return "Loan{" + "status='" + status + '\'' + ", amount=" + amount + '}';
    }
}

public class LoanFilterExample {
    public static void main(String[] args) {
        List<Loan> loans = Arrays.asList(
                new Loan("complete", 5000),
                new Loan("incomplete", 3000),
                new Loan("complete", 7000),
                new Loan("incomplete", 2000)
        );

        List<Loan> completeLoans = loans.stream()
                .filter(loan -> !"incomplete".equals(loan.getStatus()))
                .collect(Collectors.toList());

        completeLoans.forEach(System.out::println);
    }
}

In this example, the stream filters out loans with the status “incomplete” and collects the remaining loans into a list.

These detailed explanations and code examples should help in understanding the concepts and answering the questions comprehensively.

What is the Starter Dependency of the Spring Boot Module?

Spring Boot simplifies the development process by providing a set of starter dependencies. These are pre-configured sets of libraries that work together seamlessly to provide a specific functionality. Each starter dependency includes a set of libraries and configurations necessary to work with a particular technology or framework.

For example:

  • spring-boot-starter-web: Includes dependencies for building web applications, such as Spring MVC, Tomcat, and Jackson.
  • spring-boot-starter-data-jpa: Provides dependencies for working with JPA and Hibernate.
  • spring-boot-starter-security: Includes Spring Security for authentication and authorization.

These starters are designed to reduce boilerplate configuration and streamline the setup process. By including a starter dependency in your pom.xml (for Maven) or build.gradle (for Gradle), you get all the necessary libraries and configurations needed for that particular functionality.

Example using Maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Example using Gradle:

implementation 'org.springframework.boot:spring-boot-starter-web'

How Would You Differentiate Between a String, StringBuffer, and a StringBuilder?

In Java, String, StringBuffer, and StringBuilder are used to handle strings, but they have different characteristics and use cases:

  1. String:
  • Immutable: Once created, the value cannot be changed.
  • Thread-Safe: Immutable objects are inherently thread-safe.
  • Performance: Creating many strings can lead to memory overhead because each modification creates a new String object.
  • Use Case: Suitable for strings that do not require frequent modification.
  1. StringBuffer:
  • Mutable: The content can be changed without creating new objects.
  • Thread-Safe: Synchronized methods ensure thread safety, but this adds overhead.
  • Performance: Slower than StringBuilder due to synchronization overhead.
  • Use Case: Suitable for strings that require frequent modification and are accessed from multiple threads.
  1. StringBuilder:
  • Mutable: The content can be changed without creating new objects.
  • Not Thread-Safe: No synchronization, which makes it faster but not safe for multi-threaded access.
  • Performance: Faster than StringBuffer due to the lack of synchronization.
  • Use Case: Suitable for strings that require frequent modification and are accessed from a single thread.

Example:

public class StringExample {
    public static void main(String[] args) {
        // String example
        String str = "Hello";
        str = str + " World"; // Creates a new String object
        System.out.println(str); // Output: Hello World

        // StringBuffer example
        StringBuffer stringBuffer = new StringBuffer("Hello");
        stringBuffer.append(" World"); // Modifies the existing object
        System.out.println(stringBuffer); // Output: Hello World

        // StringBuilder example
        StringBuilder stringBuilder = new StringBuilder("Hello");
        stringBuilder.append(" World"); // Modifies the existing object
        System.out.println(stringBuilder); // Output: Hello World
    }
}

How to Enable Debugging Log in a Spring Boot Application?

Enabling debugging logs in a Spring Boot application can be done in multiple ways, providing detailed logs to help diagnose issues:

  1. Using application.properties:
    Add the following line to src/main/resources/application.properties to enable debug logging:
   logging.level.root=DEBUG

This sets the root logging level to DEBUG, meaning all logs with a level of DEBUG or higher will be printed.

  1. Using application.yml:
    Alternatively, you can configure logging in src/main/resources/application.yml:
   logging:
     level:
       root: DEBUG
  1. Programmatically:
    You can also enable debug logging programmatically within your Spring Boot application. For example, in the main application class:
   import org.springframework.boot.SpringApplication;
   import org.springframework.boot.autoconfigure.SpringBootApplication;
   import org.springframework.boot.logging.LogLevel;
   import org.springframework.boot.logging.LoggingSystem;

   @SpringBootApplication
   public class Application {
       public static void main(String[] args) {
           SpringApplication.run(Application.class, args);

           LoggingSystem loggingSystem = LoggingSystem.get(Application.class.getClassLoader());
           loggingSystem.setLogLevel("ROOT", LogLevel.DEBUG);
       }
   }
  1. Command Line:
    You can also enable debug logging by passing a command-line argument when starting the application:
   java -jar myapp.jar --logging.level.root=DEBUG

What Does the @SpringBootApplication Annotation Do Internally?

The @SpringBootApplication annotation is a convenience annotation that combines three other annotations:

  1. @Configuration: Indicates that the class can be used by the Spring IoC container as a source of bean definitions.
  2. @EnableAutoConfiguration: Tells Spring Boot to start adding beans based on classpath settings, other beans, and various property settings. It looks for spring-boot-autoconfigure classes and configures beans automatically.
  3. @ComponentScan: Enables component scanning, which automatically discovers and registers beans in the Spring application context. It scans the package of the class with the @SpringBootApplication annotation and its sub-packages.

Example:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MySpringBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(MySpringBootApplication.class, args);
    }
}

Internally, when @SpringBootApplication is used, the following occurs:

  • Bean Definitions: Classes annotated with @Component, @Service, @Repository, etc., are registered as beans.
  • Auto-Configuration: Spring Boot’s auto-configuration mechanism kicks in to set up default configurations based on the classpath and property settings.
  • Component Scanning: The package of the main application class and its sub-packages are scanned for Spring components.

What is the Root Application Context in Spring MVC? How is it Loaded?

In Spring MVC, the Root Application Context is the main context that is loaded during the startup of the Spring application. It contains the configuration for the entire application, including bean definitions, service configurations, and infrastructure components.

The Root Application Context is typically loaded by the ContextLoaderListener, which is configured in the web.xml file (for XML-based configuration) or through Java configuration.

Example using web.xml:

<web-app>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <!-- Other configurations -->
</web-app>

Example using Java Configuration:

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;

public class MyWebApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(AppConfig.class);
        servletContext.addListener(new ContextLoaderListener(rootContext));
    }
}

The Root Application Context is responsible for loading beans and configurations that are used throughout the application, making it a central part of the Spring MVC framework.

What is Meant by Interface?

In Java, an interface is a reference type, similar to a class, that can contain only constants, method signatures, default methods, static methods, and nested types. Interfaces cannot contain instance fields or constructors. They provide a way to achieve abstraction and multiple inheritance in Java.

An interface defines a contract that the implementing classes must follow. By using interfaces, you can ensure that a class adheres to a specific set of methods without dictating how those methods should be implemented.

Example:

interface Animal {
    void eat();
    void sleep();
}

class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating");
    }

    @Override
    public void sleep() {
        System.out.println("Dog is sleeping");
    }
}

public class InterfaceExample {
    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.eat();
        dog.sleep();
    }
}

In this example, the Animal interface defines two methods: eat and sleep. The Dog class implements the Animal interface and provides concrete implementations for these methods.

Difference Between Comparator and Comparable

In Java, Comparator and Comparable are two interfaces used for sorting objects:

  • Comparable:
  • Interface: Part of java.lang package.
  • Method: Contains a single method compareTo(Object o).
  • Use Case: Used for natural ordering of objects.
  • Implementation: The class itself implements the Comparable interface and defines the compareTo method.

Example:

class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String

 name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person other) {
        return this.age - other.age;
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class ComparableExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        Collections.sort(people);
        System.out.println(people); // Output: [Bob (25), Alice (30), Charlie (35)]
    }
}
  • Comparator:
  • Interface: Part of java.util package.
  • Method: Contains a single method compare(Object o1, Object o2).
  • Use Case: Used for custom ordering of objects.
  • Implementation: A separate class implements the Comparator interface and defines the compare method.

Example:

import java.util.*;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

class NameComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return p1.getName().compareTo(p2.getName());
    }
}

public class ComparatorExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));

        Collections.sort(people, new NameComparator());
        System.out.println(people); // Output: [Alice (30), Bob (25), Charlie (35)]
    }
}

In these examples, Comparable is used to sort Person objects by age, while Comparator is used to sort Person objects by name.

What is a Fully Qualified Domain Name (FQDN)?

A Fully Qualified Domain Name (FQDN) is the complete domain name for a specific computer or host on the internet. It includes both the hostname and the domain name. An FQDN provides the exact location of the host within the domain hierarchy, ensuring it is unique and unambiguous.

An FQDN consists of several parts:

  1. Hostname: The name of the specific host (e.g., www).
  2. Domain Name: The name of the domain (e.g., example.com).
  3. Top-Level Domain (TLD): The highest level of the domain hierarchy (e.g., .com, .org, .net).

Example:

  • www.example.com: In this FQDN, www is the hostname, example is the domain name, and .com is the TLD.

An FQDN is used to specify the exact server or service in network communications, ensuring that it can be found and accessed correctly.

What is the Working Principle of an ArrayList?

ArrayList in Java is a resizable array implementation of the List interface. It provides dynamic array capabilities, allowing elements to be added, removed, and accessed efficiently.

Key principles:

  1. Dynamic Array: ArrayList maintains an internal array to store elements. When the array reaches its capacity, it is resized to accommodate additional elements.
  2. Automatic Resizing: When elements are added and the internal array is full, ArrayList automatically creates a new array with increased capacity (typically 1.5 times the current capacity), and copies the existing elements to the new array.
  3. Indexed Access: Elements are accessed by their index, allowing constant-time access (O(1)) to elements.
  4. Insertion and Deletion: Adding or removing elements involves shifting elements, resulting in linear time complexity (O(n)).

Example:

import java.util.ArrayList;

public class ArrayListExample {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("one");
        list.add("two");
        list.add("three");

        System.out.println(list.get(1)); // Output: two

        list.remove(1);
        System.out.println(list); // Output: [one, three]
    }
}

In this example, an ArrayList is used to store and manipulate a list of strings. Elements are added, accessed by index, and removed, demonstrating the dynamic nature of ArrayList.

How Do You Handle an Exception?

Exception handling in Java is done using try, catch, finally, and throw keywords:

  1. try: The block of code that might throw an exception.
  2. catch: The block of code that handles the exception.
  3. finally: The block of code that executes regardless of whether an exception was thrown or not.
  4. throw: Used to explicitly throw an exception.

Example:

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        try {
            int result = divide(10, 0);
            System.out.println("Result: " + result);
        } catch (ArithmeticException e) {
            System.out.println("Exception caught: " + e.getMessage());
        } finally {
            System.out.println("Finally block executed");
        }
    }

    public static int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }
}

In this example, the divide method throws an ArithmeticException when division by zero is attempted. The try block contains the code that might throw the exception, the catch block handles the exception, and the finally block executes regardless of the exception.

What is a Custom Exception?

A custom exception is a user-defined exception class that extends Exception or RuntimeException. Custom exceptions provide more specific and meaningful error messages and handling mechanisms for specific application scenarios.

Steps to create a custom exception:

  1. Extend the Exception or RuntimeException class.
  2. Provide one or more constructors.

Example:

class InsufficientFundsException extends Exception {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

class BankAccount {
    private double balance;

    public BankAccount(double balance) {
        this.balance = balance;
    }

    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException("Insufficient funds for withdrawal");
        }
        balance -= amount;
    }
}

public class CustomExceptionExample {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(100);
        try {
            account.withdraw(150);
        } catch (InsufficientFundsException e) {
            System.out.println("Exception caught: " + e.getMessage());
        }
    }
}

In this example, InsufficientFundsException is a custom exception that is thrown when there are insufficient funds for a withdrawal. The custom exception provides a meaningful error message specific to the application scenario.

Difference Between Checked and Unchecked Exception

Checked and unchecked exceptions are two types of exceptions in Java:

  • Checked Exception:
  • Compile-Time: Checked exceptions are checked at compile time.
  • Must Be Handled: Must be either caught or declared in the method signature using throws.
  • Examples: IOException, SQLException.
import java.io.*;

public class CheckedExceptionExample {
    public static void main(String[] args) {
        try {
            FileReader file = new FileReader("test.txt");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}
  • Unchecked Exception:
  • Runtime: Unchecked exceptions are checked at runtime.
  • Optional Handling: Do not need to be declared or caught explicitly.
  • Examples: NullPointerException, ArrayIndexOutOfBoundsException.
public class UncheckedExceptionExample {
    public static void main(String[] args) {
        int[] arr = new int[5];
        System.out.println(arr[10]); // This will throw ArrayIndexOutOfBoundsException
    }
}

In these examples, FileNotFoundException is a checked exception, while ArrayIndexOutOfBoundsException is an unchecked exception.

If You Close the Database Connectivity in Try Block and Finally Block as Well, What is Going to Happen?

Closing database connectivity in both the try block and the finally block can lead to potential issues, such as attempting to close an already closed connection, which can result in an exception.

Best practice is to close the connection in the finally block to ensure it is closed regardless of whether an exception is thrown or not. Using the try-with-resources statement (introduced in Java 7) is the recommended approach for managing resources, as it ensures that resources are closed automatically.

Example with try-with-resources:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseExample {
    public static void main

(String[] args) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "root";
        String password = "password";

        try (Connection connection = DriverManager.getConnection(url, username, password)) {
            // Use the connection
            System.out.println("Database connection established");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

In this example, the connection is closed automatically when the try block exits, ensuring proper resource management.

Difference Between Abstract Class and Interface

Abstract classes and interfaces in Java are used to achieve abstraction, but they have different use cases and features:

  • Abstract Class:
  • Implementation: Can have both abstract methods (without implementation) and concrete methods (with implementation).
  • Fields: Can have instance variables.
  • Constructors: Can have constructors.
  • Inheritance: Supports single inheritance.
  • Use Case: Used when classes share a common base and some methods with implementation.
abstract class Animal {
    abstract void makeSound();

    public void sleep() {
        System.out.println("Sleeping");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}
  • Interface:
  • Implementation: Can have only abstract methods (before Java 8). From Java 8 onwards, can have default and static methods.
  • Fields: Only constants (static final).
  • Constructors: Cannot have constructors.
  • Inheritance: Supports multiple inheritance.
  • Use Case: Used to define a contract that classes must follow.
interface Animal {
    void makeSound();
}

class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Bark");
    }
}

In these examples, Animal is defined as both an abstract class and an interface, with Dog providing concrete implementations.

Explain the Use of Final Keyword in Variable, Method, and Class

The final keyword in Java is used to restrict the modification of variables, methods, and classes:

  • Final Variable: A variable marked as final cannot be reassigned once it is initialized. This is often used to define constants.
public class FinalVariableExample {
    public static final int CONSTANT = 10;

    public static void main(String[] args) {
        System.out.println(CONSTANT);
        // CONSTANT = 20; // Error: Cannot assign a value to final variable 'CONSTANT'
    }
}
  • Final Method: A method marked as final cannot be overridden by subclasses. This is used to prevent modification of critical methods.
class Parent {
    public final void display() {
        System.out.println("Parent display");
    }
}

class Child extends Parent {
    // public void display() { // Error: Cannot override the final method from Parent
    //     System.out.println("Child display");
    // }
}
  • Final Class: A class marked as final cannot be subclassed. This is used to prevent inheritance.
public final class FinalClass {
    public void display() {
        System.out.println("Final class display");
    }
}

// class SubClass extends FinalClass { // Error: Cannot subclass the final class 'FinalClass'
// }

In these examples, the final keyword is used to enforce immutability and prevent modification.

What’s the Difference Between User Thread and Daemon Thread?

In Java, threads can be classified into two types: user threads and daemon threads:

  • User Thread:
  • Lifecycle: Keeps the JVM running until all user threads have completed.
  • Purpose: Used for executing application tasks.
  • Creation: Created by default when a new thread is spawned.
public class UserThreadExample {
    public static void main(String[] args) {
        Thread userThread = new Thread(() -> {
            System.out.println("User thread running");
        });
        userThread.start();
    }
}
  • Daemon Thread:
  • Lifecycle: Does not prevent the JVM from exiting when all user threads have finished. Terminates when all user threads have completed.
  • Purpose: Used for background tasks (e.g., garbage collection).
  • Creation: Must be explicitly marked as a daemon thread.
public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("Daemon thread running");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
    }
}

In these examples, the user thread and daemon thread are created to demonstrate their differences in lifecycle and purpose.

What Are the Different Properties of MVC Routes?

In the context of Spring MVC, routes (or request mappings) define how HTTP requests are mapped to handler methods in controllers. The following properties are commonly used:

  1. URL Pattern: Defines the URL to which the handler method responds. Specified using @RequestMapping, @GetMapping, @PostMapping, etc.
@GetMapping("/home")
public String home() {
    return "home";
}
  1. HTTP Method: Specifies the HTTP method (GET, POST, PUT, DELETE) to which the handler method responds.
@PostMapping("/submit")
public String submit() {
    return "submit";
}
  1. Path Variables: Allows dynamic values in the URL to be mapped to method parameters.
@GetMapping("/user/{id}")
public String getUser(@PathVariable int id) {
    return "user: " + id;
}
  1. Query Parameters: Maps query parameters from the URL to method parameters.
@GetMapping("/search")
public String search(@RequestParam String query) {
    return "search results for: " + query;
}
  1. Request Headers: Maps request headers to method parameters.
@GetMapping("/headers")
public String headers(@RequestHeader("User-Agent") String userAgent) {
    return "User-Agent: " + userAgent;
}
  1. Consumes: Specifies the media types that the handler method can consume.
@PostMapping(value = "/consume", consumes = "application/json")
public String consume(@RequestBody String data) {
    return "consumed: " + data;
}
  1. Produces: Specifies the media types that the handler method can produce.
@GetMapping(value = "/produce", produces = "application/json")
public String produce() {
    return "{\"message\": \"produced\"}";
}

These properties allow for flexible and precise mapping of HTTP requests to handler methods in a Spring MVC application.

What Are Some Standard Java Pre-defined Functional Interfaces?

Java 8 introduced functional interfaces, which are interfaces with a single abstract method, used primarily for lambda expressions and method references. Some standard pre-defined functional interfaces include:

  1. Predicate: Represents a boolean-valued function of one argument.
Predicate<Integer> isEven = x -> x % 2 == 0;
System.out.println(isEven.test(4)); // Output: true
  1. Function: Represents a function that takes one argument and produces a result.
Function<String, Integer> lengthFunction = String::length;
System.out.println(lengthFunction.apply("hello")); // Output: 5
  1. Consumer: Represents an operation that takes one argument and returns no result.
Consumer<String> printConsumer = System.out::println;
printConsumer.accept("hello"); // Output: hello
  1. Supplier: Represents a supplier of results with no arguments.
Supplier<String> stringSupplier = () -> "hello";
System.out.println(stringSupplier.get()); // Output: hello
  1. BiFunction: Represents a function that takes two arguments and produces a result.
BiFunction<Integer, Integer, Integer> addFunction = Integer::sum;
System.out.println(addFunction.apply(3, 4)); // Output: 7
  1. UnaryOperator: Represents an operation on a single operand that produces a result of the same type.
UnaryOperator<Integer> square = x -> x * x;
System.out.println(square.apply(5)); // Output: 25
  1. BinaryOperator: Represents an operation on two operands of the same type that produces a result of the same type.
BinaryOperator<Integer> multiply = (a, b) -> a * b;
System.out.println(multiply.apply(3, 4)); // Output: 12

These functional interfaces provide a way to pass behavior as an argument to methods and are extensively used in the Java Streams API.

What is a Garbage Collector in Java?

The Garbage Collector (GC) in Java is responsible for automatic memory management. It reclaims memory by identifying and disposing of objects that are no longer in use, thus preventing memory leaks and optimizing the use of available memory.

Working Principle:

  1. Marking: The GC identifies which objects are reachable (i.e., still in use) by traversing object references starting from root objects like static fields and local variables.
  2. Sweeping: The GC reclaims memory occupied by unreachable objects, making it available for new objects.
  3. Compacting: The GC compacts memory by moving reachable objects together to reduce fragmentation and free up contiguous memory space.

Types of Garbage Collectors:

  1. Serial GC: A single-threaded collector suitable for small applications.
  2. Parallel GC: Uses multiple threads for parallel garbage collection.
  3. CMS (Concurrent Mark-Sweep) GC: Low-pause collector that works concurrently with application threads.
  4. G1 (Garbage-First) GC: Designed for large heap sizes and provides predictable pause times.

Example to demonstrate GC:

public class GarbageCollectorExample {
    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            String str = new String("Hello" + i);
        }
        System.gc(); // Suggests the JVM to perform garbage collection
    }
}

In this example, creating many String objects and then suggesting garbage collection demonstrates how the GC can reclaim memory.

What is Dependency Injection?

Dependency Injection (DI) is a design pattern used to achieve Inversion of Control (IoC) between classes and their dependencies. In DI, the control of creating and managing dependencies is transferred from the class itself to an external framework or container. This promotes loose coupling and enhances testability and maintainability.

Types of Dependency Injection:

  1. Constructor Injection: Dependencies are provided through the constructor.
  2. Setter Injection: Dependencies are provided through setter methods.
  3. Field Injection: Dependencies are injected directly into fields.

Example using Spring Framework:

// Service interface
public interface MyService {
    void performTask();
}

// Service implementation
public class MyServiceImpl implements MyService {
    @Override
    public void performTask() {
        System.out.println("Performing task");
    }
}

// Controller using DI
public class MyController {
    private final MyService myService;

    // Constructor injection
    public MyController(MyService myService) {
        this.myService = myService;
    }

    public void execute() {
        myService.performTask();
    }
}

// Spring configuration
@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }

    @Bean
    public MyController myController(MyService myService) {
        return new MyController(myService);
    }
}

// Main application
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        MyController controller = context.getBean(MyController.class);
        controller.execute();
    }
}

In this example, MyController is dependent on MyService. The Spring container manages these dependencies and injects MyService into MyController.

What Are the Features of a Lambda Expression?

Lambda expressions in Java 8 provide a clear and concise way to represent one method interface using an expression. They enable functional programming in Java and offer several features:

  1. Concise Syntax: Lambda expressions have a concise syntax compared to anonymous inner classes.
Runnable runnable = () -> System.out.println("Running");
  1. Functional Interface: Lambda expressions work with functional interfaces (interfaces with a single abstract method).
@FunctionalInterface
interface MyFunctionalInterface {
    void execute();
}

MyFunctionalInterface myFunc = () -> System.out.println("Executing");
  1. Type Inference: The compiler can infer the types of parameters, making the syntax even more concise.
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
  1. Method References: Lambda expressions can be replaced with method references for improved readability.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(System.out::println);
  1. Higher-Order Functions: Lambda expressions can be passed as arguments to higher-order functions.
public static void process(MyFunctionalInterface func) {
    func.execute();
}

process(() -> System.out.println("Processing"));
  1. Streams API: Lambda expressions are heavily used in the Java Streams API for operations like filtering, mapping, and reducing.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream().map(x -> x * x).collect(Collectors.toList());

These features make lambda expressions a powerful addition to the Java language, enabling more functional and expressive programming.

Can You Explain What is Lazy Loading in Hibernate?

Lazy loading in Hibernate is a strategy that delays the initialization of an object or a collection of objects until it is needed. This can improve performance and reduce memory consumption by avoiding the loading of unnecessary data from the database.

When an entity is marked for lazy loading, Hibernate will not fetch the data from the database until the application explicitly accesses the entity or collection. This is particularly useful in scenarios where not all relationships need to be loaded immediately.

Example:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
    private List<Order> orders;

    // Getters and setters
}

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String product;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    // Getters and setters
}

In this example, the orders collection in the User entity is marked for lazy loading. Hibernate will not load the orders collection from the database until the application accesses it.

public class HibernateExample {
    public static void main(String[] args) {
        SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
        Session session = sessionFactory.openSession();
        session.beginTransaction();

        User user = session.get(User.class, 1L);
        System.out.println("User name: " + user.getName());

        // Orders are not loaded until accessed
        List<Order> orders = user.getOrders();
        System.out.println("Number of orders: " + orders.size());

        session.getTransaction().commit();
        session.close();
    }
}

In this example, the orders collection is not loaded when the User entity is fetched. It is only loaded when the getOrders method is called.

How ConcurrentHashMap Works in Java?

ConcurrentHashMap in Java is a thread-safe implementation of the Map interface, designed for high concurrency. It allows concurrent read and write operations without locking the entire map, thus improving performance in multi-threaded environments.

Key features and working principles:

  1. Segmented Locking: Instead of locking the entire map, ConcurrentHashMap divides the map into segments (buckets). Each segment can be independently locked, allowing multiple threads to work on different segments simultaneously.
  2. Lock Stripping: By locking only the segment being accessed, other segments remain accessible to other threads, reducing contention.
  3. Non-blocking Reads: Reads are not blocked, even during write operations. This is achieved through the use of volatile variables and atomic operations.
  4. Atomic Operations: Uses CAS (Compare-And-Swap) operations for atomic updates, ensuring thread safety without locking.

Example:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // Adding elements
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);

        // Concurrent read and write operations
        Runnable readTask = () -> {
            for (String key : map.keySet()) {
                System.out.println(key + ": " + map.get(key));
            }
        };

        Runnable writeTask = () -> {
            map.put("four", 4);
        };

        Thread readThread = new Thread(readTask);
        Thread writeThread = new Thread(writeTask);

        readThread.start();
        writeThread.start();
    }
}

In this example, ConcurrentHashMap allows concurrent read and write operations without blocking, demonstrating its thread-safe nature.

How is the Routing Carried Out in MVC?

In the context of Spring MVC, routing refers to mapping HTTP requests to handler methods in controllers. This is achieved using annotations like @RequestMapping, @GetMapping, @PostMapping, etc.

Key components of routing in Spring MVC:

  1. Controller: Defines handler methods that process HTTP requests and return responses.
@Controller
@RequestMapping("/home")
public class HomeController {
    @GetMapping
    public String home() {
        return "home";
    }

    @PostMapping
    public String submit() {
        return "submit";
    }
}
  1. Handler Mapping: Determines which controller method should handle a specific request. Spring provides several handler mappings, such as RequestMappingHandlerMapping.
  2. Handler Adapter: Invokes the appropriate handler method. RequestMappingHandlerAdapter is the default adapter for annotated controllers.
  3. View Resolver: Resolves the logical view name returned by the controller into an actual view. Spring provides several view resolvers, such as InternalResourceViewResolver.
  4. DispatcherServlet: The central dispatcher that receives all incoming HTTP requests, delegates them to the appropriate controller, and returns the response.

Example flow:

  1. Request: An HTTP request is sent to the application.
    2. DispatcherServlet: The DispatcherServlet receives the request and looks for a matching handler mapping.
  2. Handler Mapping: Determines the appropriate controller and method based on the request URL and HTTP method.
  3. Handler Adapter: Invokes the controller method.
  4. Controller: Processes the request and returns a logical view name.
  5. View Resolver: Resolves the view name to an actual view.
  6. Response: The view is rendered and the response is sent back to the client.

In this example, the HomeController maps /home requests to the home method for GET requests and to the submit method for POST requests.

What is Java and What is Inheritance and What are OOP Concepts and What is Method?

Java is a high-level, class-based, object-oriented programming language designed to have as few implementation dependencies as possible. It is widely used for building platform-independent applications.

Object-Oriented Programming (OOP) Concepts:

  1. Encapsulation: Bundling the data (fields) and methods (functions) that operate on the data into a single unit or class.
  2. Inheritance: The mechanism by which one class (child) inherits the properties and behaviors (fields and methods) of another class (parent). This promotes code reuse and establishes a hierarchical relationship between classes.
class Animal {
    void eat() {
        System.out.println("Animal is eating");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Dog is barking");
    }
}

public class InheritanceExample {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat(); // Inherited method
        dog.bark();
    }
}
  1. Polymorphism: The ability of a single interface to represent different underlying forms (data types). This can be achieved through method overriding (runtime polymorphism) and method overloading (compile-time polymorphism).
class Animal {
    void makeSound() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}

public class PolymorphismExample {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.makeSound(); // Output: Bark
    }
}
  1. Abstraction: The concept of hiding the complex implementation details and showing only the essential features of an object.

Method: A method is a block of code that performs a specific task. It can take parameters, return a value, and be invoked on objects or classes.

public class MethodExample {
    public static void main(String[] args) {
        greet("Alice");
    }

    public static void greet(String name) {
        System.out.println("Hello, " + name);
    }
}

In this example, the greet method takes a String parameter and prints a greeting message.

What is Hibernate Caching?

Hibernate caching is a mechanism to improve the performance of an application by reducing the number of database queries. It achieves this by storing frequently accessed data in memory, allowing for faster retrieval.

Hibernate supports two levels of caching:

  1. First-Level Cache (Session Cache):
  • Scope: Exists per Hibernate session.
  • Default: Enabled by default and cannot be disabled.
  • Usage: Stores objects within a session to reduce database access for repeated queries on the same session.
Session session = sessionFactory.openSession();
Person person1 = session.get(Person.class, 1L); // First query, hits the database
Person person2 = session.get(Person.class, 1L); // Second query, hits the session cache
session.close();
  1. Second-Level Cache:
  • Scope: Exists per SessionFactory.
  • Optional: Must be explicitly enabled and configured.
  • Usage: Stores objects across multiple sessions, reducing database access for repeated queries on the same data across different sessions.

Example configuration using EHCache as the second-level cache:

hibernate.cfg.xml:

<hibernate-configuration>
    <session-factory>
        <!-- Enable second-level cache -->
        <property name="hibernate.cache.use_second_level_cache">true</property>
        <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
        <property name="net.sf.ehcache.configurationResourceName">/ehcache.xml</property>

        <!-- Enable query cache -->
        <property name="hibernate.cache.use_query_cache">true</property>

        <!-- Mapping classes -->
        <mapping class="com.example.Person"/>
    </session-factory>
</hibernate-configuration>

ehcache.xml:

<ehcache>
    <cache name="com.example.Person" maxEntriesLocalHeap="1000" timeToLiveSeconds="3600" />
</ehcache>

Person.java:

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    // Getters and setters
}

In this example, Hibernate caches Person objects in the second-level cache, improving performance by reducing the number of database queries.

What is Polymorphism in Both Overloading and Overriding Way?

Polymorphism in Java allows objects to be treated as instances of their parent class rather than their actual class. It enables a single interface to represent different underlying forms (data types). Polymorphism can be achieved through method overloading and method overriding.

  1. Method Overloading (Compile-Time Polymorphism):
  • Definition: Multiple methods with the same name but different parameter lists within the same class.
  • Resolution: Determined at compile time based on the method signature.
public class OverloadingExample {
    public void display(int a) {
        System.out.println("Integer: " + a);
    }

    public void display(String a) {
        System.out.println("String: " + a);
    }

    public static void main(String[] args) {
        OverloadingExample obj = new OverloadingExample();
        obj.display(10); // Output: Integer: 10
        obj.display("Hello"); // Output: String: Hello
    }
}
  1. Method Overriding (Runtime Polymorphism):
  • Definition: A subclass provides a specific implementation for a method already defined in its parent class.
  • Resolution: Determined at runtime based on the actual object type.
class Animal {
    void makeSound() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}

public class OverridingExample {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.makeSound(); // Output: Bark
    }
}

In these examples, method overloading demonstrates compile-time polymorphism, while method overriding demonstrates runtime polymorphism.

What Do You Know About the Secure Socket Layer (SSL)?

Secure Socket Layer (SSL) is a standard security protocol for establishing encrypted links between a web server and a browser in an online communication. SSL ensures that all data transmitted between the web server and the browser remains encrypted and secure.

Key features of SSL:

  1. Encryption: Encrypts data transmitted between the client and server, preventing eavesdropping.
  2. Authentication: Ensures that the server (and optionally the client) is authenticated.
  3. Data Integrity: Ensures that data is not tampered with during transmission.

SSL is succeeded by Transport Layer Security (TLS), which provides improved security features.

How SSL works:

  1. Handshake: The client and server initiate a connection and agree on the encryption method to be used.
  2. Certificate Exchange: The server sends its SSL certificate to the client to verify its identity.
  3. Session Keys: Both parties generate session keys for encrypting data.
  4. Encrypted Communication: Data is transmitted securely using the session keys.

Example of using SSL with Java:

import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;

public class SSLExample {
    public static void main(String[] args) {
        try {
            URL url = new URL("https://www.example.com");
            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();

            BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            reader.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

In this example, an HTTPS connection is established using SSL to securely fetch data from a URL.

When is merge() Method of the Hibernate Session Useful?

The merge() method of the Hibernate Session is useful when you need to synchronize the state of a detached entity with the current persistence context. It is particularly useful in scenarios where an entity is modified outside the current session, and you need to reattach it to the session to persist the changes.

Key points about merge():

  1. Detached Entity: The entity is not associated with any Hibernate session.
  2. Synchronization: Merges the state of the detached entity with the current session.
  3. Returns a New Instance: merge() returns a new instance of the entity that is managed by the session, whereas the original entity remains detached.

Example:

public class MergeExample {
    public static void main(String[] args) {
        SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();

        // Detached entity
        Person person = new Person();
        person.setId(1L);
        person.setName("John");

        // First session
        Session session1 = sessionFactory.openSession();
        session1.beginTransaction();
        session1.save(person);
        session1.getTransaction().commit();
        session1.close();

        // Modify detached entity
        person.setName("John Doe");

        // Second session
        Session session2 = sessionFactory.openSession();
        session2.beginTransaction();
        Person mergedPerson = (Person) session2.merge(person);
        session2.getTransaction().commit();
        session2.close();

        System.out.println("Original: " + person.getName());
        System.out.println("Merged: " + mergedPerson.getName());
    }
}

In this example, the merge() method is used to reattach the detached Person entity to the second session, synchronizing its state with the current persistence context.

What Are the Basic Annotations that Spring Boot Offers?

Spring Boot provides several annotations to simplify application development. Some of the key annotations include:

  1. @SpringBootApplication: Combines @Configuration, @EnableAutoConfiguration, and @ComponentScan to set up a Spring Boot application.
@SpringBootApplication
public class MySpringBootApplication {
    public static void main(String[] args) {
        SpringApplication.run(MySpringBootApplication.class, args);
    }
}
  1. @RestController: A combination of @Controller and @ResponseBody, used to create RESTful web services.
@RestController
@RequestMapping("/api")
public class MyRestController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello, World!";
    }
}
  1. @RequestMapping: Used to map HTTP requests to handler methods in controllers.
@RequestMapping("/home")
public class HomeController {
    @GetMapping
    public String home() {
        return "home";
    }
}
  1. @Autowired: Used for automatic dependency injection.
@Service
public class MyService {
    private final MyRepository myRepository;

    @Autowired
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }
}
  1. @Entity: Marks a class as a JPA entity.
@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    // Getters and setters
}
  1. @SpringBootTest: Used for writing integration tests.
@SpringBootTest
public class MySpringBootTest {
    @Test
    public void contextLoads() {
    }
}
  1. @EnableAutoConfiguration: Enables auto-configuration of the Spring ApplicationContext.
@EnableAutoConfiguration
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

These annotations simplify configuration and development, allowing developers to focus on writing business logic.

Difference Between Abstract Class and Interface

Abstract classes and interfaces in Java are used to achieve abstraction, but they have different use cases and features:

  • Abstract Class:
  • Implementation: Can have both abstract methods (without implementation) and concrete methods (with implementation).
  • Fields: Can have instance variables.
  • Constructors: Can have constructors.
  • Inheritance: Supports single inheritance.
  • Use Case: Used when classes share a common base and some methods with implementation.
abstract class Animal {
    abstract void makeSound();

    public void sleep() {
        System.out.println("Sleeping");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}
  • Interface:
  • Implementation: Can have only abstract methods (before Java 8). From Java 8 onwards, can have default and static methods.
  • Fields: Only constants (static final).
  • Constructors: Cannot have constructors.
  • Inheritance: Supports multiple inheritance.
  • Use Case: Used to define a contract that classes must follow.
interface Animal {
    void makeSound();
}

class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Bark");
    }
}

In these examples, Animal is defined as both an abstract class and an interface, with Dog providing concrete implementations.

How MVC Works in Spring?

Spring MVC (Model-View-Controller) is a framework for building web applications. It follows the MVC pattern, where the application is divided into three interconnected components: Model, View, and Controller.

  1. Controller: Handles HTTP requests, processes user input, and interacts with the model. It returns a logical view name.
@Controller
@RequestMapping("/home")
public class HomeController {
    @GetMapping
    public String home() {
        return "home";
    }
}
  1. Model: Represents the data and business logic of the application. It is usually composed of POJOs (Plain Old Java Objects).
public class User {
    private String name;
    private int age;
    // Getters and setters
}
  1. View: Renders the data from the model into a format suitable for user interaction, typically HTML. Spring MVC integrates with various view technologies, such as JSP, Thymeleaf, and FreeMarker.
<!-- home.jsp -->
<html>
<body>
    <h1>Welcome to the Home Page</h1>
</body>
</html>

Request Flow in Spring MVC:

  1. Request: The user sends an HTTP request to the application.
  2. DispatcherServlet: The central dispatcher that receives the request and delegates it to the appropriate controller.
  3. Handler Mapping: Determines the appropriate controller method based on the request URL and HTTP method.
  4. Controller: The controller method processes the request, interacts with the model, and returns a logical view name.
  5. View Resolver: Resolves the view name to an actual view (e.g., JSP, Thymeleaf).
  6. View: The view renders the model data into a response format (e.g., HTML) and sends it back to the user.

In this example, a request to /home is handled by the HomeController, which returns the view name home. The view resolver maps this to home.jsp, which is rendered and sent back to the user.

Explain in Brief the Role of Different MVC Components

In the MVC (Model-View-Controller) architecture, different components have distinct roles:

  1. Model:
  • Role: Represents the data and business logic of the application.
  • Responsibilities: Encapsulates the application’s data and defines methods to access and manipulate this data.
  • Example: POJOs (Plain Old Java Objects), service classes, and data access objects (DAOs).
public class User {
    private String name;
    private int age;
    // Getters and setters
}
  1. View:
  • Role: Represents the presentation layer of the application.
  • Responsibilities: Renders the model data into a format suitable for user interaction, typically HTML.
  • Example: JSP, Thymeleaf, FreeMarker, and other templating engines.
<!-- home.jsp -->
<html>
<body>
    <h1>Welcome to the Home Page</h1>
</body>
</html>
  1. Controller:
  • Role: Acts as an intermediary between the Model and View.
  • Responsibilities: Handles HTTP requests, processes user input, interacts with the model, and returns a logical view name.
  • Example: Spring MVC controllers, annotated with @Controller.
@Controller
@RequestMapping("/home")
public class HomeController {
    @GetMapping
    public String home() {
        return "home";
    }
}

Interaction Flow in MVC:

  1. Request: The user sends an HTTP request to the application.
  2. Controller: The controller method processes the request, interacts with the model to retrieve or modify data, and returns a logical view name.
  3. Model: The model contains the data and business logic that the controller interacts with.
  4. View: The view renders the model data into a response format (e.g., HTML) and sends it back to the user.

In this example, a request to /home is handled by the HomeController, which interacts with the model and returns the view name home. The view resolver maps this to home.jsp, which is rendered and sent back to the user.

How Does an Exception Propagate in the Code?

Exception propagation in Java refers to the process by which an exception is passed from the point where it occurs to the point where it is handled. This propagation continues up the method call stack until the exception is caught and handled.

  1. Method Throws an Exception: When an exception occurs in a method, it is thrown to the caller method.
  2. Caller Method: If the caller method does not handle the exception, it is propagated further up the call stack to the next caller method.
  3. Catch Block: The propagation continues until a method in the call stack has a corresponding catch block to handle the exception.
  4. Uncaught Exception: If the exception is not caught by any method in the call stack, it is propagated to the JVM, which terminates the program and prints the stack trace.

Example:

public class ExceptionPropagationExample {
    public static void main(String[] args) {
        try {
            method1();
        } catch (ArithmeticException e) {
            System.out.println("Exception caught in main: " + e.getMessage());
        }
    }

    public static void method1() {
        method2();
    }

    public static void method2() {
        method3();
    }

    public static void method3() {
        int result = 10 / 0; // ArithmeticException occurs here
    }
}

In this example:

  • The ArithmeticException occurs in method3.
  • Since method3 does not handle the exception, it is propagated to method2.
  • method2 does not handle the exception either, so it is propagated to method1.
  • method1 also does not handle the exception, so it is propagated to the main method.
  • The main method has a try-catch block that catches and handles the ArithmeticException.

The output will be:

Exception caught in main: / by zero

How Do You Create an Immutable Class in Hibernate?

To create an immutable class in Hibernate, you need to ensure that the class itself and its fields cannot be modified after creation. This involves making the class final, setting fields as private and final, and providing only getter methods. Additionally, you can use Hibernate annotations to mark the entity as immutable.

Steps to create an immutable class:

  1. Make the class final: Prevents subclassing.
  2. Make fields private and final: Ensures fields cannot be modified after initialization.
  3. Provide only getters: No setters should be provided.
  4. Hibernate annotation: Use @Immutable to indicate that the entity is immutable.

Example:

import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.annotations.Immutable;

@Entity
@Immutable
public final class Person {
    @Id
    private final Long id;
    private final String name;
    private final int age;

    public Person(Long id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

In this example, the Person class is immutable because it:

  • Is declared as final.
  • Has private and final fields.
  • Provides only getter methods.
  • Is marked with the @Immutable annotation to indicate to Hibernate that the entity is immutable.

When Hibernate interacts with this entity, it will treat it as read-only and will not allow any modifications to the entity.

Is Java an Object-Oriented Language or Not?

Java is an object-oriented programming (OOP) language, but it also has features of procedural and functional programming. Java is considered a fully object-oriented language because it supports all the core concepts of OOP:

  1. Encapsulation: Bundling data (fields) and methods (functions) that operate on the data into a single unit or class.
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
  1. Inheritance: The mechanism by which one class (child) inherits the properties and behaviors (fields and methods) of another class (parent).
class Animal {
    void eat() {
        System.out.println("Animal is eating");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Dog is barking");
    }
}
  1. Polymorphism: The ability of a single interface to represent different underlying forms (data types). This can be achieved through method overriding (runtime polymorphism) and method overloading (compile-time polymorphism).
class Animal {
    void makeSound() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}
  1. Abstraction: The concept of hiding the complex implementation details and showing only the essential features of an object.
abstract class Animal {
    abstract void makeSound();

    public void sleep() {
        System.out.println("Sleeping");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}

What is Actuator in Spring Boot?

Spring Boot Actuator provides production-ready features to help you monitor and manage your application. It exposes various endpoints to inspect the internal state of the application, such as health, metrics, environment, and more.

Key features of Spring Boot Actuator:

  1. Health Checks: Provides a /health endpoint to check the health of the application and its components.
  2. Metrics: Exposes metrics related to the application, such as memory usage, request counts, and response times.
  3. Environment: Allows you to view the environment properties and configurations.
  4. Logging: Provides endpoints to view and modify logging levels at runtime.
  5. Info: Displays application information, such as version, description, and custom details.

Example:

  1. Add Dependency: Include the Actuator dependency in your pom.xml or build.gradle.

pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

build.gradle:

implementation 'org.springframework.boot:spring-boot-starter-actuator'
  1. Enable Endpoints: Configure the Actuator endpoints in application.properties or application.yml.

application.properties:

management.endpoints.web.exposure.include=health,info

application.yml:

management:
  endpoints:
    web:
      exposure:
        include: health, info
  1. Access Endpoints: Start the application and access the Actuator endpoints, such as /actuator/health and /actuator/info.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ActuatorExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ActuatorExampleApplication.class, args);
    }
}

In this example, the Actuator endpoints are enabled and can be accessed to monitor and manage the application.

What is Dependency Injection?

Dependency Injection (DI) is a design pattern used to achieve Inversion of Control (IoC) between classes and their dependencies. In DI, the control of creating and managing dependencies is transferred from the class itself to an external framework or container. This promotes loose coupling and enhances testability and maintainability.

Types of Dependency Injection:

  1. Constructor Injection: Dependencies are provided through the constructor.
  2. Setter Injection: Dependencies are provided through setter methods.
  3. Field Injection: Dependencies are injected directly into fields.

Example using Spring Framework:

// Service interface
public interface MyService {
    void performTask();
}

// Service implementation
public class MyServiceImpl implements MyService {
    @Override
    public void performTask() {
        System.out.println("Performing task");
    }
}

// Controller using DI
public class MyController {
    private final MyService myService;

    // Constructor injection
    public MyController(MyService myService) {
        this.myService = myService;
    }

    public void execute() {
        myService.performTask();
    }
}

// Spring configuration
@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }

    @Bean
    public MyController myController(MyService myService) {
        return new MyController(myService);
    }
}

// Main application
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        MyController controller = context.getBean(MyController.class);
        controller.execute();
    }
}

In this example, MyController is dependent on MyService. The Spring container manages these dependencies and injects MyService into MyController.

How to Change Embedded Server in Spring Boot?

Spring Boot allows you to change the embedded server easily through configuration. By default, Spring Boot uses Tomcat as the embedded server, but you can switch to other servers like Jetty or Undertow.

Steps to change the embedded server:

  1. Exclude the Default Server: Exclude the default Tomcat server dependency.
  2. Include the Desired Server: Add the dependency for the desired server (e.g., Jetty, Undertow).

Example using Maven:

Exclude Tomcat and Include Jetty:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId

>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

Exclude Tomcat and Include Undertow:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

Example using Gradle:

Exclude Tomcat and Include Jetty:

implementation('org.springframework.boot:spring-boot-starter-web') {
    exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
implementation 'org.springframework.boot:spring-boot-starter-jetty'

Exclude Tomcat and Include Undertow:

implementation('org.springframework.boot:spring-boot-starter-web') {
    exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
implementation 'org.springframework.boot:spring-boot-starter-undertow'

In these examples, the default Tomcat server is excluded, and Jetty or Undertow is included as the embedded server.

Write a Program for Permutation of a String

To generate all permutations of a string, you can use a recursive approach. Here is a Java program that demonstrates this:

public class PermutationExample {
    public static void main(String[] args) {
        String str = "ABC";
        permute(str, 0, str.length() - 1);
    }

    // Method to generate permutations
    public static void permute(String str, int l, int r) {
        if (l == r) {
            System.out.println(str);
        } else {
            for (int i = l; i <= r; i++) {
                str = swap(str, l, i);
                permute(str, l + 1, r);
                str = swap(str, l, i); // backtrack
            }
        }
    }

    // Method to swap characters in a string
    public static String swap(String str, int i, int j) {
        char[] charArray = str.toCharArray();
        char temp = charArray[i];
        charArray[i] = charArray[j];
        charArray[j] = temp;
        return String.valueOf(charArray);
    }
}

In this program:

  • The permute method generates permutations by recursively swapping characters.
  • The swap method swaps characters at positions i and j.

When you run this program with the input “ABC”, it prints all permutations of the string “ABC”:

ABC
ACB
BAC
BCA
CBA
CAB

What is Self-Join and Cross-Join?

In SQL, self-join and cross-join are types of joins used to combine rows from two or more tables based on related columns.

  1. Self-Join:
  • Definition: A self-join is a join of a table with itself. It is used when you need to compare rows within the same table.
  • Use Case: Useful for hierarchical data or comparing rows in the same table.

Example:

SELECT A.employee_id, A.name AS employee_name, B.name AS manager_name
FROM employees A
JOIN employees B ON A.manager_id = B.employee_id;

In this example, the employees table is joined with itself to find the manager of each employee.

  1. Cross-Join:
  • Definition: A cross-join (or Cartesian join) returns the Cartesian product of two tables, which means it combines each row of the first table with each row of the second table.
  • Use Case: Useful when you need all possible combinations of rows from two tables.

Example:

SELECT A.product_name, B.category_name
FROM products A
CROSS JOIN categories B;

In this example, each product is combined with each category, resulting in a Cartesian product of the products and categories tables.

Difference Between Runnable Interface and Callable Interface

In Java, Runnable and Callable are two interfaces used to represent tasks that can be executed by a thread or an executor. They have different features and use cases:

  1. Runnable:
  • Method: Contains a single method run().
  • Return Type: Does not return a result (void).
  • Exception Handling: Cannot throw checked exceptions.
public class RunnableExample {
    public static void main(String[] args) {
        Runnable task = () -> System.out.println("Task executed");
        Thread thread = new Thread(task);
        thread.start();
    }
}
  1. Callable:
  • Method: Contains a single method call().
  • Return Type: Returns a result of type V.
  • Exception Handling: Can throw checked exceptions.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Callable<Integer> task = () -> {
            Thread.sleep(1000);
            return 123;
        };
        Future<Integer> future = executor.submit(task);
        try {
            Integer result = future.get();
            System.out.println("Task result: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
    }
}

In these examples, Runnable is used to execute a task without a result, while Callable is used to execute a task that returns a result.

What is Thread Starvation?

Thread starvation occurs when a thread is perpetually denied access to resources it needs for execution. This can happen when higher-priority threads monopolize the CPU, preventing lower-priority threads from running. As a result, the lower-priority threads are starved of CPU time.

Common causes of thread starvation:

  1. Priority Inversion: Higher-priority threads continuously preempt lower-priority threads, leading to starvation.
  2. Synchronization: Excessive use of synchronized blocks can cause threads to wait indefinitely for locks.
  3. Resource Contention: Threads competing for limited resources may starve if they cannot acquire the necessary resources.

Example demonstrating thread starvation:

public class ThreadStarvationExample {
    public static void main(String[] args) {
        Runnable task = () -> {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + " is running");
            while (true) {
                // Simulating long-running task
            }
        };

        Thread highPriorityThread = new Thread(task);
        highPriorityThread.setPriority(Thread.MAX_PRIORITY);
        highPriorityThread.start();

        Thread lowPriorityThread = new Thread(task);
        lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
        lowPriorityThread.start();
    }
}

In this example, the low-priority thread may be starved of CPU time because the high-priority thread is continuously running, preventing the low-priority thread from executing.

Explain Briefly About Session Interface Used in Hibernate

The Session interface in Hibernate represents a single-threaded, short-lived object used to interact with the database. It is the primary interface for performing CRUD (Create, Read, Update, Delete) operations, querying the database, and managing the lifecycle of persistent objects.

Key features of the Session interface:

  1. CRUD Operations: Provides methods to save, update, delete, and retrieve entities.
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();

Person person = new Person();
person.setName("John");
session.save(person);

transaction.commit();
session.close();
  1. Querying: Allows querying the database using HQL (Hibernate Query Language) or SQL.
List<Person> people = session.createQuery("FROM Person", Person.class).list();
for (Person p : people) {
    System.out.println(p.getName());
}
  1. Transaction Management: Supports transaction management to ensure data consistency.
Transaction transaction = session.beginTransaction();
try {
    // Perform database operations
    transaction.commit();
} catch (Exception e) {
    transaction.rollback();
}
  1. Caching: Utilizes the first-level cache to minimize database access.
Person person1 = session.get(Person.class, 1L); // Hits the database
Person person2 = session.get(Person.class, 1L); // Hits the session cache
  1. Lifecycle Management: Manages the lifecycle of persistent objects, including transient, persistent, and detached states.
Person person = new Person();
session.save(person); // Now the person is in persistent state
session.evict(person); // Now the person is in detached state

The Session interface is central to Hibernate’s ORM capabilities, providing a rich set of methods for interacting with the database and managing entity lifecycle.

What is Java and What is Method and What are OOP Concepts?

Java

is a high-level, class-based, object-oriented programming language designed to have as few implementation dependencies as possible. It is widely used for building platform-independent applications.

Object-Oriented Programming (OOP) Concepts:

  1. Encapsulation: Bundling data (fields) and methods (functions) that operate on the data into a single unit or class.
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
  1. Inheritance: The mechanism by which one class (child) inherits the properties and behaviors (fields and methods) of another class (parent).
class Animal {
    void eat() {
        System.out.println("Animal is eating");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Dog is barking");
    }
}
  1. Polymorphism: The ability of a single interface to represent different underlying forms (data types). This can be achieved through method overriding (runtime polymorphism) and method overloading (compile-time polymorphism).
class Animal {
    void makeSound() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}
  1. Abstraction: The concept of hiding the complex implementation details and showing only the essential features of an object.
abstract class Animal {
    abstract void makeSound();

    public void sleep() {
        System.out.println("Sleeping");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}

Method: A method is a block of code that performs a specific task. It can take parameters, return a value, and be invoked on objects or classes.

public class MethodExample {
    public static void main(String[] args) {
        greet("Alice");
    }

    public static void greet(String name) {
        System.out.println("Hello, " + name);
    }
}

In this example, the greet method takes a String parameter and prints a greeting message.

Difference Between Save and SaveOrUpdate

In Hibernate, save and saveOrUpdate are methods used to persist entities, but they have different behaviors:

  1. save:
  • Usage: Used to insert a new entity into the database.
  • Behavior: Generates an INSERT SQL statement.
  • Returns: The identifier (primary key) of the saved entity.
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();

Person person = new Person();
person.setName("John");
session.save(person);

transaction.commit();
session.close();
  1. saveOrUpdate:
  • Usage: Used to insert a new entity or update an existing entity.
  • Behavior: Generates either an INSERT or UPDATE SQL statement, depending on whether the entity already exists in the database.
  • Identifier: If the entity has an identifier (primary key) set, it will update the existing entity; otherwise, it will insert a new entity.
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();

Person person = new Person();
person.setId(1L); // Assuming this ID exists in the database
person.setName("John Doe");
session.saveOrUpdate(person);

transaction.commit();
session.close();

In this example:

  • The save method is used to insert a new Person entity into the database.
  • The saveOrUpdate method is used to either insert a new Person entity or update an existing one, depending on whether the id is set and exists in the database.

These detailed explanations and code examples should help in understanding the concepts comprehensively.

Empower Your Learning with Our eLearning Platform
Company
Subscribe to our newsletter

Copyright: © 2023 Outgrid WordPress theme by UiCore. All Rights Reserved.