Skip to main content

Java common questions

1. Public vs Private access modifiers

Access modifiers in Java control the visibility of classes, interfaces, variables, and methods.

Public

  • Definition: Members (variables, methods, constructors) or classes declared public are accessible from any other class, regardless of the package they are in.
  • Purpose: To expose an API (Application Programming Interface) for other parts of the application or external libraries to use.

Private

  • Definition: Members declared private are accessible only within the same class in which they are declared. They are not visible to subclasses or any other class in any package.
  • Purpose: Encapsulation – to hide the internal implementation details of a class and protect its internal state.

Key Differences

Feature public private
Visibility Everywhere (any class, any package) Only within the same class
Inheritance Public members are inherited by subclasses Private members are not inherited
Use Case Defining APIs, shared constants Hiding implementation, internal state

Example

// In package com.example
public class BankAccount {
    public String accountNumber; // Public: accessible by anyone
    private double balance;      // Private: only accessible within BankAccount

    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }

    // Public method to deposit money
    public void deposit(double amount) {
        if (amount > 0) {
            this.balance += amount;
            System.out.println("Deposited: " + amount);
        }
    }

    // Public method to get balance (controlled access)
    public double getBalance() {
        // We could add security checks here before returning
        return this.balance;
    }

    // Private helper method, not accessible outside
    private void logTransaction(String message) {
        System.out.println("LOG: " + message + " for account " + accountNumber);
    }

    public void withdraw(double amount) {
        if (amount > 0 && amount <= this.balance) {
            this.balance -= amount;
            logTransaction("Withdrawal of " + amount); // Using private method
            System.out.println("Withdrew: " + amount);
        } else {
            System.out.println("Withdrawal failed.");
        }
    }
}

// In another class, possibly another package
public class Main {
    public static void main(String[] args) {
        BankAccount acc1 = new BankAccount("12345", 1000.00);

        System.out.println("Account Number: " + acc1.accountNumber); // OK, accountNumber is public
        // System.out.println("Balance: " + acc1.balance); // ERROR! balance is private

        acc1.deposit(500.00); // OK, deposit() is public
        System.out.println("Current Balance: " + acc1.getBalance()); // OK, getBalance() is public

        acc1.withdraw(200.00); // OK
        // acc1.logTransaction("Manual log"); // ERROR! logTransaction() is private
    }
}

2. Static methods vs Instance methods

