Study Notes
Language Fundamentalscore

OOPs Concepts

Encapsulation, inheritance, polymorphism, abstraction, interfaces, abstract classes, access modifiers, and the SOLID principles in Java.

35 min read·verified Java 21·reviewed 2026-08-29

After this you should be able to

  • Model responsibilities with interfaces
  • Choose composition or inheritance deliberately
InheritancePolymorphismInterfaces

My self-contained notes on Object-Oriented Programming in Java: the four pillars, the supporting keywords, runnable code, and a practice lab. Everything I need in one place.

Encapsulation Inheritance Abstraction Polymorphism static · final · this · super
Legend + marks my own enrichment or a correction — extra context beyond the core material, verified true. Everything else is core syllabus.

Section I#

Notebook Summary — copy by hand

Terse, telegraphic, high-density. Lead with the term → then the rule. Tables for comparisons, doodles for shapes.

Classes, Objects & Constructors#

                CLASS    = blueprint / template (user-defined data type)
OBJECT   = instance of class  (new ClassName())
  has -> State (field values) + Behavior (methods) + Identity (JVM ref)
FIELDS   = data        METHODS = behavior
access   -> dot operator:  obj.field / obj.method()

CONSTRUCTOR = special method, runs on `new`, initializes object
  rules -> same name as class ; NO return type (not even void)
  types -> Default (no-arg, auto)  |  Parameterized
  can be OVERLOADED (many constructors, diff params)
  !! write ANY constructor -> default no-arg NOT auto-generated
this() -> call another constructor (must be 1st line)
            
doodle One [cookie-cutter] → stamps out 3 [cookies]. Cutter = class, cookies = objects (same shape, diff decoration).

Keywords: this / static / super#

                this   = current object ("me")
  this.x = x   -> field vs param name clash
static = belongs to CLASS not object ; 1 shared copy
  access ClassName.member (no object) ; e.g. Math.PI, Math.max()
  static method -> CANNOT use `this` or touch instance vars
super  = parent class
  super.method()  -> parent's version
  super()         -> parent constructor (1st line; auto-inserted)
            
doodle 3 [boxes] all wired to ONE shared [count] bubble = static (one copy). Each box has its own [name] bubble = instance.

Pillar 1 — Encapsulation#

                = bundle data+methods in 1 unit  +  HIDE data from outside
how -> fields `private` ; access via public getters/setters
why -> validation, security, controlled access
real -> ArrayList/HashMap: use .add()/.get(), storage hidden
            
ModifierClassPkgSubWorld
public ✓✓✓✓
protected ✓✓✓✗
default ✓✓✗✗
private ✓✗✗✗
doodle [pill capsule]: hard shell outside, powder inside = public methods wrap private data.

Pillar 2 — Inheritance#

                child ACQUIRES parent's fields+methods   (keyword: extends)
why -> reuse code + enable polymorphism
TYPES: Single A->B | Multilevel A->B->C | Hierarchical A->B,A->C
       Multiple A,B->C  = X via classes / OK via interfaces
IS-A  -> inheritance   (Dog IS-A Animal -> extends)
HAS-A -> composition   (Car HAS-A Engine -> Engine field)
tip -> "favor composition over inheritance"
            
doodle [Animal] box on top, arrows DOWN to [Dog] [Cat] [Cow] = hierarchical inheritance.

Pillar 3 — Abstraction#

                = hide complexity, show only what's needed (WHAT not HOW)
real -> ATM: buttons visible, wiring hidden
2 tools -> abstract class  |  interface
            
TraitAbstract classInterface
Methodsabstract + concreteabstract (+default 8+)
Fieldsanyconst only (static final)
Constructoryesno
Multiplesingle onlymany
Keyword extends implements
Use whenrelated + shared stateunrelated + shared ability
                abstract cannot combine with -> final / static / private
cannot instantiate abstract class
Encap vs Abstr -> Encap = HOW hide (private) ; Abstr = WHAT show
            

Pillar 4 — Polymorphism#

                = "many forms" : one interface, many implementations
            
OverloadingOverriding
Whencompile timeruntime
Wheresame classparent → child
ParamsMUST differMUST be same
Returncan differsame / covariant
akastatic polydynamic poly
                OVERLOAD -> 3 ways: diff number / diff type / diff order of params
            !! cannot overload by return type alone
OVERRIDE -> @Override ; access same-or-wider ; NOT static/final/private
KEY LINE -> Animal a = new Cat();
            ref type (Animal) = what you CAN call
            obj type (Cat)    = what ACTUALLY runs
            
doodle One [wall socket] accepting 3 diff [plugs] = same interface, many forms.
recall hook overLOAD = LOAD more versions (same class). overRIDE = child RIDES over parent.

The final keyword #

                final VAR    -> constant, no reassign
final METHOD -> cannot override
final CLASS  -> cannot extend   (e.g. String)
Math.PI = public static final  -> class-level constant
final + abstract = IMPOSSIBLE (contradictory)
            

