• Serialization & Deserialization with Immutable Classes

    Serialization is the process of converting an object into a byte stream, and deserialization is reconstructing the object back from the stream.

    For immutable classes, special care is needed because:

    1. Fields are declared final.
    2. We rely on deep copy and defensive copies to ensure immutability.
    3. The Java deserialization mechanism bypasses the constructor โ€” meaning your deep copy logic in the constructor wonโ€™t automatically run during deserialization.

    โœ… Problem Scenario

    Imagine you have an immutable class with a mutable field (Date).

    import java.io.Serializable;
    import java.util.Date;
    
    public final class Employee implements Serializable {
        private static final long serialVersionUID = 1L;
    
        private final String name;
        private final Date joiningDate; // mutable field
    
        public Employee(String name, Date joiningDate) {
            this.name = name;
            this.joiningDate = new Date(joiningDate.getTime()); // defensive copy
        }
    
        public String getName() {
            return name;
        }
    
        public Date getJoiningDate() {
            return new Date(joiningDate.getTime()); // defensive copy
        }
    }
    

    ๐Ÿ‘‰ During normal construction, joiningDate is safely deep-copied.
    ๐Ÿ‘‰ But during deserialization, Java restores fields directly from the stream, bypassing the constructor โ€” so no defensive copy is made.

    This means the deserialized object might hold a direct reference to the original mutable object stored in the stream, breaking immutability.

    โœ… Solution: Custom readObject Method

    To maintain immutability, override the readObject method and re-apply defensive copying:

    private void readObject(java.io.ObjectInputStream in)
            throws java.io.IOException, ClassNotFoundException {
        in.defaultReadObject();
    
        // Defensive copy after deserialization
        if (joiningDate != null) {
            // Reflection sets final field, so we must ensure immutability manually
            java.lang.reflect.Field field;
            try {
                field = Employee.class.getDeclaredField("joiningDate");
                field.setAccessible(true);
                field.set(this, new Date(joiningDate.getTime())); // deep copy
            } catch (Exception e) {
                throw new java.io.InvalidObjectException("Failed to maintain immutability");
            }
        }
    }
    

    โœ… Now even after deserialization, the joiningDate field is safely deep-copied.

    โœ… Alternative: readResolve Method

    Another technique is using readResolve, which allows you to replace the deserialized object with a properly constructed one:

    private Object readResolve() {
        // Recreate immutable object using constructor
        return new Employee(this.name, this.joiningDate);
    }
    

    โœ… This ensures your constructor logic (including deep copy) is applied.

    โš ๏ธ Things to Be Careful About

    1. Final Fields and Reflection
      • Java deserialization sets final fields via reflection, so ensure your custom logic (readObject or readResolve) maintains immutability.
    2. Serialization Proxy Pattern (Best Practice)
      • Instead of allowing direct serialization of your immutable class, use a proxy object that handles serialization safely.
      Example:
    private Object writeReplace() {
        return new SerializationProxy(this);
    }
    
    private Object readResolve() {
        throw new java.io.InvalidObjectException("Proxy required");
    }
    
    private static class SerializationProxy implements Serializable {
        private final String name;
        private final Date joiningDate;
    
        SerializationProxy(Employee e) {
            this.name = e.name;
            this.joiningDate = e.joiningDate;
        }
    
        private Object readResolve() {
            return new Employee(name, joiningDate); // constructor ensures immutability
        }
    }

    โœ… This is the safest approach for immutable classes.

    ๐Ÿ”‘ Summary

    • Issue: Deserialization bypasses constructor, so deep copy logic isnโ€™t applied.
    • Solution:
      • Use readObject to manually enforce immutability.
      • Or use readResolve to reconstruct via constructor.
      • Best practice: adopt Serialization Proxy Pattern for maximum safety.
  • Deep Copy in Constructor for Immutable Classes in Java

    1. Mutable Object Passed in Constructor

    • Example: If a Date or ArrayList is passed to the constructor, the caller might still hold a reference to that object and modify it later.
    • Solution: Perform deep copy inside the constructor.
    public final class Student {
        private final String name;
        private final Date dob; // mutable
    
        public Student(String name, Date dob) {
            this.name = name;
            this.dob = new Date(dob.getTime()); // defensive copy
        }
    }

    โœ… This ensures the Student object has its own copy, independent of the callerโ€™s object.

    2. Returning Mutable Objects in Getters

    • If you directly return the mutable field, external code can modify it.
    • Example:
    public Date getDob() {
        return dob; // โŒ exposes internal state
    }

    Instead, return a copy:

    public Date getDob() {
        return new Date(dob.getTime()); // โœ… defensive copy
    }

    3. Collections as Fields

    • Arrays, Lists, Maps, and Sets are mutable. If directly exposed, immutability is lost.
    • Example:
    public final class Department {
      private final List < String > employees;
    
      public Department(List < String > employees) {
        this.employees = new ArrayList < >(employees); // deep copy
      }
    
      public List < String > getEmployees() {
        return new ArrayList < >(employees); // defensive copy
      }
    }

    โœ… Both constructor and getter ensure immutability.

    4. Nested Mutable Objects

    • If your mutable field contains other mutable objects (like List<Person>), you need to ensure deep copying of each element, not just the collection.
    • Example:
    public final class Team {
      private final List < Person > members;
    
      public Team(List < Person > members) {
        this.members = new ArrayList < >();
        for (Person p: members) {
          this.members.add(new Person(p)); // assume Person has copy constructor
        }
      }
    
      public List < Person > getMembers() {
        List < Person > copy = new ArrayList < >();
        for (Person p: members) {
          copy.add(new Person(p));
        }
        return copy;
      }
    }

    โœ… Each Person is copied, ensuring no shared reference.

    โš ๏ธ What to Be Careful About

    1. Shallow Copy vs Deep Copy
      • Shallow Copy: Copies only the reference (object identity remains shared).
      • Deep Copy: Copies the actual object state, ensuring no external modification.
    2. Performance Overhead
      • Deep copying collections or nested objects can be costly in terms of memory and CPU.
      • If immutability isnโ€™t critical, consider alternatives like unmodifiable collections (Collections.unmodifiableList()).
    3. Unmodifiable Wrappers
      • Instead of deep copying, sometimes you can wrap collections in unmodifiable wrappers:

    Example :

    import java.util.Collections;
    
    public final class Department {
      private final List < String > employees;
    
      public Department(List < String > employees) {
        this.employees = Collections.unmodifiableList(new ArrayList < >(employees));
      }
    
      public List < String > getEmployees() {
        return employees; // already unmodifiable
      }
    }

    โœ… This prevents external modification but is lighter than full deep copy.

    Serialization and Deserialization

    • When your immutable class is serialized and deserialized, ensure the deep copy logic is preserved (custom read methods if needed).
    • ๐Ÿ‘‰ โ€œTo dive deeper into how serialization affects immutability and how to preserve thread-safety with techniques like readObject, readResolve, and the Serialization Proxy Pattern, check out our detailed guide Serialization in Immutable Classes in Java.โ€

    Thread Safety

    • Even with defensive copies, if you mistakenly expose a mutable reference, multiple threads can still modify the object โ†’ immutability breaks.
    • Always validate return values and constructor inputs.

    ๐Ÿ“Œ Quick Rules

    • Always deep copy mutable objects in both constructor and getters.
    • Prefer unmodifiable wrappers if deep copy is too costly.
    • Check nested mutable objects (not just top-level fields).
    • Use immutable alternatives whenever possible (LocalDate instead of Date, List.of() instead of ArrayList, etc.).

    ๐Ÿ”„ Deep Copy vs Unmodifiable Wrapper vs Immutable Alternative

    ApproachDescriptionProsConsBest Use Case
    Deep CopyCreates a completely new copy of the mutable object (including nested objects).โœ… Guarantees full immutability
    โœ… Safe from external modification
    โŒ Performance overhead (memory & CPU)
    โŒ Complex for deep/nested structures
    When you must protect complex mutable data (e.g., deep object graphs, List<Person>)
    Unmodifiable WrapperWraps a mutable object in an unmodifiable view (Collections.unmodifiableList()).โœ… Lightweight
    โœ… Prevents modification through returned collection
    โœ… Faster than deep copy
    โŒ Underlying collection can still change if reference is modified elsewhereWhen you only need to prevent external changes, not full immutability
    Immutable AlternativeUse built-in immutable types (String, LocalDate, List.of(), Map.of()).โœ… No need for copying
    โœ… Thread-safe by design
    โœ… Cleaner code
    โŒ Not always available (older Java versions)
    โŒ May not cover all use cases
    Prefer whenever possible (dates, simple collections, constants)

    โœ…Real-World Example: Department Class with Immutable Handling

    Suppose we have a Department that holds a list of employee names. Weโ€™ll implement immutability using:

    1. Deep Copy
    2. Unmodifiable Wrapper
    3. Immutable Alternative

    1๏ธโƒฃ Deep Copy Approach

    import java.util.ArrayList;
    import java.util.List;
    
    public final class DepartmentDeepCopy {
      private final List < String > employees;
    
      public DepartmentDeepCopy(List < String > employees) {
        // Deep copy the list
        this.employees = new ArrayList < >(employees);
      }
    
      public List < String > getEmployees() {
        // Return a new copy each time
        return new ArrayList < >(employees);
      }
    
      @Override
      public String toString() {
        return "DepartmentDeepCopy{" + "employees=" + employees + '}';
      }
    }

    โœ… Fully immutable, but copying occurs both in constructor and getter โ†’ overhead for large lists.

    2๏ธโƒฃ Unmodifiable Wrapper Approach

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public final class DepartmentUnmodifiable {
      private final List < String > employees;
    
      public DepartmentUnmodifiable(List < String > employees) {
        // Copy once, then wrap
        this.employees = Collections.unmodifiableList(new ArrayList < >(employees));
      }
    
      public List < String > getEmployees() {
        // Safe to return directly (already unmodifiable)
        return employees;
      }
    
      @Override
      public String toString() {
        return "DepartmentUnmodifiable{" + "employees=" + employees + '}';
      }
    }

    โœ… Lightweight and prevents modification via returned list.
    โš ๏ธ But if the original employees list passed to the constructor is later modified, it wonโ€™t affect this class (because of the copy) โ†’ safe.

    3๏ธโƒฃ Immutable Alternative (Java 9+)

    import java.util.List;
    
    public final class DepartmentImmutableAlt {
      private final List < String > employees;
    
      public DepartmentImmutableAlt(List < String > employees) {
        // Directly create an immutable list
        this.employees = List.copyOf(employees);
      }
    
      public List < String > getEmployees() {
        return employees; // already immutable
      }
    
      @Override
      public String toString() {
        return "DepartmentImmutableAlt{" + "employees=" + employees + '}';
      }
    }

    โœ… No need for manual copying or wrapping.
    โœ… Simple and efficient.
    โš ๏ธ Requires Java 9+ (List.copyOf() or List.of()).


    ๐Ÿ”Ž Testing All Three

    import java.util.ArrayList;
    import java.util.List;
    
    public class MainTest {
        public static void main(String[] args) {
            List<String> employees = new ArrayList<>();
            employees.add("Alice");
            employees.add("Bob");
    
            DepartmentDeepCopy dept1 = new DepartmentDeepCopy(employees);
            DepartmentUnmodifiable dept2 = new DepartmentUnmodifiable(employees);
            DepartmentImmutableAlt dept3 = new DepartmentImmutableAlt(employees);
    
            System.out.println(dept1);
            System.out.println(dept2);
            System.out.println(dept3);
    
            // Try modifying original list
            employees.add("Charlie");
    
            System.out.println("After modifying original list:");
            System.out.println(dept1); // Unchanged
            System.out.println(dept2); // Unchanged
            System.out.println(dept3); // Unchanged
    
            // Try modifying through getter
            try {
                dept1.getEmployees().add("David"); // modifies copy only
                dept2.getEmployees().add("David"); // throws UnsupportedOperationException
                dept3.getEmployees().add("David"); // throws UnsupportedOperationException
            } catch (Exception e) {
                System.out.println("Exception: " + e);
            }
    
            System.out.println("Final states:");
            System.out.println(dept1);
            System.out.println(dept2);
            System.out.println(dept3);
        }
    }
    

    โœ… Output :

    DepartmentDeepCopy{employees=[Alice, Bob]}
    DepartmentUnmodifiable{employees=[Alice, Bob]}
    DepartmentImmutableAlt{employees=[Alice, Bob]}
    After modifying original list:
    DepartmentDeepCopy{employees=[Alice, Bob]}
    DepartmentUnmodifiable{employees=[Alice, Bob]}
    DepartmentImmutableAlt{employees=[Alice, Bob]}
    Exception: java.lang.UnsupportedOperationException
    Final states:
    DepartmentDeepCopy{employees=[Alice, Bob]}
    DepartmentUnmodifiable{employees=[Alice, Bob]}
    DepartmentImmutableAlt{employees=[Alice, Bob]}
  • Flexible Constructor Bodies in Java 25 (JEP 513) Made Simple

    What Is JEP 513?

    Java 25 introduces Flexible Constructor Bodies (JEP 513), a feature that makes constructors more flexible and easier to write. With this feature, you can now write code before calling super() or this() inside a constructor.

    This means you can:

    • Validate inputs before passing them to the parent class.
    • Initialize fields before the parent constructor runs.
    • Avoid using static helper methods for simple tasks.

    This change makes your code cleaner and safer, especially when dealing with validation or initialization logic.

    Why This Feature Was Needed

    Before Java 25, the first line of a constructor had to be super(...) or this(...).

    This caused some problems:

    • You couldnโ€™t validate input before calling the parent constructor.
    • You often needed static helper methods to handle validation.
    • Fields in the subclass couldnโ€™t be initialized before the parent constructor ran.

    ๐Ÿ‘‰ In short: constructors felt rigid and awkward.

    What Changed in Java 25

    Now, in Java 25, you can:

    • Write statements before super(...) or this(...).
    • Validate input directly inside the constructor.
    • Assign values to fields before calling the parent constructor.

    โš ๏ธ Rules to remember:

    • You cannot use this, call instance methods, or read instance fields before super(...).
    • You can only assign values to fields.

    Example 1: Validation Before Super

    Before Java 25 (old way):

    public class Child extends Parent {
        private final String normalizedName;
    
        public Child(String name) {
            super(validateAndNormalize(name)); // must be first
            this.normalizedName = validateAndNormalize(name);
        }
    
        private static String validateAndNormalize(String s) {
            if (s == null || s.isBlank()) {
                throw new IllegalArgumentException("Name required");
            }
            return s.trim().toUpperCase();
        }
    }
    

    Problems: Duplicate validation and a static helper method needed.

    After Java 25 (New Way):

    public class Child extends Parent {
        private final String normalizedName;
    
        public Child(String name) {
            if (name == null || name.isBlank()) {
                throw new IllegalArgumentException("Name required");
            }
            String norm = name.trim().toUpperCase();
            this.normalizedName = norm;  // field initialized
            super(norm);                 // parent constructor called
        }
    }
    

    โœ… Cleaner, no duplicate validation, no static helper required.

    ๐Ÿ‘‰In the new approach, you can validate the input and initialize the field before calling super(), making the code more readable and less error-prone.

    Example 2: Fixing a Classic Bug

    Sometimes the parent constructor calls a method overridden by the child. In older versions, subclass fields might not be ready yet. causing bugs.

    Old behavior:

    class Parent {
        Parent() { setup(); }
        void setup() { System.out.println("Parent setup"); }
    }
    
    class Sub extends Parent {
        private int value;
    
        Sub(int v) {
            this.value = v; // runs AFTER super()
        }
    
        @Override
        void setup() {
            System.out.println("value = " + value);
        }
    }

    Output: value = 0 (because value wasnโ€™t set before setup() ran).

    New Java 25 behavior:

    class Sub extends Parent {
        private int value;
    
        Sub(int v) {
            this.value = v;  // initialize field first
            super();         // parent constructor runs safely
        }
    }

    Output: value = 5 (or whatever you passed).

    Key Points to Remember

    1. No this or instance method calls before super().
    2. Field initialization is allowed before super().
    3. Helps you write cleaner and safer constructors.
    4. Eliminates the need for static helper methods for validation or initialization.

    When to Use Flexible Constructor Bodies

    • To validate inputs naturally inside constructors.
    • To initialize subclass fields before parent methods run.
    • To prepare values once and reuse them in both subclass and parent constructors.

    ๐Ÿš€ Why It Matters

    • Cleaner code: You can write validation and initialization logic directly in the constructor.
    • Safer initialization: Prevents issues where the parent constructor might call overridden methods before the subclass is fully initialized.
    • No need for static helpers: Eliminates the need for static methods to handle validation or initialization.

    Key Takeaways

    • Java 25 makes constructors more flexible.
    • You can now write logic before super(...).
    • Great for validation, field setup, and cleaner code.
    • Just remember: no this or method calls before super(...).

    Further Reading

  • Java 25: Compact Source Files and Instance Main Methods (JEP 512)

    Java 25 finalizes JEP 512, which makes small programs far less noisy: you can write a main as an instance method (no static required) and you can write compact source files โ€” source files that implicitly declare a class for any top-level fields and methods. A light-weight java.lang.IO helper is available for beginner-friendly console I/O, and compact source files automatically gain access to java.base classes to reduce imports.

    1. Intro โ€” why this matters

    For decades the first Java program looked like a barrier:

    public class HelloWorld {
        public static void main(String[] args) {
            System.out.println("Hello, World!");
        }
    }

    That ceremony taught important concepts, but it also made the very first step into Java harder than necessary. JEP 512 gives a safe, compatible on-ramp: small programs can now be written with far less boilerplate while still being ordinary Java and fully upgradable into โ€œrealโ€ Java classes as needed. This change is finalized in JDK 25.


    2. What changed (short list)

    • Compact source files: a source file that contains top-level fields and methods (not inside a class) is treated as implicitly declaring a final top-level class whose members are those fields/methods. The class is unnamed and lives in the unnamed package/module. The compiler enforces that the file must have a launchable main.
    • Instance main methods: main may be declared as an instance method (for example void main() or void main(String[] args)); when needed the launcher will instantiate the class and invoke the instance main. This reduces the mental load of public static void main(String[] args).
    • java.lang.IO helper: a small console I/O helper (methods like IO.println(...) and IO.readln(...)) is provided in java.lang for convenience and is therefore available without imports. Its implementation uses System.out/System.in. Note: the static methods are not implicitly statically imported into compact source files โ€” you call them as IO.println(...).
    • Automatic java.base import in compact files: compact source files act as if the entire java.base module were imported on demand โ€” common classes like List, Map, BigInteger, etc., are directly usable without an explicit import.

    2. Compact Source Files

    Before Java 25, every program must be inside a class, and the entry point was always:

    public class HelloWorld {
        public static void main(String[] args) {
            System.out.println("Hello, World!");
        }
    }

    This was verbose for small demos or scripts.

    ๐Ÿ‘‰ With compact source files, you can now write top-level methods and fields directly in a .java file. The compiler automatically wraps them inside an implicit final class behind the scenes.

    Example 1: Hello World in a compact source file

    // Hello.java
    void main() {
        IO.println("Hello, World!");
    }

    Run it directly:

    java Hello.java

    Output:

    Hello, World!

    โžก๏ธ No class, no static, no public. Just a simple entry point.

    Example 2: Using fields and helper methods

    You can declare fields and helper methods at the top level. They all become members of the implicit class.

    // Greet.java
    String greeting = "Welcome to Java 25!";
    
    String shout(String text) {
        return text.toUpperCase();
    }
    
    void main() {
        IO.println(shout(greeting));
    }

    Run:

    java Greet.java
    

    Output:

    WELCOME TO JAVA 25!

    โžก๏ธ The compiler internally creates an unnamed class with greeting and shout as members.

    3. Instance main Methods

    Traditionally, Java only supported:

    public static void main(String[] args)

    With Java 25, you can now declare main as an instance method.

    Example 3: Instance main without arguments

    // Greeter.java
    class Greeter {
        void main() {
            IO.println("Hello from an instance main!");
        }
    }

    Run:

    java Greeter.java

    Output:

    Hello from an instance main!

    โžก๏ธ The launcher automatically creates an object of Greeter and invokes its main.

    Example 4: Instance main with arguments

    // Echo.java
    class Echo {
        void main(String[] args) {
            for (var arg : args) {
                IO.println("Arg: " + arg);
            }
        }
    }

    Run:

    java Echo.java one two three

    Output:

    Arg: one
    Arg: two
    Arg: three

    โžก๏ธ Both static and instance forms are supported:

    • void main()
    • void main(String[] args)

    4. IO Console Helper

    To make small programs even simpler, Java 25 introduces java.lang.IO, a helper class for console input/output.

    • Before Java 25: System.out.println("Hello, Java!");
    • Now: IO.println("Hello, Java!");

    Example 5: Using IO helper

    void main() {
        IO.println("Enter your name: ");
        String name = IO.readln();
        IO.println("Hello, " + name + "!");
    }

    Run and interact:

    Enter your name: 
    Ashish
    Hello, Ashish!

    โžก๏ธ No need to remember System.out or set up a Scanner.

    5. Automatic Imports

    In compact source files, common Java APIs from java.base (like List, Map, BigInteger) are automatically available without import.

    // Numbers.java
    void main() {
        var numbers = List.of(10, 20, 30);
        IO.println("Numbers: " + numbers);
    }
    

    No import java.util.List; needed.

    6. How Compact Source Evolves into a Class

    Compact source files are not a new language, theyโ€™re just a shortcut.
    You can always โ€œgrowโ€ them into normal Java classes.

    Compact form:

    void main() {
        IO.println("Quick start in Java 25!");
    }

    Expanded form:

    class QuickStart {
        void main() {
            IO.println("Quick start in Java 25!");
        }
    }

    This ensures seamless transition from beginner scripts to full OOP programs.

    7. Important gotchas & notes

    • Implicit class is unnamed and final โ€” the compiler generates a name you must not rely on in source code; you cannot new the implicitly-declared class from within the file. It’s meant as an entry-point, not a reusable API.
    • IO helper methods not implicitly imported as bare names โ€” you must call IO.println(...) (they live in java.lang so no import is needed, but static implicit import into compact files was intentionally removed).
    • No statements outside methods โ€” compact source files still require methods (you cannot write top-level statements that are executed as a script body; the design requires methods and fields so programs can grow).
    • Launcher lookup rules โ€” if a classic public static void main(String[]) exists it retains priority; instance main paths are fallbacks or alternatives. See the JEP/JVM spec for exact lookup semantics.

    8. Compatibility & tooling

    • JEP 512 was finalized for JDK 25; tooling (IDE support, linters) are already adding support (IntelliJ and other vendors published guidance shortly after the release). If your build tools or CI assume public static void main always exists, you may want to update them to accept instance mains or use explicit javac + java steps.

    9. Summary of Changes (JEP 512)

    FeatureBefore Java 25Java 25+ (JEP 512)
    Entry point declarationpublic static void main(String[] args)void main() or void main(String[] args) (static or instance)
    Class requirementExplicit public class ...Implicit class (no explicit declaration)
    Console I/OSystem.out.println with importsIO.println (available without import)
    Import statementsManual for core APIsAuto-import for java.base in compact files

    ๐Ÿ‘‰ Read more:

  • Association, Composition, and Aggregation in Java

    ๐Ÿ”นIntroduction

    In Object-Oriented Programming (OOP), classes often need to work together. For example, a Car is made of Engine, Wheels, and Seats. Similarly, a University has Students and Departments.

    In Java, these relationships are represented using:

    • Association
    • Composition
    • Aggregation

    Understanding these relationships helps us design systems that are closer to real-world modeling and easier to maintain.


    ๐Ÿ”น1. Association in Java

    Definition:
    Association represents a relationship between two separate classes that are connected through their objects. It can be one-to-one, one-to-many, many-to-one, or many-to-many.

    ๐Ÿ‘‰ In simple words:

    โ€œAssociation means two classes are related, but neither owns the other.โ€

    โœ… Example: Teacher and Student

    A teacher can teach many students, and a student can learn from multiple teachers. Both can exist independently, but they share a relationship.

    class Teacher {
        private String name;
    
        public Teacher(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    }
    
    class Student {
        private String name;
    
        public Student(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    }
    
    public class AssociationExample {
        public static void main(String[] args) {
            Teacher teacher = new Teacher("Mr. Sharma");
            Student student = new Student("Rahul");
    
            // Association - both exist independently
            System.out.println(student.getName() + " is taught by " + teacher.getName());
        }
    }
    

    ๐Ÿ† Real-world use case:

    • Doctors and Patients
    • Employees and Departments
    • Bank and Customers

    ๐Ÿ”น2. Aggregation in Java (HAS-A Relationship)

    Definition:
    Aggregation is a special form of Association where one class contains a reference to another class. However, the contained object can exist independently of the container.

    ๐Ÿ‘‰ In simple words:

    โ€œAggregation means HAS-A relationship, but the lifecycles are independent.โ€

    โœ… Example: Department and Professor

    A Department has Professors , but Professors can also work in other departments or exist even if the Department is deleted.

    • A Department may have several Professors.
    • If the Department is closed, the Professor objects can still exist independently (they may move to another Department).
    • Since a Professor can exist without being tied to a specific Department, this represents a weak association โ€” also known as Aggregation.
    import java.util.*;
    
    class Professor  {
        String name;
    
        Professor (String name) {
            this.name = name;
        }
    }
    
    class Department {
        String name;
        List<Professor> professors;
    
        Department(String name, List<Professor> professors) {
            this.name = name;
            this.teachers = professors;
        }
    }
    
    public class AggregationExample {
        public static void main(String[] args) {
            Professor p1 = new Professor("Mr. Sharma");
            Professor p2 = new Professor("Ms. Gupta");
    
            List<Professor> professors = new ArrayList<>();
            professors.add(p1);
            professors.add(p2);
    
            Department dept = new Department("Computer Science", professors);
    
            System.out.println("Department: " + dept.name);
            for (Professor p : dept.professors) {
                System.out.println("Professor: " + p.name);
            }
        }
    }
    

    ๐Ÿ† Real-world use case:

    • Library and Books (Books can exist without Library)
    • Team and Players (Players can exist without a Team)
    • Company and Employees

    ๐Ÿ”น3. Composition in Java (Strong HAS-A Relationship)

    Definition:
    Composition is a stronger form of Aggregation where the contained object cannot exist without the container. If the container object is destroyed, the contained object is also destroyed.

    ๐Ÿ‘‰ In simple words:

    โ€œComposition means HAS-A relationship with dependency. The child object cannot live without the parent object.โ€

    โœ… Example: House and Rooms

    A Room cannot exist without a House. If the House is destroyed, Rooms are also gone.

    • A House consists of several Rooms.
    • If the House object is destroyed, all its Room objects will also be destroyed.
    • Since a Room cannot exist without its House, this represents a strong association โ€” also known as Composition.
    class Room {
        private String name;
    
        Room(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    }
    
    class House {
        private Room room;
    
        House(String roomName) {
            this.room = new Room(roomName); // strong dependency
        }
    
        public Room getRoom() {
            return room;
        }
    }
    
    public class CompositionExample {
        public static void main(String[] args) {
            House house = new House("Living Room");
            System.out.println("House has a " + house.getRoom().getName());
        }
    }
    

    ๐Ÿ† Real-world use case:

    • Car and Engine (Engine cannot exist without Car in this context)
    • Human and Heart (Heart cannot exist independently)
    • Laptop and Keyboard

    ๐Ÿ”นKey Differences: Association vs Aggregation vs Composition

    FeatureAssociationAggregationComposition
    TypeGeneral relationshipWeak HAS-AStrong HAS-A
    DependencyObjects are independentContained object can exist without containerContained object cannot exist without container
    LifecycleIndependentSeparate lifecycleDependent lifecycle
    ExampleTeacher โ†” StudentDepartment โ†’ TeacherHouse โ†’ Room

    ๐ŸŽฏSummary

    • Association: Simple relationship, objects are independent.
    • Aggregation: HAS-A relationship, weaker, independent lifecycle.
    • Composition: Strong HAS-A, dependent lifecycle.

    By choosing the correct relationship, you can design better, maintainable, and real-world-like applications in Java.

  • Java super Keyword โ€“ A Complete Guide with Examples

    Introduction

    In Java, the super keyword is a reference variable used to access members (fields, methods, and constructors) of a parent class (also known as the superclass). It plays a crucial role in inheritance, allowing a subclass to interact with its parent class in different ways.

    Think of super as a bridge that connects the child class with its immediate parent class.

    Why Use super?

    • To avoid naming conflicts between superclass and subclass members.
    • To invoke the parent class constructor explicitly.
    • To reuse code from the parent class without rewriting it.

    Types of Usage of super in Java

    1. Accessing Parent Class Variables

    If a subclass defines a variable with the same name as the parent class, super helps access the parent version.

    Example:

    class Animal {
        String name = "Animal";
    }
    
    class Dog extends Animal {
        String name = "Dog";
    
        void displayNames() {
            System.out.println("Subclass name: " + name);
            System.out.println("Superclass name: " + super.name);
        }
    }
    
    public class SuperVariableExample {
        public static void main(String[] args) {
            Dog d = new Dog();
            d.displayNames();
        }
    }
    

    Output:

    Subclass name: Dog
    Superclass name: Animal
    

    ๐Ÿ‘‰ Here, super.name resolves the conflict between the child and parent class variables.

    2. Accessing Parent Class Methods

    When a subclass overrides a method, super can call the overridden method of the parent class.

    Example:

    class Animal {
        void sound() {
            System.out.println("Animal makes a sound");
        }
    }
    
    class Dog extends Animal {
        @Override
        void sound() {
            System.out.println("Dog barks");
        }
    
        void printParentSound() {
            super.sound(); // Calls parent class method
        }
    }
    
    public class SuperMethodExample {
        public static void main(String[] args) {
            Dog d = new Dog();
            d.sound();          // Subclass method
            d.printParentSound(); // Superclass method
        }
    }
    

    Output:

    Dog barks
    Animal makes a sound
    

    3. Calling Parent Class Constructor

    A subclass constructor can explicitly call the parent class constructor using super().
    If not specified, the compiler automatically inserts a super() call to the default constructor of the parent class.

    Example:

    class Animal {
        Animal() {
            System.out.println("Animal constructor called");
        }
    }
    
    class Dog extends Animal {
        Dog() {
            super(); // Calls Animal constructor
            System.out.println("Dog constructor called");
        }
    }
    
    public class SuperConstructorExample {
        public static void main(String[] args) {
            Dog d = new Dog();
        }
    }
    

    Output:

    Animal constructor called
    Dog constructor called
    

    4. Calling Parameterized Constructor of Parent Class

    You can use super(parameterList) to call a parent class constructor with arguments.

    Example:

    class Animal {
        Animal(String type) {
            System.out.println("Animal type: " + type);
        }
    }
    
    class Dog extends Animal {
        Dog(String breed) {
            super("Mammal"); // Calling parent constructor with argument
            System.out.println("Dog breed: " + breed);
        }
    }
    
    public class SuperParameterizedConstructor {
        public static void main(String[] args) {
            Dog d = new Dog("Labrador");
        }
    }
    

    Output:

    Animal type: Mammal
    Dog breed: Labrador
    

    5. super with Method Overriding and Polymorphism

    In real-world applications, we often use super inside overridden methods to extend functionality instead of completely replacing it.

    Example:

    class Vehicle {
        void start() {
            System.out.println("Vehicle is starting...");
        }
    }
    
    class Car extends Vehicle {
        @Override
        void start() {
            super.start(); // Call parent implementation
            System.out.println("Car engine is warming up...");
        }
    }
    
    public class SuperPolymorphism {
        public static void main(String[] args) {
            Car car = new Car();
            car.start();
        }
    }
    

    Output:

    Vehicle is starting...
    Car engine is warming up...
    

    6. Restrictions of super

    • super() must be the first statement in the subclass constructor (until Java 24).
    • You cannot use super in static methods because super refers to an object instance.
    • super can only access immediate parent class members, not grandparents directly.

    Real-Time Example: Employee Management

    Letโ€™s consider a real-world example where super helps reuse parent functionality.

    class Employee {
        String name;
        double salary;
    
        Employee(String name, double salary) {
            this.name = name;
            this.salary = salary;
        }
    
        void displayInfo() {
            System.out.println("Employee: " + name + ", Salary: " + salary);
        }
    }
    
    class Manager extends Employee {
        double bonus;
    
        Manager(String name, double salary, double bonus) {
            super(name, salary); // Call parent constructor
            this.bonus = bonus;
        }
    
        @Override
        void displayInfo() {
            super.displayInfo(); // Reuse parent method
            System.out.println("Bonus: " + bonus);
        }
    }
    
    public class SuperRealTimeExample {
        public static void main(String[] args) {
            Manager m = new Manager("Ashish", 80000, 10000);
            m.displayInfo();
        }
    }
    

    Output:

    Employee: Ashish, Salary: 80000.0
    Bonus: 10000.0

    ๐Ÿš€ New in Java 25: Flexible Constructor Bodies (JEP 513)

    Starting with Java 25, the rule that super(...) or this(...) must be the first statement in a constructor has been relaxed.

    Now you can write statements before calling super(...), as long as you donโ€™t use the partially constructed object (e.g., accessing this fields/methods prematurely).

    This makes it easier to perform validation, pre-computations, or initializing subclass fields before delegating to the parent constructor.

    ๐Ÿ‘Ž Before Java 25

    public class Child extends Parent {
        public Child(String name) {
            super(validate(name)); // validation must be static
        }
    
        private static String validate(String name) {
            if (name == null) throw new IllegalArgumentException();
            return name;
        }
    }
    

    ๐Ÿš€ From Java 25 Onwards

    public class Child extends Parent {
        public Child(String name) {
            if (name == null) throw new IllegalArgumentException(); // direct validation
            super(name);  // cleaner & more natural
        }
    }
    

    โœ… Key Benefits

    • Perform validation directly inside constructors.
    • Initialize subclass fields before calling the parent constructor.
    • Write cleaner, more natural, and less error-prone code.

    This feature is part of Java 25. You can read more in the official Java 25 Release Notes.

    Key Takeaways

    • super is used to access parent class variables, methods, and constructors.
    • It helps resolve conflicts when subclass members override parent members.
    • In Java 25, you can now write code before super(...) calls, making constructors much more flexible.
    • super ensures code reusability, clarity, and better design in inheritance.

    Conclusion

    The super keyword in Java is a powerful tool in object-oriented programming. It allows developers to build on existing functionality instead of rewriting everything from scratch. Whether youโ€™re handling constructors, methods, or variables, super ensures smooth interaction between child and parent classes.

  • Java Final Keyword โ€“ A Complete Guide with Examples

    Introduction

    In Java, the final keyword is a non-access modifier that can be applied to variables, methods, and classes. It is used to impose restrictions and ensure certain values, behaviors, or structures remain unchanged during program execution.

    In simple words:

    • Final variable โ†’ Constant value (cannot be changed once assigned).
    • Final method โ†’ Cannot be overridden.
    • Final class โ†’ Cannot be inherited.

    This makes final a very powerful tool in ensuring immutability, security, and proper design in Java applications.

    1. Final Variables in Java

    When a variable is declared as final, its value cannot be changed once assigned.

    Syntax:

    final double INTEREST_RATE = 0.05;

    Example: Final Variable

    In banking, the interest rate is usually fixed and should not change once set.

    class BankAccount {
        private String accountHolder;
        private double balance;
        final double INTEREST_RATE = 0.05;  // 5% fixed interest rate
    
        BankAccount(String holder, double amount) {
            this.accountHolder = holder;
            this.balance = amount;
        }
    
        void calculateInterest() {
            double interest = balance * INTEREST_RATE;
            System.out.println("Interest for " + accountHolder + " is: " + interest);
        }
    }
    
    public class FinalVariableRealExample {
        public static void main(String[] args) {
            BankAccount acc1 = new BankAccount("Alice", 10000);
            acc1.calculateInterest();
        }
    }

    Key Points:

    • A final variable must be initialized at the time of declaration or inside a constructor.
    • Once assigned, its value cannot be modified.
    • Commonly used for constants (e.g., PI, MAX_VALUE).

    2. Final Methods in Java

    When a method is declared as final, it cannot be overridden by subclasses.

    Example: Final Method

    class Vehicle {
        final void startEngine() {
            System.out.println("Engine started");
        }
    }
    
    class Car extends Vehicle {
        // โŒ This will cause an error
        // void startEngine() {  
        //     System.out.println("Car engine started");
        // }
    }
    
    public class FinalMethodExample {
        public static void main(String[] args) {
            Car car = new Car();
            car.startEngine();
        }
    }

    Why use Final Methods?

    • To prevent subclasses from changing critical methods.
    • Ensures consistent behavior across inheritance hierarchy.

    3. Final Classes in Java

    When a class is declared as final, it cannot be extended (inherited).

    Example: Final Class

    final class Bank {
        void displayBankName() {
            System.out.println("Welcome to XYZ Bank");
        }
    }
    
    // โŒ Compile-time error: Cannot inherit from final class
    // class MyBank extends Bank { }
    
    public class FinalClassExample {
        public static void main(String[] args) {
            Bank bank = new Bank();
            bank.displayBankName();
        }
    }

    Why use Final Classes?

    • To prevent inheritance for security or design reasons.
    • Commonly used in classes like java.lang.String, java.lang.Math, and java.lang.System.

    4. Final Parameters in Java

    You can also declare method parameters as final. This ensures that the parameterโ€™s value cannot be modified inside the method.

    Example: Final Parameter

    public class FinalParameterExample {
        void calculateSquare(final int number) {
            // number = number * number;  // โŒ Error: cannot assign a value to final variable
            System.out.println("Square: " + (number * number));
        }
    
        public static void main(String[] args) {
            FinalParameterExample obj = new FinalParameterExample();
            obj.calculateSquare(5);
        }
    }
    

    5. Blank Final Variable (Uninitialized Final Variable)

    A final variable that is not initialized at declaration time is called a blank final variable.
    It must be initialized in the constructor.

    Example:

    class Student {
        final int rollNumber;  // blank final variable
    
        Student(int roll) {
            rollNumber = roll;  // initialized in constructor
        }
    
        void display() {
            System.out.println("Roll Number: " + rollNumber);
        }
    }
    
    public class BlankFinalExample {
        public static void main(String[] args) {
            Student s1 = new Student(101);
            Student s2 = new Student(102);
    
            s1.display();
            s2.display();
        }
    }

    6. Static Final Variables (Constants)

    A static final variable is used to define constants (commonly written in uppercase).

    Example:

    class Constants {
        static final double PI = 3.14159;
        static final int MAX_USERS = 100;
    }
    
    public class StaticFinalExample {
        public static void main(String[] args) {
            System.out.println("PI: " + Constants.PI);
            System.out.println("Max Users: " + Constants.MAX_USERS);
        }
    }
    

    7. Final with Inheritance and Polymorphism

    • Final variable โ†’ value cannot change.
    • Final method โ†’ cannot override, but can be inherited.
    • Final class โ†’ cannot be subclassed at all.

    This provides control and security in object-oriented design.

    8. Real-world Use Cases of Final Keyword

    1. Constants definition: public static final String COMPANY_NAME = "Google";
    2. Immutable classes: The String class is final to prevent modification.
    3. Preventing override: Security-sensitive methods (e.g., Object.wait(), Object.notify()).
    4. Performance optimization: JVM can optimize final classes and methods better.

    ๐ŸŽฏConclusion

    The final keyword in Java is a simple yet powerful tool to restrict modification, inheritance, and overriding.

    • Use it for constants (final variables).
    • Secure critical methods (final methods).
    • Prevent unwanted inheritance (final classes).

    By understanding and using final effectively, you can write more secure, maintainable, and efficient Java programs.

  • How to remove elements from a list without writing loops or risking ConcurrentModificationException in Java?

    Problem

    How to remove elements from a list without writing loops or risking ConcurrentModificationException?

    Example Code

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    public class RemoveIfDemo {
        public static void main(String[] args) {
            List<Integer> numbers = new ArrayList<>(Arrays.asList(10, 25, 30, 45, 50));
    
            // Remove all numbers less than 30
            numbers.removeIf(n -> n < 30);
    
            System.out.println("After removeIf: " + numbers);
        }
    }
    

    Output

    After removeIf: [30, 45, 50]

    After using removeIf, the list contains only numbers greater than or equal to 30

  • Java static Keyword โ€“ A Complete Guide with Examples

    Introduction

    In Java, the static keyword is used for memory management and is one of the most commonly used modifiers. It can be applied to variables, methods, blocks, and nested classes.

    When a member is declared as static, it belongs to the class rather than an instance of the class. This means you can access it without creating an object.

    In this guide, weโ€™ll explore:

    • What is static in Java?
    • Static Variables
    • Static Methods
    • Static Blocks
    • Static Nested Classes
    • Restrictions of static
    • Real-world use cases
    • Updates in latest Java versions

    What is static in Java?

    The static keyword in Java is a non-access modifier that can be applied to:

    • Variables
    • Methods
    • Blocks
    • Nested Classes

    When a member is declared static, it belongs to the class itself rather than to an individual object of that class.

    ๐Ÿ‘‰ Normally, every object of a class has its own copy of instance variables and methods. But if you declare them as static, only one copy is created in memory, and all objects share it.

    Characteristics:

    1. Memory Management: Static members are created once in the method area of JVM memory when the class is loaded.
    2. Class-level association: You donโ€™t need to create an object of the class to use static members.
    3. Shared Resource: All objects of the class share the same static member.

    1. Static Variables (Class Variables)

    A static variable is also called a class variable because it is associated with the class, not the object.

    Characteristics:

    • Only one copy exists in memory, regardless of how many objects are created.
    • Initialized only once when the class is loaded.
    • Used when you want to store common property of all objects.

    Example:

    class Student {
        int rollNo;              // instance variable
        String name;             // instance variable
        static String college = "ABC University"; // static variable
    
        Student(int r, String n) {
            rollNo = r;
            name = n;
        }
    
        void display() {
            System.out.println(rollNo + " " + name + " " + college);
        }
    }
    
    public class StaticVariableDemo {
        public static void main(String[] args) {
            Student s1 = new Student(101, "John");
            Student s2 = new Student(102, "Alice");
    
            s1.display();
            s2.display();
        }
    }

    Output:

    101 John ABC University
    102 Alice ABC University

    ๐Ÿ‘‰ Both students share the same college value. If we change it for one, it changes for all.

    2. Static Methods

    A static method belongs to the class and can be called without creating an object.

    Characteristics:

    • Can access static variables and static methods directly.
    • Cannot access instance variables or methods directly (need an object).
    • Widely used in utility or helper classes.

    Example:

    class Calculator {
        static int add(int a, int b) {
            return a + b;
        }
    
        static int multiply(int a, int b) {
            return a * b;
        }
    }
    
    public class StaticMethodDemo {
        public static void main(String[] args) {
            // Accessing directly with class name
            System.out.println("Sum: " + Calculator.add(5, 3));
            System.out.println("Product: " + Calculator.multiply(4, 6));
        }
    }

    ๐Ÿ‘‰ Static methods improve efficiency and avoid unnecessary object creation.

    3. Static Blocks

    A static block is executed only once when the class is loaded into memory. It is used to initialize static variables.

    Characteristics:

    • Executes only once, when the class is loaded into memory.
    • Used for complex static variable initialization.
    • Executes before the main() method runs.

    Example:

    class Configuration {
        static String dbUrl;
        static String user;
    
        static {
            dbUrl = "jdbc:mysql://localhost:3306/mydb";
            user = "admin";
            System.out.println("Static block executed: Database configuration loaded");
        }
    }
    
    public class StaticBlockDemo {
        public static void main(String[] args) {
            System.out.println(Configuration.dbUrl);
        }
    }

    Output:

    Static block executed: Database configuration loaded
    jdbc:mysql://localhost:3306/mydb

    ๐Ÿ‘‰Very useful for loading drivers, initializing constants, and setting up environment configurations.

    4. Static Nested Classes

    A static nested class is a nested class declared with the static keyword, and it does not require an object of the outer class to be instantiated.

    Characteristics:

    • Can be accessed without creating an object of the outer class.
    • Can access only static members of the outer class.
    • Often used to logically group classes that are only used inside their outer class.

    Example:

    class Outer {
        static class Nested {
            void display() {
                System.out.println("Hello from static nested class!");
            }
        }
    }
    
    public class StaticNestedClassExample {
        public static void main(String[] args) {
            Outer.Nested nestedObj = new Outer.Nested();
            nestedObj.display();
        }
    }
    

    โœ” This improves encapsulation and keeps related classes together.

    5. Restrictions of static

    There are some limitations of using static in Java:

    • A static method cannot access non-static members directly.
    • this and super cannot be used in static context.
    • Static blocks cannot access instance variables.
    1. Static methods cannot access non-static members directly.

    Example:

    class Test {
        int x = 5; // instance variable
        static int y = 10; 
    
        static void display() {
            // System.out.println(x); // โŒ Error
            System.out.println(y);   // โœ… Allowed
        }
    }

    2. Static methods cannot use this or super keywords.
    Because they are class-level, not object-level.

    3. Static blocks cannot access instance variables directly.

    6. Real-World Use Cases of static

    • Constants: Declaring constants using static final
    class Constants {
        public static final double PI = 3.14159;
    }
    • Utility classes: Classes like Math, Collections, and Arrays have only static methods.
    • Singleton Design Pattern: Static variable used to hold the single instance.
    • Factory Methods: Returning instances without creating objects externally.
    • Static Import: Simplifies calling static members without class name.

    Example:

    import static java.lang.Math.*;
    
    public class StaticImportDemo {
        public static void main(String[] args) {
            System.out.println(PI);
            System.out.println(sqrt(16));
        }
    }

    7. Updates in Latest Java Versions

    While the static keyword itself hasnโ€™t changed much, it is widely used with new features in recent Java versions:

    • Java 8: Allowed static methods inside interfaces. (default & static methods).
    • Java 9+: Allowed private static methods inside interfaces.
    • Java 14+: Records support static methods and static fields.
    • Java 17 LTS & Java 21 LTS: static works seamlessly with new features like sealed classes and records.

    Example: Static method in Interface (Java 8+)

    interface Logger {
        static void log(String message) {
            System.out.println("Log: " + message);
        }
    }
    
    public class InterfaceStaticMethodExample {
        public static void main(String[] args) {
            Logger.log("Hello from static interface method!");
        }
    }
    

    ๐ŸŽฏConclusion

    The static keyword in Java is a powerful tool for memory management and code efficiency. It allows:

    • Shared variables (static variables)
    • Common utility methods (static methods)
    • One-time initialization (static blocks)
    • Grouping with (static nested classes)

    Itโ€™s widely used in frameworks, libraries, utility classes, and design patterns. From Java 8 onwards, static became even more useful with static methods in interfaces, making it essential for modern Java developers.

  • Java Concurrency and Multithreading โ€“ A Complete Guide with Examples

    Introduction

    In modern software development, applications are expected to be fast, responsive, and capable of handling multiple tasks simultaneously. Whether itโ€™s a banking system processing thousands of transactions, a web server handling client requests, or a gaming application rendering graphics while processing user input โ€” concurrency and multithreading play a crucial role in making these operations smooth and efficient. Java provides robust support for working with multiple threads, enabling developers to perform parallel tasks, optimize CPU usage, and improve responsiveness.

    What is Concurrency?
    • Concurrency is the ability of a program to deal with multiple tasks at the same time. It doesnโ€™t always mean they are executed simultaneously, but rather that the program can make progress on multiple tasks without waiting for one to finish completely.
    • Example: While downloading a file, your application can also allow the user to type in a search bar.
    What is Multithreading?
    • Multithreading is a specific form of concurrency where a program is divided into smaller units called threads, which run independently but share the same memory space.
    • A thread is the smallest unit of execution in a program. Multiple threads can run in parallel, depending on the number of CPU cores available.

    Concurrency vs Multithreading in simple terms:

    • Concurrency = Dealing with multiple things at once
    • Multithreading = Running multiple threads within the same program
    Why Does It Matter?
    • In a single-threaded application, tasks are executed one after the other. If one task takes time (like reading a file or fetching data from the network), the whole program is blocked.
    • With multithreading, long-running tasks can run in the background while other tasks continue executing, making programs faster and more responsive.

    1. What is Multithreading?

    Multithreading is the capability of a program to execute multiple threads simultaneously. A thread is the smallest unit of execution within a process. Unlike separate processes, threads in the same program share the same memory space (heap, method area), but each thread maintains its own stack for execution.

    In simple terms:

    • A process is like an entire program (e.g., Microsoft Word, a Java application).
    • A thread is like a worker inside that program doing a specific task (e.g., spell checking, autosaving, responding to user input).

    Why Multithreading?

    • Responsiveness โ€“ Applications donโ€™t freeze while performing heavy tasks. Example: In a chat app, one thread handles UI while another sends/receives messages.
    • Better Resource Utilization โ€“ Threads share memory and resources, reducing the cost of creating separate processes.
    • Scalability โ€“ Multithreading allows applications to take advantage of multi-core processors.
    • Improved Performance โ€“ Tasks like matrix multiplication, web crawling, or handling client requests in a server can be split across threads.

    Real-World Examples of Multithreading

    1. Web Browsers โ€“ One thread handles page rendering, another downloads images, and another plays video/audio.
    2. Video Games โ€“ Separate threads for rendering graphics, handling user input, physics calculation, and audio playback.
    3. Banking System โ€“ While one thread processes payments, another handles account updates, and another manages notifications.
    4. Servers (e.g., Tomcat, Spring Boot) โ€“ Multiple threads handle concurrent client requests.

    โœ”๏ธSimple Java Example โ€“ Without and With Threads

    a. Without Multithreading (Sequential Execution)
    public class SingleThreadExample {
        public static void main(String[] args) {
            task("Task 1");
            task("Task 2");
        }
    
        public static void task(String name) {
            for (int i = 1; i <= 5; i++) {
                System.out.println(name + " - step " + i);
            }
        }
    }

    Output:

    Task 1 - step 1
    Task 1 - step 2
    ...
    Task 1 - step 5
    Task 2 - step 1
    Task 2 - step 2
    ...
    Task 2 - step 5

    Here, Task 2 starts only after Task 1 is completely finished.

    b. With Multithreading (Concurrent Execution)
    public class MultiThreadExample {
        public static void main(String[] args) {
            Thread t1 = new Thread(() -> task("Task 1"));
            Thread t2 = new Thread(() -> task("Task 2"));
    
            t1.start();
            t2.start();
        }
    
        public static void task(String name) {
            for (int i = 1; i <= 5; i++) {
                System.out.println(name + " - step " + i);
            }
        }
    }
    

    Possible Output (interleaved, concurrent execution):

    Task 1 - step 1
    Task 2 - step 1
    Task 1 - step 2
    Task 2 - step 2
    Task 2 - step 3
    Task 1 - step 3
    ...
    

    Here, both threads run concurrently, and their steps are interleaved depending on the CPUโ€™s scheduling.

    Concurrency vs Parallelism

    • Concurrency: Multiple threads make progress at the same time, but not necessarily simultaneously (time-slicing on a single CPU).
    • Parallelism: Multiple threads execute at the same time on different CPU cores.

    ๐Ÿ‘‰ Javaโ€™s multithreading model supports both concurrency and parallelism depending on the system hardware.

    2. Creating Threads in Java

    In Java, there are multiple ways to create and start threads. At the core, every thread in Java needs a piece of code to execute โ€” defined in the run() method. To run that code concurrently, you need to create a Thread object and call its start() method.

    Here are the three main approaches:

    2.a. Extending Thread Class

    The simplest way to create a thread is by extending the built-in Thread class and overriding its run() method. Example:

    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Thread running: " + Thread.currentThread().getName());
        }
    }
    
    public class ThreadExample1 {
        public static void main(String[] args) {
            MyThread t1 = new MyThread();  // create a thread instance
            t1.start();                    // start the thread (calls run() internally)
        }
    }

    Explanation:

    • run() โ†’ contains the code that will be executed in the new thread.
    • start() โ†’ creates a new thread of execution and then calls run(). If you directly call run(), it wonโ€™t create a new thread โ€” it will just run in the current thread.

    Pros:

    • Simple to implement.
    • Useful if you donโ€™t need to inherit from another class.

    Cons:

    • Java doesnโ€™t support multiple inheritance, so if your class already extends another class, you cannot extend Thread.

    2.b. Implementing Runnable Interface

    A more flexible approach is to implement the Runnable interface. Here, you define the run() method in a class that implements Runnable and pass it to a Thread object.

    Example:

    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Thread running: " + Thread.currentThread().getName());
        }
    }
    
    public class ThreadExample2 {
        public static void main(String[] args) {
            Thread t1 = new Thread(new MyRunnable());  // pass Runnable to Thread
            t1.start();                                // start the thread
        }
    }

    Explanation:

    • Runnable is a functional interface with a single run() method.
    • You separate the task (Runnable) from the thread management (Thread).
    • This approach promotes better code reusability and object-oriented design.

    Pros:

    • Allows your class to extend another class (since youโ€™re not extending Thread).
    • Encourages separation of concerns (task vs execution).
    • Preferred in real-world applications.

    Cons:

    • Slightly more verbose than extending Thread.

    2.c. Using Lambda Expressions (Java 8 and above)

    Since Runnable is a functional interface, you can use a lambda expression to create a thread in a more concise way.

    Example:

    public class ThreadExample3 {
        public static void main(String[] args) {
            Thread t1 = new Thread(() -> 
                System.out.println("Thread with Lambda running: " + Thread.currentThread().getName())
            );
            t1.start();
        }
    }

    Explanation:

    • With a lambda, you donโ€™t need to create a separate class or implement Runnable explicitly.
    • This is widely used in modern Java projects because of its readability and conciseness.

    Pros:

    • Short and clean syntax.
    • Great for simple, one-time tasks.

    Cons:

    • If you have complex business logic, lambdas can make the code less readable compared to a dedicated class.

    โœ๏ธWhich Approach Should You Use?

    • Use Extending Thread โ†’ when your class doesnโ€™t need to extend anything else and you want a quick implementation.
    • Use Implementing Runnable โ†’ when your class already extends another class or when you want to separate logic from execution.
    • Use Lambda Expressions โ†’ when you need quick, inline, and concise thread creation.

    3. Thread Lifecycle

    In Java, a thread does not simply start and end. It passes through multiple states defined in the Thread.State enum. These states are managed by the Java Virtual Machine (JVM) and the thread scheduler (part of the JVM that decides which thread runs at a given time).

    A thread goes through these states:

    1. NEW

    • A thread is created but not yet started using the start() method.
    • Example: Thread t = new Thread();

    2. RUNNABLE

    • After start() is called, the thread is ready to run and waiting for CPU scheduling.
    • It does not mean the thread is running immediately โ€” only that itโ€™s eligible to run.

    3. RUNNING

    • When the thread scheduler picks the thread from the runnable pool, it starts execution.
    • Only one thread per CPU core can be in the running state at a time.

    4. WAITING / BLOCKED / TIMED_WAITING

    • WAITING โ†’ Thread waits indefinitely for another thread to notify it (using wait() / notify()).
    • BLOCKED โ†’ Thread is waiting to acquire a lock.
    • TIMED_WAITING โ†’ Thread is waiting for a specified period (using sleep(ms), join(ms), or wait(ms)).

    5. TERMINATED (Dead)

    • Once the thread finishes execution, it enters the terminated state.
    • A terminated thread cannot be restarted.

    โžค Code Example โ€“ Demonstrating Thread States

    public class ThreadLifecycleDemo {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                System.out.println("Thread is running...");
                try {
                    Thread.sleep(2000); // moves to TIMED_WAITING
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread finished execution");
            });
    
            System.out.println("State after creation: " + t1.getState()); // NEW
    
            t1.start();
            System.out.println("State after start(): " + t1.getState()); // RUNNABLE
    
            Thread.sleep(500);
            System.out.println("State while sleeping: " + t1.getState()); // TIMED_WAITING
    
            t1.join(); // wait for t1 to finish
            System.out.println("State after completion: " + t1.getState()); // TERMINATED
        }
    }
    

    Output:

    State after creation: NEW
    State after start(): RUNNABLE
    Thread is running...
    State while sleeping: TIMED_WAITING
    Thread finished execution
    State after completion: TERMINATED

    โœ๏ธKey Notes

    • The thread scheduler decides which thread runs, and its behavior depends on the JVM and OS.
    • Once a thread is terminated, you cannot restart it โ€” you must create a new thread object.
    • Understanding thread states is crucial for debugging concurrency issues.

    4. Thread Methods

    1. start()

    • Purpose: Launches a new thread.
    • How it works: Moves the thread from the NEW state โ†’ RUNNABLE state. The JVMโ€™s thread scheduler then decides when to move it into the RUNNING state.
    • Note: You should never call run() directly because it wonโ€™t create a new thread โ€” it will just execute in the current thread.

    2. run()

    • Purpose: Contains the logic/code that the thread will execute.
    • How it works: When start() is called, the JVM internally invokes the run() method.
    • Example: In your code, the lambda expression () -> { ... } is the body of the run() method.

    3. sleep(ms)

    • Purpose: Temporarily pauses the thread execution for the specified time (in milliseconds).
    • How it works: Moves the thread from RUNNING โ†’ TIMED_WAITING state.
    • After the sleep duration expires, the thread goes back to RUNNABLE.

    4. join()

    • Purpose: Allows one thread to wait for another thread to finish before proceeding.
    • How it works: If the main thread calls t1.join(), it goes into a WAITING state until t1 finishes execution.

    5. isAlive()

    • Purpose: Checks if a thread is still active (either RUNNABLE, RUNNING, or WAITING).
    • Returns:
      • true โ†’ thread is alive.
      • false โ†’ thread has finished execution (TERMINATED).

    Example

    public class ThreadMethods {
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                try {
                    Thread.sleep(1000);  // Thread goes to TIMED_WAITING
                    System.out.println("Thread work done");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            t1.start();   // Thread moves from NEW โ†’ RUNNABLE
            t1.join();    // Main thread waits until t1 finishes
            System.out.println("Main thread ends");
        }
    }
    

    Step-by-Step Execution

    1. t1 is created โ†’ NEW state.
    2. t1.start() โ†’ moves to RUNNABLE (waiting for CPU).
    3. Scheduler picks t1 โ†’ RUNNING.
    4. Inside run(), Thread.sleep(1000) โ†’ goes to TIMED_WAITING for 1 second.
    5. After 1 second, back to RUNNABLE, then RUNNING again โ†’ prints "Thread work done".
    6. Meanwhile, main thread calls t1.join() โ†’ goes into WAITING until t1 finishes.
    7. Once t1 finishes โ†’ main resumes and prints "Main thread ends".

    5. Synchronization

    Why synchronization is needed ?

    When multiple threads access the same mutable data concurrently, you can get race conditions and data corruption. Many seemingly simple operations (like count++) are actually compound operations โ€” they involve multiple CPU / JVM steps:

    count++ expands to:

    1. Read count from memory into a register.
    2. Add 1.
    3. Write the new value back to memory.

    If two threads do these steps concurrently, their reads/writes can interleave and one update can be lost (a lost update). Thatโ€™s why we must coordinate access to shared state โ€” i.e., synchronize

    ๐Ÿ“Œ What synchronized Does โ€” Real-Life Example

    Imagine you and your friend both want to use the same notebook to write.

    • If you both write at the same time, the text will overlap and become messy.
    • To avoid this, you decide:
      ๐Ÿ‘‰ Only one person at a time can hold the notebook and write.
      ๐Ÿ‘‰ The other person has to wait until the notebook is free.

    Thatโ€™s exactly what happens with synchronized in Java:

    • Other threads wait outside until itโ€™s free.
    • Only one thread at a time can enter the synchronized method/block.

    5.a. Using synchronized keyword

    class Counter {
        private int count = 0;
    
        public synchronized void increment() {
            count++;
        }
    
        public int getCount() {
            return count;
        }
    }
    
    public class SyncExample {
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
    
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) counter.increment();
            });
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) counter.increment();
            });
    
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            System.out.println("Final Count: " + counter.getCount());
        }
    }
    

    6. Deadlock

    Deadlock happens when two or more threads wait indefinitely for resources locked by each other.

    Example

    public class DeadlockExample {
        public static void main(String[] args) {
            final String resource1 = "Resource1";
            final String resource2 = "Resource2";
    
            Thread t1 = new Thread(() -> {
                synchronized (resource1) {
                    System.out.println("Thread 1 locked resource 1");
                    try { Thread.sleep(100);} catch (Exception e) {}
                    synchronized (resource2) {
                        System.out.println("Thread 1 locked resource 2");
                    }
                }
            });
    
            Thread t2 = new Thread(() -> {
                synchronized (resource2) {
                    System.out.println("Thread 2 locked resource 2");
                    try { Thread.sleep(100);} catch (Exception e) {}
                    synchronized (resource1) {
                        System.out.println("Thread 2 locked resource 1");
                    }
                }
            });
    
            t1.start();
            t2.start();
        }
    }
    

    7. Executors and Thread Pools

    Instead of creating threads manually, Java provides the Executor Framework for better performance and resource management.

    Example โ€“ Fixed Thread Pool

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ExecutorExample {
        public static void main(String[] args) {
            ExecutorService executor = Executors.newFixedThreadPool(2);
    
            for (int i = 1; i <= 5; i++) {
                int taskId = i;
                executor.submit(() -> {
                    System.out.println("Task " + taskId + " running in " + Thread.currentThread().getName());
                });
            }
    
            executor.shutdown();
        }
    }
    

    8. Advanced Concurrency Utilities (java.util.concurrent)

    • Locks (ReentrantLock) โ†’ More flexible than synchronized.
    • CountDownLatch โ†’ Wait for multiple threads to finish.
    • CyclicBarrier โ†’ Wait until a group of threads reach a barrier point.
    • Semaphore โ†’ Limit number of threads accessing a resource.
    • Concurrent Collections โ†’ ConcurrentHashMap, CopyOnWriteArrayList, etc.

    Example โ€“ CountDownLatch

    import java.util.concurrent.CountDownLatch;
    
    public class CountDownLatchExample {
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch latch = new CountDownLatch(3);
    
            for (int i = 1; i <= 3; i++) {
                int taskId = i;
                new Thread(() -> {
                    System.out.println("Task " + taskId + " completed");
                    latch.countDown();
                }).start();
            }
    
            latch.await(); // Main thread waits
            System.out.println("All tasks completed. Main thread continues.");
        }
    }
    

    Conclusion

    Concurrency and multithreading in Java enable developers to build faster, scalable, and more responsive applications. From creating simple threads to using advanced utilities like CountDownLatch and thread pools, Java provides a powerful concurrency toolkit.

    Key takeaways:

    • Use threads for parallelism and responsiveness.
    • Always manage synchronization to avoid race conditions.
    • Use Executor Framework instead of manually managing threads.
    • Avoid deadlocks by designing lock acquisition carefully.
    • Leverage java.util.concurrent utilities for advanced concurrency management.