Back to Blog
October 6, 2026•By Pradyumn Chaudhary

Object-Oriented Programming (OOP) in Java

Object-Oriented Programming (OOP) in Java

What is OOP and Why Do We Need It?

Object-Oriented Programming (OOP) is a programming paradigm centered around objects rather than actions or pure logic.

In OOP, you can represent any real-world entity—like a User, a BankAccount, or a Car—as an object in your code, bundling its data (attributes) and behavior (methods) together.


Procedural vs. Object-Oriented Approach

In traditional procedural programming languages (like C or BASIC), execution follows a step-by-step sequence of instructions, relying heavily on functions, conditions, and loops:

c
1// Procedural Approach (C Example)
2#include <stdio.h>
3
4struct Car {
5 char brand[20];
6 int speed;
7};
8
9void drive(struct Car c) {
10 printf("%s is driving at %d km/h\n", c.brand, c.speed);
11}
12
13int main() {
14 struct Car myCar = {"Toyota", 80};
15 drive(myCar);
16 return 0;
17}

Why and When We Shifted to OOP

As software grew more complex, maintaining global variables and detached functions became difficult. Developers realized that software ultimately models the real world—where entities have state, behavior, and clear boundaries.

OOP allows us to model these entities directly in code, making applications easier to scale, organize, and maintain:

java
1// Object-Oriented Approach (Java Example)
2class Car {
3 private String brand;
4 private int speed;
5
6 public Car(String brand, int speed) {
7 this.brand = brand;
8 this.speed = speed;
9 }
10
11 public void drive() {
12 System.out.println(brand + " is driving at " + speed + " km/h");
13 }
14}
15
16public class Main {
17 public static void main(String[] args) {
18 Car myCar = new Car("Toyota", 80);
19 myCar.drive();
20 }
21}

The Two Core Building Blocks: Class & Object

