Java Interview Opps, Collection framework, GC, Comparator - Yash-777/MyWorld GitHub Wiki
/** Number of CPUS, to place bounds on some sizings */
static final int NCPU = Runtime.getRuntime().availableProcessors();ClassNotFoundException and NoClassDefFoundError both indicate a missing class at runtime, but they occur in different scenarios and have different root causes and handling requirements.
- ClassNotFoundException happens when your code explicitly tries to load a class by name and fails because it is missing at runtime.
- NoClassDefFoundError happens when your code compiled successfully because the class was present, but the JVM cannot find the class's definition during execution.
if-else vs switch, Ternary Operator
πΉ if - elseπΉ Ternary Operator
|
πΉ switch |
|---|---|
πΉ if - else
* Evaluates boolean expression β any condition
* Handles ranges, complex conditions, multiple types
* Goes top to bottom β first match wins
* else is optional
int marks = 75;
// multiple condition expressions
if (marks >= 90) {
System.out.println("Grade A");
} else if (marks >= 75) {
System.out.println("Grade B");
} else if (marks >= 60) {
System.out.println("Grade C");
} else {
System.out.println("Fail");
}
// complex conditions β switch cannot do this β
if (age > 18 && country.equals("India") && !isBanned) {
System.out.println("Eligible");
}
// null check β switch throws NPE on null β
if (name != null && name.equals("John")) { }
// range check β switch cannot do this β
if (salary >= 50000 && salary <= 100000) { }πΉ One-line Conditional (Ternary Operator)
// condition ? true : false
int age = 20;
String result = age >= 18 ? "Adult" : "Minor"; // β
one line
// nested ternary β avoid, hard to read β οΈ
String grade = marks >= 90 ? "A" : marks >= 75 ? "B" : marks >= 60 ? "C" : "Fail";
// common uses
int max = a > b ? a : b;
String msg = user != null ? user.getName() : "Guest";
return isValid ? process() : throwError(); |
πΉ switch
* Evaluates single variable against constants
* Cleaner than multiple if-else for fixed values
* Works with int, char, String, enum β not boolean, long, float
* Fall-through β without break continues to next case
// Traditional switch β fall-through risk β οΈ
int day = 3;
switch (day) {
case 1:
System.out.println("Monday");
break; // β οΈ forget break β falls to next case
case 2:
System.out.println("Tuesday");
break;
default:
System.out.println("Other");
}
// switch with enum β Best Use Case β
// String constants -> String status = "ACTIVE";
enum Status { PENDING, ACTIVE, INACTIVE, BLOCKED }
Status status = Status.ACTIVE;
switch (status) {
case PENDING:
System.out.println("Awaiting approval");
break;
case ACTIVE:
System.out.println("User is active");
break;
case INACTIVE:
System.out.println("User inactive");
break;
case BLOCKED:
System.out.println("User blocked");
break;
} |
Encapsulation is the process of binding (wrapping) data members and member functions into a single unit.
Encapsulation @SeleniumExpress
| πEncapsulation | π Diagram π |
|---|---|
Encapsulation is the process of binding (wrapping) data members and member functions into a single unit.
Example: A class in Object-Oriented Programming (OOP).
* Data members should not be directly accessible from outside the class.
* They are usually declared as private to β restrict direct access.
* Access to these data members is provided through public methods such as getter and setter methods (or other controlled methods).
Simple Explanation: Encapsulation helps in data hiding and protects data from unauthorized access by allowing controlled interaction through methods. |
π Concept in One Line: Encapsulation = Data + Methods packed together like medicine inside a capsule with controlled access.
Class (The Container): `Data Members (Variables)` & `Member Functions (Methods)` π Both are packed into one single unit.
π Class = Capsule π
π Data = Medicine inside
π Methods = Safe way to use it
|
Polymorphism means "one name, many forms." (Method overloading, Method overriding)
The ability of a reference variable to change its behavior according to the object instance it is holding. The overridden methods behave differently based on the actual object being referenced. Polymorphism allows you to define one interface and have multiple implementations.
There are two types of Polymorphism in Java:
- Compile-time polymorphism β Method overloading (decided at compile time).
- Runtime polymorphism β Method overriding (decided at runtime).
Compile-time Polymorphism (Static binding)Method overloading
|
Runtime Polymorphism (Dynamic binding)Method overriding
|
|---|---|
* Same class, same method name, different parameters (type/count/order)
* Resolved at compile time (Static binding)
* Return type can be different but alone cannot differentiate overloading
* Access modifier can be anything β no restriction
* null β compiler picks most specific type; if tie β compile error
* @Override annotation not applicable
* Can be done in same class or child class (child overloading parent method)
* final, static, private methods all can be overloaded β
|
* Parent β Child, same method name + same parameters + same return type
* Resolved at runtime based on object type (Dynamic binding)
* Animal ref = new Dog() β ref.instanceSound() calls Dog's method
* @Override annotation recommended β catches mistakes at compile time
* Return type can be covariant (child can narrow return type β Animal β Dog) β
* Access modifier can only be widened, never narrowed
private β default β protected β public
(widening direction β)
* private methods cannot be overridden β they are invisible to child class
* final methods cannot be overridden β
* Static Hiding (Parent β Child)
static methods cannot be overridden β they get hidden instead
* constructor cannot be overridden
* Overriding method cannot throw broader checked exceptions than parent
* Parent throws IOException β Child can throw IOException or narrower β
* Parent throws IOException β Child throws Exception β
* abstract methods must be overridden in concrete child class |
/**
* Comprehensive Polymorphism Demo
* Covers:
* - Static method hiding vs Instance method overriding
* - Static method call via null object
* - Method overloading with null (String vs Object)
* - Ambiguous overloading (Integer vs Long)
* - Covariant return type in overriding
* - Access modifier rules in override/overload
*/
// ======================== PARENT CLASS ========================
class Animal {
// 1. STATIC METHOD β will be HIDDEN (not overridden) in child
static void staticSound() {
System.out.println("Animal: static sound (Animal class)");
}
// 2. INSTANCE METHOD β will be OVERRIDDEN in child
void instanceSound() {
System.out.println("Animal: instance sound");
}
// 3. OVERLOADING: null call β Object vs String
void greet(Object o) {
System.out.println("greet(Object) called");
}
void greet(String s) {
System.out.println("greet(String) called");
}
// 4. OVERLOADING: null call β Integer vs Long (AMBIGUOUS)
void calculate(Integer i) {
System.out.println("calculate(Integer) called");
}
void calculate(Long l) {
System.out.println("calculate(Long) called");
}
// 5. COVARIANT RETURN TYPE β parent returns Animal
Animal getInstance() {
System.out.println("Animal getInstance()");
return new Animal();
}
// 6. ACCESS MODIFIER β protected in parent (child can widen it to public)
protected void accessDemo() {
System.out.println("Animal: protected accessDemo()");
}
} |
// ======================== CHILD CLASS ========================
class Dog extends Animal {
// 1a. STATIC METHOD HIDING β not overriding, just hiding
// Resolved at COMPILE TIME based on reference type
static void staticSound() {
System.out.println("Dog: static sound (Dog class)");
}
// 1b. INSTANCE METHOD OVERRIDING β resolved at RUNTIME based on object type
@Override
void instanceSound() {
System.out.println("Dog: instance sound (overridden)");
}
// 5. COVARIANT RETURN TYPE β child can return Dog (subtype of Animal)
// Valid override β return type is narrowed down
@Override
Dog getInstance() {
System.out.println("Dog getInstance() β covariant return");
return new Dog();
}
// 6. ACCESS MODIFIER β widening protected β public is ALLOWED in override
// Narrowing (public β protected) is NOT allowed β compile error
@Override
public void accessDemo() {
System.out.println("Dog: public accessDemo() β widened access modifier");
}
} |
// ======================== MAIN CLASS ========================
public class PolymorphismDemo {
public static void main(String[] args) {
System.out.println("========== 1. Static Hiding vs Instance Overriding ==========");
Animal ref = new Dog(); // Animal reference, Dog object
// Static method β resolved at COMPILE TIME β Animal's method called (HIDING)
ref.staticSound(); // Output: Animal: static sound
Animal.staticSound(); // Output: Animal: static sound
Dog.staticSound(); // Output: Dog: static sound
// Instance method β resolved at RUNTIME β Dog's method called (OVERRIDING)
ref.instanceSound(); // Output: Dog: instance sound
System.out.println("\n========== 2. Static Method Call via NULL Object ==========");
Animal nullAnimal = null;
// No NullPointerException β static methods belong to CLASS not object
// Compiler replaces nullAnimal.staticSound() β Animal.staticSound()
nullAnimal.staticSound(); // Output: Animal: static sound β
System.out.println("\n========== 3. Overloading with NULL β Object vs String ==========");
Animal a = new Animal();
// String is more specific than Object β compiler picks greet(String)
a.greet(null); // Output: greet(String) called β
a.greet((Object) null); // Output: greet(Object) called β explicit cast
System.out.println("\n========== 4. Overloading NULL β Integer vs Long (AMBIGUOUS) ==========");
// a.calculate(null); β COMPILE ERROR
// Both calculate(Integer) and calculate(Long) match null
// Neither Integer nor Long is more specific than the other
// Fix: explicit cast
a.calculate((Integer) null); // Output: calculate(Integer) called β
a.calculate((Long) null); // Output: calculate(Long) called β
System.out.println("Note: a.calculate(null) β COMPILE ERROR (ambiguous method call)");
System.out.println("\n========== 5. Covariant Return Type ==========");
// Parent reference β getInstance() returns Animal type at compile time
Animal animalObj = ref.getInstance(); // Runs Dog's getInstance() at runtime
// Dog reference β can directly get Dog type
Dog dog = new Dog();
Dog dogObj = dog.getInstance(); // Dog's covariant return β
System.out.println("\n========== 6. Access Modifier in Override ==========");
// Dog widened accessDemo() from protected β public
// Calling via Animal reference still works β
ref.accessDemo(); // Output: Dog: public accessDemo()
System.out.println("\n========== Summary of Rules ==========");
System.out.println("Static method β HIDDEN (compile-time, reference type decides)");
System.out.println("Instance method β OVERRIDDEN (runtime, object type decides)");
System.out.println("null + static β No NPE, resolved by compiler to class");
System.out.println("null + overload β Most specific type wins; ambiguous = compile error");
System.out.println("Covariant return β Child can narrow return type (Dog extends Animal)");
System.out.println("Access modifier β Override can WIDEN (protectedβpublic) β
");
System.out.println(" Override can NOT NARROW (publicβprotected) β");
System.out.println("Overload β Access modifier can be anything, no restriction β
");
}
} |
========== 1. Static Hiding vs Instance Overriding ==========
Animal: static sound (Animal class)
Animal: static sound (Animal class)
Dog: static sound (Dog class)
Dog: instance sound (overridden)
========== 2. Static Method Call via NULL Object ==========
Animal: static sound (Animal class)
========== 3. Overloading with NULL β Object vs String ==========
greet(String) called
greet(Object) called
========== 4. Overloading NULL β Integer vs Long (AMBIGUOUS) ==========
calculate(Integer) called
calculate(Long) called
Note: a.calculate(null) β COMPILE ERROR (ambiguous method call)
========== 5. Covariant Return Type ==========
Dog getInstance() β covariant return
Dog getInstance() β covariant return
========== 6. Access Modifier in Override ==========
Dog: public accessDemo() β widened access modifier
========== Summary of Rules ==========
Static method β HIDDEN (compile-time, reference type decides)
Instance method β OVERRIDDEN (runtime, object type decides)
null + static β No NPE, resolved by compiler to class
null + overload β Most specific type wins; ambiguous = compile error
Covariant return β Child can narrow return type (Dog extends Animal)
Access modifier β Override can WIDEN (protectedβpublic) β
Override can NOT NARROW (publicβprotected) β
Overload β Access modifier can be anything, no restriction β
|
java.lang.Object β equals() and hashCode() β Why Both Must Be Overridden?
The Contract: If two objects are equal (equals() returns true),they must have the same hashCode. |
What happens if only equals() is overridden? |
|---|---|
class Student {
String name;
int age;
@Override
public boolean equals(Object o) {
Student s = (Student) o;
return this.name.equals(s.name) && this.age == s.age;
}
// hashCode NOT overridden β
}@Override
public int hashCode() {
return Objects.hash(name, age); // Java 7+
} |
HashMap / HashSet first use hashCode() to choose the bucket.
Then they use equals() to compare objects inside that bucket.
If two objects have different hashCode() values (default is memory address), they go into different buckets.
In that case, equals() is never called.
Student s1 = new Student("Alice", 20);
Student s2 = new Student("Alice", 20);
s1.equals(s2); // β
true β your custom equals works
Set set = new HashSet<>();
set.add(s1);
set.contains(s2); // β FALSE! Should be true but isn't
Map<Student, String> map = new HashMap<>();
map.put(s1, "Engineer");
map.get(s2); // β returns null! s2 can't find s1's bucket
|
π§ Java Memory Management Structure Overview
When a Java program runs, JVM divides memory into:
-
Heap β objects and instance variables live here;
shared across threads, Managed by:Garbage Collector- Error: OutOfMemoryError, Size: Large, Speed: Slower
- π Stores: Objects, Instance variables, Arrays
- Divided into Young Generation (Eden + S0 + S1) and Old Generation
Person objRef = new Person(); // objRef β Stack (reference) // new Person() β Heap (object)
-
Stack β
method calls, local variables;one per thread; Managed by:JVM auto (LIFO)- Error: StackOverflowError if too deep recursion, Size: Small, Speed: Fast
- π Stores: Local variables, Method calls, Primitive values, Object references (not objects)
public void test() { int x = 10; // Stack (primitive) Person p = new Person(); // p β Stack (ref), new Person() β Heap }
-
Method Area (Metaspace) β shared across threads; Managed by: JVM / Full GC
- Error: OutOfMemoryError: Metaspace, Size: Dynamic, Speed: Slower
- π Stores: Class metadata, Static variables, Method information, Constants, Annotations
- Before Java 8 β PermGen (fixed size, inside JVM)
- From Java 8+ β Metaspace (dynamic size, uses native memory)
public class Person { static int count = 0; // Metaspace (static) String name; // Heap (instance variable) }
-
PC Register β tracks current instruction per thread; one per thread
- Size: Very small, auto updated after each instruction
- For native methods β value is undefined
-
Native Method Stack β for native (C/C++) method calls via JNI; one per thread
- Error: StackOverflowError or OutOfMemoryError
System.arraycopy(src, 0, dest, 0, length); // internally calls C level code
| Area | Stores | GC Applies? |
|---|---|---|
| Eden | Newly created objects | β Minor GC |
| Survivor S0/S1 | Objects surviving Minor GC | β Minor GC |
| Old Generation | Long lived / large objects | β Major GC |
| Metaspace | Class metadata, static vars | β Full GC |
| Stack | Method calls, local variables | β Auto cleared |
| Code Cache | JIT compiled code | β Managed by JIT |
| Native Memory | OS level resources | β Manual / OS |
π§ JVM Heap Structure |
π― Object Lifecycleπ₯ flow |
|---|---|
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
JVM MEMORY
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
new Object()
β
βΌ
βββββββββββββββββββββββββββββββββββββββ HEAP MEMORY βββββββ
β βββββββββββββββββββββββββββββββββββββββ β
β β YOUNG GENERATION β β
β β β β
β β ββββββββββββββββ ββββββ ββββββ β β
β β β EDEN β β S0 β β S1 β β β
β β β β β β β β β β
β β β new Object()β βage β βage β β β
β β β allocated β β 1 β β 2 β β β
β β β here β β β β β β β
β β ββββββββ¬ββββββββ βββ¬βββ βββ¬βββ β β
β β β Minor GCβ β β β
β β β βββββββββ β β β
β β βΌ βΌ β β β
β β Eden full? β β β
β β Minor GC runs βββββββββ β β
β β survivors move S0 β S1 β β
β β age counter +1 each cycle β β
β ββββββββββββββββββββ¬βββββββββββββββββ β β
β β age >= threshold (default 15) β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββ β
β β OLD GENERATION β β
β β β β
β β Long lived objects promoted here β β
β β Large objects directly here β β
β β β β
β β Major GC runs when Old Gen fills up β β
β βββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββ NON-HEAP MEMORY ββββββ
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β METASPACE β β
β β (replaced PermGen from Java 8+) β β
β β β’ Class metadata β’ Method info β β
β β β’ Static variables β’ Constants β β
β β β’ Class loaders β’ Annotations β β
β β β β
β β grows dynamically β uses native memory β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββ ββββββββββββββββ βββββββββββββββββ β
β β CODE CACHE β β STACK β β NATIVE MEMORYβ β
β β β β β β β β
β β JIT compiled β β method calls β β direct buffer β β
β β bytecode β β local vars β β OS resources β β
β β β β stack frames β β β β
β ββββββββββββββββ ββββββββββββββββ βββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββ
Object Age β Survivor Threshold
Minor GC 1 β Object moves Eden β S0 (age = 1)
Minor GC 2 β Object moves S0 β S1 (age = 2)
Minor GC 3 β Object moves S1 β S0 (age = 3)
Minor GC 4 β Object moves S0 β S1 (age = 4)
...
Minor GC 15 β age = 15 β PROMOTED to Old Generation
ββββββββββββββββββββββββββββββββββββββββββββββ |
JVM HEAP
ββββββββββββββββββββββββββββββββββββββββββββββ
π’ YOUNG GENERATION
βββββββββββββββββββββββββ
new Object()
β
βΌ
βββββββββββββββ
β Eden β β Eden full? = NO β stays in Eden
βββββββββββββββ
β (Eden full β Minor GC)
ββββ Dead objects ββββββββββ β» GARBAGE COLLECTED
βΌ
ββββββββββββββββ
β Survivor S0 β age = 1
β Survivor S1 β age = 2, 3, 4 ... (S0 β S1 each Minor GC)
ββββββββββββββββ
β (Age β₯ Threshold, e.g., 15)
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββ
π΅ OLD GENERATION (Tenured)
ββββββββββββββββββββββββββββββββββββββββββββββ
Long-living objects promoted here
ββββ Dead objectβββββββββββββ β» GARBAGE COLLECTED
β (Old Gen fills β Major GC β slow, infrequent)
βΌ
Mark β Sweep β Compact
Still not enough memory?
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β FULL GC β
β Entire Heap + Metaspace cleaned β
β Triggered by JVM or System.gc() β
β Most expensive β Stop The World (STW) β
βββββββββββββββββββββββββββββββββββββββββββ
ββββ Dead objects βββββββββββββββββββββββ β» GARBAGE COLLECTED
β
βΌ
Still no memory?
β
βΌ
π₯ OutOfMemoryError: Java heap space
ββββββββββββββββββββββββββββββββββββββββββββββ
Garbage Collected / Removed |
| GC Type | Area Cleaned | Trigger | Speed | Stop The World |
|---|---|---|---|---|
| Minor GC | Young Gen only (Eden + Survivors) | Eden space full | β Fast | Brief pause |
| Major GC | Old Generation only | Old Gen fills up | π’ Slow | Longer pause |
| Full GC | Entire Heap + Metaspace | JVM decision / System.gc() | π’π’ Slowest | Long pause |
Java-5 π Variable Arguments (Varargs) main(String[] args) vs main(String... args)
Varargs (...) allow a method to accept zero or more arguments of the same type without explicitly creating an array.
Main Purpose π To allow flexible number of parameters
- Only ONE varargs per method allowed, and MUST be LAST parameter
static void printInfo(String name, int... scores) { System.out.println("Name: " + name); for (int s : scores) System.out.println("Score: " + s); } // β varargs not last static void wrong(int... nums, String name) { } // Compile Error // β two varargs static void wrong(int... a, int... b) { } // Compile Error
- Varargs are internally treated as an array.
String... args β compiled to β String[] args
| Without Varargs β old way (overloading needed) parameter's of sametype | With Varargs β clean single method |
|---|---|
static int add(int a, int b) { return a + b; }
static int add(int a, int b, int c) { return a + b + c; } // duplicate!
static int add(int a, int b, int c, int d) { return a + b + c + d; } // more duplicate! |
static int add(int... numbers) { // int... β int[] internally
int sum = 0;
for (int n : numbers) sum += n;
return sum;
} |
main method.
// β
Both are valid as per JLS β officially accepted
Use β public static void main(String[] args) β
always
βββββ
JLS recommended, Industry standard
IDEs auto-generate this
Clear and explicit
Avoid β public static void main(String... args) β οΈ
βββ
Valid but unconventional
Confusing β main takes exactly one array
No benefit over String[] argsInterface Methods β Java8 π (static, default), java9 π (private)
- OpenJDK source, Java's own interfaces like List, Collection, Comparator, SequencedCollection use only default and static β NOT private.
static |
default |
private |
|---|---|---|
* Introduced in Java 8
* Must have a body
* Implicitly public
* NOT inherited by implementing classes
* Cannot be overridden (belongs to interface only)
* Called via InterfaceName.method() only
* Cannot use this keyword
* Can call private static methods inside interface
* Purpose β provide utility/helper
methods related to interface |
* Introduced in Java 8
* Must have a body
* Implicitly public
* Inherited by implementing classes
* Can be overridden by implementing classes
* Can call private methods inside interface
* Uses this keyword
* Purpose β provide backward compatibility
without breaking old classes |
* Introduced in Java 9
* Must have a body
* Explicitly private (must write private)
* NOT inherited by implementing classes
* NOT accessible outside interface
* Cannot be overridden
* Two types β private and private static
* private β used by default methods
* private static β used by static methods
* Purpose β avoid code duplication inside
interface (internal helper) |
OpenJDK source, Java's own interfaces like List, Collection, Comparator, SequencedCollection use only default and static β NOT private.
Private methods improve code reusability inside interfaces. If two default methods need to share code, a private interface method allows them to do so without exposing that private method to implementing classes. HowToDoInJava
β BEFORE β Java 7 (Only Abstract Methods) |
β
AFTER β Java 8 & 9 (default + static + private) |
|---|---|
β BEFORE Java 8 β Old way (No sort() in List interface)
public class BeforeJava8 {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Charlie");
names.add("Alice");
names.add("Bob");
// β Old way β had to use Collections utility class separately
Collections.sort(names); // external utility needed
System.out.println(names); // [Alice, Bob, Charlie]
// β Old way β custom comparator
Collections.sort(names, new Comparator() {
@Override
public int compare(String a, String b) {
return b.compareTo(a); // reverse order
}
});
System.out.println(names); // [Charlie, Bob, Alice]
}
} |
Problem:
Java team wanted to add sort() directly into the List interface in Java 8. But millions of classes implemented List (ArrayList, LinkedList, custom lists). Adding abstract sort() would break ALL of them!
// βββββββββββββββββββββββββββββββββββββ
// ArrayList β Old class, NO changes made
// gets sort() for FREE via default method
// βββββββββββββββββββββββββββββββββββββ
// β
New way β sort() directly on List object (Java 8 default method)
names.sort(Comparator.naturalOrder());
System.out.println("Ascending : " + names); // [Alice, Bob, Charlie]
names.sort(Comparator.reverseOrder());
System.out.println("Descending: " + names); // [Charlie, Bob, Alice]
// βββββββββββββββββββββββββββββββββββββ
// Custom sort with lambda (Java 8)
// βββββββββββββββββββββββββββββββββββββ
List<String> cities = new ArrayList<>();
cities.add("Mumbai");
cities.add("Delhi");
cities.add("Ahmedabad");
cities.add("Pune");
// sort by string length
cities.sort((a, b) -> a.length() - b.length());
System.out.println("By Length : " + cities); // [Pune, Delhi, Mumbai, Ahmedabad] |
Source Code
java.util.Comparator β Java 8 (uses default + static, no private) |
Collection interface |
|---|---|
Comparator interface
βββββββββββββββββββββββββββββββββββββββββ
compare() β abstract
naturalOrder() β STATIC β
called via Comparator.naturalOrder()
reverseOrder() β STATIC β
called via Comparator.reverseOrder()
comparing() β STATIC β
called via Comparator.comparing()
βββββββββββββββββββββββββββββββββββββββββ
public interface Comparator<T> { //actual source (simplified)
// abstract
int compare(T o1, T o2);
// default
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
return (c1, c2) -> {
int res = this.compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
// static
static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}
static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
return Collections.reverseOrder();
}
static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor) {
return (c1, c2) -> keyExtractor.apply(c1)
.compareTo(keyExtractor.apply(c2));
}
} |
Collection interface
βββββββββββββββββββββββββββββββββββββββββ
iterator() β abstract
removeIf() β DEFAULT β
inherited, can override
clearInvalid() β DEFAULT β
inherited, can override
βββββββββββββββββββββββββββββββββββββββββ
// @since 1.2
public interface List<E> extends Collection<E> {
//...
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
}
public interface Collection<E> extends Iterable<E> {
}
// @since 1.5
public interface Iterable<T> {
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
} |
Where Private Interface Methods ARE Meant to Be Used?
Private methods improve code reusability inside interfaces. If two default methods need to share code, a private interface method allows them to do so without exposing that private method to implementing classes.
To avoid duplication, private helper methods are used inside the interface.. Available from Java 9+.
public interface Logger {
// Default method
default void logInfo(String msg) {
print("INFO", msg);
}
// Another default method
default void logError(String msg) {
print("ERROR", msg);
}
// Static method
static void logWarning(String msg) {
formatAndPrint("WARNING", msg);
}
// Private instance method (used by default methods)
private void print(String level, String msg) {
System.out.println(level + ": " + msg);
}
// Private static method (used by static method)
private static void formatAndPrint(String level, String msg) {
System.out.println(level + ": " + msg);
}
}Comparable java.lang.Comparable: int compareTo(Object o1) vs Comparator java.util.Comparator: int compare(Object o1, Object o2) to sort collections
| Feature | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(T o) | compare(T o1, T o2) |
| Sorting logic location | Inside the class | Outside the class |
| Sorting type | Natural/default ordering | Custom ordering |
| Number of sort options | Only ONE | Multiple |
| Modification required | Must modify the class | No class modification |
| Lambda support | No | Yes (Java 8+) |
| Example fields | sort by id | sort by id/name/age |
|
Comparable β natural sorting (inside class) |
Comparator β custom sorting (outside class, multiple ways) |
|---|---|
/*
* Comparable Interface (java.lang.Comparable)
*
* Method:
* int compareTo(T o)
*
* Key Points:
* - The object compares itself with another object.
* - The class must implement Comparable to define natural ordering.
* - Allows only ONE sorting sequence.
* - Sorting logic is written inside the class.
* - Used for natural ordering (default sorting).
*
* Common classes that already implement Comparable:
* String, Integer, Double, Date, Calendar, etc.
*/ |
/*
* Comparator Interface (java.util.Comparator)
*
* Method:
* int compare(T o1, T o2)
*
* Key Points:
* - Compares TWO different objects externally.
* - Sorting logic is written in a separate class or lambda.
* - Allows MULTIPLE sorting sequences.
* - Useful for customized sorting without modifying the class.
*/ |
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee implements Comparable {
private int id;
private String name;
private int age;
private long salary;
// Only ONE natural sorting sequence inside the class. Here we sort by id
@Override
public int compareTo(Employee e) {
if (this.id > e.id) {
return 1;
} else if (this.id < e.id) {
return -1;
} else {
return Character.toString(this.name.charAt(0))
.compareToIgnoreCase(Character.toString(e.name.charAt(0)));
}
}
// Multiple sorting sequences using Comparator
// Sort by Name
public static Comparator<Employee> nameComparator = new Comparator<Employee>() {
@Override
public int compare(Employee e1, Employee e2) {
return e1.getName().compareTo(e2.getName());
}
};
// Sort by ID
public static Comparator<Employee> idComparator = new Comparator<Employee>() {
@Override
public int compare(Employee e1, Employee e2) {
return Integer.compare(e1.getId(), e2.getId());
}
};
public static void main(String[] args) {
Employee e1 = new Employee(5, "Yash", 22, 1000);
Employee e2 = new Employee(8, "Tharun", 24, 25000);
Employee e3 = new Employee(3, "Arun", 21, 15000);
List<Employee> list = new ArrayList<>();
list.add(e1);
list.add(e2);
list.add(e3);
// Natural sorting using Comparable
Collections.sort(list);
System.out.println("Sort by Comparable (id): " + list);
// Sorting by name using Comparator
Collections.sort(list, Employee.nameComparator);
System.out.println("Sort by Name Comparator: " + list);
// Sorting by id using Comparator
Collections.sort(list, Employee.idComparator);
System.out.println("Sort by ID Comparator: " + list);
}
} |
|
Java 8 Lambda Example (Comparator) Example: My stackoverflow post
List list1 = Arrays.asList(5, 2, 3, 4, 1);
Collections.sort(list1); // Ascending order (natural sort)
System.out.println("Ascending Order: "+ list1 ); // [1, 2, 3, 4, 5]
List list2 = Arrays.asList(5, 2, 3, 4, 1);
// Descending order using lambda Comparator
Collections.sort(list2, (a, b) -> b - a);
System.out.println("Descending Order:"+ list2 ); // [5, 4, 3, 2, 1]
public static Comparator nameComparator =
(e1, e2) -> e1.getName().compareTo(e2.getName());
public static Comparator idComparator =
(e1, e2) -> Integer.compare(e1.getId(), e2.getId());java.util.Collections is a utility class consists exclusively of static methods that operate on or return collections.
|
|
Collection Framework is a combination of classes and interface, which is used to store and manipulate the data in the form of objects.
interface Collection<E>extends Iterable
-
Iterator is a fail-fast in nature. i.e it throws
ConcurrentModificationExceptionif a collection is modified while iterating other than itβs ownremove()method. - Where as Enumeration is fail-safe in nature. It doesnβt throw any exceptions if a collection is modified while iterating.
fail-fast vs fail-safe Iterator
| Fail-Fast Iterators (Not thread-safe) Works on original collection - throws ConcurrentModificationException Performance: Faster (no cloning overhead) Vector is FAIL-FAST, despite being thread-safe! ArrayList |
Fail-Safe Iterators (Thread-safe) Works on cloned copy Performance: Slower (cloning overhead) CopyOnWriteArrayList |
|---|---|
Hashtable traverse using Enumeration is Fail-Safe, Iterator is Fail-Fast. Behavior: Immediately throw ConcurrentModificationException if the collection is modified while iterating (except through the iterator's own methods). How it works: Maintains a modCount (modification count) that tracks structural changes. The iterator checks if this count has changed during iteration. Examples: ArrayList, HashMap, HashSet, Most collections in java.util package
Use case: When you want to detect concurrent modifications early and fail immediately. javaList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
list.add("D"); // Throws ConcurrentModificationException
}
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
// Thread 1: Iterating
Thread t1 = new Thread(() -> {
try {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println("Thread 1: " + iterator.next());
Thread.sleep(100); // Simulate work
}
} catch (ConcurrentModificationException e) {
System.out.println("Thread 1: ConcurrentModificationException caught!");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// Thread 2: Modifying (changes modCount)
Thread t2 = new Thread(() -> {
try {
Thread.sleep(150);
//list.add("E"); // Increments modCount
list.add(4, "E"); // No-Increments modCount - even T1 leads to ConcurrentModificationException
System.out.println("Thread 2: Added element E");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
/* output
Thread 1: A
Thread 1: B
Thread 2: Added element E
Thread 1: ConcurrentModificationException caught!
*/ |
Behavior: Work on a cloned copy of the collection. No exception is thrown even if the collection is modified during iteration. How it works: Creates a snapshot/clone of the collection at the time the iterator is created. Changes to the original collection don't affect the iteration. Examples: CopyOnWriteArrayList, ConcurrentHashMap, Collections in java.util.concurrent package
Use case: When you need thread-safe iteration in concurrent environments. javaList<String> list = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
list.add("D"); // No exception - iterator works on snapshot
}
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
// Thread 1: Iterating
Thread t1 = new Thread(() -> {
try {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println("Thread 1: " + iterator.next());
Thread.sleep(100); // Simulate work
}
} catch (ConcurrentModificationException e) {
System.out.println("Thread 1: ConcurrentModificationException caught!");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// Thread 2: Modifying (changes modCount)
Thread t2 = new Thread(() -> {
try {
Thread.sleep(150);
list.add("E"); // Increments modCount
System.out.println("Thread 2: Added element E");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
/* output
Thread 1: A
Thread 1: B
Thread 2: Added element E
Thread 1: C
Thread 1: D
*/ |
package java.util; JDK1.2 πHashMap vs JDK1.0 πHashtable vs JDK5 πConcurrentHashMap - Complete Comparison
ConcurrentHashMap: Maps the specified key to the specified value in this table. Neither the key nor the value can be null
| java.util.HashMap (Java 1.2) | java.util.Hashtable (Java 1.0 legacy) | java.util.concurrent.ConcurrentHashMap (Java 1.5) |
|---|---|---|
// HashMap - Allows null key and values
Map hashMap = new HashMap<>();
hashMap.put(null, "value1"); // β
OK
hashMap.put("key1", null); // β
OK
hashMap.put("key2", null); // β
OK (multiple nulls)
System.out.println(hashMap);
// Output: {null=value1, key1=null, key2=null}
|
// Hashtable - Does NOT allow null key or values
Map hashtable = new Hashtable<>();
hashtable.put(null, "value1"); // β NullPointerException
hashtable.put("key1", null); // β NullPointerException
|
// ConcurrentHashMap - Does NOT allow null key or values
Map concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put(null, "value1"); // β NullPointerException
concurrentMap.put("key1", null); // β NullPointerException
|
How it works:
βββββββββββββββββββββββββββββββββββββββββ
Step 1: key.hashCode() β generates hash number
Step 2: hash % array.length β finds bucket index
Step 3: store in that bucket β LinkedList if collision
Step 4: LinkedList > 8 nodes β converts to Red-Black Tree
βββββββββββββββββββββββββββββββββββββββββ
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
System.out.println("=== Iterator (Fail-Fast) ===");
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
System.out.println(key + " = " + map.get(key));
// Modifying during iteration
if (key.equals("A")) {
map.put("D", 4); // Modifies modCount
}
}
/*
=== Iterator (Fail-Fast) ===
A = 1
Exception in thread "main" java.util.ConcurrentModificationException
*/ |
Hashtable<String, Integer> table = new Hashtable<String, Integer>() {{
put("A", 1);
put("B", 2);
put("C", 3);
}}; // java 9+ = new Hashtable<>(Map.of("A", 1, "B", 2, "C", 3));
System.out.println("=== Iterator (Fail-Fast) ===");
try {
Iterator<String> iterator = table.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
System.out.println(key);
if (key.equals("A")) {
table.put("D", 4); // Throws exception
}
}
} catch (ConcurrentModificationException e) {
System.out.println("Iterator: ConcurrentModificationException!");
}
System.out.println("\n=== Enumeration (NOT Fail-Fast) ===");
Enumeration<String> enumeration = table.keys();
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
System.out.println(key);
if (key.equals("A")) {
table.put("D", 4); // May cause unpredictable behavior
}
}
System.out.println("Enumeration completed (no exception)");
/* output
=== Iterator (Fail-Fast) ===
A
Iterator: ConcurrentModificationException!
=== Enumeration (NOT Fail-Fast) ===
A
D
C
B
Enumeration completed (no exception)
*/ |
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
System.out.println(key + " = " + map.get(key));
// Modifying during iteration - NO EXCEPTION
if (key.equals("A")) {
map.put("D", 4);
System.out.println("Added D=4 during iteration");
}
}
System.out.println("\nFinal map: " + map);
System.out.println("No exception thrown!");
/*
A = 1
Added D=4 during iteration
B = 2
C = 3
D = 4
Final map: {A=1, B=2, C=3, D=4}
No exception thrown!
*/ |
| Performance Example | Metrix output |
|---|---|
public class ThreadSafetyComparison {
/** Number of CPUS, to place bounds on some sizings */
static final int NCPU = Runtime.getRuntime().availableProcessors();
private static final int THREAD_COUNT = 16; //NCPU=12
private static final int OPERATIONS = 1000;
public static void main(String[] args) throws InterruptedException {
System.out.println("THREAD_COUNT :"+THREAD_COUNT);
System.out.println("=== HashMap (Not Thread-Safe) ===");
testMap(new HashMap<>());
System.out.println("\n=== Hashtable (Thread-Safe - Synchronized) ===");
testMap(new Hashtable<>());
System.out.println("\n=== ConcurrentHashMap (Thread-Safe - Lock Striping) ===");
testMap(new ConcurrentHashMap<>());
}
private static void testMap(Map<Integer, Integer> map) throws InterruptedException {
long startTime = System.currentTimeMillis();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < OPERATIONS; j++) {
int key = threadId * OPERATIONS + j;
map.put(key, key);
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println("Expected size: " + (THREAD_COUNT * OPERATIONS));
System.out.println("Actual size: " + map.size());
System.out.println("Time taken: " + (endTime - startTime) + "ms");
System.out.println("Data consistency: " +
(map.size() == THREAD_COUNT * OPERATIONS ? "β
PASS" : "β FAIL"));
}
}
|
THREAD_COUNT :4
THREAD_COUNT :4
=== HashMap (Not Thread-Safe) ===
Expected size: 4000
Actual size: 3921
Time taken: 24ms
Data consistency: β FAIL
=== Hashtable (Thread-Safe - Synchronized) ===
Expected size: 4000
Actual size: 4000
Time taken: 3ms
Data consistency: β
PASS
=== ConcurrentHashMap (Thread-Safe - Lock Striping) ===
Expected size: 4000
Actual size: 4000
Time taken: 8ms
Data consistency: β
PASS
THREAD_COUNT :12
=== HashMap (Not Thread-Safe) ===
Expected size: 12000
Actual size: 10623
Time taken: 31ms
Data consistency: β FAIL
=== Hashtable (Thread-Safe - Synchronized) ===
Expected size: 12000
Actual size: 12000
Time taken: 15ms
Data consistency: β
PASS
=== ConcurrentHashMap (Thread-Safe - Lock Striping) ===
Expected size: 12000
Actual size: 12000
Time taken: 12ms
Data consistency: β
PASS
THREAD_COUNT :16
=== HashMap (Not Thread-Safe) ===
Expected size: 16000
Actual size: 15293
Time taken: 30ms
Data consistency: β FAIL
=== Hashtable (Thread-Safe - Synchronized) ===
Expected size: 16000
Actual size: 16000
Time taken: 12ms
Data consistency: β
PASS
=== ConcurrentHashMap (Thread-Safe - Lock Striping) ===
Expected size: 16000
Actual size: 16000
Time taken: 22ms
Data consistency: β
PASS
|
ConcurrentHashMap is thread-safeNOT via synchronization, but via segment-level locking (Java 7) and CAS with bucket-level locking (Java 8+) β allowing multiple threads to write in parallel without blocking each other.
How it actually works
Before Java 8 β Segment Locking |
Java 8+ β CAS + Node Level Locking |
|---|---|
ConcurrentHashMap
βββ Segment[0] π β locks only this
βββ Segment[1]
βββ Segment[2] π β locks only this
βββ Segment[15]
Map divided into 16 segments by default
Each segment is an independent lock
16 threads can write simultaneously β one per segment |
ConcurrentHashMap
βββ Node[0] π β locks only this bucket
βββ Node[1]
βββ Node[2] π β locks only this bucket
βββ Node[n]
Locks at individual bucket (node) level
Uses CAS (Compare-And-Swap) for lock-free reads
Even more granular than segments |
Java 8 ConcurrentHashMap - Node Level Lock Explanation with Example
ConcurrentMap - MultiThread |
Output |
|---|---|
public class ConcurrentMapThreadDemo {
@AllArgsConstructor
static class Data {
int value;
@Override
public String toString() {
return "Data{value=" + value +
", identityHash=" + System.identityHashCode(this) + "}";
}
}
static ConcurrentHashMap<String, Data> map = new ConcurrentHashMap<>();
// approximate bucket index (like internal spread)
static int bucketIndex(String key, int tableSize) {
int h = key.hashCode();
h ^= (h >>> 16); // spread hash
return (tableSize - 1) & h;
}
static void putKey(String key, int value) {
Data data = new Data(value);
if ( map.containsKey(key) ) {
System.err.println(Thread.currentThread().getName() +
" GET key=" + key + " val=" + map.get(key)
);
}
map.put(key, data);
int bucket = bucketIndex(key, 16); // default table approx
System.out.println(
Thread.currentThread().getName() +
" PUT key=" + key + " val=" + value +
" hash=" + key.hashCode() + " bucket=" + bucket +
" identityHash=" + System.identityHashCode(data)
);
}
public static void main(String[] args) throws Exception {
System.out.println("Initial inserting 4 keys\n");
for (int i = 1; i <= 4; i++) {
putKey("T1_Key_" + i, i);
}
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 6; i++) {
//Thread.sleep(3500);
putKey("T1_Key_" + i, (i * 10));
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
//for (int i = 6; i >= 1; i--) {
for (int i = 1; i <= 6; i++) {
putKey("T1_Key_" + i, (i * 100));
}
}, "Thread-2");
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("\nFinal Map Size = " + map.size());
}
} |
Initial inserting 4 keys
main PUT key=T1_Key_1 val=1 hash=-597618225 bucket=14 identityHash=1288141870
main PUT key=T1_Key_2 val=2 hash=-597618224 bucket=1 identityHash=2054881392
main PUT key=T1_Key_3 val=3 hash=-597618223 bucket=0 identityHash=966808741
main PUT key=T1_Key_4 val=4 hash=-597618222 bucket=3 identityHash=1908153060
Thread-1 GET key=T1_Key_1 val=Data{value=1, identityHash=1288141870}
Thread-2 GET key=T1_Key_1 val=Data{value=1, identityHash=1288141870}
Thread-1 PUT key=T1_Key_1 val=10 hash=-597618225 bucket=14 identityHash=1032642556
Thread-2 PUT key=T1_Key_1 val=100 hash=-597618225 bucket=14 identityHash=1195851372
Thread-1 GET key=T1_Key_2 val=Data{value=2, identityHash=2054881392}
Thread-1 PUT key=T1_Key_2 val=20 hash=-597618224 bucket=1 identityHash=860567101
Thread-2 GET key=T1_Key_2 val=Data{value=20, identityHash=860567101}
Thread-1 GET key=T1_Key_3 val=Data{value=3, identityHash=966808741}
Thread-2 PUT key=T1_Key_2 val=200 hash=-597618224 bucket=1 identityHash=1233176820
Thread-1 PUT key=T1_Key_3 val=30 hash=-597618223 bucket=0 identityHash=930395276
Thread-2 GET key=T1_Key_3 val=Data{value=30, identityHash=930395276}
Thread-1 GET key=T1_Key_4 val=Data{value=4, identityHash=1908153060}
Thread-2 PUT key=T1_Key_3 val=300 hash=-597618223 bucket=0 identityHash=1304384796
Thread-2 GET key=T1_Key_4 val=Data{value=40, identityHash=1920351238}
Thread-1 PUT key=T1_Key_4 val=40 hash=-597618222 bucket=3 identityHash=1920351238
Thread-2 PUT key=T1_Key_4 val=400 hash=-597618222 bucket=3 identityHash=146653903
Thread-2 GET key=T1_Key_5 val=Data{value=50, identityHash=702447243}
Thread-1 PUT key=T1_Key_5 val=50 hash=-597618221 bucket=2 identityHash=702447243
Thread-2 PUT key=T1_Key_5 val=500 hash=-597618221 bucket=2 identityHash=1987525098
Thread-1 PUT key=T1_Key_6 val=60 hash=-597618220 bucket=5 identityHash=1129205587
Thread-2 GET key=T1_Key_6 val=Data{value=60, identityHash=1129205587}
Thread-2 PUT key=T1_Key_6 val=600 hash=-597618220 bucket=5 identityHash=964659384
Final Map Size = 6,source>Java8 Source Code
public V put(K key, V value) {
return putVal(key, value, false);
}
public V putIfAbsent(K key, V value) {
return putVal(key, value, true);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node[] tab = table;;) {
Node f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node(hash, key, value)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node pred = e;
if ((e = e.next) == null) {
pred.next = new Node(hash, key, value);
break;
}
}
}
else if (f instanceof TreeBin) {
//...
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
//...
break;
}
}
}
addCount(1L, binCount);
return null;
} |
|
ConcurrentHashMap operations are thread-safe individually, but containsKey() + put() is not atomic, so another thread may update the key between those calls, causing the behavior you observed. Race condition betweencontainsKey() and put()
Even if you use:
if(map.containsKey(key)) {
map.get(key);
}
map.put(key,value);
another thread may insert between them:
T1 containsKey(K) β false
T2 put(K)
T1 put(K)
So T1 never sees T2's value. This is called a race condition.Empty bucket β CAS insert
Non-empty bucket β synchronized(head node)
Threads can still update different buckets concurrently.
ConcurrentHashMap
table[]
β Thread behavior:
βββ bucket0 β Node Thread1 β bucket2 π
βββ bucket1 β Node Thread2 β bucket7 π
βββ bucket2 β Node Thread3 β bucket11 π
βββ bucketN For such cases atomic methods like |
|
| synchronized HashMap | ConcurrentHashMap | |
|---|---|---|
| Lock scope | Entire map π | One bucket/segment |
| Concurrent writers | β1 at a time | β Many simultaneously |
| How | synchronized keyword | CAS + node locks |
| Performance | Slow under contention | High throughput |
ConcurrentHashMap does not allow null keys or values to avoid ambiguity during concurrent access because
get() returning nullcannot distinguish between "key not present" and "value is null".
Main Reason: Avoid Ambiguity During Concurrent Access
HashMap (single-threaded) |
ConcurrentHashMap (multi-threaded)β No null (to avoid ambiguity) |
|---|---|
// null key maps to bucket[0]
map.put(null, "hello"); // β
works
map.put("k", null); // β
works
// Safe in single thread:
if (map.containsKey("k")) {
V v = map.get("k"); // safe
// you control timing
} |
map.put(null, "val"); // β NullPointerException
map.put("k", null); // β NullPointerException
// WHY? Race between check + get:
if (map.containsKey("k")) { // Thread A checks β true
// Thread B removes "k" HERE!
V v = map.get("k"); // Thread A gets β null π₯
Cannot determine null because:
1. key was removed? β null
2. key exists but value was null? β null
IMPOSSIBLE to distinguish safely!
}
// containsKey + get is NOT atomic
// Another thread can act in between |
HashMap vs ConcurrentHashMap: Java 7 and Java 8+ Internal Comparison
HashMap (Single-threaded)Java 7 Source Java 8 |
ConcurrentHashMap (Multi-threaded - Before Java 8)Java 7 Source |
ConcurrentHashMap (Java 8+)Java 8+ Source |
|---|---|---|
| π’ Hash Spreading | π’ Hash Spreading | π’ Hash Spreading |
/** * Java 7 HashMap hash spreading */
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/** * Java 8 HashMap hash spreading */
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}Used before bucket index calculation. | /** * Spread hash across segments */
private static int hash(int h) {
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
} | /** * Fast hash spreading */
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
} |
| π Bucket Index Calculation | π Bucket Index Calculation | π Bucket Index Calculation |
// Java 7
int index = (tab.length - 1) & hash;
// table = new Entry[DEFAULT_INITIAL_CAPACITY];
// Java 8
int index = (n - 1) & root.hash; // Node[] tab, TreeNode root | int index = (seg.table.length - 1) & hash;
//
static int indexFor(int h, int length) {
return h & (length-1);
} | int index = (n - 1) & hash; |
| π³ Collision Handling | π³ Collision Handling | π³ Collision Handling |
Java 7
Entry -> Entry -> Entry Entry -> TreeNode | HashEntry β HashEntry β HashEntry Only linked list. No tree structure. | Node -> Node -> Node If bucket β₯ 8 TreeNode (Red-Black Tree) Improves worst-case lookup. |
| π³ Treeify Threshold (Treeification was introduced in Java8) | π³ Treeify Threshold |
π³ Treeify Threshold (Treeification was introduced in Java8) |
// Java 7 does NOT support treeification at all.
// Buckets are always linked lists, so there is no TREEIFY_THRESHOLD.
//
// Java8
static final int TREEIFY_THRESHOLD = 8;
if bucket size >= 8
convert linked list β red-black tree
static final int UNTREEIFY_THRESHOLD = 6;
When resizing/shrinking:
if bucket size <= 6
convert tree β linked list
static final int MIN_TREEIFY_CAPACITY = 64;
Important rule:
More buckets reduces collisions better than building trees
if table size < 64
DO NOT treeify
instead resize table
bucket
A β B β C β D β E β F β G β H
(8 nodes)
table size β₯ 64
β
convert to
D
/ \
B F
/ \ / \
A C E G
\
H |
β Not supported
// Java 7 does NOT support treeification at all.
// Buckets are always linked lists, so there is no TREEIFY_THRESHOLD. | static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64; |
| π Resize Trigger | π Resize Trigger | π Resize Trigger |
if (size > threshold) resize(); Threshold: // java 7 Treeify Threshold
threshold = (int)(capacity * loadFactor); Default capacity = 16 loadFactor = 0.75 |
Resize occurs per Segment. Segment.rehash() Only the locked segment grows. |
Resize controlled by: sizeCtl Determines when to resize. |
1 create new table
2 rehash all entries
3 move nodes Concurrent resize may create cyclic linked list. | Segment locked
Segment resized
Entries moved Other segments unaffected. |
Uses cooperative resizing. Multiple threads move entries. Internal structures: ForwardingNode
nextTable
transferIndex |
| π Size Tracking | π Size Tracking | π Size Tracking |
transient int size; Simple counter. Not thread safe. |
Each segment tracks count. Segment.count Total size = sum of segments. |
Striped counters. baseCount CounterCell[] Reduces contention. |
| π Iterator Behavior | π Iterator Behavior | π Iterator Behavior |
|
Fail-Fast iterator. modCount Modification during iteration throws: ConcurrentModificationException |
Weakly consistent iterator. No exception. May reflect partial updates. |
Weakly consistent iterator. Allows concurrent modifications. |
| π§© Node Types | π§© Node Types | π§© Node Types |
Entry TreeNode (Java 8+) | HashEntry | Node // regular node
TreeNode // red-black tree node
TreeBin // tree container
ForwardingNode // during resize
ReservationNode // computeIfAbsentUsed for trees and resizing. |
| βοΈ Compute APIs | βοΈ Compute APIs | βοΈ Compute APIs |
compute()
computeIfAbsent()
computeIfPresent()
merge() Not thread safe. |
Supported but rely on segment locking. |
Atomic operations. compute()
computeIfAbsent()
merge() Executed with bucket synchronization. |
| ποΈ Storage Structure | ποΈ Storage Structure | ποΈ Storage Structure |
// Single flat bucket array
transient Entry<K,V>[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value; // not volatile
Entry<K,V> next;
final int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
key = k;
value = v;
next = n;
hash = h;
}
} |
// Array of Segments (default 16)
final Segment<K,V>[] segments;
static class Segment<K,V> extends ReentrantLock
implements Serializable {
transient volatile HashEntry<K,V>[] table;
transient int threshold;
final float loadFactor;
Segment(float lf, int threshold,
HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
}
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value; // visibility across threads
volatile HashEntry<K,V> next;
} |
// Flat Node array (similar to HashMap)
// but supports CAS + volatile
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
static class Node<K,V>
implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
// Segment kept only for serialization compatibility
static class Segment<K,V> extends ReentrantLock
implements Serializable {
final float loadFactor;
Segment(float lf) {
this.loadFactor = lf;
}
} |
| β put("apple", 1) Flow | β put("apple", 1) Flow | β put("apple", 1) Flow |
// Step 1: hash the key
int hash = hash("apple");
// Step 2: find bucket index
int i = indexFor(hash, table.length);
// Step 3: walk linked list
for (Entry e = table[i];
e != null; e = e.next) {
if (e.hash == hash && e.key.equals("apple")) {
e.value = 1; // overwrite
return;
}
}
// Step 4: insert new entry
// β οΈ NO LOCK (not thread-safe)
table[i] = new Entry(hash, "apple", 1, table[i]); |
// Step 1: compute hash
int hash = hash("apple");
// Step 2: find segment
Segment<K,V> seg = segmentFor(hash);
// Step 3: lock that segment only
seg.lock();
try {
int i = indexFor(hash, seg.table.length);
HashEntry e = seg.table[i];
while (e != null) {
if (e.hash == hash && e.key.equals("apple")) {
e.value = 1;
return;
}
e = e.next;
}
seg.table[i] =
new HashEntry(
hash,"apple",1,
seg.table[i]);
} finally {
seg.unlock();
} |
// Step 1: spread hash
int hash = spread("apple".hashCode());
// Step 2: find bucket
int i = (n - 1) & hash;
// Step 3: read head node
Node<K,V> f = tabAt(table, i);
if (f == null) {
// CAS insertion
casTabAt(table, i, null,
new Node(hash,"apple",1,null));
} else {
// lock only head node
synchronized (f) {
if (tabAt(table, i) == f) {
Node e = f;
while (e.next != null) {
if (e.hash == hash
&& e.key.equals("apple")) {
e.val = 1;
return;
}
e = e.next;
}
e.next =
new Node(hash,"apple",1,null);
}
}
// Convert to Tree if bucket >= 8
treeifyBin(table, i);
} |
|
π Locking None β Not thread safe |
π Locking ReentrantLock per Segment Maximum β 16 concurrent writers |
π Locking CAS (empty bucket) synchronized on head node |
|
null key/value β Allowed |
null key/value β Not allowed |
null key/value β Not allowed |
// HashMap internal Entry structure (Java 7)
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
// Creates a new entry
Entry(int h, K k, V v, Entry<K,V> n) {
key = k;
value = v;
next = n;
hash = h;
}
}
// Entry set view
private final class EntrySet
extends AbstractSet<Map.Entry<K,V>> { }Example HashMap<String,Integer> map = new HashMap<>();
map.put("Apple", 10);
map.put("Banana", 20);
map.put(null, 30); // HashMap allows null key
System.out.println(map.get("Apple")); |
// The segments, each of which is a specialized hash table.
final Segment<K,V>[] segments;
//Segment uses locking mechanism (extends ReentrantLock)
static class Segment<K,V>
extends ReentrantLock implements Serializable {
private static final long serialVersionUID =
2249069246763182397L;
transient volatile HashEntry<K,V>[] table;
transient int threshold;
final float loadFactor;
Segment(float lf, int threshold,
HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
}
final class EntrySet
extends AbstractSet<Map.Entry<K,V>> { }Example ConcurrentHashMap<String,Integer> map = new ConcurrentHashMap<>();
map.put("Apple", 10);
map.put("Banana", 20);
// map.put(null,30); β throws NullPointerException
// map.put(10,null); β throws NullPointerException |
// Segment class retained only for serialization compatibility
static class Segment<K,V>
extends ReentrantLock implements Serializable {
private static final long serialVersionUID =
2249069246763182397L;
final float loadFactor;
Segment(float lf) {
this.loadFactor = lf;
}
}/* -------- Node structure -------- */
// Key-value entry (Java 8+)
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
// Main table storing nodes
transient volatile Node<K,V>[] table;
// Used during resizing
private transient volatile Node<K,V>[] nextTable;Example ConcurrentHashMap<String,Integer> map = new ConcurrentHashMap<>();
map.put("Apple", 10);
map.put("Banana", 20);
// map.put(null,10); β Not allowed
// map.put("Orange", null); β Not allowed |
TreeSet/TreeMap & HashSet/HashMap β Data Structure & Null Values
HashSet / HashMap |
TreeSet/TreeMap |
|---|---|
Internal Data Structure β Array + LinkedList + Red-Black Tree
HashMap = Hash + Map β key-value + null key β
HashSet = Hash + Set β only keys + null β
How it works:
βββββββββββββββββββββββββββββββββββββββββ
Step 1: key.hashCode() β generates hash number
Step 2: hash % array.length β finds bucket index
Step 3: store in that bucket β LinkedList if collision
Step 4: LinkedList > 8 nodes β converts to Red-Black Tree
βββββββββββββββββββββββββββββββββββββββββ
Why HashMap allows null key?
// HashMap handles null key specially:
static final int hash(Object key) {
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// null key always goes to bucket index 0 β special hardcoded handling |
Internal Data Structure β Red-Black Tree (always)
TreeMap = Tree + Map β key-value + NO null key β
TreeSet = Tree + Set β only keys + NO null β
How it works:
βββββββββββββββββββββββββββββββββββββββββ
Step 1: compare(newKey, rootKey) β left if smaller, right if bigger
Step 2: insert and auto-balance β Red-Black Tree rules
Step 3: always maintains sorted order
βββββββββββββββββββββββββββββββββββββββββ
Why TreeMap/TreeSet doesn't allow null key?
// TreeMap internally does this for every insert:
key.compareTo(anotherKey)
// if key is null:
null.compareTo("Alice") // β NullPointerException!
// null has no compareTo method β cannot be compared β cannot be placed in tree |
Complete Summary Table
| Feature | HashMap |
HashSet |
TreeMap |
TreeSet |
|---|---|---|---|---|
| Internal Structure | Array + LinkedList + RBTree | Array + LinkedList + RBTree | Red-Black Tree | Red-Black Tree |
| Null Key | β One allowed | β | β NPE | β |
| Null Value | β Many allowed | β One allowed | β allowed | β NPE |
| Ordering | β No order | β No order | β Sorted by key | β Sorted |
| Duplicates | β No dup keys | β No dups | β No dup keys | β No dups |
| Performance | O(1) avg | O(1) avg | O(log n) | O(log n) |
| Thread Safe | β No | β No | β No | β No |
| Based On | HashMap | β | NavigableMap | TreeMap |
ArrayList Default to empty-array, The default capacity of 10 is NOT allocated immediately. It's allocated lazily on the first add() operation. - ArrayList grows by approximately 50% (1.5x) of current capacity, not when it reaches size 10.
Capacity: 15 β 22 (15 + 15/2 = 22, rounded down from 22.5)
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
Capacity: 15 β 22 (15 + 15/2 = 22, rounded down from 22.5)System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
//The number of times this list has been <i>structurally modified</i>.
protected transient int modCount = 0;
}
public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public Vector() {
this(10);
}
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
}
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final int DEFAULT_CAPACITY = 10;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// The size of the ArrayList (the number of elements it contains).
private int size;
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
// ...
}
}Garbage Collection (GC)
Primary Purpose: Automatic memory management - the GC identifies and removes objects that are no longer reachable/referenced, freeing up memory for new objects.
class System (sourece)
public final class System {
/** Don't let anyone instantiate this class */
private System() {
}
public final static PrintStream out = null;
public final static PrintStream err = null;
private static volatile Console cons = null;
// @since 1.6
public static Console console() {
if (cons == null) {
synchronized (System.class) {
cons = sun.misc.SharedSecrets.getJavaIOAccess().console();
}
}
return cons;
}
public static native long currentTimeMillis();
//@since 1.5
public static native long nanoTime();
/** Copies an array from the specified source array, beginning at the
* specified position, to the specified position of the destination array.
* A subsequence of array components are copied from the source
* array referenced by <code>src</code> to the destination array
* referenced by <code>dest</code>. The number of components copied is
* equal to the <code>length</code> argument. The components at
* positions <code>srcPos</code> through */
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
private static Properties props;
private static native Properties initProperties(Properties props);
public static String lineSeparator() {
return lineSeparator;
}
private static String lineSeparator;
public static void setProperties(Properties props) {
//...
}
public static String getProperty(String key) {
// ...
}
public static Properties getProperties() {
//...
}
public static String setProperty(String key, String value) {
//...
}
/** Removes the system property indicated by the specified key.
* @since 1.5 */
public static String clearProperty(String key) {
checkKey(key);
SecurityManager sm = getSecurityManager();
if (sm != null) {
sm.checkPermission(new PropertyPermission(key, "write"));
}
return (String) props.remove(key);
}
// Terminates the currently running Java Virtual Machine. The
public static void exit(int status) {
Runtime.getRuntime().exit(status);
}
/** Calling the <code>gc</code> method suggests that the Java Virtual
* Machine expend effort toward recycling unused objects in order to
* make the memory they currently occupy available for quick reuse.
* When control returns from the method call, the Java Virtual
* Machine has made a best effort to reclaim space from all discarded
* objects. */
public static void gc() {
Runtime.getRuntime().gc();
}
@CallerSensitive
public static void load(String filename) {
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
// ...
}Object: Created based on the class template using a constructor.
Real-world objects contain state and behavior. state is stored in fields and behavior is exposed through methods.
public class Object {
// The {@code Class} object that represents the runtime class of this object.
public final native Class<?> getClass();
protected native Object clone() throws CloneNotSupportedException;
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
/** Causes the current thread to wait until another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object, or
* some other thread interrupts the current thread, or a certain
* amount of real time has elapsed. */
public final void wait(long timeout, int nanos) throws InterruptedException {
//...
}
public final void wait() throws InterruptedException {
wait(0);
}
public final native void wait(long timeout) throws InterruptedException;
/** Wakes up a single thread that is waiting on this object's
* monitor. If any threads are waiting on this object, one of them
* is chosen to be awakened. The choice is arbitrary and occurs at
* the discretion of the implementation. A thread waits on an object's
* monitor by calling one of the {@code wait} methods. */
public final native void notify();
/** Wakes up all threads that are waiting on this object's monitor. A
* thread waits on an object's monitor by calling one of the {@code wait} methods. */
public final native void notifyAll();
/**
* Called by the garbage collector on an object when garbage collection
* determines that there are no more references to the object.
* A subclass overrides the {@code finalize} method to dispose of
* system resources or to perform other cleanup. */
protected void finalize() throws Throwable { }
}π Garbage Collector (GC), External Call System.gc() is just a request. JVM decides the clean
What is GC?
- Automatically removes unused objects from Heap
- Runs in background via GC thread
- All application threads pause during GC β called "Stop The World (STW)"
- Cannot be forced β
System.gc()is just a request, JVM decides-
System.gc()Internally callsRuntime.getRuntime().gc()β both are same - Calling it explicitly is bad practice in production code. JVM may ignore it β no guarantee it will run
-
Person p = new Person(); new Person() β Heap, 'p' reference β Stack
p = null; eligible for GC β no reference pointing to object
Simple Rule:
ββββββββββββββββββββββββββββββββββββββββββ
Objects (Heap) β GC cleans β
Classes (Metaspace)β GC cleans (Full GC) β
Methods (Stack) β JVM auto cleans β
Instructions (PC) β JVM auto cleans β
Native (C/C++) β OS/manual cleans β
ββββββββββββββββββββββββββββββββββββββββββSummary Table
| Memory Area | GC Applies? | Cleaned By | Error if Full |
|---|---|---|---|
| Heap (Young + Old) | β Yes | Minor / Major / Full GC | OutOfMemoryError |
| Metaspace | β Yes (partial) | Full GC only | OutOfMemoryError: Metaspace |
| Stack | β No | JVM auto (LIFO) | StackOverflowError |
| PC Register | β No | JVM auto | β |
| Native Method Stack | β No | OS / JNI manually | StackOverflowError |
Object eligible for GC, finalize() |
GC Types, Algorithms, Flags |
|---|---|
Object eligible for GC when:
* No references pointing to it
* Reference set to null
* Reference out of scope
* Island of isolation (objects pointing to each other but no outside reference)
What causes frequent GC?
* Too small heap (-Xmx too low)
* Memory leak (references held unnecessarily)
* Large object creation (directly goes to Old Generation)
* Poor object reuse (creating new objects in loops)
* Static references (held for entire app lifetime)
Finalization (Deprecated Java 9+, Removed Java 18+)
* finalize() called before GC removes object β unreliable, avoid
* Use try-with-resources or Cleaner API instead |
GC Types
* Minor GC β cleans Young Generation (Eden + Survivors) β fast, frequent
* Major GC β cleans Old Generation β slow, infrequent
* Full GC β cleans Entire Heap + Metaspace β slowest, STW pause
GC Algorithms (JVM options)
* Serial GC β single thread, simple (-XX:+UseSerialGC)
* Parallel GC β multiple threads, throughput (-XX:+UseParallelGC)
* G1 GC β default Java 9+, balanced (-XX:+UseG1GC)
* ZGC β low latency, Java 15+ (-XX:+UseZGC)
* Shenandoah β low pause, Java 12+ (-XX:+UseShenandoahGC)
JVM Heap Size Flags
-Xms β initial heap size e.g. -Xms256m
-Xmx β maximum heap size e.g. -Xmx1024m
-Xmn β young generation size e.g. -Xmn256m |
Java-1.2 π Purpose of public abstract class java.lang.ref.Reference { }
Reference<T> is the base class for special reference types used by the Garbage Collector.
It allows the JVM to manage memory more flexibly than normal strong references.
Why it exists?
// Normal strong reference β object NEVER GC'd until reference is null
Person p = new Person(); // p holds strong reference
// GC cannot collect this β even if memory is low| Problem | Solution via Reference |
|---|---|
| Caches holding objects forever |
SoftReference β JVM auto collects when memory is low |
| No way to tell JVM "collect if memory needed" |
WeakReference β JVM collects at next GC cycle |
| No way to know if object is already GC'd |
PhantomReference + ReferenceQueue β notifies after object removed |
finalize() unreliable for cleanup |
PhantomReference + Cleaner API β reliable post-GC cleanup |
| Strong reference held unnecessarily | Set reference to null or use WeakReference to allow GC |
4 Types of References |
GC Collection Order |
|---|---|
import java.lang.ref.*;
Person person = new Person("Alice");
// βββββββββββββββββββββββββββββββββββββ
// 1. STRONG REFERENCE (default)
// βββββββββββββββββββββββββββββββββββββ
// * Never GC'd until explicitly null
// * Normal Java reference
Person strong = new Person("Alice"); // strong reference
strong = null; // now eligible for GC
// βββββββββββββββββββββββββββββββββββββ
// 2. SOFT REFERENCE (Java 1.2)
// βββββββββββββββββββββββββββββββββββββ
// * GC collects ONLY when JVM runs low on memory
// * Best for: CACHE (image cache, data cache)
SoftReference<Person> soft = new SoftReference<>(new Person("Alice"));
Person s = soft.get(); // returns object if not GC'd yet
if (s == null) {
// GC already collected it β memory was low
// reload / recreate object
}
// βββββββββββββββββββββββββββββββββββββ
// 3. WEAK REFERENCE (Java 1.2)
// βββββββββββββββββββββββββββββββββββββ
// * GC collects at NEXT GC cycle regardless of memory
// * Best for: WeakHashMap, listeners, canonicalized mappings
WeakReference<Person> weak = new WeakReference<>(new Person("Alice"));
Person w = weak.get(); // returns object if not yet GC'd
if (w == null) {
// GC already collected it
}
// WeakHashMap β real use case
// key GC'd automatically when no strong reference to key exists
WeakHashMap<Person, String> map = new WeakHashMap<>();
// βββββββββββββββββββββββββββββββββββββ
// 4. PHANTOM REFERENCE (Java 1.2)
// βββββββββββββββββββββββββββββββββββββ
// * get() always returns NULL β cannot access object
// * Used to know EXACTLY when object is removed from memory
// * Best for: cleanup actions, resource management (Cleaner API)
ReferenceQueue<Person> queue = new ReferenceQueue<>();
PhantomReference<Person> phantom = new PhantomReference<>(new Person("Alice"), queue);
phantom.get(); // always returns null β by design
// when object GC'd β phantom reference enqueued in ReferenceQueue
Reference<?> ref = queue.poll(); // poll to detect cleanup
if (ref != null) {
// object fully removed from memory β safe to do cleanup
} |
Memory pressure increases
β
βΌ
Strong Reference β β NEVER collected (unless null)
β
βΌ
Soft Reference β β
collected when LOW MEMORY
β
βΌ
Weak Reference β β
collected at NEXT GC cycle
β
βΌ
Phantom Reference β β
collected AFTER finalization
enqueued in ReferenceQueue
Strong β I will NEVER let go
Soft β Let go only if REALLY needed
Weak β Let go at NEXT GC
Phantom β Already gone β just notify me |
Summary Table
| Type | GC Collects When |
get() returns |
Use Case |
|---|---|---|---|
| Strong | Never (until null) | Object always | Default everywhere Normal variables everywhere |
| Soft | Low memory | Object or null | Image/data caching β SoftReference based cache |
| Weak | Next GC cycle | Object or null |
WeakHashMap, ThreadLocal internals`Preventing memory leaks |
| Phantom | After finalization | Always null
|
Cleanup, resource mgmtCleaner API (replaced finalize()) |
Object finalize() Method: The finalize() method is called by the garbage collector before an object is destroyed.
| Purpose | Important Notes |
|---|---|
|
Perform cleanup operations (close files, release resources, database connections)
Last chance to perform actions before object is garbage collected class Resource {
@Override
protected void finalize() throws Throwable {
try {
// Cleanup code
System.out.println("Cleaning up resources");
// Close connections, files, etc.
} finally {
super.finalize();
}
}
}
|
java// Modern approach - Try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt")) {
// Use resource
} // Automatically closed, no finalize needed
public class HikariDataSource extends HikariConfig implements DataSource, Closeable {
// Shutdown the DataSource and its associated pool.
@Override public void close() {
// ...
}
}
// @since 1.5
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
|
Factory Class: A Factory Class is a design pattern that provides a centralized way to create objects without exposing the creation logic to the client.