Interview quick-fire#

QA
Instantiate abstract class?No
Interface constructor?No
Override static method?No (hidden, not overridden)
Constructor final?No
Multiple inheritance via classes?No (Diamond)
...via interfaces?Yes
Overload constructors?Yes
Overload main()?Yes (JVM calls String[] only)
== vs .equals() ref/address vs content/value
                4 PILLARS mnemonic -> "A PIE"
  Abstraction · Polymorphism · Inheritance · Encapsulation
1-liners:
  Encap  = wrap + protect data
  Inher  = child gets parent's stuff
  Abstr  = hide complexity, show interface
  Poly   = one interface, many forms
final = brake pedal -> stops inherit / override / reassign
            

Section II#

Detailed Study Notes

1. Classes & Objects #

A class is a blueprint; an object is a concrete thing built from that blueprint.

A class is a user-defined data type that groups together two kinds of members: fields (the data — also called attributes or state) and methods (the behavior — functions that belong to the class). The class itself is just a description. It doesn't occupy meaningful memory for data until you create an object from it using the new operator. That object is called an instance.

The cleanest analogy: a class is a cookie cutter, and each object is a cookie. The cutter defines the shape; each cookie is a separate, real thing you can decorate differently. Or: a class is an architect's blueprint, and each object is an actual house built from it — you can't live in the blueprint.

Every object carries three things:

  • State — the current values held in its fields (color = "Red" ).
  • Behavior — what it can do, exposed through its methods (accelerate() ).
  • Identity — a unique reference the JVM assigns so it can tell two objects apart even if their state is identical. + Practically this is the object's reference (think of it as its address); it's what == compares.

You reach an object's members with the dot operator: myCar.color reads a field, myCar.accelerate() invokes a method.

Why it matters This is the foundation under all four pillars. "Encapsulation," "inheritance," and the rest are just rules about how classes and objects relate. If the class/object distinction is fuzzy, everything above it stays fuzzy.
Common pitfall Confusing the class with the object. Car is the type; myCar is the value. You can have many objects of one class, each with its own independent state.

→ Code: Sample 1 — Car class & object

2. Constructors #

A constructor is a special method that initializes a new object the moment it's created.

When you write new Rectangle(10, 20), the JVM allocates memory for the object and then immediately runs a constructor to set its initial state. Constructors follow two strict rules: the name must match the class name exactly, and they have no return type — not even void. (If you accidentally give it a return type, Java treats it as an ordinary method, not a constructor — a classic silent bug. + )

There are two kinds. A default (no-argument) constructor is supplied automatically by Java if you write none at all; it sets fields to their defaults (0, null, false ). A parameterized constructor takes arguments so the caller can set initial state. A class can have several constructors as long as their parameter lists differ — this is constructor overloading, the same mechanism as method overloading.

The constructor is also the natural place for validation — reject bad input before the object ever exists in a broken state:

Critical gotcha The default no-arg constructor is auto-generated only if you declare no constructors. The instant you write any constructor (e.g. a parameterized one), Java stops providing the default. If you still need new Rectangle() with no args, you must add that no-arg constructor yourself — otherwise it won't compile.
Interview angle "Can a constructor be final, static, or abstract?" — No to all three. + Constructors aren't inherited, so final /abstract (which are about overriding/subclassing) are meaningless, and a static constructor would contradict the whole point of initializing a specific instance.

→ Code: Sample 2 — Constructors & validation

3. The this keyword #

this is a reference to the current object — "me, the object whose method is running right now."

Its most common job is resolving a name clash. When a constructor parameter has the same name as a field (a very normal situation), this.name = name means "assign the parameter name to this object's field name." Without this, both names refer to the parameter and the field never gets set.

Other uses: this.someMethod() calls another method on the same object; this() calls another constructor of the same class (constructor chaining — and it must be the very first statement); and you can pass this as an argument to hand the current object to another method.

Why it matters Without this, you'd be forced to give parameters different names from fields, which makes code noisier. It also unlocks constructor chaining and fluent/builder-style APIs.
Interview angle + "What is this inside a static method?" — It doesn't exist. Static methods belong to the class, not any object, so there is no "current object" to reference. Trying to use this there is a compile error.

→ Code: Sample 3 — this keyword

4. The static keyword #

static means the member belongs to the class itself, not to any individual object — there is exactly one shared copy.

A normal (instance) field gets its own copy in every object. A static field exists once, in the class, and is shared by every instance. Classic use cases: a shared constant like a company name, or a counter that tracks how many objects have been created. A static method is behavior that doesn't depend on any particular object's data, so it can be called without creating an instance — you call it on the class: ClassName.method().

The standard library is full of examples. Math.PI is public static final double — one value, class-level, never created per-object. Math.max(), Math.sqrt(), and friends are static methods: you never write new Math().

