Blog Accolite Interview Experiences

Interview Question asked in Accolite

Question 1. First Print Odd, then Print Even for Numbers 1-10

In this scenario, we want to print the odd numbers first and then the even numbers. We can achieve this by using two threads and the join() method to make sure the thread printing even numbers waits until the thread printing odd numbers has finished.

Here’s a simple implementation:

Java
Thread t1 = new Thread(() -> {
    for(int i=1; i<=10; i++){
        if(i%2 !=0){
            System.out.println("Odd number: "+ i);
        }
    }
});

Thread t2 = new Thread(() -> {
    for(int i=1; i<=10; i++){
        if(i%2 ==0){
            System.out.println("Even number: "+ i);
        }
    }
});

t1.start();
t2.start();

try {
    t1.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

In this code, t1 and t2 are started concurrently. t1 prints the odd numbers from 1 to 10, and t2 prints the even numbers from 1 to 10. The join() method is called on t1, which means that the main thread will wait for t1 to finish its execution before it continues. However, this does not guarantee that t1 will finish before t2 starts printing, because t1 and t2 are running concurrently.

If you want to ensure that all odd numbers are printed before any even numbers, you would need to call join() on t1 before starting t2. But in your current code, t1 and t2 are running concurrently, so their output could interleave.

Fix

Java
Thread t1 = new Thread(() -> {
    for(int i=1; i<=10; i++){
        if(i%2 !=0){
            System.out.println("Odd number: "+ i);
        }
    }
});

Thread t2 = new Thread(() -> {
    for(int i=1; i<=10; i++){
        if(i%2 ==0){
            System.out.println("Even number: "+ i);
        }
    }
});

t1.start();

try {
    t1.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

t2.start();

In this code, t1 is started and prints the odd numbers from 1 to 10. After t1 has finished (ensured by t1.join()), t2 is started and prints the even numbers from 1 to 10. This ensures that all odd numbers are printed before any even numbers.

Question 2. Print Numbers using Odd and Even Threads

Sure, I can help you with that. Here’s how you can use two threads to print numbers from 1 to 10 in sequence such that one thread prints only odd numbers and the other thread prints only even numbers. We’ll use wait() and notify() to synchronize the threads:

Java
public class Main {
    private int number = 1;
    private final int MAX = 10;
    private final Object lock = new Object();

    void printOdd() {
        synchronized (lock) {
            while (number <= MAX) {
                while (number % 2 == 0) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println(Thread.currentThread().getName() + ": " + number++);
                lock.notify();
            }
        }
    }

    void printEven() {
        synchronized (lock) {
            while (number <= MAX) {
                while (number % 2 != 0) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println(Thread.currentThread().getName() + ": " + number++);
                lock.notify();
            }
        }
    }

    public static void main(String[] args) {
        Main main = new Main();
        Thread t1 = new Thread(main::printOdd, "Odd");
        Thread t2 = new Thread(main::printEven, "Even");
        t1.start();
        t2.start();
    }
}

In this code, printOdd and printEven are synchronized on a common lock object. Each method waits while the condition for its numbers is not met, and notifies the other thread when it has printed a number. This ensures that the numbers are printed in sequence.

Question3: Difference between @primary and @Qualifier in Spring Boot

In the dynamic world of Spring Boot applications, managing dependencies can become a complex task. Here’s where annotations like @Primary and @Qualifier come in, playing a crucial role in dependency injection. But what exactly do they do, and how are they different? This blog dives deep to clarify the distinctions between these annotations and empower you to make informed decisions in your Spring Boot projects.

Understanding Dependency Injection:

Dependency injection is a core concept in Spring Boot, promoting loose coupling between classes. Essentially, a class doesn’t create its own dependencies but relies on Spring to inject them at runtime. This fosters modularity, testability, and easier maintenance of your codebase.

Introducing the Key Players:

  • @Primary: This annotation acts as a champion, designating a preferred bean among multiple beans of the same type. When Spring encounters an ambiguous injection point (multiple candidate beans), the one marked with @Primary gets injected by default.
  • @Qualifier: This annotation acts as a specific scout, allowing you to handpick a particular bean during injection. You can use @Qualifier along with a qualifier value (a string identifier) to tell Spring which exact bean to inject.

When to Use Which:

  • Use @Primary When:
    • You have a common base functionality with multiple specific implementations, and you want to choose the default one.
    • It simplifies injection by avoiding the need to explicitly specify the bean in most scenarios.
  • Use @Qualifier When:
    • You have multiple implementations with distinct purposes.
    • You want to choose the most suitable bean based on your specific needs in a particular part of your code.

Example in Action:

Java

Java
// Interface with multiple implementations
public interface DataFormatter {
  String formatData(Object data);
}

@Component
@Primary  // This is the default implementation
public class JsonDataFormatter implements DataFormatter {
  @Override
  public String formatData(Object data) {
    return "JSON format";
  }
}

@Component
public class CsvDataFormatter implements DataFormatter {
  @Override
  public String formatData(Object data) {
    return "CSV format";
  }
}

@Service
public class DataService {

  @Autowired
  private DataFormatter dataFormatter;  // Ambiguous dependency (without @Qualifier)

  public void processData(Object data) {
    String formattedData = dataFormatter.formatData(data);
    // Use the injected dataFormatter implementation (default: JsonDataFormatter)
  }
}

// Specifying a particular implementation using @Qualifier
@Service
public class ReportService {

  @Autowired
  @Qualifier("csvDataFormatter")
  private DataFormatter dataFormatter; 

  public void generateReport(Object data) {
    String formattedData = dataFormatter.formatData(data);
    // Use the CsvDataFormatter specifically for report generation
  }
}

In this example:

  • DataService has an @Autowired dependency on DataFormatter.
  • Both JsonDataFormatter and CsvDataFormatter implement the interface.
  • JsonDataFormatter is marked as @Primary, making it the default choice.
  • Without @Qualifier, DataService will get JsonDataFormatter injected.
  • ReportService uses @Qualifier("csvDataFormatter") to explicitly request the CsvDataFormatter for its specific report generation needs.

Precedence:

Remember, @Qualifier takes precedence over @Primary. Even if a bean is marked as @Primary, you can still use @Qualifier to inject a different bean of the same type.

Question 4: What is Circular Dependency in Spring Boot?

Circular Dependencies: The Gordian Knot of Spring Boot Applications

Spring Boot applications are known for their simplicity and elegance, but even the most streamlined code can encounter challenges. One such hurdle is the dreaded circular dependency.

Understanding Circular Dependencies:

A circular dependency occurs when two or more beans in your Spring Boot application depend on each other for initialization. This creates a catch-22 situation:

  • Bean A needs Bean B to be injected before it can be fully functional.
  • However, Bean B also requires Bean A to be injected before it’s ready.

Spring Boot, by default, attempts to create all beans at startup. When it encounters a circular dependency, it throws an exception, halting application initialization. This can be a significant roadblock during development and deployment.

Common Causes of Circular Dependencies:

  • Overly Coupled Classes: Tight coupling between classes can lead to situations where each class relies heavily on the other, creating a circular dependency.
  • Unskilled Dependency Injection: Improper use of dependency injection can inadvertently introduce circular dependencies. Injecting unnecessary dependencies or having circular references within your code can create this issue.
  • Third-Party Library Conflicts: Occasionally, conflicts between third-party libraries can introduce circular dependencies if they have unintended reliance on each other.

Symptoms of Circular Dependencies:

  • BeanCurrentlyInCreationException: This exception is a telltale sign of a circular dependency during application startup.
  • StackOverflowError: In extreme cases, circular dependencies can lead to a stack overflow error as Spring becomes stuck in an infinite loop trying to create the beans.

Strategies to Break the Cycle:

  • Refactoring for Loose Coupling: Restructure your code to loosen the coupling between classes. Extract interfaces or separate functionalities into independent classes to reduce reliance on each other.
  • Utilize Constructor Injection: Favor constructor injection over setter injection for dependencies. Constructor injection forces the dependencies to be provided upfront, potentially helping to break circular chains.
  • Introduce a Service Layer: Create a service layer that acts as a mediator between your beans. This layer can hold the dependencies needed by both beans, eliminating the direct circular reference.
  • Leverage Lazy Initialization: Annotate a bean with @Lazy to defer its initialization until it’s actually needed. This can help break circular dependencies if one bean only needs the other under specific circumstances.

Example Scenario:

Java
public class UserService {

  private OrderService orderService;

  @Autowired
  public UserService(OrderService orderService) {
    this.orderService = orderService;
  }

  public User getUserDetails(Long userId) {
    User user = // fetch user details
    user.setOrders(orderService.getOrdersForUser(userId));
    return user;
  }
}

public class OrderService {

  private UserService userService;

  @Autowired
  public OrderService(UserService userService) {
    this.userService = userService;
  }

  public List<Order> getOrdersForUser(Long userId) {
    User user = userService.getUserDetails(userId);  // Circular dependency!
    return user.getOrders();
  }
}

In this example, UserService and OrderService have a circular dependency. UserService needs OrderService to fetch user orders, but OrderService needs UserService to get user details.

Refactored Example (using Service Layer):

Java
public interface OrderDetailsService {

  OrderDetails getOrderDetailsForUser(Long userId);
}

@Service
public class OrderDetailsServiceImpl implements OrderDetailsService {

  private OrderRepository orderRepository;
  private UserRepository userRepository;

  @Autowired
  public OrderDetailsServiceImpl(OrderRepository orderRepository, UserRepository userRepository) {
    this.orderRepository = orderRepository;
    this.userRepository = userRepository;
  }

  @Override
  public OrderDetails getOrderDetailsForUser(Long userId) {
    User user = userRepository.findById(userId);
    List<Order> orders = orderRepository.findByUser(user);
    return new OrderDetails(user, orders);
  }
}

public class UserService {

  private OrderDetailsService orderDetailsService;

  @Autowired
  public UserService(OrderDetailsService orderDetailsService) {
    this.orderDetailsService = orderDetailsService;
  }

  public User getUserDetails(Long userId) {
    OrderDetails orderDetails = orderDetailsService.getOrderDetailsForUser(userId);
    return orderDetails.getUser();
  }
}

public class OrderService {

  private OrderDetailsService orderDetailsService;

  @Autowired
  public OrderService(OrderDetailsService orderDetailsService)

Question 5: We are given two beans bean1 and bean2, how to determine the order of bean creation?

In Spring Boot, by default, the order of bean creation isn’t explicitly guaranteed. Spring manages bean creation and initialization based on dependencies and lifecycle methods. However, there are a few ways to influence or at least predict the order in which your beans are created:

  1. Dependency Injection:
    • Spring creates beans in a topologically sorted order based on their dependencies. A bean is created only after all its dependencies are available.
    • If bean1 has a dependency on bean2, then bean2 will be created before bean1.
  2. @DependsOn Annotation:
    • You can explicitly specify the order using the @DependsOn annotation.
    • By adding @DependsOn("bean2") to the definition of bean1, you force Spring to create bean2 before bean1.
  3. Bean Initialization Methods:
    • Spring provides lifecycle methods like @PostConstruct that are called after a bean is fully constructed.
    • You can use these methods to perform actions after a bean is initialized, but they don’t directly control creation order.

Here’s a breakdown of how these methods can influence order:

  • Scenario 1 (Dependency Injection):
Java
@Component
public class Bean1 {

  @Autowired
  private Bean2 bean2;

  // ...
}

@Component
public class Bean2 {

  // ...
}

In this case, since Bean1 has a dependency on Bean2, Spring will create Bean2 before Bean1.

  • Scenario 2 (Using @DependsOn):
Java
@Component
@DependsOn("bean2")  // Force bean2 creation before bean1
public class Bean1 {

  // ...
}

@Component
public class Bean2 {

  // ...
}

Here, @DependsOn("bean2") on Bean1 ensures bean2 is created before bean1.

  • Scenario 3 (Limited Control with @PostConstruct):
Java
@Component
public class Bean1 {

  @Autowired
  private Bean2 bean2;

  @PostConstruct
  public void afterInit() {
    // This method is called after bean1 is fully constructed
    System.out.println("Bean1 initialized");
  }
}

@Component
public class Bean2 {

  @PostConstruct
  public void afterInit() {
    // This method is called after bean2 is fully constructed
    System.out.println("Bean2 initialized");
  }
}

In this case, @PostConstruct methods are called after the respective bean’s initialization (not creation). While the order of these calls might suggest creation order, it’s not guaranteed. Spring might create them in any order as long as dependencies are met.

Key Points:

  • Implicit Order: Spring’s default bean creation order is based on dependency resolution.
  • Explicit Control: Use @DependsOn for situations where order is critical.
  • Limited Control with Lifecycle Methods: @PostConstruct methods offer limited control and shouldn’t be solely relied upon for determining creation order.

By understanding these concepts, you can make informed decisions about how to manage bean creation order in your Spring Boot applications. If strict order is crucial, consider refactoring your code to eliminate circular dependencies and leverage constructor injection whenever possible.

Question 6: In database can primary key null?

No, in relational databases, primary keys typically cannot contain null values. Here’s why:

  • Uniqueness: A primary key’s primary purpose is to uniquely identify each row in a table. Null values, by definition, represent the absence of a value. If a primary key could be null, multiple rows could potentially have the same null value, violating the principle of uniqueness.
  • Data Integrity: Primary keys are essential for enforcing data integrity within a database. Foreign keys in other tables often reference the primary key of another table. If the primary key could be null, these foreign key relationships could become invalid, leading to inconsistencies in the data.
  • Efficiency: Many database operations, like indexing and searching, rely on the uniqueness and presence of values in the primary key. Allowing null values in the primary key can hinder the efficiency of these operations.

Exceptions (Rare Cases):

There might be some edge cases where specific database systems allow null values in primary keys under certain conditions. However, this is generally not recommended and can lead to complications in database design and maintenance.

Alternatives for Handling Missing Data:

  • Default Values: You can define a default value for a column that acts as the primary key if no other value is provided. This ensures uniqueness and avoids null values.
  • Separate Not-Null Column: In some scenarios, you might have a separate column to indicate the absence of data (e.g., a flag set to true if the data is missing). The primary key itself would still not allow null values.

Question 7: We have two tables one has the primary key other has foreign key, can we delete the primary record from table?

Yes, you can delete a record from the table with the primary key, even if another table has a foreign key referencing it. However, the outcome depends on the configured behavior for handling such deletes, which is determined by the database system and the specific constraints in place. Here are the two main scenarios:

  1. On Delete Cascade:
    • If the foreign key constraint has ON DELETE CASCADE defined, then deleting the record from the primary key table will also automatically delete any referencing records in the foreign key table. This ensures data integrity by maintaining consistency between the tables.
  2. Restricted Delete (Default Behavior):
    • By default, most database systems will prevent you from deleting a record from the primary key table if there are existing foreign key references in another table. This is to avoid creating “orphaned” records in the foreign key table (records referencing a non-existent primary key).
    • In this case, you’ll encounter an error message or exception during the delete operation.

How to Handle Restricted Deletes:

There are a few ways to handle restricted deletes:

  • Delete Referring Records First: You can first delete the referencing records in the foreign key table before deleting the primary key record. This ensures there are no remaining foreign key constraints referencing the deleted record.
  • Disable Foreign Key Constraint (Temporary): As a temporary measure, you can disable the foreign key constraint before deleting the primary key record. However, this should be done with caution as it can introduce data integrity issues if not re-enabled properly. (Not recommended for production environments!)
  • Modify Data in Foreign Key Table: In some cases, you might be able to modify the data in the foreign key table to remove the reference to the deleted primary key record. This could involve setting the foreign key value to null (if allowed) or updating it to reference a different valid record.

Choosing the Right Approach:

The best approach depends on your specific situation and data integrity requirements. Here are some general guidelines:

  • ON DELETE CASCADE: Use this if you want to ensure automatic deletion of referencing records when a primary key record is deleted. However, be cautious as it can lead to unintended data loss.
  • Manual Deletion with Referential Integrity Checks: This approach offers more control but requires ensuring you delete referencing records first to maintain data consistency.
  • Modifying Foreign Key Data: This might be suitable for specific scenarios, but exercise caution to avoid introducing inconsistencies.

By understanding these concepts, you can effectively manage deletes involving primary and foreign key relationships in your database. Remember to choose the approach that best aligns with your data integrity needs and application logic.

Question 8: What is default port for http and https?

The default ports for HTTP and HTTPS are:

  • HTTP: Port 80
  • HTTPS: Port 443

These ports are widely known and used by default by web browsers and servers. When you access a website using a URL in your browser (e.g., http://www.example.com), the browser automatically connects to the web server on port 80 (for HTTP) unless otherwise specified in the URL.

Similarly, for HTTPS connections, which are encrypted for secure communication, the browser connects to the server on port 443 by default.

It’s important to note that these are the default ports, and it’s possible to configure web servers to use different ports for HTTP and HTTPS. However, using the standard ports is generally recommended for better compatibility and user experience.

Question 9: Default scope of Spring bean?

The default scope for a bean definition in Spring (including a String bean) is singleton. This means:

  • Spring creates only one instance of the bean throughout the application’s lifecycle.
  • Any subsequent requests for the same bean will return the same instance that was already created.

This behavior applies to String beans as well. If you define a String bean in your Spring configuration without explicitly specifying a scope, Spring will create a single String object and inject that same object wherever it’s needed.

Here’s a breakdown of the default scope and its implications:

Benefits of Singleton Scope:

  • Improved Performance: Creating objects can be resource-intensive. By having a single instance, Spring avoids the overhead of creating new String objects for every request.
  • Memory Efficiency: Since only one instance exists, memory usage is optimized as you’re not creating multiple copies of the same String data.

Drawbacks of Singleton Scope (for String Beans):

  • Limited Mutability: Strings in Java are immutable, meaning their content cannot be changed after creation. This might not be an issue for static values, but if you need a String that can be modified after injection, the singleton scope might not be suitable.

Alternatives for String Beans (if Mutability is Needed):

  • Prototype Scope: If you require a modifiable String bean for each injection point, you can explicitly define the scope as prototype in your Spring configuration. This will create a new String object for each injection.
  • Consider a separate class: In some cases, it might be better to create a separate class that encapsulates String data and provides methods to modify it if needed.

In summary:

  • The default scope for String beans (and all beans in general) is singleton.
  • This is efficient for static String values.
  • If you need a modifiable String for each injection, consider using prototype scope or a separate class.

Remember to choose the appropriate scope based on your specific requirements and whether mutability is necessary for your String beans.

Question 10: What consume more memory String or character?

In most cases, a String will consume more memory than a single character. Here’s a breakdown of why:

  • String Object Overhead: A String in Java is an object. In addition to the actual characters that make up the String data, it also has some overhead for storing information like the length of the String and other internal bookkeeping.
  • Character Size: A single character typically occupies 1 byte of memory (in some cases, it might be 2 bytes for Unicode characters).

Memory Consumption Example:

  • Consider a String containing the word “hello”.
    • The String itself might store the characters ‘h’, ‘e’, ‘l’, ‘l’, and ‘o’. (5 characters)
    • However, the String object also has additional memory overhead for storing its length (potentially 4 bytes for an integer) and other reference information.

Therefore, even though the String data itself might be 5 bytes, the overall memory consumption of the String object can be more than the memory used by a single character.

Nuances to Consider:

  • String Pooling: Java has a String pool that can store commonly used String literals. If you create multiple String objects with the same content, they might refer to the same object in the pool, reducing memory usage.
  • Character Encoding: The memory usage of a character can vary depending on the character encoding used (e.g., ASCII vs. Unicode).

In conclusion:

  • Strings generally consume more memory than single characters due to object overhead.
  • String pooling can optimize memory usage for frequently used String literals.
  • The specific memory consumption can vary depending on character encoding and the JVM implementation.

Question 11: Suppose char name [] ={‘N’, ‘E’, ‘E’, ‘L’, ‘A’, ‘B’, ‘H’}, Sting name1=”NEELABH” what is there reason behind the it String taking extra space?

In the scenario you provided, the String name1 = "NEELABH" will take extra space compared to the char name[] = {'N', 'E', 'E', 'L', 'A', 'B', 'H'} character array. Here’s why:

String vs. Character Array:

  • Character Array: This is a basic array that directly stores the characters in memory. Each character typically occupies 1 byte (or 2 bytes for some Unicode characters). In your example, the name array holds 7 characters, potentially using 7 bytes of memory.
  • String: A String in Java is an object. It not only stores the characters themselves but also has additional information:
    • Length: The String object keeps track of the total number of characters it holds. This typically requires 4 bytes of memory to store an integer value (length).
    • Immutability: Strings in Java are immutable, meaning their content cannot be changed after creation. This requires some internal bookkeeping within the String object.
    • Object Overhead: Every object in Java has some overhead for housekeeping purposes, such as references and type information.

Memory Breakdown:

  • Character Array: The name array likely uses around 7 bytes (assuming 1 byte per character).
  • String: The name1 String object might use around 15-20 bytes of memory. This includes:
    • 7 bytes for the actual characters (‘N’, ‘E’, ‘E’, ‘L’, ‘A’, ‘B’, ‘H’).
    • 4 bytes for the String length (integer).
    • 4-9 bytes for object overhead (depending on the JVM implementation).

String Pooling (Potential Optimization):

Java has a String pool that can store commonly used String literals. If you create multiple String objects with the same content (e.g., "NEELABH"), they might refer to the same object in the pool, reducing memory usage. However, this optimization doesn’t always happen, and it depends on how the String is created.

In essence:

  • The character array is a more basic data structure, directly storing the characters.
  • The String object has additional functionalities and information, leading to a slightly larger memory footprint.

Key Takeaways:

  • Strings offer features like immutability and ease of use, but they come with a slight memory overhead.
  • Character arrays are more memory-efficient for storing raw character data but lack the functionalities of Strings.
  • The choice between String and character array depends on your specific needs. If memory optimization is crucial and you don’t require String functionalities like immutability, a character array might be a better option.

Question 12: What is Immutable in Java?

In Java, an immutable object is an object whose state cannot be changed after it’s been created. Once initialized, its internal data remains fixed throughout its lifetime. This concept offers several advantages in terms of thread safety, security, and reasoning about program behavior.

Here are some key characteristics of immutable objects:

  • Fixed Internal State: Once an immutable object is created, the values of its fields cannot be modified. Any attempt to modify the state will result in a new object being created with the updated values.
  • Final Fields: Immutable classes typically declare their fields as final. This prevents accidental modification of the fields after object creation.
  • Defensive Copying: In some cases, immutable objects might create copies of internal data structures (like arrays) when they need to be modified. This ensures that the original data remains unchanged.

Benefits of Immutability:

  • Thread Safety: Since their state is fixed, immutable objects can be safely shared between multiple threads without the need for synchronization. This simplifies concurrent programming and reduces the risk of race conditions.
  • Security: Immutability helps prevent accidental or malicious modification of an object’s data, enhancing program security.
  • Predictable Behavior: The state of an immutable object remains consistent throughout its lifetime, making it easier to reason about program behavior and debug issues.
  • Caching: Immutable objects can be effectively cached as their state won’t change, potentially improving performance.

Examples of Immutable Objects in Java:

  • String: The String class in Java is a classic example of an immutable object. Once a String is created, its content cannot be modified. Any methods that appear to modify a String (like concat) actually create a new String object with the updated content.
  • Integer, Long, Double, etc.: Java’s wrapper classes for primitive data types (Integer, Long, Double, etc.) are immutable. When you attempt to modify an Integer object, for example, a new Integer object is created with the new value.
  • Immutable Collections: Java provides libraries like Guava (from Google) that offer immutable collection implementations (like ImmutableList and ImmutableMap). These collections ensure that the underlying data cannot be modified after creation.

Creating Your Own Immutable Classes:

You can create your own immutable classes in Java by following these principles:

  • Declare the class as final to prevent inheritance.
  • Make all fields final.
  • Provide a constructor to initialize all final fields.
  • Don’t provide setter methods to modify the object’s state.
  • If necessary, create new immutable objects with updated data instead of modifying the existing object.

By embracing immutability, you can write more robust, secure, and easier-to-understand Java code.

Here’s an example of how to create an immutable class in Java:

Java
public final class Person {

  private final String name;
  private final int age;

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

  public String getName() {
    return name;  // Return a copy to avoid modifying original data
  }

  public int getAge() {
    return age;
  }

  // Don't provide setter methods - state cannot be changed after creation

  // Example method to create a new Person object with updated age
  public Person withNewAge(int newAge) {
    return new Person(name, newAge);
  }
}

Explanation:

  • This Person class is declared as final to prevent inheritance.
  • It has two private final fields, name and age, to store the person’s information.
  • The constructor initializes these final fields with the provided values.
  • There are getter methods for name and age, but they return copies of the data (for name) to avoid modifying the original object’s state.
  • The class doesn’t provide any setter methods as the object’s state shouldn’t be changed after creation.
  • The withNewAge method demonstrates how to create a new Person object with an updated age while keeping the original object immutable.

Usage Example:

Java
Person person1 = new Person("Alice", 30);
System.out.println(person1.getName()); // Output: Alice
System.out.println(person1.getAge()); // Output: 30

// Person's state cannot be modified directly
// person1.name = "Bob";  // This would cause a compilation error

Person person2 = person1.withNewAge(31);
System.out.println(person1.getAge()); // Output: 30 (original object remains unchanged)
System.out.println(person2.getAge()); // Output: 31 (new object with updated age)

In this example, creating a new Person object with an updated age is achieved by using the withNewAge method, which creates a new immutable object with the desired changes.

This is a basic example, but it demonstrates the principles of creating immutable classes in Java. You can extend this concept to more complex objects with appropriate methods to create new immutable objects reflecting any necessary state changes.

Question13: Why we do need immutable class?

There are several compelling reasons why you might want to use immutable classes in your Java applications:

1. Thread Safety:

  • Immutable objects shine in multithreaded environments. Since their state cannot be changed after creation, you can safely share them between multiple threads without worrying about data corruption caused by concurrent access. This eliminates the need for complex synchronization mechanisms, simplifying concurrent programming.

2. Enhanced Security:

  • Immutability safeguards your objects from accidental or malicious modifications. Once an immutable object’s state is set, it remains constant, preventing unintended changes that could lead to security vulnerabilities or unexpected behavior.

3. Predictable Behavior:

  • The fixed state of immutable objects makes reasoning about program behavior much easier. You can be confident that the object’s data won’t change unexpectedly, leading to more predictable and maintainable code.

4. Improved Caching:

  • Immutable objects are ideal candidates for caching because their state remains consistent. The cached object can be reused across different parts of your program without any concerns about modifications. This can potentially improve performance by reducing redundant object creation.

5. Easier Debugging:

  • With an immutable object, you know its state won’t change after creation. This simplifies debugging as you can focus on the initial state and how it interacts with other parts of your code, rather than tracking down potential state changes during program execution.

6. Referential Transparency:

  • Immutable objects promote referential transparency, meaning that replacing an object with another object with the same state will have the same overall effect on the program. This makes your code more functional and easier to reason about.

When to Consider Immutability:

While immutability offers numerous benefits, it’s not always the right choice. Here are some situations where immutability might be particularly advantageous:

  • Objects representing data that shouldn’t be modified (e.g., configuration settings, user information).
  • Objects passed between multiple threads to avoid synchronization issues.
  • Objects used as keys in collections (ensures consistent hashing).

Drawbacks to Consider:

  • Creation Overhead: Creating new immutable objects for every state change can introduce some overhead compared to mutable objects that can be modified in-place. However, this overhead is often negligible compared to the benefits of immutability.
  • Less Flexibility: Immutability can limit the ways you interact with an object’s data. If frequent modifications are necessary, mutability might be a better fit.

In conclusion:

Immutable classes offer a powerful tool for writing robust, secure, and maintainable Java applications. By understanding their benefits and drawbacks, you can make informed decisions about when to leverage immutability for a cleaner, more predictable codebase.

Question 14: Is Premitive class immutable in Java?

Yes, primitive data types in Java (like int, long, double, char, etc.) and their corresponding wrapper classes (like Integer, Long, Double, Character, etc.) are all immutable.

Here’s a breakdown of immutability in these contexts:

Primitive Data Types:

  • Primitive data types like int store their value directly in memory.
  • Once assigned a value, a primitive variable cannot be modified to hold a different value. Any attempt to modify it will create a new memory location with the new value.
  • Primitive data types are fundamental building blocks and don’t exhibit object-like behavior like mutability.

Wrapper Classes:

  • Wrapper classes provide object-oriented interfaces for primitive data types.
  • These wrapper classes (e.g., Integer for int) are also immutable.
  • When you try to modify a wrapper object (e.g., by calling setInt() on an Integer), a new Integer object is created with the updated value. The original object remains unchanged.

Example:

Java
int x = 10;
x = 20;  // Creates a new memory location with the value 20, original x remains 10

Integer y = new Integer(30);
y = y.intValue() + 10;  // Creates a new Integer object with the value 40, original y remains 30 (using intValue() to get the primitive value)

Immutability Benefits for Primitives and Wrappers:

  • Thread Safety: Since their values cannot be changed, primitives and wrapper objects can be safely shared between threads without synchronization concerns.
  • Simplicity: Immutability avoids the complexity of managing mutable state, especially in multithreaded environments.

In summary:

  • Primitive data types and their wrapper classes are all immutable in Java.
  • This immutability simplifies memory management, threading, and reasoning about program behavior.

Question 15: Is BigInteger is these immutable?

Yes, BigInteger in Java is an immutable class. This means that once a BigInteger object is created with a certain value, its internal state cannot be modified. Any methods that appear to change the value of a BigInteger object actually create a new BigInteger object with the updated value.

Here’s why immutability is beneficial for BigInteger:

  • Thread Safety: Since the state of a BigInteger object is fixed, it can be shared safely between multiple threads without the need for synchronization. This simplifies concurrent calculations involving large integers.
  • Security: Immutability safeguards BigInteger objects from accidental or malicious modifications during calculations. This helps maintain data integrity in sensitive operations.
  • Predictable Behavior: The immutable nature of BigInteger makes its behavior more predictable. You can be confident that the value won’t change unexpectedly during calculations, leading to more reliable results.

How Immutability Works with BigInteger Methods:

Even though methods like add, multiply, and subtract seem to modify the BigInteger object, they actually return a new BigInteger object with the result of the operation. The original object remains unchanged.

Here’s an example:

Java
BigInteger num1 = new BigInteger("12345");
BigInteger num2 = new BigInteger("54321");

BigInteger sum = num1.add(num2);  // sum will hold a new BigInteger object with the value 66666

System.out.println(num1); // Output: 12345 (original value remains unchanged)
System.out.println(sum);  // Output: 66666

Alternatives for Mutability (if Needed):

While immutability offers advantages, there might be rare cases where you need a mutable representation of a large integer. In such scenarios, you could consider:

  • Primitive Data Types (for Smaller Values): If you’re dealing with integers within the range of primitive data types (int, long), you can use them directly for better performance.
  • Custom Mutable Class (Not Recommended): This approach is generally discouraged as it introduces complexity and potential thread safety issues. It’s advisable to explore alternative solutions that leverage immutability.

In conclusion:

The immutability of BigInteger simplifies concurrent programming, enhances security, and promotes predictable behavior in calculations involving large integers. By understanding this concept, you can effectively utilize BigInteger for robust and reliable mathematical operations in your Java applications.

Question 16: Find the second largest element in an array without sorting, without internal methods and at max single loop?

Java
public class Solution {
    public static void main(String[] args) {
        int [] array = {2,3,19, 17, 20, 7, 16};
        System.out.println(getSecondLargestElement(array));
    }
    //max1 = 2,max2=2, max1=3, max2=2, max1=19, max2= 3
    public static int getSecondLargestElement(int [] nums){
        int maxValue1 = Integer.MIN_VALUE;;
        int maxValue2 = Integer.MIN_VALUE;
        for(int i=0; i< nums.length; i++){
            if(nums[i]> maxValue1){
                maxValue2 = maxValue1;
                maxValue1 = nums[i];
            }else if(nums[i]> maxValue2){
                maxValue2 = nums[i];
            }
        }
        return maxValue2;
    }
}

Avatar

Neelabh

About Author

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

You may also like

Blog Design Pattern

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

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

Base64 Decode

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