Inheritance vs. Composition, Packages, and Access Control
When 'is-a' (inheritance) actually fits better than 'has-a' (composition) — and why most experienced Java developers reach for composition by default.
What you'll learn
- Distinguish an 'is-a' relationship (inheritance) from a 'has-a' relationship (composition)
- Extend a class correctly, including calling super()
- Explain why composition is favored over inheritance for code reuse in most modern Java design
Prerequisites
Explanation
Inheritance (class Course extends Content) models a genuine is-a relationship: a Course really is a kind of Content, so it makes sense for it to automatically get Content's fields and methods and be usable anywhere a Content is expected. The subclass's constructor must call super(...) — the parent class's constructor — either explicitly as the first line, or implicitly (Java inserts a no-argument super() call automatically if you don't write one, which fails to compile if the parent has no no-argument constructor). A subclass can override an inherited method by redeclaring it with the same signature, annotated with @Override (not required by the compiler, but it catches a real class of typos: if the signature doesn't actually match a parent method, @Override turns that mismatch into a compile error instead of a silent new, unrelated method).
Composition (a class holding a reference to another class as a field, rather than extending it) models a has-a relationship: an Enrollment has a Learner and has a Course — it isn't a kind of either one. The practical, well-established guidance — often summarized as "favor composition over inheritance" — is that composition is usually the safer default for code reuse: inheritance couples a subclass tightly to its parent's implementation details, not just its public contract, so a seemingly-safe change to the parent class can silently break every subclass in ways that are hard to see locally. Composition avoids that: a class using another class only through its public methods can have that inner object replaced or changed far more safely, because there's no hidden dependency on the other class's internal implementation choices.
Packages (package com.visasparkschools.enrollment;, matched by a directory structure) organize related classes and control visibility at a coarser grain than the four access modifiers: private (this class only), default/package-private (no modifier at all — visible to the whole package, a genuinely useful and often-overlooked middle ground), protected (package, plus subclasses anywhere), and public (everywhere). Choosing the narrowest access level that still works is the same discipline as encapsulating fields: it keeps the number of places able to depend on an implementation detail as small as possible, which is exactly what makes future changes safe.
Example
Inheritance (extends, is-a) versus composition (a field holding another object, has-a), modeled in JS classes -- the design tradeoff is identical to Java's.
// Inheritance: Course IS-A Content
class Content {
constructor(title) { this.title = title; }
describe() { return "Content: " + this.title; }
}
class Course extends Content {
constructor(title, moduleCount) {
super(title); // must call the parent constructor
this.moduleCount = moduleCount;
}
describe() { // overriding the parent's method
return super.describe() + ` (${this.moduleCount} modules)`;
}
}
console.log(new Course("Java Basics", 6).describe());
// Composition: Enrollment HAS-A Learner and HAS-A Course (not "is a" either one)
class Enrollment {
constructor(learner, course) {
this.learner = learner; // held by reference, not inherited
this.course = course;
}
summary() {
return `${this.learner} enrolled in ${this.course.title}`;
}
}
console.log(new Enrollment("Alice", new Course("Java Basics", 6)).summary());Try it yourself
Add a second subclass Quiz extends Content, override describe(), and print an instance of it.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
Model Java inheritance: class Shape with area() returning 0, and class Circle extends Shape with a constructor(radius) calling super() and overriding area() to return Math.PI * radius * radius.
Checks: the base class's default behavior is unchanged · Circle overrides area() correctly · a Circle is-a Shape (inheritance relationship holds)
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
Model composition: class Engine with horsepower() returning a fixed number, and class Car that HAS-A Engine (stored as a field, not extended) with a describe() method combining the car's name and its engine's horsepower.
Checks: describe() correctly delegates to the held Engine · Car is not an Engine (composition, not inheritance)
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Reaching for `extends` purely to reuse a few methods, when the relationship isn't really 'is-a' -- this creates tight coupling to the parent's internals for a benefit composition gives more safely.
- Forgetting that a subclass constructor implicitly calls the parent's no-argument constructor unless you write an explicit super(...) call -- this fails to compile if the parent has no no-argument constructor.
- Marking a method @Override on something that isn't actually overriding anything (a typo'd method name, or mismatched parameters) and not noticing, because without @Override the compiler treats it as a brand-new, unrelated method rather than flagging the mismatch.
Knowledge check
Takeaway
Use inheritance only for a genuine 'is-a' relationship where the subclass should be usable anywhere the parent is expected; default to composition ('has-a', a field holding another object) for reuse, since it couples you only to the other class's public contract, not its internals.
Summary
extends models is-a; a field holding another object models has-a. Subclass constructors must reach the parent constructor via super(). @Override catches signature mismatches at compile time. Packages and access modifiers (private, package-private, protected, public) control how narrowly a class's internals are exposed.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.