Java Interview Experience for 3–7 experience Guys(Contains two Tech Round Interview Questions).

Ajay Rathod
18 min readOct 25, 2024

Hello folks, Welcome to another interview article. Here we will discsuss recent Java Experience of a Java Developer having 3–7 years of experience. He has two interview rounds which are purely java technical one and expectation was to clear all two to proceed further.

Are you preparing for a job interview as a Java developer?

Find my book Guide To Clear Java Developer Interview here Gumroad(PDFFormat) and Amazon (Kindle eBook).

Guide To Clear Spring-Boot Microservice Interview here Gumroad (PDFFormat) and Amazon (Kindle eBook).

Guide To Clear Front-End Developer Interview here Gumroad (PDF Format) and Amazon (Kindle eBook).

Download the sample copy here:

Guide To Clear Java Developer Interview[Free Sample Copy]

Guide To Clear Spring-Boot Microservice Interview[Free Sample Copy]

Guide To Clear Front-End Developer Interview [Sample_Copy]

Ready for personalized guidance? Book your 1-on-1 session with me here: https://topmate.io/ajay_rathod11

Technical Round 1:

How to create a custom immutable class in java using modern Java features like List.copyOf and record?

Creating a custom immutable class using modern Java features such as List.copyOf and record is straightforward and concise. Records are a new feature in Java (introduced in Java 14 as a preview and finalized in Java 16) designed to simplify the creation of immutable data classes.

Using record with List.copyOf

Let’s create a custom immutable Employee class using record and ensure that any list attributes are immutable by using List.copyOf.

Example:

Step 1: Define the record

Define a record with fields name and roles. In the canonical constructor, use List.copyOf to ensure the roles list is immutable.

import java.util.List;

public record Employee(String name, List<String> roles) {
public Employee {
// Validate and create an unmodifiable copy of the provided list
roles = List.copyOf(roles);
}
}

Step 2: Use the record

Create and use instances of the Employee record.

import java.util.List;

public class Main {
public static void main(String[] args) {
List<String> roles = List.of("Developer", "Team Lead");
Employee emp = new Employee("Alice", roles);

System.out.println(emp); // Employee[name=Alice, roles=[Developer, Team Lead]]

// Trying to modify the roles list after creating the Employee object
// This will throw UnsupportedOperationException because the list is unmodifiable
try {
emp.roles().add("Manager");
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify roles list: " + e);
}

// Trying to modify the roles list via getter
try {
emp.roles().set(0, "Manager");
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify roles list: " + e);
}

System.out.println(emp); // The Employee object remains unchanged
}
}

Summary of Features Used:

1. record: Simplifies the creation of immutable data classes by providing a concise syntax. Records are inherently final, and their fields are private and final.

2. List.copyOf: Ensures that the list provided to the Employee record is immutable, preventing external modification.

Additional Points:

• Validation: You can add validation logic in the canonical constructor of the record.

  • Custom Methods: You can define custom methods in the record if needed.

Example with Validation:

import java.util.List;

public record Employee(String name, List<String> roles) {
public Employee {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be null or blank");
}
if (roles == null || roles.isEmpty()) {
throw new IllegalArgumentException("Roles cannot be null or empty");
}
// Create an unmodifiable copy of the provided list
roles = List.copyOf(roles);
}

// You can add custom methods if needed
public String getFirstRole() {
return roles.isEmpty() ? "No roles" : roles.get(0);
}
}

What are these terminologies Serialization, deserialization, and externalization in java?

Serialization is the process of converting an object’s state into a byte stream, so it can be easily saved to a file, sent over a network, or stored in a database. This byte stream can then be deserialized back into a copy of the object.

Deserialization

Deserialization is the reverse process of serialization. It involves converting a byte stream back into a copy of the original object. This allows the object to be reconstructed from its serialized form.

Externalization

Externalization is an alternative to serialization in Java. It allows developers to have more control over the serialization process by implementing the Externalizable interface. This interface requires the implementation of two methods: writeExternal and readExternal, which handle the custom serialization and deserialization logic.

import java.io.*;

class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;

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

@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}

