Skip to content
Raymond Cheung edited this page Jun 26, 2025 · 16 revisions

Java Core Concepts

Object-Oriented Programming

  • What are the differences between Abstract Class and Interface?
    • Abstract Classes:
      • An abstract class is used as a base class and cannot be instantiated.
      • It can contain both abstract and non-abstract (concrete) methods.
      • It can inherit from both abstract and non-abstract classes.
      • All abstract methods must be defined by the inheriting classes unless the subclass is also abstract.
      • An abstract class is declared with the abstract keyword and cannot be declared final.
      • An abstract method only contains a method signature and no body.
      • Abstract methods cannot be declared final, native, private, static, or synchronized.
    • Interfaces:
      • Interfaces define a set of declared public methods without method bodies.
      • They provide a public API and are implemented by Java classes.
      • Interfaces are implicitly abstract and do not need the abstract keyword.
      • An interface is declared using the interface keyword, followed by the method declarations.
      • Interface names are typically adjectives (ending in "able" or "ible") or nouns.
      • A class can implement multiple interfaces, and an interface can extend multiple interfaces.
  • What is the anonymous class? Why use it?
    • Definition: An anonymous class is a class without a specified name, defined and instantiated in a single expression.
    • Usage: It is used when a class is needed only once and does not need to be reused or referenced elsewhere in the code.
    • Common Use Cases:
      • Implementing interfaces or extending classes in a concise manner.
      • Passing event listeners or callbacks in GUI applications.
    • Benefits:
      • Simplifies code by eliminating the need for separate class declarations.
      • Keeps code compact and easier to maintain when a class is used only once.
  • What are Object methods?
    • equals(Object obj):
      • Compares the current object with the specified object for equality. The default implementation checks for reference equality (i.e., whether the two references point to the same object). This method can be overridden in subclasses to provide custom equality logic.
    • hashCode():
      • Returns a hash code value for the object, which is used in hash-based collections like HashMap, and HashSet. The default implementation provides an integer based on the memory address of the object, but it can be overridden for more meaningful hash code generation.
    • toString():
      • Returns a string representation of the object. The default implementation returns the class name followed by the memory address. It is commonly overridden to provide a more informative string that represents the state of the object.
    • getClass():
      • Returns the Class object that represents the runtime class of the current object. This can be used to inspect the object's class type dynamically.
    • clone():
      • Creates and returns a copy of the object. The default implementation performs a shallow copy, and it can be overridden to perform a deep copy if necessary. This method requires that the class implements the Cloneable interface.
    • notify():
      • Wakes up a single thread that is waiting on the object's monitor. The thread that gets awakened can then attempt to acquire the monitor lock of the object.
    • notifyAll():
      • Wakes up all threads that are waiting on the object's monitor.
    • wait():
      • Causes the current thread to release the monitor and enter the waiting state until it is awakened by another thread (via notify() or notifyAll()).
    • finalize():
      • Called by the Garbage Collector before an object is destroyed. It gives the object a chance to clean up any resources (e.g., closing files or releasing network connections) before being removed from memory. However, this method is considered obsolete, and its use is discouraged in modern Java programming.
  • What are the differences between overriding and overloading?
    • Definition:
      • Overloading: Overloading occurs when two or more methods in the same class have the same name but differ in parameters (number or type of parameters). It is resolved at compile time (static polymorphism).
      • Overriding: Overriding occurs when a subclass provides its own implementation for a method that is already defined in its superclass. The method signature in the subclass must be the same as the one in the superclass. It is resolved at runtime (dynamic polymorphism).
    • Purpose:
      • Overloading: To provide multiple ways to perform the same operation using different parameters, making the code more flexible.
      • Overriding: To modify or extend the functionality of a method in the parent class, allowing a subclass to provide its specific behaviour.
    • Method Signature:
      • Overloading: Methods must have the same name but different method signatures (i.e., different parameter types or numbers of parameters).
      • Overriding: The method signature in the subclass must be exactly the same as the method in the superclass ( same name, same number, and type of parameters).
    • Return Type:
      • Overloading: The return type can be different for overloaded methods as long as the parameter list is different.
      • Overriding: The return type must be the same (or a subtype) as the method being overridden in the superclass ( this is known as covariant return type).
    • Access Modifier:
      • Overloading: Overloaded methods can have different access modifiers (e.g., public, private).
      • Overriding: The access modifier of the overridden method in the subclass cannot be more restrictive than the method in the superclass. For example, if the superclass method is public, the subclass method must also be public.
    • Constructor:
      • Overloading: Can be done with constructors as well, where you define multiple constructors with different parameter lists.
      • Overriding: Constructors cannot be overridden in Java.
    • Static Methods:
      • Overloading: Static methods can be overloaded just like instance methods.
      • Overriding: Static methods cannot be overridden. If a static method in the subclass has the same name and signature as the method in the superclass, it is method hiding (not true overriding).
  • Can the method signature of overloading differ by only return type?
    • Return type alone cannot be used to differentiate overloaded methods.
    • If two methods differ only in their return type and the parameters are the same, it will result in a compilation error because the method signature is considered the same, and Java cannot distinguish them based on return type alone.

Modifiers and Keywords

  • What are the modifiers?
    • Access Modifiers:
      • public: The member (class, method, or variable) is accessible from any other class.
      • protected: The member is accessible within its own package and by subclasses.
      • private: The member is accessible only within its own class.
      • default (no modifier): The member is accessible only within its own package (package-private).
    • Non-Access Modifiers:
      • static: Denotes that a member belongs to the class rather than any specific instance of the class.
      • final: Prevents a class from being subclassed, a method from being overridden, or a variable from being reassigned.
      • abstract: Specifies that a class cannot be instantiated and may contain abstract methods that must be implemented by subclasses.
      • synchronized: Ensures that a method or block of code can be accessed by only one thread at a time.
      • volatile: Indicates that a variable may be modified asynchronously by different threads, ensuring visibility of changes across threads.
      • transient: Prevents a variable from being serialised.
      • native: Specifies that a method is implemented in native code (typically written in another language like C or C++).
      • strictfp: Ensures consistent floating-point calculations across different platforms.
    • Other Modifiers:
      • interface: Used to define an interface.
      • enum: Used to define an enumeration type.
      • @Override: Indicates that a method is overriding a method from a superclass or interface. It is not strictly a modifier but is commonly used with methods.
  • What is the static keyword?
    • Static data members, methods, constants, and initialisers are associated with the class itself, rather than with instances of the class.
    • Static members can be accessed within the class in which they are defined, or in another class using the dot operator.
    • Both static methods and static variables are accessed through the class name. They apply to the entire class and are shared by all instances of the class.
    • A static data member is accessed through the class name. There is only one instance of a static data member, regardless of how many instances of the class exist.
  • What is the final keyword?
    • A final class cannot be extended.
    • A final method cannot be overridden.
    • A final data member is initialised only once and cannot be changed.
    • A data member declared as static final is set at compile time and cannot be modified.
  • What is the finally keyword?
    • The finally block in Java is used to execute important code such as cleanup tasks (e.g., closing file streams or database connections) after a try block, regardless of whether an exception was thrown or not.
    • It is guaranteed to execute after the try and catch blocks, but before the control flow exits the method.
  • When the finally block can't be executed?
    • If the JVM exits: The finally block will not be executed if the JVM exits (for example, through System.exit()).
    • If the thread is killed: If the thread executing the finally block is forcibly killed (e.g., via Thread.stop()), the finally block might not run.
    • If the process crashes: If the JVM crashes due to an unhandled fatal error, the finally block may not be executed.
  • What are the differences among these keywords: final, finally, finalize?
    • final:
      • Used with variables: Marks a variable as constant, meaning its value cannot be changed after initialisation.
      • Used with methods: Prevents a method from being overridden by subclasses.
      • Used with classes: Prevents a class from being extended by other classes.
    • finally:
      • Used in exception handling: A block of code that follows a try block and is guaranteed to execute after the try and catch blocks, regardless of whether an exception was thrown or not.
      • It is typically used to release resources or perform clean-up operations.
    • finalize:
      • Used with methods: A method in the Object class that is called by the Garbage Collector before an object is destroyed.
      • It is meant for cleanup, but its use is discouraged because the exact time of execution is not predictable.