Static methods

  • Definition: Belong to the class itself, not to any specific instance (object) of the class. They are called using the class name (ClassName.methodName()).
  • Memory: Only one copy of a static method exists in memory, shared among all instances of the class (and even accessible without any instance).
  • Access:
    • Can access only static variables of the class.
    • Cannot access instance variables or instance methods directly (because they don't operate on a specific instance).
    • Cannot use this or super keywords.
  • Purpose: Utility functions, factory methods, operations that are not tied to the state of a specific object.

Instance methods

  • Definition: Belong to an instance (object) of the class. They are called on an object (objectReference.methodName()).
  • Memory: Each instance has its own conceptual "copy" or access to these methods, which operate on that instance's data.
  • Access:
    • Can access both static and instance variables of the class.
    • Can access both static and instance methods.
    • Can use this (to refer to the current instance) and super (to refer to the superclass instance).
  • Purpose: To operate on or query the state (instance variables) of a specific object.

Key Differences

Feature static method instance method
Association Belongs to the class Belongs to an object (instance)
Invocation ClassName.methodName() objectReference.methodName()
this keyword Cannot use this Can use this to refer to the current instance
Access to Members Can access static members directly. Cannot access instance members directly. Can access both static and instance members directly.
State Generally stateless or works with static state. Operates on the state of a specific instance.

Example

class Dog {
    private String name; // Instance variable
    private static int dogCount = 0; // Static variable

    public Dog(String name) {
        this.name = name;
        dogCount++; // Increment static counter for each new dog
    }

    // Instance method
    public void bark() {
        System.out.println(name + " says Woof!"); // Accesses instance variable 'name'
    }

    // Instance method that can also access static members
    public void displayInfo() {
        System.out.println("My name is " + this.name + ". There are " + dogCount + " dogs in total.");
    }

    // Static method
    public static int getDogCount() {
        // System.out.println(name); // ERROR! Cannot access instance variable 'name' from a static context
        // bark(); // ERROR! Cannot call instance method 'bark()' from a static context
        return dogCount; // Accesses static variable 'dogCount'
    }

    // Static utility method
    public static String getSpecies() {
        return "Canis familiaris";
    }
}

public class Main {
    public static void main(String[] args) {
        // Calling static method using class name
        System.out.println("Species: " + Dog.getSpecies());
        System.out.println("Initial dog count: " + Dog.getDogCount());

        Dog dog1 = new Dog("Buddy");
        Dog dog2 = new Dog("Lucy");

        // Calling instance methods using object reference
        dog1.bark(); // Buddy says Woof!
        dog2.bark(); // Lucy says Woof!

        dog1.displayInfo(); // My name is Buddy. There are 2 dogs in total.

        // Calling static method (can also be called via an instance, but not recommended)
        System.out.println("Current dog count: " + Dog.getDogCount()); // Prints 2
        // System.out.println(dog1.getDogCount()); // Also works, but IDE might warn: "Static member accessed via instance reference"
    }
}

3. Primitive data types vs Objects

Primitive data types

  • Definition: Fundamental data types predefined by Java. They are not objects.
  • Types: byte, short, int, long, float, double, char, boolean.
  • Storage: Store the actual binary value directly in the memory location where the variable is allocated (stack for local variables, or part of an object's memory on the heap).
  • Default Value: Have default values (e.g., 0 for numeric types, false for boolean, \u0000 for char) if they are instance or static variables. Local primitive variables must be initialized.
  • Behavior: Cannot have methods called on them. Operations are performed using operators (e.g., +, -, *, /).
  • Nullability: Cannot be null.

Objects

  • Definition: Instances of classes (user-defined or built-in like String, ArrayList). They represent more complex data structures and encapsulate data (fields) and behavior (methods).
  • Storage: Variables of object types store a reference (memory address) to the actual object, which resides on the heap.
  • Default Value: The default value for an object reference is null if it's an instance or static variable. Local object references must be initialized.
  • Behavior: Have methods that can be called on them to perform operations or query their state.
  • Nullability: Can be null, meaning the reference variable does not point to any object. This can lead to NullPointerException if not handled.

Key Differences

Feature Primitive Data Types Objects
Nature Basic, fundamental types Instances of classes, complex data structures
Storage Stores actual value directly Stores a reference (address) to the data on the heap
Memory Allocation Stack (for locals) or direct part of object Heap (for the object itself)
Methods Cannot have methods Have methods
Nullability Cannot be null Can be null
Default Value Specific to type (e.g., 0, false) null (for object references)
Examples int, double, char, boolean String, ArrayList, custom class instances

Example

public class PrimitiveVsObject {
    int instancePrimitiveInt; // Default value 0
    String instanceObjectString; // Default value null

    public static void main(String[] args) {
        // Primitive types
        int localPrimitiveInt = 10;          // Stores the value 10
        double localPrimitiveDouble = 20.5;  // Stores the value 20.5
        boolean localPrimitiveBoolean = true; // Stores the value true
        char localPrimitiveChar = 'A';       // Stores the character 'A'

        // int uninitializedLocalPrimitive;
        // System.out.println(uninitializedLocalPrimitive); // COMPILE ERROR: variable might not have been initialized

        System.out.println("Primitive int: " + localPrimitiveInt);
        // localPrimitiveInt.toString(); // ERROR! Primitives don't have methods

        // Objects
        String objString1 = "Hello"; // objString1 stores a reference to "Hello" object on the heap
        String objString2 = new String("World"); // objString2 stores a reference to a new "World" object

        // String uninitializedLocalObject;
        // System.out.println(uninitializedLocalObject.length()); // COMPILE ERROR: variable might not have been initialized (and then NPE if it was null)

        String nullString = null;
        // System.out.println(nullString.length()); // RUNTIME ERROR: NullPointerException

        System.out.println("Object String 1: " + objString1);
        System.out.println("Length of String 1: " + objString1.length()); // Objects have methods

        PrimitiveVsObject pvo = new PrimitiveVsObject();
        System.out.println("Instance primitive int default: " + pvo.instancePrimitiveInt);
        System.out.println("Instance object String default: " + pvo.instanceObjectString);
    }
}

4. Wrapper classes (Integer, String) vs Primitive types (int, String)

This question mixes two concepts slightly. Integer is a wrapper class for the primitive int. String is an object type, not a primitive, and not a wrapper for a primitive in the same way Integer is. char is the primitive for character data.

Let's address Integer vs int, and then briefly touch upon String.

Wrapper Classes (e.g., Integer) vs Primitive types (e.g., int)

  • Primitive Type (int):
    • As described above: stores actual value, no methods, cannot be null, more efficient.
  • Wrapper Class (Integer):
    • An object that "wraps" or encapsulates a primitive value. java.lang package provides wrapper classes for all primitives: Byte, Short, Integer, Long, Float, Double, Character, Boolean.
    • Purpose:
      1. To use primitive values in contexts where objects are required (e.g., Java Collections like ArrayList<Integer>, Generics).
      2. To provide utility methods related to the primitive type (e.g., Integer.parseInt(), Integer.MAX_VALUE).
      3. To allow primitive values to be null.
    • Behavior: Instances of wrapper classes are objects, so they are stored on the heap, can be null, and have methods.

String

  • String is an Object Type: It's a class (java.lang.String) representing a sequence of characters. It is not a primitive type.
  • Immutable: String objects are immutable in Java. Once created, their value cannot be changed.
  • Special Treatment: Java provides special support for strings, such as the string pool (for string literals) and the + operator for concatenation.
  • Not a Wrapper for a Primitive: While it deals with character data, it doesn't "wrap" a single primitive char in the same way Integer wraps int. An array of char (char[]) is closer to the raw data that String manages.

Key Differences (int vs Integer)

Feature int (Primitive) Integer (Wrapper Object)
Nature Primitive data type Object (instance of java.lang.Integer)
Storage Actual value Reference to an object on the heap
null Cannot be null Can be null
Methods No methods Has methods (e.g., intValue(), compareTo())
Collections Cannot be directly used in collections like ArrayList (before Java 5 without autoboxing) Can be used in collections (ArrayList<Integer>)
Performance Generally faster, less memory overhead Slower, more memory overhead
Default Value 0 (if instance/static variable) null (if instance/static variable)

Example

import java.util.ArrayList;
import java.util.List;

public class WrapperVsPrimitive {
    public static void main(String[] args) {
        // int (primitive) vs Integer (wrapper object)
        int primitiveInt = 100;
        Integer wrapperInt = Integer.valueOf(100); // Explicit boxing (older way)
        Integer autoBoxedInt = 100; // Autoboxing (Java 5+)

        System.out.println("Primitive int: " + primitiveInt);
        System.out.println("Wrapper Integer: " + wrapperInt);
        System.out.println("Autoboxed Integer: " + autoBoxedInt);

        // primitiveInt.compareTo(200); // ERROR: int has no methods
        System.out.println("Compare wrapperInt to 200: " + wrapperInt.compareTo(200)); // -1

        Integer nullInteger = null;
        // int anotherPrimitive = nullInteger; // This would cause NullPointerException if unboxing is attempted

        List<Integer> numberList = new ArrayList<>();
        numberList.add(primitiveInt); // Autoboxing: int to Integer
        numberList.add(wrapperInt);
        // List<int> primitiveList = new ArrayList<>(); // ERROR: Generic types must be reference types

        int unboxedInt = wrapperInt; // Auto-unboxing: Integer to int

        // String (Object)
        String strLiteral = "Java"; // String literal, often from string pool
        String strObject = new String("Java"); // Explicitly creates a new String object on heap

        char[] charArray = {'J', 'a', 'v', 'a'}; // Primitive char array
        String strFromChars = new String(charArray);

        System.out.println("String literal: " + strLiteral);
        System.out.println("String object: " + strObject);
        System.out.println("String from char array: " + strFromChars);

        // strLiteral is an object, it has methods
        System.out.println("Length of strLiteral: " + strLiteral.length());
    }
}

5. Array vs List

Array

  • Definition: A fixed-size, ordered collection of elements of the same data type. The type can be primitive (e.g., int[]) or object (e.g., String[], MyObject[]).
  • Size: Fixed at the time of creation. Cannot be changed dynamically.
  • Type Safety: Strong type checking at compile time. An int[] can only hold ints.
  • Performance: Generally faster for element access (using index) if the index is known, due to direct memory access.
  • Features: Basic operations provided by language syntax (e.g., array[index], array.length). No built-in methods for common data manipulations like add, remove (except by creating a new array).
  • Generics: Cannot be used directly with generics in the way collections can (e.g., you can't have Array<T>).

List (Interface)

  • Definition: An interface (java.util.List) representing an ordered collection of elements (also known as a sequence). Allows duplicate elements.
  • Implementations: Common implementations include ArrayList, LinkedList, Vector, Stack.
  • Size: Dynamic. Lists can grow or shrink as elements are added or removed.
  • Type Safety: Uses generics for type safety (e.g., List<String>, List<Integer>).
  • Performance: Varies by implementation. ArrayList is generally fast for random access, while LinkedList is faster for insertions/deletions in the middle.
  • Features: Rich set of methods for adding, removing, searching, iterating, sorting, etc. (e.g., add(), remove(), get(), size(), isEmpty()).
  • Generics: Designed to work with generics.

Key Differences

Feature Array List (Interface)
Size Fixed, determined at creation Dynamic, can change
Type Can hold primitives or objects Can only hold objects (primitives are autoboxed)
Mutability Size is immutable Size is mutable
Methods Limited (e.g., length property) Rich API (e.g., add, remove, size)
Performance Fast direct access by index Varies by implementation (ArrayList good for access, LinkedList for middle ops)
Generics Limited support Full generic support
Implementation Language construct Interface with multiple implementations

Example

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ArrayVsListExample {
    public static void main(String[] args) {
        // --- Array ---
        System.out.println("--- Array ---");
        // Declaration and initialization
        String[] stringArray = new String[3]; // Fixed size of 3
        stringArray[0] = "Apple";
        stringArray[1] = "Banana";
        stringArray[2] = "Cherry";
        // stringArray[3] = "Date"; // ArrayIndexOutOfBoundsException

        System.out.println("Array size: " + stringArray.length);
        System.out.println("Element at index 1: " + stringArray[1]);

        // Iterating an array
        for (String fruit : stringArray) {
            System.out.println(fruit);
        }

        int[] intArray = {1, 2, 3, 4, 5}; // Array of primitives
        System.out.println("Int Array: " + Arrays.toString(intArray));


        // --- List ---
        System.out.println("\n--- List (ArrayList implementation) ---");
        // Declaration and initialization (using ArrayList)
        List<String> stringList = new ArrayList<>(); // Dynamic size
        stringList.add("Dog");
        stringList.add("Cat");
        stringList.add("Elephant");

        System.out.println("List size: " + stringList.size());
        System.out.println("Element at index 1: " + stringList.get(1));

        stringList.add("Fish"); // Size dynamically increases
        System.out.println("New list size: " + stringList.size());
        System.out.println("List elements: " + stringList);

        stringList.remove("Cat");
        System.out.println("After removing Cat: " + stringList);

        // List of Integers (uses wrapper class)
        List<Integer> integerList = new ArrayList<>();
        integerList.add(10); // Autoboxing: int to Integer
        integerList.add(20);
        System.out.println("Integer List: " + integerList);
    }
}

6. Set vs List

Both Set and List are interfaces in the Java Collections Framework, extending Collection.

List

  • Definition: An ordered collection of elements. Allows duplicate elements.
  • Order: Maintains the insertion order of elements (or can be sorted explicitly). Elements can be accessed by their integer index (position).
  • Duplicates: Allows duplicate elements. You can add the same element multiple times.
  • Common Implementations: ArrayList, LinkedList, Vector.
  • Use Case: When the order of elements matters, and duplicates are acceptable. E.g., a list of steps in a recipe, a sequence of user actions.

Set

  • Definition: A collection that contains no duplicate elements.
  • Order:
    • HashSet: Makes no guarantees as to the iteration order of the set; it may even change over time.
    • LinkedHashSet: Maintains insertion order.
    • TreeSet: Stores elements in a sorted order (natural order or by a Comparator).
  • Duplicates: Does not allow duplicate elements. If you try to add an element that is already present (according to its equals() method), the add() method returns false and the set remains unchanged.
  • Common Implementations: HashSet, LinkedHashSet, TreeSet.
  • Use Case: When you need to store unique items, and the primary concern is checking for the existence of an item. E.g., a collection of unique visitors to a website, distinct words in a document.

Key Differences

Feature List Set
Order Ordered (maintains insertion order or sorted) Generally unordered (HashSet), or ordered (LinkedHashSet, TreeSet)
Duplicates Allows duplicate elements Does not allow duplicate elements
Element Access Positional access using get(index) No direct get(index) (except by iteration or converting to List)
add() method Adds element, usually returns true Adds element if not present, returns true if added, false if already present
Primary Use Ordered sequences, allowing duplicates Collections of unique items
Implementations ArrayList, LinkedList HashSet, LinkedHashSet, TreeSet

Example

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

public class SetVsListExample {
    public static void main(String[] args) {
        // --- List Example (ArrayList) ---
        System.out.println("--- List (ArrayList) ---");
        List<String> fruitList = new ArrayList<>();
        fruitList.add("Apple");
        fruitList.add("Banana");
        fruitList.add("Apple"); // Duplicate allowed
        fruitList.add("Orange");

        System.out.println("Fruit List: " + fruitList); // Order is maintained
        System.out.println("Element at index 0: " + fruitList.get(0)); // Access by index

        // --- Set Examples ---
        System.out.println("\n--- Set (HashSet - unordered) ---");
        Set<String> fruitHashSet = new HashSet<>();
        System.out.println("Added Apple: " + fruitHashSet.add("Apple"));
        System.out.println("Added Banana: " + fruitHashSet.add("Banana"));
        System.out.println("Added Apple again: " + fruitHashSet.add("Apple")); // Duplicate not added, returns false
        System.out.println("Added Orange: " + fruitHashSet.add("Orange"));

        System.out.println("Fruit HashSet: " + fruitHashSet); // Order not guaranteed, no duplicates
        // fruitHashSet.get(0); // COMPILE ERROR: No get(index) method

        System.out.println("\n--- Set (LinkedHashSet - insertion order) ---");
        Set<String> fruitLinkedHashSet = new LinkedHashSet<>();
        fruitLinkedHashSet.add("Apple");
        fruitLinkedHashSet.add("Banana");
        fruitLinkedHashSet.add("Apple"); // Duplicate not added
        fruitLinkedHashSet.add("Orange");
        fruitLinkedHashSet.add("Grape");

        System.out.println("Fruit LinkedHashSet: " + fruitLinkedHashSet); // Insertion order maintained, no duplicates

        System.out.println("\n--- Set (TreeSet - sorted order) ---");
        Set<String> fruitTreeSet = new TreeSet<>();
        fruitTreeSet.add("Orange");
        fruitTreeSet.add("Apple");
        fruitTreeSet.add("Banana");
        fruitTreeSet.add("Apple"); // Duplicate not added
        fruitTreeSet.add("Grape");

        System.out.println("Fruit TreeSet: " + fruitTreeSet); // Sorted order (natural string order), no duplicates
    }
}

7. Comparable vs Comparator

Both are interfaces in Java used for sorting objects.

Comparable<T>

  • Definition: An interface (java.lang.Comparable) that a class can implement to define its "natural ordering."
  • Method: It has a single method: int compareTo(T o).
    • Returns a negative integer if this object is less than o.
    • Returns zero if this object is equal to o.
    • Returns a positive integer if this object is greater than o.
  • Usage:
    • Implemented by the class whose instances you want to sort.
    • Used by sorting methods like Collections.sort(List<T>) and Arrays.sort(T[]) by default if no Comparator is provided.
    • Data structures like TreeSet and TreeMap use the natural ordering if no Comparator is specified.
  • Modification: Requires modifying the source code of the class itself.
  • Ordering: Provides a single way of sorting (the natural order).

Comparator<T>

  • Definition: An interface (java.util.Comparator) that can be implemented to define custom or multiple sorting orders for objects of a class.
  • Method: It has a primary method: int compare(T o1, T o2).
    • Returns a negative integer if o1 is less than o2.
    • Returns zero if o1 is equal to o2.
    • Returns a positive integer if o1 is greater than o2.
    • (Also has other default/static methods like thenComparing, reversed, etc., since Java 8).
  • Usage:
    • Implemented in a separate class, or as an anonymous class, or a lambda expression.
    • Passed as an argument to sorting methods (e.g., Collections.sort(List<T>, Comparator<T>), Arrays.sort(T[], Comparator<T>)).
    • Can be provided to constructors of TreeSet and TreeMap to define a specific order.
  • Modification: Does not require modifying the source code of the class being sorted.
  • Ordering: Allows defining multiple, different sorting strategies for the same class.

Key Differences

Feature Comparable<T> Comparator<T>
Package java.lang java.util
Method compareTo(T o) compare(T o1, T o2)
Implementation Implemented by the class itself Implemented in a separate class/lambda
Purpose Defines natural ordering of objects Defines custom/alternative ordering of objects
Modification Requires changing the class being sorted Does not require changing the class being sorted
Flexibility Provides one sorting logic (natural order) Provides multiple sorting logics
Usage Collections.sort(list) uses it by default Passed as an argument to Collections.sort(list, comparator)

Example

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

// Class implementing Comparable for natural ordering (by age)
class Person implements Comparable<Person> {
    String name;
    int age;

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

    @Override
    public int compareTo(Person other) {
        // Natural ordering: by age
        return Integer.compare(this.age, other.age);
        // If this.age < other.age, returns negative
        // If this.age == other.age, returns zero
        // If this.age > other.age, returns positive
    }

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

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

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

        System.out.println("Original list: " + people);

        // 1. Sorting using Comparable (natural order: by age)
        Collections.sort(people); // Uses Person's compareTo() method
        System.out.println("Sorted by age (Comparable): " + people);

        // Reset list for next sort
        people.clear();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 35));
        people.add(new Person("Diana", 25));


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

        // 3. Sorting using Comparator (lambda expression for reverse age order)
        // No need to reset list, just re-sort
        Comparator<Person> reverseAgeComparator = (p1, p2) -> Integer.compare(p2.age, p1.age);
        // Or use Java 8 Comparator helpers:
        // Comparator<Person> reverseAgeComparator = Comparator.comparingInt((Person p) -> p.age).reversed();

        Collections.sort(people, reverseAgeComparator);
        System.out.println("Sorted by age descending (Lambda Comparator): " + people);

        // 4. Sorting by name then by age (if names are same)
        Comparator<Person> nameThenAgeComparator = Comparator
            .comparing((Person p) -> p.name)
            .thenComparingInt(p -> p.age);
        Collections.sort(people, nameThenAgeComparator);
        System.out.println("Sorted by name then age: " + people);
    }
}