public class SerializationExample {
public static void main(String[] args) {
Person person = new Person("John Doe", 30);

// Serialization
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
oos.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}

// Deserialization
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
Person deserializedPerson = (Person) ois.readObject();
System.out.println(deserializedPerson);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}

Externalization

import java.io.*;

class Person implements Externalizable {
private String name;
private int age;

public Person() {
// No-arg constructor for deserialization
}

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

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}

@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}

public class ExternalizationExample {
public static void main(String[] args) {
Person person = new Person("Jane Doe", 25);

// Serialization
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ext"))) {
person.writeExternal(oos);
} catch (IOException e) {
e.printStackTrace();
}

// Deserialization
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ext"))) {
Person deserializedPerson = new Person();
deserializedPerson.readExternal(ois);
System.out.println(deserializedPerson);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
  • Serialization: Automatic process using Serializable interface.
  • Deserialization: Automatic process to reconstruct the object.
  • Externalization: Manual process using Externalizable interface for custom serialization logic

What is the Purpose of serialVersionUID?

The serialVersionUID is a unique identifier for each class that implements the Serializable interface in Java. It is used during the deserialization process to ensure that a loaded class corresponds exactly to a serialized object. If the serialVersionUID of the class does not match the serialVersionUID of the serialized object, an InvalidClassException is thrown.

Purpose of serialVersionUID

  1. Version Control: It helps in version control of serialized objects. If you modify a class (e.g., add a new field), you can update the serialVersionUID to indicate that the class has changed.
  2. Compatibility: It ensures that the serialized and deserialized objects are compatible. If the class definition changes but the serialVersionUID remains the same, the old serialized objects can still be deserialized into the new class definition.
  3. Avoiding InvalidClassException: By explicitly defining serialVersionUID, you can avoid InvalidClassException during deserialization when there are minor changes in the class.
import java.io.Serializable;

public class Person implements Serializable {
private static final long serialVersionUID = 1L; // Explicitly defined serialVersionUID

private String name;
private int age;

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

// Getters and setters
}

Default serialVersionUID

If you do not explicitly define a serialVersionUID, the Java compiler will generate one automatically based on various aspects of the class. However, this automatically generated serialVersionUID can change if the class structure changes, potentially causing InvalidClassException during deserialization.

Best Practices

  • Always explicitly define a serialVersionUID in your serializable classes.
  • Update the serialVersionUID when you make incompatible changes to the class structure.

What is the difference between Comparable and Comparator?

In Java, both Comparable and Comparator interfaces are used to define the natural ordering of objects, but they serve different purposes and are used in different scenarios.

Comparable

Key Points:

• Method to Implement: The Comparable interface requires the implementation of the compareTo(T o) method.

• Natural Ordering: By implementing Comparable, you define the “natural” ordering of objects for a class.

• Consistent with equals: It’s generally recommended, but not required, that compareTo be consistent with equals. That is, (a.compareTo(b) == 0) should imply a.equals(b).

public 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 Integer.compare(this.age, other.age); // Natural ordering by age
}

// Getters and toString() method
}

Comparator

• Method to Implement: The Comparator interface requires the implementation of the compare(T o1, T o2) method.

• Custom Ordering: Allows you to define custom orderings separate from the natural ordering.

• Lambda Expressions: Since Java 8, Comparator can also be used with lambda expressions for concise and flexible comparisons.

import java.util.Comparator;

public class PersonNameComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName()); // Custom ordering by name
}
}

Usage Example

import java.util.*;

public class Main {
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));

// Sorting using Comparable (natural ordering by age)
Collections.sort(people);
System.out.println("Sorted by age: " + people);

// Sorting using Comparator (custom ordering by name)
Collections.sort(people, new PersonNameComparator());
System.out.println("Sorted by name: " + people);
}
}

What are Generics and wildcard in generics?

Generics in Java allow you to define classes, interfaces, and methods with type parameters. This provides type safety by enabling compile-time type checking and eliminating the need for type casting. Generics are commonly used with collections, but they can be applied to any class or method.

public class Box<T> {
private T content;

public void setContent(T content) {
this.content = content;
}

public T getContent() {
return content;
}

public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
System.out.println(stringBox.getContent());

Box<Integer> integerBox = new Box<>();
integerBox.setContent(123);
System.out.println(integerBox.getContent());
}
}