staticinstance (non-static)
Belongs tothe classeach object
Copies in memoryone, sharedone per object
Accessed via ClassName.x object.x
Can accessonly static membersstatic + instance
Has this? noyes
Common pitfall / interview trap "Can a static method access instance variables directly?" — No. A static method has no this, so there's no object whose instance fields it could read. It can only touch other static members (or operate on an object explicitly passed in as a parameter + ).
Trade-off + Static state is effectively global state. It's convenient for counters and utilities, but shared mutable static fields are hard to test and unsafe across threads. Prefer static for true constants and pure helper functions; be cautious with mutable static data.

→ Code: Sample 4 — static counter & method

5. The super keyword #

super is to the parent class what this is to the current object.

Inside a child class, super.method() calls the parent's version of a method (useful when you override a method but still want the parent's behavior plus a bit more), super.field reaches a parent field that a child field is shadowing, and super(args) calls the parent's constructor.

A key detail about object construction: super() must be the first statement in a child constructor, and + if you don't write it, the compiler inserts a call to the parent's no-arg super() for you. This is why a parent's constructor always runs before the child's — construction flows from the top of the hierarchy down. (If the parent has no no-arg constructor, you must call super(args) explicitly, or it won't compile. + )

Why it matters Overriding usually means "extend the parent's behavior," not "throw it away." super.method() lets you reuse the parent's logic and add to it instead of duplicating it.

→ Code: Sample 5 — super & constructor order

6. Encapsulation — Pillar 1 #

Encapsulation is bundling data and the methods that operate on it into one unit, while restricting direct outside access to that data.

There are two ideas packed into the word. First, bundling: the fields and the methods that use them live together in a single class. Second, data hiding: the fields are marked private so nothing outside the class can read or write them directly. The outside world interacts only through a controlled public surface — typically getters (read) and setters (write).

Picture a medicine capsule: a hard shell wraps the powder inside. You swallow the capsule whole; you never handle the powder. The class is the shell, the private fields are the powder, the public methods are the surface you're allowed to touch.

The payoff is control. If balance is public, anyone can write account.balance = -5000 and corrupt your object. If it's private and the only way in is deposit() /withdraw(), every change runs through your validation. You can also change how data is stored internally without breaking callers, because they only ever saw the methods.

Why it matters It's the difference between an object that protects its own invariants and a bag of public variables anyone can corrupt. Encapsulated objects are safer, easier to change, and easier to reason about.
Real-world connection Java's own Collections (ArrayList, HashMap ) are textbook encapsulation: you call .add() and .get() and never know — or need to know — how the data is stored internally.
Trade-off + Don't cargo-cult getters/setters onto every field. A setter that just blindly assigns is no better than a public field. Encapsulation earns its keep when the methods add something: validation, derived values, immutability, or a stable API over a changing internal representation.

→ Code: Sample 6 — BankAccount encapsulation

7. Access Modifiers #

Access modifiers control who can see a class member; they're the mechanism that makes data hiding possible.

Java has four levels, from most open to most restricted: public (everyone), protected (same package + subclasses anywhere), default / package-private (same package only — written by using no keyword), and private (same class only).

ModifierSame classSame packageSubclass (diff pkg)Everywhere
public ✓✓✓✓
protected ✓✓✓✗
default (none) ✓✓✗✗
private ✓✗✗✗
Interview angle The favorite trick question: "Can a subclass in a different package access a protected member?" — Yes. That's exactly the gap between protected and default. + A subtle follow-up: a subclass can access a protected member it inherits, but not a protected member on an unrelated instance of the parent type from another package.
Best practice + Default to private for fields and only widen access when you have a reason. The narrowest workable access keeps your class's internals free to change.

8. Inheritance — Pillar 2 #

Inheritance lets a new class acquire the fields and methods of an existing class, then add to or change them.

The existing class is the parent (superclass / base class); the new one is the child (subclass / derived class). The keyword is extends. The child automatically gets everything non-private from the parent and can add new members or override inherited behavior. Think of a generic smartphone (parent) and an iPhone (child): the iPhone has everything a smartphone has, plus Face ID and AirDrop.

There are four shapes of inheritance:

TypeShapeJava via classes?
SingleA → B✓
MultilevelA → B → C✓
HierarchicalA → B, A → C✓
MultipleA, B → C✗ (use interfaces)

Java deliberately forbids multiple inheritance through classes to avoid the Diamond Problem: if class C inherited from both A and B, and both define the same method, C wouldn't know which to use. + Interfaces sidestep this — a class can implement many interfaces, and if two interfaces supply the same default method, the compiler forces you to override it and pick.

IS-A vs HAS-A — the design test Ask which sentence is true. "A Dog IS-A Animal" → inheritance (Dog extends Animal ). "A Car HAS-A Engine" → composition (give Car an Engine field). Getting this right is most of good OO design.
Trade-off / best practice "Favor composition over inheritance." + Inheritance tightly couples child to parent — a change in the parent can silently break every subclass (the "fragile base class" problem). Composition (holding an object instead of extending it) is more flexible and easier to test. Use inheritance for genuine IS-A relationships, composition for everything else.

→ Code: Sample 7 — Inheritance & extends

9. Abstraction — Pillar 3 #

Abstraction means hiding implementation complexity and exposing only what the user needs — focus on what an object does, not how.

An ATM is the perfect mental model. You see "insert card, enter PIN, withdraw cash." Hidden behind that simple interface: PIN verification, balance checks, account deduction, the cash-dispensing hardware. You operate the machine without understanding any of it. That's abstraction — a clean surface over hidden complexity.

Java offers two tools to express abstraction. An abstract class is a partially built template: it can mix fully written (concrete) methods with abstract methods that have no body and must be filled in by subclasses. You can't instantiate it directly — it only makes sense once a subclass completes it. An interface is a pure contract: traditionally just method signatures with no implementation at all, describing a capability that any class can promise to provide.

An abstract method is declared with the abstract keyword and ends in a semicolon — no curly braces, no body. Any class containing even one abstract method must itself be declared abstract. A subclass must either implement all inherited abstract methods or be declared abstract too.

Why it matters Abstraction lets you program to a contract instead of a concrete implementation. Code that depends on an abstract Employee works unchanged whether the real object is a FullTimeEmployee or an HourlyEmployee — and that's exactly what makes polymorphism possible.
Pitfall abstract cannot be combined with final (final forbids subclassing, abstract requires it), static, or private (an abstract method must be overridable by a subclass, which private prevents).
Encapsulation vs Abstraction — don't mix them up Encapsulation is about how you hide data (private fields + accessor methods) — its goal is data protection. Abstraction is about what you expose (a simplified interface) — its goal is reducing complexity for the user. One-liner: encapsulation hides data; abstraction hides complexity. Encapsulation is the mechanism, abstraction is the design intent.

→ Code: Sample 8 — Abstract Employee/Payroll · Sample 9 — Interfaces

10. Abstract Class vs Interface #

Use an abstract class for closely related types that share state and code; use an interface for a capability unrelated types can all provide.

FeatureAbstract classInterface
Methodsabstract + concrete abstract (+ default /static since Java 8)
Fieldsany kind, mutable constants only (implicitly public static final )
Constructoryesno
Inheritanceextend one onlyimplement many
Method accessany modifierpublic (abstract methods)
Best forrelated classes + shared stateunrelated classes + shared behavior

Decision rule: ask "does this thing need shared state and partially shared code among related classes?" → abstract class. "Is this just a capability that otherwise-unrelated classes can opt into?" → interface. A Dog and Cat share Animal state and some behavior → abstract class. A Duck and an Airplane have nothing in common except both can fly → Flyable interface.

Modern Java note + Since Java 8, interfaces can have default methods (with a body) and static methods, so the old "interfaces have zero implementation" line is no longer strictly true. The clean way to say it now: an abstract class can hold state (instance fields) and a constructor; an interface cannot. That, plus single-vs-multiple inheritance, is the real dividing line.

11. Polymorphism — Pillar 4 #

Polymorphism — "many forms" — lets one interface have many implementations, so the same call behaves differently depending on the object.

Think of a universal travel power socket: one socket shape accepts many plug types, and what happens depends on what you plug in. Same interface, different behavior. Java has two distinct flavors.

Compile-time (static) polymorphism — Method Overloading#

Several methods in the same class share a name but differ in their parameter list. The compiler picks the right one at compile time based on the arguments you pass. You can overload three ways: a different number of parameters, different types, or a different order of types. The classic motivation: instead of addTwo(), addThree(), … you just write several add() methods and let the compiler match.

Pitfall You cannot overload by changing only the return type. int add(int,int) and double add(int,int) won't compile — the compiler resolves overloads from the argument list alone and the return type doesn't disambiguate the call.

Runtime (dynamic) polymorphism — Method Overriding#

A child class provides its own version of a method already defined in the parent, with the same signature. Which version runs is decided at runtime based on the actual object's type, not the reference type. Mark overrides with @Override — it's optional but it makes the compiler catch typos (if your "override" doesn't actually match a parent method, you get an error instead of a silent new method).

Override rules: same name and parameters; return type same or a subtype (covariant); access modifier the same or wider (never narrower); and you cannot override static, final, or private methods. + A same-signature static method in a subclass isn't overriding at all — it's method hiding, resolved by reference type, not object type. This is a favorite trick question.

The line interviewers love Animal a = new Cat(); The reference type (Animal ) decides what methods you're allowed to call. The object type (Cat ) decides which implementation actually runs. So a.sound() compiles against Animal but executes Cat 's version. Reference = what you can call; object = what executes.
OverloadingOverriding
Lives insame classparent → child
Parametersmust differmust match
Return typecan differsame or covariant
Resolvedcompile timeruntime
Also calledstatic polymorphismdynamic polymorphism
Why it matters Overriding is what makes a collection of Animal references each "do their own thing" when you loop and call sound(). It's the engine behind extensible, plugin-style designs: add a new subclass and existing code uses it without changes.

→ Code: Sample 10 — Overloading · Sample 11 — Overriding

12. The final keyword #

final locks something down — it says "this cannot change or be extended."

It works three ways. On a variable, the value can't be reassigned after it's set — that's how you make a constant (final int MAX = 100 ). On a method, it can't be overridden by any subclass — useful when a method's behavior is critical and must not be altered. On a class, it can't be extended at all — String is final, which is part of why strings are safe to share freely.

When you see public static final together — as in Math.PI — that's the idiom for a class-level constant: one shared copy (static ), reachable from anywhere (public ), and unchangeable (final ).

Pitfall + For a final object reference, only the reference is frozen, not the object's contents. final List<Integer> list = new ArrayList<>(); still lets you call list.add(1) — you just can't point list at a different list. final freezes the binding, not the object's internal state.
Quick logic check final + abstract is a contradiction: abstract demands subclassing/overriding, final forbids it. The compiler rejects the combination.

→ Code: Sample 12 — final variants


Key Takeaways#

  • Class vs object is the bedrock: class = blueprint (fields + methods), object = instance with state, behavior, identity.
  • The four pillars — "A PIE": Abstraction (hide complexity), Polymorphism (one interface, many forms), Inheritance (child reuses parent), Encapsulation (wrap + protect data).
  • Encapsulation vs Abstraction: encapsulation hides data (the how); abstraction hides complexity (the what).
  • Inheritance vs Composition: use IS-A for extends, HAS-A for fields; favor composition when in doubt.
  • Abstract class vs Interface: abstract class = related types with shared state; interface = a capability many unrelated types can implement.
  • Polymorphism: overloading is compile-time (same class, different params); overriding is runtime (parent→child, same signature). Animal a = new Cat() — reference type gates what you can call, object type decides what runs.
  • Keywords: this = current object, super = parent, static = class-level shared, final = locked. Static methods have no this; super() /this() must be the first line of a constructor.
  • They all interlock: encapsulation needs methods → forms an abstraction; abstraction defines contracts → fulfilled differently via inheritance → invoked via polymorphism.

Section III#

Code Samples — runnable

Java 17 (any modern JDK 8+ works). Each: filename, run command, key-line comments, expected output. Plain javac /java, no external libraries.

1. Car — class & object #


class Car {
    String color;        // instance field (state)
    int speed;            // each Car gets its own copy
    void accelerate() {   // method (behavior)
        speed += 10;
    }
}

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();  // 'new' creates an object (instance)
        myCar.color = "Red";       // '.' accesses a field
        myCar.accelerate();          // '.' invokes a method
        System.out.println(myCar.color + " car, speed " + myCar.speed);
    }
}

            
Run · Expected output $ javac Main.java && java Main Red car, speed 10