8. Interface vs Abstract class

Both are mechanisms for abstraction in Java, but they have different characteristics and use cases.

Interface

  • Definition: A blueprint of a class. It specifies a contract that implementing classes must adhere to.
  • Methods:
    • Traditionally, interfaces could only have public abstract methods (method signatures without implementation) and public static final constants.
    • Java 8 added public default methods (with implementation, can be overridden) and public static methods (with implementation, cannot be overridden by implementing classes).
    • Java 9 added private and private static methods (helper methods for default/static methods within the interface).
  • Variables: Can only declare public static final variables (constants). Instance variables are not allowed.
  • Constructors: Cannot have constructors. Cannot be instantiated directly.
  • Implementation: A class implements an interface. A class can implement multiple interfaces (multiple inheritance of type).
  • Access Modifiers: All members are implicitly public. Methods are implicitly abstract (unless default or static).
  • Purpose: To define a contract, achieve multiple inheritance of type, achieve loose coupling. Good for defining capabilities (e.g., Serializable, Runnable, Flyable).

Abstract Class

  • Definition: A class that cannot be instantiated directly and is meant to be subclassed. It can provide a partial implementation of a class.
  • Methods: Can have abstract methods (no implementation) and concrete methods (with implementation). Can also have static and final methods.
  • Variables: Can have instance variables (non-static) and static variables. They can have any access modifier (public, protected, private, default). Can also be final.
  • Constructors: Can have constructors. These are called when an instance of a concrete subclass is created (usually via super() call from subclass constructor).
  • Implementation: A class extends an abstract class. A class can extend only one abstract class (or any class).
  • Access Modifiers: Members can have any access modifier.
  • Purpose: To provide a common base class with some shared code and some parts that subclasses must implement. Good for "is-a" relationships where subclasses share common state and behavior.

Key Differences

Feature Interface Abstract Class
Multiple Inheritance A class can implement multiple interfaces. A class can extend only one abstract class.
Methods Abstract, default, static, private methods. Abstract and concrete methods. Can be static, final.
Variables Only public static final constants. Instance variables, static variables, constants. Any access modifier.
Constructors No constructors. Can have constructors.
Implementation Detail Focuses on "what" a class can do (contract). Can provide some "how" (partial implementation).
Keyword interface, implements abstract class, extends
State Cannot have instance state (non-final variables). Can have instance state (instance variables).
Use Case Defining capabilities, API contracts, mixins (with default methods). Code reuse, common base for related classes.
Access Modifiers Implicitly public for members. Members can have any access modifier.