Wildcards in Generics

Wildcards are used in generics to represent an unknown type. They are useful when you want to work with classes, methods, or interfaces that operate on a generic type, but you don’t know or don’t care about the exact type.

Types of Wildcards

  1. Unbounded Wildcard (?):
  • Represents an unknown type.
  • Useful when you want to work with any type.
public void printBox(Box<?> box) {
System.out.println(box.getContent());
}

2. Bounded Wildcard (? extends Type):

  • Represents an unknown type that is a subtype of a specified type.
  • Useful for reading data from a generic object.
public void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}

Lower Bounded Wildcard (? super Type):

  • Represents an unknown type that is a supertype of a specified type.
  • Useful for writing data to a generic object.
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
import java.util.*;

public class WildcardExample {
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);

printNumbers(intList);
printNumbers(doubleList);

List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList);
}

public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}

public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
}

Summary

  • Generics: Provide type safety and eliminate the need for type casting.
  • Wildcards: Represent unknown types and are useful for flexibility in generic programming.
  • Unbounded Wildcard (?): Any type.
  • Bounded Wildcard (? extends Type): Subtypes of a specified type.
  • Lower Bounded Wildcard (? super Type): Supertypes of a specified type.

Generics and wildcards together provide powerful tools for creating flexible and type-safe code in Java.

Write a program to Count each character occurrence in a String?

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

public class CharacterCount {
public static void main(String[] args) {
String input = "hello world";
Map<Character, Integer> charCountMap = new HashMap<>();

// Iterate through each character in the string
for (char c : input.toCharArray()) {
// Update the count for each character
charCountMap.put(c, charCountMap.getOrDefault(c, 0) + 1);
}

// Print the characters and their counts
for (Map.Entry<Character, Integer> entry : charCountMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}

Explanation

  1. Create a HashMap: Map<Character, Integer> charCountMap = new HashMap<>();
  2. Iterate through each character: for (char c : input.toCharArray())
  3. Update the count: charCountMap.put(c, charCountMap.getOrDefault(c, 0) + 1);
  4. Print the results: Iterate through the HashMap entries and print each character and its count.

Whats the Difference between Factory and Abstract Factory design pattern?

The Factory and Abstract Factory design patterns are both creational patterns used to create objects, but they serve different purposes and are used in different scenarios.

Factory Pattern

The Factory Pattern provides a way to create objects without specifying the exact class of the object that will be created. It defines an interface for creating an object, but lets subclasses alter the type of objects that will be created.

Key Points

  • Purpose: To create objects without specifying the exact class.
  • Implementation: Uses a single method to create objects.
  • Flexibility: Allows for the creation of objects from a single family.
  • Example Use Case: Creating different types of shapes (e.g., Circle, Square).
// Product Interface
interface Shape {
void draw();
}

// Concrete Products
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Circle");
}
}

class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing Square");
}
}

// Factory
class ShapeFactory {
public Shape getShape(String shapeType) {
if (shapeType == null) {
return null;
}
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (shapeType.equalsIgnoreCase("SQUARE")) {
return new Square();
}
return null;
}
}

// Client
public class FactoryPatternDemo {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();

Shape shape1 = shapeFactory.getShape("CIRCLE");
shape1.draw();

Shape shape2 = shapeFactory.getShape("SQUARE");
shape2.draw();
}
}

Abstract Factory Pattern

The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is a super-factory that creates other factories.

Key Points

  • Purpose: To create families of related or dependent objects.
  • Implementation: Uses multiple factory methods to create objects.
  • Flexibility: Allows for the creation of objects from multiple families.
  • Example Use Case: Creating different types of shapes and colors.
// Abstract Products
interface Shape {
void draw();
}

interface Color {
void fill();
}

// Concrete Products
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Circle");
}
}

class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing Square");
}
}

class Red implements Color {
@Override
public void fill() {
System.out.println("Filling Red");
}
}

class Blue implements Color {
@Override
public void fill() {
System.out.println("Filling Blue");
}
}

// Abstract Factory
interface AbstractFactory {
Shape getShape(String shapeType);
Color getColor(String colorType);
}

