Java OOP: Classes and Objects
A class describes a kind of thing. An object is one instance with its own state. Learn the difference by creating two library books, borrowing one, and following exactly which object each reference reaches.
Before you start: review methods and returned values, reference values, fields versus locals, and conditional decisions. Every example is a complete, editable Java 21 program. Use Run code to execute it; no keyboard input or external services are required.
What you will be able to do
- Distinguish a class, an object, and a reference variable.
- Read new Book() and access an object's fields with dot notation.
- Call instance methods on a specific object from static main.
- Trace independent objects, aliases, and null references.
- Separate state-changing actions from read-only queries and printed output.
- Test a two-book model with successful and rejected state changes.
Why introduce an object after learning methods?
Imagine a tiny library desk with two physical books. Each has a title and a borrowing status. A program with separate firstTitle, firstBorrowed, secondTitle, and secondBorrowed variables works for a moment, but it is easy to update one title and the wrong status together. We want each book's related information to stay together.
Object-oriented programming, or OOP, lets us model an individual thing with state and behavior. State is what the program remembers about it; behavior is what the program can ask it to do. Here a book remembers its title and whether it is borrowed, and it can respond to borrow, returnBook, and summary requests.
This is an introduction to modeling, not a requirement to make every calculation an object. The price calculator in the previous lesson still benefits from small calculation methods. A book is useful here because its borrowing state continues between actions and belongs to one particular item. This exercise is an in-memory simulation, not a database-backed library service.
A class describes the kind; an object is one instance
Book is the class: the declaration describing which fields and methods this kind of book has. A Book object is one instance created from that description. Think of the class as the design of the record, and each object as one actual record with its own current values. Merely writing the class does not create a book instance.
Read Book first = new Book() in three pieces. Book is the variable's type; first is a local reference variable; new Book() creates an instance and produces a reference to it. The assignment stores that reference in first. The variable is not itself the book, and its name does not become the book's title.
The parentheses in new Book() invoke a constructor. We have not declared any constructor in Book, so Java supplies a default no-argument constructor here. You will write constructors later. For now, keep the distinction simple: declare the type, create an instance, then use the reference to work with that instance.
Main.java · Create and name one book
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
River Maps
Borrowed: false
Read the first program one step at a time
class Bookdescribes a kind of object; it does not run main or create a book by itself.new Book()creates one instance, starting with the declared field values.firststores the reference.first.titleselects the title field on that instance.- Assigning the title changes that field; it does not change the class name or the variable name.
- The two print calls observe the current title and borrowed status.
Try renaming the local variable first to shelfCopy everywhere. The output should stay the same because variable names are not stored as titles.
Use dot notation to select a particular object's field
A field is a variable declared in the class body, outside its methods. The title field has type String and the borrowed field has type boolean. In first.title, the expression before the dot chooses the object, while the name after the dot chooses a field on that object. The assignment first.title = "River Maps" changes that object's title.
The initializers title = "Untitled" and borrowed = false give each new Book its starting state. Creating first and second with two new expressions creates two separate Book objects. Updating first.borrowed does not update second.borrowed because these are instance fields, not shared class-wide fields.
These examples leave fields package-accessible so Main and Book in the same file can show the mechanics directly. This is a deliberate teaching simplification, not a finished encapsulated design: callers can still assign an invalid title or bypass borrowing rules. A later lesson will protect fields with private access and a carefully chosen public API.
Main.java · Two instances, two sets of fields
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
River Maps: true
Night Garden: false
An instance method acts on the object receiving the call
In book.borrow(), book is the receiver: the object asked to perform the action. Inside that instance method, borrowed refers to the receiving object's field. A successful call changes that field to true and returns true. If it is already true, the method returns false without changing anything.
The returned boolean answers whether this request changed the state, not what the current borrowed field is. That is why the second borrow returns false while the book remains borrowed. Likewise, returnBook returns true only when a borrowed book becomes available; returning an already available book is rejected with false.
Each method owns one small transition rule. main chooses the order of requests and prints the returned results. The methods themselves do not print. That separation lets you use the same behavior from a console, tests, or another caller without embedding a particular display format into the state-changing operation.
Main.java · Ask one object to change its state
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
First borrow: true
Second borrow: false
River Maps | borrowed
Return: true
Second return: false
River Maps | available
Trace the borrowing sequence before running it
- Start available:
borrowedis false. - The first borrow returns true and changes the field to true.
- The second borrow returns false and leaves the field true.
- The first return returns true and changes the field to false.
- The second return returns false and leaves the field false.
The boolean result describes whether an action succeeded. The field describes the state that remains afterward. Keep those two meanings separate.
A query reports state; an action changes it
summary is a query: it builds and returns a String describing the current object. The conditional expression chooses borrowed or available from the boolean field. There is no assignment to a field, so reading the summary twice should produce the same result unless some other action has changed the object in between.
borrow and returnBook are actions, while summary is a read-only observation. This distinction is a useful design habit, not a Java keyword enforced by the compiler. A badly written summary could change a field and still compile, which is why the lab explicitly tests that reading does not change state.
main is static because it is the program entry point, but Book's behavior methods are instance methods. A static caller can invoke them through an object reference such as book.borrow(). Do not make every method or field static to silence an error; doing so can erase the per-object ownership the model needs.
Main.java · Read the same object without changing it
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
River Maps | available
River Maps | available
Still borrowed: false
Two references can point to the same book
Book desk = first does not contain new, so this assignment does not create another Book. It copies the reference value. Now first and desk are two variables through which the same object can be reached. Calling desk.borrow changes the very book that first also refers to.
The expression first == desk compares the references for identity: do they refer to the same object? It returns true for this alias. Both summary calls then show borrowed, not because Java synchronized two books, but because there has only been one Book object throughout the example.
This is the same reference-value idea you saw with arrays and method parameters. Assignment and argument passing do not automatically deep-copy objects. When tracing a program, count creation expressions separately from variable names: three reference variables can still refer to only one book.
Main.java · An assignment copies a reference, not a book
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Same object: true
River Maps | borrowed
River Maps | borrowed
Two books may have the same title without being the same object
A library can own two physical copies of River Maps. In this model, two new Book() calls create two instances even if you assign the same title to both. The String content comparison returns true, while comparing the Book references with == returns false.
Borrowing the first copy leaves the second available. A title is data, not a unique object identifier, and equal-looking fields do not merge objects. When a bug seems to change the wrong book, ask whether the variables refer to distinct instances before inspecting the text displayed to the user.
For Strings, use equals when you mean equal text. For these Book references, use == only when you mean identity. This introductory class does not override equals; calling first.equals(second) would not automatically compare titles either. Defining domain equality is a separate design decision, not something field declarations provide for free.
Main.java · Equal field values do not imply one object
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Same title: true
Same object: false
River Maps | borrowed
River Maps | available
Predict a change
Change only the second title to Night Garden. The title comparison becomes false, but the identity comparison stays false. Restore matching titles and borrow second instead of first. Now only the second summary should say borrowed.
Reassigning a reference is different from mutating an object
After desk = first, both names reach one object. But desk = new Book() changes which object desk refers to. It does not replace the reference stored in first, and it does not erase or rename the original book. first still reaches the original River Maps object.
Only the new book receives the later title assignment and borrow call through desk. Read the final two summaries: River Maps remains available, while Night Garden is borrowed. The state difference comes from the receivers of the operations, not from which variable was declared first.
An object can continue to exist while another reachable reference still points to it. Do not describe a reassignment as immediately deleting the old object. Garbage collection is about reachability, with timing outside this lesson's control; ordinary application code should not depend on when memory is reclaimed.
Main.java · Reassigning one variable leaves the other alone
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Same object: false
River Maps | available
Night Garden | borrowed
Understand field defaults and a missing reference
DraftBook deliberately omits field initializers. Its String field starts as null, int field as zero, and boolean field as false. These are field defaults. They do not mean Java silently initializes every local variable: a local Book selected declared inside main must be definitely assigned before you read it.
null means a reference does not currently refer to an object. It is not an empty Book and not a borrowed or available book. Printing a null reference as part of a String is allowed, as the example shows; dereferencing it to read a field or call an instance method is a different operation and can throw NullPointerException.
The regular Book class gives title an explicit starting value because an unnamed draft is not helpful in our report. Initial values express a modeling choice, while validation expresses which changes are allowed. Defaults alone do not enforce rules such as nonblank titles or valid transitions.
Main.java · Field defaults are not local-variable defaults
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Title: null
Pages: 0
Borrowed: false
Guard a missing selection before using its methods
A desk can have no current book selected, so selected = null is an intentional state of the reference variable. Check selected == null before calling selected.summary(). The example handles absence with a message and creates a real book separately afterward; it does not hide missing data by pretending a book already existed.
In a condition such as selected != null && selected.borrow(), short-circuit evaluation avoids the method call when selected is null. But decide whether the model should allow absence in the first place. A guard is useful at a boundary where no selection is legitimate; adding null checks everywhere without a contract can conceal a broken caller.
All examples fit in Main.java with one public class Main and a second, package-private top-level class Book. Book is not nested inside main or inside Main. The runner compiles this one source file and launches Main; when splitting ordinary files later, keep a public top-level class in its matching filename. No imports, inheritance, database, or custom constructor are needed for this lesson.
Main.java · No selected book is different from an available book
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
No book selected.
Untitled | available
Repair exercise: the second book is not independent
The desk needs two independent books, but changing second also changes what first reports. Run this valid-but-wrong program, count the new expressions, and repair the second book's creation without changing the report lines.
Main.java · Find the accidental alias
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
The starter compiles, but currently prints the same changed book twice:
Night Garden: true
Night Garden: true
Reveal the repair and explanation
The original assignment made second an alias of first. Both the title assignment and the borrowed assignment therefore targeted the same object. A second new Book() creates a different instance with its own fields. The first book retains River Maps and false, while only the second becomes Night Garden and true. Merely renaming a variable would not fix the reference relationship.
Main.java · Two independently created books
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
River Maps: false
Night Garden: true
Independent lab: a two-book library desk
Build a two-book desk simulation. Each Book tracks its own title and borrowed status; actions return whether a transition succeeded, and summary returns text without changing fields. Use the fixed request sequence below so you can verify each state change by hand.
- Declare a package-private top-level Book in the same file as public Main. Give each instance String title = "Untitled" and boolean borrowed = false; do not make either field static.
- Implement boolean borrow(): reject an already borrowed book with false and no mutation; otherwise set borrowed to true and return true. Do not print inside the method.
- Implement boolean returnBook(): reject an already available book with false and no mutation; otherwise set borrowed to false and return true. No console output belongs here either.
- Implement String summary() returning title + " | " + either "borrowed" or "available". Repeated queries must not mutate fields.
- Create two independent books titled River Maps and Night Garden. Borrow first twice, inspect second, return first through an alias, return first again, then borrow second. Print each result with the labels in the supplied sequence.
- Print the final summaries, identity checks, and repeated-query comparison. Verify that first finishes available and second finishes borrowed. Using an alias must not create a third Book.
Use this request order: borrow first, borrow first again, query second, return first through desk, return first again, borrow second, then report identities and final summaries. The expected output is:
First borrow: true
First borrow again: false
Second before borrow: Night Garden | available
Return through alias: true
First return again: false
Second borrow: true
First: River Maps | available
Second: Night Garden | borrowed
Alias shares first: true
Different books: true
Queries unchanged: true
Main.java · Build your two-book model
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Hint 1: choose the state owner
Keep title and borrowed inside Book, outside its methods, with no static modifier. Create first and second separately in main. A method call such as first.borrow() must work with the receiver's borrowed field rather than a local variable that disappears after the call.
Hint 2: write the transitions before printing
For borrow, reject when borrowed is already true; otherwise set it to true and return true. For returnBook, reject when it is false; otherwise set it to false and return true. summary only returns text. Put all printing in main so the action result and the later state can be inspected separately.
Reveal the complete explained solution
Main.java · Complete two-book desk simulation
Predict the result, edit Main.java, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
The first borrow succeeds and the duplicate request is rejected. The second instance remains available. desk refers to first, so returning through desk changes first back to available; a second return is rejected. Borrowing second affects only Night Garden. Two creation expressions and three reference variables represent two Book objects, with first and desk sharing one. The final read-only calls reproduce both saved summaries.
Test changes and boundaries
Predict every returned boolean before running. Inspect both books after each sequence, not just the object receiving the last request. Reset restores the original example.
- Create a fresh Book: it must be Untitled | available. Returning it immediately must yield false and leave it available.
- Test borrow, borrow, returnBook, returnBook, borrow: results must be true, false, true, false, true, with the final status borrowed.
- Create two instances with identical titles. Their title content should match, their identity comparison should be false, and borrowing one must leave the other available.
- Replace the second new Book() with first and predict the changed report before running. Repair the alias, then separately reassign desk to a fresh Book and confirm first is unaffected.
- Call summary repeatedly between actions and check both title and borrowed remain unchanged. Test a null selection through a guard, not an unguarded call, and explain why the absent reference has no borrowing status.
Common mistakes and how to diagnose them
Declaring a reference without creating an instance
Book book; declares a local variable but does not assign it. Reading it gives a definite-assignment compilation error. Book book = null compiles, but calling book.borrow() then fails at runtime. Book book = new Book() creates the instance needed for the call.
Calling Book.borrow() instead of book.borrow()
borrow is an instance method and needs a receiver. From static main, create a Book and call it through that reference. Do not turn borrowed into static just to remove an error: that would share borrowing state across instances.
Making an accidental alias instead of a second book
Book second = first copies a reference. Count new expressions and compare references with == to diagnose unexpected shared state. Create another instance when the domain requires two independent books.
Confusing a failed request with the final field value
A second borrow returns false because it did not change the state; the existing borrowed field is still true. Read action results and state separately. A false result does not automatically mean the book is available.
Putting model methods inside main or declaring two public classes
Methods belong inside a class body, not another method. These runners use one public Main in Main.java and one non-public top-level Book alongside it. Keep the closing brace of Main before class Book, and do not mark Book public in this one-file exercise.
Assuming a readable model is already protected
Package-accessible fields can still be assigned directly, so this exercise demonstrates object identity and method contracts rather than full encapsulation. The methods behave correctly when used as specified; later private fields and constructors will enforce stronger boundaries.
Frequently asked questions
Is a class the same thing as an object?
No. Book is a class declaration and type. new Book() creates an instance. You can create multiple independent instances of the same class, or have multiple references to a single instance.
Why can static main call a non-static method?
main calls the method through a receiver such as first.borrow(). It does not need to be an instance method itself to use another object's behavior; it needs a valid reference to that object.
Where is the constructor if we did not write one?
When this Book class declares no constructors, Java supplies its default no-argument constructor. The empty parentheses in new Book() invoke it. Once you declare a constructor yourself, a no-argument constructor is not automatically added alongside it.
Does Book second = first copy all fields?
No. It copies the reference value, so both names refer to the same object. A separate new Book() creates another instance, but copying selected values into it would still need an explicit policy, especially for nested mutable data.
Should I put everything in classes now?
Use a model when state and behavior belong together, as borrowing status belongs to one book. Keep simple stateless calculations simple. This lesson does not require inheritance, getters for every field, or a class for every temporary value.
One class, separate instances, deliberate references
Book describes the model. Each new instance has its own title and borrowing status. A receiver selects which object a method acts on; an alias gives another route to the same object. Reassigning a reference changes that route, while a field assignment changes the reached object.
Before moving on, explain the difference between a failed borrow and an available book, and why two equal titles do not imply one instance. Next: class attributes and methods, where you will examine instance versus static members more closely.
Can you follow each book's state?
Answer all six questions, then read the explanations. Quiz answers are temporary on this page and do not save course progress.
References and related learning
The library scenario, explanations, examples, and exercises are original SovranCode material. The Java 21 specification documents the underlying language rules.
- Java 21 specification: class members, fields, and methods
- Java 21 specification: initial values of variables
- Java 21 specification: class instance creation
Review copied reference values · Review fields and locals · Practice method contracts · Java course roadmap.
Last updated: