Oops
Oops
1
6 Abstract Classes and Interfaces 20
6.1 Abstract Classes (C++ / Java) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
6.2 Interfaces (Java) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
7 Inheritance in Depth 23
7.1 Single Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
7.2 Multiple Inheritance & the “Diamond Problem” (C++) . . . . . . . . . . . . . . . . . 23
7.3 Interface Inheritance (Java) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
7.4 Aggregation vs. Composition vs. Association . . . . . . . . . . . . . . . . . . . . . . . 25
2
14 Summary & Key Takeaways 48
• Why OOP?
– Modularity & Maintainability: Code is structured as discrete pieces (classes) that can be
understood, tested, and maintained separately.
– Reusability: Through inheritance and composition, you can reuse existing classes rather than
rewriting common functionality.
– Abstraction: OOP lets you hide implementation details and expose only what’s necessary.
– Flexibility & Extensibility: Polymorphism enables swapping out implementations at run-
time or compile-time without changing calling code.
Originally popularized by languages like Smalltalk in the 1970s and then C++ in the 1980s, OOP
has since become a dominant paradigm in mainstream languages—Java, C#, Python, Ruby, and
others.
1 // File : Person . h
2 # ifndef PERSON_H
3 # define PERSON_H
4
5 # include < string >
6 # include < iostream >
7
8 class Person {
9 private :
10 std :: string name ;
11 int age ;
12
3
13 public :
14 // Constructor
15 Person ( const std :: string & name , int age )
16 : name ( name ) , age ( age ) {}
17
18 // Accessors ( getters )
19 std :: string getName () const { return name ; }
20 int getAge () const { return age ; }
21
22 // Mutators ( setters )
23 void setName ( const std :: string & newName ) { name = newName ; }
24 void setAge ( int newAge ) { age = newAge ; }
25
26 // Behavior / Method
27 void introduce () const {
28 std :: cout << " Hi , I ’m " << name << " , and I ’m "
29 << age << " years old .\ n " ;
30 }
31 };
32
33 # endif // PERSON_H
21 // Setter
22 public void setName ( String name ) {
23 this . name = name ;
24 }
25
4
30 // Behavior / Method
31 public void introduce () {
32 System . out . println ( " Hi , I ’m " + name
33 + " , and I ’m " + age + " years old . " ) ;
34 }
35 }
5
1. Encapsulation
2. Abstraction
3. Inheritance
4. Polymorphism
3.1 Encapsulation
Definition Encapsulation is the bundling of data (attributes) and code (methods) together such
that the internal representation of an object is hidden from the outside. Access to data is restricted
through well-defined interfaces (e.g., getters/setters or public methods), preventing external code
from putting the object into an invalid or inconsistent state.
Why It Matters
– Data Hiding / Information Hiding: Clients of a class don’t need to know—and shouldn’t know—the
details of how the class stores or manages its state.
– Maintainability: If you decide to change internal data representation (e.g., switch from std::vector
to std::list), external code need not change as long as the public interface remains stable.
– Safety: Prevents external code from directly manipulating fields in invalid ways (e.g., setting age
to a negative number).
1 class BankAccount {
2 private :
3 int accountNumber ;
4 double balance ;
5
6 public :
7 BankAccount ( int acctNum , double initialBalance )
8 : accountNumber ( acctNum ) , balance ( initialBalance ) {
9 if ( initialBalance < 0) {
10 throw std :: invalid_argument (
11 " Initial balance cannot be negative " ) ;
12 }
13 }
14
15 double getBalance () const {
16 return balance ;
17 }
18
19 void deposit ( double amount ) {
20 if ( amount <= 0) {
21 throw std :: invalid_argument (
22 " Deposit amount must be positive " ) ;
23 }
24 balance += amount ;
25 }
6
26
27 void withdraw ( double amount ) {
28 if ( amount <= 0) {
29 throw std :: invalid_argument (
30 " Withdrawal amount must be positive " ) ;
31 }
32 if ( amount > balance ) {
33 throw std :: runtime_error ( " Insufficient funds " ) ;
34 }
35 balance -= amount ;
36 }
37 };
7
3.2 Abstraction
Definition Abstraction means exposing only the relevant functionality of an object and hiding the
unnecessary details. While encapsulation is more about “hiding data,” abstraction is about “hiding
complexity.” You present a simplified model to the user of a class, exposing only what’s necessary
to accomplish a task.
Why It Matters
– Reduced Complexity: Users of a class need only understand its interface, not the intricate internal
workings.
– Focus on What, Not How: Clients can call sort() on an array without needing to know whether
QuickSort, MergeSort, or TimSort is used underneath.
– Layered Architecture: High-level modules don’t need to know about low-level implementation
details, enabling separation of concerns.
The copyData method works with any InputStream—the client doesn’t care if it’s reading from
a file, network socket, or memory buffer.
3.3 Inheritance
Definition Inheritance allows one class (subclass or derived class) to acquire the properties (fields,
methods) of another class (superclass or base class). It models an “is-a” relationship: if Dog inherits
from Animal, then a Dog is an Animal.
8
Why It Matters
– Code Reuse: You can reuse fields and methods of the base class without rewriting them in the
derived class.
– Polymorphic Substitutability: You can treat a derived class object as if it were an instance of the
base class.
1 class Animal {
2 public :
3 void eat () { std :: cout << " Animal eats .\ n " ; }
4 void sleep () { std :: cout << " Animal sleeps .\ n " ; }
5
6 // Virtual method for polymorphism
7 virtual void makeSound () {
8 std :: cout << " Animal makes a sound .\ n " ;
9 }
10
1 class Animal {
2 public void eat () {
3 System . out . println ( " Animal eats . " ) ;
4 }
5 public void sleep () {
6 System . out . println ( " Animal sleeps . " ) ;
9
7 }
8 public void makeSound () {
9 System . out . println ( " Animal makes a sound . " ) ;
10 }
11 }
12
13 class Dog extends Animal {
14 @Override
15 public void makeSound () {
16 System . out . println ( " Woof ! " ) ;
17 }
18 public void fetch () {
19 System . out . println ( " Dog fetches the ball . " ) ;
20 }
21 }
22
3.4 Polymorphism
Definition Polymorphism (from Greek “many forms”) enables objects of different types to be
treated as objects of a common supertype. There are two main categories:
a. Compile-time (Static) Polymorphism: Achieved via method (or operator) overloading.
b. Runtime (Dynamic) Polymorphism: Achieved via method overriding (with virtual func-
tions in C++, or simply overridden methods in Java) and dynamic dispatch.
Polymorphism allows writing flexible, extensible code. For example, a function that takes
an Animal reference can work with Dog, Cat, or any future subclass of Animal that overrides
makeSound().
10
3.4.2 Java Runtime Polymorphism Example
– Destructor: A special method called when an object goes out of scope (or is deleted); used to
release resources.
Example:
1 class MyClass {
2 private :
3 int * data ;
4 public :
5 // Default constructor
6 MyClass () : data ( new int [100]) {
7 std :: cout << " Constructor : Allocated array of 100 ints .\ n " ;
8 }
9
10 // Parameterized constructor
11 MyClass ( int size ) : data ( new int [ size ]) {
12 std :: cout << " Constructor : Allocated array of "
13 << size << " ints .\ n " ;
14 }
15
16 // Copy constructor ( Rule of Three !)
17 MyClass ( const MyClass & other ) {
18 // Deep copy
19 data = new int [100];
20 std :: copy ( other . data , other . data + 100 , data ) ;
21 std :: cout << " Copy constructor : Deep copy performed .\ n " ;
22 }
23
11
24 // Assignment operator ( Rule of Three )
25 MyClass & operator =( const MyClass & other ) {
26 if ( this != & other ) {
27 delete [] data ;
28 data = new int [100];
29 std :: copy ( other . data , other . data + 100 , data ) ;
30 std :: cout << " Assignment operator : Deep copy performed .\ n " ;
31 }
32 return * this ;
33 }
34
35 // Destructor
36 ~ MyClass () {
37 delete [] data ;
38 std :: cout << " Destructor : Freed allocated memory .\ n " ;
39 }
40 };
Key points:
1. Default Constructor: If you do not define any constructor, C++ provides a default (no-
argument) constructor that does nothing. Once you define any constructor, the default is not
auto-generated.
2. Copy Constructor & Assignment Operator: If your class manages resources (heap memory,
file handles, network sockets), you usually need to implement the copy constructor and assign-
ment operator to avoid shallow copies. This is often referred to as the “Rule of Three” (later
“Rule of Five” with move semantics).
3. Destructor: Responsible for cleaning up any acquired resources. Always declare the destructor
virtual if your class is intended to be used polymorphically (so derived-class destructors are
called properly).
– finalize() method (deprecated since Java 9 and removed in later versions) was historically
invoked by the GC before reclaiming an object, but you should avoid using it. Instead, use
try-with-resources or close() patterns (e.g., implement AutoCloseable).
Example:
1 public class ResourceHolder implements AutoCloseable {
2 private SomeResource res ;
3
4 public ResourceHolder ( String filename ) throws IOException {
5 res = new SomeResource ( filename ) ;
6 System . out . println ( " Acquired resource . " ) ;
7 }
8
9 @Override
10 public void close () {
12
11 if ( res != null ) {
12 res . release () ;
13 System . out . println ( " Released resource . " ) ;
14 }
15 }
16
17 @Deprecated
18 @Override
19 protected void finalize () throws Throwable {
20 try {
21 if ( res != null ) {
22 res . release () ;
23 System . out . println (
24 " Released resource in finalize () " ) ;
25 }
26 } finally {
27 super . finalize () ;
28 }
29 }
30 }
Usage:
1 public static void main ( String [] args ) {
2 try ( ResourceHolder rh = new ResourceHolder ( " data . txt " ) ) {
3 // Use rh ; resource a u t o closed at end of try block
4 } catch ( IOException e ) {
5 e . printStackTrace () ;
6 }
7 }
– protected: Members accessible in the class and in derived classes (but not from unrelated code).
1 class Base {
2 public :
3 int x ; // Accessible from anywhere
4 protected :
5 int y ; // Accessible in Base and in any subclass
6 private :
7 int z ; // Accessible only within Base
8 };
9
13
13 x = 1; // OK ( public )
14 y = 2; // OK ( protected )
15 // z = 3; // ERROR ( private to Base )
16 }
17 };
– protected: Accessible within the same package, and in subclasses (even if in a different package).
– default (package-private) (no modifier): Accessible only within the same package.
– Useful for disambiguating instance variables from parameters when names collide, or for passing
the current object’s reference to another method, etc.
14
4.3.1 C++ Example
1 class Rectangle {
2 private :
3 int width , height ;
4
5 public :
6 Rectangle ( int width , int height ) {
7 // Parameter names are same as member names
8 this - > width = width ;
9 this - > height = height ;
10 }
11
12 bool isSquare () const {
13 return this - > width == this - > height ;
14 }
15 };
– Often used for utility functions, constants, or data shared across all instances.
1 class MathUtils {
2 public :
3 static const double PI ; // Declaration
4 static int add ( int a , int b ) {
5 return a + b ;
6 }
7 };
15
8
9 // Definition of static const double outside the class
10 const double MathUtils :: PI = 3 .14 15 92 65 358 97 93 ;
11
12 int main () {
13 double area = MathUtils :: PI * 2 * 2;
14 int sum = MathUtils :: add (3 , 4) ; // 7
15 return 0;
16 }
– You can also have const member variables (initialized only via constructor’s initializer list).
1 class Person {
2 private :
3 std :: string name ;
4 int age ;
5
6 public :
7 Person ( const std :: string & name , int age )
8 : name ( name ) , age ( age ) {}
9
16
19 int getAge () const {
20 return age ;
21 }
22
17
32 return width * height ;
33 }
34 }
18
7 System . out . println ( " Printing double : " + x ) ;
8 }
9
10 public void print ( String s ) {
11 System . out . println ( " Printing string : " + s ) ;
12 }
13
14 public static void main ( String [] args ) {
15 Printer p = new Printer () ;
16 p . print (42) ; // print ( int )
17 p . print (3.1415) ; // print ( double )
18 p . print ( " Hello World " ) ; // print ( string )
19 }
20 }
19
23 Base * b1 = new Base () ;
24 Base * b2 = new Derived () ;
25 greet ( b1 ) ; // Prints Hello from Base !
26 greet ( b2 ) ; // Prints Hello from Derived !
27 delete b1 ;
28 delete b2 ;
29 return 0;
30 }
1 class Base {
2 public void sayHello () {
3 System . out . println ( " Hello from Base ! " ) ;
4 }
5 }
6
7 class Derived extends Base {
8 @Override
9 public void sayHello () {
10 System . out . println ( " Hello from Derived ! " ) ;
11 }
12 }
13
14 public class Main {
15 static void greet ( Base b ) {
16 b . sayHello () ; // Calls Derived ’s method if b is instance of
Derived
17 }
18
19 public static void main ( String [] args ) {
20 Base b1 = new Base () ;
21 Base b2 = new Derived () ;
22 greet ( b1 ) ; // Hello from Base !
23 greet ( b2 ) ; // Hello from Derived !
24 }
25 }
20
3 // Pure virtual function : makes Shape abstract
4 virtual double area () const = 0;
5
6 // A virtual destructor is strongly recommended
7 virtual ~ Shape () = default ;
8 };
9
10 class Circle : public Shape {
11 private :
12 double radius ;
13 public :
14 Circle ( double r ) : radius ( r ) {}
15 double area () const override {
16 return 3.1415 92653589 793 * radius * radius ;
17 }
18 };
19
20 class Rectangle : public Shape {
21 private :
22 double width , height ;
23 public :
24 Rectangle ( double w , double h ) : width ( w ) , height ( h ) {}
25 double area () const override {
26 return width * height ;
27 }
28 };
29
30 int main () {
31 // Shape s ; // Error : cannot instantiate abstract class
32 Shape * c = new Circle (2.5) ;
33 Shape * r = new Rectangle (3.0 , 4.0) ;
34 std :: cout << " Circle area : " << c - > area () << " \ n " ; // 19.634954...
35 std :: cout << " Rectangle area : " << r - > area () << " \ n " ; // 12.0
36 delete c ;
37 delete r ;
38 return 0;
39 }
21
16 public double area () {
17 return Math . PI * radius * radius ;
18 }
19 }
20
21 public class Rectangle extends Shape {
22 private double width , height ;
23 public Rectangle ( double w , double h ) {
24 this . width = w ;
25 this . height = h ;
26 }
27 @Override
28 public double area () {
29 return width * height ;
30 }
31 }
32
33 public class Main {
34 public static void main ( String [] args ) {
35 // Shape s = new Shape () ; // Error : cannot instantiate abstract
class
36 Shape c = new Circle (2.5) ;
37 Shape r = new Rectangle (3 , 4) ;
38 System . out . println ( " Circle area : " + c . area () ) ;
39 System . out . println ( " Rectangle area : " + r . area () ) ;
40 }
41 }
– Java allows multiple interfaces to be implemented by a single class (C++ uses multiple inheritance
of classes/interfaces to achieve this effect).
22
16 @Override
17 public void move ( int dx , int dy ) {
18 x += dx ;
19 y += dy ;
20 System . out . println ( " Player moved to ( " + x + " , " + y + " ) " ) ;
21 }
22
23 @Override
24 public void draw () {
25 System . out . println ( " Drawing player at ( " + x + " , " + y + " ) " ) ;
26 }
27
28 public static void main ( String [] args ) {
29 Player p = new Player (0 , 0) ;
30 p . move (5 , 3) ; // Player moved to (5 , 3)
31 p . draw () ; // Drawing player at (5 , 3)
32 }
33 }
7 Inheritance in Depth
Inheritance comes in several flavors and has important design trade-offs. Below, we explore single
vs. multiple inheritance, the diamond problem, and best practices around using inheritance vs.
composition.
– Avoids ambiguity: there is only one path up the hierarchy to find a method or field.
– The classic “diamond problem” arises when two base classes share a common ancestor, and the
derived class inherits from both. Without caution, you end up with two copies of the common
ancestor’s members.
23
Diamond Example
1 class LivingBeing {
2 public :
3 void breathe () { std :: cout << " Breathing ...\ n " ; }
4 };
5
6 class Mammal : public LivingBeing {
7 public :
8 void feedMilk () { std :: cout << " Feeding milk ...\ n " ; }
9 };
10
11 class WingedAnimal : public LivingBeing {
12 public :
13 void flapWings () { std :: cout << " Flapping wings ...\ n " ; }
14 };
15
16 // Bat inherits from both Mammal and WingedAnimal
17 class Bat : public Mammal , public WingedAnimal {
18 public :
19 void echoLocate () { std :: cout << " E c h o locating ...\ n " ; }
20 };
21
22 int main () {
23 Bat b ;
24 b . breathe () ;
25 // ERROR : ambiguous : which breathe () ? from Mammal :: LivingBeing
26 // or WingedAnimal :: LivingBeing
27 return 0;
28 }
24
24 b . feedMilk () ;
25 b . flapWings () ;
26 b . echoLocate () ;
27 return 0;
28 }
By inheriting LivingBeing virtually in both Mammal and WingedAnimal, there is only one shared
LivingBeing subobject in Bat, so calling b.breathe() is unambiguous.
1 interface Winged {
2 void flapWings () ;
3 }
4
5 interface Mammalian {
6 void feedMilk () ;
7 }
8
25
1. Association: A general “uses-a” relationship; one object holds a reference/pointer to another.
Neither “owns” the other.
– Example: A Driver class has an Engine reference, but the engine could exist independently
of the driver.
– Example: A Team class aggregates Player objects. Even if the Team is disbanded, Player
objects can still exist.
– Example: A House class is composed of Room objects; if you delete the House, the Room objects
cease to exist.
26
8 Memory Layout & Object Lifecycle
Understanding how objects live in memory and how they are constructed and destroyed is especially
important in C++ (less so in Java, where GC abstracts much of this away).
– Heap Allocation: Objects created via new (e.g., MyClass* p = new MyClass();). Must be
explicitly deallocated with delete; otherwise memory leaks occur.
1 void foo () {
2 MyClass obj1 ; // Allocated on stack ;
3 // destroyed at end of foo ()
4 MyClass * obj2 = new MyClass () ;
5 // ... use obj2 ...
6 delete obj2 ; // Must manually delete ( calls destructor )
7 }
8.1.2 Java
– In Java, all objects are allocated on the heap (technically, some JVMs may optimize small
objects to be stack-allocated, but that’s transparent to the developer).
– Primitive local variables (e.g., int x = 5;) are stored on the stack; object references (e.g., Person
p = new Person();) are on the stack, but the actual Person object is on the heap.
2. Member objects are constructed next, in the order they appear in the class definition (regard-
less of initializer list order).
4. Destruction happens in reverse: first the derived class’s destructor body, then member fields’
destructors (in reverse order), then base class destructors (in reverse inheritance order).
3 class A {
4 public :
5 A () { std :: cout << " A :: A () \ n " ; }
27
6 ~ A () { std :: cout << " A ::~ A () \ n " ; }
7 };
8
9 class B {
10 public :
11 B () { std :: cout << " B :: B () \ n " ; }
12 ~ B () { std :: cout << " B ::~ B () \ n " ; }
13 };
14
15 class C : public A {
16 private :
17 B b_member ;
18 public :
19 C () : b_member () {
20 std :: cout << " C :: C () \ n " ;
21 }
22 ~ C () {
23 std :: cout << " C ::~ C () \ n " ;
24 }
25 };
26
27 int main () {
28 C c;
29 return 0;
30 }
Output:
2. Instance field initializers & instance initializer blocks (in textual order).
1 public class A {
2 {
3 System . out . println ( " A : Instance initializer " ) ;
4 }
5 public A () {
6 System . out . println ( " A : Constructor " ) ;
7 }
28
8 }
9
10 public class B extends A {
11 {
12 System . out . println ( " B : Instance initializer " ) ;
13 }
14 public B () {
15 System . out . println ( " B : Constructor " ) ;
16 }
17 }
18
19 public class Main {
20 public static void main ( String [] args ) {
21 B b = new B () ;
22 }
23 }
Output:
A: Instance initializer
A: Constructor
B: Instance initializer
B: Constructor
Note: Java’s GC finally calls finalize() (if implemented) unpredictably, but you should not
rely on finalizers.
8.3 Copy Constructor, Assignment Operator, and the Rule of Three/Five (C++)
When a C++ class manages dynamically allocated resources, you must carefully implement:
– Copy constructor: to make a deep copy when constructing a new object from an existing one.
– Copy assignment operator: to free existing resources and copy data from the source.
– With C++11, we also consider move constructor and move assignment operator for more
efficient transfers (“Rule of Five”).
29
13 }
14
15 // Copy Constructor
16 Buffer ( const Buffer & other )
17 : size ( other . size ) , data ( new char [ other . size ]) {
18 std :: copy ( other . data , other . data + size , data ) ;
19 }
20
21 // Copy Assignment
22 Buffer & operator =( const Buffer & other ) {
23 if ( this != & other ) {
24 delete [] data ; // Free old memory
25 size = other . size ;
26 data = new char [ size ];
27 std :: copy ( other . data , other . data + size , data ) ;
28 }
29 return * this ;
30 }
31
32 // ( Optional ) Move Constructor ( C ++11+)
33 Buffer ( Buffer && other ) noexcept
34 : size ( other . size ) , data ( other . data ) {
35 other . size = 0;
36 other . data = nullptr ;
37 }
38
39 // ( Optional ) Move Assignment
40 Buffer & operator =( Buffer && other ) noexcept {
41 if ( this != & other ) {
42 delete [] data ;
43 size = other . size ;
44 data = other . data ;
45 other . size = 0;
46 other . data = nullptr ;
47 }
48 return * this ;
49 }
50 };
30
9.1 Virtual Tables (vtables) & Dynamic Dispatch
– When a class has one or more virtual methods, the compiler typically creates a virtual table
(vtable) for that class.
– Each object of such a class stores a hidden pointer (vptr) to its class’s vtable.
– The vtable is essentially an array of function pointers, one for each virtual method. When you
call a virtual method on an object, the program looks up the function pointer in the vtable and
jumps to it—achieving runtime polymorphism.
– In C++, “interfaces” (in the Java sense) are typically modeled as classes with only pure virtual
methods (and no data members).
1 class Drawable {
2 public :
3 virtual void draw () const = 0;
4 virtual ~ Drawable () = default ;
5 };
6
7 class Circle : public Drawable {
8 public :
9 void draw () const override {
10 // draw a circle
11 }
12 };
31
1 # include < memory >
2 # include < iostream >
3
4 class Widget {
5 public :
6 Widget () { std :: cout << " Widget acquired .\ n " ; }
7 ~ Widget () { std :: cout << " Widget destroyed .\ n " ; }
8 void doWork () { std :: cout << " Widget working ...\ n " ; }
9 };
10
11 int main () {
12 {
13 std :: unique_ptr < Widget > w1 = std :: make_unique < Widget >() ;
14 w1 - > doWork () ;
15 // When w1 goes out of scope , ~ Widget () is called automatically
16 }
17
18 {
19 std :: shared_ptr < Widget > w2 = std :: make_shared < Widget >() ;
20 {
21 std :: shared_ptr < Widget > w3 = w2 ;
22 w3 - > doWork () ;
23 // w2 and w3 share ownership ; destructed only when both go out
of scope
24 }
25 std :: cout << " w3 out of scope , but widget still alive via w2 \ n " ;
26 }
27 std :: cout << " w2 out of scope , Widget destroyed now \ n " ;
28 return 0;
29 }
Key Takeaways:
– Use std::shared_ptr when multiple owners are needed, but be mindful of overhead and potential
cycles (use std::weak_ptr to break cycles).
– Rely on RAII: wrap resources (file handles, mutexes, sockets) in classes whose destructors release
them.
– Runtime Polymorphism: Virtual dispatch has some overhead (indirection through vtable).
Templates are resolved at compile time, so there is no runtime dispatch cost.
32
4 }
5
6 int main () {
7 int x = maxValue (3 , 7) ; // instantiate maxValue < int >
8 double y = maxValue (3.14 , 2.71) ; // instantiate maxValue < double >
9 }
– You cannot predict exactly when GC will run, so you should not rely on finalizers (finalize()),
as they are deprecated and have unpredictable timing.
33
1 Class <? > cls = Class . forName ( " com . example . MyClass " ) ;
2 Method m = cls . getMethod ( " myMethod " , String . class , int . class ) ;
3 Object instance = cls . getConstructor () . newInstance () ;
4 Object result = m . invoke ( instance , " hello " , 42) ;
Dynamic Proxies: Java can generate proxy objects at runtime that implement a set of inter-
faces, delegating method calls to an InvocationHandler. Widely used in AOP (Aspect-Oriented
Programming) and RPC frameworks.
1 import java . lang . reflect .*;
2
3 interface Service {
4 void serve () ;
5 }
6
7 class RealService implements Service {
8 @Override
9 public void serve () {
10 System . out . println ( " Serving ... " ) ;
11 }
12 }
13
14 class LoggingHandler implements Invoca tionHand ler {
15 private final Object target ;
16 public LoggingHandler ( Object target ) {
17 this . target = target ;
18 }
19 @Override
20 public Object invoke ( Object proxy , Method method ,
21 Object [] args ) throws Throwable {
22 System . out . println ( " Before method : " + method . getName () ) ;
23 Object result = method . invoke ( target , args ) ;
24 System . out . println ( " After method : " + method . getName () ) ;
25 return result ;
26 }
27 }
28
29 public class Main {
30 public static void main ( String [] args ) {
31 Service real = new RealService () ;
32 Service proxy = ( Service ) Proxy . newProxyInstance (
33 Service . class . getClassLoader () ,
34 new Class <? >[]{ Service . class } ,
35 new LoggingHandler ( real )
36 );
37 proxy . serve () ;
38 // Output :
39 // Before method : serve
40 // Serving ...
41 // After method : serve
42 }
43 }
34
10.3 Default and Static Methods on Interfaces
Since Java 8, interfaces can have default methods (with a method body) and static methods.
This allows interface evolution without breaking existing implementations.
1 interface Calculator {
2 int add ( int a , int b ) ;
3
4 default int subtract ( int a , int b ) {
5 return a - b ; // Default implementation
6 }
7
8 static int multiply ( int a , int b ) {
9 return a * b ;
10 }
11 }
12
13 class MyCalculator implements Calculator {
14 @Override
15 public int add ( int a , int b ) {
16 return a + b ;
17 }
18 }
19
20 public class Main {
21 public static void main ( String [] args ) {
22 Calculator calc = new MyCalculator () ;
23 System . out . println ( calc . add (3 , 4) ) ; // 7
24 System . out . println ( calc . subtract (10 , 5) ) ; // 5 ( default method )
25 System . out . println ( Calculator . multiply (2 , 8) ) ; // 16 ( static
method )
26 }
27 }
1. Single-Responsibility Principle (SRP) A class should have exactly one reason to change.
It should do one thing well. Example: Don’t mix file I/O and business logic in the same class.
Instead, separate them into FileManager and BusinessProcessor.
2. Open/Closed Principle (OCP) Software entities (classes, modules, functions) should be open
for extension but closed for modification. Using interfaces or abstract classes, you can add new
behavior by extending existing code rather than altering it.
35
1 // Without OCP : to add a new shape , we must modify ShapeProcessor
2 class ShapeProcessor {
3 public double area ( Shape s ) {
4 if ( s instanceof Circle ) {
5 return Math . PI * (( Circle ) s ) . radius * (( Circle ) s ) . radius ;
6 } else if ( s instanceof Rectangle ) {
7 Rectangle r = ( Rectangle ) s ;
8 return r . width * r . height ;
9 }
10 // If we add a new shape , we must modify this code !
11 return 0;
12 }
13 }
14
15 // With OCP : each shape knows how to compute its own area
16 interface Shape {
17 double area () ;
18 }
19 class Circle implements Shape { /* ... */ }
20 class Rectangle implements Shape { /* ... */ }
21
22 class ShapeProcessor {
23 public double area ( Shape s ) {
24 return s . area () ; // No need to modify this when adding new
shapes
25 }
26 }
36
19 // Client code violation :
20 void resize Rectangl eTo ( Rectangle r , int w , int h ) {
21 r . setWidth ( w ) ;
22 r . setHeight ( h ) ;
23 // Now expects area to be w * h
24 System . out . println ( r . area () ) ;
25 // But if r is Square , area is w * w or h *h , not w * h
26 }
4. Interface Segregation Principle (ISP) Clients should not be forced to depend on interfaces
they do not use. It’s better to have many smaller, specific interfaces rather than a large, “fat”
interface. Example: Instead of a single IMachine interface with print(), scan(), fax(), provide
IPrinter, IScanner, IFax separately. A Printer only implements IPrinter.
5. Dependency Inversion Principle (DIP) High-level modules should not depend on low-level
modules; both should depend on abstractions (e.g., interfaces). Abstractions should not depend
on details; details should depend on abstractions. Example: Instead of a Keyboard class directly
instantiating a USBPort, define an interface Port and let both USBPort and BluetoothPort
implement it. Then Keyboard depends on Port, not on a specific USBPort class.
1 interface Port {
2 void connect () ;
3 }
4
5 class USBPort implements Port {
6 public void connect () {
7 System . out . println ( " Connected via USB " ) ;
8 }
9 }
10
11 class BluetoothPort implements Port {
12 public void connect () {
13 System . out . println ( " Connected via Bluetooth " ) ;
14 }
15 }
16
17 class Keyboard {
18 private Port port ;
19 public Keyboard ( Port port ) {
20 this . port = port ;
21 }
22 public void plugIn () {
23 port . connect () ;
24 }
25 }
26
27 // Usage :
28 Port usb = new USBPort () ;
29 Keyboard keyboard = new Keyboard ( usb ) ;
30 keyboard . plugIn () ; // Connected via U S B
37
11.2 DRY, KISS, YAGNI
• DRY (Don’t Repeat Yourself ): Avoid duplicating code. If the same logic appears in multiple
places, factor it out into a single method/class.
• KISS (Keep It Simple, Stupid): Design systems in the simplest way possible. Don’t over-
engineer.
• YAGNI (You Aren’t Gonna Need It): Don’t add features or abstractions until they are
strictly necessary. Premature generalization often leads to unnecessary complexity.
• Coupling: A measure of how dependent modules or classes are on one another. Low coupling is
desirable—modules should interact through well-defined interfaces, minimizing the ripple effect
of changes.
• Creational Patterns
– Singleton: Ensures a class has only one instance and provides a global access point.
– Factory Method: Defines an interface for creating an object but lets subclasses decide which
class to instantiate.
– Abstract Factory: Provides an interface for creating families of related or dependent objects
without specifying their concrete classes.
– Builder: Separates the construction of a complex object from its representation.
– Prototype: Create new objects by copying an existing object (prototype) rather than creating
from scratch.
• Structural Patterns
– Adapter: Converts the interface of a class into another interface clients expect.
– Decorator: Attach additional responsibilities dynamically to an object.
– Facade: Provides a unified interface to a set of interfaces in a subsystem.
– Composite: Compose objects into tree structures to represent part-whole hierarchies.
38
– Proxy: Provide a surrogate or placeholder for another object to control access to it.
• Behavioral Patterns
– Strategy: Define a family of algorithms, encapsulate each one, and make them interchangeable
at runtime.
– Observer: Define a one-to-many dependency between objects so that when one object changes
state, all its dependents are notified.
– Command: Encapsulate a request as an object, thereby allowing parameterization of clients
with queues, requests, and operations.
– Iterator: Provide a way to access elements of an aggregate object sequentially without expos-
ing its underlying representation.
– State: Allow an object to change its behavior when its internal state changes.
You don’t need to memorize all patterns; instead, understand the intent behind each and examine
a few common ones in detail (e.g., Singleton, Factory, Observer, Strategy).
1. Overusing Inheritance Inheritance should model an “is-a” relationship. If you find yourself us-
ing inheritance simply to reuse code, consider composition instead. Deep inheritance hierarchies
can lead to brittle code—changes in a base class can cascade unexpectedly.
Better approach:
1 public List < String > getPhoneNumbers () {
2 return Collections . unmodifiableList ( phoneNumbers ) ;
3 }
3. Leaking this During Construction (C++) If you register this (e.g., to an observer) in a
constructor before derived classes have been fully initialized, you can get into trouble when a
virtual call is made on a partly constructed object.
4. Forgetting to Make Destructors Virtual (C++) If you call delete on a base-class pointer
that points to a derived-class object, and you forgot to declare the base destructor as virtual,
only the base destructor will run—resulting in a resource leak (or other undefined behavior).
39
5. Unintentional Object Slicing (C++) If you assign a derived-class object to a base-class
object by value, only the base portion is copied—derived-class data is “sliced” off.
1 class Base { /* ... */ };
2 class Derived : public Base { /* ... additional data members ... */ };
3
4 Derived d ;
5 Base b = d ; // Slicing : the Derived - specific data is lost
6. Excessive Use of Static Methods/Fields Relying too heavily on static members can lead
to code that is hard to test (because static state is global) and inflexible (no easy way to swap
implementations at runtime).
7. Ignoring Thread Safety In multi-threaded programs, careless use of mutable shared objects
(especially static fields) can lead to race conditions. Use synchronization (synchronized in Java,
or std::mutex in C++) or immutable classes when possible.
13.1 Requirements
1. Task
– Has an ID, title, description, priority (LOW, MEDIUM, HIGH), status (NEW, IN_PROGRESS, DONE),
and an optional assignee (User).
– Can be marked as done or reopened.
2. User
3. TaskRepository (Interface)
40
5. TaskService
41
29 }
30
31 public void setEmail ( String email ) {
32 // Simple email validation
33 if ( email == null || ! email . contains ( " @ " ) ) {
34 throw new I l l e g a l A r g u m e n t E x c e p t i o n ( " Invalid email address " ) ;
35 }
36 this . email = email ;
37 }
38
39 @Override
40 public String toString () {
41 return " User { " +
42 " userID = " + userID +
43 " , name = ’ " + name + ’\ ’ ’ +
44 " , email = ’ " + email + ’\ ’ ’ +
45 ’} ’;
46 }
47 }
42
31 public String getTitle () {
32 return title ;
33 }
34
43
85 this . status = Status . IN_PROGRESS ;
86 this . updatedAt = LocalDateTime . now () ;
87 }
88
44
10 if ( tasks . containsKey ( task . getTaskID () ) ) {
11 throw new I l l e g a l A r g u m e n t E x c e p t i o n (
12 " Task ID already exists : " + task . getTaskID () ) ;
13 }
14 tasks . put ( task . getTaskID () , task ) ;
15 }
16
17 @Override
18 public Optional < Task > getTaskByID ( int taskID ) {
19 return Optional . ofNullable ( tasks . get ( taskID ) ) ;
20 }
21
22 @Override
23 public void updateTask ( Task task ) {
24 if (! tasks . containsKey ( task . getTaskID () ) ) {
25 throw new N o S u c h E l e m e n t E x c e p t i o n (
26 " Task not found : " + task . getTaskID () ) ;
27 }
28 tasks . put ( task . getTaskID () , task ) ;
29 }
30
31 @Override
32 public void deleteTask ( int taskID ) {
33 tasks . remove ( taskID ) ;
34 }
35
36 @Override
37 public List < Task > getAllTasks () {
38 return new ArrayList < >( tasks . values () ) ;
39 }
40 }
45
20 " Task not found : " + taskID ) ) ;
21 task . assignTo ( user ) ;
22 repo . updateTask ( task ) ;
23 }
24
25 public void changePriority ( int taskID , Priority newPriority ) {
26 Task task = repo . getTaskByID ( taskID )
27 . orElseThrow (() -> new N o S u c h E l e m e n t E x c e p t i o n (
28 " Task not found : " + taskID ) ) ;
29 task . setPriority ( newPriority ) ;
30 repo . updateTask ( task ) ;
31 }
32
33 public void markDone ( int taskID ) {
34 Task task = repo . getTaskByID ( taskID )
35 . orElseThrow (() -> new N o S u c h E l e m e n t E x c e p t i o n (
36 " Task not found : " + taskID ) ) ;
37 task . markDone () ;
38 repo . updateTask ( task ) ;
39 }
40
41 public List < Task > getTasksByUser ( User user ) {
42 return repo . getAllTasks () . stream ()
43 . filter ( t -> t . getAssignee () != null
44 && t . getAssignee () . getUserID () == user . getUserID () )
45 . collect ( Collectors . toList () ) ;
46 }
47
46
12 service . createTask (100 , " Setup Dev Environment " ,
13 " Install IDE , SDK , etc . " , Priority . MEDIUM ) ;
14 service . createTask (101 , " Design Database Schema " ,
15 " Create ER diagram " , Priority . HIGH ) ;
16 service . createTask (102 , " Implement Login " ,
17 " Use JWT authentication " , Priority . HIGH ) ;
18
19 // Assign tasks
20 service . assignTask (100 , alice ) ;
21 service . assignTask (101 , bob ) ;
22
23 // Mark tasks done
24 service . markDone (100) ;
25
26 // List tasks by user
27 System . out . println ( " Alice ’s Tasks : " ) ;
28 for ( Task t : service . getTasksByUser ( alice ) ) {
29 System . out . println ( t ) ;
30 }
31
32 System . out . println ( " \ nHigh Priority Tasks : " ) ;
33 for ( Task t : service . g et Ta sks By Pr ior it y ( Priority . HIGH ) ) {
34 System . out . println ( t ) ;
35 }
36 }
37 }
Sample Output:
Alice’s Tasks:
Task{taskID=100, title=’Setup Dev Environment’, priority=MEDIUM,
status=DONE, assignee=Alice,
createdAt=2025-05-31T12:34:56.789,
updatedAt=2025-05-31T12:35:01.234}
47
• Inheritance & Interfaces: Though we didn’t define a subclass hierarchy here, the pattern is
ready to extend—for instance, you could have PersistentTaskRepository implementing TaskRepository
that stores tasks in a database.
• SOLID Principles: SRP (each class has one responsibility), OCP (we can add new repository
implementations without modifying existing code), DIP (high-level TaskService depends on the
abstraction TaskRepository).
• Write small programs that define class hierarchies, use virtual methods, show slicing issues,
demonstrate composition over inheritance, etc.
• Be able to trace through object lifetimes (especially in C++), including order of construc-
tion/destruction.
• You may be asked design-oriented questions (e.g., how you would refactor a monolithic class,
or how to design a plugin architecture). Use SOLID principles, DRY/KISS/YAGNI, and show
awareness of cohesion and coupling.
• Be ready to discuss trade-offs between inheritance and composition, or static vs. dynamic
polymorphism (templates vs. virtual methods).
48
• For C++: “Rule of Three/Five,” forgetting to mark destructors as virtual, object slicing,
multiple inheritance’s diamond problem.
• For Java: exposing internal collections, careless use of static fields (global state), overuse of
reflection, ignoring thread safety.
• Interviews often test not just your code, but your ability to communicate technical ideas.
Practice explaining polymorphism to someone who knows only procedural programming, or
walk through how a virtual table lookup happens in memory.
• Spend some time sketching out small class hierarchies or systems (like the Task Management
example above). This helps solidify theoretical knowledge by applying it in a design context.
• Review Standard Library/Framework Classes: For C++, know how std::vector, std::string,
and std::unique_ptr relate to OOP. For Java, be familiar with java.util collections, java.lang.Object
methods (e.g., toString(), equals(), hashCode()), and where inheritance/polymorphism come
into play.
• Whiteboard Coding: Practice sketching class definitions, inheritance hierarchies, and code
fragments on a whiteboard (or paper). Many interviews require writing code by hand.
• Debug Common Interview Snippets: Look for typical small OOP “gotchas”—for example, a
base class method not being declared virtual, or a missing override annotation that leads to
an unintended overload.
• Ask Clarifying Questions: If an interviewer says, “Design a class hierarchy for geometric
shapes,” clarify expected operations (area, perimeter, drawing, serialization) before jumping in.
Good design often hinges on understanding requirements.
• Time Management: In a timed setting, outline your solution first (class names, main methods,
relationships), then fill in details. This shows the interviewer you have a structured approach.
49