+ Why: speed started at its default 0 (Java zero-initializes instance fields), then accelerate() added 10. A second new Car() would have its own independent speed.

2. Constructors & validation #


class Rectangle {
    double length, breadth;

    Rectangle(double length, double breadth) {  // parameterized constructor
        if (length >= 0 && breadth >= 0) {        // validate before storing
            this.length = length;                  // this.field = param
            this.breadth = breadth;
        } else {
            System.out.println("length & breadth must be >= 0");
        }
    }
    double area() { return length * breadth; }
}

public class Main {
    public static void main(String[] args) {
        Rectangle r1 = new Rectangle(10, 20);
        System.out.println("area = " + r1.area());
        Rectangle r2 = new Rectangle(10, -5); // fails validation; fields stay 0
        System.out.println("area = " + r2.area());
    }
}

            
Run · Expected output $ java Main.java area = 200.0 length & breadth must be >= 0 area = 0.0

+ Why: Because we declared a constructor, Java did not generate a no-arg one — new Rectangle() would now fail to compile. For r2, validation rejected the values so the fields kept their defaults (0.0), giving area 0.0. + Single-file source launch (java Main.java ) works on JDK 11+.

3. this keyword #


class Employee {
    String name;
    int id;

    Employee(String name) {        // param 'name' shadows field 'name'
        this(name, 0);                 // this(): call other constructor, MUST be 1st line
    }
    Employee(String name, int id) {
        this.name = name;             // without 'this', name=name does nothing
        this.id = id;
    }
    void print() { System.out.println(name + " #" + id); }
}

