Introduction
This chapter prepares the ground for the code we will write later. We will talk about objects, classes, responsibilities, encapsulation, and inheritance without pretending to exhaust topics about which entire books have been written. If this is the first time you have encountered these words, do not try to memorize them right away: try to follow the problem that makes them useful.
Imagine a small library. A reader asks for a book, someone checks whether it is available and, if it is, records the loan. We can describe the work as a sequence of statements, as a set of functions, or as a collaboration between entities representing books, readers, and loans. These are different ways of looking at the same problem. Java lets us use them together; object orientation is the thread we will use to organize the application.
Definition – Programming paradigm. A way of formulating problems and organizing solutions. It is neither a list of keywords nor a guarantee of quality. A good program may contain procedural parts, functions, and objects, provided the choices are consistent with the problem.
A Step Back from the Machine to Concepts
The history of languages is not a staircase on which every step makes the previous one useless. Machine language describes instructions executable by a processor; assembly gives symbolic names to instructions and data, but remains close to the machine's architecture. Higher-level languages let us express algorithms, structures, and constraints without rewriting them each time as a sequence of elementary instructions. They remain languages, however: none understands on its own what we mean by available book or valid loan. We have to build that meaning ourselves.
Let us pause to consider Ada Lovelace and Charles Babbage, because the question that accompanied them is ours: how do we describe a sequence of operations to a machine without confusing the sequence with the problem we want to solve? In the notes accompanying her translation of an article on Babbage's Analytical Engine, published in 1843, Lovelace also presented a table of instructions for calculating Bernoulli numbers. The reproduction and explanation preserved by the Computer History Museum show the historical document. You do not need to know those numbers to grasp the point: a complex calculation is broken into steps that a machine designed to execute them could have followed. Simply calling it “the first program” would erase historical debates that we do not need here; what interests us is the idea of making operations and data explicit.
Another important name in this historical perspective is Konrad Zuse. His Plankalkül was a very early design for a high-level language, recalled in the Computer History Museum's historical profile. Do not imagine a straight line from those ideas to Java, as if every language had been born to correct the previous one. The problems, available machines, and purposes were different. This brief historical look shows that the search for expression closer to the problem has accompanied programming since long before modern object-oriented languages.
The figure compares the two ways of describing computation through a diagram, without presenting itself as a facsimile of the documents. In the Lovelace panel, look at the three columns: an operation uses variables and leads to a result. In the Zuse panel, observe the different ambition: a notation for describing data and rules at a level less tied to the machine's immediate instructions. The two documents are not stages in a direct line of descent toward Java; they are historical evidence of questions that will return in our library example.
Let us therefore start with what the processor executes. In machine language, instructions are encoded in a form specific to the architecture: a program built for one processor family does not automatically become executable on another. Assembly language replaces many numerical encodings with symbolic names for operations, registers, and labels. If we want to add two values, instead of remembering the instruction's entire encoding, we can write an operation with a mnemonic name. This helps a person read the program, but does not remove the need to reason about registers, addresses, and instructions available on that machine. An assembly example always requires the architecture to be stated: there is no single assembly syntax valid for every processor.
A small “Hello, world!” in assembly shows which details the programmer must follow. This is a historical example for 32-bit Linux on x86, written in NASM syntax and using the old system calls through int 0x80; it is not a Java listing and should not be read as a recipe for a modern system.
section .text
global _start
_start:
mov edx, len
mov ecx, msg
mov ebx, 1
mov eax, 4
int 0x80
mov eax, 1
mov ebx, 0
int 0x80
section .data
msg db 'Hello, world!', 0xa
len equ $ - msg
The final two lines define the message and calculate its length. The mov instructions place the data required by the write call into registers: length, message address, destination, and operation number. int 0x80 passes control to the operating system; the second call terminates the process. Even to print a sentence, we therefore need to know where the data is and how that platform requests output. In the library program, by contrast, we would like to reason first about the request for a loan. The distance between these two levels explains the usefulness of subsequent abstractions, without erasing the fact that the program must ultimately be executed by a machine.
With higher-level languages, we can give a procedure a name such as recordLoan and let the compiler and execution environment translate the description into the necessary operations. The distance shrinks, but does not disappear: the computer does not know on its own that a copy already on loan cannot be lent again. That is a rule of our problem. The same was true of the historical example of C, a language born in a context different from Java's: it offered tools for composing procedures and working close to the machine, but the quality of the solution still depended on how the programmer organized data and responsibilities.
Further reading – Why look back. History does not demonstrate that each paradigm is “better” than its predecessor. Rather, it shows that people have invented different abstractions to manage complexity. An abstraction is a description that leaves some details in the background to focus on those that matter at that moment. Assembly hides part of the numerical encoding of instructions; a procedure hides the steps needed for a task; an object may hide how it stores its state. In each case, we still need to know which details become important again when something does not work.
Looking at the Work as a Procedure
Let us return to the library. We can write the flow as four steps: receive the request, find the book, check its availability, and record the loan. The figure shows this order. A procedure groups statements that perform a task; a function is a form of procedure that produces a result usable by its caller. In everyday language, the terms sometimes overlap, but here we care about the practical question: what must each piece of the program receive, and what must it return?
Dividing the work into small steps makes the program easier to read and correct. The danger arises when every step can freely modify shared data. If, for example, the number of active loans is a variable accessible to any part of the application, whoever records a loan may update it while another part changes it for a different reason. The error does not arise because the program is procedural: it arises from a hidden dependency between operations and state.
Important note – Global data is not mandatory. A procedure can receive the necessary data as parameters and return a result. A procedural program can also be modular and well designed. The statement “procedures necessarily use global variables” would give a false picture of the paradigm.
We call this way of approaching a problem divide and conquer: break what seems too large into smaller tasks, each simple enough to understand and check. The metaphor does not yet say how to divide. If we split a long function into ten functions that all read the same global variable, we have obtained shorter files but no clearer responsibilities. If, instead, a function receives the requested book and returns an outcome stating whether the loan was recorded, its caller can reason about its contract without inspecting every internal statement.
The distinction between parameter, return value, and shared data will be easier to see in the code in later chapters, but we can already follow it in words. A parameter is information that the caller passes to the procedure; a return value is the answer the procedure gives back to the caller. Shared data, by contrast, remains accessible from several parts of the program even when it does not appear in the call. It may be useful, but it is a dependency that must be designed and declared. In a library, the catalog may be shared data; the title the reader is looking for is more naturally a search parameter. The search function can return a list of the copies found.
Following an Error Rather Than Merely Naming It
Imagine that the library stores the number of active loans in a shared variable. The recordLoan procedure adds a record and increases that number. The printReceipt procedure, on the other hand, merely displays a message. As long as everyone goes through recordLoan, the number matches the records. A new colleague adds quickLoan: it creates the record directly and then calls printReceipt. The reader receives a receipt, so at first glance everything seems correct. Only later does a check reveal one fewer loan than there should be.
The defect lies neither in printing nor in the counter's initial value. It lies in the fact that two operations that had to happen together were accessible separately. If we corrected only quickLoan, another function could repeat the same error tomorrow. The more robust solution is to concentrate the rule in one place: to create a valid loan, an operation must be called that records the loan and updates, or derives, the total. Printing receives the result of that operation and does not change the count.
In our diagram, Print and Output make the same risk visible. Print prepared printing and updated some data; Output performed only the low-level operation toward the terminal. If a new function called Output directly, it skipped the step entrusted to Print. The message appeared on the screen, but the state that the rest of the program expected to be updated remained old. The error could emerge much later, in a function that read that data and appeared entirely unrelated to printing. We do not need to claim that every procedural program is hard to debug to recognize the problem: here, the link between the call and the data change was invisible to whoever used Output.
Print maintains the expected shared data. The dashed arrow indicates the missing update; the consequence may propagate as far as main.We can follow that propagation without attributing anything mysterious to it. Suppose f4() reads the counter that has fallen behind and chooses the wrong branch; f2() and f3() then receive inconsistent values; finally, the main program displays an incorrect result or terminates because a check has failed. The point at which the problem is observed does not coincide with the point where it was introduced. A chain reaching all the way to main gives us a good reason to draw a module's boundaries before looking only at the line that fails.
Definition – Side effect. An operation produces a side effect when it changes something its caller can observe beyond its return value: for example, a shared counter, a file, or the contents of the screen. A side effect may be necessary; it becomes dangerous when it remains hidden or is not coordinated with other changes.
Now suppose one screen reads the total while another part of the program is adding a loan. We have an additional problem, concurrency, which will require specific rules. For now, it is enough to understand the simpler cause: shared data multiplies the points from which a rule can be broken. Restricting access to that data reduces the points to check, but does not replace checking the operations that remain public.
Correcting a Hidden Dependency
Let us group operations according to their responsibility. One module receives the request, one knows the catalog, and one records loans. Those who use the catalog do not need to know the details of how it is stored; an operation that answers “is this book available?” is enough. In the figure, the names of the parts make the same work shown earlier recognizable.
This is an initial example of encapsulation: a part's internal details are accessible only through the operations it chooses to expose. Encapsulation does not originate with objects. Procedural languages and modules can express it through different tools. Objects will bring the same idea into a form that associates state and behavior with an entity in the model.
Further reading – Two meanings of
staticin C. A localstaticvariable retains its value between calls to the function containing it, while its name remains visible only in that function. A function declaredstaticat file scope, on the other hand, has visibility limited to the source file in which it is defined. The first use concerns the data's lifetime, the second a function's visibility. In Java, we will encounter the same keyword, but with the language's own rules: we must not automatically transfer the intuition built from C.
In the figure, the loans module exposes record and find, while keeping the structure containing the records inside. The receipts module does not update the count: it receives the data to display. This distribution is not a language trick; it is a decision about who can do what. If we change how loans are stored, whoever prints a receipt continues to use the public operation, provided its meaning remains the same.
It is worth returning precisely to the small historical C example. A local variable declared static retains its value between calls to the function, while being visible only there. A function declared static at file scope, on the other hand, is visible only within that translation unit. These are two different uses of the same keyword. Neither automatically makes the logic correct: a function that adds values into a persistent variable, for example, still requires a decision about how to reset the sum and what happens if it is used by concurrent calls. In Java, we will see other tools for expressing visibility and state; we will not mechanically transfer C's rule to the word static we encounter in Java code.
What About Functions?
We can approach part of the problem as a transformation: given a set of books and a title, we obtain the matching books. A pure function returns the same result for the same arguments and produces no observable effects outside the function. This is a useful property because it makes the result easier to reason about. Not every task can be pure: recording a loan changes the library's state and, sooner or later, must produce an effect.
Java provides functional tools that we will encounter later, particularly when we treat functions as values and work with collections. For now, one distinction is enough: choosing a pure function for a data transformation may be appropriate; choosing an object to represent a loan with identity and duration may be equally appropriate. Performance and readability depend on the actual program, not on the paradigm's label.
Let us try formulating two different questions. “Which books in the catalog contain the word sea in the title?” is a question about data already available. We can give it a function that receives the catalog and the search word, and returns a result without changing the catalog. If we call it again with the same data, we expect the same result. “Lend this copy to the reader,” on the other hand, is a command: after executing it, the copy's availability changes. Trying to describe both operations as if they had no effects does not clarify the problem.
Immutability and recursion may incur costs and make a program hard to read. That is a possibility, not the destiny of functional programming. An algorithm can use immutable data with structures that share internal parts, or it can be expressed without deep recursion. The only serious way to compare two solutions for cost and clarity is to look at the algorithm, the structures used, and the actual workload. In the coming chapters, we will use objects and functions together when each helps describe part of the problem.
Looking at the Work as a Collaboration Between Objects
In our library, we recognize at least a Book, a Reader, and a Loan. A book has information, such as its title and author. A loan connects a reader to a book and knows when it began. The library offers operations for searching and recording. These entities are not automatic copies of physical objects: they are modeling choices. If the program only needs to print a catalog, perhaps every loan need not be represented. If it must check returns and due dates, that concept becomes important.
Definition – Object. An object is an entity in the program with an identity in the model, a state observable through the prescribed operations, and behavior. In Java, many objects are instances of classes. Not everything a program uses is an object: the language also has primitive types, which we will encounter in the chapter on syntax.
Whoever decides an object's operations also decides which details to hide. A Loan, for example, may offer an operation to close it. If its return date could be freely changed from outside without respecting the rules, the program might represent an impossible situation. A meaningful name helps us read the code, but alone does not make it correct or self-documenting: clear responsibilities, understandable contracts, and examples showing the limitations are needed.
Let us follow a complete request. Reader identifies who requests the volume; Catalog finds the copy; an operation of Library checks that the copy is available and creates a Loan. Finally, the receipt reads the loan's data. We have built a sequence of collaborations, not a list of nouns to turn automatically into classes. “Available” could be a state stored in the copy or an answer calculated from the records: the choice depends on how the system will keep its data consistent. When we add the actual classes, this question will determine methods and checks.
Key concept – Responsibilities and messages. Saying that one object “asks” another for something is a readable way to describe a call to an operation. The caller knows the operation's contract, not necessarily its internal steps. If
CatalogoffersfindByTitle,Librarymust know what data to provide and what kind of answer to expect; it does not need to know the algorithm the catalog uses to search.Essential reference – A historical snapshot of the paradigm. Research by David E. Monarchi and Gretchen I. Puhr, A Research Typology for Object-Oriented Analysis and Design, published in 1992, offers a historical framework for object-oriented analysis and design. That framework helps us understand the questions posed by methods and tools of the time; it is not a Java specification and does not make every characteristic mandatory for every program. In our example, we can check the ideas one by one: a
Loanstores part of the state, exposes operations, and collaborates withCatalog; the model's quality depends on the rules those operations protect, not simply on the number of objects.
The table called objects “anonymous entities.” In a Java program, we may give a variable the name loan, but that name belongs to the reference in the code, not to the object as an intrinsic identity: another reference may point to the same instance. The table also mentioned hierarchies and abstract objects. A general class may be declared abstract when it makes no sense to create an instance directly that fulfills its contract; there is no need to make every base class abstract. Inheritance and messages are possible tools, while clear names and readable responsibilities require design work: objects do not become self-documenting on their own.
Finally, modeling the library's state does not imply that every change in the system is represented by a modified field in an object. Some results may be calculated from immutable data; other tasks, such as recording a loan, change state. Several activities may act simultaneously, requiring coordination rules that we will study in the chapter on threads. Java also allows procedures and functions: the useful choice is the one that makes the problem, the responsibility for the change, and the evidence that the rule remains true recognizable.
Classes of Objects
Think about the book you are reading. You can talk about a book as a general concept while distinguishing this copy from the others. The class describes the shared characteristics and operations; each object created according to that description has its own state. Two books may have the same title without being the same copy. Later, we will also see that identity and equality are different questions: distinct objects can have equal contents.
Definition – Class and instance. A class is a declaration that describes a type of objects and may define their state and behavior. An instance is an object created according to that class. A class is not “the single general object” from which identical copies arise: it is the description the JVM uses to create distinct objects.
A useful model arises from what the program must do, not from every property a book has in the real world. If we need to find a volume in the catalog, its title and author matter. The number of pages may matter for a printed book; for a digital book, it might mean something different. The figure shows one possible generalization, not a classification valid for every library.
To avoid a frequent confusion, let us also distinguish the title from the copy. “The Name of the Rose” is a title that may appear in several physical copies. If only one copy is on loan, the others may be available. Representing everything with a single Book object equipped with just one available field may therefore be insufficient. We can describe the work with one object and each copy with another, or use a different model if the application only manages digital titles. The class does not come before the problem: it arises from the distinctions the problem forces us to make.
Likewise, two objects describing distinct copies may have the same title and author. Their contents are similar, but they are not the same copy. The program must know when it is asking “are they the very same instance?” and when it is asking “do they represent the same value?” You do not need to know Java's operators yet to make this distinction; we will need it when comparing objects in code.
PrintedBook and DigitalBook are two possible specializations of Book. The arrows point toward the more general class; specific properties remain in the class that owns them.We can also classify books by subject: starting from Book, we might draw branches such as history, physics, literature, science, and mathematics. This scheme distinguishes broader and narrower categories, but does not by itself decide how to design the classes. A mathematics book for school belongs both to mathematics and to school textbooks: are these really two types of object to express with two superclasses, or two properties by which the catalog classifies it? In our library model, subject and intended school use can be book data, even when the printed/digital distinction changes the available operations. We can therefore assign several categories to the same copy without inventing a subclass for every combination. We check the choice by asking which operations change: a category used only to find books can remain data; a difference in behavior requires a contract we can explain.
Inheritance and Polymorphism
When we say that DigitalBook is a Book, we are proposing a specialization relationship. In Java, a class can extend another class and acquire the accessible characteristics of the base class, also called the superclass. The subclass adds or modifies what makes the variant specific. We will use these two names consistently: “base class” for the general part and “subclass” for the specialized one.
Definition – Polymorphism. Code can use a reference of a general type and encounter objects of more specific types. When it calls an overridden method, the implementation appropriate to the actual object is executed. This does not mean that an object changes class during execution: it means that a common operation can have specific behaviors.
Suppose every Book can produce a short description. The catalog can ask a book for its description without knowing beforehand whether it is printed or digital; the specific object supplies the expected behavior. This is a real advantage when the relationship between the types is stable and the rules are clear. It is not an automatic shortcut for reusing code: a poor hierarchy makes the program harder to change.
Vehicles help us isolate the mechanism. A train, a plane, and a car can all respond to the request “describe how you move.” The request is shared; the response changes according to the actual vehicle. Code collecting the descriptions can work through a general type. This does not mean that the three vehicles are interchangeable in every operation: only the promises contained in the common contract apply to all. An “open your wings” operation does not belong to that contract.
In Java, the reference through which we use an object has a declared type, while the object created has an actual type. If the called method can be overridden, the implementation is selected according to the actual object. It is dynamic binding that makes polymorphism concrete in this case. The difference between the reference's type and the object's type deserves code examples: we will return to it when we can read declarations and methods.
DigitalBook can be a type of Book; a Library has a collection of books, but is not a collection of books.A mathematics book may belong to several catalog categories. If we use inheritance arrows to represent those categories, however, we risk confusing book classification with the rules of Java classes. Java allows a class to extend only one class, while it may implement several interfaces. A catalog label may also simply be data. Before drawing arrows, we therefore ask what common contract the code needs and which names merely describe different ways of finding a book.
The figure's second relationship is composition: an object contains or uses other objects to perform its work. When a property can change independently of the object's identity, or when we want to replace a behavior without inventing a type relationship, composition is often simpler. Later in the book, we will return to the comparison with code examples.
Inheritance also allows implementations to be reused, but reuse alone is not enough to justify a hierarchy. If DigitalBook extends Book, it inherits what the base class makes available and may add specific behaviors. But if Book promises that every copy has physical pages to turn, the digital book does not fulfill that promise: we have chosen an overly narrow generalization. It is better to correct the common concept or compose the object with separate services, rather than forcing a subclass to simulate operations it does not have.
Important note – A base-class change really does propagate. Changing a superclass method may change the behavior of many subclasses; it is not automatically a maintenance advantage. Anyone modifying a hierarchy must also check the contracts of the classes extending it. The promise “just change one class and everything else remains correct” cannot be made without evidence.
Encapsulation and Valid State
An object is not a bag of freely modifiable variables. If it represents a loan, it should prevent meaningless combinations: a return before the start, or a closure recorded twice without a rule. Hiding fields is a useful step, but is not enough. Even a public method can break the state if it allows any value. Encapsulation works when the exposed operations protect the invariants, the conditions that must remain true while the object is used.
Key concept – Hiding does not mean guaranteeing. A class with private fields may still behave incorrectly. Methods must check arguments and maintain the object's rules; tests must also check cases that could violate them.
A stack of data makes push and pop concrete and makes visible what encapsulation must defend. A stack stores elements in a precise order: the last inserted is the first removed. The call push("Ada") inserts a name; push("Grace") places the second above the first. At this point, its contents, read from the top, are:
| Position in the stack | Value after the two insertions |
|---|---|
Top: next pop |
"Grace" |
| Below the top | "Ada" |
A first call to pop() returns "Grace" and leaves "Ada" on top; a second returns "Ada". If the stack's user could move the two elements directly inside the internal container, the rule would no longer be guaranteed. If they call pop() a third time, however, hiding the container is not enough: the operation's contract must state what happens when the stack is empty. It may report an error or choose another declared response, but must not leave the caller guessing an accidental behavior. The table makes the stack’s behavior explicit; a complete implementation of the data structure can explore the choices without leaving the encapsulation example incomplete.
A well-designed public interface often allows the internal representation to change without changing the object's users. It does not promise that every change will remain local: if the meaning of operations, response times, or exceptions change, other components may be affected. That is why objects require attention to dependencies and checks, just as procedural modules do.
The stack also shows us the difference between a promise about order and a promise about capacity. A stack with fixed space may be full; one that grows may still encounter resource limits. Public operations must make these cases recognizable, and the internal representation must support the chosen contract. Later, we will build a stack; the model needed to understand encapsulation is already here, with both the normal and the empty case.
Another useful rule is to distinguish visibility from consistency. Making a field private limits who can access it directly; it does not ensure that its value is valid. A public closeLoan function accepting any date might still record a return before the loan. The object must check the date at the operation's boundary and decide how to communicate rejection. This is the part of encapsulation that protects meaning as well as memory.
Some Good Rules to Start With
An object should have a responsibility that the reader can describe in one sentence. Its state should remain valid from creation and after every operation. Before choosing inheritance, ask whether “is a” really makes sense and whether the subclass's behavior respects the expectations of the base class's users. A television and a light bulb can both be turned on, but that does not make the television a light bulb: they can share an operation without belonging to the same family.
Let us return to the loan. If we entrust a single Library object with searching for books, checking copies, the due-date calendar, and printing receipts, every change seems to require entering the same class. For example, changing the receipt format might force us to reread the rule preventing a copy already in use from being lent. We can give Catalog the responsibility for finding a copy, Loan the responsibility for representing its duration and outcome, and a presentation component the responsibility for producing the receipt. Library coordinates the request without necessarily holding every detail. The division works if we can say which object maintains the invariant “the same copy cannot be lent twice in the same period”: splitting a large class into three classes that freely modify one another would not solve the problem.
Try changing the requirement: the library allows a reservation when all copies are in use. Is a reservation a kind of book, a reader's behavior, or a new concept connecting the reader, the work, and a position in the waiting list? Draw two alternatives and follow a request through to its answer. The best solution is the one in which you can identify who decides whether the reservation is valid and who updates availability when a copy becomes free again. This exercise tests the responsibility rule on a new case; it cannot be solved by counting classes or repeating “is a.”
These rules do not replace practice. At first, it is normal to model too many objects or give one object every responsibility. The remedy is to check a concrete use case, change the model when it creates friction, and keep the code small enough to correct.
Is All That Glitters Gold?
No. The language offers classes, methods, interfaces, and inheritance; the programmer chooses how to use them. A program can be organized in a way similar to objects even in a language with no classes, by explicitly passing a data structure to the functions that manage it. Imagine in C a Book structure and functions such as lend(Book *copy, Reader *reader): data and operations collaborate around the same concept, but the link must be expressed by the programmer. The prototypes must declare the same parameters used in the definitions, and the memory associated with names requires explicit management. These details are part of the example's correctness, not a cosmetic difference from Java syntax.
Likewise, Java can be written with plenty of classes but be hard to read. An elegant name does not save a confused responsibility. An object that hides every piece of data without explaining what it does does not help the code's reader. The aim is a model that makes the problem's rules explicit and allows them to be checked.
Further reading – A little history. Simula made the concepts of class and object practical for describing systems to simulate. Alan Kay gave great importance to the idea of entities collaborating by exchanging messages; in Smalltalk, this way of thinking became central. The spread of graphical interfaces and more powerful computers made models capable of bringing state and behavior together attractive. In the 1980s, the name object oriented spread among very different languages and tools, fueling debate about what was essential: messages, encapsulation, dynamic binding of calls, or inheritance. This history reminds us that a shared word does not guarantee an identical programming model.
An Object Without the Word class
A C structure accompanied by functions receiving a pointer to that structure shows that organizing data and operations around a concept is a design choice, even when the language offers no class syntax. If we omit the necessary parameter from function declarations or leave management of the memory associated with the name incomplete, the program cannot be verified. Both aspects should be made explicit before using it as an example.
The important comparison can already be made without compiling C. In the function-based model, a call such as lend(copy, reader) explicitly indicates which structure will be modified. In Java, a call such as copy.lendTo(reader) places the copy to the left of the dot: the method is invoked on that object and receives the other necessary data. The syntax changes, but in both cases we must decide who owns the loan rule. Java gives us tools to represent it and restrict access to it; it does not design it for us.
How We Got Here
Simula, Smalltalk, and Alan Kay's work recur in the history of the object-oriented paradigm. Simula made classes and objects central to describing systems; Smalltalk gave great importance to objects communicating through messages. Readers may encounter the expression sending messages even when, in Java, we write more concretely about a method call. These are not two different Java instructions: they are different ways of describing collaboration between entities.
In subsequent years, languages and tools gave different weight to inheritance, encapsulation, interfaces, and dynamic binding of calls. That is why there is no need to seek a historical definition making all “object-oriented” languages identical. To work in Java, we will use a more useful question: which data and operations belong to a concept, what does it promise others, and how do we maintain that promise as the program changes? A paradigm's popularity alone does not guarantee good answers; examples and checks do.
Putting the Model to the Test
Imagine that the library must remember the return date of every loan. Would you add the date to Book, Reader, or Loan? Justify the choice by saying which event that data belongs to and which operations can modify it. Then try a second case: the library must know how many books are available. Is it better to store a number anyone can modify, or derive or update it through a controlled operation? You do not need to write Java to answer; you need to make responsibility visible.
The activity has succeeded if your model distinguishes at least Book, Reader, and Loan, identifies who knows availability, and prevents an impossible state. If two responsibilities seem plausible, describe the use case that decides between them: we will need it when building the actual classes.