Java foundations · Lesson 31 About 80 min with practice
Java Abstract Classes and Interfaces
Separate what a caller needs from how an implementation works. Build a small interface, reuse suitable state through an abstract class, and supply interchangeable implementations to one caller.
Before you start: review inheritance and super, polymorphism, and encapsulation. All examples are complete Java 21 Main.java programs. Run, edit, and reset them without installing a local JDK or supplying input.
What you will be able to do
Define a public behavior contract with an interface.
Implement its abstract method with a public override.
Use an abstract class for shared validated state without instantiating it directly.
Combine extends and implements deliberately rather than treating them as alternatives.
Explain default methods, multiple interface capabilities, and default conflicts.
Inject different implementations into one caller without casts or type checks.
Start with what the caller needs, not a family tree
A lesson reminder can be displayed as an email-style preview, an inbox-style preview, or a log-style preview. The caller needs one capability: turn a message into preview text. It should not need to know whether that text comes from a destination-based class or a completely separate implementation.
Our contract is String preview(String message). Accepted messages are non-null and nonblank, surrounding Java-defined whitespace is stripped, and the method returns a deterministic String without changing state. Invalid messages throw IllegalArgumentException with a documented message. The contract includes behavior, not just a method signature.
No notification is sent in this lesson. EmailPreview does not connect to SMTP, InboxPreview does not persist messages, and LogPreview does not write logs. Every implementation only returns text, and main prints it. This explicit boundary keeps the Java design exercise separate from real integrations and delivery claims.
JAVA 21 COMPILER
Main.java · Use a capability through an interface reference
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){NotificationChannel channel =newLogPreview();System.out.println(channel.preview(" Lesson ready "));}}interfaceNotificationChannel{Stringpreview(String message);}classLogPreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){if(message ==null|| message.isBlank()){thrownewIllegalArgumentException("Message must not be blank.");}return"LOG: "+ message.strip();}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · compare the shared contract with the selected implementation
LOG: Lesson ready
An interface declares a capability that different classes can provide
NotificationChannel is an interface, not a concrete object you can create with new NotificationChannel(). You can declare a variable of that interface type and assign a compatible implementing object. The actual class supplies the preview implementation, and normal instance dispatch selects it when the shared caller invokes the method.
The declaration String preview(String message); is implicitly public and abstract in this interface. A class implementing it must provide a public compatible method, or remain abstract itself. An omitted public modifier in a class method would give package access and incorrectly reduce the promised visibility.
Interfaces have no per-instance fields or constructors. Their declared fields are public static final constants, not storage for each object's mutable state. Modern Java interfaces can also contain default, static, and private methods, so saying that interfaces can never contain method bodies would be incorrect. This lesson uses one small default method deliberately.
An abstract class can hold state and run a constructor
EmailPreview and InboxPreview both need a validated destination. NamedChannel is an abstract class that stores that destination in a private final field, exposes a read-only getter, and leaves preview abstract. An abstract method declaration ends with a semicolon rather than providing an empty placeholder body.
You cannot instantiate NamedChannel directly, but its constructor still runs while a concrete subclass is initialized. EmailPreview calls super(destination), so the shared validation and storage happen before the subclass is ready. The abstract base does not need a dummy preview result merely to satisfy the compiler.
An abstract class can mix abstract and concrete methods, and it can even be abstract without declaring an abstract method. Here abstraction is useful because the destination machinery is real shared implementation, while the format remains an intentional extension point. The constructor does not call that overridable extension point before subclass initialization.
JAVA 21 COMPILER
Main.java · A concrete child initializes shared abstract-class state
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){NamedChannel channel =newEmailPreview(" learner@example.test ");System.out.println(channel.getDestination());System.out.println(channel.preview("Lesson ready"));}}interfaceNotificationChannel{Stringpreview(String message);defaultStringdescribe(String message){return"Preview: "+preview(message);}}finalclassTextRules{privateTextRules(){}staticStringrequireText(String value,String label){if(value ==null|| value.isBlank()){thrownewIllegalArgumentException(label +" must not be blank.");}return value.strip();}}abstractclassNamedChannelimplementsNotificationChannel{privatefinalString destination;protectedNamedChannel(String destination){this.destination =TextRules.requireText(destination,"Destination");}publicfinalStringgetDestination(){return destination;}@OverridepublicabstractStringpreview(String message);}classEmailPreviewextendsNamedChannel{publicEmailPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"EMAIL to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classInboxPreviewextendsNamedChannel{publicInboxPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"INBOX to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classLogPreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){return"LOG: "+TextRules.requireText(message,"Message");}}classPreviewService{privatefinalNotificationChannel channel;publicPreviewService(NotificationChannel channel){if(channel ==null){thrownewIllegalArgumentException("Channel must not be null.");}this.channel = channel;}publicStringrender(String message){return channel.preview(message);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · compare the shared contract with the selected implementation
learner@example.test
EMAIL to learner@example.test: Lesson ready
Trace construction step by step
new EmailPreview supplies a destination to the concrete constructor.
super(destination) invokes the abstract base constructor on that same object.
The base validates and strips the destination, then stores it in its private final field.
The completed concrete object can now implement preview using its inherited getter.
The abstract base participates in construction even though new NamedChannel is illegal. Do not call overridable methods from that constructor: subclass initialization may not be finished.
implements and extends solve different parts of the design
NamedChannel implements NotificationChannel, so its concrete children are also NotificationChannel implementations. EmailPreview extends NamedChannel to reuse the destination state and constructor. The class inheritance and interface relationship work together; they are not mutually exclusive choices.
Both concrete destination-based classes override preview with @Override and public access. They read the inherited destination getter and use the shared TextRules validation utility. The utility is not a superclass: reusing one validation function does not require every caller to belong to one class hierarchy.
LogPreview implements the same interface directly and has no destination field. Forcing it to extend NamedChannel would require meaningless destination data simply to fit the hierarchy. An interface lets otherwise unrelated classes offer one compatible capability without inheriting irrelevant storage.
JAVA 21 COMPILER
Main.java · Share destination rules, specialize the preview
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){NamedChannel email =newEmailPreview("learner@example.test");NamedChannel inbox =newInboxPreview("Dashboard");System.out.println(email.preview("Lesson ready"));System.out.println(inbox.preview("Lesson ready"));}}interfaceNotificationChannel{Stringpreview(String message);defaultStringdescribe(String message){return"Preview: "+preview(message);}}finalclassTextRules{privateTextRules(){}staticStringrequireText(String value,String label){if(value ==null|| value.isBlank()){thrownewIllegalArgumentException(label +" must not be blank.");}return value.strip();}}abstractclassNamedChannelimplementsNotificationChannel{privatefinalString destination;protectedNamedChannel(String destination){this.destination =TextRules.requireText(destination,"Destination");}publicfinalStringgetDestination(){return destination;}@OverridepublicabstractStringpreview(String message);}classEmailPreviewextendsNamedChannel{publicEmailPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"EMAIL to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classInboxPreviewextendsNamedChannel{publicInboxPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"INBOX to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classLogPreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){return"LOG: "+TextRules.requireText(message,"Message");}}classPreviewService{privatefinalNotificationChannel channel;publicPreviewService(NotificationChannel channel){if(channel ==null){thrownewIllegalArgumentException("Channel must not be null.");}this.channel = channel;}publicStringrender(String message){return channel.preview(message);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · compare the shared contract with the selected implementation
EMAIL to learner@example.test: Lesson ready
INBOX to Dashboard: Lesson ready
Keep callers typed to the capability they actually use
The mixed NotificationChannel array can contain both an EmailPreview and a LogPreview. Each iteration calls preview with the same source expression; the implementing class determines the returned format. No instanceof branch or downcast is necessary to decide which method to run.
An interface reference does not expose every method on the actual object. getDestination exists on NamedChannel, not NotificationChannel, so a caller typed only to the interface cannot invoke it directly. That restriction is helpful when the caller needs previews rather than destination-specific details.
A new implementation that honors the same validation, return, and no-side-effect promises can join the loop without editing it. The compiler checks that the required methods exist; tests must still verify the behavioral promises. A method that secretly sends a message would violate this lesson's preview-only contract even if its signature compiled.
JAVA 21 COMPILER
Main.java · The interface does not require one class hierarchy
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){NotificationChannel[] channels ={newEmailPreview("learner@example.test"),newLogPreview()};for(NotificationChannel channel : channels){System.out.println(channel.preview("Practice now"));}}}interfaceNotificationChannel{Stringpreview(String message);defaultStringdescribe(String message){return"Preview: "+preview(message);}}finalclassTextRules{privateTextRules(){}staticStringrequireText(String value,String label){if(value ==null|| value.isBlank()){thrownewIllegalArgumentException(label +" must not be blank.");}return value.strip();}}abstractclassNamedChannelimplementsNotificationChannel{privatefinalString destination;protectedNamedChannel(String destination){this.destination =TextRules.requireText(destination,"Destination");}publicfinalStringgetDestination(){return destination;}@OverridepublicabstractStringpreview(String message);}classEmailPreviewextendsNamedChannel{publicEmailPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"EMAIL to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classInboxPreviewextendsNamedChannel{publicInboxPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"INBOX to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classLogPreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){return"LOG: "+TextRules.requireText(message,"Message");}}classPreviewService{privatefinalNotificationChannel channel;publicPreviewService(NotificationChannel channel){if(channel ==null){thrownewIllegalArgumentException("Channel must not be null.");}this.channel = channel;}publicStringrender(String message){return channel.preview(message);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · compare the shared contract with the selected implementation
EMAIL to learner@example.test: Practice now
LOG: Practice now
Read the whole flow through one shared operation
The three-channel example creates concrete objects at the setup boundary, then processes interface references. Object creation still needs a concrete implementation choice; using an interface does not eliminate the need to decide which behavior is wanted somewhere in the program.
Each preview validates and strips the message, reads any fixed destination, and returns one String. main owns the printing. Reordering the array should reorder the three result lines without changing the loop or an individual channel's stored destination.
An empty channel array produces no result lines. A null array entry would not mean a default channel: calling through it would throw NullPointerException. Decide and test such input rules at the boundary rather than assuming interface types automatically provide null safety.
JAVA 21 COMPILER
Main.java · One loop uses three implementations
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){NotificationChannel[] channels ={newEmailPreview("learner@example.test"),newInboxPreview("Dashboard"),newLogPreview()};for(NotificationChannel channel : channels){System.out.println(channel.preview("Lesson ready"));}}}interfaceNotificationChannel{Stringpreview(String message);defaultStringdescribe(String message){return"Preview: "+preview(message);}}finalclassTextRules{privateTextRules(){}staticStringrequireText(String value,String label){if(value ==null|| value.isBlank()){thrownewIllegalArgumentException(label +" must not be blank.");}return value.strip();}}abstractclassNamedChannelimplementsNotificationChannel{privatefinalString destination;protectedNamedChannel(String destination){this.destination =TextRules.requireText(destination,"Destination");}publicfinalStringgetDestination(){return destination;}@OverridepublicabstractStringpreview(String message);}classEmailPreviewextendsNamedChannel{publicEmailPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"EMAIL to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classInboxPreviewextendsNamedChannel{publicInboxPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"INBOX to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classLogPreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){return"LOG: "+TextRules.requireText(message,"Message");}}classPreviewService{privatefinalNotificationChannel channel;publicPreviewService(NotificationChannel channel){if(channel ==null){thrownewIllegalArgumentException("Channel must not be null.");}this.channel = channel;}publicStringrender(String message){return channel.preview(message);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · compare the shared contract with the selected implementation
EMAIL to learner@example.test: Lesson ready
INBOX to Dashboard: Lesson ready
LOG: Lesson ready
A default method provides behavior without adding instance storage
NotificationChannel.describe has a body marked default. It returns Preview: followed by preview(message). The normal virtual call inside that method reaches the concrete implementation, so both email-style and log-style descriptions retain their own format.
A default method is not an interface constructor, a static method, or a separate object. It can use the interface's public contract but does not introduce per-object fields. Concrete classes may override a default method when they preserve its contract, just as intended overrides use @Override elsewhere.
If a class inherits unrelated default methods with the same signature, it may need to override and resolve the conflict explicitly; multiple interface inheritance is not a promise that conflicting behavior will merge automatically. A concrete class method takes precedence over an interface default, and more-specific interface rules also matter. Prefer small coherent defaults rather than hiding major policy in them.
JAVA 21 COMPILER
Main.java · A default method can call the implemented contract
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){NotificationChannel email =newEmailPreview("learner@example.test");NotificationChannel log =newLogPreview();System.out.println(email.describe("Practice now"));System.out.println(log.describe("Practice now"));}}interfaceNotificationChannel{Stringpreview(String message);defaultStringdescribe(String message){return"Preview: "+preview(message);}}finalclassTextRules{privateTextRules(){}staticStringrequireText(String value,String label){if(value ==null|| value.isBlank()){thrownewIllegalArgumentException(label +" must not be blank.");}return value.strip();}}abstractclassNamedChannelimplementsNotificationChannel{privatefinalString destination;protectedNamedChannel(String destination){this.destination =TextRules.requireText(destination,"Destination");}publicfinalStringgetDestination(){return destination;}@OverridepublicabstractStringpreview(String message);}classEmailPreviewextendsNamedChannel{publicEmailPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"EMAIL to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classInboxPreviewextendsNamedChannel{publicInboxPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"INBOX to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classLogPreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){return"LOG: "+TextRules.requireText(message,"Message");}}classPreviewService{privatefinalNotificationChannel channel;publicPreviewService(NotificationChannel channel){if(channel ==null){thrownewIllegalArgumentException("Channel must not be null.");}this.channel = channel;}publicStringrender(String message){return channel.preview(message);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · compare the shared contract with the selected implementation
Preview: EMAIL to learner@example.test: Practice now
Preview: LOG: Practice now
Follow describe into the concrete preview
The caller requests describe through NotificationChannel.
The inherited default body prefixes Preview:.
Its preview(message) call dispatches on the same concrete receiver.
EmailPreview or LogPreview supplies the remaining text.
The default reuses a capability; it does not create a second object or add a destination field.
A class can implement several interfaces while extending one class
Badge implements Identified and Printable. A variable of either interface type can refer to the same Badge instance, exposing the capability promised by that reference type. The identity comparison shows two views of one object, not two copies or two independently constructed badges.
Java still permits only one direct class superclass. Implementing several interfaces adds compatible contracts without inheriting several classes' instance fields and constructors. An interface can itself extend other interfaces when it is defining a combined capability, while a class uses implements for interfaces.
Do not bundle unrelated methods into one oversized interface merely because one class happens to implement them all. A caller that only needs id should depend on Identified, while one that only needs text can depend on Printable. Small contracts make it easier for different implementations to provide just the useful behavior.
JAVA 21 COMPILER
Main.java · One class can implement two capabilities
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
Expected output · compare the shared contract with the selected implementation
STUDENT-01
Learning badge
Same object: true
Use composition to supply an implementation to a caller
PreviewService receives a NotificationChannel in its constructor and rejects null. Its render method delegates to that collaborator's preview. The service does not extend a channel, inspect its class, or construct a fixed email implementation inside render; setup supplies the desired behavior.
This is a simple form of dependency injection: the dependency is provided as an ordinary constructor argument. No framework is required. The service holds a reference, not a deep copy, so its behavior still depends on the supplied object's contract; a final field stabilizes the reference rather than freezing arbitrary collaborator internals.
Choose an interface for a capability that several implementations can offer. Choose an abstract base when related implementations genuinely share suitable state or construction code. Use both when those reasons overlap, as NamedChannel does, and use composition when a caller needs to use the capability rather than become one implementation.
JAVA 21 COMPILER
Main.java · Supply a collaborator instead of branching by type
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){PreviewService email =newPreviewService(newEmailPreview("learner@example.test"));PreviewService inbox =newPreviewService(newInboxPreview("Dashboard"));System.out.println(email.render("Review complete"));System.out.println(inbox.render("Review complete"));}}interfaceNotificationChannel{Stringpreview(String message);defaultStringdescribe(String message){return"Preview: "+preview(message);}}finalclassTextRules{privateTextRules(){}staticStringrequireText(String value,String label){if(value ==null|| value.isBlank()){thrownewIllegalArgumentException(label +" must not be blank.");}return value.strip();}}abstractclassNamedChannelimplementsNotificationChannel{privatefinalString destination;protectedNamedChannel(String destination){this.destination =TextRules.requireText(destination,"Destination");}publicfinalStringgetDestination(){return destination;}@OverridepublicabstractStringpreview(String message);}classEmailPreviewextendsNamedChannel{publicEmailPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"EMAIL to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classInboxPreviewextendsNamedChannel{publicInboxPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"INBOX to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classLogPreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){return"LOG: "+TextRules.requireText(message,"Message");}}classPreviewService{privatefinalNotificationChannel channel;publicPreviewService(NotificationChannel channel){if(channel ==null){thrownewIllegalArgumentException("Channel must not be null.");}this.channel = channel;}publicStringrender(String message){return channel.preview(message);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · compare the shared contract with the selected implementation
EMAIL to learner@example.test: Review complete
INBOX to Dashboard: Review complete
Verify the contract independently from the inheritance shape
Run the same valid, padded, null, empty, and whitespace-only messages against every implementation and the default describe method. Check exact Strings and repeated calls. Invalid input must fail with the same documented message, while accepted input must not change destinations or the caller's original String.
Test named-channel construction separately: null and blank destinations are rejected, padded destinations are stored stripped, and the destination getter remains stable after previews. Test service construction with null and with every implementation. Reorder the shared array and try an empty one without changing the caller.
Finally test deliberate compilation failures: instantiating an abstract class or interface, omitting the required implementation in a concrete class, reducing public access, and leaving conflicting defaults unresolved. These structural checks complement behavioral tests; passing one does not prove the other.
Repair exercise: an implementation is not public
This class attempts to implement the interface with a package-access method. The starter intentionally fails compilation because the interface promises public access. Repair the implementation's visibility and add @Override; do not weaken or remove the interface contract.
JAVA 21 COMPILER
Main.java · Find the visibility error
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){NotificationChannel channel =newSimplePreview();System.out.println(channel.preview("Lesson ready"));}}interfaceNotificationChannel{Stringpreview(String message);defaultStringdescribe(String message){return"Preview: "+preview(message);}}finalclassTextRules{privateTextRules(){}staticStringrequireText(String value,String label){if(value ==null|| value.isBlank()){thrownewIllegalArgumentException(label +" must not be blank.");}return value.strip();}}abstractclassNamedChannelimplementsNotificationChannel{privatefinalString destination;protectedNamedChannel(String destination){this.destination =TextRules.requireText(destination,"Destination");}publicfinalStringgetDestination(){return destination;}@OverridepublicabstractStringpreview(String message);}classEmailPreviewextendsNamedChannel{publicEmailPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"EMAIL to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classInboxPreviewextendsNamedChannel{publicInboxPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"INBOX to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classLogPreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){return"LOG: "+TextRules.requireText(message,"Message");}}classPreviewService{privatefinalNotificationChannel channel;publicPreviewService(NotificationChannel channel){if(channel ==null){thrownewIllegalArgumentException("Channel must not be null.");}this.channel = channel;}publicStringrender(String message){return channel.preview(message);}}classSimplePreviewimplementsNotificationChannel{Stringpreview(String message){return"SIMPLE: "+TextRules.requireText(message,"Message");}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
This starter intentionally fails compilation. Expect a diagnostic about attempting to assign weaker access privileges; no program output should run. A diagnostic may contain extra line numbers depending on your edit.
Reveal the explained repair
The bodyless interface method is implicitly public. A concrete implementation cannot satisfy that promise with a less-visible method. public String preview(String message) keeps the contract, and @Override verifies that the signature actually implements the required operation.
JAVA 21 COMPILER
Main.java · Preserve public access
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){NotificationChannel channel =newSimplePreview();System.out.println(channel.preview("Lesson ready"));}}interfaceNotificationChannel{Stringpreview(String message);defaultStringdescribe(String message){return"Preview: "+preview(message);}}finalclassTextRules{privateTextRules(){}staticStringrequireText(String value,String label){if(value ==null|| value.isBlank()){thrownewIllegalArgumentException(label +" must not be blank.");}return value.strip();}}abstractclassNamedChannelimplementsNotificationChannel{privatefinalString destination;protectedNamedChannel(String destination){this.destination =TextRules.requireText(destination,"Destination");}publicfinalStringgetDestination(){return destination;}@OverridepublicabstractStringpreview(String message);}classEmailPreviewextendsNamedChannel{publicEmailPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"EMAIL to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classInboxPreviewextendsNamedChannel{publicInboxPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"INBOX to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classLogPreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){return"LOG: "+TextRules.requireText(message,"Message");}}classPreviewService{privatefinalNotificationChannel channel;publicPreviewService(NotificationChannel channel){if(channel ==null){thrownewIllegalArgumentException("Channel must not be null.");}this.channel = channel;}publicStringrender(String message){return channel.preview(message);}}classSimplePreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){return"SIMPLE: "+TextRules.requireText(message,"Message");}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Build a preview-only notification capability, a shared destination base, three concrete implementations, and a composed service. Every method returns text or validates input; no method sends, stores, or logs a notification. Use one Main.java with separate package-private model types for the website runner.
Declare NotificationChannel with String preview(String message) and default describe(String message) returning "Preview: " + preview(message). The shared contract rejects null/blank messages with IllegalArgumentException and Message must not be blank.; accepted text is stripped.
Implement a package-private TextRules utility with static requireText(String value, String label). Reject null/blank with <label> must not be blank. and return value.strip(). Give the utility a private constructor; keep it independent of the channel hierarchy.
Declare abstract NamedChannel implements NotificationChannel. Store a private final destination validated by its protected constructor with label Destination. Expose final getDestination and declare public abstract String preview(String message). Do not instantiate the abstract base or call its abstract method from its constructor.
Implement EmailPreview and InboxPreview extending NamedChannel. Their public preview overrides return EMAIL to <destination>: <message> and INBOX to <destination>: <message>. Implement LogPreview directly against the interface, returning LOG: <message>, with no irrelevant destination field.
Implement PreviewService with a private final NotificationChannel supplied through its constructor. Reject null with Channel must not be null. and delegate render(String message) to channel.preview(message). No type branches, downcasts, printing, networking, or persistence belong in model methods.
In main create the three channels and use one interface-typed loop for the supplied message. Print the default description, the inbox service result, and caught invalid-message and invalid-destination errors. Verify every implementation's shared contract and repeated-call behavior before finishing.
Build the contract first, then shared state, then concrete behavior. The finished program should produce:
EMAIL to learner@example.test: Lesson ready
INBOX to Dashboard: Lesson ready
LOG: Lesson ready
Preview: EMAIL to learner@example.test: Practice now
INBOX to Dashboard: Review complete
Rejected message: Message must not be blank.
Rejected destination: Destination must not be blank.
JAVA 21 COMPILER
Main.java · Build the notification preview contract
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){// TODO: create email, inbox, and log preview implementations.// TODO: report through one NotificationChannel loop.// TODO: use the default description and composed service.// TODO: catch the two specified invalid-input cases.}}interfaceNotificationChannel{// TODO: public behavior contract and default description.}abstractclassNamedChannelimplementsNotificationChannel{// TODO: shared validated destination, but no dummy preview body.}// TODO: TextRules, concrete channel classes, and PreviewService.
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Stage 1 hints: write a small contract
Declare preview without a body in NotificationChannel. Give describe a default body that calls preview. Put text validation in a separate utility so implementations without a destination can reuse it too.
Stage 2 hints: share only relevant state
NamedChannel stores one private final destination and validates it during construction. Its abstract preview declaration ends with a semicolon. EmailPreview and InboxPreview call super and supply public overrides; LogPreview implements the interface directly.
Stage 3 hints: keep callers interchangeable
Use NotificationChannel[] and the same preview call in every loop iteration. Supply a channel to PreviewService through its constructor. Test invalid input with try/catch; model methods should return text or throw, not print or deliver anything.
Reveal the complete explained solution
JAVA 21 COMPILER
Main.java · Complete notification preview lab
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){NotificationChannel[] channels ={newEmailPreview(" learner@example.test "),newInboxPreview(" Dashboard "),newLogPreview()};for(NotificationChannel channel : channels){System.out.println(channel.preview(" Lesson ready "));}System.out.println(channels[0].describe("Practice now"));PreviewService service =newPreviewService(channels[1]);System.out.println(service.render("Review complete"));try{
channels[2].preview(" ");}catch(IllegalArgumentException error){System.out.println("Rejected message: "+ error.getMessage());}try{newEmailPreview(" ");}catch(IllegalArgumentException error){System.out.println("Rejected destination: "+ error.getMessage());}}}interfaceNotificationChannel{Stringpreview(String message);defaultStringdescribe(String message){return"Preview: "+preview(message);}}finalclassTextRules{privateTextRules(){}staticStringrequireText(String value,String label){if(value ==null|| value.isBlank()){thrownewIllegalArgumentException(label +" must not be blank.");}return value.strip();}}abstractclassNamedChannelimplementsNotificationChannel{privatefinalString destination;protectedNamedChannel(String destination){this.destination =TextRules.requireText(destination,"Destination");}publicfinalStringgetDestination(){return destination;}@OverridepublicabstractStringpreview(String message);}classEmailPreviewextendsNamedChannel{publicEmailPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"EMAIL to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classInboxPreviewextendsNamedChannel{publicInboxPreview(String destination){super(destination);}@OverridepublicStringpreview(String message){return"INBOX to "+getDestination()+": "+TextRules.requireText(message,"Message");}}classLogPreviewimplementsNotificationChannel{@OverridepublicStringpreview(String message){return"LOG: "+TextRules.requireText(message,"Message");}}classPreviewService{privatefinalNotificationChannel channel;publicPreviewService(NotificationChannel channel){if(channel ==null){thrownewIllegalArgumentException("Channel must not be null.");}this.channel = channel;}publicStringrender(String message){return channel.preview(message);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Both named implementations reuse abstract-base construction and destination storage, while LogPreview fulfills the same interface without that state. The loop and PreviewService use only the interface capability. The default description dispatches preview on the email object. All outputs are String previews; the caught errors demonstrate the same validation policy, not attempted delivery.
Test changes and boundaries
Use each implementation through NotificationChannel, then through PreviewService. Repeat calls, reorder implementations, and use an empty array. Results must follow the implementation without changes to the shared caller.
Test valid, padded, null, empty, and whitespace-only messages through preview, describe, and service.render. Every invalid message must yield Message must not be blank.; accepted messages are stripped without mutating caller data.
Reject null/blank destinations in both named implementations. Verify padded destinations are normalized and their getters stay unchanged after repeated previews. LogPreview must not require a fake destination.
Attempt to instantiate NamedChannel or NotificationChannel and omit a required public method in a concrete implementation. Observe the intended compiler errors; repair contracts rather than adding dummy behavior.
Test a class implementing two small interfaces and an unresolved pair of conflicting defaults. Confirm that multiple capabilities do not create multiple objects or automatically merge incompatible behavior. Keep the final lab preview-only, with no actual delivery integration.
Common mistakes and how to diagnose them
Trying to instantiate the contract
An interface or abstract class is a reference type you can program against, not a directly instantiable concrete implementation. Construct EmailPreview, InboxPreview, or LogPreview and store that reference under the shared type.
Forgetting public on the implementing method
The bodyless interface method is public even when the keyword is omitted there. An implementing class method needs public access; package access cannot satisfy the stronger promise.
Giving an abstract method an empty placeholder body
An abstract declaration has no method body. If a concrete implementation is required, implement real contract-respecting behavior rather than returning a dummy String just to remove an error.
Forcing every implementation into the stateful base
LogPreview does not need a destination. It can implement NotificationChannel directly, while the named previews reuse an abstract base. Shared capability does not imply identical storage or construction needs.
Assuming interface fields are instance state
Interface fields are constants rather than per-object storage. Store each destination in a class instance. A final reference to a mutable object would also not make that object deeply immutable.
Treating defaults or composition as automatic policy safety
Default conflicts may need explicit resolution, and injected collaborators still need to honor the contract. Tests must verify validation and no side effects; neither an interface keyword nor a final collaborator field proves those behaviors.
Frequently asked questions
When should I choose an interface instead of an abstract class?
Use an interface to describe a capability without requiring one storage or inheritance design. Use an abstract base when suitable shared state and construction genuinely help related implementations. This lesson uses both for different reasons.
Can an abstract class have a constructor and concrete methods?
Yes. Its constructor runs during concrete-subclass initialization, and concrete methods can provide shared behavior. The class still cannot be instantiated directly while declared abstract.
Can an interface contain method bodies?
Modern Java supports default, static, and private interface methods. Our default describe method reuses the public preview contract, while the abstract preview declaration still requires a concrete implementation.
Can one class extend an abstract class and implement interfaces?
Yes. A class has one direct superclass and may implement multiple interfaces. It can also satisfy an interface through an implementing superclass, as the named preview classes do here.
Does the email example actually send anything?
No. Every channel returns a String preview. There is no SMTP connection, inbox storage, log writing, user authorization, or delivery guarantee in this teaching program.
A contract for callers, an implementation for each object
The interface tells callers what they can ask for. Concrete classes fulfill that promise. An abstract base can share validated state without inventing a dummy implementation, and composition lets a service use any compatible implementation. Public overrides, input validation, and side-effect rules are all part of the contract.
Before continuing, explain why LogPreview does not extend NamedChannel, why describe reaches different preview methods, and why final on a collaborator field does not guarantee deep immutability. Next: nested and anonymous classes.
KNOWLEDGE CHECK
Can you choose and implement the right contract?
Answer six questions and read the explanations. Quiz answers are temporary and do not save course progress.
References and related learning
These explanations, preview programs, and exercises are original SovranCode teaching material. The official Java 21 specification documents the underlying language rules.