public class Main {
    public static void main(String[] a) {
        new Employee("Ada").print();
        new Employee("Lin", 7).print();
    }
}

            
Run · Expected output $ java Main.java Ada #0 Lin #7

+ Why: this(name, 0) chains to the two-arg constructor — constructor reuse. Move it below another statement and it won't compile: this() /super() must be first.

4. static counter & method #


class Counter {
    static int count = 0;     // ONE copy shared by all instances
    Counter() { count++; }       // every constructor call bumps the shared counter
    static int cube(int x) {     // static method: no object needed
        return x * x * x;
    }
}

public class Main {
    public static void main(String[] a) {
        new Counter(); new Counter(); new Counter();
        System.out.println(Counter.count);       // access via CLASS, not object
        System.out.println(Counter.cube(4));    // call static method on class
    }
}

            
Run · Expected output $ java Main.java 3 64

+ Why: The three constructor calls all increment the same count, so it ends at 3. If cube() tried to read a non-static field, it wouldn't compile — a static method has no this.

5. super & constructor order #


class Animal {
    Animal() { System.out.println("Animal constructor"); }
    void sound() { System.out.println("Some generic sound"); }
}
class Dog extends Animal {
    Dog() {
        super();                       // calls Animal() — implicit even if omitted
        System.out.println("Dog constructor");
    }
    @Override void sound() {
        super.sound();                  // reuse parent behavior...
        System.out.println("Woof!");     // ...then add to it
    }
}
public class Main {
    public static void main(String[] a) { new Dog().sound(); }
}

            
Run · Expected output $ java Main.java Animal constructor Dog constructor Some generic sound Woof!