Example

// --- Interface ---
interface Playable {
    // public static final String TYPE = "Playable"; // Implicitly public static final
    String TYPE = "Playable"; // Same as above

    void play(); // Implicitly public abstract

    default void displayType() { // Default method (Java 8+)
        System.out.println("This is of type: " + TYPE);
    }

    static String getCategory() { // Static method (Java 8+)
        return "Entertainment";
    }
}

interface Recordable {
    void record();
    void stopRecording();
}

// --- Abstract Class ---
abstract class MediaDevice {
    protected String deviceName; // Instance variable (can be protected, private, etc.)
    private boolean powerOn;     // Instance variable

    public MediaDevice(String deviceName) { // Constructor
        this.deviceName = deviceName;
        this.powerOn = false;
        System.out.println(deviceName + " device created.");
    }

    public void powerOn() { // Concrete method
        this.powerOn = true;
        System.out.println(deviceName + " powered ON.");
    }

    public void powerOff() { // Concrete method
        this.powerOn = false;
        System.out.println(deviceName + " powered OFF.");
    }

    public boolean isPoweredOn() {
        return powerOn;
    }

    public abstract void showStatus(); // Abstract method - must be implemented by subclasses
}

// --- Concrete Class ---
// Implements multiple interfaces and extends one abstract class
class SmartSpeaker extends MediaDevice implements Playable, Recordable {
    public SmartSpeaker(String name) {
        super(name); // Call superclass constructor
    }

    @Override
    public void play() {
        if (isPoweredOn()) {
            System.out.println(deviceName + " is playing music.");
        } else {
            System.out.println(deviceName + " is off. Cannot play.");
        }
    }

    @Override
    public void record() {
        if (isPoweredOn()) {
            System.out.println(deviceName + " is recording audio.");
        } else {
            System.out.println(deviceName + " is off. Cannot record.");
        }
    }

    @Override
    public void stopRecording() {
         if (isPoweredOn()) {
            System.out.println(deviceName + " stopped recording.");
        }
    }

    @Override
    public void showStatus() {
        System.out.println("Status for " + deviceName + ": Power " + (isPoweredOn() ? "ON" : "OFF"));
    }
}

public class InterfaceAbstractExample {
    public static void main(String[] args) {
        SmartSpeaker alexa = new SmartSpeaker("Alexa Echo");

        alexa.powerOn();
        alexa.showStatus();

        alexa.play();       // From Playable interface
        alexa.record();     // From Recordable interface
        alexa.stopRecording();

        alexa.displayType(); // Default method from Playable interface
        System.out.println("Category: " + Playable.getCategory()); // Static method from Playable

        alexa.powerOff();
        alexa.showStatus();
        alexa.play();
    }
}

9. Final vs Static keyword

These keywords serve very different purposes in Java.

final keyword

The final keyword can be applied to variables, methods, and classes. Its meaning changes based on context.

  • final variable:
    • Primitive: The value of the variable cannot be changed once assigned. It becomes a constant. Must be initialized at declaration or in the constructor (if it's an instance variable).
    • Object Reference: The reference variable cannot be reassigned to point to a different object. However, the state of the object it points to can be changed (if the object itself is mutable).
  • final method:
    • A final method cannot be overridden by subclasses.
    • Used to prevent subclasses from altering critical behavior.
  • final class:
    • A final class cannot be subclassed (extended).
    • E.g., String, Integer are final classes.
    • Used for security or to ensure the immutability or specific behavior of the class cannot be compromised by subclassing.

static keyword

The static keyword indicates that a member (variable or method) or a nested class belongs to the class itself, rather than to instances of the class.

  • static variable (class variable):
    • There is only one copy of the static variable shared among all instances of the class.
    • It is initialized when the class is loaded.
    • Can be accessed using ClassName.variableName.
  • static method (class method):
    • Can be called without creating an instance of the class (ClassName.methodName()).
    • Can only access static variables and call other static methods directly.
    • Cannot use this or super.
  • static block:
    • A block of code that is executed once when the class is loaded into memory. Used for static initialization.
  • static nested class:
    • A nested class declared static. It does not have an implicit reference to an instance of the outer class. It can only access static members of the outer class directly. Can be instantiated without an instance of the outer class.

Key Differences

Feature final static
Purpose To restrict modification/extension/overriding. Creates constants. To associate a member with the class rather than an instance. Shared memory.
Applicable to Variables, methods, classes. Variables, methods, blocks, nested classes.
Variables Value cannot be changed (for primitives); reference cannot be changed (for objects). Single copy shared by all instances. Belongs to the class.
Methods Cannot be overridden. Belongs to the class, called via ClassName.method(). No this.
Classes Cannot be subclassed. (For nested classes) Does not hold reference to outer class instance.
Memory Does not directly impact memory model like static (except making references unchangeable). Static members are stored in a separate memory area (e.g., method area/metaspace).

Example

class MyMath {
    // final variable (constant)
    public static final double PI = 3.14159; // 'static final' makes it a class constant

    // instance variable (can be final)
    private final int id; // Must be initialized in constructor or at declaration
    private String instanceName;

    // static variable
    private static int instanceCount = 0;

    public MyMath(int id, String name) {
        this.id = id; // Initialize final instance variable
        this.instanceName = name;
        instanceCount++;
    }

    // final method (cannot be overridden)
    public final void printID() {
        System.out.println("ID: " + this.id);
    }

    // instance method
    public void printName() {
        System.out.println("Name: " + this.instanceName);
    }

    // static method
    public static int getInstanceCount() {
        // System.out.println(instanceName); // ERROR: Cannot access instance variable from static context
        // printName(); // ERROR: Cannot call instance method from static context
        return instanceCount;
    }

    // static block
    static {
        System.out.println("MyMath class loaded. Initial instance count: " + instanceCount);
        // PI = 3.14; // Can initialize static final here if not at declaration
    }
}

// final class (cannot be extended)
final class ImmutablePoint {
    private final int x;
    private final int y;

    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }
    public int getY() { return y; }
}

// class TryingToExtend extends ImmutablePoint {} // ERROR: Cannot inherit from final ImmutablePoint

class AdvancedMath extends MyMath {
    public AdvancedMath(int id, String name) {
        super(id, name);
    }

    // @Override
    // public final void printID() { // ERROR: Cannot override final method from MyMath
    //     System.out.println("Advanced ID: " + super.id); // 'id' is private, but can be accessed if it were protected
    // }

    @Override
    public void printName() { // OK to override non-final method
        System.out.print("Advanced Math - ");
        super.printName();
    }
}


public class FinalStaticExample {
    public static void main(String[] args) {
        System.out.println("Accessing static final constant: MyMath.PI = " + MyMath.PI);
        // MyMath.PI = 3.0; // ERROR: Cannot assign a value to final variable PI

        System.out.println("Initial instance count (static method): " + MyMath.getInstanceCount()); // 0

        MyMath math1 = new MyMath(1, "MathObj1");
        // math1.id = 2; // ERROR: Cannot assign a value to final variable id

        math1.printID();    // Calls final method
        math1.printName();  // Calls instance method

        MyMath math2 = new MyMath(2, "MathObj2");
        System.out.println("Instance count after 2 objects (static method): " + MyMath.getInstanceCount()); // 2

        AdvancedMath advMath = new AdvancedMath(3, "AdvMathObj1");
        advMath.printID();   // Calls inherited final method
        advMath.printName(); // Calls overridden method in AdvancedMath

        ImmutablePoint p = new ImmutablePoint(10, 20);
        // p = new ImmutablePoint(30, 40); // This would be allowed if 'p' itself was not final.
                                        // Here, the object p refers to is immutable.

        final ImmutablePoint finalP = new ImmutablePoint(5,5);
        // finalP = new ImmutablePoint(1,1); // ERROR: finalP is a final reference.
        // The object finalP points to is also immutable due to its class design.
    }
}

10. == vs equals() method

These are used for comparison in Java, but they compare different things.

== operator

  • Primitives: For primitive types (int, char, boolean, etc.), == compares their actual values.
    • int a = 5; int b = 5; then a == b is true.
  • Objects: For object reference types (String, ArrayList, custom objects, etc.), == compares their memory addresses (references). It checks if two reference variables point to the exact same object in memory.
    • String s1 = new String("hello"); String s2 = new String("hello"); then s1 == s2 is false because s1 and s2 refer to two different objects in memory, even though their content is the same.
    • String s3 = "hello"; String s4 = "hello"; then s3 == s4 is true due to String interning (string pool optimization for literals).
    • MyObject o1 = new MyObject(); MyObject o2 = o1; then o1 == o2 is true because o2 is assigned the same reference as o1.

