OOPs Concepts
Encapsulation, inheritance, polymorphism, abstraction, interfaces, abstract classes, access modifiers, and the SOLID principles in Java.
After this you should be able to
- Model responsibilities with interfaces
- Choose composition or inheritance deliberately
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.
Section I#
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)
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)
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
| Modifier | Class | Pkg | Sub | World |
|---|---|---|---|---|
public
| ✓ | ✓ | ✓ | ✓ |
protected
| ✓ | ✓ | ✓ | ✗ |
| default | ✓ | ✓ | ✗ | ✗ |
private
| ✓ | ✗ | ✗ | ✗ |
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"
Pillar 3 — Abstraction#
= hide complexity, show only what's needed (WHAT not HOW)
real -> ATM: buttons visible, wiring hidden
2 tools -> abstract class | interface
| Trait | Abstract class | Interface |
|---|---|---|
| Methods | abstract + concrete | abstract (+default 8+) |
| Fields | any | const only (static final) |
| Constructor | yes | no |
| Multiple | single only | many |
| Keyword |
extends
|
implements
|
| Use when | related + shared state | unrelated + 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
| Overloading | Overriding | |
|---|---|---|
| When | compile time | runtime |
| Where | same class | parent → child |
| Params | MUST differ | MUST be same |
| Return | can differ | same / covariant |
| aka | static poly | dynamic 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
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#
| Q | A |
|---|---|
| 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#
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.
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:
new Rectangle()
with no args, you must add that no-arg constructor yourself — otherwise it won't compile.
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.
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.
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().
| static | instance (non-static) | |
|---|---|---|
| Belongs to | the class | each object |
| Copies in memory | one, shared | one per object |
| Accessed via |
ClassName.x
|
object.x
|
| Can access | only static members | static + instance |
Has this?
| no | yes |
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 +
).
→ 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. +
)
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.
ArrayList, HashMap
) are textbook encapsulation: you call .add()
and .get()
and never know — or need to know — how the data is stored internally.
→ 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).
| Modifier | Same class | Same package | Subclass (diff pkg) | Everywhere |
|---|---|---|---|---|
public
| ✓ | ✓ | ✓ | ✓ |
protected
| ✓ | ✓ | ✓ | ✗ |
| default (none) | ✓ | ✓ | ✗ | ✗ |
private
| ✓ | ✗ | ✗ | ✗ |
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.
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:
| Type | Shape | Java via classes? |
|---|---|---|
| Single | A → B | ✓ |
| Multilevel | A → B → C | ✓ |
| Hierarchical | A → B, A → C | ✓ |
| Multiple | A, 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.
Dog extends Animal
). "A Car HAS-A
Engine" → composition (give Car
an Engine
field). Getting this right is most of good OO design.
→ 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.
Employee
works unchanged whether the real object is a FullTimeEmployee
or an HourlyEmployee
— and that's exactly what makes polymorphism possible.
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).
→ 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.
| Feature | Abstract class | Interface |
|---|---|---|
| Methods | abstract + concrete |
abstract (+ default
/static
since Java 8)
|
| Fields | any kind, mutable |
constants only (implicitly public static final
)
|
| Constructor | yes | no |
| Inheritance | extend one only | implement many |
| Method access | any modifier | public (abstract methods) |
| Best for | related classes + shared state | unrelated 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.
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.
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.
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.
| Overloading | Overriding | |
|---|---|---|
| Lives in | same class | parent → child |
| Parameters | must differ | must match |
| Return type | can differ | same or covariant |
| Resolved | compile time | runtime |
| Also called | static polymorphism | dynamic polymorphism |
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
).
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.
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 nothis;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#
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);
}
}
+
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());
}
}
+
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();
}
}
+
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
}
}
+
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(); }
}
+
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());
}
}
+ 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);
}
}
+
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
}
}
+
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();
}
}
+
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);
}
}
+
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
}
}
+
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]);
}
}
+
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#
# 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
Build a class and create two objects#
Goal: Confirm you understand class vs object and per-object state.
-
Write a class
Bookwith fieldstitle(String) andpages(int). -
Add a method
summary()that prints"<title> has <pages> pages". -
In
main, create two differentBookobjects with different values and callsummary()on each.
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
}
}
+
b2.pages
is 0: instance fields default to zero/null/false and each object is independent.
Constructor + this
+ validation
#
Goal: Initialize state safely through a constructor.
-
Give
Booka constructorBook(String title, int pages)usingthis.title = titleetc. -
Reject negative
pages(print a message and setpagesto 0 instead). - Create one valid and one invalid book; print both summaries.
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();
}
}
+
new Book()
now fails to compile
— declaring any constructor removes the auto default. Add an explicit Book(){}
if you need it.
Encapsulate a bank account#
Goal: Make data private and guard all changes.
-
Class
Accountwithprivate double balance. -
Public
deposit(amt)(reject ≤ 0) andwithdraw(amt)(reject >balance). -
Public
getBalance(). Inmain, try a bad withdraw and a good deposit.
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());
}
}
+
acc.balance = 1000;
won't compile (balance has private access
). That's the proof: the only path to the data is your validated methods.
Inheritance with super
#
Goal: Reuse parent behavior and extend it.
-
Class
Vehiclewith methodstart()printing"Engine on". -
Class
Car extends Vehicle; overridestart()to callsuper.start()then print"Car ready". -
Create a
Carand callstart().
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(); }
}
+
Remove super.start()
and "Engine on"
vanishes — overriding replaces
the method; super
is how you keep the parent's part.
Polymorphism over an abstract type#
Goal: Loop over a parent-typed array and watch each object behave differently.
-
Abstract class
Shapewith abstract methoddouble area(). -
Circle(radius) andSquare(side) extend it and implementarea(). -
Put one of each in a
Shape[], loop, and print each area.
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());
}
}
+
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.
Interface + overloading + the reference/object trap#
Goal: Combine pillars and nail the trickiest interview point.
-
Interface
Payablewithvoid pay(double amt). -
Class
Wallet implements Payable; also overloadpay()withpay(double amt, String note). -
Override
pay(double)to print the amount; the overload prints amount + note. -
In
main: assign aWalletto aPayablereference. Callpay(10)through it. Then try calling the 2-arg overload through thePayablereference.
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
}
}
+
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.
- 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
privatefield 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 callsuper(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).