+ Why: Parent constructor runs first (top-down construction), even though we called new Dog(). Then sound() shows super.sound() reusing the parent's logic before adding its own.

6. Encapsulation — BankAccount #


class BankAccount {
    private double balance;        // hidden — no direct outside access
    public double getBalance() { return balance; }   // controlled read
    public void deposit(double amt) {
        if (amt <= 0) { System.out.println("Amount must be > 0"); return; }
        balance += amt;
    }
    public void withdraw(double amt) {
        if (amt >balance) { System.out.println("Insufficient funds"); return; }
        balance -= amt;
    }
}
public class Main {
    public static void main(String[] a) {
        BankAccount acc = new BankAccount();
        // acc.balance = -5000;  // COMPILE ERROR: balance is private
        acc.deposit(100);
        acc.withdraw(30);
        acc.withdraw(999);
        System.out.println("Balance: " + acc.getBalance());
    }
}

            
Run · Expected output $ java Main.java Insufficient funds Balance: 70.0

+ Why: Every change routes through validated methods, so the object can never enter an invalid state. The commented line shows what encapsulation prevents.

7. Inheritance — extends #


class CreditCard {
    String number;
    void pay(double amt) { System.out.println("Paid " + amt); }
}
class RewardsCard extends CreditCard {   // inherits number + pay()
    int points;
    void redeem(int p) { points -= p; System.out.println("Redeemed " + p); }
}
public class Main {
    public static void main(String[] a) {
        RewardsCard c = new RewardsCard();
        c.points = 500;
        c.pay(99.0);     // inherited from CreditCard
        c.redeem(200);    // added by RewardsCard
        System.out.println("Points left: " + c.points);
    }
}

            
Run · Expected output $ java Main.java Paid 99.0 Redeemed 200 Points left: 300

+ Why: RewardsCard never declares pay() or number, yet can use them — that's inheritance eliminating duplicate code.

8. Abstraction — abstract class (Payroll) #


abstract class Employee {
    String name;
    Employee(String name) { this.name = name; }

    abstract double calculateSalary();   // no body — subclass MUST implement
    void printInfo() {                     // concrete shared method
        System.out.println(name + " earns " + calculateSalary());
    }
}
class FullTime extends Employee {
    double monthly;
    FullTime(String n, double m) { super(n); monthly = m; }
    double calculateSalary() { return monthly; }
}
class Hourly extends Employee {
    double rate; int hours;
    Hourly(String n, double r, int h) { super(n); rate = r; hours = h; }
    double calculateSalary() { return rate * hours; }
}
public class Main {
    public static void main(String[] a) {
        Employee[] payroll = { new FullTime("Ada", 5000), new Hourly("Lin", 40, 120) };
        for (Employee e : payroll) e.printInfo();  // polymorphism in action
    }
}

            
Run · Expected output $ java Main.java Ada earns 5000.0 Lin earns 4800.0

+ Why: new Employee(...) is illegal (abstract). The loop holds Employee references but each calculateSalary() runs the subclass's version — abstraction + polymorphism together.

9. Interfaces — multiple implementation #


interface Flyable { void fly();  }   // methods implicitly public abstract
interface Swimmable { void swim(); }

class Duck implements Flyable, Swimmable {  // many interfaces — allowed
    public void fly()  { System.out.println("Duck flies"); }
    public void swim() { System.out.println("Duck swims"); }
}
public class Main {
    public static void main(String[] a) {
        Duck d = new Duck();
        d.fly(); d.swim();
        Flyable f = d;   // treat Duck as just a Flyable
        f.fly();
    }
}

            
Run · Expected output $ java Main.java Duck flies Duck swims Duck flies

+ Why: Implementing two interfaces is Java's answer to multiple inheritance. Overriding methods from an interface must be public (interface methods are implicitly public).

10. Overloading (compile-time) #