equals() method

  • Definition: A method defined in the java.lang.Object class. Its default implementation in Object class behaves exactly like == for objects (i.e., it compares references).
    • public boolean equals(Object obj)
  • Overriding: Classes like String, Integer, Date, and collection classes override the equals() method to provide "logical" or "content" equality. They compare the actual content or state of the objects, not just their memory addresses.
    • For String, equals() compares the sequence of characters.
    • For Integer, equals() compares the wrapped integer value.
  • Custom Classes: If you don't override equals() in your custom class, it will inherit the Object class's implementation, which means it will perform a reference comparison (==). It's a common practice to override equals() (and hashCode()) in custom classes if you need to define logical equality based on the object's attributes.
  • Contract: When overriding equals(), you must also override hashCode() to maintain the general contract: if a.equals(b) is true, then a.hashCode() == b.hashCode() must be true.

Key Differences

Feature == Operator equals() Method
Type Operator Method
Primitives Compares values Not applicable (cannot call methods on primitives)
Objects (Default) Compares references (memory addresses) Compares references (default Object.equals() behavior)
Objects (Overridden) Still compares references Compares content/state (if overridden appropriately, e.g., in String, Integer)
Usage Value comparison for primitives, reference comparison for objects. Content/logical comparison for objects (when properly overridden).

Example

class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // getters...

    // Without overriding equals() and hashCode(), Point instances
    // will be compared by reference using the default Object.equals().

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // Same object reference
        if (o == null || getClass() != o.getClass()) return false; // Null or different class
        Point point = (Point) o;
        return x == point.x && y == point.y; // Compare content
    }

    @Override
    public int hashCode() {
        // A simple hash code implementation
        int result = x;
        result = 31 * result + y;
        return result;
        // Or use Objects.hash(x, y) from Java 7+
    }
}

public class EqualsVsDoubleEqual {
    public static void main(String[] args) {
        // --- Primitives ---
        int a = 10;
        int b = 10;
        int c = 20;

        System.out.println("Primitives:");
        System.out.println("a == b: " + (a == b)); // true (10 == 10)
        System.out.println("a == c: " + (a == c)); // false (10 == 20)
        // a.equals(b); // COMPILE ERROR: equals() cannot be called on primitives

        // --- Objects (String - special case with string pool) ---
        String s1 = "hello"; // String literal, goes into string pool
        String s2 = "hello"; // Another literal, reuses from string pool
        String s3 = new String("hello"); // Explicitly creates a new object on the heap
        String s4 = new String("hello"); // Explicitly creates another new object on the heap

        System.out.println("\nStrings:");
        System.out.println("s1 == s2: " + (s1 == s2)); // true (both point to the same object in string pool)
        System.out.println("s1 == s3: " + (s1 == s3)); // false (s1 is in pool, s3 is a new heap object)
        System.out.println("s3 == s4: " + (s3 == s4)); // false (s3 and s4 are distinct heap objects)

        System.out.println("s1.equals(s2): " + s1.equals(s2)); // true (content is the same)
        System.out.println("s1.equals(s3): " + s1.equals(s3)); // true (content is the same)
        System.out.println("s3.equals(s4): " + s3.equals(s4)); // true (content is the same)

        // --- Objects (Custom Class Point) ---
        Point p1 = new Point(1, 2);
        Point p2 = new Point(1, 2);
        Point p3 = p1; // p3 now refers to the same object as p1

        System.out.println("\nCustom Objects (Point):");
        System.out.println("p1 == p2: " + (p1 == p2)); // false (p1 and p2 are different objects in memory)
        System.out.println("p1 == p3: " + (p1 == p3)); // true (p1 and p3 refer to the same object)

        // If Point did NOT override equals(), this would be false.
        // Since Point DOES override equals() to compare x and y, this is true.
        System.out.println("p1.equals(p2): " + p1.equals(p2)); // true (content is the same due to overridden equals)
        System.out.println("p1.equals(p3): " + p1.equals(p3)); // true (same object, content also same)

        Object o = new Object();
        Object o2 = new Object();
        System.out.println("o.equals(o2) without override: " + o.equals(o2)); // false, default is ==
    }
}

