Deep dive into OOP, Collections, Multithreading, and Java 8+ features that every developer must master
π Table of Contents
- Object-Oriented Programming
- Strings Deep Dive
- Arrays Explained
- Interfaces & Abstract Classes
- Packages
- Exception Handling
- Collections Framework
- Multithreading
- Java 8+ Features
- Memory Management
π¨ Object-Oriented Programming
This is THE most important section for technical interviews
The Four Pillars
**Explain OOP principles with real-world examples AND show me code."
1. Encapsulation
Definition: Wrapping data and methods that operate on that data within a single unit (class).
Detailed Explanation:
// β Bad: No encapsulation
class BankAccountBad {
public double balance;
// Anyone can do this:
// account.balance = -1000000; // Invalid state!
}
// β
Good: Proper encapsulation
class BankAccount {
private double balance; // Hidden from outside
private String accountNumber;
// Controlled access through methods
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
} else {
throw new IllegalArgumentException("Amount must be positive");
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
} else {
throw new IllegalArgumentException("Invalid withdrawal amount");
}
}
}
Why Encapsulation?
- Data Hiding - Internal state is protected
- Validation - Can validate data before setting
- Flexibility - Can change internal implementation without affecting users
- Maintainability - Clear interface for interacting with class
Common Mistake: "Can we achieve 100% encapsulation in Java?"
Explanation: No! Reflection API can access private members. Also, if you return mutable objects from getters, caller can modify them.
class Employee {
private List<String> skills = new ArrayList<>();
// β Bad: Returns mutable reference
public List<String> getSkills() {
return skills;
}
// β
Good: Returns unmodifiable view
public List<String> getSkillsSafe() {
return Collections.unmodifiableList(skills);
}
// β
Better: Returns defensive copy
public List<String> getSkillsCopy() {
return new ArrayList<>(skills);
}
}
2. Inheritance
Definition: Mechanism where one class acquires properties of another class.
// Parent class (Superclass/Base class)
class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + " is eating");
}
public void sleep() {
System.out.println(name + " is sleeping");
}
}
// Child class (Subclass/Derived class)
class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // Call parent constructor
this.breed = breed;
}
// Dog-specific method
public void bark() {
System.out.println(name + " is barking!");
}
// Override parent method
@Override
public void eat() {
System.out.println(name + " is eating dog food");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
public void meow() {
System.out.println(name + " is meowing!");
}
}
public class InheritanceDemo {
public static void main(String[] args) {
Dog dog = new Dog("Buddy", 3, "Golden Retriever");
dog.eat(); // Dog's version
dog.sleep(); // Inherited from Animal
dog.bark(); // Dog-specific
Cat cat = new Cat("Whiskers", 2);
cat.eat(); // Animal's version
cat.meow(); // Cat-specific
}
}
Types of Inheritance in Java:
// 1. Single Inheritance
class A { }
class B extends A { }
// 2. Multilevel Inheritance
class A { }
class B extends A { }
class C extends B { } // C inherits from B, B inherits from A
// 3. Hierarchical Inheritance
class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
class Bird extends Animal { }
// β Multiple Inheritance NOT allowed with classes
// class C extends A, B { } // Compilation error!
// β
Multiple Inheritance allowed with interfaces
interface Flyable { }
interface Swimmable { }
class Duck implements Flyable, Swimmable { }
Common Question: "Why doesn't Java support multiple inheritance with classes?"
Explanation: Diamond Problem
/*
A
/ \
B C
\ /
D
If B and C both override a method from A,
which version does D inherit?
*/
// This is why it's not allowed:
class A {
void display() { System.out.println("A"); }
}
class B extends A {
void display() { System.out.println("B"); }
}
class C extends A {
void display() { System.out.println("C"); }
}
// β Not allowed
// class D extends B, C {
// // Which display() to inherit?
// }
3. Polymorphism
Definition: One interface, multiple implementations.
Two Types:
A. Compile-time Polymorphism (Method Overloading)
class Calculator {
// Same method name, different parameters
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
// β NOT overloading (return type doesn't matter)
// public double add(int a, int b) {
// return a + b;
// }
}
Rules for Method Overloading:
- Must have different parameter lists
- May have different return types
- May have different access modifiers
- May throw different exceptions
B. Runtime Polymorphism (Method Overriding)
class Shape {
public void draw() {
System.out.println("Drawing a shape");
}
public double area() {
return 0.0;
}
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing a circle");
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
private double length, width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
@Override
public double area() {
return length * width;
}
}
public class PolymorphismDemo {
public static void main(String[] args) {
// Polymorphic reference
Shape shape1 = new Circle(5.0);
Shape shape2 = new Rectangle(4.0, 6.0);
// Runtime decides which method to call
shape1.draw(); // "Drawing a circle"
shape2.draw(); // "Drawing a rectangle"
System.out.println("Circle area: " + shape1.area());
System.out.println("Rectangle area: " + shape2.area());
// Array of shapes (powerful!)
Shape[] shapes = {
new Circle(3.0),
new Rectangle(2.0, 5.0),
new Circle(4.5)
};
for (Shape shape : shapes) {
shape.draw();
System.out.println("Area: " + shape.area());
}
}
}
Rules for Method Overriding:
- Must have same method signature
- Return type must be same or covariant
- Access modifier must be same or less restrictive
- Cannot override
final,static, orprivatemethods - Exception thrown can be same, subclass, or none
Common Mistake:
class Parent {
public Number getValue() { // Returns Number
return 10;
}
}
class Child extends Parent {
@Override
public Integer getValue() { // β
Covariant return type
return 20;
}
}
Upcasting and Downcasting (VERY IMPORTANT!)
This is where many developers get confused. Let's break it down with examples.
Understanding Reference Type vs Object Type
class Animal {
String name = "Animal";
public void eat() {
System.out.println("Animal is eating");
}
public void sleep() {
System.out.println("Animal is sleeping");
}
}
class Dog extends Animal {
String name = "Dog"; // Variable hiding (not overriding!)
String breed = "Golden Retriever";
@Override
public void eat() {
System.out.println("Dog is eating");
}
public void bark() {
System.out.println("Dog is barking");
}
}
public class UpcastingDowncasting {
public static void main(String[] args) {
// Normal case: Reference type = Object type
Dog dog1 = new Dog();
dog1.eat(); // β
Dog is eating
dog1.bark(); // β
Dog is barking
dog1.sleep(); // β
Animal is sleeping (inherited)
System.out.println(dog1.breed); // β
Golden Retriever
// UPCASTING: Parent reference, Child object
Animal animal = new Dog(); // β
Implicit upcasting
/*
KEY CONCEPT:
- Reference type (Animal) determines WHAT you can call
- Object type (Dog) determines WHICH implementation runs
*/
animal.eat(); // β
Dog is eating (Dog's implementation)
animal.sleep(); // β
Animal is sleeping (inherited method)
// β CANNOT access Dog-specific methods
// animal.bark(); // Compilation Error!
// animal.breed; // Compilation Error!
// Variable access is based on REFERENCE TYPE
System.out.println(animal.name); // "Animal" (not "Dog"!)
// DOWNCASTING: Cast back to access Dog-specific members
if (animal instanceof Dog) {
Dog dog2 = (Dog) animal; // β
Explicit downcasting
dog2.bark(); // β
Now works!
System.out.println(dog2.breed); // β
Now works!
}
}
}
Output:
Dog is eating
Dog is barking
Animal is sleeping
Golden Retriever
Dog is eating
Animal is sleeping
Animal
Dog is barking
Golden Retriever
Why Does This Happen?
/*
MEMORY LAYOUT:
HEAP:
βββββββββββββββββββββββββββ
β Dog Object β
βββββββββββββββββββββββββββ€
β Animal part: β
β name = "Animal" β
β eat() β overridden β
β sleep() β
βββββββββββββββββββββββββββ€
β Dog part: β
β name = "Dog" β
β breed = "G.Retriever" β
β bark() β
βββββββββββββββββββββββββββ
β
β
animal reference (Animal type)
β
Can only see Animal part through this reference!
*/
Common Mistakes with Upcasting
class Parent {
int x = 10;
public void display() {
System.out.println("Parent display");
}
}
class Child extends Parent {
int x = 20; // Variable hiding
int y = 30; // Child-specific variable
@Override
public void display() {
System.out.println("Child display");
}
public void childMethod() {
System.out.println("Child method");
}
}
public class CommonMistakes {
public static void main(String[] args) {
Parent p = new Child(); // Upcasting
// 1. Method calls - Uses OBJECT type (Child)
p.display(); // β
"Child display" (polymorphism works!)
// 2. Variable access - Uses REFERENCE type (Parent)
System.out.println(p.x); // β
10 (NOT 20!)
// 3. Child-specific members - NOT accessible
// System.out.println(p.y); // β Compilation Error
// p.childMethod(); // β Compilation Error
// 4. Need downcasting to access child members
Child c = (Child) p;
System.out.println(c.x); // β
20
System.out.println(c.y); // β
30
c.childMethod(); // β
"Child method"
}
}
Output:
Child display
10
20
30
Child method
Downcasting Dangers
public class DowncastingDanger {
public static void main(String[] args) {
Animal animal1 = new Dog(); // Dog object
Animal animal2 = new Animal(); // Animal object
// Safe downcasting
if (animal1 instanceof Dog) {
Dog dog = (Dog) animal1; // β
Safe
dog.bark();
}
// Unsafe downcasting
// Dog dog2 = (Dog) animal2; // β ClassCastException at runtime!
// ALWAYS check with instanceof before downcasting
if (animal2 instanceof Dog) {
Dog dog2 = (Dog) animal2;
dog2.bark();
} else {
System.out.println("animal2 is not a Dog!");
}
}
}
Practical Example: Why Upcasting Matters
class Shape {
public void draw() {
System.out.println("Drawing shape");
}
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing circle with radius: " + radius);
}
public double getRadius() {
return radius;
}
}
class Rectangle extends Shape {
private double length, width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public void draw() {
System.out.println("Drawing rectangle: " + length + "x" + width);
}
}
public class WhyUpcastingMatters {
// β
Good: Can accept any Shape
public static void drawShape(Shape shape) {
shape.draw(); // Polymorphism!
}
// β Bad: Needs separate method for each shape
// public static void drawCircle(Circle circle) { }
// public static void drawRectangle(Rectangle rect) { }
public static void main(String[] args) {
Shape[] shapes = {
new Circle(5.0),
new Rectangle(4.0, 6.0),
new Circle(3.0)
};
// Single loop handles all shapes!
for (Shape shape : shapes) {
drawShape(shape); // Upcasting happens automatically
// If you need specific behavior, downcast
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
System.out.println(" Radius: " + circle.getRadius());
}
}
}
}
Key Takeaways on References
1. Method Calls (Polymorphism)
Parent p = new Child();
p.method(); // Calls Child's implementation β
2. Variable Access (No Polymorphism)
Parent p = new Child();
System.out.println(p.variable); // Parent's variable β οΈ
3. Member Access (Reference Type Decides)
Parent p = new Child();
// p.childMethod(); // β Can't access
// p.childVariable; // β Can't access
Child c = (Child) p; // Downcast first
c.childMethod(); // β
Now accessible
4. The Golden Rule
Reference Type β What you CAN call (compile-time)
Object Type β Which implementation RUNS (runtime)
4. Abstraction
Definition: Hiding implementation details and showing only functionality.
// Abstract class
abstract class Vehicle {
protected String brand;
public Vehicle(String brand) {
this.brand = brand;
}
// Abstract method (no implementation)
public abstract void start();
public abstract void stop();
// Concrete method (with implementation)
public void displayBrand() {
System.out.println("Brand: " + brand);
}
}
class Car extends Vehicle {
public Car(String brand) {
super(brand);
}
@Override
public void start() {
System.out.println(brand + " car starting with key");
}
@Override
public void stop() {
System.out.println(brand + " car stopping with brake");
}
}
class Motorcycle extends Vehicle {
public Motorcycle(String brand) {
super(brand);
}
@Override
public void start() {
System.out.println(brand + " motorcycle kick-starting");
}
@Override
public void stop() {
System.out.println(brand + " motorcycle stopping");
}
}
public class AbstractionDemo {
public static void main(String[] args) {
// β Cannot instantiate abstract class
// Vehicle v = new Vehicle("Generic");
Vehicle car = new Car("Toyota");
Vehicle bike = new Motorcycle("Harley");
car.start();
bike.start();
}
}
The this Keyword
class Employee {
private String name;
private int age;
// 1. Differentiate between instance variable and parameter
public Employee(String name, int age) {
this.name = name; // this.name refers to instance variable
this.age = age;
}
// 2. Call another constructor
public Employee(String name) {
this(name, 0); // Calls Employee(String, int)
}
// 3. Pass current object as parameter
public void display() {
printEmployee(this);
}
private void printEmployee(Employee emp) {
System.out.println(emp.name + ", " + emp.age);
}
// 4. Return current object
public Employee setName(String name) {
this.name = name;
return this; // Method chaining
}
public Employee setAge(int age) {
this.age = age;
return this;
}
public static void main(String[] args) {
// Method chaining using 'this'
Employee emp = new Employee("John")
.setName("John Doe")
.setAge(30);
}
}
The super Keyword
class Parent {
protected int value = 10;
public Parent() {
System.out.println("Parent constructor");
}
public Parent(int value) {
this.value = value;
System.out.println("Parent parameterized constructor");
}
public void display() {
System.out.println("Parent display");
}
}
class Child extends Parent {
private int value = 20;
public Child() {
super(); // 1. Call parent constructor
System.out.println("Child constructor");
}
public Child(int parentValue, int childValue) {
super(parentValue); // Must be first statement
this.value = childValue;
}
@Override
public void display() {
super.display(); // 2. Call parent method
System.out.println("Child display");
}
public void showValues() {
System.out.println("Child value: " + this.value); // 20
System.out.println("Parent value: " + super.value); // 3. Access parent variable
}
}
Access Modifiers
// File: AccessModifiers.java
package com.example;
public class AccessModifiers {
public int publicVar = 1; // Accessible everywhere
protected int protectedVar = 2; // Same package + subclasses
int defaultVar = 3; // Same package only (package-private)
private int privateVar = 4; // Same class only
public void display() {
// All accessible within same class
System.out.println(publicVar);
System.out.println(protectedVar);
System.out.println(defaultVar);
System.out.println(privateVar);
}
}
// File: SamePackageClass.java
package com.example;
class SamePackageClass {
void test() {
AccessModifiers obj = new AccessModifiers();
System.out.println(obj.publicVar); // β
System.out.println(obj.protectedVar); // β
System.out.println(obj.defaultVar); // β
// System.out.println(obj.privateVar); // β Compilation error
}
}
// File: DifferentPackageClass.java
package com.other;
import com.example.AccessModifiers;
class DifferentPackageClass {
void test() {
AccessModifiers obj = new AccessModifiers();
System.out.println(obj.publicVar); // β
// System.out.println(obj.protectedVar); // β
// System.out.println(obj.defaultVar); // β
// System.out.println(obj.privateVar); // β
}
}
// File: SubclassInDifferentPackage.java
package com.other;
import com.example.AccessModifiers;
class SubclassInDifferentPackage extends AccessModifiers {
void test() {
// β
Can access through inheritance (without object)
System.out.println(publicVar); // β
System.out.println(protectedVar); // β
(through inheritance)
// System.out.println(defaultVar); // β
// System.out.println(privateVar); // β
// β οΈ CRITICAL: Cannot access protected through object reference!
SubclassInDifferentPackage obj = new SubclassInDifferentPackage();
// System.out.println(obj.protectedVar); // β Compilation error!
// This is also NOT allowed
AccessModifiers parent = new AccessModifiers();
// System.out.println(parent.protectedVar); // β Compilation error!
}
}
Protected Access - The Tricky Part (MUST UNDERSTAND!)
This is one of the most misunderstood concepts in Java. Let me explain with a detailed example:
// File: Parent.java
package com.parent;
public class Parent {
protected int protectedVar = 100;
protected void protectedMethod() {
System.out.println("Protected method");
}
}
// File: Child.java
package com.child;
import com.parent.Parent;
public class Child extends Parent {
public void demonstrateProtectedAccess() {
// β
WAY 1: Access directly (through inheritance)
System.out.println(protectedVar); // Works!
protectedMethod(); // Works!
// β
WAY 2: Access through 'this'
System.out.println(this.protectedVar); // Works!
this.protectedMethod(); // Works!
// β
WAY 3: Access through 'super'
System.out.println(super.protectedVar); // Works!
super.protectedMethod(); // Works!
// β WAY 4: Access through object reference - DOES NOT WORK!
Child childObj = new Child();
// System.out.println(childObj.protectedVar); // β Compilation Error!
// childObj.protectedMethod(); // β Compilation Error!
// β WAY 5: Access through parent object - DOES NOT WORK!
Parent parentObj = new Parent();
// System.out.println(parentObj.protectedVar); // β Compilation Error!
// parentObj.protectedMethod(); // β Compilation Error!
}
}
Why This Behavior?
/*
RULE FOR PROTECTED ACCESS IN DIFFERENT PACKAGE:
Protected members are accessible in subclass ONLY through:
1. Direct access (inherited members)
2. 'this' reference
3. 'super' reference
NOT accessible through:
1. Object references (even of same subclass)
2. Parent class object references
REASON: Protected is meant for inheritance, not for general access
through objects. It's about "is-a" relationship, not "has-a".
*/
Complete Example Showing All Cases:
// File: Vehicle.java
package com.vehicles;
public class Vehicle {
protected String brand = "Generic";
protected int speed = 0;
protected void accelerate() {
speed += 10;
System.out.println("Accelerating to " + speed);
}
}
// File: Car.java
package com.cars;
import com.vehicles.Vehicle;
public class Car extends Vehicle {
public void testProtectedAccess() {
System.out.println("=== Testing Protected Access ===");
// β
CASE 1: Direct access (through inheritance)
System.out.println("Brand: " + brand); // Works!
System.out.println("Speed: " + speed); // Works!
accelerate(); // Works!
// β
CASE 2: Through 'this'
System.out.println("Brand via this: " + this.brand); // Works!
this.accelerate(); // Works!
// β
CASE 3: Through 'super'
System.out.println("Brand via super: " + super.brand); // Works!
super.accelerate(); // Works!
// β CASE 4: Through object of same class
Car anotherCar = new Car();
// System.out.println(anotherCar.brand); // β ERROR!
// anotherCar.accelerate(); // β ERROR!
// β CASE 5: Through parent class object
Vehicle vehicle = new Vehicle();
// System.out.println(vehicle.brand); // β ERROR!
// vehicle.accelerate(); // β ERROR!
// β CASE 6: Through upcasted reference
Vehicle upcastedCar = new Car();
// System.out.println(upcastedCar.brand); // β ERROR!
// upcastedCar.accelerate(); // β ERROR!
}
public static void main(String[] args) {
Car car = new Car();
car.testProtectedAccess();
}
}
Memory Layout Explanation:
When you have: Car extends Vehicle
MEMORY:
βββββββββββββββββββββββββββ
β Car Object β
βββββββββββββββββββββββββββ€
β Vehicle part (inherited)β
β brand = "Generic" β β Protected variable
β speed = 0 β β Protected variable
β accelerate() β β Protected method
βββββββββββββββββββββββββββ€
β Car part β
β (Car-specific stuff) β
βββββββββββββββββββββββββββ
Access Rules in Different Package:
1. Through inheritance (brand, this.brand, super.brand) β
- You're accessing YOUR OWN inherited copy
2. Through object (obj.brand) β
- You're trying to access SOMEONE ELSE'S copy
- Java says: "Protected is for YOUR inheritance, not others"
Practical Example:
// File: BankAccount.java
package com.bank;
public class BankAccount {
protected double balance = 1000.0;
protected void withdraw(double amount) {
if (amount <= balance) {
balance -= amount;
}
}
}
// File: SavingsAccount.java
package com.accounts;
import com.bank.BankAccount;
public class SavingsAccount extends BankAccount {
public void myTransactions() {
// β
Can access my own inherited balance
System.out.println("My balance: " + balance);
withdraw(100); // β
Works
// β Cannot access other account's balance through object
SavingsAccount otherAccount = new SavingsAccount();
// System.out.println(otherAccount.balance); // β Error!
// Why? To protect encapsulation! You shouldn't access
// protected members of other objects directly.
}
}
Key Takeaway:
/*
GOLDEN RULE FOR PROTECTED ACCESS:
In a DIFFERENT package, protected members are accessible ONLY when:
1. You're in a subclass
2. You access through inheritance (no object reference)
Think of it as: "Protected is for ME (through inheritance),
not for THEM (through objects)"
Same Package: No restrictions (works like public)
Different Package: Only through inheritance, not through objects
*/
Access Modifier Table:
| Modifier | Same Class | Same Package | Subclass | Universe |
|---|---|---|---|---|
public |
β | β | β | β |
protected |
β | β | β | β |
| default | β | β | β | β |
private |
β | β | β | β |
Note: For protected in "Subclass" column - accessible only through inheritance (this/super), NOT through object references!
π Strings Deep Dive
**Why are Strings immutable? What's the String pool?"
String Immutability
public class StringImmutability {
public static void main(String[] args) {
String s1 = "Hello";
s1.concat(" World"); // Creates new String, doesn't modify s1
System.out.println(s1); // "Hello" (unchanged)
s1 = s1.concat(" World"); // Now s1 points to new String
System.out.println(s1); // "Hello World"
// Memory visualization:
/*
HEAP:
"Hello" object β s1 initially points here
"Hello World" object β s1 points here after concat
The original "Hello" remains unchanged!
*/
}
}
Why Immutable?
- String Pool Optimization - Reuse same String object
- Security - Strings used in sensitive operations (passwords, URLs, file paths)
- Thread Safety - No synchronization needed
- Hashcode Caching - Hashcode calculated once and cached
String Pool (Intern Pool)
public class StringPool {
public static void main(String[] args) {
// Literal - Goes to String pool
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true (same reference)
// new keyword - Goes to heap
String s3 = new String("Hello");
System.out.println(s1 == s3); // false (different references)
System.out.println(s1.equals(s3)); // true (same content)
// intern() - Move to pool
String s4 = s3.intern();
System.out.println(s1 == s4); // true (now same reference)
/*
MEMORY LAYOUT:
STRING POOL (in Heap):
βββββββββββββββ
β "Hello" β β s1, s2, s4 point here
βββββββββββββββ
REGULAR HEAP:
βββββββββββββββ
β new String β β s3 points here
β "Hello" β
βββββββββββββββ
*/
}
}
String vs StringBuilder vs StringBuffer
public class StringComparison {
public static void main(String[] args) {
// 1. String (Immutable, Thread-safe by default)
String str = "Hello";
str += " World"; // Creates new object each time
// Performance: O(nΒ²) for n concatenations
// 2. StringBuilder (Mutable, NOT thread-safe)
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // Modifies same object
// Performance: O(n) for n concatenations
// 3. StringBuffer (Mutable, Thread-safe)
StringBuffer sbf = new StringBuffer("Hello");
sbf.append(" World"); // Synchronized methods
// Performance: O(n) but slower than StringBuilder due to synchronization
// Performance test
long startTime, endTime;
// String concatenation (SLOW)
startTime = System.nanoTime();
String s = "";
for (int i = 0; i < 10000; i++) {
s += "a"; // Creates 10000 new objects!
}
endTime = System.nanoTime();
System.out.println("String: " + (endTime - startTime) / 1000000 + " ms");
// StringBuilder (FAST)
startTime = System.nanoTime();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
builder.append("a");
}
endTime = System.nanoTime();
System.out.println("StringBuilder: " + (endTime - startTime) / 1000000 + " ms");
}
}
When to Use What?
| Scenario | Use |
|---|---|
| Fixed string, no modifications | String |
| Single-threaded string building | StringBuilder |
| Multi-threaded string building | StringBuffer |
| Simple concatenations (<< 100) |
String (compiler optimizes) |
Common String Methods
public class StringMethods {
public static void main(String[] args) {
String str = " Hello World ";
// Length
System.out.println(str.length()); // 15
// Character access
System.out.println(str.charAt(2)); // ' '
// Substring
System.out.println(str.substring(2, 7)); // "Hello"
// Trim
System.out.println(str.trim()); // "Hello World"
// Case conversion
System.out.println(str.toUpperCase());
System.out.println(str.toLowerCase());
// Replace
System.out.println(str.replace("World", "Java"));
// Split
String[] words = str.trim().split(" ");
for (String word : words) {
System.out.println(word);
}
// Contains, startsWith, endsWith
System.out.println(str.contains("World")); // true
System.out.println(str.trim().startsWith("Hello")); // true
System.out.println(str.trim().endsWith("World")); // true
// equals vs equalsIgnoreCase
String s1 = "hello";
String s2 = "HELLO";
System.out.println(s1.equals(s2)); // false
System.out.println(s1.equalsIgnoreCase(s2)); // true
// isEmpty vs isBlank (Java 11+)
System.out.println("".isEmpty()); // true
System.out.println(" ".isEmpty()); // false
System.out.println(" ".isBlank()); // true (Java 11+)
// Format
String formatted = String.format("Name: %s, Age: %d", "John", 30);
System.out.println(formatted);
}
}
equals() vs ==
public class StringEquality {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "Hello";
String s3 = new String("Hello");
// == compares references
System.out.println(s1 == s2); // true (same object in pool)
System.out.println(s1 == s3); // false (different objects)
// equals() compares content
System.out.println(s1.equals(s2)); // true
System.out.println(s1.equals(s3)); // true
// Question: trap
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2); // true (cached)
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // false (not cached)
// Integer cache: -128 to 127
}
}
π’ Arrays Explained
Array Basics
public class ArrayBasics {
public static void main(String[] args) {
// Declaration and initialization
int[] arr1 = new int[5]; // Default values: 0
int[] arr2 = {1, 2, 3, 4, 5};
int[] arr3 = new int[]{1, 2, 3, 4, 5};
// Access
System.out.println(arr2[0]); // 1
arr2[0] = 10; // Modify
// Length
System.out.println(arr2.length); // 5 (property, not method)
// Enhanced for loop
for (int num : arr2) {
System.out.print(num + " ");
}
System.out.println();
// Anonymous array
printArray(new int[]{1, 2, 3});
}
static void printArray(int[] arr) {
for (int num : arr) {
System.out.print(num + " ");
}
System.out.println();
}
}
2D Arrays
public class TwoDArray {
public static void main(String[] args) {
// Rectangular array
int[][] matrix = new int[3][3];
int[][] matrix2 = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// Access
System.out.println(matrix2[1][1]); // 5
// Jagged array (different column sizes)
int[][] jagged = new int[3][];
jagged[0] = new int[2]; // 2 columns
jagged[1] = new int[4]; // 4 columns
jagged[2] = new int[3]; // 3 columns
// Traverse
for (int i = 0; i < matrix2.length; i++) {
for (int j = 0; j < matrix2[i].length; j++) {
System.out.print(matrix2[i][j] + " ");
}
System.out.println();
}
// Enhanced for loop
for (int[] row : matrix2) {
for (int num : row) {
System.out.print(num + " ");
}
System.out.println();
}
}
}
Arrays Utility Class
import java.util.Arrays;
public class ArraysUtility {
public static void main(String[] args) {
int[] arr = {5, 2, 8, 1, 9};
// Sort
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 5, 8, 9]
// Binary search (array must be sorted)
int index = Arrays.binarySearch(arr, 5);
System.out.println("Index of 5: " + index);
// Fill
int[] arr2 = new int[5];
Arrays.fill(arr2, 10);
System.out.println(Arrays.toString(arr2)); // [10, 10, 10, 10, 10]
// Copy
int[] copy = Arrays.copyOf(arr, arr.length);
int[] copyRange = Arrays.copyOfRange(arr, 1, 4);
// Equals
System.out.println(Arrays.equals(arr, copy)); // true
// Compare
System.out.println(Arrays.compare(arr, copy)); // 0 (equal)
}
}
Array vs ArrayList
import java.util.ArrayList;
public class ArrayVsArrayList {
public static void main(String[] args) {
// Array: Fixed size
int[] arr = new int[5];
// arr[5] = 10; // ArrayIndexOutOfBoundsException
// ArrayList: Dynamic size
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
System.out.println(list.size()); // 3
// Performance
// Array: O(1) access, fixed size
// ArrayList: O(1) access, O(n) resize (amortized O(1) add)
}
}
π Interfaces & Abstract Classes
Interface
// Interface: Contract
interface Drawable {
// All methods are public abstract by default
void draw();
void resize(int width, int height);
// Default method (Java 8+)
default void display() {
System.out.println("Displaying drawable");
}
// Static method (Java 8+)
static void info() {
System.out.println("This is Drawable interface");
}
// Constants (public static final by default)
int MAX_SIZE = 1000;
}
class Circle implements Drawable {
@Override
public void draw() {
System.out.println("Drawing circle");
}
@Override
public void resize(int width, int height) {
System.out.println("Resizing circle");
}
// Can override default method
@Override
public void display() {
System.out.println("Displaying circle");
}
}
// Multiple interfaces
interface Printable {
void print();
}
class Document implements Drawable, Printable {
@Override
public void draw() {
System.out.println("Drawing document");
}
@Override
public void resize(int width, int height) {
System.out.println("Resizing document");
}
@Override
public void print() {
System.out.println("Printing document");
}
}
Abstract Class vs Interface
// Abstract class: Partial implementation
abstract class Animal {
protected String name;
// Constructor allowed
public Animal(String name) {
this.name = name;
}
// Concrete method
public void eat() {
System.out.println(name + " is eating");
}
// Abstract method
public abstract void makeSound();
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " barks");
}
}
// Interface: Full abstraction
interface Swimmable {
void swim();
}
class Duck extends Animal implements Swimmable {
public Duck(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " quacks");
}
@Override
public void swim() {
System.out.println(name + " is swimming");
}
}
Abstract Class vs Interface:
| Feature | Abstract Class | Interface |
|---|---|---|
| Methods | Both abstract & concrete | Abstract, default, static (Java 8+) |
| Variables | Any type | public static final only |
| Constructor | Yes | No |
| Multiple Inheritance | No | Yes |
| Access Modifiers | All | public only |
| When to use | "is-a" relationship | "can-do" capability |
Common Question: "When to use abstract class vs interface?"
Explanation:
- Abstract Class: Common base implementation, "is-a" relationship (Dog is-a Animal)
- Interface: Capabilities, "can-do" relationship (Duck can-swim, can-fly)
(Continuing with remaining sections...)
π¦ Packages
Package Basics
// File: com/example/myapp/Calculator.java
package com.example.myapp;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
// File: com/example/myapp/Main.java
package com.example.myapp;
// Same package - no import needed
public class Main {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(5, 3));
}
}
// File: com/example/other/Test.java
package com.example.other;
import com.example.myapp.Calculator; // Different package - import needed
public class Test {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(10, 20));
}
}
Built-in Packages
// java.lang - Automatically imported
// String, System, Math, Integer, etc.
// java.util - Collections, Date, etc.
import java.util.ArrayList;
import java.util.HashMap;
// java.io - Input/Output
import java.io.File;
import java.io.IOException;
// java.sql - Database
import java.sql.Connection;
import java.sql.DriverManager;
// Wildcard import (imports all classes)
import java.util.*; // Not recommended for production
β οΈ Exception Handling
Exception Hierarchy
Throwable
/ \
Error Exception
/ / \
OutOfMemoryError IOException RuntimeException
StackOverflowError / | \
NullPointer ArrayIndexOutOfBounds ArithmeticException
Checked vs Unchecked Exceptions
import java.io.*;
public class ExceptionTypes {
public static void main(String[] args) {
// 1. Checked Exception (Compile-time)
// Must handle with try-catch or declare with throws
try {
FileReader fr = new FileReader("file.txt");
} catch (FileNotFoundException e) {
System.out.println("File not found");
}
// 2. Unchecked Exception (Runtime)
// Optional to handle
int[] arr = {1, 2, 3};
// System.out.println(arr[5]); // ArrayIndexOutOfBoundsException
// 3. Error
// Don't catch errors (JVM issues)
// recursiveMethod(); // StackOverflowError
}
}
try-catch-finally
public class ExceptionHandling {
public static void main(String[] args) {
// Basic try-catch
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero");
}
// Multiple catch blocks
try {
String str = null;
System.out.println(str.length());
int[] arr = new int[5];
System.out.println(arr[10]);
} catch (NullPointerException e) {
System.out.println("Null pointer");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index out of bounds");
} catch (Exception e) { // Catch-all (must be last)
System.out.println("Some exception occurred");
}
// finally: Always executes
try {
System.out.println("Try block");
return; // Even with return, finally executes
} catch (Exception e) {
System.out.println("Catch block");
} finally {
System.out.println("Finally block"); // This WILL execute
}
// try-with-resources (Java 7+)
try (FileReader fr = new FileReader("file.txt")) {
// Resource automatically closed
} catch (IOException e) {
e.printStackTrace();
}
// No need for finally to close resource
}
}
throw vs throws
public class ThrowThrows {
// throws: Declares that method might throw exception
public void method1() throws IOException {
throw new IOException("File error"); // throw: Actually throws exception
}
// Custom exception
public void validateAge(int age) throws InvalidAgeException {
if (age < 18) {
throw new InvalidAgeException("Age must be 18 or above");
}
}
}
// Custom exception class
class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
π Collections Framework
Collection Hierarchy
Collection (interface)
/ | \
List Set Queue
/ \ / \
ArrayList LinkedList HashSet TreeSet
List Interface
import java.util.*;
public class ListDemo {
public static void main(String[] args) {
// ArrayList: Dynamic array
List<String> arrayList = new ArrayList<>();
arrayList.add("Apple");
arrayList.add("Banana");
arrayList.add("Apple"); // Duplicates allowed
arrayList.add(null); // Null allowed
System.out.println(arrayList.get(0)); // Random access O(1)
arrayList.remove(0); // O(n)
// LinkedList: Doubly linked list
List<String> linkedList = new LinkedList<>();
linkedList.add("First");
linkedList.add("Second");
// When to use what?
// ArrayList: Fast random access, slow insertion/deletion
// LinkedList: Slow random access, fast insertion/deletion
}
}
Set Interface
import java.util.*;
public class SetDemo {
public static void main(String[] args) {
// HashSet: No order, no duplicates
Set<String> hashSet = new HashSet<>();
hashSet.add("Apple");
hashSet.add("Banana");
hashSet.add("Apple"); // Duplicate ignored
System.out.println(hashSet); // [Banana, Apple] (unordered)
// LinkedHashSet: Insertion order maintained
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Apple");
linkedHashSet.add("Banana");
linkedHashSet.add("Cherry");
System.out.println(linkedHashSet); // [Apple, Banana, Cherry]
// TreeSet: Sorted order
Set<Integer> treeSet = new TreeSet<>();
treeSet.add(5);
treeSet.add(1);
treeSet.add(3);
System.out.println(treeSet); // [1, 3, 5] (sorted)
}
}
Map Interface
import java.util.*;
public class MapDemo {
public static void main(String[] args) {
// HashMap: Key-value pairs, no order
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("Apple", 100);
hashMap.put("Banana", 150);
hashMap.put("Apple", 120); // Overwrites previous value
System.out.println(hashMap.get("Apple")); // 120
System.out.println(hashMap.containsKey("Banana")); // true
// Iteration
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// TreeMap: Sorted by keys
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("Zebra", 1);
treeMap.put("Apple", 2);
treeMap.put("Mango", 3);
System.out.println(treeMap); // {Apple=2, Mango=3, Zebra=1}
}
}
Comparable vs Comparator
import java.util.*;
// Comparable: Natural ordering (inside class)
class Student implements Comparable<Student> {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student other) {
return this.age - other.age; // Sort by age
}
@Override
public String toString() {
return name + "(" + age + ")";
}
}
// Comparator: Custom ordering (outside class)
class NameComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
return s1.name.compareTo(s2.name); // Sort by name
}
}
public class ComparableVsComparator {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("John", 22));
students.add(new Student("Alice", 20));
students.add(new Student("Bob", 21));
// Natural ordering (Comparable)
Collections.sort(students);
System.out.println(students); // Sort by age
// Custom ordering (Comparator)
Collections.sort(students, new NameComparator());
System.out.println(students); // Sort by name
// Lambda (Java 8+)
Collections.sort(students, (s1, s2) -> s1.age - s2.age);
}
}
β±οΈ Multithreading
Creating Threads
// Method 1: Extend Thread class
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
// Method 2: Implement Runnable interface (Preferred)
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
public class ThreadCreation {
public static void main(String[] args) {
// Method 1
MyThread t1 = new MyThread();
t1.start();
// Method 2
Thread t2 = new Thread(new MyRunnable());
t2.start();
// Lambda (Java 8+)
Thread t3 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
});
t3.start();
}
}
Synchronization
class Counter {
private int count = 0;
// Synchronized method
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizationDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// Without synchronization: Race condition
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(); // Wait for t1 to finish
t2.join(); // Wait for t2 to finish
System.out.println("Count: " + counter.getCount()); // 2000 (with sync)
}
}
π Java 8+ Features
Lambda Expressions
import java.util.*;
public class LambdaDemo {
public static void main(String[] args) {
// Traditional way
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Before Java 8
for (Integer num : numbers) {
System.out.println(num);
}
// Java 8: Lambda
numbers.forEach(num -> System.out.println(num));
// Method reference
numbers.forEach(System.out::println);
}
}
Stream API
import java.util.*;
import java.util.stream.*;
public class StreamDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Filter even numbers and square them
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(result); // [4, 16, 36, 64, 100]
// Sum
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum);
}
}
π§ Memory Management
Stack vs Heap
public class MemoryManagement {
public static void main(String[] args) {
int x = 10; // Stack
String str = new String("Hello"); // Reference in stack, object in heap
/*
STACK:
main() frame
ββ x: 10
ββ str: reference β "Hello" object in heap
HEAP:
String object: "Hello"
*/
}
}
Garbage Collection
public class GarbageCollectionDemo {
public static void main(String[] args) {
String s1 = new String("Hello");
String s2 = new String("World");
s1 = null; // Object eligible for GC
s2 = null; // Object eligible for GC
// Request GC (not guaranteed)
System.gc();
// finalize() called before GC (deprecated in Java 9+)
}
}
π― Final Question: Tips
Most Important Concepts to Master:
- OOP principles with examples
- String immutability and pool
- Collections: HashMap internals
- Exception handling hierarchy
- Multithreading and synchronization
- Java 8 streams and lambda
- Memory management
How to Learn Effectively:
- Start with simple explanation
- Connect to real-world examples
- Write and run code examples
- Understand trade-offs and when to use what
- Practice explaining concepts out loud
π¨βπ» Author
Rajat
- GitHub: @rajat12826
That's Part 2! Master these concepts and you're ready for any Java interview! π
Made with β€οΈ for aspiring developers

Top comments (0)