class Calc {
    int add(int a, int b)            { return a + b; }            // 2 ints
    int add(int a, int b, int c)     { return a + b + c; }        // 3 ints (diff count)
    double add(double a, double b)   { return a + b; }            // diff types
    void show(String s, int n) { System.out.println(s + n); }       // (String,int)
    void show(int n, String s) { System.out.println(n + s); }       // diff order
}
public class Main {
    public static void main(String[] a) {
        Calc c = new Calc();
        System.out.println(c.add(2, 3));        // -> int version
        System.out.println(c.add(2, 3, 4));     // -> 3-arg version
        System.out.println(c.add(2.5, 3.5));    // -> double version
        c.show("x=", 9);
    }
}

            
Run · Expected output $ java Main.java 5 9 6.0 x=9

+ Why: The compiler picks each overload from the argument types at compile time. Adding double add(int,int) would fail — return type alone can't distinguish overloads.

11. Overriding (runtime) + reference vs object #


class Animal {
    void sound() { System.out.println("Some sound"); }
}
class Cat extends Animal {
    @Override void sound() { System.out.println("Meow"); }   // same signature, new body
}
public class Main {
    public static void main(String[] a) {
        Animal x = new Animal();
        Animal y = new Cat();    // ref type Animal, object type Cat
        x.sound();                  // Some sound
        y.sound();                  
        
        // Meow  <- decided at RUNTIME by object type 
    }
}
            
Run · Expected output $ java Main.java Some sound Meow

+ Why: Even though y is declared Animal, the JVM dispatches to Cat.sound() because the actual object is a Cat. The reference type only limits which methods you may call; the object type decides which body runs.

12. final — three uses #


final class Constants {            // final class: cannot be extended
    static final double PI = 3.14159;  // final var: constant, no reassign
    final void show() {              // final method: cannot be overridden
        System.out.println("PI = " + PI);
    }
}
public class Main {
    public static void main(String[] a) {
        new Constants().show();
        // Constants.PI = 3.0;  // COMPILE ERROR: cannot assign a final variable
        final int[] arr = {1, 2 };
        arr[0] = 99;                  // OK! final freezes the reference, not contents
        System.out.println(arr[0]);
    }
}

            
Run · Expected output $ java Main.java PI = 3.14159 99

+ Why: The array line is the key insight — final on a reference type stops re-pointing the variable, but the object it points to is still mutable.

Section IV#

Hands-On Lab — practice
One-time Setup
                    
# 1. Check the JDK is installed (need 11+ for `java File.java`; 8+ otherwise)
java -version
javac -version

# 2. Make a working folder
mkdir oop-lab && cd oop-lab

# 3. Put each exercise in its own Main.java, then:
javac Main.java && java Main
# or single-file shortcut (JDK 11+):
java Main.java
                    
                
Warm-up · 1

Build a class and create two objects#

Goal: Confirm you understand class vs object and per-object state.

  1. Write a class Book with fields title (String) and pages (int).
  2. Add a method summary() that prints "<title> has <pages> pages".
  3. In main, create two different Book objects with different values and call summary() on each.
Predict, then verify: If you set pages on only the first book, what does the second book's pages print before you assign it? Why?
Solution

class Book {
    String title;
    int pages;
    void summary() { System.out.println(title + " has " + pages + " pages"); }
}
public class Main {
    public static void main(String[] a) {
        Book b1 = new Book(); b1.title = "Java"; b1.pages = 300;
        Book b2 = new Book(); b2.title = "OOP";   // pages left default
        b1.summary();
        b2.summary();   // pages prints 0 — each object has its OWN fields
    }
}

Expected output Java has 300 pages OOP has 0 pages

+ b2.pages is 0: instance fields default to zero/null/false and each object is independent.

Warm-up · 2

Constructor + this + validation #

Goal: Initialize state safely through a constructor.

  1. Give Book a constructor Book(String title, int pages) using this.title = title etc.
  2. Reject negative pages (print a message and set pages to 0 instead).
  3. Create one valid and one invalid book; print both summaries.
Predict, then verify: After you add this constructor, will new Book() still compile? Why or why not?
Solution

class Book {
    String title; int pages;
    Book(String title, int pages) {
        this.title = title;
        if (pages <0) { System.out.println("pages can't be negative"); this.pages = 0; }
        else this.pages = pages;
    }
    void summary() { System.out.println(title + " has " + pages + " pages"); }
}
public class Main {
    public static void main(String[] a) {
        new Book("Java", 300).summary();
        new Book("Bad", -5).summary();
    }
}

Expected output Java has 300 pages pages can't be negative Bad has 0 pages

+ new Book() now fails to compile — declaring any constructor removes the auto default. Add an explicit Book(){} if you need it.

Core · 3

Encapsulate a bank account#

Goal: Make data private and guard all changes.

  1. Class Account with private double balance.
  2. Public deposit(amt) (reject ≤ 0) and withdraw(amt) (reject >balance).
  3. Public getBalance(). In main, try a bad withdraw and a good deposit.