(The remaining answers will follow this detailed format. This is a lot of information, so I'll continue in subsequent responses if needed to avoid making one response excessively long.) Okay, continuing with the detailed explanations:


11. Autoboxing vs Unboxing

These are convenience features introduced in Java 5 that allow automatic conversion between primitive types and their corresponding wrapper classes.

Autoboxing

  • Definition: The automatic conversion that the Java compiler makes between a primitive type and its corresponding object wrapper class. For example, converting an int to an Integer, a double to a Double, etc.
  • When it happens:
    • When a primitive value is passed as a parameter to a method that expects an object of the corresponding wrapper class.
    • When a primitive value is assigned to a variable of the corresponding wrapper class.
    • When a primitive is added to a collection that expects objects (e.g., ArrayList<Integer>).

Unboxing

  • Definition: The automatic conversion that the Java compiler makes from a wrapper class object to its corresponding primitive type. For example, converting an Integer to an int, a Double to a double, etc.
  • When it happens:
    • When an object of a wrapper class is passed as a parameter to a method that expects a value of the corresponding primitive type.
    • When an object of a wrapper class is assigned to a variable of the corresponding primitive type.
    • In arithmetic operations involving wrapper objects (the object is unboxed to a primitive before the operation).
  • Caution: Unboxing a null wrapper object will result in a NullPointerException.

Key Differences

Feature Autoboxing Unboxing
Conversion Primitive type → Wrapper class object Wrapper class object → Primitive type
Example int i = 10; Integer obj = i; Integer obj = new Integer(10); int i = obj;
Potential Issue Less direct, slight performance overhead. NullPointerException if wrapper object is null.

Example

import java.util.ArrayList;
import java.util.List;

public class AutoboxingUnboxingExample {
    public static void main(String[] args) {
        // --- Autoboxing ---
        // Primitive int to Integer object
        int primitiveInt = 10;
        Integer wrapperInt = primitiveInt; // Autoboxing: int -> Integer
        // Under the hood, this is like: Integer wrapperInt = Integer.valueOf(primitiveInt);

        System.out.println("Primitive int: " + primitiveInt);
        System.out.println("Autoboxed Integer: " + wrapperInt);

        List<Integer> integerList = new ArrayList<>();
        integerList.add(20); // Autoboxing: int 20 -> Integer.valueOf(20)
        integerList.add(primitiveInt); // Autoboxing

        System.out.println("List of Integers: " + integerList);

        takesIntegerObject(30); // Autoboxing: int 30 -> Integer.valueOf(30)

        // --- Unboxing ---
        Integer anotherWrapperInt = Integer.valueOf(50);
        int anotherPrimitiveInt = anotherWrapperInt; // Unboxing: Integer -> int
        // Under the hood, this is like: int anotherPrimitiveInt = anotherWrapperInt.intValue();

        System.out.println("\nUnboxed primitive int: " + anotherPrimitiveInt);

        int sum = anotherWrapperInt + 5; // Unboxing: anotherWrapperInt is unboxed to int before addition
        System.out.println("Sum (unboxing in expression): " + sum);

        int valFromList = integerList.get(0); // Unboxing: Element from list (Integer) to int
        System.out.println("Value from list (unboxed): " + valFromList);

        takesPrimitiveInt(anotherWrapperInt); // Unboxing: Integer -> int

        // --- Potential NullPointerException with Unboxing ---
        Integer nullInteger = null;
        try {
            // int problematicInt = nullInteger; // This would cause NullPointerException
            if (nullInteger != null && nullInteger > 0) { // Guarded unboxing
                 System.out.println("This won't print");
            }
            // Or more directly, to show the error:
             int npeInt = nullInteger.intValue(); // This is what unboxing effectively does
        } catch (NullPointerException e) {
            System.out.println("\nCaught NullPointerException during unboxing: " + e.getMessage());
        }
    }

    public static void takesIntegerObject(Integer num) {
        System.out.println("Method takesIntegerObject received: " + num);
    }

    public static void takesPrimitiveInt(int num) {
        System.out.println("Method takesPrimitiveInt received: " + num);
    }
}

12. Checked exceptions vs Unchecked exceptions

Java's exception handling mechanism categorizes exceptions into these two main types, which differ in how the compiler enforces their handling. Both are subclasses of java.lang.Throwable. Error is another subclass of Throwable for severe system issues, usually not handled by applications.

Checked Exceptions

  • Definition: Exceptions that the Java compiler checks at compile-time. They represent exceptional conditions that a well-written application should anticipate and recover from.
  • Subclasses of: java.lang.Exception (but not java.lang.RuntimeException or its subclasses).
  • Handling:
    • A method that might throw a checked exception must either:
      1. Handle it using a try-catch block.
      2. Declare it in its throws clause, propagating the responsibility to the caller.
  • Examples: IOException, SQLException, FileNotFoundException, ClassNotFoundException.
  • Purpose: To force programmers to deal with predictable, though exceptional, situations.

Unchecked Exceptions (Runtime Exceptions)

  • Definition: Exceptions that the Java compiler does not check at compile-time. They usually indicate programming errors or unexpected conditions that are often unrecoverable or should have been prevented by better coding.
  • Subclasses of: java.lang.RuntimeException (which itself is a subclass of java.lang.Exception). Error and its subclasses are also unchecked, but typically not caught.
  • Handling:
    • The compiler does not require you to handle or declare them.
    • You can still use try-catch blocks for them, but it's often a sign of fixing a symptom rather than the root cause (e.g., catching NullPointerException instead of ensuring an object is not null).
  • Examples: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException, ArithmeticException, ClassCastException.
  • Purpose: To indicate bugs in the program logic or runtime issues that are generally not anticipated for recovery by the immediate calling code.

Key Differences

Feature Checked Exceptions Unchecked Exceptions (Runtime Exceptions)
Compiler Check Checked at compile-time. Not checked at compile-time.
Handling Must be handled (try-catch) or declared (throws). Handling/declaring is optional.
Parent Class Subclass of Exception (excluding RuntimeException). Subclass of RuntimeException. (Error is also unchecked).
Nature External conditions, often recoverable (e.g., file not found, network error). Programming errors, internal logic flaws (e.g., null pointer, bad index).
Examples IOException, SQLException, FileNotFoundException NullPointerException, ArrayIndexOutOfBoundsException, ArithmeticException

Example

import java.io.File;
import java.io.FileReader;
import java.io.IOException; // Checked exception
import java.io.FileNotFoundException; // Checked exception, subclass of IOException

public class CheckedUncheckedExceptions {

    // Method that might throw a checked exception (IOException)
    // It must declare it in the 'throws' clause or handle it.
    public static void readFile(String fileName) throws FileNotFoundException, IOException {
        File file = new File(fileName);
        FileReader fr = new FileReader(file); // Might throw FileNotFoundException
        // ... read file ... (further operations might throw IOException)
        System.out.println("Successfully opened (but not read): " + fileName);
        fr.close(); // Might throw IOException
    }

    // Method that might cause an unchecked exception
    public static void processNumber(String numberStr, int divisor) {
        if (numberStr == null) {
            // This would lead to NullPointerException if not checked
            System.out.println("Input string is null, cannot parse.");
            return;
        }
        try {
            int number = Integer.parseInt(numberStr); // Might throw NumberFormatException (unchecked)
            int result = number / divisor;            // Might throw ArithmeticException (unchecked if divisor is 0)
            System.out.println("Result: " + result);
        } catch (NumberFormatException e) {
            System.err.println("Unchecked Exception: Invalid number format - " + e.getMessage());
        } catch (ArithmeticException e) {
            System.err.println("Unchecked Exception: Cannot divide by zero - " + e.getMessage());
        }
        // No need to declare NumberFormatException or ArithmeticException in throws clause.
    }

    public static void main(String[] args) {
        // --- Handling Checked Exception ---
        System.out.println("--- Checked Exception Example ---");
        try {
            readFile("existing_file.txt"); // Assume this file does not exist for demo
                                            // To make it work, create an empty "existing_file.txt"
            // readFile("non_existent_file.txt");
        } catch (FileNotFoundException e) {
            System.err.println("Checked Exception Caught: File not found - " + e.getMessage());
        } catch (IOException e) {
            System.err.println("Checked Exception Caught: IO Error - " + e.getMessage());
        } finally {
            System.out.println("File reading attempt finished.");
        }

        // --- Triggering Unchecked Exceptions ---
        System.out.println("\n--- Unchecked Exception Example ---");
        processNumber("100", 5);     // Valid
        processNumber("abc", 5);     // NumberFormatException
        processNumber("100", 0);     // ArithmeticException
        processNumber(null, 5);      // Handled null case inside processNumber

        // Example of an unhandled unchecked exception (if not caught inside method)
        // String text = null;
        // System.out.println(text.length()); // This would throw NullPointerException
    }
}
// To run the "readFile" part without error, create an empty file named "existing_file.txt" in the same directory.

13. Thread vs Runnable

Both are used for creating and managing threads in Java for concurrent execution.

Thread class (java.lang.Thread)

  • Definition: A class that represents a thread of execution.
  • Usage:
    1. Extend Thread: Create a subclass of Thread and override its run() method to define the task the thread will execute. Then create an instance of this subclass and call its start() method.
    2. Pass Runnable to Thread constructor: (More common and flexible) Create an instance of Thread by passing a Runnable object to its constructor.
  • State: A Thread object encapsulates the thread's state, priority, name, etc.
  • Limitation: If you extend Thread, your class cannot extend any other class (Java doesn't support multiple class inheritance).

Runnable interface (java.lang.Runnable)

  • Definition: A functional interface with a single abstract method: void run().
  • Usage:
    1. Implement the Runnable interface in a class and provide the implementation for the run() method.
    2. Create an instance of this class (the Runnable object).
    3. Create a Thread object, passing the Runnable object to its constructor: Thread t = new Thread(myRunnable);.
    4. Call t.start() to begin execution.
  • Flexibility: Since it's an interface, your class can implement Runnable and still extend another class. This promotes better separation of concerns: the task (Runnable) is separate from the execution mechanism (Thread).
  • Reusability: The same Runnable instance can potentially be executed by multiple threads if its run() method is thread-safe or designed for it (though typically one Runnable is passed to one Thread for a specific task).

Key Differences

Feature Thread Class Runnable Interface
Type Class Interface
Inheritance Extending Thread means your class cannot extend other classes. Implementing Runnable allows your class to extend another class.
Separation Task logic is tightly coupled with the Thread object. Promotes separation of task logic from thread mechanism.
Flexibility Less flexible due to inheritance restriction. More flexible.
Object Sharing Each thread is a new object with its own state. Multiple threads can share the same Runnable object (if designed to be shareable/thread-safe).
Primary Role Represents the worker, the actual thread of execution. Represents the task to be executed.
Recommended Use Generally, implementing Runnable is preferred over extending Thread. Preferred approach for defining a task.

Example

// Approach 1: Extending Thread
class MyThread extends Thread {
    private String threadName;

    public MyThread(String name) {
        super(name); // Set thread name via superclass constructor
        this.threadName = name;
        System.out.println("Creating " +  threadName );
    }

    @Override
    public void run() {
        System.out.println("Running " +  threadName );
        try {
            for(int i = 4; i > 0; i--) {
                System.out.println("Thread: " + threadName + ", Count: " + i);
                Thread.sleep(50); // Simulate work
            }
        } catch (InterruptedException e) {
            System.out.println("Thread " +  threadName + " interrupted.");
        }
        System.out.println("Thread " +  threadName + " exiting.");
    }
}

// Approach 2: Implementing Runnable
class MyRunnable implements Runnable {
    private String taskName;
    private Thread thread; // Optional: to hold the thread that runs this runnable

    public MyRunnable(String name) {
        this.taskName = name;
        System.out.println("Creating task " +  taskName );
    }

    @Override
    public void run() {
        System.out.println("Running task " +  taskName + " on thread " + Thread.currentThread().getName());
        try {
            for(int i = 4; i > 0; i--) {
                System.out.println("Task: " + taskName + ", Count: " + i + " (Thread: " + Thread.currentThread().getName() + ")");
                Thread.sleep(70); // Simulate work
            }
        } catch (InterruptedException e) {
            System.out.println("Task " +  taskName + " interrupted.");
        }
        System.out.println("Task " +  taskName + " exiting.");
    }

    public void start() { // Convenience method to create and start the thread
        System.out.println("Starting task " + taskName);
        if (thread == null) {
            thread = new Thread(this, taskName + "-Thread"); // Pass runnable and a name for the thread
            thread.start();
        }
    }
}

public class ThreadVsRunnableExample {
    public static void main(String[] args) {
        System.out.println("--- Extending Thread ---");
        MyThread thread1 = new MyThread("Thread-A (extends Thread)");
        MyThread thread2 = new MyThread("Thread-B (extends Thread)");

        thread1.start(); // Calls run() method of MyThread
        thread2.start(); // Calls run() method of MyThread

        // Wait for threads to finish before proceeding (optional, for cleaner output)
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("--- Finished Extending Thread Example ---");


        System.out.println("\n--- Implementing Runnable ---");
        MyRunnable runnableTask1 = new MyRunnable("Task-X (implements Runnable)");
        MyRunnable runnableTask2 = new MyRunnable("Task-Y (implements Runnable)");

        // Manually creating Thread objects for Runnable tasks
        Thread t3 = new Thread(runnableTask1, "Thread-C (for Task-X)");
        Thread t4 = new Thread(runnableTask2, "Thread-D (for Task-Y)");

        t3.start(); // Calls run() method of MyRunnable (runnableTask1)
        t4.start(); // Calls run() method of MyRunnable (runnableTask2)

        // Or using the convenience start method in MyRunnable
        // MyRunnable runnableTask3 = new MyRunnable("Task-Z (implements Runnable)");
        // runnableTask3.start();

        try {
            t3.join();
            t4.join();
            // if (runnableTask3.thread != null) runnableTask3.thread.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("--- Finished Implementing Runnable Example ---");

        // Using Runnable with Lambda (Java 8+)
        System.out.println("\n--- Runnable with Lambda ---");
        Runnable lambdaTask = () -> {
            System.out.println("Running lambda task on thread " + Thread.currentThread().getName());
            try {
                for(int i = 2; i > 0; i--) {
                    System.out.println("Lambda Task, Count: " + i + " (Thread: " + Thread.currentThread().getName() + ")");
                    Thread.sleep(60);
                }
            } catch (InterruptedException e) {
                System.out.println("Lambda task interrupted.");
            }
            System.out.println("Lambda task exiting.");
        };

        Thread t5 = new Thread(lambdaTask, "Thread-E (for Lambda)");
        t5.start();
        try {
            t5.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("--- Finished Runnable with Lambda Example ---");
    }
}

Why Runnable is generally preferred:

  1. Decoupling: It separates the task (what to do) from the worker (the thread that does it).
  2. Flexibility: Your task class can still extend another class if needed, as Java does not support multiple inheritance of classes.
  3. Reusability: A Runnable can be submitted to an ExecutorService or other concurrency utilities more easily.

14. StringBuilder vs StringBuffer

Both StringBuilder and StringBuffer are mutable sequence of characters, used when you need to perform many modifications to strings (concatenation, insertion, deletion). The String class in Java is immutable.

String (briefly, for context)

  • Immutable. Every modification creates a new String object.
  • Inefficient for frequent modifications (e.g., building a string in a loop).

StringBuffer

  • Definition: A mutable sequence of characters.
  • Thread-Safety: Synchronized. Its methods (like append(), insert(), delete()) are synchronized, making it thread-safe. This means only one thread can access a StringBuffer object's methods at a time.
  • Performance: Due to synchronization, it's slower than StringBuilder in a single-threaded environment.
  • Introduced: Java 1.0.

StringBuilder

  • Definition: A mutable sequence of characters.
  • Thread-Safety: Not synchronized. Its methods are not synchronized, making it non-thread-safe.
  • Performance: Faster than StringBuffer in a single-threaded environment because it doesn't have the overhead of synchronization.
  • Introduced: Java 5, as a non-synchronized alternative to StringBuffer.

Key Differences

Feature StringBuffer StringBuilder
Mutability Mutable Mutable
Thread-Safety Synchronized (thread-safe) Not synchronized (not thread-safe)
Performance Slower due to synchronization overhead Faster (no synchronization overhead)
Introduced Java 1.0 Java 5
Typical Use Multi-threaded environments where shared mutable string is needed (rarely, as better concurrency utilities exist). Single-threaded environments, or when synchronization is handled externally. Most common choice for string building.

When to use which?

  • StringBuilder: Use in most cases, especially in single-threaded applications or when the StringBuilder instance is confined to a single thread (e.g., local variable within a method). This is the common recommendation.
  • StringBuffer: Use only if you need a mutable string that will be accessed and modified by multiple threads concurrently, and you need built-in synchronization. However, even in multi-threaded scenarios, it's often better to use StringBuilder within synchronized blocks or use other concurrency utilities if complex coordination is needed.
  • String: Use for fixed string values or when immutability is desired. String concatenation with + is often optimized by the compiler to use StringBuilder under the hood for simple cases, but explicit StringBuilder is better for loops.

Example

public class StringBuilderStringBufferExample {

    public static final int ITERATIONS = 100000;

    public static void main(String[] args) {
        // --- String Concatenation (for comparison, less efficient in loops) ---
        long startTime = System.nanoTime();
        String str = "";
        for (int i = 0; i < ITERATIONS / 10; i++) { // Reduced iterations for String due to slowness
            str += "a"; // Creates new String object in each iteration
        }
        long endTime = System.nanoTime();
        System.out.println("String concatenation time: " + (endTime - startTime) / 1_000_000.0 + " ms (for " + ITERATIONS/10 + " iterations)");

        // --- StringBuffer ---
        startTime = System.nanoTime();
        StringBuffer sBuffer = new StringBuffer();
        for (int i = 0; i < ITERATIONS; i++) {
            sBuffer.append("a");
        }
        endTime = System.nanoTime();
        System.out.println("StringBuffer append time:  " + (endTime - startTime) / 1_000_000.0 + " ms");

        // --- StringBuilder ---
        startTime = System.nanoTime();
        StringBuilder sBuilder = new StringBuilder();
        for (int i = 0; i < ITERATIONS; i++) {
            sBuilder.append("a");
        }
        endTime = System.nanoTime();
        System.out.println("StringBuilder append time: " + (endTime - startTime) / 1_000_000.0 + " ms");

        // --- Functionality (same for both StringBuilder and StringBuffer) ---
        StringBuilder sb = new StringBuilder("Hello");
        sb.append(" World");    // "Hello World"
        sb.insert(5, ", Java"); // "Hello, Java World"
        sb.delete(5, 11);       // "Hello World" (deletes ", Java")
        sb.reverse();           // "dlroW olleH"
        String finalString = sb.toString();
        System.out.println("\nFinal string from StringBuilder: " + finalString);

        // --- Thread Safety Demo (Conceptual) ---
        // To truly demonstrate thread-safety issues with StringBuilder vs StringBuffer,
        // you'd need multiple threads modifying the same instance.

        // StringBuffer (thread-safe)
        StringBuffer sharedBuffer = new StringBuffer();
        Runnable taskBuffer = () -> {
            for (int i = 0; i < 1000; i++) {
                sharedBuffer.append("X");
            }
        };

        // StringBuilder (not thread-safe)
        StringBuilder sharedBuilder = new StringBuilder();
        Runnable taskBuilder = () -> {
            for (int i = 0; i < 1000; i++) {
                sharedBuilder.append("Y"); // Potential race condition
            }
        };

        Thread t1 = new Thread(taskBuffer);
        Thread t2 = new Thread(taskBuffer);
        Thread t3 = new Thread(taskBuilder);
        Thread t4 = new Thread(taskBuilder);

        t1.start(); t2.start();
        t3.start(); t4.start();

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

        System.out.println("\nShared StringBuffer length (expected 2000): " + sharedBuffer.length());
        // For StringBuilder, the length might not be 2000 due to race conditions.
        // This simple append might not always show it, more complex operations would.
        System.out.println("Shared StringBuilder length (potentially < 2000): " + sharedBuilder.length());
        // To make StringBuilder safe in a multi-threaded context, you would do:
        // synchronized(sharedBuilder) { sharedBuilder.append("Y"); }
    }
}

Output for the performance part will show StringBuilder is generally faster than StringBuffer, and both are much faster than repeated String concatenation in a loop. The thread-safety demo for length might consistently show 2000 for StringBuilder in simple appends because append(char) operations might be atomic enough on some JVMs/OSs for this trivial case. A more complex sequence of operations or more contention would more reliably show data corruption or incorrect lengths with StringBuilder in a concurrent setting without external synchronization.


15. Synchronized methods vs Synchronized blocks

Both are mechanisms in Java to control access to shared resources by multiple threads, preventing race conditions and ensuring data consistency. They work by acquiring an intrinsic lock (also called a monitor lock) associated with an object.

Synchronized Methods

  • Definition: A method declared with the synchronized keyword.
  • Locking:
    • Instance Method: When a synchronized instance method is called, the thread acquires the intrinsic lock for that specific instance (the this object) of the class. No other thread can execute any other synchronized instance method on the same object until the lock is released.
    • Static Method: When a synchronized static method is called, the thread acquires the intrinsic lock for the Class object associated with that class. No other thread can execute any other synchronized static method in the same class until the lock is released.
  • Scope: The entire method is synchronized. The lock is acquired when the method is entered and released when the method exits (either normally or due to an unhandled exception).
  • Granularity: Coarser-grained locking. If only a small part of the method needs synchronization, the entire method still gets locked, potentially reducing concurrency.

Synchronized Blocks

  • Definition: A block of code marked with the synchronized keyword, followed by an object reference in parentheses. synchronized (objectReference) { // code to be synchronized }
  • Locking: The thread acquires the intrinsic lock for the object specified in objectReference. No other thread can execute another synchronized block on the same objectReference until the lock is released.
  • Object for Locking:
    • this: Locks on the current instance (similar to a synchronized instance method, but for a specific block).
    • MyClass.class: Locks on the Class object (similar to a synchronized static method, but for a specific block).
    • Any other object: Can use a dedicated lock object (e.g., private final Object lock = new Object();). This is often preferred for finer-grained control and to avoid unintended lock contention.
  • Scope: Only the code within the block is synchronized.
  • Granularity: Finer-grained locking. Allows you to synchronize only the critical sections of code that access shared resources, leaving other parts of the method non-synchronized, potentially improving concurrency.

Key Differences

Feature Synchronized Method Synchronized Block
Scope Entire method body. Specific block of code within a method.
Lock Object Implicit: this for instance methods, ClassName.class for static methods. Explicit: Specified object (objectReference).
Granularity Coarser-grained. Finer-grained, more flexible.
Flexibility Less flexible in choosing the lock object. More flexible; can use different locks for different blocks even in the same method.
Performance Can lead to lower concurrency if the whole method doesn't need synchronization. Can offer better concurrency by only locking critical sections.
Readability Can be simpler for methods where the entire logic needs to be atomic. Can make code slightly more complex but offers more control.

When to use which?

  • Synchronized Method: Use when the entire logic of a method needs to be executed atomically with respect to other synchronized methods on the same object (or class for static methods). It's simpler to write.
  • Synchronized Block:
    • Use when only a part of a method needs synchronization.
    • Use when you need to synchronize on an object other than this or ClassName.class.
    • Use when different critical sections within the same class need to be protected by different locks to allow more concurrency (e.g., one block locks on lock1, another on lock2).
    • Often preferred for better performance and finer control, especially in complex concurrent applications. Using private dedicated lock objects (private final Object lock = new Object();) is a good practice to avoid exposing locks and potential deadlocks if external code tries to lock on your public objects.

Example

class Counter {
    private int count = 0;
    private int anotherCount = 0;

    // Lock object for finer-grained synchronization
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    // Synchronized instance method (locks on 'this' object)
    public synchronized void incrementMethodSync() {
        // Entire method is synchronized
        System.out.println(Thread.currentThread().getName() + " acquired lock for incrementMethodSync on " + this);
        count++;
        try { Thread.sleep(10); } catch (InterruptedException e) {} // Simulate work
        System.out.println(Thread.currentThread().getName() + " releasing lock for incrementMethodSync");
    }

    // Synchronized static method (locks on 'Counter.class' object)
    private static int staticCount = 0;
    public static synchronized void incrementStaticMethodSync() {
        System.out.println(Thread.currentThread().getName() + " acquired lock for incrementStaticMethodSync on Counter.class");
        staticCount++;
        try { Thread.sleep(10); } catch (InterruptedException e) {}
        System.out.println(Thread.currentThread().getName() + " releasing lock for incrementStaticMethodSync");
    }


    // Method using synchronized block (locks on 'this' object)
    public void incrementBlockThisSync() {
        // Non-synchronized part
        System.out.println(Thread.currentThread().getName() + " executing non-sync part before block (incrementBlockThisSync)");
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " acquired lock for block on " + this);
            count++;
            try { Thread.sleep(10); } catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + " releasing lock for block on " + this);
        }
        // Non-synchronized part
        System.out.println(Thread.currentThread().getName() + " executing non-sync part after block (incrementBlockThisSync)");
    }

    // Method using synchronized block with a dedicated lock object
    public void incrementBlockDedicatedLock() {
        System.out.println(Thread.currentThread().getName() + " executing non-sync part before block (incrementBlockDedicatedLock)");
        synchronized (lock1) {
            System.out.println(Thread.currentThread().getName() + " acquired lock1");
            anotherCount++;
             try { Thread.sleep(10); } catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + " releasing lock1");
        }
    }

    public void anotherOperationWithDifferentLock() {
        synchronized (lock2) {
            System.out.println(Thread.currentThread().getName() + " acquired lock2 for another operation");
            // Perform some other critical operation on a different resource
            try { Thread.sleep(10); } catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + " releasing lock2");
        }
    }

    public int getCount() {
        return count;
    }
    public int getAnotherCount() {
        return anotherCount;
    }
    public static int getStaticCount() {
        return staticCount;
    }
}

