Course outline

Java / Basic / Lesson 6

Classes, objects, and encapsulation

Model a small stateful object whose constructor and methods protect valid transitions. Distinguish instances, static members, access control, and interface behavior.

About 5 min readContent reviewed
On this page

Lesson goals

  • Protect object invariants through constructors and methods.
  • Distinguish instance state from static state.
  • Use an interface to describe replaceable behavior.

Give each object a clear responsibility

A class defines a type with fields, constructors, and behavior; an object is one instance of that type. Two instances can hold independent state even though they share the same method definitions. Model a responsibility such as a stock item that permits only positive additions, rather than a bag of publicly writable fields. An invariant is a condition that should hold whenever the object is externally observable.

The constructor establishes the initial valid state, and public methods preserve it. Validation is not merely a user-interface concern because another caller can construct the object directly. Keep illegal states difficult to create, and choose failure behavior that callers can understand. A getter alone does not make a design encapsulated if it returns a mutable internal collection that callers can freely modify.

Establish and preserve an invariant

The counter example accepts a nonnegative initial value and permits positive additions. Private state prevents direct outside assignment, while the accessor returns a primitive snapshot. Math.addExact prevents silent integer overflow from violating the intended count. The output is 5 after adding 3 to the initial 2. The constructor's checks apply before a usable object is returned.

A constructor has the class name and no return type; writing void Counter creates an ordinary method rather than a constructor. this distinguishes a field from a parameter with the same name. Keep constructor work focused on establishing valid local state, avoiding surprising I/O, remote calls, or leaking this to other threads before initialization finishes.

java A counter protects its state
public class Main {
static final class Counter {
 private int value;
 Counter(int initial) { if(initial<0) throw new IllegalArgumentException("negative"); value=initial; }
 void add(int n) { if(n<=0) throw new IllegalArgumentException("nonpositive"); value=Math.addExact(value,n); }
 int value() { return value; }
}
 public static void main(String[] args) throws Exception {
Counter c=new Counter(2);
c.add(3);
System.out.println(c.value());
 }
}
Expected output
5

Both construction and mutation validate the domain. The private field cannot be assigned directly by a caller.

Separate object state and class-wide behavior

An instance field belongs to an object, while a static field belongs to the class as loaded. A static mutable counter is shared state, not a fresh counter for each request, and can create both lifecycle and concurrency problems. Static methods are useful for operations without instance-specific state, such as a numeric utility, but should not become hidden global storage.

final prevents a variable from being reassigned after initialization; it does not freeze a referenced object's contents. A final List reference can still identify a mutable list. For an immutable value object, make its state private, establish it fully during construction, avoid leaking mutable internals, and control any referenced mutable objects through copying or an explicitly immutable representation.

Describe behavior through an interface

An interface describes a behavioral contract that several classes can implement. A variable declared as the interface can hold an implementing object, and an overridden instance method dispatches to that object's implementation. The example's PriceRule reference points to HalfPrice, so applying it to 10 returns 5. This differs from overloading, which the compiler chooses using signatures and declared types.

Use @Override to catch misspellings or incompatible signatures. A subtype should honor the promised input and result behavior rather than merely compile. Composition can combine collaborators without forcing an inheritance hierarchy. Prefer a small interface that represents a real substitution point; creating an interface for every class adds ceremony without automatically improving design.

java Runtime dispatch through an interface
public class Main {
interface PriceRule { int apply(int amount); }
static final class HalfPrice implements PriceRule {
 @Override public int apply(int amount) { return amount/2; }
}
 public static void main(String[] args) throws Exception {
PriceRule rule=new HalfPrice();
System.out.println(rule.apply(10));
 }
}
Expected output
5

The declared type is PriceRule; the actual object supplies HalfPrice.apply. The example deliberately uses an even integer amount.

Choose identity and value semantics deliberately

Object identity answers whether two references designate the same object. Value equality answers whether their relevant state is equivalent, using a documented equals contract. Classes used as map keys need compatible equals and hashCode implementations and stable equality-relevant state while stored. A mutable key can become effectively unreachable through lookup after its hash-relevant fields change.

Java 17 records provide concise shallowly immutable data carriers and generate value-oriented methods, but do not deeply freeze mutable components. Sealed types can restrict permitted implementations when a closed domain is intentional. These tools are standard in the baseline, yet encapsulation still depends on domain design, validation, and clear ownership rather than a keyword alone.

Common errors and fixes

non-static variable cannot be referenced from a static context

Why it happens
Code used instance state without identifying an object.
How to fix it
Create or receive the intended instance, or redesign a genuinely stateless operation.
Prevent it next time
Keep instance and class responsibilities explicit.

method does not override or implement a method from a supertype

Why it happens
The intended override has the wrong signature.
How to fix it
Match the interface contract and keep @Override.
Prevent it next time
Let compiler checks guard implementation changes.

An invariant was bypassed

Why it happens
A field or mutable internal object was exposed directly.
How to fix it
Restrict mutation and return an appropriate copy or immutable view.
Prevent it next time
Review ownership at every public method.

Check your understanding

1. Does final int[] values prevent values[0]=9?

Show worked solution

No. final prevents assigning a different array to the variable, while the existing array remains mutable. Use a defensive copy or another ownership policy if callers must not change internal elements.

2. Design a constructor for a positive-capacity container.

Show worked solution

Reject capacity <= 0 before exposing a usable object, store the accepted capacity privately, and make later methods preserve occupancy between zero and capacity. Test invalid construction and both transition boundaries instead of testing only getters.

Key takeaways

  • Constructors establish invariants; methods preserve them.
  • final references do not deeply freeze objects.
  • Interfaces describe behavioral substitution, not just matching syntax.

Official references