Think of OOP like building real things. Everything in OOP revolves around just two main concepts: Class and Object.

  • Class: The design, template, or blueprint. (It doesn't take space in physical memory until created).

  • Object: The actual physical thing created using that blueprint. (It actually exists and takes up memory).


Understanding with a Simple Example

Imagine you want to manufacture a Smartphone.

  1. First, engineers design a blueprint on paper. They decide:

    • What properties it will have: brand, color, ram

    • What actions it can do: makeCall(), takePhoto()This blueprint on paper is your Class*.*

  2. Next, the factory uses that blueprint to make actual phones (like your phone, your friend's phone, etc.). These physical phones in your hand are Objects*.*


1. Class (The Blueprint)

A class defines what data (attributes) an object will store and what actions (methods) it can perform.

java
1// Blueprint: Class definition
2class Smartphone {
3 // State / Data (Attributes)
4 String brand;
5 String color;
6 int ramInGB;
7
8 // Behavior / Action (Methods)
9 void makeCall(String phoneNumber) {
10 System.out.println("Calling " + phoneNumber + "...");
11 }
12}

Note: Class names in Java should always follow TitleCase (also called PascalCase), where every word starts with a capital letter (e.g., Smartphone, BankAccount). While Java won't throw an error if you don't follow this, it is standard industry convention and makes your code clean and readable.

2. Object (The Real Deal)

An object is an actual instance created from the class. You create it using the new keyword in Java. Once created, each object gets its own copy of the variables defined in the class and can perform the actions defined in it.

java
1public class Main {
2 public static void main(String[] args) {
3 // Creating an Object from the Smartphone class
4 Smartphone myPhone = new Smartphone();
5
6 // Setting data for THIS specific object
7 myPhone.brand = "Samsung";
8 myPhone.color = "Black";
9 myPhone.ramInGB = 8;
10
11 // Calling a method on this object
12 myPhone.makeCall("9876543210");
13
14 // Creating ANOTHER object from the SAME blueprint
15 Smartphone friendPhone = new Smartphone();
16 friendPhone.brand = "Apple";
17 friendPhone.color = "White";
18 friendPhone.ramInGB = 6;
19 }
20}

Reference Variables & Heap Memory

When you write Smartphone myPhone = new Smartphone();:

  1. new Smartphone(): Allocates memory for the object in the Heap at runtime. (Dynamic Memory Allocation)

  2. myPhone: Is a Reference Variable stored on the Stack. It holds the memory address pointing to the actual object sitting in the Heap.

plaintext
1STACK MEMORY HEAP MEMORY
2-------------- -----------------------
3| | | SmartPhone Object: |
4| myPhone---|------------|-> brand: "Samsung" |
5| | | color: "Black" |
6| | | ramInGb: 8 |
7-------------- -----------------------

Instance Variables vs. Local Variables

  • Instance Variables (Class Attributes): Belong to the object in the Heap. Java automatically initializes them with default values if you don't assign them.

  • Local Variables: Created inside methods/blocks on the Stack. Java does NOT give them default values. You must initialize them before using them, or the compiler throws an error.

Variable TypeWhere it livesDefault Value?Example Defaults
Instance VariableHeap (inside object)Yesint $\rightarrow$ 0, double $\rightarrow$ 0.0, boolean $\rightarrow$ false, String / Objects $\rightarrow$ null
Local VariableStack (inside method)No (Must assign before use)N/A (Compile error if read unassigned)

Quick Code Example

java
1class Smartphone {
2 // Instance variables (Get automatic default values)
3 int ramInGB; // Default: 0
4 String brand; // Default: null
5 boolean is5G; // Default: false
6
7 void checkStorage() {
8 // Local variable
9 int appCount;
10
11 // System.out.println(appCount);
12 // ❌ COMPILE ERROR! Local variable 'appCount' must be initialized before use.
13
14 appCount = 5;
15 System.out.println(appCount); // ✅ Works fine now
16 }
17}

Constructors in Java

A Constructor is a special method used to initialize a newly created object. It sets up the initial state (values) for the object when memory is allocated.

  • Auto-called: Executes automatically when you use the new keyword.

  • Same Name: Must have the exact same name as the Class.

  • No Return Type: Unlike regular methods, constructors do not have a return type (not even void).

  • Cannot be called manually: You cannot do myPhone.SmartPhone() later in your code—it runs only once during object creation.


Default Constructor

If you do not write any constructor in your class, Java automatically provides a default no-argument constructor behind the scenes.

java
1class SmartPhone {
2 // Provided automatically by Java compiler if no constructor is written
3 SmartPhone() {
4 // Empty body - sets instance variables to default values (0, null, false)
5 }
6}

Important Rule: If you define any custom constructor (parameterized or no-arg), Java stops providing the default constructor automatically.

Types of Constructors

1. No-Argument Constructor (Explicit)

A constructor that takes no parameters. You write it yourself to give default custom values to objects.

java
1class SmartPhone {
2 String brand;
3
4 // Explicit No-Arg Constructor
5 SmartPhone() {
6 brand = "Generic Brand";
7 }
8}

2. Parameterized Constructor

A constructor that takes arguments so you can initialize different objects with custom values at creation time.

java
1class SmartPhone {
2 String brand;
3 int ram;
4
5 // Parameterized Constructor
6 SmartPhone(String b, int r) {
7 brand = b;
8 ram = r;
9 }
10}

Constructor Overloading

Just like normal methods, you can have multiple constructors in the same class, as long as they have different parameter lists (different number, types, or order of parameters).

java
1class SmartPhone {
2 String brand;
3 int ram;
4
5 // Constructor 1: Default
6 SmartPhone() {
7 this.brand = "Unknown";
8 this.ram = 4;
9 }
10
11 // Constructor 2: Takes only brand
12 SmartPhone(String brand) {
13 this.brand = brand;
14 this.ram = 4; // default ram
15 }
16
17 // Constructor 3: Takes both brand and ram
18 SmartPhone(String brand, int ram) {
19 this.brand = brand;
20 this.ram = ram;
21 }
22}

Constructor Chaining (using this())

Constructor Chaining is the process of calling one constructor from another constructor within the same class using the this() keyword. It avoids code duplication.

Rule: this() call must be the very first statement inside the constructor body.

Example 1: Forwarding defaults to a master constructor

java
1class SmartPhone {
2 String brand;
3 int ram;
4
5 // Default constructor calls Parameterized constructor
6 SmartPhone() {
7 this("Generic Brand", 4); // Calls Constructor 2
8 }
9
10 // Master Parameterized Constructor
11 SmartPhone(String brand, int ram) {
12 this.brand = brand;
13 this.ram = ram;
14 }
15}

Example 2: Multi-step chaining

java
1class SmartPhone {
2 String brand;
3 int ram;
4 boolean is5G;
5
6 // 1st Constructor
7 SmartPhone() {
8 this("Unknown"); // Calls 2nd
9 }
10
11 // 2nd Constructor
12 SmartPhone(String brand) {
13 this(brand, 8, true); // Calls 3rd
14 }
15
16 // 3rd Constructor (Main setup)
17 SmartPhone(String brand, int ram, boolean is5G) {
18 this.brand = brand;
19 this.ram = ram;
20 this.is5G = is5G;
21 }
22}

Memory Error: OutOfMemoryError

When you create objects using new, memory is allocated in the Heap.

If you create millions of objects continuously (or run into an infinite loop creating objects) until the Heap space gets completely exhausted, Java throws an OutOfMemoryError at runtime.

java
1// Example that can crash the JVM Heap
2while (true) {
3 SmartPhone p = new SmartPhone(); // Continuously fills Heap memory
4}
5// Output: java.lang.OutOfMemoryError: Java heap space

Destructor in Java (Does it exist?)

Unlike C++, Java does NOT have explicit destructors.

In C++, developers must manually delete objects to free memory. In Java, memory management is handled automatically by the Garbage Collector (GC), which runs in the background and frees up Heap memory occupied by unreferenced objects.


Key Points to Remember

  • No Manual Freeing: You cannot manually destroy an object in Java.

  • finalize() is Deprecated: Java used to have a finalize() method that ran before garbage collection, but it has been deprecated and should not be used.

  • Resource Cleanup: For closing resources like files, database connections, or network sockets, use AutoCloseable with a try-with-resources block instead:

java
1// Recommended way to clean up resources in Java
2try (FileReader fr = new FileReader("file.txt")) {
3 // Read file operations
4} catch (IOException e) {
5 e.printStackTrace();
6} // 'fr' is automatically closed here when exiting the block

The this Keyword in Java

The this keyword is a reference variable in Java that refers to the current object (the specific object whose method or constructor is currently running).

Think of this as the object referring to itself.


Key Uses of this

1. Distinguishing Instance Variables from Parameters (Shadowing)

When a constructor or method parameter has the exact same name as an instance variable, the parameter name "shadows" (hides) the instance variable. Use this to explicitly tell Java you mean the instance variable.

java
1class SmartPhone {
2 String brand; // Instance variable
3
4 SmartPhone(String brand) { // Parameter has same name
5 // this.brand refers to the instance variable
6 // brand refers to the constructor parameter
7 this.brand = brand;
8 }
9}

2. Constructor Chaining (this())

You can use this() to invoke another constructor within the same class.

Rule: The this() call must be the very first line inside the constructor.

java
1class SmartPhone {
2 String brand;
3 int ram;
4
5 SmartPhone() {
6 this("Generic Brand", 4); // Calls the parameterized constructor below
7 }
8
9 SmartPhone(String brand, int ram) {
10 this.brand = brand;
11 this.ram = ram;
12 }
13}

3. Passing the Current Object as an Argument

You can pass this into another method if that method needs to operate on the current object.

java
1class SmartPhone {
2 void display() {
3 Printer printer = new Printer();
4 printer.printPhoneDetails(this); // Passes 'this' phone object
5 }
6}

Object Size in Java

When you create an object in Java (e.g., new Object()), it doesn't just store your variables. Java adds some hidden metadata overhead so the Java Virtual Machine (JVM) can manage the object (locking, garbage collection, type tracking).

In a standard 64-bit JVM (with Compressed OOPs enabled by default), every object's memory layout consists of 3 main parts:

plaintext
1┌─────────────────────────────────────────────────────────-┐
2│ JAVA OBJECT MEMORY LAYOUT │
3├─────────────────────────────────────────────────────────-┤
4│ 1. Object Header (12 Bytes) │
5│ ├── Mark Word (8 Bytes) -> HashCode, GC, Locks │
6│ └── Klass Word (4 Bytes) -> Pointer to Class Metadata│
7├─────────────────────────────────────────────────────────-┤
8│ 2. Instance Fields / Data (Variable Bytes) │
9│ └── Your actual primitive variables & references │
10├─────────────────────────────────────────────────────────-┤
11│ 3. Padding (0 to 7 Bytes) │
12│ └── Extra empty space to make total size multiple of 8│
13└─────────────────────────────────────────────────────────-┘

The 3 Parts Breakdown

1. Object Header (12 Bytes Metadata)

Every object in Java comes with a 12-byte header split into two sections:

  • Mark Word (8 Bytes): Stores internal JVM metadata:

    • Identity HashCode: Generated when System.identityHashCode(obj) is called.

    • GC Age: Tracks how many Garbage Collection cycles this object survived.

    • Locking & Synchronization Info: Stores synchronization status (whether a thread locked this object using synchronized).

  • Klass Word (4 Bytes with Compressed OOPs):

    • A pointer that tells the JVM: "I am an instance of SmartPhone.class!" It links the object in Heap to its class definition in Metaspace.

2. Instance Fields (Your Actual Data)

This is where your actual variables live. Primitive data types have fixed memory sizes:

Data TypeMemory Size
boolean, byte1 byte
char, short2 bytes
int, float4 bytes
long, double8 bytes
Object References (e.g., String brand)4 bytes (with Compressed OOPs)

3. Alignment Padding (Rule of 8)

The JVM processes memory in 8-byte chunks for 64-bit CPU architecture efficiency.

8-Byte Alignment Rule: The total memory consumed by any object MUST be a multiple of 8 bytes.

If Header + Fields equals 21 bytes, the JVM adds 3 bytes of empty padding to round it up to 24 bytes.


Real Example Calculation

Let's calculate the memory occupied by this SmartPhone object:

java
1class SmartPhone {
2 int ramInGB; // 4 bytes
3 boolean is5G; // 1 byte
4 String brand; // 4 bytes (Reference pointer)
5}

Step-by-Step Breakdown:

  1. Header: 12 bytes

  2. Fields: 4 (int) + 1 (boolean) + 4 (reference) = 9 bytes

  3. Subtotal: 12 + 9 = 21 bytes

  4. Padding: 21 is not a multiple of 8. The next multiple of 8 is 24 $\rightarrow$ 3 bytes of padding added.

  5. Total Heap Size = 24 Bytes

Key Takeaways:

  1. An empty Java object (new Object()) takes 16 bytes (12B Header + 4B Padding).

  2. Object references inside an object take 4 bytes on the Heap (pointing to another Heap memory address).

  3. Padding stores no data—it exists solely for CPU alignment performance.

Primitives vs. Reference Types (And Why Both Exist)

This is one of the biggest confusion points in Java: Why do we have int AND Integer? Why not just use one?

Let's clear this up once and for all.


1. Primitive Variables vs. Reference Variables

Java has two main types of variables:

Primitives (int, double, boolean, char, etc.)

  • What they are: Raw, basic values stored directly inside the variable on the Stack.

  • No methods, no null: You can't call .something() on an int, and it cannot be null.

java
1int age = 20; // The number 20 is stored directly inside 'age' on the Stack

Reference Variables / Objects (String, Integer, SmartPhone, etc.)

  • What they are: Variables that hold a memory address (pointer) pointing to the actual object sitting in the Heap.

  • Has methods and can be null: They can access methods and can be set to null (pointing to nothing).

java
1Integer count = new Integer(10); // 'count' lives on Stack, holding an address pointing to '10' in Heap
plaintext
1STACK MEMORY HEAP MEMORY
2┌──────────────────────────┐ ┌──────────────────────────┐
3│ int age = 20 │ │ │
4│ │ │ │
5│ Integer count ───────────┼──────────>│ [Integer Object: 10] │
6└──────────────────────────┘ └──────────────────────────┘

2. Wrapper Classes (Integer, Long, Boolean, etc.)

Java provides a Wrapper Class for every primitive type:

Primitive TypeWrapper Class (Reference Object)
intInteger
longLong
doubleDouble
booleanBoolean
charCharacter

3. Why Do BOTH Exist? (Pros & Cons)

If Integer can do everything int does, why keep primitives? Performance.

Primitives (int, long)

  • ✅ Pros: Extremely fast, uses very little memory (int = 4 bytes flat, no object header overhead).

  • ❌ Cons: Cannot be used in Java Collections (ArrayList<int> is invalid), cannot represent a missing/null value.

Wrapper Classes (Integer, Long)

  • ✅ Pros: Works with Collections (ArrayList<Integer>), supports utility methods (Integer.parseInt()), can be null (useful for database fields).

  • ❌ Cons: Much slower, takes ~16+ bytes of memory (12B header + 4B data + padding) vs. 4 bytes for primitive int.


4. Autoboxing and Unboxing

Because primitives and Wrapper objects are different, Java provides automatic conversion between them so you don't have to write manual conversion code every time.

plaintext
1Autoboxing
2 Primitive ───────────────> Wrapper Object
3 (int) <─────────────── (Integer)
4 Unboxing
  • Autoboxing: Primitive $\rightarrow$ Wrapper Object (Integer.valueOf())

  • Unboxing: Wrapper Object $\rightarrow$ Primitive (obj.intValue())

Autoboxing (Primitive $\rightarrow$ Wrapper Object)

The automatic conversion that the Java compiler makes between a primitive type and its corresponding Wrapper Object.

  • Happens automatically when you assign a primitive value to a Wrapper reference or pass it into a method expecting an Object (like adding to an ArrayList).
java
1// Behind the scenes: Compiler converts this to Integer.valueOf(10)
2Integer obj = 10; // Autoboxing (int primitive -> Integer object)
3
4List<Integer> list = new ArrayList<>();
5list.add(5); // Autoboxing (int primitive 5 -> Integer object)

Unboxing (Wrapper Object $\rightarrow$ Primitive)

The reverse process: converting a Wrapper Object back to its corresponding primitive type.

  • Happens automatically when an object is used in mathematical operations (+, -, *), assigned to a primitive variable, or evaluated in conditions (if, while).
java
1Integer obj = new Integer(20);
2
3// Behind the scenes: Compiler converts this to obj.intValue()
4int num = obj; // Unboxing (Integer object -> int primitive)
5
6int sum = obj + 5; // Unboxing happens to perform addition

Common Pitfall: NullPointerException during Unboxing

Since Wrapper Objects can be null, trying to unbox a null object into a primitive variable will crash your program at runtime with a NullPointerException.

java
1Integer count = null;
2
3// ❌ RUNTIME ERROR! Throws NullPointerException
4// Java tries to run count.intValue(), but 'count' is null!
5int total = count;

Call by Value vs. Call by Reference

Here is a absolute truth in Java that trips up even experienced developers:

Java is ALWAYS Call by Value. There is NO Call by Reference in Java.


What Happens When You Pass Arguments to a Function?

When you pass a variable to a method, Java always creates a copy of the variable's value and passes that copy into the function's parameter (on the Stack).

1. Passing Primitives (Pure Value Copy)

If you pass a primitive variable (int, boolean, etc.), Java copies the actual value. Changing the parameter inside the function does not affect the original variable.

java
1void modifyAge(int age) {
2 age = 30; // Modifies local copy on method Stack frame
3}
4
5int originalAge = 20;
6modifyAge(originalAge);
7System.out.println(originalAge); // Output: 20 (Unchanged!)

2. Passing Objects / Reference Variables (Copy of the Reference Pointer)

When you pass an Object, people often mistake it for Call by Reference because changes inside the method reflect outside. However, Java is still passing by value! The value being copied and passed is the memory address (reference pointer). Both the original reference variable and the method's parameter copy now point to the exact same object in the Heap.

java
1class SmartPhone {
2 String brand = "Samsung";
3}
4
5class Main {
6 static void updateBrand(SmartPhone phoneCopy) {
7 phoneCopy.brand = "Apple"; // Modifies the actual object in Heap!
8 }
9
10 public static void main(String[] args) {
11 SmartPhone myPhone = new SmartPhone();
12 updateBrand(myPhone);
13 System.out.println(myPhone.brand); // Output: Apple
14 }
15}
plaintext
1STACK MEMORY HEAP MEMORY
2 ┌──────────────────────────────┐ ┌──────────────────────┐
3 │ myPhone (Ref Address: 0x101) ├────────────────>│ SmartPhone Object │
4 │ │ │ │
5 │ phoneCopy (Copy: 0x101) ────┼────────────────>│ - brand: "Apple" │
6 └──────────────────────────────┘ └──────────────────────┘

Proof that Java is NOT Call by Reference: If you reassign phoneCopy = new SmartPhone() inside updateBrand(), the original myPhone outside will NOT change to point to the new object.

Shallow Copy vs. Deep Copy

When you want to duplicate an object, how far down do you copy its data? This depends on whether you perform a Shallow Copy or a Deep Copy.


1. Shallow Copy (Surface Level)

A Shallow Copy creates a new object, but it simply copies the exact values of all fields from the original object:

  • Primitive fields get copied directly.

  • Reference fields (nested objects) get their memory addresses copied.

This means both the original object and the new copy will share the same nested objects in Heap memory.

plaintext
1ORIGINAL OBJECT (HEAP) SHALLOW COPY (HEAP)
2┌─────────────────────────────┐ ┌─────────────────────────────┐
3│ ram = 8 │ │ ram = 8 │
4│ batteryRef (0x101) ─────────┼──────┐ ┌──────┼─ batteryRef (0x101) │
5└─────────────────────────────┘ │ │ └─────────────────────────────┘
6 ▼ ▼
7 ┌─────────────────────┐
8 │ Shared Battery │
9 │ Address: 0x101 │
10 └─────────────────────┘

Code Example (Shallow Copy using Copy Constructor)

java
1class Battery {
2 int capacity = 5000;
3}
4
5class SmartPhone {
6 int ram = 8;
7 Battery battery = new Battery();
8
9 // Default Constructor
10 SmartPhone() {}
11
12 // Shallow Copy Constructor
13 SmartPhone(SmartPhone original) {
14 this.ram = original.ram;
15 this.battery = original.battery; // Just copies the reference pointer!
16 }
17}
18
19public class Main {
20 public static void main(String[] args) {
21 SmartPhone phone1 = new SmartPhone();
22
23 // Create a shallow copy
24 SmartPhone phone2 = new SmartPhone(phone1);
25
26 // Modifying battery in phone2 will ALSO change phone1's battery!
27 phone2.battery.capacity = 3000;
28
29 System.out.println(phone1.battery.capacity); // Output: 3000 (Affected!)
30 }
31}

2. Deep Copy (Completely Independent)

A Deep Copy creates a new object AND manually creates new instances for every nested object inside it.

  • Primitive fields are copied directly.

  • Reference fields point to brand new objects created on the Heap.

  • The copy and the original share zero memory addresses.

plaintext
1ORIGINAL OBJECT (HEAP) DEEP COPY (HEAP)
2 ┌─────────────────────────────┐ ┌─────────────────────────────┐
3 │ ram = 8 │ │ ram = 8 │
4 │ batteryRef (0x101) ─────────┼──┐ ┌──┼─ batteryRef (0x202) │
5 └─────────────────────────────┘ │ │ └─────────────────────────────┘
6 ▼ ▼
7 ┌──────────┐ ┌──────────┐
8 │ Battery │ │ Battery │
9 │ (0x101) │ │ (0x202) │
10 └──────────┘ └──────────┘

Code Example (Deep Copy using Copy Constructors)

java
1class Battery {
2 int capacity;
3
4 Battery(int capacity) {
5 this.capacity = capacity;
6 }
7
8 // Copy Constructor for Battery
9 Battery(Battery originalBattery) {
10 this.capacity = originalBattery.capacity;
11 }
12}
13
14class SmartPhone {
15 int ram;
16 Battery battery;
17
18 SmartPhone(int ram, int batteryCapacity) {
19 this.ram = ram;
20 this.battery = new Battery(batteryCapacity);
21 }
22
23 // Deep Copy Constructor for SmartPhone
24 SmartPhone(SmartPhone original) {
25 this.ram = original.ram;
26 // Explicitly allocate a NEW Battery object on the Heap
27 this.battery = new Battery(original.battery);
28 }
29}
30
31public class Main {
32 public static void main(String[] args) {
33 SmartPhone phone1 = new SmartPhone(8, 5000);
34
35 // Create a deep copy
36 SmartPhone phone2 = new SmartPhone(phone1);
37
38 // Modifying battery in phone2 will NOT affect phone1!
39 phone2.battery.capacity = 3000;
40
41 System.out.println(phone1.battery.capacity); // Output: 5000 (Safe & Unchanged!)
42 System.out.println(phone2.battery.capacity); // Output: 3000
43 }
44}
FeatureShallow CopyDeep Copy
Nested ObjectsShared (pointers copied)Completely new objects created
IndependenceModifying nested data affects bothCompletely independent
Speed & MemoryFaster, uses less memorySlower, allocates new Heap memory
Best Used WhenObjects contain only primitives or immutable dataObjects contain nested, mutable references

The static Keyword in Java

In Java, the static keyword means "belongs to the class itself, not to any individual object."

When a member (variable or method) is declared static, it exists at the class level. It is shared across all instances (objects) created from that class.


1. Static Variables (Class Variables)

  • Class-Level Data: Shared equally by every object of the class. If one object modifies a static variable, the change reflects across all objects.

  • Where they live: They are NOT stored inside individual object instances on the Heap. They live in a special memory area managed by JVM (Metaspace / Class Static Area).

  • How to access: Can be accessed using an object reference, but the recommended way is directly using the class name (ClassName.variableName).

java
1class SmartPhone {
2 String model; // Instance variable (Each phone has its own model)
3 static String os = "Android"; // Static variable (Shared by ALL SmartPhone objects)
4
5 SmartPhone(String model) {
6 this.model = model;
7 }
8}
9
10public class Main {
11 public static void main(String[] args) {
12 SmartPhone p1 = new SmartPhone("Pixel");
13 SmartPhone p2 = new SmartPhone("Galaxy");
14
15 // Recommended access using Class name
16 System.out.println(SmartPhone.os); // Output: Android
17
18 // Access via object also works, but points to the same shared variable
19 p1.os = "Android 15";
20 System.out.println(p2.os); // Output: Android 15
21 }
22}

2. Static Methods

A static method belongs to the class and can be executed without creating any object of that class (e.g., Math.sqrt() or main()).

Because static methods run at the class level without requiring an object, they follow 3 strict rules:

  • Rule 1: Static methods can ONLY directly call other static methods.
    Reasoning: A non-static method belongs to an instance and requires a specific object to execute. If a static method tries to call a non-static method without an object present, Java has no idea which object's method to run!

  • Rule 2: Static methods can ONLY directly access static variables.
    Reasoning: Instance variables exist inside an object in Heap memory. Since a static method can be executed even when zero objects exist, accessing an instance variable would mean trying to read memory that hasn't been created yet.

  • Rule 3: Static methods CANNOT use the this keyword.
    Reasoning: The this keyword represents the current object instance. Since static methods belong to the class and run without an instance, this has no object to refer to (this == null).

java
1class SmartPhone {
2 String model = "Pixel";
3 static String os = "Android";
4
5 static void printInfo() {
6 System.out.println(os); // ✅ Allowed (static accessing static)
7
8 // System.out.println(model);
9 // ❌ COMPILE ERROR! Cannot make a static reference to the non-static field model
10
11 // System.out.println(this.os);
12 // ❌ COMPILE ERROR! Cannot use 'this' in a static context
13
14 // displayModel();
15 // ❌ COMPILE ERROR! Cannot call non-static method directly
16 }
17
18 void displayModel() {
19 System.out.println(model); // Non-static method accessing instance variable
20 }
21}

3. Static Block

A Static Initialization Block is a block of code used to initialize static variables or execute class-level setup tasks.

  • Runs Exactly Once: Executes automatically when the class is first loaded into JVM memory by the ClassLoader.

  • Runs Before Constructor: Executes before any constructor runs, and even before any object is created.

java
1class SmartPhone {
2 static String os;
3
4 // Static Block
5 static {
6 os = "Android OS";
7 System.out.println("1. Static block executed (Class loaded!)");
8 }
9
10 SmartPhone() {
11 System.out.println("2. Constructor executed (Object created!)");
12 }
13}
14
15public class Main {
16 public static void main(String[] args) {
17 System.out.println("Main method started.");
18
19 SmartPhone p1 = new SmartPhone();
20 SmartPhone p2 = new SmartPhone();
21 }
22}

output:

plaintext
1Main method started.
21. Static block executed (Class loaded!)
32. Constructor executed (Object created!)
42. Constructor executed (Object created!)

Notice how the static block runs only once when SmartPhone is first referenced, while the constructor runs for every new object.

4. What Can and Cannot Be static?

ComponentCan be static?Reasoning
VariablesYESUsed to store shared, class-level state.
MethodsYESUsed for utility functions that don't need object state (e.g., Math.max()).
ParametersNOMethod parameters are local variables created on the stack frame when a method is called. They only exist for that specific execution, making static meaningless.
Top-Level ClassNOA top-level class is already at the package level. Making it static would be redundant because there is no outer instance enclosing it.
Nested ClassYESA class defined inside another class can be static.

Nested Classes: A static nested class is an inner class that can be instantiated without needing an instance of the outer class. We will cover Nested Classes in detail later in this blog!

The final Keyword in Java

In Java, the final keyword is used to restrict modifications. It acts as a lock on variables, methods, and classes:

  • For Variables: Makes the value constant (unmodifiable).

  • For Methods: Prevents method overriding. (Don't worry if this sounds unfamiliar right now—we will break down method overriding in detail when we reach the Polymorphism section later in this blog!)

  • For Classes: Prevents class inheritance. (If you're wondering how class relationships work, sit tight—we'll cover inheritance inside out in the upcoming Pillars of OOP section!)


1. final Variables (Constants)

Once a final variable is assigned a value, it cannot be reassigned. Any attempt to change its value results in a compile-time error.

java
1public class Main {
2 public static void main(String[] args) {
3 final double PI = 3.14159;
4
5 // PI = 3.14;
6 // ❌ COMPILE ERROR! Cannot assign a value to final variable 'PI'
7 }
8}

Important Rule for Initialization: A final variable does not need to be initialized at the exact line of declaration, but it must be initialized before use (e.g., inside the constructor for instance variables). Once assigned, it becomes immutable.

2.static final Variables (Class Constants)

When you combine static and final, you create a true global constant:

  • static $\rightarrow$ Only one copy exists in memory, shared across all objects.

  • final $\rightarrow$ Value cannot be changed.

java
1class Circle {
2 // Shared by all Circle instances AND cannot be changed
3 public static final double PI = 3.14159;
4}

3.Why Naming Convention Uses ALL_CAPS?

By standard Java naming convention, static final primitive variables (and immutable constants) are written in ALL_CAPITAL_LETTERS with words separated by underscores (SNAKE_CASE):

  • Math.PI

  • Integer.MAX_VALUE

  • static final int MAX_RETRY_COUNT = 5;

Why? It gives an immediate visual signal to anyone reading your code that this variable is a compile-time constant and cannot be modified anywhere.

4. What Can Be final?

TargetCan be final?Behavior & Reasoning
VariablesYESPrevents Reassignment. Primitive values cannot be changed. For object references, the reference pointer cannot point to a new object (though internal object state can still be modified).
ParametersYESPrevents Reassignment inside Method. The method parameter value cannot be modified within the function body, protecting input data from accidental mutation.
MethodsYESPrevents Overriding. A final method cannot be overridden by child classes during inheritance. Used to preserve core security or implementation logic.
ClassesYESPrevents Inheritance. A final class cannot be extended (extends) by any other class (e.g., Java's built-in String class is final for security and performance).

Code Example: final in Action

java
1// 1. FINAL CLASS: Cannot be extended by any other class
2final class DatabaseConfig {
3
4 // 2. FINAL VARIABLE: Value can be assigned only once
5 final String dbUrl = "jdbc:mysql://localhost:3306/my_db";
6
7 // STATIC FINAL CONSTANT: Global constant shared across class
8 public static final int MAX_CONNECTIONS = 100;
9
10 // 3. FINAL METHOD: Cannot be overridden by child classes
11 final void printConnectionStatus() {
12 System.out.println("Connected to: " + dbUrl);
13 }
14
15 // 4. FINAL PARAMETER: 'timeoutInSeconds' cannot be modified inside the method
16 void connect(final int timeoutInSeconds) {
17 // timeoutInSeconds = 60;
18 // ❌ COMPILE ERROR! Cannot assign a value to final parameter 'timeoutInSeconds'
19
20 System.out.println("Connecting with timeout: " + timeoutInSeconds + "s");
21 }
22}
23
24// ❌ COMPILE ERROR! Cannot inherit from final class 'DatabaseConfig'
25// class CustomConfig extends DatabaseConfig {}
26
27public class Main {
28 public static void main(String[] args) {
29 DatabaseConfig config = new DatabaseConfig();
30
31 // config.dbUrl = "jdbc:postgresql://localhost:5432/my_db";
32 // ❌ COMPILE ERROR! Cannot reassign final variable 'dbUrl'
33
34 config.connect(30);
35 config.printConnectionStatus();
36 }
37}

The 4 Pillars of Object-Oriented Programming

Welcome to the heart of modern software engineering!

Up until now, we’ve covered the fundamental mechanics of Java—how classes, objects, memory, constructors, and keywords (static, final) operate behind the scenes. Now, it's time to put those mechanics to work.

Object-Oriented Programming is built on 4 core pillars. These aren't just features of Java—they are practical design principles that help us write clean, secure, reusable, and scalable code that mirrors the real world.

plaintext
1┌─────────────────────────────────────────────────────────┐
2 │ THE 4 PILLARS OF OOP IN JAVA │
3 └─────────────────────────────────────────────────────────┘
4 │ │ │ │
5 ▼ ▼ ▼ ▼
6 ┌──────────────┐┌────────────┐ ┌─────────────┐┌──────────────┐
7 │Encapsulation ││Inheritance │ │ Abstraction ││ Polymorphism │
8 └──────────────┘└────────────┘ └─────────────┘└──────────────┘

Quick Overview of the Pillars

Here is what each pillar means:

  1. Encapsulation: Bundling data (variables) and methods together into a single unit (a class) while restricting direct access to internal details for data protection.

  2. Inheritance: Allowing a child class to acquire properties and behaviors from a parent class to foster maximum code reuse.

  3. Abstraction: Hiding complex implementation details from the user and exposing only the essential operational features.

  4. Polymorphism: The ability of a single method, object, or action to take on multiple forms depending on the context.

Pillar 1: Encapsulation

Encapsulation is the first pillar of Object-Oriented Programming. It literally means "enclosing into a capsule."

In Java, Encapsulation revolves around two core rules:

  1. Bundling: Storing data (variables) and the methods that operate on that data together inside a single unit (a class).

  2. Data Hiding: Protecting sensitive data from direct, unrestricted access or modification from outside the class.

Real-World Analogy: Think of a capsule pill. The medicine inside is protected by the outer casing. You cannot touch the raw medicine directly—you interact with it through the pill as a single unit.


Why Unrestricted Access is Dangerous

If class fields are left open without protection, any external code can alter them to invalid or harmful states:

java
1class BankAccount {
2 public double balance; // ❌ Unrestricted public access
3}
4
5public class Main {
6 public static void main(String[] args) {
7 BankAccount account = new BankAccount();
8 account.balance = -10000; // Dangerous! Balance changed directly to a negative number.
9 }
10}

To prevent this, we hide data using Access Modifiers and expose controlled interactions via Getters and Setters.

Understanding Packages in Java

Before diving into Access Modifiers, let's clarify what a Package is:

A Package in Java is simply a folder that groups related classes and interfaces together (e.g., package com.app.banking;). It helps organize code and prevents naming collisions between classes.

Access Modifiers in Java

Access Modifiers control who can access a class variable, method, or constructor.
Here are the 4 Access Modifiers ordered from Most Restrictive $\rightarrow$ Most Permissive: $$\text{private} \longrightarrow \text{default} \longrightarrow \text{protected} \longrightarrow \text{public}$$

  1. private: Accessible ONLY inside the same class. Outside classes cannot see or touch private members.

  2. default (package-private): Used when no modifier keyword is written. Accessible by any class in the same package, but invisible outside the package.

  3. protected: Accessible by any class in the same package AND by child (inherited) classes in different packages.

  4. public: Accessible from anywhere in the project across all packages.

Access ModifierSame ClassSame PackageSubclass (Outside Package)World (Any Package)
private✅❌❌❌
default✅✅❌❌
protected✅✅✅❌
public✅✅✅✅
Code Example: Implementing Proper Encapsulation

To implement encapsulation properly:

  • Make instance variables private.

  • Provide public getter and setter methods to validate and control access.

java
1class BankAccount {
2 // 1. Data Hiding (Private variables)
3 private String accountNumber;
4 private double balance;
5
6 // Constructor
7 public BankAccount(String accountNumber, double initialBalance) {
8 this.accountNumber = accountNumber;
9 if (initialBalance >= 0) {
10 this.balance = initialBalance;
11 } else {
12 this.balance = 0;
13 }
14 }
15
16 // 2. Getter (Controlled Read Access)
17 public double getBalance() {
18 return this.balance;
19 }
20
21 // 3. Setter with Validation (Controlled Write Access)
22 public void deposit(double amount) {
23 if (amount > 0) {
24 this.balance += amount;
25 System.out.println("Deposited: $" + amount);
26 } else {
27 System.out.println("Invalid deposit amount!");
28 }
29 }
30}
31
32public class Main {
33 public static void main(String[] args) {
34 BankAccount acc = new BankAccount("ACC12345", 500.0);
35
36 // acc.balance = -1000;
37 // ❌ COMPILE ERROR! balance has private access in BankAccount
38
39 // Access data securely through public methods
40 acc.deposit(200.0);
41 System.out.println("Current Balance: $" + acc.getBalance());
42 }
43}

What is a Package in Java?

A Package in Java acts like a folder in your computer's file system that groups related classes, interfaces, and sub-packages together. Packages solve two major problems in software development: they prevent naming conflicts (e.g., two different developers can create a class named User as long as they live in different packages) and they enforce access protection by controlling which classes can see and interact with one another.


Types of Packages

In Java, packages are broadly divided into two categories:

  1. Built-in Packages (Standard Java Library): These are pre-written packages provided by Java to handle common tasks out of the box.

    • java.util: Contains utility classes like Scanner, ArrayList, and HashMap.

    • java.io: Handles input/output operations like reading and writing files.

    • java.lang: Contains core fundamental classes like String, Math, and System (this package is automatically imported in every Java file).

  2. User-Defined Packages: These are custom packages created by developers to organize their own project code cleanly into logical modules.

    • Created using the package keyword at the very top of a Java file (e.g., package com.mycompany.banking;).

    • To use a class from another package, you import it using the import keyword (e.g., import com.mycompany.banking.BankAccount;).

Pillar 2: Inheritance

Inheritance is a mechanism in Java where one class (child or subclass) acquires the properties (fields) and behaviors (methods) of another class (parent or superclass).

It establishes an IS-A relationship between objects. For example:

  • A Dog IS-A Animal

  • A SmartPhone IS-A ElectronicDevice


Why Do We Need Inheritance?

  1. Code Reusability: You write common fields and methods once in a parent class and automatically reuse them across all child classes without duplicating code.

  2. Supports Polymorphism: Inheritance lets you treat a child object as an instance of its parent class, enabling dynamic method dispatch at runtime (e.g., calling animal.makeSound() works whether animal holds a Dog or a Cat).


The extends Keyword & final Constraint

In Java, inheritance is achieved using the extends keyword.

java
1// Parent Class (Superclass)
2class Vehicle {
3 double speed;
4
5 void startEngine() {
6 System.out.println("Engine started...");
7 }
8}
9
10// Child Class (Subclass) inherits speed and startEngine()
11class Car extends Vehicle {
12 int numberOfDoors = 4;
13}
14
15public class Main{
16 public static void main(String[] args){
17 Car c1 = new Car();
18
19 // inherited parent class method and attributes
20 c1.speed = 47.3;
21 System.out.println(c1.speed);
22 c1.startEngine();
23
24 }
25}

output:

plaintext
147.3
2Engine started...

Crucial Rule: A final class cannot be inherited. If you declare final class Vehicle, no other class can extend it.

Types of Inheritance in Java

plaintext
11. Single 2. Multi-Level 3. Hierarchical
2 [ A ] [ A ] [ A ]
3 │ │ ┌──┴──┐
4 ▼ ▼ ▼ ▼
5 [ B ] [ B ] [ B ] [ C ]
6 │
7 ▼
8 [ C ]

1. Single Inheritance

A single child class extends a single parent class.

java
1class Animal {
2 void eat() { System.out.println("Eating..."); }
3}
4
5class Dog extends Animal {
6 void bark() { System.out.println("Barking..."); }
7}
8
9public class Main{
10 public static void main(String[] args){
11 Dog d1 = new Dog();
12
13 d1.eat(); // Eating...
14 d1.bark(); // Barking...
15 }
16}
plaintext
1[ Animal ]
2 │
3 ▼
4 [ Dog ]

2. Multi-Level Inheritance

A child class inherits from a parent class, which itself inherits from another class (forming a chain).

java
1class Animal {
2 void eat() { System.out.println("Eating..."); }
3}
4
5class Dog extends Animal {
6 void bark() { System.out.println("Barking..."); }
7}
8
9// inherit the method and attributes from both Animal and Dog
10class Puppy extends Dog {
11 void weep() { System.out.println("Weeping..."); }
12}
plaintext
1[ Animal ]
2 │
3 ▼
4 [ Dog ]
5 │
6 ▼
7 [ Puppy ]

3. Hierarchical Inheritance

Multiple child classes extend the exact same parent class.

java
1class Animal {
2 void eat() { System.out.println("Eating..."); }
3}
4
5class Dog extends Animal {
6 void bark() { System.out.println("Barking..."); }
7}
8
9class Cat extends Animal {
10 void meow() { System.out.println("Meowing..."); }
11}
plaintext
1[Animal]
2 ┌──┴──┐
3 [Dog] [Cat]

Multiple Inheritance & The Diamond Problem

Java does NOT support Multiple Inheritance using classes(a class cannot extend more than one class at the same time: class C extends A, B❌).

Why? The Diamond Problem

If Class A has a method show(), and both Class B and Class C override show(), what happens if Class D inherits from both B and C?

plaintext
1[ A ] (show)
2 ╱ ╲
3 ╱ ╲
4 [ B ] [ C ]
5 (show) (show)
6 ╲ ╱
7 ╲ ╱
8 [ D ] <-- Which show() should D run? Compiler Confusion!

When D.show() is called, the compiler cannot decide whether to execute B's version or C's version. To prevent this ambiguity, Java disables multiple inheritance for classes.

Note: Multiple inheritance in Java is achieved safely using Interfaces, which we will cover in detail later in this blog!

The super Keyword in Java

The super keyword is a reference variable used to refer directly to the immediate parent class object.

3 Main Uses of super
  1. To Access Parent Class Variable (Handling Variable Shadowing)
    Used when a child class defines an instance variable with the exact same name as a variable in the parent class.
java
1class Vehicle {
2 int maxSpeed = 120;
3}
4
5class Car extends Vehicle {
6 int maxSpeed = 200; // Variable Shadowing
7
8 void displaySpeeds() {
9 System.out.println("Child maxSpeed: " + maxSpeed); // Output: 200
10 System.out.println("Parent maxSpeed: " + super.maxSpeed); // Output: 120
11 }
12}
  1. To Access Parent Class Method (Handling Method Overriding)
    Used when a child class overrides a parent method, but still wants to execute the parent's original implementation.
java
1class Vehicle {
2 void displayInfo() {
3 System.out.println("Vehicle information");
4 }
5}
6
7class Car extends Vehicle {
8 void displayInfo() {
9 super.displayInfo(); // Calls parent's displayInfo() first
10 System.out.println("Car specific information");
11 }
12}
  1. Call Parent Class Constructor (super())
    Used to invoke the parent class constructor from inside the child class constructor.

Automatic Behavior: If you don't explicitly write super(), the Java compiler automatically inserts a hidden super() call as the very first line of every child constructor!

java
1class Vehicle {
2 String type;
3
4 Vehicle(String type) {
5 this.type = type;
6 System.out.println("Vehicle constructor called");
7 }
8}
9
10class Car extends Vehicle {
11 int doors;
12
13 Car(String type, int doors) {
14 super(type); // Must be the FIRST line in child constructor
15 this.doors = doors;
16 System.out.println("Car constructor called");
17 }
18}

Pillar 3: Polymorphism

The word Polymorphism comes from Greek:

  • Poly = Many

  • Morph = Forms

In Object-Oriented Programming, Polymorphism is the ability of a single method, operator, or object to behave differently based on the context in which it is used.

Real-World Analogy: Think of a person. You can act as a student in college, a customer in a shop, and a son/daughter at home. You are the same person, but you exhibit different behaviors depending on the situation!

The 2 Types of Polymorphism in Java

plaintext
1┌─────────────────────────────────┐
2 │ POLYMORPHISM │
3 └─────────────────────────────────┘
4 │
5 ┌──────────────────────┴──────────────────────┐
6 ▼ ▼
7┌───────────────────────────┐ ┌───────────────────────────┐
8│ Compile-Time Polymorphism │ │ Runtime Polymorphism │
9│ (Static / Early) │ │ (Dynamic / Late) │
10└─────────────┬─────────────┘ └─────────────┬─────────────┘
11 │ │
12 ▼ ▼
13 [ Method Overloading ] [ Method Overriding ]

1. Compile-Time Polymorphism (Static / Early Binding)

Compile-Time Polymorphism occurs when the decision of which method to execute is made by the compiler at compile time. It is achieved using Method Overloading.

Method Overloading

A class can have multiple methods with the exact same name, as long as their parameter lists are different (either in number of parameters, types of parameters, or order of parameters).

java
1class Calculator {
2 // Method 1: Adds two integers
3 int add(int a, int b) {
4 return a + b;
5 }
6
7 // Method 2: Overloaded - Adds three integers
8 int add(int a, int b, int c) {
9 return a + b + c;
10 }
11
12 // Method 3: Overloaded - Adds two doubles
13 double add(double a, double b) {
14 return a + b;
15 }
16}
17
18public class Main {
19 public static void main(String[] args) {
20 Calculator calc = new Calculator();
21
22 // Compiler knows exactly which method to bind based on the arguments
23 System.out.println(calc.add(5, 10)); // Calls Method 1 -> Output: 15
24 System.out.println(calc.add(5, 10, 15)); // Calls Method 2 -> Output: 30
25 System.out.println(calc.add(2.5, 3.5)); // Calls Method 3 -> Output: 6.0
26 }
27}

Why is it called Compile-Time? Because the JVM doesn't need to guess at runtime. The Java compiler reads the argument signatures and links the exact method call during compilation.

2. Runtime Polymorphism (Dynamic / Late Binding)

Runtime Polymorphism occurs when the decision of which overridden method to execute is resolved at runtime based on the actual object created in Heap memory. It is achieved using Method Overriding.

Method Overriding

When a child class provides a specific implementation for a method that is already defined in its parent class (with the exact same name, return type, and parameter list).

java
1class Animal {
2 void sleep() {
3 System.out.println("Animal is sleeping...");
4 }
5}
6
7class Dog extends Animal {
8 @Override
9 void sleep() {
10 System.out.println("Dog is sleeping...");
11 }
12}
13
14public class Main {
15 public static void main(String[] args) {
16 Dog myDog = new Dog();
17
18 // Calling overridden method
19 myDog.sleep(); // Output: Dog is sleeping...
20 }
21}
Upcasting & Dynamic Method Dispatch

When a parent class reference variable points to a child class object (Upcasting), Java decides which method to run based on the actual object in Heap, not the reference type!

java
1class Payment {
2 void processPayment() {
3 System.out.println("Processing generic payment...");
4 }
5}
6
7class CreditCardPayment extends Payment {
8 @Override
9 void processPayment() {
10 System.out.println("Processing payment via Credit Card...");
11 }
12}
13
14class UPIPayment extends Payment {
15 @Override
16 void processPayment() {
17 System.out.println("Processing payment via UPI...");
18 }
19}
20
21public class Main {
22 public static void main(String[] args) {
23 // Parent reference pointing to Child objects (Upcasting)
24 Payment p1 = new CreditCardPayment();
25 Payment p2 = new UPIPayment();
26
27 // Dynamic Method Dispatch (Resolved at Runtime)
28 p1.processPayment(); // Output: Processing payment via Credit Card...
29 p2.processPayment(); // Output: Processing payment via UPI...
30 }
31}

Critical Rules & Exceptions in Method Overriding

Not everything in Java can participate in Runtime Polymorphism. Here are the 4 major rules and the reasoning behind each:

  • Rule 1: static methods CANNOT be overridden (Method Hiding instead)

    • Reasoning: Method overriding relies on dynamic dispatch on an object instance stored in the Heap. Since static methods belong to the class itself and are resolved at compile time using class references, they cannot be dynamically dispatched at runtime. If a child class defines a static method with the same signature, it simply hides the parent's static method.
  • Rule 2: private methods CANNOT be overridden

    • Reasoning: Overriding requires the child class to be aware of the parent method. Since private members are completely hidden and invisible outside their own class, child classes cannot see them to override them.
  • Rule 3: final methods CANNOT be overridden

    • Reasoning: The final keyword explicitly locks a method's implementation to prevent any subclass from altering its behavior.
  • Rule 4: Instance Variables / Fields CANNOT exhibit Polymorphism

    • Reasoning: Polymorphism in Java applies ONLY to methods, not fields/variables. Variables are resolved at compile time based solely on the reference type, not the underlying object in Heap memory.
java
1class Parent {
2 int value = 10;
3}
4
5class Child extends Parent {
6 int value = 20; // Variable Shadowing
7}
8
9public class Main {
10 public static void main(String[] args) {
11 Parent obj = new Child();
12
13 // Fields are NOT polymorphic! Evaluated by Reference Type (Parent)
14 System.out.println(obj.value); // Output: 10 (Not 20!)
15 }
16}

Pillar 4: Abstraction

Abstraction is the fourth pillar of Object-Oriented Programming, and it is often considered the most confusing. Let's make it crystal clear.

Abstraction is the practice of focusing on WHAT an object does, while hiding HOW it actually does it.

Real-World Analogy: When you step on the gas pedal of a car, you know what happens: the car speeds up. You don't need to know how the fuel injectors, spark plugs, and pistons work under the hood to drive it. The complex mechanical details are abstracted away behind a simple interface (the pedal).


Low-Level vs. High-Level Abstraction in Java

  1. Low-Level Abstraction (Data Level):

    • Done using Encapsulation (making fields private and using getters/setters).

    • Hides internal object variables from direct outside tampering.

  2. High-Level Abstraction (Behavioral Level):

    • Done using Abstract Classes and Interfaces.

    • Completely separates the "WHAT" (the contract or idea) from the "HOW" (the actual code execution).

Why Normal (Concrete) Classes Are Tightly Coupled

In a regular concrete class, the method declaration (the "WHAT") and the method implementation (the "HOW") are glued together:

java
1class Car {
2 void accelerate() {
3 // 'WHAT' (accelerate) and 'HOW' (internal combustion math) are locked together!
4 System.out.println("Injecting fuel into cylinders...");
5 }
6}

If you ever want an ElectricCar to accelerate differently using batteries, a standard concrete class forces you to overwrite existing code or create messy conditionals. High-level abstraction solves this by separating the declaration from the implementation.

1. Abstract Class (Partial Blueprint for a Family of Things)

An Abstract Class is a class declared with the abstract keyword. It serves as a partial template for a family of closely related objects (e.g., Dog, Cat, Lion all belonging to the family Animal).

It can contain both abstract methods (method declarations with no body) and concrete methods (methods with actual code bodies).

Core Rules of Abstract Classes
Feature / RuleBehavior & Reasoning
Cannot Be InstantiatedYou cannot use new Animal(). Since abstract classes contain incomplete methods (no bodies), creating an object directly from them would result in undefined behavior.
Meant to Be ExtendedAn abstract class exists solely to be inherited (extends) by concrete child classes.
Constructors Allowed?YES. Even though you cannot instantiate an abstract class directly, it can have a constructor. It runs when a child class object is created via super().
Can it be final?NO. abstract demands inheritance, while final prevents inheritance. Combining abstract final causes a compile error.
static Members?YES. It can have static variables, static methods, and static blocks that belong to the class level.
private Methods?YES, for helper methods inside the class. However, an abstract method CANNOT be private because child classes must see it to override it!
final Methods?YES, for concrete methods you want to prevent child classes from changing. However, an abstract method CANNOT be final because it must be overridden!
Multiple Inheritance?NO. A class can extend only one abstract class at a time (standard Java single-inheritance limitation).
Code Example: Abstract Class
java
1// Abstract Class (Family Blueprint)
2abstract class BankAccount {
3 String accountHolder;
4
5 // 1. Constructor (Used by child classes)
6 BankAccount(String accountHolder) {
7 this.accountHolder = accountHolder;
8 }
9
10 // 2. Concrete Method (Common "HOW" shared by ALL bank accounts)
11 void printReceipt() {
12 System.out.println("Receipt printed for: " + accountHolder);
13 }
14
15 // 3. Abstract Method (Only "WHAT" - Each bank account calculates interest differently)
16 abstract void calculateInterest();
17}
18
19// Concrete Child Class 1
20class SavingsAccount extends BankAccount {
21 SavingsAccount(String name) {
22 super(name);
23 }
24
25 // MUST define the abstract method, otherwise compile-time error!
26 @Override
27 void calculateInterest() {
28 System.out.println("Calculating 4% interest for Savings Account.");
29 }
30}
31
32// Concrete Child Class 2
33class FixedDepositAccount extends BankAccount {
34 FixedDepositAccount(String name) {
35 super(name);
36 }
37
38 @Override
39 void calculateInterest() {
40 System.out.println("Calculating 7.5% interest for Fixed Deposit.");
41 }
42}

2. Interface (A Pure Contract / Capability)

An Interface in Java is a 100% pure specification of behavior (until Java 8). It defines WHAT an object can do, without saying anything about HOW it does it.
While Abstract Classes represent identities ("IS-A" relationship within a family), Interfaces represent roles, capabilities, or behaviors (often named as adjectives ending in -able: Flyable, Cloneable, Runnable, Payable).

Analogy: A Bird IS-A Animal (Abstract Class). But both an Airplane and a Bird can implement Flyable (Interface), even though they are completely unrelated objects!

Core Rules of Interfaces
Feature / RuleBehavior & Reasoning
Pure "WHAT" ContractAll declared methods are implicitly public abstract by default.
No Instance FieldsCannot hold normal variables. Any variables declared in an interface are implicitly public static final (global constants).
No ConstructorsSince interfaces hold zero instance state, they have no constructors and cannot be instantiated with new.
Default Methods (Java 8+)Interfaces can now have methods with bodies using the default keyword. Introduced so developers could add new features to existing interfaces without breaking legacy code that implemented them!
Multiple Inheritance SupportedA single class can implement multiple interfaces (e.g., implements Flyable, Swimmable).
How Interfaces Solve the Diamond Problem in Multiple Inheritance

If a class implements two interfaces (A and B) that declare the exact same method signature, there is no ambiguity because neither interface provides code execution—the implementing class provides the single, unified implementation body!

Even if both interfaces contain identical default methods, Java forces the implementing class to explicitly override and resolve the conflict:

java
1interface Flyable {
2 void fly();
3}
4
5interface Swimmable {
6 void swim();
7}
8
9// A Duck can do BOTH roles!
10class Duck implements Flyable, Swimmable {
11 @Override
12 public void fly() {
13 System.out.println("Duck is flying in the sky.");
14 }
15
16 @Override
17 public void swim() {
18 System.out.println("Duck is swimming in water.");
19 }
20}

Comparison Table: Abstract Class vs. Interface

FeatureAbstract ClassInterface
Core ConceptPartial blueprint for a family of related things ("IS-A").A contract of capabilities or roles ("CAN-DO").
Inheritance KeywordExtended using extends.Implemented using implements.
Multiple InheritanceNo. A class can extend only 1 abstract class.Yes. A class can implement multiple interfaces.
MethodsCan have abstract, concrete, static, and final methods.Abstract by default. (Can have default & static methods since Java 8).
Fields / VariablesCan have instance variables (private, protected, etc.).Only public static final constants.
ConstructorYes, has constructors called by subclasses via super().No, cannot have constructors.
Access ModifiersMethods can be public, protected, or private.All methods are implicitly public by default.
PerformanceSlightly faster (direct class hierarchy lookup).Slightly slower due to dynamic interface lookup tables.

Use an Abstract Class when you have a core base identity shared by closely related classes that need to share common state (variables) and code execution (concrete methods).

Use an Interface when you want to define a peripheral role, capability, or contract that can be applied to completely unrelated classes across your application.

Java-Specific Object Rules & Design Constructs

Beyond the four core pillars of OOP, Java includes specific rules, root utilities, and design constructs that shape how objects are structured, stored, and managed in memory.


1. Why Only One public Class Per .java File?

In Java, you can define multiple classes inside a single .java file, but only ONE top-level class can be marked public, and the file name MUST match that public class name exactly (case-sensitive).

For example, if your public class is named SmartPhone, the file name must be SmartPhone.java.

The Core Reasons

  1. JVM & ClassLoader Fast-Lookup: When you compile a .java file, javac generates a separate .class bytecode file for every single class inside it. At runtime, the ClassLoader locates bytecode using package directory paths. Matching the file name to the public class lets the JVM find and load classes instantly without scanning the entire file contents.

  2. Clean Project Structure: In large projects, finding where a public component lives would be difficult if public classes could hide inside arbitrarily named files. The rule guarantees a 1:1 match between class names and file names.

  3. API Boundaries: A public class forms part of your package's exposed interface. Enforcing one public class per file keeps major architectural components modular and easy to navigate.

java
1// File Name: SmartPhone.java
2
3// 1. PUBLIC CLASS: Only ONE allowed per file. File MUST be SmartPhone.java
4public class SmartPhone {
5 private String brand;
6 private Battery battery = new Battery();
7
8 public SmartPhone() {
9 System.out.println("SmartPhone created!");
10 }
11}
12
13// 2. DEFAULT / PACKAGE-PRIVATE CLASS: Multiple allowed in the same file!
14class Battery {
15 int capacity = 5000;
16}
17
18// ❌ COMPILE ERROR IF ADDED IN THIS FILE:
19// public class Camera {}
20// "The public type Camera must be defined in its own file named Camera.java"

2. POJO (Plain Old Java Object) Class

A POJO is an ordinary Java class that has no special framework dependencies—it does not extend framework classes, implement framework interfaces, or require external annotations.

It serves as a simple data container to hold state and transfer data cleanly across application layers.

Characteristics of a Standard POJO:
  • Instance fields are private (Encapsulation).

  • Provides public Getters and Setters for controlled field access.

  • Provides a No-Argument Constructor.

  • Contains no framework-specific boilerplate or code

java
1// Standard POJO Class
2public class UserPOJO {
3 private String name;
4 private int age;
5
6 // Default No-Arg Constructor
7 public UserPOJO() {}
8
9 // Parameterized Constructor
10 public UserPOJO(String name, int age) {
11 this.name = name;
12 this.age = age;
13 }
14
15 // Getters and Setters
16 public String getName() { return name; }
17 public void setName(String name) { this.name = name; }
18
19 public int getAge() { return age; }
20 public void setAge(int age) { this.age = age; }
21}

3. The java.lang.Object Class (Root of All Classes)

In Java, every single class directly or indirectly inherits from java.lang.Object.
Even if you write class SmartPhone {}, the Java compiler automatically converts it to:

java
1class SmartPhone extends Object {}

Because of this universal inheritance, every object in Java inherits a set of core built-in methods provided by Object.

plaintext
1┌───────────────────────────────┐
2 │ java.lang.Object Class │
3 └───────────────┬───────────────┘
4 │
5 ┌──────────────────┬───────────────┼───────────────┬──────────────────┐
6 ▼ ▼ ▼ ▼ ▼
7 [ String Output ] [ Comparison ] [ Metadata ] [ GC Cleanup ] [ Concurrency ]
8 • toString() • equals() • getClass() • finalize() • wait()
9 • hashCode() • clone() • notify()
10 • notifyAll()

Key Methods Provided by the Object Class

CategoryMethodDefault Behavior
String RepresentationtoString()Returns ClassName@HexHashCode (e.g., SmartPhone@15db9742). Override to display meaningful field values.
Comparisonequals(Object obj)Compares memory addresses (==). Override for logical field equality.
HashinghashCode()Returns a numerical hash value based on memory location by default. Used in collections like HashMap.
MetadatagetClass()Returns the runtime Class type of the object.
Garbage Collectionfinalize()(Deprecated) Called by GC before destroying an unreferenced object.
Cloningclone()Creates a shallow field-for-field copy of the object (requires implementing the Cloneable interface).
Concurrencywait(), notify(), notifyAll()Low-level thread synchronization mechanisms built onto object monitor locks.

Why Must We Override equals() and hashCode() for Custom Classes?

By default, the Object class implementation of equals() uses reference equality (==). It checks whether two reference variables point to the exact same memory address on the Heap.

java
1class Student {
2 int id;
3 Student(int id) { this.id = id; }
4}
5
6public class Main {
7 public static void main(String[] args) {
8 Student s1 = new Student(101);
9 Student s2 = new Student(101);
10
11 // Default Object.equals() compares Heap memory addresses!
12 System.out.println(s1.equals(s2)); // Output: false (Even though IDs match!)
13 }
14}
❌ The Problem with Default Object Methods

Here is how the java.lang.Object class implements these methods behind the scenes:

java
1// Default implementation inside java.lang.Object
2public class Object {
3
4 public boolean equals(Object obj) {
5 return (this == obj); // Compares MEMORY ADDRESSES, not field values!
6 }
7
8 public native int hashCode(); // Returns internal memory address hash!
9}
The equals() & hashCode() Contract

If two objects are logically equal (their field values match), they MUST return true for equals() AND return the exact same hashCode() integer.

If you override equals() but forget to override hashCode(), hash-based collections like HashMap or HashSet will place identical objects into different buckets and fail to retrieve them!

java
1import java.util.Objects;
2
3class Student {
4 int id;
5 String name;
6
7 Student(int id, String name) {
8 this.id = id;
9 this.name = name;
10 }
11
12 // Overriding equals() for logical field comparison
13 @Override
14 public boolean equals(Object o) {
15 if (this == o) return true; // Same memory reference
16 if (o == null || getClass() != o.getClass()) return false; // Null or different class
17
18 Student student = (Student) o;
19 return id == student.id && Objects.equals(name, student.name);
20 }
21
22 // Overriding hashCode() to match logical equality
23 @Override
24 public int hashCode() {
25 return Objects.hash(id, name);
26 }
27}

4. Nested Classes in Java

A Nested Class is a class defined inside an outer enclosing class. Nested classes group logically related components together, improving readability and encapsulation.

plaintext
1┌───────────────────────────────┐
2 │ NESTED CLASSES │
3 └───────────────┬───────────────┘
4 │
5 ┌──────────────────────┴──────────────────────┐
6 ▼ ▼
7 [ Static Nested Class ] [ Inner Classes ]
8 │
9 ┌────────────────────────┼────────────────────────┐
10 ▼ ▼ ▼
11 [ Member Inner Class ] [ Local Inner Class ] [ Anonymous Inner Class ]

1. Static Nested Class

Declared with the static keyword inside an outer class. It cannot directly access non-static fields or methods of the outer class—it acts like a regular top-level class scoped inside the outer namespace for organizational convenience.

java
1class Outer {
2 static int outerData = 10;
3
4 static class StaticNested {
5 void display() {
6 System.out.println("Static Outer Data: " + outerData);
7 }
8 }
9}
10
11// Instantiated independently without creating an Outer instance:
12Outer.StaticNested nested = new Outer.StaticNested();

2. Member Inner Class (Non-Static)

A non-static class defined directly inside an outer class. It has full access to all outer class fields and methods (including private ones).

java
1class Outer {
2 private String secret = "Top Secret";
3
4 class Inner {
5 void reveal() {
6 System.out.println("Outer Secret: " + secret); // Direct access to private fields!
7 }
8 }
9}
10
11// Requires an Outer class instance to instantiate:
12Outer outer = new Outer();
13Outer.Inner inner = outer.new Inner();

3. Local Inner Class

Defined inside a method body or code block. Its scope is restricted entirely to that method execution.

java
1class Outer {
2 void process() {
3 // Class scoped inside method execution
4 class LocalPrinter {
5 void print() {
6 System.out.println("Printing from Local Inner Class...");
7 }
8 }
9
10 LocalPrinter lp = new LocalPrinter();
11 lp.print();
12 }
13}

4. Anonymous Inner Class

An inner class without a name declared and instantiated at the exact same time. Used to create a quick, one-time inline implementation of an interface or class method without writing a separate subclass file.

java
1abstract class Greeting {
2 abstract void greet();
3}
4
5public class Main {
6 public static void main(String[] args) {
7 // Anonymous Inner Class implementing 'Greeting' on the fly
8 Greeting g = new Greeting() {
9 @Override
10 void greet() {
11 System.out.println("Hello from Anonymous Class!");
12 }
13 };
14
15 g.greet();
16 }
17}

Advanced Interface Types

Beyond standard contracts, Java features two special types of interfaces: Functional Interfaces and Marker Interfaces.


1. Functional Interface (Single Abstract Method - SAM)

A Functional Interface is an interface that contains EXACTLY ONE abstract method. It can contain any number of default or static methods, but only one un-implemented abstract method.

It is marked with the @FunctionalInterface annotation (optional, but forces the compiler to throw an error if someone accidentally adds a second abstract method).

Why Are Functional Interfaces Important?

They are the foundation of Functional Programming in Java! Functional interfaces allow us to use Lambda Expressions (->), letting us pass executable code/behavior directly as method arguments instead of writing clunky anonymous inner classes.

java
1@FunctionalInterface
2interface Calculator {
3 int operate(int a, int b); // Single Abstract Method (SAM)
4}
5
6public class Main {
7 public static void main(String[] args) {
8 // 1. Old Anonymous Class Way (Verbose)
9 Calculator addOld = new Calculator() {
10 public int operate(int a, int b) { return a + b; }
11 };
12
13 // 2. Modern Lambda Expression Way (Clean & Concise!)
14 Calculator addLambda = (a, b) -> a + b;
15
16 System.out.println(addLambda.operate(10, 20)); // Output: 30
17 }
18}
Common Built-in Functional Interfaces:
  • Comparable<T> $\rightarrow$ Contains compareTo()

  • Runnable $\rightarrow$ Contains run()

  • Callable<V> $\rightarrow$ Contains call()

2. Marker Interface (Tagging Interface)

A Marker Interface is an interface that contains ZERO methods and ZERO fields.

It is used as a "tag" or "marker" to signal metadata to the JVM or compiler that a class possesses a special capability or permission.

java
1// Definition of a Marker Interface (Completely Empty!)
2public interface Serializable {
3 // No fields, no methods!
4}

Famous Built-in Marker Interfaces:

  1. Cloneable: Signals to Object.clone() that this object is allowed to be copied field-by-field. Calling clone() on a class that doesn't implement Cloneable throws CloneNotSupportedException.

  2. Serializable: Signals to the JVM that an object's state can be flattened into a byte stream and saved to a file or sent across a network.

  3. RandomAccess: Used by collection classes (like ArrayList) to indicate that they support ultra-fast, constant-time $O(1)$ random positional index lookup.

How Marker Interfaces Work Under the Hood

The JVM checks for marker capabilities at runtime using the instanceof operator:

java
1SmartPhone phone = new SmartPhone();
2
3if (phone instanceof Cloneable) {
4 // JVM permits object cloning
5} else {
6 throw new CloneNotSupportedException();
7}

Appendix: Type Casting & Memory Lookup Rules

To write clean OOP code, you need to understand how Java casts objects across class hierarchies and how memory handles variables vs. methods.

1. Upcasting vs. Downcasting

Type casting between objects in an inheritance hierarchy is called Object Type Casting.

plaintext
1[ Parent Class ]
2 ▲ │
3 Upcasting │ │ Downcasting
4 │ ▼
5 [ Child Class ]

Upcasting (Child $\rightarrow$ Parent)

Casting a child object reference to a parent class reference type.

  • Automatic / Safe: Done implicitly by Java without explicit casting.

  • Core of Polymorphism: Allows parent references to point to any child object in Heap memory.

java
1class Animal {
2 void makeSound() { System.out.println("Some sound..."); }
3}
4
5class Dog extends Animal {
6 void bark() { System.out.println("Woof!"); }
7}
8
9public class Main {
10 public static void main(String[] args) {
11 // Upcasting: Dog object assigned to Animal reference
12 Animal animal = new Dog(); // Safe & Implicit
13
14 animal.makeSound(); // Output: Some sound...
15 // animal.bark(); ❌ COMPILE ERROR! Parent reference can only see methods defined in Animal.
16 }
17}

Downcasting (Parent $\rightarrow$ Child)

Casting a parent reference back down to a child class type.

  • Explicit / Risky: Must be written explicitly with (ChildClass).

  • Requires Safety Check: If the object in Heap is NOT actually an instance of that child class, Java throws a runtime ClassCastException.

java
1public class Main {
2 public static void main(String[] args) {
3 Animal animal = new Dog(); // Upcasted
4
5 // Downcasting explicitly back to Dog reference
6 if (animal instanceof Dog) {
7 Dog myDog = (Dog) animal; // Explicit Downcasting
8 myDog.bark(); // ✅ Works! Output: Woof!
9 }
10 }
11}

2. Variable Shadowing vs. Method Overriding (Memory Resolution)

What happens when both the Parent and Child classes have fields and methods with the same name?

Golden Rule:
  • Methods are resolved dynamically at RUNTIME based on the actual object in Heap (Polymorphic / Overriding).

  • Variables / Fields are resolved at COMPILE TIME based on the reference type on Stack (Variable Shadowing / Not Polymorphic).

java
1class Parent {
2 int x = 10; // Variable
3
4 void display() {
5 System.out.println("Parent Method");
6 }
7}
8
9class Child extends Parent {
10 int x = 20; // Variable Shadowing!
11
12 @Override
13 void display() {
14 System.out.println("Child Method");
15 }
16}
17
18public class Main {
19 public static void main(String[] args) {
20 Parent obj = new Child(); // Upcasting
21
22 // 1. Field Access -> Evaluated by REFERENCE TYPE (Parent on Stack)
23 System.out.println(obj.x);
24 // Output: 10 (Not 20!)
25
26 // 2. Method Execution -> Evaluated by ACTUAL OBJECT (Child on Heap)
27 obj.display();
28 // Output: Child Method
29 }
30}