Lecture #2: Basic Concepts of Object-Oriented Programming
Objects, classes, messages, encapsulation, and a first look at inheritance — with Java examples.
Introduction
Before you can write good object-oriented code, you need a mental model of what an “object” actually is and why programmers bothered inventing this style of programming in the first place. This chapter builds that model from scratch. We will start by looking at how software was traditionally organized before objects existed — the procedural style, familiar from C — and see the specific pain points that motivated a different way of thinking. Then we will introduce objects, classes, instances, and messages one at a time, with small Java examples you can run and modify yourself. We’ll finish with the first, simplest form of inheritance: how one class can build on top of another.
Nothing here requires you to have programmed in Java before, but basic familiarity with variables, functions/methods, and types (int, boolean, etc.) is assumed.
1. The problem: designing software that models a real system
Imagine you are asked to write software that manages a small library: books that can be checked out, readers who borrow them, due dates, fines for late returns, and so on. This is a good example to keep in mind throughout the chapter, because it’s the kind of system where the software has to represent a chunk of the real world — books, people, loans — and the “things” in that world have both data (a book has a title, an ISBN, a status) and behavior (a book can be checked out, a loan can be extended, a reader can be fined).
Any real system you might model in software — a library, a fleet of vehicles, a set of bank accounts, the UI of an application — has this same shape: a collection of entities, each with its own characteristics (data) and its own capabilities (behavior). The central design question of this chapter is: how do we organize the data and the behavior in our program so that they mirror this structure well?
2. Procedural design: data and functions live apart
You already know this style from C: a program is { data } + { functions } — structs hold information, free functions take a pointer to a struct and operate on it (checkOut(&book), printBook(&book)). Nothing in the language ties a function to “its” struct; that link exists only by convention and by the programmer’s discipline. Any code anywhere can reach into book.isCheckedOut directly, bypassing checkOut entirely, and nothing stops printBook from being handed a struct that isn’t really a book. Picture this as two separate regions of a program — a “data” area and a “functions” area — connected only by parameter passing.
This works fine at small scale, but as a codebase grows to hundreds of structs and thousands of functions, two costs recur: no protection (any code can corrupt a struct into an inconsistent state), and no natural grouping (the functions for one “kind of thing” spread across files, so “what can I do with a Book?” means hunting for every function that happens to take a Book*).
3. Object-oriented design: fuse data and behavior into one unit
Object-oriented programming addresses this by eliminating the separation. Instead of keeping data and functions in two different regions of the program, we bundle the data that describes one entity together with the functions that operate on that data into a single unit called an object.
program = { objects interacting with each other }
An object is, informally, “a struct that also carries around the functions that know how to work on it, and that hides its internal fields from the outside.” Concretely, an object has two parts:
- State (data): its own private set of variables, called instance variables (also called fields or attributes). This is the object’s own little memory area — no other object can see or touch it directly.
- Behavior (functions): its own functions, called methods, which are the only way to read or change that state from the outside. A method runs “inside” the object, with direct access to that object’s instance variables.
This is the fundamental shift: instead of asking “which function do I call, and which struct do I pass it?”, you ask “which object do I ask to do something?” The object itself is responsible for keeping its own data consistent — nobody else is allowed to fiddle with it directly. Instead of a shared data region and a shared function region, each object now carries its own data and its own functions together, as a single unit.
4. Classes: the blueprint for a kind of object
If every object carries its own data and functions, we need some way to describe, once, what kind of data and what kind of functions a particular category of object has. That description is called a class.
A class is a blueprint (a template, a type) from which individual objects are created. Every object that exists in an object-oriented program is an instance of some class — objects don’t appear out of nowhere; you first describe a class, and then create objects that follow its shape.
In Java, a class looks like this:
class ClassName {
// instance variables (fields) — the object's state
Type field1;
Type field2;
// methods — the object's behavior
ReturnType methodName(Type parameter1, Type parameter2) {
// local declarations
// method body
}
}
Let’s write a Book class for our library example:
class Book {
// instance variables
String title;
String isbn;
boolean checkedOut;
// methods
void checkOut() {
checkedOut = true;
}
void returnBook() {
checkedOut = false;
}
boolean isAvailable() {
return !checkedOut;
}
void display() {
System.out.println(title + " (" + isbn + ") - "
+ (checkedOut ? "checked out" : "available"));
}
}
Compare this to the procedural version above. The fields (title, isbn, checkedOut) and the functions that work on them (checkOut, returnBook, isAvailable, display) are no longer two separate things connected by convention — they are written inside the same class, and the methods no longer need a Book parameter, because they will always run “attached to” a particular book. Also notice there is no setCheckedOut(boolean) method — on purpose. The class only exposes the operations that make logical sense (checkOut, returnBook), not raw field access. This is a first, small example of a design decision that is central to OOP: the object decides what can be done to it, rather than exposing its internals for anyone to modify however they like.
Here is a second class, a bit richer, to reinforce the idea — a Rectangle described by two corner points:
class Point {
double x, y;
}
class Rectangle {
// fields
Point topLeft, bottomRight;
// methods
double width() {
return bottomRight.x - topLeft.x;
}
double height() {
return bottomRight.y - topLeft.y;
}
double area() {
return width() * height();
}
void display() {
System.out.println("Rectangle " + width() + " x " + height());
}
}
Notice that Rectangle uses Point as the type of its fields — a class can use another class as a building block, the same way int or double are used as field types. This is completely ordinary in OOP: most classes are built out of other classes. Also notice that Rectangle.display() and Book.display() have the same name but completely different bodies — each class defines “how to display myself” in whatever way makes sense for it. We’ll come back to this idea (it’s called polymorphism) in section 7.
5. Instances: many objects, one class
A class by itself doesn’t do anything — it’s just a description. To actually get an object you can use, you must instantiate the class, i.e. ask the runtime to create a new object that follows that blueprint. In Java this is done with the new keyword:
new Book()
This expression creates a brand-new Book object in memory, with its own independent copies of title, isbn, and checkedOut, and returns a reference to it, which you typically store in a variable:
Book b1 = new Book();
Book b2 = new Book();
Here b1 and b2 are two different objects, both instances of the same class Book. They share exactly the same set of methods (both know how to checkOut(), returnBook(), display(), …) because that behavior comes from the class definition, which is written only once. What differs between b1 and b2 is purely their state — the actual values currently sitting in their instance variables. If you set b1.title = "Effective Java" and b2.title = "Clean Code", the two objects diverge because their data diverges, even though they run identical code.
This is the key relationship to internalize: the class defines the shape and the behavior (written once); each instance has its own state (as many copies as you create).
A short example putting this together, mirroring how a main method might use these classes:
public class LibraryDemo {
public static void main(String[] args) {
Book b1 = new Book();
b1.title = "Effective Java";
b1.isbn = "978-0134685991";
b1.checkOut();
b1.display(); // Effective Java (978-0134685991) - checked out
Rectangle rec = new Rectangle();
rec.topLeft = new Point();
rec.bottomRight = new Point();
rec.bottomRight.x = 10;
rec.bottomRight.y = 5;
rec.display(); // Rectangle 10.0 x 5.0 — a completely independent object
}
}
6. Messages: how objects talk to each other
If objects hide their data and only expose behavior through methods, how does the rest of the program get anything done? The answer is: by sending messages. In object-oriented terminology, calling a method on an object is called “sending a message to that object.” The syntax is:
receiverObject.methodName(arguments);
When this executes, the receiver object is asked to run the method named methodName, using the arguments provided, and — crucially — that method executes inside the receiver’s own environment, meaning it has direct access to that specific object’s instance variables. The set of all methods a class makes available is sometimes called that class’s protocol: the complete list of messages you’re allowed to send to any instance of it.
Book b = new Book();
b.title = "Refactoring";
b.checkOut(); // send the "checkOut" message to b
System.out.println(b.isAvailable()); // false
b.returnBook(); // send the "returnBook" message to b
System.out.println(b.isAvailable()); // true
Each line that ends in .methodName(...) is a message send: it names a receiver (b), a method, and possibly arguments, and the method body runs using b’s own fields. If you had a second book Book b2 = new Book();, sending it the same messages would affect its fields, not b’s — each object’s state is genuinely separate.
Try predicting the output of this sequence before reading the comment:
Book b = new Book();
b.title = "The Pragmatic Programmer";
System.out.println(b.isAvailable()); // true (checkedOut starts as false)
b.checkOut();
System.out.println(b.isAvailable()); // false
b.checkOut(); // checking out an already-checked-out book...
System.out.println(b.isAvailable()); // still false
b.returnBook();
System.out.println(b.isAvailable()); // true
This little exercise highlights something important: because the object controls its own transitions, you get the chance to build validation and rules into the methods themselves (for example, checkOut() could refuse to do anything if the book is already checked out) — something that’s much harder to guarantee when any code can write directly into a struct’s fields.
7. Two ways of writing the same idea, and why the OOP way wins
Let’s compare the object-oriented style directly against the procedural style for the exact same operations, to make the “inversion” concrete:
| Object-oriented | Procedural |
|---|---|
b.checkOut() | checkOutBook(b) |
b.isAvailable() | isBookAvailable(b) |
b.display() | displayBook(b) |
rec.display() | displayRectangle(rec) |
Superficially these look like trivial rewordings, but the underlying design consequence is significant, and it shows up in three related ideas:
Encapsulation. In the OOP version, nothing outside the Book class can set checkedOut directly — the field can be made inaccessible from outside by marking it private (private boolean checkedOut;), so the only way to change a book’s status is by sending it a message (checkOut(), returnBook()), which means the class itself can guarantee its data never ends up in a nonsensical state. In the procedural version, any function anywhere could write b->isCheckedOut = 42; and nothing would stop it. (The examples in this chapter leave fields unmarked for brevity — the full set of Java visibility keywords, private included, gets its own chapter later in the course.)
Polymorphism. Notice that both Book and Rectangle have a method called display(), but each does something completely different, appropriate to what it represents. In the procedural style you’re forced to give these different names (displayBook, displayRectangle) precisely because there’s no object to disambiguate which behavior you mean. In OOP, the receiver itself determines which display() gets run — you send the same message (display()) to different kinds of objects, and each responds in its own way. This means code that works with “a bunch of displayable things” doesn’t need to know or care what specific kind of thing each one is.
Modularity. Because a class packages its data and behavior together, and hides the data, you can change how a Book stores its state internally (say, replace the boolean checkedOut with a Loan object that also tracks the borrower and due date) without touching any code elsewhere in the program that just calls book.checkOut() and book.isAvailable(). As long as the protocol (the set of methods) doesn’t change, callers are insulated from implementation changes. This is what allows large object-oriented systems — hundreds or thousands of classes — to be developed and modified by different people without everyone needing to know everyone else’s internals.
In short: procedural programming asks “which function do I call, and what data do I pass it?” Object-oriented programming inverts this to “which object do I ask, and what do I ask it to do?” The data has effectively become active — it knows how to do things to itself, rather than being an inert bag of fields waiting for external code to work on it.
8. this: an object referring to itself
Frequently, one method of an object needs to call another method of that same object. To do this, an object can send a message to itself using the special reference this, which always denotes “the object currently executing this method”:
this.methodName(arguments);
this only makes sense inside the body of a method — it refers to whichever object received the message that triggered that method’s execution. Since sending a message to yourself is extremely common, Java lets you drop this. and simply write methodName(arguments) — the compiler understands this as an implicit self-message.
Here’s Rectangle extended with perimeter(), which is naturally expressed in terms of the methods we already wrote:
class Rectangle {
Point topLeft, bottomRight;
double width() {
return bottomRight.x - topLeft.x;
}
double height() {
return bottomRight.y - topLeft.y;
}
double area() {
return this.width() * this.height(); // explicit this
}
double perimeter() {
return 2 * (width() + height()); // this is implicit here
}
}
And in our Book class, we could route every status check through a single method, so that if the rule for “is this book available” ever gets more complicated (e.g. reserved books aren’t available even if not checked out), there’s only one place to update:
class Book {
String title;
boolean checkedOut;
boolean reserved;
boolean isAvailable() {
return !checkedOut && !reserved;
}
void display() {
// reuse isAvailable() instead of re-deriving the same logic here
System.out.println(title + " - " + (isAvailable() ? "available" : "unavailable"));
}
}
Composing an object’s methods out of its other methods (via this, explicit or implicit) is completely normal — in fact, well-designed classes are usually a small number of “core” methods with everything else built on top of them.
9. A first look at inheritance: subclasses extend classes
So far every class we’ve written has stood alone. But very often, one kind of object is really “a more specific version” of another kind. Think about extending our library: a regular Book can be checked out and returned. An AudioBook is a book too, but it also has a duration in minutes and a narrator’s name, and needs an extra method to report its length. Rather than writing a whole new, unrelated class and duplicating everything Book already does, we can define AudioBook as a subclass of Book:
class AudioBook extends Book {
// additional instance variables
String narrator;
int durationMinutes;
// additional methods
void printDuration() {
System.out.println(durationMinutes + " minutes, narrated by " + narrator);
}
}
The keyword extends establishes an inheritance relationship: AudioBook is the subclass, Book is the superclass. This gives every AudioBook object everything a Book already has — the fields title, isbn, checkedOut, and the methods checkOut(), returnBook(), isAvailable(), display() — plus its own additional field narrator, durationMinutes, and its own additional method printDuration(). You never need to retype or copy-paste the inherited members; they simply come along for free:
AudioBook ab = new AudioBook();
ab.title = "Dune"; // inherited field, works fine
ab.narrator = "Scott Brick"; // AudioBook's own field
ab.durationMinutes = 1230;
ab.checkOut(); // inherited method
ab.printDuration(); // AudioBook's own method
System.out.println(ab.isAvailable()); // inherited method, still works
Conceptually, inheritance expresses “an AudioBook is a Book, with something extra” — it’s how you capture the everyday intuition that some categories are specializations of a broader category, without re-describing the broad category’s structure and behavior every time.
To make the mechanism a bit more concrete, here’s a second inheritance example working through two related classes, a basic answering machine and one that can also record messages — a good illustration because the subclass both reuses and adds to the superclass’s behavior:
class AnsweringMachine {
String greeting;
void setGreeting(String text) {
greeting = text;
}
void playGreeting() {
System.out.println("Greeting: " + greeting);
}
void clearGreeting() {
greeting = null;
}
}
class RecordingAnsweringMachine extends AnsweringMachine {
java.util.List<String> messages = new java.util.ArrayList<>();
void recordMessage(String msg) {
messages.add(msg);
}
void playMessages() {
for (String m : messages) {
System.out.println("Message: " + m);
}
}
void clearMessages() {
messages.clear();
}
void reset() {
this.clearGreeting(); // inherited method, invoked via this
this.clearMessages(); // own method
}
}
Look closely at reset(): it calls clearGreeting(), which RecordingAnsweringMachine never defined itself — it was inherited from AnsweringMachine — right alongside clearMessages(), which is defined locally. From inside the subclass, this doesn’t distinguish between “methods I wrote” and “methods I inherited”: they’re all simply part of what the object can do. That’s the practical payoff of inheritance — a RecordingAnsweringMachine object genuinely has all the capabilities of a plain AnsweringMachine, automatically, and adds its own on top.
A good exercise here: sketch this as a class diagram, and work out how an AnsweringMachine object’s memory layout differs from a RecordingAnsweringMachine object’s, and which messages each can receive.
This is only the first, simplest layer of inheritance — later material covers overriding inherited methods, super, and the root Object class that every Java class ultimately descends from — but the core idea to take away here is: a subclass automatically has everything its superclass has, and can add new fields and new methods on top.
Summary
- Traditional procedural design keeps data (structs) and functions in two separate spaces, connected only because functions take the data as a parameter. Nothing stops code from reading or corrupting a struct’s fields directly, and there’s no language-enforced grouping of “which functions belong to which data.”
- Object-oriented design fuses data and behavior into a single unit, the object: its instance variables hold its private state, and its methods are the only sanctioned way to inspect or change that state.
- A class is the blueprint — written once — that describes the fields and methods a category of object will have. An instance (created with
new) is one concrete object built from that blueprint; many instances of the same class share identical behavior but each has its own independent state. - Calling a method on an object is called sending it a message:
receiver.method(args). The method executes using the receiver’s own instance variables. The full set of messages a class understands is its protocol. - Comparing OOP to procedural code side by side reveals three intertwined benefits: encapsulation (no uncontrolled outside access to an object’s data), polymorphism (the same message name can trigger different, receiver-appropriate behavior — e.g. every class can have its own
display()), and modularity (an object’s internal implementation can change freely as long as its protocol doesn’t). - Inside a method, an object can send itself a message using
this(often left implicit); this is how methods of the same class build on one another. - Inheritance (
class Sub extends Super) lets one class automatically acquire all the fields and methods of another, while adding its own on top — the natural way to express “this kind of object is a more specific version of that kind of object.”
With these building blocks — object, class, instance, message, encapsulation, and basic inheritance — you have the vocabulary needed to read and write straightforward Java class hierarchies, and a foundation the next chapters will build on (method overriding, super, abstract classes, and the Object root class).