Equality and Hashing

  • What is Hash Code?
    • Definition:
      • The hash code is an integer value that Java uses for hashing algorithms in collections like HashMap and HashSet.
      • The default implementation of the hashCode() method in the Object class is derived from the memory address of the object, but this can be overridden to provide more meaningful and custom hash codes.
    • Use in Collections:
      • In hash-based collections, such as HashMap, objects are placed into buckets based on their hash code. Objects with the same hash code are placed in the same bucket, which improves the efficiency of searching and inserting elements.
      • A good hashCode() implementation helps in reducing collisions, where multiple objects end up in the same bucket, thus improving performance.
    • Contract with equals():
      • If two objects are considered equal according to the equals() method, they must also have the same hash code. This is the hash code contract.
      • If two objects are not equal (as per equals()), they may or may not have the same hash code.
      • However, if you override equals(), you should also override hashCode() to ensure the consistency of the contract.
    • Collisions:
      • A collision occurs when two objects have the same hash code. In such cases, the collection uses additional logic (like linked lists or balanced trees) to resolve the collision.
      • While the hash code is designed to minimise collisions, it is still possible for different objects to produce the same hash code, especially in large collections.
    • Best Practices for hashCode():
      • When overriding hashCode(), try to use the fields that determine equality (i.e., those used in the equals() method) in generating the hash code.
      • The hash code should remain consistent for an object during its lifetime, meaning that the hash code should not change if the object's properties used in equals() do not change.
  • What are the rules to follow when implementing hashCode()?
    • Consistency with equals():
      • Equal objects must have equal hash codes: If two objects are considered equal by the equals() method, they must return the same hash code when hashCode() is called.
      • This is crucial because hash-based collections like HashMap use the hash code to quickly locate objects, and if equals() says two objects are equal, they must also have the same hash code for the collection to work correctly.
    • Consistency:
      • The hash code of an object should remain constant during the execution of the program as long as the object's state does not change. This means that if the fields used in equals() do not change, the hash code should not change.
      • If the fields used in equals() are modified, the object's hash code may change, which could break the contract with hash-based collections.
    • Unequal objects may have different hash codes:
      • It is not required that unequal objects (i.e., objects that are not equal according to equals()) have different hash codes. However, it is desirable to have distinct hash codes for unequal objects to improve the performance of hash-based collections.
      • While not mandatory, the goal is to minimise hash collisions (when different objects have the same hash code).
    • Avoiding Magic Numbers:
      • When implementing hashCode(), avoid using "magic numbers" (arbitrary constant values). Instead, use the fields of the object to generate the hash code in a way that reflects its content.
      • The hash code should be based on the object's state (i.e., the values of its fields), especially those that are used in equals().
    • Efficient Implementation:
      • Hash code generation should be efficient. While the hash code does not need to be unique for each object (it's acceptable for two unequal objects to have the same hash code), it's important that the hash code is computed in a reasonable amount of time.
      • Use a good algorithm that minimises collisions while remaining efficient.
  • How does equals() work?
    • Default Implementation:
      • The default equals() method in the Object class compares the memory addresses (references) of two objects to determine if they are the same object.
      • It checks if the two references point to the same memory location, which means it will return true only if both references refer to the exact same object in memory.
      • This is not useful for value-based comparisons (e.g., comparing the values of attributes in two different Person objects).
    • Overriding equals():
      • To compare the values within two objects (such as comparing two Person objects based on their name and age), you need to override the equals() method in the class.
      • When overriding equals(), it is important to follow the contract that defines how equality should work. The contract of equals() is:
        • Reflexive: An object must equal itself. a.equals(a) should return true.
        • Symmetric: If a.equals(b) is true, then b.equals(a) should also return true.
        • Transitive: If a.equals(b) and b.equals(c) are both true, then a.equals(c) should also return true.
        • Consistent: Repeated calls to a.equals(b) should always return the same result, unless the object's state changes.
        • Null comparison: a.equals(null) should return false.
    • Common Implementation:
      • A typical custom implementation of equals() compares the relevant fields of two objects to determine equality.
      • If all fields are equal, the objects are considered equal.
  • What's the difference between == and equals()?
    • == (Reference Equality Operator):
      • Purpose: The == operator checks whether two references point to the exact same object in memory (i.e., it checks if they are the same object).
      • Comparison: It compares the memory addresses of two objects, not their content.
      • Use case: == is used for primitive data types (like int, char, etc.) and to check if two object references refer to the same object.
    • equals() (Content/Value Equality Method):
      • Purpose: The equals() method checks whether two objects are logically equivalent, meaning it compares their content or state, not their memory references.
      • Comparison: It compares the actual data (field values) within the objects. This method must be overridden in custom classes to provide meaningful equality based on the content.
      • Use case: equals() is used when you want to compare two objects to see if they are meaningfully equal, based on their contents, rather than whether they are the same object in memory.
  • Are two Strings with the same value ==?
    • In Java, two String objects with the same value might not be considered equal using ==, even though their content is identical. This is because == checks if the two references point to the exact same object in memory, not if their values are the same.
    • However, due to String interning in Java, strings with the same literal value often point to the same memory location, which means == can return true in such cases.
      • For example, the expression "Hello" == "Hello" returns true due to string interning in Java.
  • Do two Integer objects with the same value return true when compared using ==?
    • No, two Integer objects with the same value may not return true when compared using ==, because == compares references, not values. However, due to autoboxing and integer caching, Integer values between -128 and 127 may return true.
  • What's the difference between == and hashCode() in Java?
    • ==: This is a reference comparison operator. It checks whether two references point to the same object in memory (i.e., if they are the exact same instance). For primitive types, == compares their actual values.
      • For example, str1 == str2 checks if str1 and str2 are referring to the same memory location.
    • hashCode(): This method returns an integer value (hash code) that is computed based on the object's internal state. It's used primarily in hash-based collections (like HashMap and HashSet) to determine the object's bucket location. It does not compare object references or values, but it helps in efficiently locating objects in a hash-based collection.
      • Two different objects can have the same hash code, but they are not necessarily the same object or equal. This is called a hash collision.
      • The hashCode() method is often used in conjunction with equals() to check object equality. If two objects are equal according to equals(), they must return the same hash code.

Object Creation and Classloading

  • What are the differences between new and getInstance()?
    • new: Directly creates a new instance of a class.
    • getInstance(): Typically used in design patterns, especially to return a pre-existing or controlled instance, ensuring object creation is managed.
  • How to create immutable objects?
    • An object is considered immutable if its state cannot change after it is constructed. Leveraging immutability is a widely accepted strategy for writing reliable, thread-safe, and simple code.
    • Avoid Setter Methods
      • Do not provide "setter" methods that modify the object's state or the fields it refers to.
    • Make Fields Final and Private
      • Declare all fields as final to ensure their values cannot be changed after initialisation.
      • Use private access modifiers to restrict direct access to fields.
    • Prevent Method Overriding
      • Declare the class as final to prevent inheritance.
      • Use a private constructor and provide factory methods to create instances.
    • Handle Mutable Fields Carefully
      • Avoid exposing or modifying mutable objects referenced by instance fields.
      • Do not provide methods that allow changes to the mutable fields.
      • Never store direct references to external, mutable objects passed to the constructor. Instead:
        • Create and store copies of those objects during construction.
        • Return copies of internal mutable objects in methods instead of exposing the original references.
  • Why is String Immutable?
    • Security
      • Strings are often used to represent sensitive data like network parameters, database URLs, usernames, and passwords.
      • If strings were mutable, malicious code could alter these values, posing security risks.
    • Synchronisation and Concurrency
      • Immutability ensures thread safety by default, as the state of a string cannot change.
      • This eliminates synchronisation issues in multithreaded environments.
    • Caching and Memory Efficiency
      • The Java compiler optimises string usage by interning strings.
      • When two strings have the same value (e.g., a = "test" and b = "test"), they point to the same object in the string pool, reducing memory overhead.
    • Class Loading
      • Strings are used as arguments in class loading (e.g., Class.forName(String className)).
      • If mutable, modifying a string could result in incorrect or malicious classes being loaded.
    • String Pool
      • Java maintains a string pool for managing string literals efficiently.
      • Immutable strings ensure that objects in the pool remain unchanged and can be reused safely.
      • Strings can be added to the pool explicitly using the intern() method, which ensures a single instance for equivalent strings.
    • Conclusion
      • String immutability in Java is a deliberate design choice to enhance security, optimise memory usage, support efficient concurrency, and enable predictable behaviour. Being a reference type aligns with Java's object-oriented paradigm, making strings versatile and memory-efficient.
  • Why is String a Reference Type?
    • Memory Management
      • Strings can be large, and storing them as reference types allows the JVM to manage memory more efficiently by pointing multiple references to the same object when possible.
    • Support for Null
      • As a reference type, a string can represent a nonexistent value using null.
      • This is useful for indicating the absence of a string value.
    • Behavioural Characteristics
      • Reference types allow methods to be invoked directly on the string object, enabling a rich set of operations (e.g., substring(), toUpperCase()).
  • What are the advantages of immutable objects?
    • Thread-Safety
      • Immutable objects are inherently thread-safe, as their state cannot change once created.
        • This eliminates the need for synchronisation or locks, reducing complexity and potential bottlenecks in multithreaded applications.
    • Ease of Use in Concurrency
      • Since the value of immutable objects cannot change, they can be safely shared across multiple threads without coordination.
      • Threads can access the object without fear of interference or inconsistent state.
    • No Synchronisation Overhead
      • Because immutable objects do not require locking, they avoid the performance costs associated with synchronized blocks.
      • This also eliminates contention issues where threads compete to access a single mutable resource.
    • Predictability and Reliability
      • Immutable objects guarantee that their state will remain consistent and predictable throughout their lifecycle.
      • This makes reasoning about program behaviour simpler and reduces the likelihood of bugs related to unexpected changes.
    • Ease of Sharing
      • Immutable objects can be freely shared between different parts of a program or across threads without concerns about unintended modifications.
    • Simplified Design
      • With immutability, developers avoid the complexities of managing state changes, particularly in concurrent or distributed systems.
      • This results in cleaner, more maintainable code.
    • Copy-on-Update
      • When an updated version of an immutable object is needed, a new object is created with the desired changes.
      • This ensures that the original object remains unchanged, preserving its original state for other uses.
    • Summary
      • Immutable objects simplify concurrent programming by eliminating the need for synchronisation, enhancing predictability, and improving performance by avoiding locking bottlenecks.
      • They are especially valuable in multithreaded environments where shared state is a common source of complexity and bugs.
  • How does clone() work?
    • The clone() method in Java is a method of the Object class that is used to create a copy (clone) of an object.
    • Shallow vs Deep Copy:
      • If the object has fields that are references to other objects, those objects are not cloned by default.
      • To make a deep copy, you need to manually clone the objects that the fields reference within the clone() method.
    • Custom Cloning:
      • If your object contains mutable references and you want to ensure deep cloning, you should override the clone() method and clone those references manually.
    • CloneNotSupportedException:
      • If you try to clone an object that does not implement Cloneable, a CloneNotSupportedException is thrown. This is a checked exception, so it must be handled.
  • Can Integer be cast as an Object? If so, any problems?
    • Yes, an Integer can be cast to an Object because Integer is a subclass of Object.
    • However, once cast to an Object, you can no longer directly perform arithmetic operations on it. You would need to cast it back to an Integer (or use the appropriate Integer methods) before performing any arithmetic operations.
  • How does ClassLoader work in Java?
    • In Java, a ClassLoader is a part of the JRE that is responsible for loading classes into memory during the execution of a program. The process of loading a class into memory is known as dynamic loading.
    • Key Concepts of ClassLoader:
      • Role of ClassLoader:
        • A ClassLoader is responsible for loading .class files into the JVM from different sources (e.g., file systems, network, or JAR files).
        • The ClassLoader converts the binary data from the class files into a usable Class object that can be used by the JVM to create instances, invoke methods, and perform other tasks.
      • How ClassLoader Works:
        • When a class is referenced for the first time (either directly or indirectly), the JVM will ask the ClassLoader to load it.
        • The ClassLoader then searches for the class file based on its fully qualified name (package name + class name).
        • Once found, the class is loaded into memory and initialised.
        • If the class is not found, the ClassLoader will throw a ClassNotFoundException.
        • If the class has a runtime error, the ClassLoader will throw a NoClassDefFoundError.
        • If the class file format is invalid or corrupted, the ClassLoader will throw a ClassFormatError.
      • ClassLoader Hierarchy:
        • Java has a hierarchical model for ClassLoaders, where one ClassLoader can delegate the class loading responsibility to its parent ClassLoader. The hierarchy generally follows this order:
          • Bootstrap ClassLoader (the parent of all loaders, loads core JDK classes like java.lang.Object).
          • Extension ClassLoader (loads classes from JDK extensions, e.g., from the lib/ext directory).
          • System/Application ClassLoader (loads classes from the classpath, which includes directories or JAR files specified for a Java application).
      • Custom ClassLoader:
        • You can create a custom ClassLoader by extending the ClassLoader class and overriding the findClass() method.
        • This is typically used for loading classes from non-standard locations, like from a network or a custom file system.
        • Delegation Model:
          • ClassLoaders in Java use a parent delegation model.
          • When a class is requested, the child ClassLoader first delegates the request to its parent ClassLoader.
          • This allows core Java classes to be loaded by the Bootstrap ClassLoader before any custom classes.
          • If the parent ClassLoader cannot load the class, it will attempt to load the class itself.
        • Loading Process:
          • Loading: The bytecode of a class is read from its source (e.g., file, JAR) and brought into memory.
          • Linking: After loading, a class is linked, which includes verification, preparation, and resolution of symbolic references.
          • Initialisation: The class is initialised (static blocks and static fields are initialised).
        • Example: If a class MyClass is referenced, the process can go like this:
          • The Application ClassLoader is asked to load MyClass.
          • It delegates the request to the Extension ClassLoader.
          • The Extension ClassLoader delegates it to the Bootstrap ClassLoader.
          • If no class is found in any of the parent ClassLoaders, the Application ClassLoader searches for the class in the specified classpath.
          • If found, it is loaded into memory.
        • Why ClassLoader is Important:
          • Security: Different ClassLoaders can be used to load classes in a controlled environment, ensuring that only trusted code is executed.
          • Custom Class Loading: Custom ClassLoaders enable scenarios such as plugin systems where classes are loaded dynamically from external sources.
          • Performance: Java ClassLoaders are designed to be efficient, leveraging caching mechanisms to avoid loading the same class multiple times.

Memory Management

  • What are value types and reference types?
    • Value Types (Primitive Types):
      • Includes: boolean, byte, char, short, int, long, float, double.
      • Stores actual data directly in memory.
      • When assigned to another variable, the data is copied, creating an independent copy.
      • Passing a value type to a method passes a copy of the value. The original data cannot be modified by the method.
    • Reference Types:
      • Includes all classes, arrays, and interfaces.
      • Stores a reference (memory address) pointing to the object in memory, not the actual data.
      • When assigned to another reference type, both variables point to the same object in memory.
      • Passing a reference type to a method allows the method to modify the contents of the object, but not the reference itself.
    • Autoboxing and Unboxing:
      • Autoboxing: The automatic conversion of a primitive type to its corresponding wrapper class. (e.g., int to Integer).
      • Unboxing: The automatic conversion of a wrapper class back to its corresponding primitive type. ( e.g., Integer to int).
    • Key Differences:
      • Value types directly store data; reference types store references to data.
      • When assigning a value type, a copy of the data is made. When assigning a reference type, the reference ( memory address) is copied, and both variables point to the same object.
      • Methods using value types cannot modify the original value, whereas methods using reference types can modify the object's content.
  • What is the difference between heap and stack memory in Java? What types of objects are in them?
    • Stack Memory:
      • The stack is used for storing local variables, method calls, and method parameters.
      • The stack operates in a LIFO (Last In, First Out) manner. When a method is called, a new stack frame is created containing the local variables and parameters of that method. When the method returns, the stack frame is popped off the stack and memory is freed.
      • Primitive types (e.g., int, char, etc.) are stored on the stack.
      • The stack is fast and is automatically managed, meaning it is automatically allocated and deallocated as methods are called and returned.
    • Heap Memory:
      • The heap is used for storing objects and arrays (reference types).
      • Objects are dynamically allocated on the heap, and they are accessed through references. When you create an object using the new keyword, the object is placed on the heap, and a reference to that object is stored on the stack.
      • The heap is managed by Garbage Collection (GC), which automatically deallocates objects that are no longer referenced, preventing memory leaks.
      • Reference types (e.g., objects and arrays) are stored on the heap.
      • Unlike the stack, the heap does not follow the LIFO principle and objects remain in memory until they are Garbage Collected.
    • The stack stores primitive types, local variables, and method calls with fast allocation and deallocation, while the heap stores objects and arrays with Garbage Collection managing memory. The stack is limited in size, while the heap can grow as needed but is subject to Garbage Collection to free unused memory.
  • How to avoid a memory leak from a stack?
    • Use Local Variables:
      • Local variables are automatically removed from the stack once the method execution is completed, preventing unnecessary memory usage.
    • Limit Recursion:
      • Excessive recursive calls can lead to stack overflow errors. Be mindful of the recursion depth or refactor to use iteration where possible.
    • Avoid Circular References:
      • Circular references between objects can prevent Garbage Collection, causing memory to be held unintentionally. Use weak references or redesign the structure to avoid such references.
    • Release Resources:
      • Always release resources like file handles, database connections, and network sockets explicitly after usage to prevent memory leaks.
    • Optimise Data Structures:
      • Choose efficient data structures and avoid storing unnecessary data on the stack. For large data, consider using the heap instead.
    • Monitor Stack Usage:
      • Regularly monitor stack usage during the execution of your program, especially in applications with deep recursion or high thread usage, to detect potential stack overflows.
  • What is the difference between memory models of Java 8 and 21?
    • Metaspace vs PermGen: In Java 8, the PermGen space was replaced by Metaspace, which dynamically resizes itself based on the application's demand for class metadata. This avoids the fixed size limitations of PermGen, reducing the likelihood of running out of memory due to class metadata.
    • Java 8 also introduced the ability to tune Metaspace's size with options like -XX:MetaspaceSize and -XX:MaxMetaspaceSize.
    • Garbage Collection Enhancements: Java 21 continues to build on the improved Garbage Collection mechanisms that were introduced in Java 8. With the introduction of ZGC in later versions of Java, the Garbage Collection process became more efficient, offering low-latency Garbage Collection for large heaps, making it suitable for applications that require minimal pauses.
    • Stack and Heap: The underlying concepts of heap and stack memory management remain similar across Java 8 and Java 21, but the ability to fine-tune and dynamically adjust memory usage has been enhanced with newer JVM options.
  • What are the differences among Strong, Weak, Soft, and Phantom references?
    • Strong references are the default and are used for regular object management.
    • Weak references are used for objects that should be garbage collected once they are no longer in use (e.g., caches).
    • Soft references are typically used for memory-sensitive caches that should be reclaimed when memory is needed.
    • Phantom references are used for cleanup actions after an object is collected.

Garbage Collection

  • What is JVM?
    • The Java Virtual Machine (JVM) is a virtual machine that enables a computer to run Java programs. It serves as the runtime environment for Java bytecode, providing an abstraction layer between the Java code and the hardware it runs on.
    • Execution of Bytecode:
      • Java source code is compiled into bytecode by the Java compiler. The JVM then interprets or compiles the bytecode into machine code that the underlying operating system can execute.
    • Memory Management:
      • The JVM manages memory allocation for Java programs, handling the stack, heap, and Garbage Collection to ensure efficient memory use.
    • Platform Independence:
      • Java programs are platform-independent because the JVM abstracts the underlying operating system and hardware. As long as the JVM is installed on a machine, Java applications can run on that machine without modification.
    • Garbage Collection:
      • The JVM includes an automatic Garbage Collector that reclaims memory from objects that are no longer in use, reducing the need for manual memory management.
    • JVM Architecture:
      • The JVM consists of several key components, including the Class Loader, Bytecode Verifier, Interpreter, Just-In-Time (JIT) Compiler, and Garbage Collector.
      • Class Loader: Loads class files into memory.
      • Bytecode Verifier: Ensures the integrity of the bytecode before execution.
      • Interpreter: Executes bytecode line by line.
      • JIT Compiler: Compiles bytecode into native machine code at runtime to improve performance.
      • Garbage Collector: Automatically manages memory by reclaiming unused objects.
    • Security:
      • The JVM provides a secure execution environment by validating bytecode and preventing harmful operations like unauthorised file access or network connections.
  • What is Garbage Collection?
    • In Java, Garbage Collection (GC) is the process by which the JVM automatically reclaims memory that is no longer in use by the application. The Garbage Collector identifies and removes objects that are no longer referenced or needed, freeing up memory for future use. This process helps avoid memory leaks and ensures efficient memory management.
    • Managed vs Unmanaged Resources:
      • Managed Resources: These are objects that the Garbage Collector can handle. Once they are no longer referenced, the Garbage Collector reclaims the memory occupied by these objects.
      • Unmanaged Resources: Some resources, such as file handlers, window handlers, network sockets, or database connections, are not managed by the Garbage Collector. These require explicit management (e.g., closing the resource after use) since the Garbage Collector does not have the logic to clean them up.
    • Summary:
      • Garbage Collection in Java helps to automatically manage memory by reclaiming unused objects.
      • There are several types of Garbage Collectors (e.g., Serial, Parallel, CMS, G1, ZGC, Shenandoah) designed for different performance needs.
      • The Garbage Collector manages "managed resources" (objects), while "unmanaged resources" (such as file handles or network sockets) need explicit handling from the programmer.
  • How does the Garbage Collector work in Java?
    • In Java, Garbage Collection (GC) is the automatic process of reclaiming memory by deleting objects that are no longer in use or reachable. It is managed by the JVM, which helps prevent memory leaks and ensures that the application does not run out of memory.
    • Memory Management:
      • Heap Memory: Java objects are stored in the heap memory. The heap is divided into two areas:
        • Young Generation (new objects are created here).
        • Old Generation (objects that have survived multiple GC cycles are promoted here).
      • Stack Memory: Local variables and method calls are stored in the stack memory and are not part of the GC process.
    • Mark-and-Sweep Algorithm:
      • Marking Phase: The Garbage Collector identifies all the objects that are still reachable or referenced by active threads, static fields, and local variables. These objects are "marked" as live.
      • Sweeping Phase: Once the reachable objects are marked, the GC will remove the objects that are not marked, meaning they are no longer reachable and can be safely deallocated. This process frees up memory.
    • Generational Garbage Collection:
      • Java's Garbage Collector is generational, which means it divides the heap into different regions based on the age of the objects. This helps improve performance by focusing GC efforts on young, newly created objects, which are more likely to become unreachable quickly.
      • Young Generation: Includes objects created recently. It consists of:
        • Eden Space: Where new objects are allocated.
        • Survivor Space (S0 and S1): Where objects are moved after surviving GC cycles in Eden.
      • Old Generation: Objects that have survived several Garbage Collection cycles are promoted to the old generation.
      • Permanent Generation (Metaspace in newer versions): Stores metadata related to classes and methods, such as class definitions, method definitions, and method references. (Note: In JDK 8, PermGen was replaced with Metaspace.)
    • GC Triggers:
      • Heap Memory is Full: The JVM will initiate a GC cycle when the heap memory is getting full or when an object cannot be allocated in the heap.
      • System Calls or Memory Constraints: The GC can be triggered by system signals or when the JVM decides memory reclamation is necessary.
    • Garbage Collection Pauses:
      • Garbage Collection introduces pauses to the application while it performs its work. These pauses can vary in length, depending on the size of the heap, the type of collector used, and the number of objects to be reclaimed.
      • Stop-the-world Events: During GC, application threads may be stopped while the collector works (known as stop-the-world events).
    • Finalisation and finalize() Method:
      • Java allows an object to perform clean-up operations before it is collected using the finalize() method. However, relying on finalize() is generally discouraged as it is unpredictable and may delay object deallocation.
    • Garbage Collection Process Overview:
      • Young Generation Collection:
        • Most objects are created in the Young Generation. When this space fills up, a minor GC occurs. Objects that survive minor GCs are promoted to the Old Generation.
      • Old Generation Collection:
        • When the Old Generation fills up, a major GC (or full GC) occurs. This is typically more expensive because it involves scanning all generations, including the Old Generation.
      • Garbage Collector Threads:
        • The JVM runs several Garbage Collector threads to handle these tasks concurrently, optimising pause times and throughput.
    • Manual Memory Management:
      • Java does not provide explicit memory management like C or C++ (e.g., free() or delete), but developers can influence Garbage Collection:
      • System.gc(): A suggestion to the JVM to initiate Garbage Collection, though it is not guaranteed to be executed immediately.
      • Object References: To help Garbage Collection, you can nullify object references explicitly when you no longer need them.
    • Conclusion:
      • The Garbage Collector in Java helps automate memory management, ensuring that unused objects are cleaned up without developer intervention. However, understanding how it works can help optimise performance, avoid memory leaks, and reduce the impact of GC pauses.
  • How many types of Garbage Collectors are there? What is G1? What is ZGC?
    • Serial Garbage Collector:
      • A simple, single-threaded collector suitable for single-core machines or applications with small heaps.
      • All Garbage Collection tasks are handled by one thread, making it less efficient for large, multithreaded applications.
    • Parallel Garbage Collector (Throughput Collector):
      • Uses multiple threads to perform Garbage Collection, improving throughput for multicore systems.
      • Aimed at applications with large heaps and a need for high throughput, where the pause times of GC are less critical.
    • Concurrent Mark-Sweep (CMS) Collector:
      • Focuses on minimising Garbage Collection pause times by performing most of the work concurrently with the application threads.
      • Suitable for applications that require low-latency and real-time responsiveness.
    • G1 Garbage Collector:
      • Designed for large heap sizes, with a focus on minimising both pause times and GC overhead.
      • Divides the heap into regions and collects garbage incrementally, making it a good option for applications with large datasets or requirements for predictable pause times.
    • ZGC (Z Garbage Collector):
      • A low-latency Garbage Collector designed for large heap sizes, optimised for applications with strict latency requirements.
      • Uses a concurrent approach to Garbage Collection to ensure minimal pauses, even for large heaps (up to several terabytes).
    • Shenandoah Garbage Collector:
      • A low-latency Garbage Collector that aims to reduce GC pause times by performing most of the work concurrently with application threads.
      • Similar to ZGC, it is designed for large heaps and low-latency applications.
  • What are GC roots, and what are they used for?
    • In Java, GC roots are special references used by the Garbage Collector (GC) to identify live objects in the heap. The GC starts its object reachability analysis from these roots. Any object directly or indirectly referenced by a GC root is considered "reachable" and thus not eligible for Garbage Collection.
    • GC roots are the starting points for the mark phase of the Garbage Collection process, during which the GC traverses the object graph to identify which objects are still in use.
  • Explain the different types of GC roots in Java.
    • References from Active Threads
      • Objects that are referenced directly by the stack frames of active threads are treated as GC roots.
      • Examples include:
        • Local variables
        • Input/output parameters
        • Temporary variables in method calls
    • Static Fields
      • Objects referenced by static fields of loaded classes are considered GC roots.
    • Class References
      • Class objects loaded by the class loader (and not yet unloaded) are GC roots. These class objects often reference static fields or constants that themselves act as GC roots.
    • JNI References
      • Objects referenced by native code (e.g., through Java Native Interface or JNI) are treated as GC roots.
      • These references are maintained outside the JVM, and the Garbage Collector considers them reachable.
    • Finaliser Queue
      • Objects pending finalisation are temporarily considered GC roots. These are objects whose finalize() methods are waiting to be executed by the finaliser thread.
    • Monitor Locks
      • Objects used as monitors (e.g., objects on which a synchronized block is applied) are considered GC roots while the monitor is in use.
    • References from Java Native Threads
      • Objects referenced by native thread stacks (e.g., threads started using native libraries) are treated as GC roots.

Concurrency and Multithreading

  • What is multithreading?
    • Multithreading is a programming concept that allows a CPU to execute multiple threads concurrently within a single process. A thread is the smallest unit of execution within a program. Multithreading enables a program to perform multiple operations or tasks at the same time, which can improve the performance of applications, especially on multicore processors.
    • Key Points:
      • Thread: A thread is a lightweight process that can run independently and share resources (such as memory) with other threads in the same process.
      • Concurrency: In a multithreaded program, tasks may appear to be executed simultaneously, though in reality, they are interleaved on a single-core processor, or truly run in parallel on multiple cores.
      • Parallelism: When multiple threads are executed at the same time, on different CPU cores, it is called parallelism. This is a type of multithreading that enhances performance, especially for CPU-bound tasks.
    • Benefits of Multithreading:
      • Improved application performance: By utilising multiple cores, tasks can be processed simultaneously, leading to better performance, especially for CPU-bound tasks.
      • Responsiveness: In GUI applications or real-time systems, multithreading helps maintain responsiveness by offloading time-consuming tasks to background threads, allowing the main thread to remain responsive to user input.
      • Efficient resource use: Multithreading allows better utilisation of CPU resources, leading to more efficient processing, especially in I/O-bound applications.
  • What is a synchronized modifier?
    • Synchronised Modifier in Java ensures that only one thread can access a particular method or block of code at any given time, making it thread-safe.
    • Synchronised Method: When a method is declared with the synchronized keyword, it allows only one thread to execute that method at a time for a given instance of the class.
      • If multiple threads attempt to execute the synchronised method concurrently, they will be queued and executed sequentially.
    • Synchronised Block: A specific section of code can be synchronised using a synchronized block instead of synchronising the entire method. This provides more granular control over which parts of code should be synchronised.
      • You can specify which object's monitor (lock) will be used to synchronise the block.
    • Thread Safety: The main purpose of using synchronized is to prevent data inconsistencies when multiple threads access shared resources. It ensures that only one thread at a time can modify the shared resource.
  • What are the problems of threading and synchronisation?
    • Race Conditions
      • Description: Occurs when multiple threads access shared resources simultaneously, leading to unpredictable results or data corruption.
      • Cause: When threads read and write shared variables without proper synchronisation.
      • Solution: Proper synchronisation using synchronised methods, blocks, or locks to ensure mutual exclusion.
    • Deadlock
      • Description: A situation where two or more threads are blocked forever, waiting for each other to release resources.
      • Cause: Threads hold some resources while waiting for others, forming a cycle of dependencies.
      • Solution: Avoid nested locks, implement lock timeouts, or use higher-level concurrency mechanisms like ReentrantLock.
    • Livelock
      • Description: Similar to deadlock, but the threads involved are not blocked—they are active, but unable to make progress because they keep changing states in response to each other.
      • Cause: Threads continually attempt to resolve conflicts but do not make progress, often because of improper coordination.
      • Solution: Introduce delays, improve resource allocation logic, or use more efficient locking strategies.
    • Thread Interference
      • Description: Occurs when multiple threads modify shared data at the same time without synchronisation, leading to inconsistencies.
      • Cause: Threads interfere with each other when accessing shared variables.
      • Solution: Use synchronisation (e.g., synchronized keyword), locks, or atomic variables to prevent this.
    • Starvation
      • Description: A thread is perpetually denied access to resources because other threads are constantly executing.
      • Cause: Poor thread scheduling or locking strategies, leading to some threads being consistently preempted.
      • Solution: Use fair locks (ReentrantLock with fairness policy) to ensure all threads get a chance to execute.
    • Priority Inversion
      • Description: A lower-priority thread holds a resource needed by a higher-priority thread, which leads to the higher-priority thread being blocked.
      • Cause: Improper handling of thread priorities in the system.
      • Solution: Priority inheritance protocols, or using a proper threading framework with adequate resource management.
    • Thread Overhead
      • Description: Creating and managing threads can be resource-intensive, especially when the number of threads exceeds the available system resources (CPU, memory).
      • Cause: Creating too many threads or overuse of threads in resource-constrained environments.
      • Solution: Use thread pools (e.g., ExecutorService) to manage threads efficiently and avoid excessive context switching.
    • Context Switching Overhead
      • Description: When the CPU switches between threads, it incurs a cost due to saving and loading thread states.
      • Cause: Frequent context switching can degrade performance.
      • Solution: Reduce unnecessary thread creation and switching by managing threads efficiently with thread pools.
    • Complexity in Debugging
      • Description: Concurrency bugs are often non-deterministic, meaning they may only appear under certain conditions or timing, making them difficult to reproduce and debug.
      • Cause: The non-linear execution of threads leads to unpredictable behaviour.
      • Solution: Use proper logging, debugging tools, and thread-safe design patterns (e.g., immutability, thread pools) to simplify debugging.
    • Synchronisation Overhead
      • Description: Synchronisation mechanisms such as synchronized blocks or locks can introduce performance bottlenecks due to context switching and lock contention.
      • Cause: Lock contention, where multiple threads try to acquire the same lock simultaneously.
      • Solution: Minimise the scope of synchronised blocks, use lock-free data structures, or adopt fine-grained locking strategies.
    • Memory Consistency Errors
      • Description: Occurs when one thread modifies shared data but another thread does not see the updated value due to improper synchronisation.
      • Cause: Memory visibility issues, where threads don't have a consistent view of shared memory.
      • Solution: Use the volatile keyword, synchronized blocks, or higher-level concurrency mechanisms (e.g., AtomicInteger, ReentrantLock) to ensure visibility of changes across threads.
  • What is volatile? Does volatile guarantee synchronisation?
    • Definition: The volatile keyword ensures that a variable's value is always read from and written to main memory, bypassing local CPU caches. It makes the value visible across all threads, ensuring visibility and preventing threads from working with stale data.
    • Key Features:
      • Visibility Guarantee: Ensures that changes to the variable are visible to all threads immediately.
      • No Caching: Prevents threads from caching the value of a volatile variable, ensuring that all threads access the most recent value from main memory.
      • No Atomicity Guarantee: Unlike ThreadLocal, volatile does not guarantee atomicity or synchronisation for compound operations (e.g., incrementing a counter).
    • Usage:
      • Typically used when you need to share a flag or state between threads (e.g., a termination flag).
      • Ensures the latest value of a variable is visible to all threads without needing synchronisation.
    • Advantages:
      • Simple and lightweight mechanism for ensuring visibility between threads.
      • No synchronisation required, which reduces overhead in scenarios like flags or status indicators.
    • Disadvantages:
      • Does not provide atomicity; operations like increments or checks are not thread-safe on volatile variables.
      • Does not solve the issue of thread interference or consistency for complex operations.
  • What is ThreadLocal?
    • Definition: ThreadLocal provides thread-local storage for variables. Each thread accessing a ThreadLocal variable has its own independent copy of the variable. These copies are not shared between threads.
    • Key Features:
      • Thread-Specific Data: Each thread has its own independent copy of the ThreadLocal variable.
      • No Sharing: Values stored in ThreadLocal are not visible to other threads. Each thread sees its own copy.
      • Automatic Cleanup: The ThreadLocal class handles cleanup of resources when the thread terminates.
    • Usage:
      • Useful when you need thread-specific data, such as session-specific variables, database connections, or user-specific information.
      • It's often used in situations where creating a new object for each thread is more efficient than synchronising access to a shared object.
    • Advantages:
      • Thread-specific data without synchronisation.
      • Helps avoid contention between threads for shared resources.
      • Automatically managed, no need for explicit synchronisation.
    • Disadvantages:
      • Not shared across threads, so cannot be used for sharing state between threads.
      • Can lead to memory leaks if not handled properly (especially in thread pools).
  • How does ThreadLocal differ from volatile?
    • ThreadLocal provides each thread with its own independent copy of a variable, while volatile ensures that a variable's value is consistently visible across all threads without caching, but does not provide thread-local storage.
    • Use ThreadLocal if:
      • You need thread-specific variables and want to avoid synchronisation.
      • You don't need to share data between threads, but instead, each thread should maintain its own separate instance.
    • Use volatile if:
      • You need a simple mechanism for ensuring that a variable is visible to all threads.
      • You don't need complex synchronisation, just visibility between threads (e.g., flags, status updates).
  • How can you declare a Thread?
    • Extending the Thread Class: For simple thread implementations without requiring reusable Runnable logic.
    • Implementing the Runnable Interface: Enables code reuse and better separation of thread execution logic.
    • Using Lambda Expressions: Preferred for short and simple thread logic.
    • Using Java 21 Virtual Threads: High-concurrency applications like servers or event-driven systems.
  • What are the differences between Thread and Runnable?
    • Prefer Runnable for Thread Logic
      • Implementing Runnable is generally recommended as it offers better flexibility, promotes code reuse, and adheres to the single-responsibility principle.
    • Use Thread for Direct Extension
      • Use the Thread class only when extending it provides specific advantages or when the thread logic is directly tied to the thread behaviour.
    • Modern Alternatives
      • With Java 21, virtual threads offer a lightweight and resource-efficient approach to concurrency, which can work with both Thread and Runnable.
  • What is a deadlock?
    • A deadlock occurs in multithreaded programming when two or more threads are waiting for each other to release resources, resulting in a state where none of the threads can proceed. Deadlocks typically happen when threads hold some resources and simultaneously request others held by another thread.
  • How can you avoid a deadlock?
    • Lock Ordering: Always acquire locks in a consistent global order across threads.
    • Try-and-Timeout Locks: Use java.util.concurrent.locks such as ReentrantLock with a timeout to avoid indefinite blocking.
    • Minimise Lock Scope: Keep the critical section (code inside a synchronized block) as short as possible to reduce contention.
    • Avoid Nested Locks: Limit or eliminate nested locks where one lock leads to acquiring another lock.
    • Use Higher-Level Concurrency Utilities: Prefer high-level abstractions from java.util.concurrent, such as ExecutorService, ConcurrentHashMap, or BlockingQueue, which handle concurrency without explicit locks.
    • Deadlock Detection: Tools like thread dump analysis (e.g., jstack or profiling tools) can help identify and debug deadlocks.
  • What is a race condition?
    • A race condition occurs in multithreaded programming when two or more threads access shared data simultaneously, and at least one of the threads modifies the data. The outcome of the execution depends on the sequence or timing of thread execution, making the program's behaviour unpredictable and potentially incorrect.
    • Prevent race conditions by synchronising access to shared data, using atomic operations, or leveraging thread-local storage or immutability.
  • What are async and await?
    • async: Marks a method as asynchronous, allowing it to execute code without blocking the current thread.
    • await: Pauses execution in an async method until the awaited task is complete, then resumes execution.
  • Why use asynchronous programming?
    • Improved Responsiveness: Keeps the application UI or server responsive during long-running operations.
    • Concurrency: Enables multiple tasks to run concurrently without blocking threads.
    • Efficiency: Makes better use of system resources, such as threads and CPU cycles.
    • Non-Blocking I/O: Ideal for I/O-bound operations like file reading, database queries, or API calls.
  • What are the disadvantages of asynchronous programming?
    • Complexity: Harder to read, write, and debug due to non-linear flow.
    • Error Handling: Managing exceptions is more intricate compared to synchronous code.
    • Deadlocks: Misuse of await or improper synchronisation can lead to deadlocks.
    • Callback Hell: Excessive nesting of asynchronous operations can reduce code clarity.
    • Overhead: Can introduce additional overhead for task creation and scheduling.
  • What is AtomicInteger?
    • AtomicInteger: A thread-safe class in Java (java.util.concurrent.atomic) that provides atomic operations on an int value without requiring synchronisation.
    • Commonly used in multithreaded environments for counter increments, updates, or other atomic operations.
  • How does AtomicInteger work?
    • Lock-Free Mechanism: Uses a low-level hardware-based atomic operation called CAS (Compare-And-Swap) to ensure thread safety without blocking.
    • Provides methods like:
      • get(): Fetches the current value.
      • set(value): Sets a new value.
      • incrementAndGet(): Atomically increments the value by 1.
      • compareAndSet(expected, newValue): Updates the value if it matches the expected value.
  • What is CAS (Compare-And-Swap)?
    • Definition: A hardware-supported atomic instruction used to update a variable if its current value matches an expected value.
    • How it works:
      • Compare: The current value is compared to an expected value.
      • Swap: If the comparison succeeds, the value is updated with a new value in a single atomic step.
      • Fail: If the comparison fails, no update is performed, and the operation can be retried.
    • Advantages:
      • Lock-Free: Avoids the overhead of traditional synchronisation mechanisms like synchronized blocks or ReentrantLock.
      • Scalability: Performs better in high-concurrency environments.
      • Non-blocking: Threads are not blocked, reducing contention and context-switching overhead.
    • Limitations:
      • Busy Spinning: Repeatedly retries if updates fail due to contention, leading to CPU wastage.
      • ABA Problem: The value can change back to the original (A -> B -> A) without CAS detecting it.
      • Complexity: Not suitable for complex updates involving multiple variables.
  • How would you synchronise access to a variable in the following scenarios:
    • Single writer and multiple readers?
      • Best Solution: Use a ReentrantReadWriteLock.
        • The writer acquires the write lock, ensuring exclusive access.
        • Readers acquire the read lock, allowing concurrent access by multiple readers.
      • Alternative Solution: Use volatile.
        • Ensures visibility of changes to all threads but doesn't guarantee atomicity, making it suitable only if writes are infrequent.
    • Multiple writers and multiple readers?
      • Best Solution: Use a ReentrantReadWriteLock.
        • Ensures exclusive access for writers using the write lock and concurrent access for readers with the read lock.
      • Alternative Solutions:
        • Use synchronized blocks or methods to protect both read and write operations.
        • Use atomic variables (e.g., AtomicReference, AtomicInteger) for single-value updates.
        • Use a concurrent collection like ConcurrentHashMap or CopyOnWriteArrayList for complex scenarios.
    • Multiple writers and a single reader?
      • Best Solution: Use synchronisation (synchronized blocks/methods) or a ReentrantLock.
        • Ensures mutual exclusion for writers while guaranteeing thread-safe reads.
        • The single reader doesn't need special handling if synchronisation ensures memory consistency.
      • Alternative Solution: Use atomic variables for simple updates.
        • E.g., AtomicInteger, AtomicReference.
  • What are I/O-bound tasks vs CPU-bound tasks?
    • I/O-Bound Tasks:
      • I/O-bound tasks are tasks that spend most of their time waiting for input/output operations to complete, such as reading from or writing to files, network communication, or database queries.
      • These tasks are typically limited by the speed of the I/O devices (e.g., disk, network), rather than by the CPU.
      • Examples of I/O-bound tasks include web scraping, downloading files, processing user input, and handling network requests.
      • I/O-bound tasks are generally well-suited for asynchronous programming or parallelism, as they can benefit from concurrent execution while waiting for I/O operations to complete.
    • CPU-Bound Tasks:
      • CPU-bound tasks are tasks that require significant computation or processing power and spend most of their time performing computations rather than waiting for I/O operations.
      • These tasks are limited by the CPU's processing capacity and often involve complex mathematical calculations, data processing, or algorithmic operations.
      • Examples of CPU-bound tasks include numerical simulations, cryptographic operations, image processing, and video encoding.
      • CPU-bound tasks can benefit from parallelism, especially multiprocessing, to leverage multiple CPU cores for concurrent execution and achieve faster computation times.

Concurrency vs Parallelism

  • Concurrency
    • Definition: The ability of a system to handle multiple tasks or processes simultaneously, where these tasks can start, run, and complete in overlapping time periods.
    • Execution:
      • Tasks may appear to run simultaneously but are often interleaved on a single-core processor.
      • On multi-core processors, tasks can execute concurrently.
    • Techniques Used:
      • Multitasking
      • Multithreading
      • Asynchronous programming
    • Goal: Improve system responsiveness and efficiency by managing multiple tasks at once, even if they are not running simultaneously.
    • Analogy: Multiple people working on different tasks in the same room, taking turns using a single tool.
  • Parallelism
    • Definition: The simultaneous execution of multiple tasks or processes to improve performance by utilising multiple computational resources.
    • Execution:
      • Tasks are explicitly divided into smaller subtasks, which are executed concurrently on separate CPU cores or processors.
      • All subtasks run truly simultaneously.
    • Techniques Used:
      • Parallel processing
      • Parallel algorithms
      • Parallel computing frameworks
    • Goal: Achieve faster task execution by distributing workloads across multiple processing units.
    • Analogy: Multiple people working on different tasks in the same room, each with their own tools.
Feature Concurrency Parallelism
Task Execution Tasks overlap in time (may or may not run simultaneously) Tasks run simultaneously
System Capability Works on single-core or multi-core systems Requires multi-core or distributed systems
Primary Objective Responsiveness and efficiency Speed and performance
Example Multithreading in Java Matrix multiplication using GPUs

References:

Data Structures and Algorithms

HashMap

  • How does HashMap work?
    • A HashMap is implemented using an array of buckets, where each bucket can hold multiple entries.
    • Entries are stored as key-value pairs (Map.Entry<K, V>).
    • Hashing uses both hashCode() and equals() to ensure correct key placement and retrieval.
    • Proper implementation of hashCode() and equals() in custom keys is essential to prevent issues like incorrect bucket placement or retrieval.
    • Resizing and rehashing are costly operations, so the initial capacity and load factor should be carefully chosen for performance-critical applications.
  • How to implement a HashMap?
    • Basic Structure:
      • Use an array of linked lists (buckets) to handle collisions.
      • Each element (key-value pair) is stored in a linked list at a particular index (bucket) of the array.
    • Steps to Implement:
      • Create an array of LinkedList or Bucket objects, each bucket will hold key-value pairs.
      • Hash function: Implement a hash function to compute the index for each key. This ensures that each key is mapped to a unique bucket based on its hash value.
      • Collision handling: If two keys have the same hash value (i.e., hash collision), store them in the same bucket using a linked list.
      • Insert operation: To insert a key-value pair, compute the index using the hash function, then insert it into the corresponding bucket.
      • Search operation: To retrieve a value, compute the index using the hash function and look up the key in the linked list at that index.
      • Delete operation: To remove a key, compute the index and remove the key-value pair from the linked list at that index.
    • Examples:
  • What are the differences between HashMap and Hashtable?
    • Thread-safety:
      • HashMap: Not thread-safe. Multiple threads can access and modify it concurrently without synchronisation.
      • Hashtable: Thread-safe. It synchronises methods to ensure thread safety, which can lead to performance overhead.
    • Null values:
      • HashMap: Allows one null key and multiple null values.
      • Hashtable: Does not allow null keys or null values. Attempting to add null will throw a NullPointerException.
    • Performance:
      • HashMap: Typically faster than Hashtable because it is not synchronized and allows concurrent access without locking.
      • Hashtable: Slower than HashMap due to the synchronisation mechanism that ensures thread safety.
    • Iteration:
      • HashMap: Iterators are fail-fast, meaning they throw ConcurrentModificationException if the map is modified while iterating.
      • Hashtable: Enumerations are used for iteration, which are not fail-fast.
    • Legacy:
      • HashMap: Part of the Java Collections Framework introduced in Java 1.2.
      • Hashtable: Part of the original version of Java and considered a legacy class since Java 1.2.
    • Usage:
      • HashMap: Preferred for most modern applications, especially when thread safety is not a concern or is handled separately.
      • Hashtable: Generally avoided in modern code due to performance concerns and the availability of more efficient alternatives.
  • How are hashCode() and equals() methods related to HashMap/HashSet?
    • hashCode():
      • Determines the bucket location for storing or retrieving elements in HashMap and HashSet.
      • If two objects have the same hashCode, they are placed in the same bucket (collision).
    • equals():
      • Ensures logical equality between objects within a bucket.
      • Used to identify the correct element during retrieval or when checking for duplicates.
  • What happens when you call a get() method on a HashMap/HashSet?
    • The hash code of the key determines the bucket index.
    • The bucket is searched for a matching key (via equals() comparison).
  • What happens when you call a put() method on a HashMap/HashSet?
    • The key's hash code is used to compute the bucket index.
    • If the key does not already exist, a new entry is added to the bucket.
    • If the key exists, the value is replaced.
  • How to return keys of Map in order?
    • LinkedHashMap:
      • Maintains the insertion order of keys.
      • When iterating over the keys, they will be returned in the order they were inserted into the map.
    • TreeMap:
      • Sorts keys according to their natural order (if the keys are Comparable) or by a specified comparator.
      • If you need keys to be returned in sorted order, use a TreeMap.

HashSet and TreeSet

  • Apart from HashSet, what are other Set implementations?
    • TreeSet:
      • Implements NavigableSet (extends SortedSet).
      • Stores elements in a sorted order, either using their natural ordering or a provided comparator.
      • Offers logarithmic time complexity for operations like add, remove, and contains.
      • Does not allow null elements.
    • LinkedHashSet:
      • Extends HashSet and implements Set.
      • Maintains the insertion order of elements.
      • Offers constant-time performance for add, remove, and contains operations.
      • Allows null elements.
    • EnumSet:
      • A specialised Set implementation for enum types.
      • Offers very high performance due to its internal bit vector representation.
      • Only works with enum types and does not allow null elements.
    • CopyOnWriteArraySet:
      • Implements Set and is part of the java.util.concurrent package.
      • A thread-safe variant of Set where all mutative operations (add, remove, etc.) create a new copy of the underlying array.
      • Best suited for scenarios where reads vastly outnumber writes.
  • What is TreeSet? When to use it?
    • It stores elements in a sorted order, either using their natural ordering (if they implement Comparable) or using a provided Comparator.
    • TreeSet is backed by a Red-Black Tree data structure, which ensures that the elements are sorted and allows for efficient operations.
    • Need for sorted data: Use TreeSet when you need to maintain a collection of elements in sorted order and want to take advantage of the set property (no duplicates).
    • Efficient search: If you require efficient searching, as TreeSet offers fast lookups (O(log n)) due to the tree structure.
    • Range queries: When you need to perform range-based operations (e.g., finding all elements between two values), TreeSet supports methods like subSet(), headSet(), and tailSet(), which allow for efficient range queries.
    • Custom sorting: Use TreeSet when you want to sort elements in a custom order using a comparator.

LinkedList and ArrayList

  • What are the differences between LinkedList and ArrayList?
    • Underlying Data Structure:
      • LinkedList: Uses a doubly linked list as its underlying data structure.
      • ArrayList: Uses a dynamic array (resizable array) as its underlying data structure.
    • Performance:
      • LinkedList:
        • Faster for insertion and deletion at the beginning and middle (O(1) if the node is already found).
        • Slower for random access (O(n)) because it requires traversing the list to find the element.
      • ArrayList:
        • Faster for random access (O(1)) due to direct indexing.
        • Slower for insertion and deletion (O(n)) when elements are added or removed, as elements need to be shifted.
    • Memory Consumption:
      • LinkedList: Each node in the list has extra memory for storing pointers (next and previous node), so it has a higher memory overhead.
      • ArrayList: Stores elements in contiguous memory locations, which leads to lower memory consumption compared to LinkedList.
    • Resizing:
      • LinkedList: Does not require resizing because elements are linked together and do not rely on a fixed array size.
      • ArrayList: May require resizing (reallocating a larger array) when the array grows beyond its capacity, which can be an expensive operation.
    • Thread-Safety:
      • Both LinkedList and ArrayList are not thread-safe by default. If thread safety is needed, external synchronisation must be applied.
    • Use Cases:
      • LinkedList: Ideal when you expect many insertions and deletions (especially at the beginning or middle) and don't need fast random access.
      • ArrayList: Ideal when you need fast random access and fewer insertions or deletions.
    • Iteration:
      • LinkedList: Iterating through a LinkedList may be slower than ArrayList, as it involves traversing through each node.
      • ArrayList: Iteration is faster as elements are stored in contiguous memory locations.
    • Performance in Different Operations:
      • LinkedList:
        • Access by index: O(n)
        • Insert/Delete at the beginning: O(1)
        • Insert/Delete at the end: O(1)
        • Insert/Delete in the middle: O(n)
      • ArrayList:
        • Access by index: O(1)
        • Insert/Delete at the beginning: O(n)
        • Insert/Delete at the end: O(1)
        • Insert/Delete in the middle: O(n)
    • Summary:
      • ArrayList: Best for fast random access, but insertion and deletion are slower.
      • LinkedList: Best for frequent insertions and deletions, but random access is slower.
    • Reference:
  • What is the difference between a single and a double linked list?
    • Single Linked List
      • In a single linked list, each node contains a reference (or pointer) to the next node in the sequence.
      • The nodes are connected unidirectionally, meaning each node points to the next node in the list.
      • Traversal in a single linked list is only possible in one direction, typically from the head (the first node) to the tail (the last node).
    • Double Linked List
      • In a double linked list, each node contains two references: one to the next node and one to the previous node in the sequence.
      • The nodes are connected bidirectionally, meaning each node points both to the next node and the previous node.
      • Traversal in a double linked list is possible in both forward and backward directions, allowing efficient traversal in either direction.
    • In summary, while single linked lists allow traversal in one direction only, double linked lists enable traversal in both forward and backward directions by maintaining additional pointers to the previous nodes. This bidirectional traversal capability comes at the cost of increased memory overhead due to the extra pointers stored in each node.

Stack

  • How to implement a Stack?
    • A Stack is a collection that follows the Last In, First Out (LIFO) principle, where the last element added to the stack is the first one to be removed.
    • Using ArrayList:
      • ArrayList is used to store the elements of the stack.
      • push() adds an element to the end (top) of the stack.
      • pop() removes the element from the end (top) and returns it.
      • peek() returns the top element without removing it.
    • Using an Array:
      • The array is used to store elements in the stack, with top tracking the index of the last added element.
      • When an element is pushed, it is added to the top + 1 position.
      • When an element is popped, it is removed from the top position and the top index is decremented.
      • This implementation requires a fixed size for the stack (capacity), and it will throw an exception if the stack exceeds the capacity.
    • OpenJDK: Stack
    • Princeton: Bags, Queues, and Stacks

Concurrent Data Structures

  • What's the difference among ConcurrentHashMap, HashMap, and SynchronizedMap?
    • Use ConcurrentHashMap when you need high concurrency with thread safety and reduced contention. It's ideal for multithreaded environments.
    • Use HashMap when you don't need thread safety or when you provide your own synchronisation.
    • Use SynchronizedMap when you need to make a HashMap thread-safe with external synchronisation but don't require high concurrency.
  • What's CopyOnWriteArrayList?
    • CopyOnWriteArrayList is a thread-safe variant of ArrayList. It is part of the java.util.concurrent package.
    • This class implements the List interface and provides thread-safe operations by copying the underlying array whenever it is modified.
    • Read-heavy scenarios: Because reads do not require synchronisation, it is particularly useful when read operations vastly outnumber write operations.
    • Expensive writes: Write operations (e.g., add, remove, etc.) are costly because the array has to be copied on each modification.
    • CopyOnWriteArrayList is best suited for applications where read operations are frequent and write operations are rare. It provides thread safety by copying the array on each modification, making it suitable for concurrent environments where reading the list is more common than modifying it.

Sorted Data Structures

  • Any data structures with built-in sorting?
    • Arrays:
      • Sorting: Can be sorted using algorithms like QuickSort, MergeSort, or built-in methods like Arrays.sort().
      • Search: Binary search is possible on a sorted array (O(log n)).
    • ArrayList:
      • Sorting: Can be sorted using Collections.sort() or List.sort().
      • Search: Binary search is possible if the list is sorted.
    • PriorityQueue:
      • Insertion/Enqueue: O(log n)
      • Deletion/Dequeue: O(log n)
      • Peek: O(1) (gives the highest or lowest element depending on the order).
      • Search: O(n) (not directly supported but possible with iteration).
    • TreeSet:
      • Structure: A sorted set backed by a Red-Black Tree (or other balanced tree structures).
      • Sorting: Maintains elements in natural order (or via a comparator).
      • Search/Insert/Delete: O(log n) due to the tree structure.
    • TreeMap:
      • Structure: A map backed by a Red-Black Tree.
      • Sorting: Keys are maintained in sorted order.
      • Insert/Delete/Search: O(log n) due to the tree structure.

Sorting: Summary of Time and Space Complexities

Sorting Algorithm Worst Case Time Complexity Best Case Time Complexity Space Complexity
Bubble Sort O(n²) O(n) O(1)
Selection Sort O(n²) O(n²) O(1)
Insertion Sort O(n²) O(n) O(1)
Merge Sort O(n log n) O(n log n) O(n)
Quick Sort O(n²) O(n log n) O(log n)
Heap Sort O(n log n) O(n log n) O(1)
Radix Sort O(nk) O(nk) O(n + k)

Spring Framework

  • What is the Spring Framework?
    • The Spring Framework is an open-source, feature-rich application framework for Java. It simplifies the development of enterprise-level Java applications by providing robust infrastructure support. Spring offers a comprehensive set of tools and features for building scalable, maintainable, and testable applications efficiently.

Spring JPA vs JDBC Template

  • Spring JPA (Java Persistence API)
    • Abstraction Level: High-level abstraction for working with databases.
    • Focus: Simplifies database interaction using the Object-Relational Mapping (ORM) model.
    • Key Features:
      • Entity Mapping: Automatically maps Java objects to database tables.
      • Query Language: Supports JPQL (Java Persistence Query Language), which works with entities rather than SQL tables.
      • Automatic Transactions: Manages transactions automatically through Spring's transaction management.
      • Lazy Loading: Allows lazy loading of related entities for better performance.
      • Integration with Spring Data: Spring Data JPA provides easy-to-use CRUD operations via repository interfaces.
      • Caching: Built-in support for caching with a 2nd level cache (e.g., Hibernate cache).
    • Advantages:
      • Reduces boilerplate code, especially for CRUD operations.
      • Easier to work with complex object-relational mappings.
      • Provides powerful features like lazy loading, cascading, and automatic transaction management.
    • Disadvantages:
      • Can be more complex and heavy compared to plain JDBC for simple queries.
      • Less fine-grained control over SQL and performance optimisations.
  • Spring JDBC Template
    • Abstraction Level: Lower-level abstraction, closer to raw SQL execution.
    • Focus: Simplifies JDBC (Java Database Connectivity) programming by providing a template-based approach to interacting with the database.
    • Key Features:
      • SQL Queries: Requires writing native SQL queries.
      • Exception Handling: Provides a consistent exception hierarchy, converting SQL exceptions into Spring's DataAccessException.
      • No ORM: Does not handle object-relational mapping; you need to manually map results to Java objects.
      • Transaction Management: Requires manual transaction management or integration with Spring's transaction management.
      • Lightweight: More lightweight compared to JPA, especially for simple use cases.
    • Advantages:
      • Offers fine-grained control over SQL execution, ideal for performance-sensitive applications.
      • Simple and efficient for scenarios where ORM-based solutions are not needed.
      • Great for legacy databases or complex queries that do not map well to an ORM model.
    • Disadvantages:
      • Requires writing more boilerplate code for mapping result sets to objects.
      • No automatic handling of complex relationships between objects.
      • Can be error-prone when dealing with complex queries or large datasets.
  • When to Use JPA vs JDBC Template
    • Use JPA if:
      • You prefer working with high-level abstractions.
      • Your application has complex relationships between entities.
      • You want automatic transaction management and caching.
    • Use JDBC Template if:
      • You need full control over SQL execution.
      • You're working with a legacy database or complex, performance-sensitive queries.
      • You want lightweight access to the database with minimal overhead.
Feature Spring JPA Spring JDBC Template
Level of Abstraction High (ORM) Low (direct SQL)
SQL vs JPQL Uses JPQL (object-based queries) Uses native SQL (table-based queries)
Entity Mapping Automatic mapping to objects Manual mapping of result sets to objects
Transaction Management Automatic (via Spring) Manual or via Spring's transaction management
Complexity Easier for complex object relationships Requires more code and care for relationships
Performance Tuning Limited control over SQL Fine-grained control over SQL and performance
Caching Built-in 2nd level caching No built-in caching support
Use Case Ideal for complex applications with object relationships Suitable for simple, performance-sensitive queries