OOP Fundamentals
A class is a blueprint; an object (instance) is a concrete piece of memory built from that blueprint via new.
🎯 What you'll learn
- Classes and Objects
- Constructors and
this - Encapsulation and Access Modifiers
- Static vs Instance Members
- Method Overloading
1Classes and Objects
A class is a blueprint; an object (instance) is a concrete piece of memory built from that blueprint via new.
public class Account {
String owner;
double balance;
Account(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
void deposit(double amount) {
balance += amount;
}
}
Account a = new Account("Ada", 100.0);
a.deposit(50.0);
System.out.println(a.balance); // 150.0Every field not explicitly initialized gets a type-appropriate default (0, 0.0, false, null) the moment the object is allocated, before the constructor body runs.
2Constructors and this
A constructor has no return type and shares the class's name. this refers to the current instance and is most commonly needed to disambiguate a field from a same-named parameter, or to call another constructor in the same class.
public class Point {
int x, y;
Point(int x, int y) {
this.x = x; // this.x = field, x = parameter — without `this`, x = x would be a no-op
this.y = y;
}
Point() {
this(0, 0); // constructor chaining — must be the FIRST statement in the constructor
}
}If you write no constructor at all, the compiler supplies a public no-arg default constructor. The moment you write any constructor yourself, that default disappears — a common surprise when adding a parameterized constructor later breaks code that relied on new Foo().
3Encapsulation and Access Modifiers
| Modifier | Same class | Same package | Subclass (different package) | Everywhere |
|---|---|---|---|---|
private | yes | no | no | no |
| (package-private, no modifier) | yes | yes | no | no |
protected | yes | yes | yes | no |
public | yes | yes | yes | yes |
public class BankAccount {
private double balance; // hidden — only accessible through methods below
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
balance += amount;
}
}Encapsulation isn't just "hide the field" — it's controlling the invariants: deposit can reject invalid input, but a public balance field could be set to anything by any caller.
4Static vs Instance Members
public class Counter {
static int totalCreated = 0; // ONE copy, shared across all instances, tied to the class
int id; // one copy PER instance
Counter() {
id = ++totalCreated;
}
}
Counter c1 = new Counter();
Counter c2 = new Counter();
System.out.println(Counter.totalCreated); // 2 — accessed via the class, not an instance
System.out.println(c1.id); // 1
System.out.println(c2.id); // 2Static methods cannot use this and cannot call instance (non-static) methods or fields directly — there is no receiving object to resolve them against.
5Method Overloading
Multiple methods can share a name if their parameter lists differ (in number, order, or type) — this is resolved entirely at compile time based on the declared (static) types of the arguments.
void print(int x) { System.out.println("int: " + x); }
void print(double x) { System.out.println("double: " + x); }
void print(String x) { System.out.println("String: " + x); }
print(5); // "int: 5"
print(5.0); // "double: 5.0"
print("hi"); // "String: hi"Overload resolution prefers, in order: an exact match, then a widening primitive conversion, then autoboxing, then varargs — which means adding an overload can silently change which method an existing call site resolves to.
6Instance Initializer Blocks and Field Initialization Order
Fields and instance initializer blocks run, in the order they appear in the source file, after default zeroing and before the constructor body:
public class Demo {
int a = 10;
{ System.out.println("initializer block, a=" + a); } // runs 2nd
int b = a + 5; // runs 3rd
Demo() {
System.out.println("constructor, b=" + b); // runs 4th
}
}7Packages and Imports
Packages namespace classes and map (by convention, not requirement) to directory structure. import is purely a compile-time convenience for using a shorter name — it has zero runtime effect.
package com.example.banking;
import java.util.List; // lets you write "List" instead of "java.util.List"Under the HoodHow it works internally▾
Every object on the heap begins with an object header, not just its fields. In HotSpot, a typical object's memory layout is: a mark word (8 bytes on 64-bit JVMs — holds the identity hash code, GC age bits, and the object's lock state for synchronized), followed by a class pointer (a reference to the object's Class metadata — normally compressed to 4 bytes on modern heaps under 32GB via "compressed oops"), followed by the object's actual declared fields in an implementation-defined order (not necessarily source order — the JVM is free to reorder/pack fields to minimize padding). This means even an "empty" object (class Empty {}) still consumes header bytes (commonly 16 bytes on a 64-bit JVM with compressed oops) before a single field is stored — object allocation is never free, no matter how small the class looks in source.
new is a two-step process at the bytecode level: new then invokespecial. Compiling new Account("Ada", 100.0) produces roughly: a new instruction (allocates zeroed memory for the object and pushes a reference to it), a dup (duplicate that reference on the operand stack), pushes for the constructor arguments, then invokespecial (calls the constructor on that already-allocated object). This is a real, important distinction from method calls: constructors are invoked with invokespecial — a direct, non-virtual dispatch resolved at compile time — never through the dynamic-dispatch mechanism (invokevirtual) that ordinary instance methods use, which is precisely why calling an overridable method from within a constructor (see Pitfall 3) can observe a subclass in a not-yet-fully-initialized state: the object already exists and has a type, but the subclass constructor's own field initializers haven't run yet.
Static members live in the Class object's storage, not per-instance. A static field is allocated once, as part of the class's metadata area (conceptually "on the Class object" — the actual memory region has changed across JVM versions, from PermGen pre-Java-8 to Metaspace since), created when the class is first loaded and initialized (triggered by first active use — first instantiation, first static method call, or first static field access), not when any instance is created. This is exactly why Counter.totalCreated in section 4 has a single, shared value regardless of how many Counter objects exist — instances never own a copy of it at all.
⚠️ Common Pitfalls
- Exposing a mutable field's reference through a plain getter —
public List<String> getItems() { return items; }hands callers a live reference to your internal list; they can mutate it and bypass every invariant your class was supposed to enforce. Return a copy or an unmodifiable view when encapsulation matters. - Forgetting the default constructor disappears once you write any constructor — adding
Account(String owner, double balance)silently breaks everynew Account()call elsewhere in the codebase unless you also keep a no-arg constructor. - Calling an overridable instance method from a constructor — the subclass's fields aren't initialized yet when the superclass constructor runs (per the Under the Hood note above), so an override that reads subclass state there can observe defaults (
0/null) instead of the values you expect. - Overload resolution surprises with autoboxing and widening — a call like
print(5)prefers an exactintoverload over adoubleor boxedIntegeroverload, and adding a new, more-specific overload later can silently redirect existing call sites without any compile error. - Confusing package-private (no modifier) with
protected— package-private grants access only within the same package, regardless of inheritance;protectedadditionally grants access to subclasses in other packages. Mixing these up creates accidental encapsulation leaks or unexpectedAccessDenied-style compile errors.
📋 Quick Reference
// class + constructor + this
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
Point() { this(0, 0); } // constructor chaining, must be first statement
}
// access modifiers, most to least restrictive
private // same class only
(none) // + same package
protected // + subclasses (any package)
public // everywhere
// static: one per class, not per instance
class C { static int shared; int perInstance; }
// overloading resolved at compile time by parameter types
void f(int x) {}
void f(double x) {}
// field init order: defaults -> field initializers/instance blocks (source order) -> constructor body🎤 Interview Q&A
Q1What are the four OOP principles?core▾
Encapsulation (hide state behind methods), abstraction (expose essential behaviour), inheritance (reuse via is-a), polymorphism (one interface, many implementations).
- Which matters most in practice? Encapsulation and polymorphism via interfaces.
Q2Abstract class vs interface in Java?core▾
Abstract classes can have state, constructors and partial implementations, but single inheritance. Interfaces define contracts, support multiple implementation, and can have default/static/private methods (no instance state).
- When abstract class? Shared state and template-method behaviour.
Q3Explain the equals/hashCode contract.core▾
If two objects are equal, they must have the same hash code; override both together. Otherwise HashMap/HashSet behave incorrectly (duplicates, failed lookups).
- Easy way? Records or IDE-generated/Objects.hash.
Q4What do access modifiers mean?core▾
private (class only), package-private (no modifier, same package), protected (package + subclasses), public (everyone). Default to the most restrictive.
- Modules? JPMS adds module-level encapsulation.
Q5What does final mean in different places?senior▾
Final variable: can't be reassigned; final method: can't be overridden; final class: can't be extended. Sealed classes (Java 17) restrict which classes may extend.
- Sealed + records benefit? Exhaustive pattern matching in switch.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.