Construct meaningful state
A class organizes state and operations. Before choosing fields, write the rules that make an instance valid. For a capacity limit, examples include a present name, a quantity from 1 through 99 and no public stage where the object contains only half its data. A constructor can reject invalid input before delivering the instance to its caller. The rule must remain true in later operations: checking creation alone is insufficient if a setter accepts forbidden values.
In class Ticket { Ticket(String id) {} }, the explicit declaration does not come with a generated Ticket(). A call without arguments fails compilation; it does not automatically receive null. When you need an alternative, define its meaning: a no-argument constructor producing an empty identifier might compile while violating the domain. Another possibility is a factory that delivers an object only after gathering and validating its data. In the questions, distinguish compilation rejection, an exception during construction and an object created with inappropriate state.
Shared contract and composition
An interface names operations that different implementations must provide. In interface Meter { int read(); }, read is public even without the public keyword. A concrete class must provide a compatible public implementation; a package-access method does not meet that obligation. Compilation checks part of the contract. The team still has to define units, absent results, errors and observable effects.
Imagine ReceiptService, which prepares text and sends it through a Sender supplied to its constructor. The contract returns an acceptance reference or throws SendFailure. SMTP and in-memory adapters translate their details into that boundary. ReceiptService stores the Sender and delegates the operation without naming providers. This composition allows implementation replacement. Extending an SMTP client merely to reuse send ties the service to that type; storing Object and casting to SMTP retains the same dependency.
An acceptance reference does not prove recipient delivery. If an implementation returns success after merely storing a failed attempt, it may implement the interface syntactically while violating its semantic contract. The design question requires both.
Defaults and inherited behavior
An interface may supply a default method. This provides reusable behavior, but not a priority rule based on implements order. If two unrelated interfaces offer source() and the class does not implement it, the conflict must be resolved. A public class implementation can define what source means for that adapter. Before deciding, ask whether both interfaces use the same name for the same concept; combining incompatible results merely to satisfy the compiler may introduce another defect.
A superclass presents a different case. If Parent already supplies public String mode(), and an interface offers a compatible default with that signature, Worker extending Parent inherits the concrete class implementation. Changing the reference type to the interface does not select another body. To diagnose, first identify signatures and method origins, then the implementation the class actually has. Do not generalize this rule to private, static or abstract methods with different inheritance rules.
Instance state and class state
An instance field belongs to each object; a static field belongs to the class. In a counter with static int total and int own, two calls on a and one on b yield total 3, a.own 2 and b.own 1 if all begin at zero and execution is sequential. Creating b does not reset total: there is no separate copy of that field for each new object.
The choice should match data ownership. A per-client request limit accidentally stored in static may be replaced by the next client’s configuration. Even without threads, two instances no longer have independent policies. Correcting this requires deciding who owns the limit and how it is supplied; creating more instances does not fix the shared field.
These examples isolate sharing to make reasoning easier. static supplies no mutual exclusion, and total++ does not become a safe cross-thread count merely because it operates on a class field. The concurrency lesson will address coordination. Do not infer a guarantee that the sequential example does not analyze.
Records: components and construction
A record describes a fixed component set. In record Station(String code, int platforms) {}, the second value is accessed through platforms(), with no get prefix or argument. A record may implement interfaces, but it is final and cannot extend a chosen class or add instance fields outside its components. This suits value representation; it does not remove the need to establish value rules.
A compact constructor validates or normalizes parameters before the compiler assigns them to fields. In Route { code = Objects.requireNonNull(code).strip(); }, code denotes the parameter. Discarding strip’s result leaves the text unnormalized. Writing this.code = ... in that body is a compilation error: field assignment occurs implicitly after normal completion. In a public record, an explicitly declared canonical constructor must also be public.
Decide separately what to do with empty text. Rejecting null and stripping whitespace may produce an empty value; if that is invalid, another check is needed. The Route example tests only its two stated rules. The WindowLimits snippet below uses primitive components and explicitly checks the range of both.
Equality and value stability
A record does not make objects referenced by its components immutable. In Parcel(int[] weights), the caller may retain the array and change weights after construction; the generated accessor also returns that reference. Copying on input closes the first path. Copying on output closes the second. An int array has no nested objects to copy, but equality remains a separate decision.
With generated equals, two Parcel instances built from distinct arrays {3, 8} differ: the array component’s equality uses identity. Equal elements are insufficient. Cloning at both boundaries protects state, but reconstruction through accessors produces another array and fails to preserve generated equality. Equality and hashing would need to be designed together, or a better representation chosen.
WindowLimits has only two integers, and its API may still change. Primitive int components first and second remove the array from the representation and allow generated value equality. A variable number of elements would lead to a different decision. The collections lesson examines component and key choices further.
Review a reconstructed value
A record review should trace construction, access and reconstruction. Consider a stored limit of 15 whose accessor returns at most 10. The field still holds 15. Building another record from the accessor result stores 10; generated equals reads fields, so the objects differ. The compiler accepts the accessor but does not verify that the API preserves the expectation of reconstructing an equal value from its components.
Two domain decisions are possible: reject values above the limit or normalize them during construction. In either case, the accessor should faithfully represent the component. If a calculated view applies a limit only for display, a separate method can name that purpose while preserving the component accessor.
In the guided WindowLimits review, classify four proposals: a shared array; copying only on input; copies at both boundaries with generated equals; and two int components. For each, identify who can change state and whether reconstruction through accessors preserves equality. Select the representation meeting both rules and explain the alternatives’ failures. Assessment requires selecting answers and interpreting snippets without writing or executing programs.
record WindowLimits(int first, int second) {
WindowLimits {
if (first < 0 || first > 99 || second < 0 || second > 99) {
throw new IllegalArgumentException("limits must be in 0..99");
}
}
}
// Read the values; no execution is required.
// var original = new WindowLimits(12, 40);
// var reconstructed = new WindowLimits(original.first(), original.second());
// original.equals(reconstructed) is true.
// new WindowLimits(12, 100) rejects the second limit.
The record has package access; examples assume code with access to the type. 12 and 40 pass validation and reconstruction preserves both components. 100 is rejected before the caller receives an instance. There is no shared array or method changing limits.
Common pitfalls
Confusing defaults with textual priority; narrowing interface method access; storing per-client state in static fields; assuming deep copies in records; transforming accessors without reviewing equality.
Related topics: References, identity and text · Methods, contracts and control flow · Collections, generics and keys · Concurrency, state and cancellation
A useful type preserves its contract from construction through use. For records, check state stability and equality separately.
Reference: JLS 25: Record classes · Java SE 25; no preview features