Interfaces in Java — exercises with shapes, a library and sensors (and the cast that worked until a T-shirt showed up)
I had a catalog of books, and I happily wrote Book b = (Book) v; inside a loop over an array of Sellable. It worked on Monday, Tuesday and Wednesday. On Thursday I added a T-shirt to the catalog and the program crashed with a ClassCastException. The cast hadn’t changed. What had changed was the content of the array. The compiler let me write the cast because it could be right. The program failed because, this time, it wasn’t.
I learned the hard way that, with interfaces, the type of the variable doesn’t tell you which object is inside. Today we’ll practice exactly that: three programs of growing difficulty, three snippets to debug and a cheat sheet to take home.
These exercises build on the theory of interfaces and review what we saw in the practice of interfaces. You can do them without those two, but it will be harder.
Table of Contents
How to use this article
- Read the task of a level and don’t look at the solution yet.
- Write your own version, compile it and compare your output with the expected one.
- If you get stuck, look only at the 🔵 hint and try again.
- When you finish, compare with the solution and look at why it’s written that way, not just whether it prints the same thing.
No exercise uses Scanner. The data is hard-coded so your output is identical to the expected one and you can check it at a glance.
Level 1 — Basic: shapes with Measurable
Task. Create an interface Measurable with:
double area()String name()- a
default String describe()method that returns the name and the area with two decimals, for exampleCircle with area 12.57.
Create three classes that implement it: Rectangle (width and height), Circle (radius) and Square (side). Their name() methods return "Rectangle", "Circle" and "Square". In a Shapes class with a main, store in an array of type Measurable a 3 × 4 rectangle, a circle of radius 2 and a square of side 3.5. Write two static methods:
totalArea(Measurable[]): adds up the areas.largest(Measurable[]): returns the shape with the biggest area.
The main prints the description of each shape, the total area (as Total area: 36.82) and the largest shape (as Largest: followed by its description).
Expected output:
Rectangle with area 12.00 Circle with area 12.57 Square with area 12.25 Total area: 36.82 Largest: Circle with area 12.57
describe() doesn’t need to know which shape it is: name() and area() are enough, and both are already part of the contract. For largest, start from the first shape and compare it with the rest.
Level 2 — Intermediate: a library
Task. A library lends books, rents out rooms and lends laptops that can also be booked by day. Model it with interfaces:
Resource: has aString name().LendableextendsResource:boolean lend(String member),void giveBack(),boolean available()and adefault String status()that returnsname: availableorname: on loan.BookableextendsResource:boolean book(String member, String day)(days from Monday to Friday; it returnsfalseif the day is already taken or doesn’t exist),String bookedBy(String day)(nullif the day is free) and adefault boolean isFree(String day).BookisLendable,RoomisBookableandLaptopis both.
In Library.main, create a Resource[] array with the book “Java for FP2”, “Room 3” and “Laptop 07”, and run this script:
- Ana borrows everything that can be lent.
- Luis tries to borrow the book.
- Luis books Room 3 on Monday.
- Marta books Room 3 on Monday.
- Marta books Laptop 07 on Tuesday.
- Print the status of every resource (the loan status of whatever is lendable, and the Monday and Tuesday bookings of whatever is bookable).
Use these messages: Ana borrows Java for FP2: OK (or unavailable), Luis books Room 3 on Monday: OK (or taken), then a --- Status --- line, and for bookable resources Room 3: Monday: booked by Luis, Tuesday: free.
Expected output:
Ana borrows Java for FP2: OK Ana borrows Laptop 07: OK Luis borrows Java for FP2: unavailable Luis books Room 3 on Monday: OK Marta books Room 3 on Monday: taken Marta books Laptop 07 on Tuesday: OK --- Status --- Java for FP2: on loan Room 3: Monday: booked by Luis, Tuesday: free Laptop 07: on loan Laptop 07: Monday: free, Tuesday: booked by Marta
The array is of type Resource, so before lending or booking you have to ask first with instanceof Lendable / instanceof Bookable. The laptop answers yes to both questions, which is why it shows up twice in the status.
Level 3 — Challenge: a sensor station
Task. A weather station has sensors of several kinds. Define:
Sensor:String id(),String unit(),double read(), a static methodformat(double value, String unit)that returns the value with one decimal and the unit (27.5 °C), and adefault String summary()that returnsid (unit).CalibratableextendsSensor:void calibrate(double offset)and adefault summary()that adds, calibratableto the one fromSensor.AlarmableextendsSensor:double threshold(), adefault boolean inAlarm()(read() > threshold()) and adefault summary()that adds, alarm > thresholdto the one fromSensor(for example, alarm > 30.0 °C).Thermometerimplements **CalibratableandAlarmable**, so it gets two conflictingsummary()methods. Resolve it so that it showsT1 (°C), alarm > 30.0 °C, calibratable.Humidityis only aSensor(unit%).Pressureis onlyAlarmable(unithPa).
In Station, create the array with: thermometers T1 (27.5 °C, threshold 30) and T2 (32.1 °C, threshold 30), a humidity sensor H1 (61.0 %) and a pressure sensor P1 (1013.2 hPa, threshold 1020). Write a method report(Sensor[], String title) that prints a === title === line, then each summary followed by -> and its reading, adds ⚠ ALARM when it applies, and ends with Alarms: n. Print the report with the title “Initial report”, calibrate every calibratable sensor by +3.0 and print it again with the title “After calibrating”.
Expected output:
=== Initial report === T1 (°C), alarm > 30.0 °C, calibratable -> 27.5 °C T2 (°C), alarm > 30.0 °C, calibratable -> 32.1 °C ⚠ ALARM H1 (%) -> 61.0 % P1 (hPa), alarm > 1020.0 hPa -> 1013.2 hPa Alarms: 1 === After calibrating === T1 (°C), alarm > 30.0 °C, calibratable -> 30.5 °C ⚠ ALARM T2 (°C), alarm > 30.0 °C, calibratable -> 35.1 °C ⚠ ALARM H1 (%) -> 61.0 % P1 (hPa), alarm > 1020.0 hPa -> 1013.2 hPa Alarms: 2
In Thermometer, summary() has to call one of the two versions with Interface.super.summary() and add whatever is missing. Notice that the summary printed for a thermometer has both suffixes (alarm and calibratable) but the prefix T1 (°C) only once.
What’s wrong here?
Three snippets, one bug each. Before reading the explanation, do two things: predict whether it compiles and, if it does, what it prints (or what happens when you run it).
Snippet 1 — The cast that worked until a T-shirt showed up
interface Sellable {
String name();
}
class Book implements Sellable {
public String name() { return "Book"; }
public String author() { return "Sergio"; }
}
class TShirt implements Sellable {
public String name() { return "T-shirt"; }
}
public class F1 {
public static void main(String[] args) {
Sellable[] catalog = { new Book(), new TShirt() };
for (Sellable v : catalog) {
Book l = (Book) v;
System.out.println(l.name() + " by " + l.author());
}
}
}
Think about it before moving on: does it compile? What does it print?
Explanation. It compiles just fine: the compiler accepts a cast from Sellable to Book because some Sellable could be a Book. But on the second turn of the loop, v is a TShirt:
Book by Sergio Exception in thread "main" java.lang.ClassCastException: class TShirt cannot be cast to class Book (TShirt and Book are in unnamed module of loader 'app') at F1.main(F1.java:19)
“ClassCastException … cannot be cast to class …”. A downcast is a promise to the compiler that gets checked at run time. If the object isn’t of that class, the program crashes. The first line (“Book by Sergio”) did get printed, because the first object really was a Book.
Fix. Ask before you convert:
for (Sellable v : catalog) {
if (v instanceof Book l) {
System.out.println(l.name() + " by " + l.author());
} else {
System.out.println(v.name() + " (no author)");
}
}
Book by Sergio T-shirt (no author)
With instanceof Book l, the check and the conversion happen in a single step, and l only exists inside the if. There’s no way to forget the check.
Snippet 2 — Two greetings, no winner
interface Friendly {
default String greeting() { return "Hi"; }
}
interface Formal {
default String greeting() { return "Good morning"; }
}
class Receptionist implements Friendly, Formal {
}
public class F2 {
public static void main(String[] args) {
System.out.println(new Receptionist().greeting());
}
}
Does it compile? What does it print?
Explanation. It doesn’t compile. Receptionist inherits two default greeting() methods and Java doesn’t know which one to use:
F2.java:9: error: types Friendly and Formal are incompatible;
class Receptionist implements Friendly, Formal {
^
class Receptionist inherits unrelated defaults for greeting() from types Friendly and Formal
1 error
“inherits unrelated defaults for greeting() from types …”. Two interfaces, the same default, and no decision: the diamond. Java would rather force you to decide than pick wrong for you.
Fix. The class overrides the method and decides what to do, calling each version with Interface.super:
class Receptionist implements Friendly, Formal {
@Override
public String greeting() { // we decide
return Formal.super.greeting() + ", " + Friendly.super.greeting().toLowerCase();
}
}
Good morning, hi
Snippet 3 — The constant that wouldn’t budge
interface Config {
int RETRIES = 3;
}
class Client implements Config {
void connect() {
while (RETRIES > 0) {
System.out.println("Trying… attempts left: " + RETRIES);
RETRIES--;
}
}
}
public class F3 {
public static void main(String[] args) {
new Client().connect();
}
}
Does it compile? What does it print?
Explanation. It doesn’t compile. RETRIES is declared in an interface, so it’s public static final even if you don’t write it. And a constant can’t be modified:
F3.java:9: error: cannot assign a value to static final variable RETRIES
RETRIES--;
^
1 error
“cannot assign a value to static final variable”. The fields of an interface are constants, no matter how many classes inherit them. If you need a counter that changes, it isn’t a constant.
Fix. Copy the value into a local variable and modify the copy:
class Client implements Config {
void connect() {
int remaining = RETRIES; // local copy: this one can change
while (remaining > 0) {
System.out.println("Trying… attempts left: " + remaining);
remaining--;
}
}
}
Trying… attempts left: 3 Trying… attempts left: 2 Trying… attempts left: 1
Solutions
Look at this section only after you’ve tried the exercise. If your code is different but gives the same output and honors the contracts, it’s a valid solution too.
Level 1 — Solution
Measurable.java
public interface Measurable {
double area();
String name();
default String describe() { // relies on name() and area(): works for every shape
return name() + " with area " + String.format("%.2f", area());
}
}
Rectangle.java
public class Rectangle implements Measurable {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override public double area() { return width * height; }
@Override public String name() { return "Rectangle"; }
}
Circle.java
public class Circle implements Measurable {
private final double radius;
public Circle(double radius) { this.radius = radius; }
@Override public double area() { return Math.PI * radius * radius; }
@Override public String name() { return "Circle"; }
}
Square.java
public class Square implements Measurable {
private final double side;
public Square(double side) { this.side = side; }
@Override public double area() { return side * side; }
@Override public String name() { return "Square"; }
}
Shapes.java
public class Shapes {
static double totalArea(Measurable[] shapes) {
double total = 0;
for (Measurable m : shapes) {
total += m.area();
}
return total;
}
static Measurable largest(Measurable[] shapes) {
Measurable largest = shapes[0]; // start from the first one and keep comparing
for (Measurable m : shapes) {
if (m.area() > largest.area()) {
largest = m;
}
}
return largest;
}
public static void main(String[] args) {
Measurable[] shapes = { new Rectangle(3, 4), new Circle(2), new Square(3.5) };
for (Measurable m : shapes) {
System.out.println(m.describe());
}
System.out.printf("Total area: %.2f%n", totalArea(shapes));
System.out.println("Largest: " + largest(shapes).describe());
}
}
Why it’s written this way:
describe()isdefaultbecause it doesn’t depend on any particular shape: it usesname()andarea(), which belong to the contract. All three shapes get it for free.- The
totalAreaandlargestmethods take aMeasurable[]. If tomorrow you add aTriangle, you don’t have to touch them. largeststarts fromshapes[0]. It’s safer than starting from0or from some made-up value, because the starting shape is one of the real ones.- The arithmetic checks out: 12.00 + 12.57 + 12.25 = 36.82 (the circle is π · 2² = 12.566…, which rounds to 12.57).
Level 2 — Solution
Resource.java, Lendable.java and Bookable.java
public interface Resource {
String name();
}
public interface Lendable extends Resource { // inherits name() from Resource
boolean lend(String member);
void giveBack();
boolean available();
default String status() {
return name() + ": " + (available() ? "available" : "on loan");
}
}
public interface Bookable extends Resource {
boolean book(String member, String day);
String bookedBy(String day); // null if the day is free
default boolean isFree(String day) { // relies on bookedBy
return bookedBy(day) == null;
}
}
Book.java
public class Book implements Lendable {
private final String title;
private String lentTo;
public Book(String title) { this.title = title; }
@Override public String name() { return title; }
@Override public boolean available() { return lentTo == null; }
@Override
public boolean lend(String member) {
if (!available()) return false;
lentTo = member;
return true;
}
@Override public void giveBack() { lentTo = null; }
}
Room.java
public class Room implements Bookable {
private static final String[] DAYS = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" };
private final String name;
private final String[] bookings = new String[DAYS.length];
public Room(String name) { this.name = name; }
@Override public String name() { return name; }
private int indexOf(String day) {
for (int i = 0; i < DAYS.length; i++) {
if (DAYS[i].equals(day)) return i;
}
return -1;
}
@Override
public boolean book(String member, String day) {
int i = indexOf(day);
if (i == -1 || bookings[i] != null) return false;
bookings[i] = member;
return true;
}
@Override
public String bookedBy(String day) {
int i = indexOf(day);
return i == -1 ? null : bookings[i];
}
}
Laptop.java
public class Laptop implements Lendable, Bookable { // two capabilities in one class
private static final String[] DAYS = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" };
private final String name;
private String lentTo;
private final String[] bookings = new String[DAYS.length];
public Laptop(String name) { this.name = name; }
@Override public String name() { return name; }
@Override public boolean available() { return lentTo == null; }
@Override
public boolean lend(String member) {
if (!available()) return false;
lentTo = member;
return true;
}
@Override public void giveBack() { lentTo = null; }
private int indexOf(String day) {
for (int i = 0; i < DAYS.length; i++) {
if (DAYS[i].equals(day)) return i;
}
return -1;
}
@Override
public boolean book(String member, String day) {
int i = indexOf(day);
if (i == -1 || bookings[i] != null) return false;
bookings[i] = member;
return true;
}
@Override
public String bookedBy(String day) {
int i = indexOf(day);
return i == -1 ? null : bookings[i];
}
}
Library.java
public class Library {
public static void main(String[] args) {
Resource[] resources = {
new Book("Java for FP2"),
new Room("Room 3"),
new Laptop("Laptop 07")
};
// Ana borrows everything that can be lent
for (Resource r : resources) {
if (r instanceof Lendable p) { // only if it is lendable
System.out.println("Ana borrows " + r.name() + ": " + (p.lend("Ana") ? "OK" : "unavailable"));
}
}
// Luis tries the book
Lendable book = (Lendable) resources[0];
System.out.println("Luis borrows " + book.name() + ": " + (book.lend("Luis") ? "OK" : "unavailable"));
// Bookings
Bookable room = (Bookable) resources[1];
System.out.println("Luis books " + room.name() + " on Monday: " + (room.book("Luis", "Monday") ? "OK" : "taken"));
System.out.println("Marta books " + room.name() + " on Monday: " + (room.book("Marta", "Monday") ? "OK" : "taken"));
Bookable laptop = (Bookable) resources[2];
System.out.println("Marta books " + laptop.name() + " on Tuesday: " + (laptop.book("Marta", "Tuesday") ? "OK" : "taken"));
// Status
System.out.println("--- Status ---");
for (Resource r : resources) {
if (r instanceof Lendable p) { // only if it is lendable
System.out.println(p.status());
}
if (r instanceof Bookable b) { // only if it is bookable
System.out.println(r.name() + ": Monday: " + (b.isFree("Monday") ? "free" : "booked by " + b.bookedBy("Monday"))
+ ", Tuesday: " + (b.isFree("Tuesday") ? "free" : "booked by " + b.bookedBy("Tuesday")));
}
}
}
}
Why it’s written this way:
LendableandBookableextendResource. That way the array can be of typeResource(everything has a name), and any class that implements one of the two is forced to havename().Laptopimplements both interfaces: that’s why it answers yes to bothinstanceofchecks and shows up twice in the status. There’s nodefaultconflict:status()belongs only toLendableandisFree()belongs only toBookable.bookedByreturnsnullfor a free day andisFree()builds on it. It’s a nice example of adefaultthat uses the contract to build something new.RoomandLaptoprepeat the booking code. That’s on purpose, to keep the focus on interfaces. If you wanted to avoid it, you could move that part into a helper class and reuse it.
Level 3 — Solution
Sensor.java, Calibratable.java and Alarmable.java
public interface Sensor {
String id();
String unit();
double read();
static String format(double value, String unit) { // called as Sensor.format(...)
return String.format("%.1f %s", value, unit);
}
default String summary() {
return id() + " (" + unit() + ")";
}
}
public interface Calibratable extends Sensor {
void calibrate(double offset);
@Override
default String summary() {
return Sensor.super.summary() + ", calibratable";
}
}
public interface Alarmable extends Sensor {
double threshold();
default boolean inAlarm() {
return read() > threshold();
}
@Override
default String summary() {
return Sensor.super.summary() + ", alarm > " + Sensor.format(threshold(), unit());
}
}
Thermometer.java
public class Thermometer implements Calibratable, Alarmable {
private final String id;
private double reading;
private final double threshold;
public Thermometer(String id, double reading, double threshold) {
this.id = id;
this.reading = reading;
this.threshold = threshold;
}
@Override public String id() { return id; }
@Override public String unit() { return "°C"; }
@Override public double read() { return reading; }
@Override public double threshold() { return threshold; }
@Override public void calibrate(double offset) { reading += offset; }
// Two interfaces bring their own summary(): we must decide which one wins
@Override
public String summary() {
return Alarmable.super.summary() + ", calibratable";
}
}
Humidity.java and Pressure.java
public class Humidity implements Sensor {
private final String id;
private final double reading;
public Humidity(String id, double reading) {
this.id = id;
this.reading = reading;
}
@Override public String id() { return id; }
@Override public String unit() { return "%"; }
@Override public double read() { return reading; }
}
public class Pressure implements Alarmable {
private final String id;
private final double reading;
private final double threshold;
public Pressure(String id, double reading, double threshold) {
this.id = id;
this.reading = reading;
this.threshold = threshold;
}
@Override public String id() { return id; }
@Override public String unit() { return "hPa"; }
@Override public double read() { return reading; }
@Override public double threshold() { return threshold; }
}
Station.java
public class Station {
static void report(Sensor[] sensors, String title) {
System.out.println("=== " + title + " ===");
int alarms = 0;
for (Sensor s : sensors) {
String line = s.summary() + " -> " + Sensor.format(s.read(), s.unit());
if (s instanceof Alarmable a && a.inAlarm()) { // does it have the capability? is it active?
line += " ⚠ ALARM";
alarms++;
}
System.out.println(line);
}
System.out.println("Alarms: " + alarms);
}
public static void main(String[] args) {
Sensor[] sensors = {
new Thermometer("T1", 27.5, 30),
new Thermometer("T2", 32.1, 30),
new Humidity("H1", 61.0),
new Pressure("P1", 1013.2, 1020)
};
report(sensors, "Initial report");
for (Sensor s : sensors) {
if (s instanceof Calibratable c) { // only the calibratable ones
c.calibrate(3.0);
}
}
report(sensors, "After calibrating");
}
}
Why it’s written this way:
Sensor.super.summary()insideCalibratableandAlarmablelets each capability add its own part without repeating what they have in common.Thermometerinherits twosummary()methods and has to decide. Here we callAlarmable.super.summary()(which already includesT1 (°C)and the alarm) and add, calibratableby hand. We can’t call both versions, because theT1 (°C)prefix would come out twice.Sensor.format(...)is astaticmethod of the interface: you call it with the interface name, and bothAlarmableandStationuse it.- In
report,s instanceof Alarmable a && a.inAlarm()asks about the capability and its state in a single condition. The humidity sensor (which isn’tAlarmable) never reaches the second half. - After calibrating, T1 goes from 27.5 to 30.5 and passes its threshold: the second report has two alarms.
Watch it step by step in Python Tutor
Python Tutor runs the code line by line and draws variables, objects and references. Choose Java. Here is snippet 1 already fixed, using the classic instanceof plus cast so every step is easy to see:
public class Tutor {
interface Sellable { String name(); }
static class Book implements Sellable {
public String name() { return "Book"; }
public String author() { return "Sergio"; }
}
static class TShirt implements Sellable {
public String name() { return "T-shirt"; }
}
public static void main(String[] args) {
Sellable[] catalog = { new Book(), new TShirt() };
for (int i = 0; i < catalog.length; i++) {
Sellable v = catalog[i];
if (v instanceof Book) {
Book l = (Book) v;
System.out.println(l.name() + " by " + l.author());
} else {
System.out.println(v.name() + " (no author)");
}
}
}
}
Expected output:
Book by Sergio T-shirt (no author)
What to watch, step by step:
- In the
catalogarray there are two different objects: aBookand aTShirt. Both are stored asSellable, but each one keeps its own class. - On the first turn,
v instanceof Bookis true. ThenBook l = (Book) vcreates a variablelthat points to the same object asv: nothing gets copied, there’s just one more arrow. - On the second turn,
vpoints to theTShirt. The condition is false and the block is skipped: the cast doesn’t even run. - Now change the program: delete the
ifand leave onlyBook l = (Book) v;. When it reaches the second turn it should stop with aClassCastException: that’s the bug from snippet 1, live.
Cheat sheet: interfaces in Java
The essentials in 30 seconds
- An interface is a contract: it says what can be done, not how.
- You fulfil it with
implements, and you must override everything abstract, withpublic. - A class has one parent (
extends) and many interfaces (implements A, B). - A variable of interface type can hold any object that fulfils it, but it only sees what the contract declares.
- To use something specific to a class:
instanceoffirst, conversion after. defaultgives an optional body;staticbelongs to the interface; fields are constants.
Syntax
| I want to… | Code | Result |
|---|---|---|
| Declare a contract | interface Measurable { double area(); } | public abstract method |
| Fulfil it | class Circle implements Measurable { public double area() {…} } | public is mandatory |
| Several interfaces | class Laptop implements Lendable, Bookable | Fulfils both contracts |
| Extend an interface | interface Lendable extends Resource | Inherits its methods |
| Variable of interface type | Measurable m = new Circle(2); | No cast, upwards |
| Array of interface type | Measurable[] shapes = {…}; | Mixes different classes |
| Ask about the capability | if (r instanceof Lendable p) { p.lend(…); } | Checks and converts |
| Classic cast | Book b = (Book) v; | ClassCastException if it isn’t one |
| Method with a body | default String describe() {…} | Optional to override |
| Choose in a conflict | Alarmable.super.summary() | Calls that version |
| Utility of the interface | static String format(…) {…} | Sensor.format(…) |
| Constant | int MAX = 3; | public static final |
Templates from the exercises
**A default that builds on the contract** (Level 1):
default String describe() {
return name() + " with area " + String.format("%.2f", area());
}
Ask about the capability before using it (Level 2):
for (Resource r : resources) {
if (r instanceof Lendable p) {
p.lend("Ana");
}
}
**Resolve a default conflict** (Level 3):
@Override
public String summary() {
return Alarmable.super.summary() + ", calibratable";
}
The most common mistakes
| Symptom | Likely cause |
|---|---|
| “attempting to assign weaker access privileges; was public” | Missing public when overriding an interface method |
| “is not abstract and does not override abstract method…” | A method of the contract isn’t implemented |
| “Lendable is abstract; cannot be instantiated” | You wrote new on an interface |
| “cannot find symbol” with an interface-type variable | You use a method of the class without instanceof + conversion |
| “incompatible types: X cannot be converted to Y” | The downcast is missing |
ClassCastException | A cast without checking the real type with instanceof |
| “inherits unrelated defaults for…” | Two identical default methods: override and use Interface.super |
| “cannot assign a value to static final variable” | You’re trying to change a constant of the interface |
Before you hand it in, ask yourself
- Did I write **
public** on every method that implements the interface? - Do I ask with **
instanceof** before any downcast? - Does my code depend on the interface (
Measurable,Lendable) and not on concrete classes? - If two interfaces bring the same
default, did I decide which one wins?
If you want to review the topic from the ground up, go back to the theory of interfaces.
Keep goingAll Java (en) articlesDid this article help you? Here is how you can support SergioLearns →