Predict, then verify: Try writing acc.balance = 1000; in main. What happens at compile time, and what does that prove about encapsulation?
Solution

class Account {
    private double balance;
    public void deposit(double amt) {
        if (amt <= 0) { System.out.println("bad deposit"); return; }
        balance += amt;
    }
    public void withdraw(double amt) {
        if (amt >balance) { System.out.println("insufficient"); return; }
        balance -= amt;
    }
    public double getBalance() { return balance; }
}
public class Main {
    public static void main(String[] a) {
        Account acc = new Account();
        acc.withdraw(50);     // insufficient (balance 0)
        acc.deposit(200);
        acc.withdraw(50);
        System.out.println(acc.getBalance());
    }
}

Expected output insufficient 150.0

+ acc.balance = 1000; won't compile (balance has private access ). That's the proof: the only path to the data is your validated methods.

Core · 4

Inheritance with super #

Goal: Reuse parent behavior and extend it.

  1. Class Vehicle with method start() printing "Engine on".
  2. Class Car extends Vehicle; override start() to call super.start() then print "Car ready".
  3. Create a Car and call start().
Predict, then verify: If you remove super.start(), what disappears from the output — and what does that tell you about overriding vs reusing?
Solution

class Vehicle {
    void start() { System.out.println("Engine on"); }
}
class Car extends Vehicle {
    @Override void start() {
        super.start();                     // reuse parent
        System.out.println("Car ready");     // extend it
    }
}
public class Main {
    public static void main(String[] a) { new Car().start(); }
}

Expected output Engine on Car ready

+ Remove super.start() and "Engine on" vanishes — overriding replaces the method; super is how you keep the parent's part.

Core · 5

Polymorphism over an abstract type#

Goal: Loop over a parent-typed array and watch each object behave differently.

  1. Abstract class Shape with abstract method double area().
  2. Circle (radius) and Square (side) extend it and implement area().
  3. Put one of each in a Shape[], loop, and print each area.
Predict, then verify: The array's type is Shape, but which area() runs for each element — and at compile time or runtime is that decided?
Solution

abstract class Shape { abstract double area(); }
class Circle extends Shape {
    double r; Circle(double r){ this.r = r; }
    double area() { return Math.PI * r * r; }
}
class Square extends Shape {
    double s; Square(double s){ this.s = s; }
    double area() { return s * s; }
}
public class Main {
    public static void main(String[] a) {
        Shape[] shapes = { new Circle(2), new Square(3) };
        for (Shape sh : shapes)
            System.out.printf("%.2f%n", sh.area());
    }
}

Expected output 12.57 9.00

+ Decided at runtime by each object's real type (dynamic dispatch). This is why you can add a new Shape subclass and the loop just works.

Challenge · 6

Interface + overloading + the reference/object trap#

Goal: Combine pillars and nail the trickiest interview point.

  1. Interface Payable with void pay(double amt).
  2. Class Wallet implements Payable; also overload pay() with pay(double amt, String note).
  3. Override pay(double) to print the amount; the overload prints amount + note.
  4. In main: assign a Wallet to a Payable reference. Call pay(10) through it. Then try calling the 2-arg overload through the Payable reference.
Predict, then verify: Through the Payable reference, can you call pay(10, "tip")? Why or why not? (Hint: reference type vs object type.)
Solution

interface Payable { void pay(double amt); }

class Wallet implements Payable {
    public void pay(double amt) {               // overrides interface method
        System.out.println("Paid " + amt);
    }
    public void pay(double amt, String note) {   // OVERLOAD (extra param)
        System.out.println("Paid " + amt + " (" + note + ")");
    }
}
public class Main {
    public static void main(String[] a) {
        Payable p = new Wallet();   // ref type Payable, object type Wallet
        p.pay(10);                    // OK: pay(double) is in the interface
        // p.pay(10, "tip");          // COMPILE ERROR: not declared in Payable
        ((Wallet) p).pay(10, "tip"); // works after casting to the real type
    }
}

Expected output Paid 10.0 Paid 10.0 (tip)

+ The reference type (Payable ) gates which methods are visible — and the overload isn't in the interface, so the direct call won't compile. Casting to Wallet reveals it. Meanwhile pay(10) runs Wallet 's body via dynamic dispatch. This single exercise ties together interfaces, overloading, overriding, and reference-vs-object.

Self-check rubric — how to know you've got it
  • You can explain class vs object and why two objects have independent state (Ex 1).
  • You can predict that adding a constructor removes the default no-arg one (Ex 2).
  • You can state why a private field can't be assigned from outside, and route all changes through methods (Ex 3).
  • You know super.x() reuses the parent and that overriding replaces unless you call super (Ex 4).
  • You can say, unprompted, that overriding is resolved at runtime by the object's real type (Ex 5).
  • You can explain why an overload not in the interface is invisible through the interface reference, and fix it with a cast (Ex 6).
Hit all six without peeking at solutions → you're interview-ready on Java OOPs fundamentals.