public class SyncMethodVsBlockExample {
    public static void main(String[] args) throws InterruptedException {
        Counter c1 = new Counter();
        Counter c2 = new Counter(); // Different instance

        // Test synchronized instance method
        // Threads will contend for lock on c1 if they call incrementMethodSync on c1
        Thread t1 = new Thread(() -> { for(int i=0; i<3; i++) c1.incrementMethodSync(); }, "T1-MethodSync-c1");
        Thread t2 = new Thread(() -> { for(int i=0; i<3; i++) c1.incrementMethodSync(); }, "T2-MethodSync-c1");
        // This thread operates on a different object c2, so it won't contend with t1, t2 for c1's lock
        Thread t_c2 = new Thread(() -> { for(int i=0; i<3; i++) c2.incrementMethodSync(); }, "T_MethodSync-c2");

        // Test synchronized block on 'this'
        Thread t3 = new Thread(() -> { for(int i=0; i<3; i++) c1.incrementBlockThisSync(); }, "T3-BlockThisSync-c1");

        // Test synchronized block on dedicated lock
        // These two threads can run concurrently if they use different lock objects (lock1 vs lock2)
        // or if they operate on different Counter instances.
        // If incrementBlockDedicatedLock and anotherOperationWithDifferentLock are called on SAME c1 instance,
        // they can run concurrently because they use different locks (lock1, lock2).
        Thread t4 = new Thread(() -> { for(int i=0; i<3; i++) c1.incrementBlockDedicatedLock(); }, "T4-BlockLock1-c1");
        Thread t5 = new Thread(() -> { for(int i=0; i<3; i++) c1.anotherOperationWithDifferentLock(); }, "T5-BlockLock2-c1");

        // Test static synchronized method
        // Threads will contend for Counter.class lock
        Thread t6 = new Thread(() -> { for(int i=0; i<3; i++) Counter.incrementStaticMethodSync(); }, "T6-StaticSync");
        Thread t7 = new Thread(() -> { for(int i=0; i<3; i++) Counter.incrementStaticMethodSync(); }, "T7-StaticSync");


        System.out.println("--- Starting Method Sync Demo (on c1) ---");
        t1.start(); t2.start(); t_c2.start();
        t1.join(); t2.join(); t_c2.join();
        System.out.println("c1.count after method sync: " + c1.getCount()); // Expected: 6
        System.out.println("c2.count after method sync: " + c2.getCount()); // Expected: 3

        c1.count = 0; // Reset for next test

        System.out.println("\n--- Starting Block 'this' Sync Demo (on c1) ---");
        t3.start(); // Let's start another one to see contention
        Thread t3_contend = new Thread(() -> { for(int i=0; i<3; i++) c1.incrementBlockThisSync(); }, "T3_Contend-BlockThisSync-c1");
        t3_contend.start(); t3.join(); t3_contend.join();
        System.out.println("c1.count after block 'this' sync: " + c1.getCount()); // Expected: 6

        System.out.println("\n--- Starting Block Dedicated Lock Demo (on c1, different locks) ---");
        // t4 and t5 use different locks on the same object c1, so they can run more concurrently
        t4.start(); t5.start();
        t4.join(); t5.join();
        System.out.println("c1.anotherCount after block dedicated lock: " + c1.getAnotherCount()); // Expected: 3

        System.out.println("\n--- Starting Static Method Sync Demo ---");
        t6.start(); t7.start();
        t6.join(); t7.join();
        System.out.println("Counter.staticCount after static sync: " + Counter.getStaticCount()); // Expected: 6
    }
}

I will continue with the next set of questions.