// Concrete Factories
class ShapeFactory implements AbstractFactory {
@Override
public Shape getShape(String shapeType) {
if (shapeType == null) {
return null;
}
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (shapeType.equalsIgnoreCase("SQUARE")) {
return new Square();
}
return null;
}

@Override
public Color getColor(String colorType) {
return null; // ShapeFactory doesn't create colors
}
}

class ColorFactory implements AbstractFactory {
@Override
public Shape getShape(String shapeType) {
return null; // ColorFactory doesn't create shapes
}

@Override
public Color getColor(String colorType) {
if (colorType == null) {
return null;
}
if (colorType.equalsIgnoreCase("RED")) {
return new Red();
} else if (colorType.equalsIgnoreCase("BLUE")) {
return new Blue();
}
return null;
}
}

// Factory Producer
class FactoryProducer {
public static AbstractFactory getFactory(String choice) {
if (choice.equalsIgnoreCase("SHAPE")) {
return new ShapeFactory();
} else if (choice.equalsIgnoreCase("COLOR")) {
return new ColorFactory();
}
return null;
}
}

// Client
public class AbstractFactoryPatternDemo {
public static void main(String[] args) {
AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");

Shape shape1 = shapeFactory.getShape("CIRCLE");
shape1.draw();

Shape shape2 = shapeFactory.getShape("SQUARE");
shape2.draw();

AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");

Color color1 = colorFactory.getColor("RED");
color1.fill();

Color color2 = colorFactory.getColor("BLUE");
color2.fill();
}
}

Key Differences

Scope:

  • Factory Pattern: Focuses on creating one type of product.
  • Abstract Factory Pattern: Focuses on creating families of related products.

Complexity:

  • Factory Pattern: Simpler, with a single factory method.
  • Abstract Factory Pattern: More complex, with multiple factory methods and factories.

Flexibility:

  • Factory Pattern: Less flexible, suitable for a single product family.
  • Abstract Factory Pattern: More flexible, suitable for multiple product families.

Usage:

  • Factory Pattern: Use when you need to create objects of a single family.
  • Abstract Factory Pattern: Use when you need to create objects of multiple families or related objects.

Technical Round 2:

This was more on spring, spring boot, rest and microservice based.

Write down a Program as a REST API to fetch data by ID and name?

Below is an example of a simple Spring Boot REST API that fetches data by ID and name. This example uses an in-memory list to store data for simplicity.

  1. Create a Spring Boot application.
  2. Define a Person entity.
  3. Create a PersonController to handle HTTP requests.
  4. Implement methods to fetch data by ID and name.

Java Code-

Step 1: Create a Spring Boot Application

Create a new Spring Boot application using Spring Initializr or your preferred method.

Step 2: Define the Person Entity

package com.example.demo;

public class Person {
private Long id;
private String name;

// Constructors, getters, and setters

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

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

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

Step 3: Create the PersonController

package com.example.demo;

import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/persons")
public class PersonController {

private List<Person> persons = new ArrayList<>();

// Constructor to add some sample data
public PersonController() {
persons.add(new Person(1L, "John Doe"));
persons.add(new Person(2L, "Jane Doe"));
persons.add(new Person(3L, "Alice Smith"));
}

@GetMapping("/{id}")
public Person getPersonById(@PathVariable Long id) {
Optional<Person> person = persons.stream().filter(p -> p.getId().equals(id)).findFirst();
return person.orElse(null);
}

@GetMapping("/name/{name}")
public List<Person> getPersonByName(@PathVariable String name) {
return persons.stream().filter(p -> p.getName().equalsIgnoreCase(name)).collect(Collectors.toList());
}
}

Step 4: Main Application Class

package com.example.demo;

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

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

Tell me about Internal working of ArrayList?

The ArrayList class in Java is a part of the Java Collections Framework and provides a resizable array implementation. It is a widely used data structure due to its dynamic nature and ease of use. Here's a detailed look at the internal workings of ArrayList.

Key Components

Internal Array: The actual array that holds the elements.

private transient Object[] elementData;

Size Field: Keeps track of the number of elements in the ArrayList.

private int size;

Key Operations

