Classes, Objects, and EncapsulationIntroductionObject-oriented programming (OOP) organizes software around objects that combine state and behavior. Java is strongly object-oriented: applications are typically designed as cooperating classes, although primitive types such as int, double, and boolean are also available for efficiency and simplicity. A class is a definition or blueprint. An object is a concrete instance created from that definition. For example, a BankAccount class may specify an account number, balance, deposit behavior, and withdrawal behavior. A particular customer’s account is an object of that class. This chapter develops the skills required to model real-world entities in Java. It explains fields, methods, constructors, access modifiers, static members, packages, encapsulation, immutability, and records. These features are not isolated syntax elements; together they support maintainable, secure, and understandable software. Learning ObjectivesBy the end of this chapter, you should be able to: - distinguish classes, objects, fields, methods, and constructors;
- design classes that represent real-world entities;
- create objects and invoke instance methods;
- apply access modifiers appropriately;
- explain the difference between instance and static members;
- organize related classes into packages;
- implement encapsulation and validate object state;
- distinguish mutable, immutable, and record-based designs;
- select an appropriate class design for a practical application.
1. Classes and ObjectsA class defines the structure and capabilities of a category of objects. Its fields represent data, while its methods represent operations. An object has identity, state, and behavior: - Identity distinguishes one object from another.
- State is the current value of its fields.
- Behavior is what the object can do through its methods.
The following class models a simple lamp: public class Lamp {
private boolean on;
public void turnOn() {
on = true;
}
public void turnOff() {
on = false;
}
public boolean isOn() {
return on;
}
}The declaration private boolean on; is a field. The methods change or report the lamp’s state. An object can be created with the new operator: Lamp deskLamp = new Lamp();
deskLamp.turnOn();
System.out.println(deskLamp.isOn()); // true deskLamp is a reference variable. It refers to an object stored in memory; it is not itself the object. Several reference variables may refer to the same object:
Lamp firstReference = new Lamp();
Lamp secondReference = firstReference;
secondReference.turnOn();
System.out.println(firstReference.isOn()); // true The two variables refer to one lamp. Assigning an object reference does not copy the object. FieldsA field is declared within a class but outside methods and constructors. It may be an instance field or a static field. Instance fields belong separately to each object: public class Student {
private String name;
private int year;
}Every Student object has its own name and year. If a field is not explicitly initialized, Java supplies a default value: numeric fields become zero, boolean fields become false, and reference fields become null. Relying on defaults is sometimes valid, but explicit initialization often communicates intent more clearly. A local variable inside a method is different. Local variables do not receive automatic default values and must be initialized before use. MethodsA method specifies an operation. Its declaration includes a return type, name, parameter list, and body: public double calculateArea(double radius) {
return Math.PI * radius * radius;
}The parameter radius is input supplied by the caller. The double before the method name is the return type. A method that returns no value uses void. Java passes arguments by value. For primitive arguments, the value itself is copied. For object arguments, the reference value is copied, so a method can modify the referred object but cannot replace the caller’s reference by assigning a new reference to its parameter. Method overloading allows methods in one class to share a name when their parameter lists differ: public void print(String message) { }
public void print(int number) { }Return type alone cannot distinguish overloaded methods. Therefore, two methods with identical names and parameters but different return types are not valid overloads. 2. Constructors and Object InitializationA constructor initializes a newly created object. It has the same name as the class and no return type, not even void. public class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
}The keyword this refers to the current object. It is useful when parameter names match field names. The expression new Book("Clean Code", "Robert C. Martin") allocates an object and invokes the constructor. If a class declares no constructor, Java supplies a public or package-access no-argument constructor according to the class’s accessibility. However, once any constructor is declared, Java does not automatically supply a no-argument constructor. A class may provide multiple constructors: public class Product {
private String code;
private double price;
public Product(String code) {
this(code, 0.0);
}
public Product(String code, double price) {
if (code == null || code.isBlank()) {
throw new IllegalArgumentException("Code is required");
}
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
this.code = code;
this.price = price;
}
}The expression this(...) invokes another constructor in the same class. It must be the first statement in the constructor. Constructor chaining avoids duplicated initialization logic. A constructor should establish a valid initial state. If an object can be created with contradictory or invalid data, errors may appear much later and become difficult to diagnose. 3. Access Modifiers and EncapsulationAccess modifiers control where a class member can be used. | Modifier | Same class | Same package | Subclass in another package | Any class |
|---|
private | Yes | No | No | No | | (no modifier) | Yes | Yes | Limited through inheritance rules | No | protected | Yes | Yes | Yes, through inheritance or qualifying rules | No | public | Yes | Yes | Yes | Yes |
A top-level class may be public or package-private; it cannot be private or protected as a top-level declaration. Encapsulation means combining data and operations in a class while controlling direct access to internal representation. A commonly used pattern is private fields with public methods that provide carefully defined operations: public class Thermostat {
private double temperature;
public Thermostat(double initialTemperature) {
setTemperature(initialTemperature);
}
public double getTemperature() {
return temperature;
}
public void setTemperature(double temperature) {
if (temperature < 5.0 || temperature > 35.0) {
throw new IllegalArgumentException("Temperature out of range");
}
this.temperature = temperature;
}
}The setter does more than expose assignment: it protects an invariant, namely that the temperature remains between 5 and 35 degrees Celsius. Encapsulation therefore supports correctness, security, and future change. The class can later change its internal representation without requiring every client to change. Getters and setters are not automatically good encapsulation. A getter that exposes a mutable internal collection, or a setter that accepts every value without validation, may still undermine the design. Prefer domain-specific operations such as deposit(amount) or addItem(item) when they express valid actions more accurately than unrestricted setters. Information HidingInformation hiding is the design principle of concealing decisions that are likely to change. A class might store a date as separate integers today and as a library type tomorrow. Client code should depend on stable behavior rather than the storage details. A well-encapsulated class generally: - keeps representation fields private;
- validates input at boundaries;
- maintains class invariants after every public operation;
- exposes the smallest useful public interface;
- avoids returning direct references to mutable internal data.
4. A Worked Example: A Bank AccountConsider an account with an account number, owner, and balance. The account must not permit negative deposits or withdrawals greater than the current balance. public class BankAccount {
private final String accountNumber;
private final String owner;
private double balance;
public BankAccount(String accountNumber, String owner, double openingBalance) {
if (accountNumber == null || accountNumber.isBlank()) {
throw new IllegalArgumentException("Account number is required");
}
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("Owner is required");
}
if (openingBalance < 0) {
throw new IllegalArgumentException("Opening balance cannot be negative");
}
this.accountNumber = accountNumber;
this.owner = owner;
this.balance = openingBalance;
}
public void deposit(double amount) {
requirePositive(amount);
balance += amount;
}
public void withdraw(double amount) {
requirePositive(amount);
if (amount > balance) {
throw new IllegalStateException("Insufficient funds");
}
balance -= amount;
}
public String getAccountNumber() {
return accountNumber;
}
public String getOwner() {
return owner;
}
public double getBalance() {
return balance;
}
private static void requirePositive(double amount) {
if (amount <= 0 || Double.isNaN(amount)) {
throw new IllegalArgumentException("Amount must be positive");
}
}
}The account number and owner are final, so their references cannot be reassigned after construction. The balance is mutable because legitimate operations change it. Clients cannot assign a negative balance directly because the field is private. The invariant is checked by the constructor and by each operation that can affect balance. For financial software, double may be unsuitable for exact monetary calculations because binary floating-point representation can introduce rounding effects. A production banking system commonly uses BigDecimal with an explicitly chosen rounding policy and currency scale. The example uses double only to focus on class design. 5. Static Members and Class-Level StateAn instance member belongs to each object. A static member belongs to the class itself and is shared by all instances. public class Ticket {
private static int nextNumber = 1;
private final int number;
public Ticket() {
number = nextNumber++;
}
public int getNumber() {
return number;
}
}Each ticket has its own number, but all tickets use the same nextNumber. A static method can be invoked through the class name: Ticket first = new Ticket();
Ticket second = new Ticket();
System.out.println(first.getNumber()); // 1
System.out.println(second.getNumber()); // 2 A static method has no particular object and therefore cannot directly access instance fields or use this. It can access static members. Constants are commonly declared as public static final: public final class MathConstants {
public static final double TAX_RATE = 0.20;
private MathConstants() { }
}The final reference cannot be reassigned, but if a static final field refers to a mutable object, the object itself may still be changed. Static mutable state should be used carefully because it creates global coupling and can cause testing or concurrency problems. 6. Packages and Importing TypesA package groups related types and creates a namespace. It helps organize projects and control package-level access. A source file may begin with: package edu.example.accounts; The directory structure normally corresponds to the package name. A class in another package can be used with its fully qualified name: edu.example.accounts.BankAccount account; More conveniently, it can be imported: import edu.example.accounts.BankAccount; Classes in java.lang, such as String and Math, are imported automatically. Types in other packages require an import or a fully qualified name. Package design should reflect responsibility. For example, model may contain domain classes, service may contain application operations, and repository may contain persistence code. Packages should not be treated merely as arbitrary folders; their boundaries communicate architectural intent. The package-private access level, obtained by omitting a modifier, allows access only within the same package. It is useful when several closely related classes need to cooperate without exposing implementation details to the entire application. 7. ImmutabilityAn immutable object cannot change observable state after construction. Immutability simplifies reasoning, supports safe sharing, and reduces accidental side effects. A typical immutable class: - is often declared
final to prevent problematic subclassing; - uses private final fields;
- initializes all state in the constructor;
- provides no mutating methods;
- does not expose mutable internal objects directly;
- makes defensive copies when necessary.
public final class Address {
private final String city;
private final String country;
public Address(String city, String country) {
if (city == null || city.isBlank() || country == null || country.isBlank()) {
throw new IllegalArgumentException("City and country are required");
}
this.city = city;
this.country = country;
}
public String getCity() {
return city;
}
public String getCountry() {
return country;
}
}String is immutable, so returning its reference is safe. However, a final reference to a mutable list does not make the list immutable:
private final List<String> tags; The list could still be changed unless the class copies it and returns an unmodifiable view or immutable copy. For example: this.tags = List.copyOf(tags); List.copyOf rejects null elements and creates an unmodifiable list. Defensive copying is especially important at trust boundaries, where external code might otherwise retain a reference to internal state.
Immutability does not mean that an object cannot be replaced. A program may create a new immutable object representing a changed value. The original object remains unchanged. 8. RecordsA record is a concise Java declaration for a class whose main purpose is to carry data. For example: public record Point(int x, int y) { }This declaration provides private final component fields, a canonical constructor, accessor methods named x() and y(), equals, hashCode, and toString. Record accessors are not named getX() and getY(). A record can validate its components using a compact constructor: public record EmailAddress(String value) {
public EmailAddress {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("Invalid email address");
}
}
}Records are appropriate for immutable data carriers and value-like objects. They are less appropriate when an object has substantial mutable behavior, requires a no-argument constructor for a framework, or needs identity-based lifecycle management. A record is only shallowly immutable. If a component refers to a mutable object, that object may still change. For example, a record containing List<String> should use a defensive immutable copy if the record’s abstraction requires deep immutability. Records can define methods, static members, and additional constructors, but they cannot extend another class. They implicitly extend java.lang.Record and can implement interfaces. 9. Equality, Identity, and Object RepresentationThe operators == and != compare primitive values, but for object references they compare identity: whether two references point to the same object. The equals method is used for logical equality. String a = new String("Java");
String b = new String("Java");
System.out.println(a == b); // false, usually distinct objects
System.out.println(a.equals(b)); // true, equal character sequencesA class that overrides equals should normally override hashCode consistently: equal objects must have equal hash codes. This is essential when objects are used in HashSet or as keys in HashMap. Records automatically generate suitable equals and hashCode implementations based on their components. For ordinary classes, the programmer must decide which fields define logical identity. toString() should provide a useful representation for logging and debugging, but sensitive data such as passwords and payment credentials should not be included.
10. Applied Case: Library Lending SystemBackgroundAn educational library needs software to track books and loans. A book may be borrowed by one member at a time. The system must prevent invalid operations such as lending an already borrowed book or returning a book that is not on loan. Problem AnalysisThe core entities are Book, Member, and Loan. A book has stable identifying information and a changing availability state. A member has identity information and a collection of current loans. A loan connects a book, a member, and a due date. A poor design might expose fields publicly: public boolean borrowed;
public String memberName; Any client could assign contradictory values. A better design makes state private and exposes meaningful operations. Approach and SolutionAn illustrative Book class might be: public class Book {
private final String isbn;
private final String title;
private boolean available = true;
public Book(String isbn, String title) {
if (isbn == null || isbn.isBlank() || title == null || title.isBlank()) {
throw new IllegalArgumentException("ISBN and title are required");
}
this.isbn = isbn;
this.title = title;
}
public boolean isAvailable() {
return available;
}
public void borrow() {
if (!available) {
throw new IllegalStateException("Book is already borrowed");
}
available = false;
}
public void returnBook() {
if (available) {
throw new IllegalStateException("Book is not currently borrowed");
}
available = true;
}
public String getIsbn() { return isbn; }
public String getTitle() { return title; }
}The public interface expresses library actions instead of exposing the available field. A service class could coordinate a loan by checking the member’s borrowing limit, calling book.borrow(), and creating a Loan record containing the relevant references and due date. OutcomeThe design prevents clients from directly creating an invalid book state. Book identification remains stable, while availability changes only through controlled operations. If the library later changes its availability representation—for example, by introducing reservation states—the public methods can remain stable while the implementation evolves. Learning PointsThe case illustrates several principles: - classes should represent cohesive concepts;
- constructors should reject invalid initial state;
- methods should express domain rules;
- private fields protect invariants;
- immutable identity fields are useful where identifiers should not change;
- a record can represent a simple value such as a due-date summary, but not necessarily a stateful book.
11. Challenges, Limitations, and Best PracticesObject-oriented design can be misused. Creating a class for every noun may produce unnecessary complexity. Inheritance can create rigid dependencies when composition would be clearer. Excessive getters and setters can expose implementation rather than meaningful behavior. Static state can make tests order-dependent. Mutable objects can cause unexpected changes when aliases are shared. Recommended practices include: - give each class a clear responsibility;
- choose names that reflect the domain;
- keep fields private unless there is a compelling reason otherwise;
- validate constructor arguments and public method inputs;
- prefer immutable fields and value objects where practical;
- use
BigDecimal for exact monetary calculations; - use
equals for logical equality and == for identity or primitive comparison; - override
hashCode whenever equals is overridden; - avoid exposing mutable collections directly;
- keep public APIs small and stable;
- document units, valid ranges, nullability, and exceptions;
- use packages to communicate boundaries;
- test both successful operations and invalid transitions.
Encapsulation is also a security practice. It limits the code that can change sensitive state and creates clear points for validation, auditing, authorization, and logging. Nevertheless, encapsulation is not a substitute for access control, encryption, secure storage, or threat analysis. A private field does not protect data from every runtime or deployment threat. Contemporary Java PerspectiveModern Java supports several styles within the object model. Traditional mutable classes remain appropriate for entities whose state changes over time. Immutable classes and records are effective for values, messages, configuration objects, and data-transfer structures. Interfaces allow behavior contracts without forcing clients to depend on implementation classes. When designing an application, ask three questions: Does the object have an identity and lifecycle? If so, a class may be appropriate. Is it primarily a stable value? An immutable class or record may be preferable. Does it represent a shared operation rather than state? A stateless service or static utility may be sufficient. These choices should follow the domain rather than fashion. Chapter SummaryClasses define the structure and behavior of objects. Objects are instances with identity, state, and behavior. Fields store state, methods provide operations, and constructors establish valid initial conditions. Instance members belong to individual objects, whereas static members belong to the class and are shared. Access modifiers control visibility, while encapsulation hides representation and protects invariants. Private fields combined with validated, meaningful operations generally produce safer designs than unrestricted public data. Packages organize types and provide namespace and package-level access boundaries. Mutable objects can change after construction; immutable objects cannot change their observable state. Immutability simplifies reasoning and safe sharing, but defensive copying is needed when fields refer to mutable objects. Records provide concise declarations for immutable, data-oriented types, although their immutability is shallow unless mutable components are handled carefully. Good class design balances simplicity, responsibility, validation, maintainability, and domain meaning. The most effective Java programs use classes and objects to make valid states easy to create and invalid states difficult to represent. Key TermsClass: A blueprint defining data and behavior for objects. Object: A concrete instance of a class. Field: A variable declared in a class that stores object or class state. Method: A named operation defined in a class. Constructor: A special operation that initializes a new object. Instance member: A member associated with a particular object. Static member: A member associated with the class rather than one object. Reference: A value that identifies or points to an object. Encapsulation: Combining state and behavior while controlling access to representation. Information hiding: Concealing implementation decisions likely to change. Invariant: A condition that must remain true for a valid object. Access modifier: A keyword controlling visibility of a class or member. Package: A namespace that groups related Java types. Immutable object: An object whose observable state cannot change after construction. Defensive copy: A separate copy made to prevent external mutation of internal state. Record: A concise Java type intended primarily to carry data. Identity: The property that distinguishes one object from another. Logical equality: Equality determined by meaningful object content, usually through equals. Review QuestionsShort Answer Questions- What is the difference between a class and an object?
- Why is the
this keyword used in a constructor? - What is the difference between an instance field and a static field?
- Why should mutable fields generally be declared
private? - What type of Java design is especially suitable for a simple immutable data carrier?
Long Answer Questions- Explain how constructors and validation work together to establish a valid object state.
- Describe Java’s four principal access levels and give an appropriate use for each.
- Explain encapsulation and information hiding, including why public getters and setters do not automatically guarantee good encapsulation.
- Compare mutable classes, immutable classes, and records. Identify a suitable use for each.
- Explain the relationship between
equals, hashCode, identity, and the use of objects in hash-based collections.
Analytical / Application Questions- Design a
TemperatureReading class that stores a sensor identifier and Celsius temperature. Specify its fields, constructor validation, accessors, and whether the class should be mutable or immutable. Justify your choices. - A class returns its internal
ArrayList directly from a getter. Analyze the defect and propose two ways to protect encapsulation. - A library application has a public
borrowed field that clients can change freely. Redesign the relevant interface so that invalid transitions are prevented, and explain the invariant your design maintains.
Multiple Choice Questions with AnswersQuestion 1Which statement best describes an object in Java? A. A concrete instance of a class B. A package containing related classes C. A method that returns a value D. A keyword used to control visibility Correct Answer: A. A concrete instance of a class Question 2What must a constructor have in Java? A. A return type of void B. The same name as its class and no return type C. The keyword static D. At least one parameter Correct Answer: B. The same name as its class and no return type Question 3Which access modifier permits access only within the declaring class? A. public B. protected C. private D. package Correct Answer: C. private Question 4What is the principal purpose of encapsulation? A. To make every field globally accessible B. To hide representation and control access to object state C. To prevent all methods from returning values D. To ensure every class uses inheritance Correct Answer: B. To hide representation and control access to object state Question 5Which member is shared by all instances of a class? A. An instance field B. A local variable C. A static field D. A constructor parameter Correct Answer: C. A static field Question 6What happens when a class declares a constructor but no no-argument constructor? A. Java always adds a public no-argument constructor B. The class cannot have any objects C. Java does not automatically add a no-argument constructor D. All constructors become static Correct Answer: C. Java does not automatically add a no-argument constructor Question 7Which feature is most directly associated with a concise immutable data carrier? A. Record B. Package declaration C. Static initializer D. Primitive variable Correct Answer: A. Record Question 8Why might a class use a defensive copy for a list field? A. To allow callers to modify internal state freely B. To prevent external references from modifying internal state C. To convert the list into a primitive type D. To make the class automatically serializable Correct Answer: B. To prevent external references from modifying internal state Question 9For object references, what does == primarily compare? A. Logical content B. The objects’ hash codes C. Whether the references identify the same object D. The classes’ package names Correct Answer: C. Whether the references identify the same object Question 10What is a key requirement when overriding equals in an ordinary Java class? A. The class must also override hashCode consistently B. The class must become static C. Every field must become public D. The constructor must have no parameters Correct Answer: A. The class must also override hashCode consistently
|