Theory summary
Look around your room. A lamp has a switch and a light. A pet has a name and can eat and play. Each thing keeps some information about itself and knows how to do some actions. Object-oriented programming, or OOP, builds programs the same way: the program is a team of small objectsa bundle of data plus the actions that work on that data that keep their own information and ask each other to do things.
Almost every app you use is written this way: games, banks, shops, phone apps. Big programs are hard to change without breaking something. OOP gives programmers tools to decide where each piece of knowledge lives, so that a change in one place does not break ten others. That is the real goal of every idea in this summary.
You will learn what classes and objects are, how objects hide their insides (encapsulation), how one class can build on another (inheritance), how the same call can do different things (polymorphism), what interfaces and abstract classes are for, why we often prefer building with parts instead of inheriting, how to spot messy code, what happens when things go wrong (exceptions), how to write code that works for many types (generics), how memory is cleaned, how to read and write data, and how other languages do OOP without classes. Most examples use Java, the language of the quiz. At the end you will be ready to answer it.
The big picture: objects talking to objects
Think of a school. The teacher does not open the student's backpack and look inside. She asks: 'please show me your homework'. The student decides how to find it. In OOP, objects work like this. One object sends a request, called a method call, and the other object decides how to answer.
| Idea | In one sentence | Everyday picture |
|---|---|---|
| Encapsulation | An object keeps its data private and offers only safe actions. | A vending machine: you press buttons, you cannot reach inside. |
| Abstraction | We show what an object can do and hide how it does it. | You drive a car with a wheel and pedals, without knowing the engine. |
| Inheritance | A new class starts from an old one and adds or changes things. | A puppy is a dog, and a dog is an animal. |
| Polymorphism | One request, many answers, depending on the object. | 'Make a sound' makes a dog bark and a cat meow. |
| Composition | An object is built from other objects. | A bike has wheels, a chain and a bell. |
Classes, objects and encapsulation
A cookie cutter is not a cookie. It is the shape used to make many cookies. A class is the cutter: it says which data (called fields) every thing of this kind has and which actions (called methods) it can do. An object is one cookie made from it, also called an instance. Two accounts made from the same Account class have the same methods, but each one has its own balance.
class Account {
private int balance; // field: data of each object
Account(int start) { // constructor: runs when the object is born
this.balance = start;
}
int balance() { return balance; }
}
Account a = new Account(10);
Account b = new Account(99); // independent from aConstructors, this and static
- A constructor sets up a new object. It has the same name as the class and no return type.
- If you write no constructor, the compiler adds one with no arguments. As soon as you write any constructor, that free one disappears.
this.xmeans 'the field x of this object'. It helps when a parameter has the same name as the field.this(...)calls another constructor of the same class. It must be the first line of the constructor.- A
staticfield belongs to the class, not to an object. There is one copy for everybody, like the number on a school bell. A normal (instance) field exists once per object.
Order of construction
When new runs, Java first runs the constructor of the superclass (the class above, see inheritance). Then it runs the field initialisers and the { } initialisation blocks, in the order they are written in the file. Only at the end does the rest of the constructor body run. An int field that copies another field copies the value at that moment and stays unlinked afterwards.
Access levels
| Modifier | Who can use the member |
|---|---|
private | Only the class itself |
| no modifier (package-private) | Classes in the same package (folder of classes) |
protected | The package and the subclasses |
public | Everybody |
Passing objects and comparing them
Java passes everything by value. For an object, the value that is copied is the reference, like a paper with the address of a house. If a method uses the address to paint the door, the owner sees the new color. But if the method writes a new address on its own copy of the paper, the owner's paper does not change. In short: changing the object through a parameter is visible to the caller, reassigning the parameter is not.
== on two object variables asks 'is it the very same object?' (identity). equals is meant to ask 'do they have the same contents?', but the version inherited from Object also compares identity. A class must override equals (and hashCode) to compare fields. A Java record writes them for you.
Encapsulation
Encapsulation means the data of an object can only be reached through the methods of its own class. Then the class can protect its invariants, rules that must always be true, such as 'a balance is never negative'. It can also change how it stores things without breaking its users.
↑ Back to the contentsLife of an object: memory and garbage collection
Imagine your desk and a big storage room. Small notes stay on the desk and are thrown away when you leave. Bigger things go to the storage room. In Java, the desk is the stack: local variables and parameters live in the box of each method call and vanish when it returns. The storage room is the heap: every object made with new lives there. A variable like Point p holds only a reference (an address) to the object on the heap.
STACK (desk) HEAP (storage room)
┌─────────────┐
│ p ──────────┼───────► [ Point x=1 y=2 ]
└─────────────┘Who throws away objects nobody uses? In C++ with manual management, you write delete yourself. Forget it and you have a leak. Delete too early and you have a dangling pointer. Java gives this job to the garbage collector (GC).
What is garbage?
An object is garbage when the program has no way to reach it. The GC starts from the roots (local variables on the stacks and static fields), follows every reference, and marks what it reaches. What is not marked can be freed. This is tracing, or mark and sweep. Reachability is transitive: an object only pointed to by unreachable objects is unreachable too.
root a ──► [1] ──► [2] ──► [3]
after a.next = null :
root a ──► [1] [2] ──► [3] (2 and 3 are garbage)- Assigning
nullremoves only that one reference. If another path reaches the object, it stays alive. - Reference counting frees an object when its counter hits zero, but it cannot see cycles (A points to B and B points to A). Tracing, used by the JVM, handles cycles: if nobody reachable points to the pair, both are freed.
- Being eligible does not mean being collected right now. The virtual machine decides when.
Generations and leaks
Most objects die young (a temporary result, a loop variable) and the ones that survive tend to live long. This is the generational hypothesis. So the heap is split: new objects are born in a small young area (eden) that is cleaned often, survivors are promoted to the old generation, cleaned rarely.
↑ Back to the contentsInheritance: a class that builds on another
A puppy is a dog, and a dog is an animal. Everything true for animals is true for dogs, and dogs add their own tricks. Inheritance does this in code: class Manager extends Employee makes Manager a subclass of Employee (the superclass). It receives the members of the superclass and may add or change behaviour. Because it keeps every promise of the superclass, a Manager can be used wherever an Employee is expected. The opposite is false.
Object
│
Animal
╱ ╲
Dog Cat
│
Puppy- Java allows one superclass only (single inheritance). This avoids getting two versions of one method by two paths. To fulfil many contracts, a class
implementsseveral interfaces. Objectis the root and gives every classtoString,equalsandhashCode.- A subclass object contains the fields of the whole chain. A
privatefield of the superclass exists in it, but only the superclass methods can touch it. That keeps the superclass invariants safe. finalon a variable forbids reassignment, on a method forbids overriding, on a class forbids inheriting (likeString).
Constructors in a chain
Every constructor begins by calling another: this(...) or super(...). If you write neither, the compiler inserts super(). If the superclass has only constructors with parameters, that hidden call cannot work and the code does not compile: the subclass must call super(args) explicitly. So construction climbs up to Object, and the bodies run from the most general class to the most specific.
new C() where C extends B extends A
A body runs first -> B body -> C bodyOverriding rules
To override a method, the subclass repeats its signature with a new body. It may offer more than the superclass, never less: the access can stay the same or widen (protected to public), the return type can be a subtype, and the checked exceptions cannot be broader. This way any code written for the superclass still works.
Casting up and down
An upcast (Animal a = new Cat()) is implicit and always safe. A downcast ((Dog) a) must be written and is checked while running against the real class of the object: if a holds a Cat, it throws ClassCastException. Test first with instanceof, which in modern Java also declares the new variable: if (a instanceof Dog d).
Polymorphism and dynamic dispatch
Say 'make a sound' to a room with a dog, a cat and a cow. Each one answers in its own way, and you did not need to know which animal it was. That is polymorphism, 'many forms'. In code, we write against a general type (a superclass or an interface), for example rule.apply(total), and every object runs its own version. New kinds can be added without changing the caller.
Two types for one variable
In Shape s = new Circle(); the variable has a declared (static) type, Shape, and the object has a real (dynamic) type, Circle. The compiler uses the declared type to check that the call is allowed. The virtual machine uses the real type to pick the code. Choosing at run time is dynamic dispatch (also late binding). It is usually done with a per-class table of methods, called a vtable.
s.area()
declared type Shape -> compiler: does area() exist? yes
real type Circle -> run time: use Circle's area()
search starts at the real class and climbs:
Puppy -> Dog -> Animal -> ObjectIf a subclass does not override the method, the search climbs and finds the nearest superclass version. Objects of the superclass itself keep the original. Inside an override, super.draw() calls the version of the direct superclass of the class where it is written, with no dynamic dispatch. It lets you extend instead of replace.
Overloading is not overriding
| Point | Overloading | Overriding |
|---|---|---|
| What | Same name, different parameters, same class | Same signature, new body in a subclass |
| Chosen | At compile time, by the declared types of the arguments | At run time, by the real class of the receiver |
| Example | print(Object) and print(String) | Dog.sound() replaces Animal.sound() |
Java does single dispatch: at run time only the class of the object that receives the call counts, never the classes of the arguments. If you pass a String stored in an Object variable, the compiler picks print(Object) and it stays that way. When behaviour must depend on both objects, programmers use double dispatch (the Visitor pattern).
Why it matters: killing the if chains
If the same decision by type (if role == DEVELOPER ... else if role == DBA ...) is repeated in many methods, every new role means editing all of them, and forgetting one is easy. With polymorphism, each role is a class that implements a common interface and carries its own rules. Old code does not change: open for extension, closed for modification (the open-closed principle).
Example of rules in a list, applied in order, each starting from the result of the previous one: a price of 10000 cents, then 10% off, then 500 off, then 10% off again.
- start
- 10,000 cents
- 10% off
- 9,000 cents
- minus 500
- 8,500 cents
- 10% off
- 7,650 cents
Abstraction: interfaces and abstract classes
A wall socket does not care if you plug in a lamp or a toaster. It only defines the shape of the plug. An interface is the plug: it lists the method signatures that can be asked of an object, and the classes that implement it supply the code. Code written against the interface works with any implementation, present or future.
An abstract class is a recipe with a missing step. It can have fields, constructors and finished methods, but leaves one or more methods without a body, marked abstract. Because part of it is missing, you cannot create an object directly from it. The first concrete subclass must give a body to every inherited abstract method.
| Feature | Interface | Abstract class |
|---|---|---|
| Instance fields and constructors | No | Yes |
| How many a class can use | Many (implements A, B) | One (uses the only extends) |
| Best for | A role that unrelated types can play | Sharing state and code among close relatives |
Default methods and the diamond
Once, adding a method to an interface broke every class that implemented it. A default method brings a body that classes inherit, so forEach could enter the Java 8 collections without breaking old code. But a class may then receive two defaults with the same signature. Java settles it with three rules:
- A method from a class always beats one from an interface (the 'class wins' rule). So a
defaultcan never change a class that already had that method, and cannot redefinetoStringorequals. - A more specific interface beats the one it extends.
- If there is still a tie, the class does not compile until it overrides the method itself.
Template Method
The abstract class can fix the order of the steps in a final method that subclasses cannot change, and leave some steps abstract or overridable. The subclass fills the gaps, and the overall order stays guaranteed by the superclass. This is the Template Method pattern.
abstract class Game {
final void play() { // order is fixed
start(); turn(); end();
}
void start() { } // optional step
abstract void turn(); // each game fills this
void end() { }
}Two design principles
- Dependency inversion: business rules and details should both depend on abstractions. If
Checkoutdoesnew SaoPauloTax()inside, it is glued to that class. Receive aTaxinterface through the constructor (constructor injection) and it can use any tax. - Interface segregation: prefer small interfaces, one per role. A wide
Documentinterface (read, write, delete) forces a read-only class to throwUnsupportedOperationException, which breaks whoever trusts the contract. Split it, and let methods ask for the narrowest type they need (Readablefor apreview).
Composition against inheritance
There are two ways to reuse a class. Inheritance says 'is a': a dog is an animal. Composition says 'has a': a car has an engine. A car cannot be used where an engine is expected, so Car extends Engine is wrong. The deciding question is can one be used in place of the other? If yes, inheritance can be considered. If not, hold the other object in a field and delegate work to it.
class Car {
private final Engine engine; // has-a
Car(Engine engine) { this.engine = engine; }
void start() { engine.ignite(); } // delegates
}Aggregation and composition in UML
Both are whole-part links. In aggregation (hollow diamond) the part has its own life and may belong to several wholes: a team and its athletes. In composition (filled diamond) the part belongs to one whole, which creates and destroys it: a building and its flats, an order and its lines. Which one you choose depends on the system's requirements.
Why 'prefer composition over inheritance'
- Strongest coupling. A subclass depends on how the superclass is written, not only on what it promises. With composition, you only use the public interface and can even swap the object while running.
- Fragile base class. A
CountingBagthat overridesaddandaddAllcan count items twice if the superclassaddAllitself callsadd. That internal detail is not in the contract and may change. If the counter instead holds a bag and delegates, the inner calls never come back to it. - Too much inherited.
Stack extends ArrayListalso inheritsadd(0, x)andremove, so a client can break the last-in-first-out rule. With composition the class exposes onlypushandpop. The realjava.util.Stack(extendsVector) has this flaw. - Behaviour fixed at birth. Inheritance decides behaviour when the object is created. With composition, the varying part lives in another object behind an interface, and putting a different object in the field changes behaviour while running. It also avoids a subclass explosion when there are several axes (shipping and tax).
The Liskov substitution principle
A program written for the base type must stay correct with any subtype. In maths every square is a rectangle. But if Rectangle has setWidth and setHeight, a mutable Square that changes both sides breaks the user who sets 5 by 4 and expects area 20. So 'is-a' in inheritance is about behaviour, not about how things are classified in the real world.
| Part of the contract | A subtype may | Example that breaks it |
|---|---|---|
| Precondition (what it asks) | Be equal or weaker: accept at least as much | Base accepts any positive amount, subclass refuses amounts above 1000 |
| Postcondition (what it guarantees) | Be equal or stronger: promise at least as much | Base guarantees a non-negative result, subclass may return a negative one |
Coupling, cohesion and code smells
A good toolbox keeps hammers with hammers and screws with screws. That is cohesion: the parts of a class work towards one goal. A cohesive class has one reason to change (the single responsibility principle), is small and easy to name and test. A sign of low cohesion is a class whose methods use different sets of fields.
Coupling is how much one class depends on others, like wires between boxes. You cannot remove it, since objects must work together, but you can choose it: depend on few things, and on stable ones such as interfaces. The classic goal is high cohesion and low coupling.
- Afferent coupling: how many classes depend on me. If it is high, I carry a lot of responsibility and changing my public interface is risky.
- Efferent coupling: how many classes I depend on. If it is high, I have many outside reasons to change.
- A class used by 20 others and using none is the riskiest one to change in its interface. Point dependencies toward stable modules.
Code smells
A code smell is a pattern that suggests code is hard to understand or change. The program works and the tests pass, but the next change will be slow. Refactoring means changing the structure without changing the behaviour, and the proof is that the same tests pass before and after.
| Smell | What it looks like | Usual cure |
|---|---|---|
| Long Method | 40 lines in blocks separated by comments | Extract Method: each block becomes a named method |
| God Class | One class validates, computes, e-mails and reports; hard to test | Extract Class: one per responsibility, the old one coordinates |
| Feature Envy | A method uses only getters of another class | Move Method to the class that owns the data (tell, don't ask) |
| Shotgun Surgery | One change needs edits in many places (switches for payment methods) | Gather them: an interface with one class per method |
| Primitive Obsession | Strings and ints for e-mail, tax number, money; swapped arguments compile | Small types (value objects) that validate in the constructor |
| Conditional chain on a type code | Same if chain by kind in many methods | Replace with polymorphism |
For this repository's measurement, adding a fourth kind of delivery to a checkout:
- if chains
- 4 files
- polymorphism
- 1 files
The trade is not free: with polymorphism, a new kind is one new class, but a new operation on all kinds edits the interface and every class. Choose by what changes more often.
Cyclomatic complexity
It counts the independent paths through a method, so it hints at how many tests you need. In the quiz, start at 1 and add 1 for every `for`, `if`, ternary `?:`, `&&` and `||`. A method with one if and one for has complexity 3. High values suggest extracting methods or using polymorphism.
Exceptions: when something goes wrong
In a relay race, if a runner drops the baton, she shouts and the team stops its normal plan until someone knows what to do. An exception is that shout. Throwing it interrupts the normal flow, and the virtual machine unwinds the call stack method by method, looking for a catch that matches. If it reaches the top without finding one, the thread ends and the error is printed. This lets a low-level method detect a problem and a higher level, which knows what to do, handle it, with no return codes passed hand to hand.
main() ── has catch (IOException)
└ load()
└ read() <- throws IOException here
search: read -> load -> main (found!)Checked and unchecked
Everything thrown descends from Throwable. Checked exceptions (for example IOException) are failures that can happen even in a correct program, like a missing file. The compiler forces you to handle them or declare them with throws. Unchecked ones (RuntimeException and its children, such as NullPointerException) usually show a bug, and Error is a serious failure of the machine. The compiler does not force you to catch them.
try, catch, finally
- When a statement in
trythrows, the rest of thetryis skipped. Control goes to the first matchingcatch, then tofinally, and then the program continues after the whole structure. There is no way back to the failing line. - The
catchblocks are tried in order and the first whose type equals the exception or is a superclass wins. So put the most specific first. Putting a superclass before its subclass does not compile, because the second block would be dead code. finallyruns however control leaves thetry: normal end,return, caught or uncaught exception. It does not run only if the machine stops (for exampleSystem.exit).
static int value() {
int x = 1;
try { x = 2; return x; } // result 2 is saved first
finally { x = 3; } // too late to change it
} // returns 2Try-with-resources
For files and connections, write try (Res a = ...; Res b = ...). Objects that implement AutoCloseable are closed for you when the block ends, with or without an exception. They are closed in the reverse order of opening, like a stack, because a later resource may use an earlier one.
Translate, but keep the cause
Each layer should speak its own language: whoever calls place(order) does not need to know SQL, so you catch SQLException and throw OrderNotSavedException. The new exception must carry the original as its cause: new OrderNotSavedException(msg, e). This is exception chaining; the printed trace shows both, joined by Caused by. Without it, the diagnosis is lost.
Generics and collections
Labeled boxes are better than mystery boxes. A List<String> is a list labeled 'strings only'. The compiler rejects add(42) and knows that get(0) returns a String. Without the label, everything is an Object, and every read needs a cast that may fail while running. Generics let you write a class or method once and use it with many types, still checked.
The main collections
| Interface | Contract | Common implementations |
|---|---|---|
List | Ordered by position, duplicates allowed | ArrayList, LinkedList |
Set | No duplicates | HashSet (no order, fast), LinkedHashSet (insertion order), TreeSet (sorted, O(log n)) |
Queue / Deque | Waiting line, one or both ends | ArrayDeque |
Map | Unique keys, each with one value | HashMap, TreeMap |
In a Map, put with a new key adds an entry. With a key already there, it replaces the value and returns the old one, so the size counts distinct keys. TreeSet sorts by natural order (Comparable) or by a Comparator.
Rules of the Java generics
- Only reference types.
List<int>does not compile. Use the wrapperInteger(andDouble,Boolean). The compiler converts in both directions (autoboxing and unboxing), at the cost of one object per element. - Invariance.
Integeris a subtype ofNumber, butList<Integer>is not aList<Number>. If it were, you could add a3.14into a list of integers. - Bounds. An unbounded
Tonly has the methods ofObject.T extends Comparable<T>unlockscompareTo, somax(a, b)works for any type that compares with its own kind. - Wildcards.
? extends Tfor things you only read from (a producer).? super Tfor things you only write to (a consumer). Memory aid PECS: producer extends, consumer super. Copying fromList<Integer>toList<Number>usescopy(List<? super T> dst, List<? extends T> src). - Type erasure. After checking, the compiler replaces
Tby its bound (orObject) and inserts casts. The compiled code does not know whatTwas. SoList<String>andList<Integer>have the same class at run time, and you cannot writenew T(),new T[10]orx instanceof List<String>.
Fail-fast iterators
Collections of java.util count their modifications. If you remove from a list inside a for-each loop, the iterator notices and throws ConcurrentModificationException. The name is misleading: no second thread is needed. To remove while looping, use the iterator's own remove() or removeIf.
Streams, input and output, serialisation
A stream is a garden hose: data flows in one direction, and you take it piece by piece. A byte stream (InputStream, OutputStream) moves raw 8-bit numbers with no meaning. A character stream (Reader, Writer) adds the encoding, the rule that turns bytes into letters. Using the wrong kind on text corrupts accented letters.
Byte and character are the same only for plain ASCII. UTF-8 is variable length: the first 128 characters take 1 byte, an accented letter such as ç takes 2, the euro sign 3, an emoji 4. To count bytes of a text, add the size of each character (the word ação has 4 characters but 6 bytes). Read text with a character stream and state the encoding, or you will cut accented letters in half.
- a
- 1 bytes
- ç
- 2 bytes
- €
- 3 bytes
- emoji
- 4 bytes
Buffers
Going to the operating system for every single character is like walking to the library for each word. A buffered stream (BufferedReader) brings a whole page at once and hands out letters from memory. Writing is similar, with one warning: data reaches the destination only when the buffer fills, or on flush() or close().
Serialisation
An object in memory is a web of references that only makes sense inside the running program. To save it in a file or send it over a network, you serialise it: turn its state into a line of bytes (Java binary, with ObjectOutputStream) or text (JSON, XML). A Java class opts in by implementing the marker interface Serializable.
transientleaves a field out: use it for passwords, caches, connections. When the object is read back, it is rebuilt without calling the constructor of the serialisable class, and transient fields come back asnull,0orfalse.- The stream also stores the class name and a version number. On reading it compares with the loaded class. If you changed the class and declared no
serialVersionUID, the computed number changes and reading old files throwsInvalidClassException. Declaringprivate static final long serialVersionUIDlets the class evolve compatibly.
OOP across languages: traits, interfaces, protocols
Not every language has classes and inheritance like Java. But they all need ways to group data and behaviour and to let different things answer the same request. Here is how some do it.
| Language | How it does polymorphism | Reuse |
|---|---|---|
| Java, C# | Nominal interfaces: a class must declare implements | Inheritance and composition |
| TypeScript | Structural interfaces: matching members are enough | Classes and composition |
| Go | Structural interfaces, no classes, no inheritance | Composition |
| Rust | Traits on structs and enums, static (T: Display) or dynamic (dyn Trait) | No implementation inheritance |
| Elixir | Protocols (defprotocol) implemented per data type | Functions and modules, no classes |
Nominal against structural
In a nominal system (Java), a value belongs to a type because its class says so by name. In a structural system (TypeScript, Go), having the needed members is enough. A Book with a price fits Priced in TypeScript without saying implements, and a plain object { price: 5 } fits too, which is handy for test doubles. The same class without implements is refused in Java.
interface Priced { price: number }
class Book { constructor(public price: number) {} }
const items: Priced[] = [new Book(10), { price: 5 }]; // both acceptedNominal typing records intent and avoids coincidences (two run methods that mean different things). Structural typing gives flexibility and needs no adapters for types you do not own. In Go, a small interface can be defined on the consumer side, and an existing type, even from another library, satisfies it unchanged.
The expression problem
Objects group code by variant (one class per kind). Adding a kind is easy, adding an operation means editing every class. Functions with pattern matching group code by operation. Adding an operation is easy, adding a kind means editing every function. This repository measured it: adding 'take and pay' to a cart was one new file with objects, and four edits inside one file with functions. The opposite change has the opposite cost.
↑ Back to the contentsPutting it all together
All the tools of OOP answer one question: where does each piece of knowledge live, and how does a change spread? Use this table when you meet a quiz question.
| If the problem is... | Think of... | Because... |
|---|---|---|
| Outsiders break my rules | Encapsulation, defensive copy | Rules live next to the data |
The same if by kind everywhere | Polymorphism | Each kind carries its own rules |
| Hard to swap a dependency | Interface and constructor injection | Depend on abstractions |
| Subclass breaks when the parent changes | Composition | Inheritance is the strongest coupling |
| Reading old binary files fails | serialVersionUID | The format is coupled to the class |
| Memory grows forever with a GC | Reachability, static collections | GC frees only the unreachable |
| Which method runs? | Declared type for overloads, real type for overrides | Two different moments |
Common mistakes
- Believing that
privateis enough while a getter returns the internal list. - Mixing up overloading (compile time) and overriding (run time), and forgetting that static methods and fields are not overridden.
- Using inheritance only to reuse code, or because 'a square is a rectangle'.
- Calling an overridable method from a constructor.
- Catching a superclass exception before its subclass, or throwing a new exception without the cause.
- Thinking
List<Integer>is aList<Number>, or that the typeTstill exists at run time. - Trusting the garbage collector to close files.