  1. Adding Elements:
  • When an element is added, ArrayList checks if there is enough capacity in the internal array.
  • If the array is full, it is resized (usually doubled in size) to accommodate more elements.
  • The new element is then added to the end of the array.
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Ensure capacity
elementData[size++] = e; // Add element
return true;
}

private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 times old capacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}

Accessing Elements:

  • Elements are accessed by their index using the get method.
  • The method simply returns the element at the specified index.
public E get(int index) {
rangeCheck(index);
return elementData(index);
}

private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

E elementData(int index) {
return (E) elementData[index];
}

Removing Elements:

  • When an element is removed, all subsequent elements are shifted one position to the left to fill the gap.
  • The size of the ArrayList is decremented.
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
elementData[--size] = null; // Clear to let GC do its work
return oldValue;
}

Resizing:

  • The internal array is resized when the current capacity is insufficient to accommodate new elements.
  • The resizing operation involves creating a new array with a larger capacity and copying the existing elements to the new array.

Performance Characteristics

  • Time Complexity:
  • Access by Index: O(1) — Direct access to elements by index.
  • Add (Amortized): O(1) — Adding elements is generally O(1), but resizing can make it O(n) in the worst case.
  • Remove: O(n) — Removing elements requires shifting subsequent elements.
  • Search: O(n) — Linear search for elements.

How to increase the size of an ArrayList in java?

In Java, the ArrayList class automatically increases its size when elements are added beyond its current capacity. This resizing is handled internally by the ArrayList class, so you typically don't need to manually increase its size. However, understanding how this resizing works and how you can influence it can be useful.

Internal Resizing Mechanism

When you add an element to an ArrayList and it exceeds the current capacity, the ArrayList automatically resizes itself. The resizing process involves:

  1. Creating a new array with a larger capacity.
  2. Copying the existing elements to the new array.

Ensuring Capacity

You can manually ensure that the ArrayList has enough capacity to accommodate a certain number of elements using the ensureCapacity method. This can be useful if you know in advance that you'll be adding a large number of elements, as it can reduce the number of resizing operations.

import java.util.ArrayList;

public class ArrayListExample {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();

// Adding elements to the ArrayList
for (int i = 0; i < 10; i++) {
list.add(i);
}

// Print the current size and capacity
System.out.println("Size: " + list.size());
System.out.println("Capacity (before ensuring capacity): " + getCapacity(list));

// Ensure capacity
list.ensureCapacity(20);

// Print the capacity after ensuring capacity
System.out.println("Capacity (after ensuring capacity): " + getCapacity(list));

// Adding more elements to the ArrayList
for (int i = 10; i < 20; i++) {
list.add(i);
}

// Print the final size and capacity
System.out.println("Final Size: " + list.size());
System.out.println("Final Capacity: " + getCapacity(list));
}

// Utility method to get the capacity of an ArrayList
private static int getCapacity(ArrayList<?> list) {
try {
java.lang.reflect.Field field = ArrayList.class.getDeclaredField("elementData");
field.setAccessible(true);
return ((Object[]) field.get(list)).length;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
}

Explanation

  1. Adding Elements: The for loop adds 10 elements to the ArrayList.
  2. Ensuring Capacity: The ensureCapacity(20) method call ensures that the ArrayList can accommodate at least 20 elements without resizing.
  3. Adding More Elements: Another for loop adds 10 more elements to the ArrayList.
  4. Utility Method: The getCapacity method uses reflection to access the internal array's length, which represents the capacity of the ArrayList.

Output

The output will show the size and capacity of the ArrayList before and after ensuring capacity, as well as the final size and capacity after adding more elements.

What is the difference between Synchronized (HashMap) vs ConcurrentHashMap?

HashMap and ConcurrentHashMap are both implementations of the Map interface in Java, but they are designed for different use cases, especially when it comes to concurrency.

- Synchronized HashMap

A HashMap can be synchronized externally to make it thread-safe. This is typically done by wrapping the HashMap using Collections.synchronizedMap.

Key Points

  • Thread Safety: Achieved by synchronizing the entire map.
  • Performance: Synchronizing the entire map can lead to significant contention and performance degradation in a multi-threaded environment.
  • Usage: Suitable for scenarios with low concurrency or where the map is mostly read-only.

- ConcurrentHashMap

ConcurrentHashMap is designed specifically for concurrent access. It provides thread safety without locking the entire map, thus offering better performance in a multi-threaded environment.

Key Points

  • Thread Safety: Achieved through fine-grained locking (using segments or internal locks).
  • Performance: Better performance under high concurrency due to reduced contention.
  • Usage: Suitable for scenarios with high concurrency where multiple threads frequently read and write to the map.

How to Configure a Spring Boot application?

Configuring a Spring Boot application involves setting up various properties and components to customize the behavior of the application. Here are the key steps and methods to configure a Spring Boot application:

1. Using application.properties or application.yml

Spring Boot allows you to configure your application using a properties file (application.properties) or a YAML file (application.yml). These files are typically placed in the src/main/resources directory.

2. Using Command-Line Arguments

You can override properties defined in application.properties or application.yml by passing them as command-line arguments when starting the application.

3. Using Environment Variables

Spring Boot can also read configuration properties from environment variables. This is useful for configuring the application in different environments (e.g., development, testing, production).

4. Using Java Configuration

You can configure your Spring Boot application programmatically using Java configuration classes annotated with @Configuration.

5. Using Profiles

Spring Boot supports profiles to configure different environments (e.g., development, testing, production). You can define profile-specific properties files like application-dev.properties and application-prod.properties.

6. Using @Value and @ConfigurationProperties

You can inject configuration properties into your Spring beans using the @Value annotation or the @ConfigurationProperties annotation.

Whats the key difference between @Component vs @Service annotations?

Semantics:

  • @Component: Generic and can be used for any Spring-managed component.
  • @Service: Specifically indicates that the class provides business logic or services.

Readability and Maintainability:

  • @Component: Less specific, so it may not convey the exact role of the class.
  • @Service: More specific, making it clear that the class is part of the service layer, which improves code readability and maintainability.

Can we use @Component be used in place of @Service?

Yes, you can use @Component in place of @Service: Both annotations will register the class as a Spring bean.

Prefer @Service for service layer classes: Using @Service makes the role of the class more explicit, improving code readability and maintainability.

How does @RequestBody convert data into JSON?

How @RequestBody Works

  1. Client Request: The client sends an HTTP request with a JSON payload.
  2. Controller Method: The controller method is annotated with @RequestBody on a parameter.
  3. HttpMessageConverter: Spring uses HttpMessageConverter to convert the JSON payload into a Java object.
  4. Jackson Library: The MappingJackson2HttpMessageConverter uses the Jackson library to perform the actual conversion.

What are the ConcurrentHashMap exceptions you have encountered?

ConcurrentModificationException

  • Description: Unlike HashMap, ConcurrentHashMap does not throw ConcurrentModificationException during concurrent modifications. However, you should still be aware of the potential for inconsistent views if you iterate over the map while it is being modified.

What is Persist in hibernate?

In Hibernate, persist is a method provided by the EntityManager interface (in JPA) and the Session interface (in Hibernate). It is used to save a transient entity instance to the database. The persist method makes a transient instance persistent, meaning it transitions the entity from the transient state to the persistent state.

Differences between persist and save

Return Type:

  • persist: void (does not return the identifier).
  • save: Returns the generated identifier of the entity.

Immediate Insert:

  • persist: Does not guarantee an immediate insert; the insert happens when the transaction is committed or the session is flushed.
  • save: May trigger an immediate insert.

Entity State:

  • persist: Ensures the entity is in the persistent state.
  • save: Also ensures the entity is in the persistent state but returns the identifier.

Difference between wait and join in multithreading?

  • wait: Used for inter-thread communication, requires synchronization, releases the lock, and waits for notify or notifyAll.
  • join: Used to wait for a thread to finish, does not require synchronization, does not release the lock, and waits for the thread to die.

Thanks for Reading

  • 👏 Please clap for the story and follow me 👉
  • 📰 Read more content on my Medium (70+ stories on Java Developer interview)

Find my books here:

--

--

Ajay Rathod
Ajay Rathod

Written by Ajay Rathod

Java Programmer | AWS Certified | Writer | Find My Books on Java Interview here - https://rathodajay10.gumroad.com | YouTube - https://www.youtube.com/@ajtheory

Responses (3)