Saltar al contenido
ES EN

Solved exercise: polymorphism and dynamic binding in Java

Solved Java exam exercise: a shape collection built on an abstract class, overridden methods and dynamic binding, with hints, tested code and common mistakes.

Updated on

Exam Code Java Object-oriented programming Exam level Tested code

Exam Code: Solved exercise: polymorphism and dynamic binding in Java
Object-Oriented Programming (CS2) · Midterm examQuestion 2 · 2.5 points · 30 min

You must manage a collection of flat geometric shapes, and you must model it with polymorphism. An abstract class Shape declares the methods area(), name() and dimensions(). It also defines toString() exactly once, built on those three methods. The concrete subclasses are Circle (radius), Rectangle (base and height), Triangle (base and height) and Square (side). Square must extend Rectangle.

A class ShapeCollection stores the shapes in insertion order, with positions numbered from 0. It provides the methods add(Shape s), remove(int pos), print(...), totalArea(), size() and largest(). The method remove returns false if the position does not exist. None of these methods may use instanceof or ask for the concrete type of a shape in any other way.

The Main program reads commands from standard input, one per line, until end of input, and ignores blank lines. These are the commands:C r adds a circle.R b h adds a rectangle.S s adds a square.T b h adds a triangle.D i deletes the shape at position i.L lists the collection.A prints the summary.M prints the shape with the largest area.Dimensions are positive real numbers written with a decimal point. Positions are integers and may be negative.

Output format. Every real number is printed with two decimals and a decimal point. A shape is described as Name(dimensions) area=A. The dimensions are written as follows:r=... for the circle;s=... for the square;b=... h=... for the rectangle and the triangle.The names are Circle, Rectangle, Square and Triangle. Command L prints one line i: description per shape, or the single line (empty) if there are none.

Command A prints Total: n shapes, area X. Command M prints Largest: description. If two shapes tie, the one at the lower position wins. If the collection is empty, M prints Largest: none. A valid deletion prints nothing, and an invalid one prints ERROR position i with the value read. Add commands produce no output, and an empty input produces no output at all.

Sample input

C 1
R 2 3
S 1.5
T 4 2.5
L
D 1
D 7
L
A
M

Expected output

0: Circle(r=1.00) area=3.14
1: Rectangle(b=2.00 h=3.00) area=6.00
2: Square(s=1.50) area=2.25
3: Triangle(b=4.00 h=2.50) area=5.00
ERROR position 7
0: Circle(r=1.00) area=3.14
1: Square(s=1.50) area=2.25
2: Triangle(b=4.00 h=2.50) area=5.00
Total: 3 shapes, area 10.39
Largest: Triangle(b=4.00 h=2.50) area=5.00
Correct hierarchy: abstract class Shape with abstract methods, and subclasses Circle, Rectangle, Square (a child of Rectangle) and Triangle
0.75
Correct use of overriding: toString defined once in Shape on top of overridden methods; Square redefines only what changes
0.75
ShapeCollection: add, remove by position with bounds checking, list, total area and largest shape, all without querying the concrete type
0.5
Reads commands until end of input and prints the exact output format (two decimals with a dot, error messages)
0.5

Hints

Hint 1 · Where does the description format belong?

Every description has the same shape: a name, the dimensions in parentheses, then the area. Write that common part in one place only: the toString() of Shape. The parts that differ (name, dimensions, area formula) are the abstract methods each subclass overrides. When toString() calls area(), Java runs the version that belongs to the object's actual class.

Hint 2 · What does Square really need to redefine?

Suppose Square extends Rectangle and its constructor calls super(side, side). Then the inherited area is already correct. Only the name and the way the dimensions are shown change, so override just name() and dimensions(). For the subclass to read the side, the field base in Rectangle must be protected.

Hint 3 · How do you validate a delete and find the largest?

A position is valid only if 0 <= pos < size. Check this before calling remove on the list, because a negative position is invalid too. To find the largest shape, walk the list while keeping the best shape seen so far. Replace it only when the new area is strictly greater, so the first shape survives a tie.

Solution

Explained solution

The central idea is that ShapeCollection only ever handles references of type Shape. It never needs to know whether it holds a circle or a triangle. The compiler checks that area() exists in Shape. At run time, the virtual machine picks which version to execute from the object's actual class. That is dynamic binding, and it is why totalArea() and largest() are loops of a few lines.

All classes live in a single file, Main.java. Only Main is public; the others have package-private visibility. That is enough in an exam and avoids juggling several files.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

abstract class Shape {
    public abstract double area();

    public abstract String name();

    protected abstract String dimensions();

    @Override
    public String toString() {
        // Common template: all three calls are resolved by dynamic binding
        return String.format(Locale.ROOT, "%s(%s) area=%.2f", name(), dimensions(), area());
    }
}

class Circle extends Shape {
    private final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public String name() {
        return "Circle";
    }

    @Override
    protected String dimensions() {
        return String.format(Locale.ROOT, "r=%.2f", radius);
    }
}

class Rectangle extends Shape {
    protected final double base;
    protected final double height;

    Rectangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    public double area() {
        return base * height;
    }

    @Override
    public String name() {
        return "Rectangle";
    }

    @Override
    protected String dimensions() {
        return String.format(Locale.ROOT, "b=%.2f h=%.2f", base, height);
    }
}

class Square extends Rectangle {
    Square(double side) {
        super(side, side);
    }

    // area() is inherited from Rectangle: no need to redefine it
    @Override
    public String name() {
        return "Square";
    }

    @Override
    protected String dimensions() {
        return String.format(Locale.ROOT, "s=%.2f", base);
    }
}

class Triangle extends Shape {
    private final double base;
    private final double height;

    Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    public double area() {
        return base * height / 2.0;
    }

    @Override
    public String name() {
        return "Triangle";
    }

    @Override
    protected String dimensions() {
        return String.format(Locale.ROOT, "b=%.2f h=%.2f", base, height);
    }
}

class ShapeCollection {
    private final List<Shape> elements = new ArrayList<>();

    void add(Shape s) {
        elements.add(s);
    }

    boolean remove(int pos) {
        if (pos < 0 || pos >= elements.size()) {
            return false;
        }
        elements.remove(pos);
        return true;
    }

    void print(PrintWriter out) {
        if (elements.isEmpty()) {
            out.println("(empty)");
            return;
        }
        for (int i = 0; i < elements.size(); i++) {
            out.println(i + ": " + elements.get(i));
        }
    }

    double totalArea() {
        double total = 0.0;
        for (Shape s : elements) {
            total += s.area();
        }
        return total;
    }

    int size() {
        return elements.size();
    }

    Shape largest() {
        Shape best = null;
        for (Shape s : elements) {
            if (best == null || s.area() > best.area()) {
                best = s;
            }
        }
        return best;
    }
}

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter out = new PrintWriter(System.out);
        ShapeCollection shapes = new ShapeCollection();
        String line;
        while ((line = in.readLine()) != null) {
            line = line.trim();
            if (line.isEmpty()) {
                continue;
            }
            String[] p = line.split("\\s+");
            switch (p[0]) {
                case "C" -> shapes.add(new Circle(Double.parseDouble(p[1])));
                case "R" -> shapes.add(new Rectangle(Double.parseDouble(p[1]), Double.parseDouble(p[2])));
                case "S" -> shapes.add(new Square(Double.parseDouble(p[1])));
                case "T" -> shapes.add(new Triangle(Double.parseDouble(p[1]), Double.parseDouble(p[2])));
                case "D" -> {
                    int pos = Integer.parseInt(p[1]);
                    if (!shapes.remove(pos)) {
                        out.println("ERROR position " + pos);
                    }
                }
                case "L" -> shapes.print(out);
                case "A" -> out.println(String.format(Locale.ROOT, "Total: %d shapes, area %.2f",
                        shapes.size(), shapes.totalArea()));
                case "M" -> {
                    Shape m = shapes.largest();
                    out.println("Largest: " + (m == null ? "none" : m));
                }
                default -> {
                    // unknown command: ignored
                }
            }
        }
        out.flush();
    }
}

The abstract class. Shape cannot be instantiated, and it forces every subclass to implement area(), name() and dimensions(). Its toString() is a small template method: it fixes the format Name(dimensions) area=A and delegates the variable parts to the subclasses. Every String.format call uses Locale.ROOT, so the decimal separator is always a dot. On a machine set to a German or French locale, %.2f would print a comma and the output would stop matching.

The subclasses. Circle, Rectangle and Triangle override all three methods with the @Override annotation. The annotation makes the compiler complain if a signature does not match. Square is the interesting one. Its constructor calls super(side, side), and it does not redefine area(), because Rectangle already computes base * height. It overrides only name() and dimensions(). The latter reads base, which is why that field is protected. To revise constructor chaining, see the solved exercise on inheritance, constructors and super.

Dynamic binding at work. In the example, the third shape is a Square with side 1.5. When print evaluates i + ": " + elements.get(i), Java calls toString(), which is the one defined in Shape. Inside it, name() and dimensions() resolve to Square, while area() climbs up to Rectangle. That is how the line 2: Square(s=1.50) area=2.25 appears. The static type of the reference is Shape, but the object's class decides which code runs.

The collection. ShapeCollection wraps a List<Shape>. Its remove checks pos < 0 || pos >= elements.size() before calling elements.remove(pos); without that guard an IndexOutOfBoundsException would be thrown. The test "One element and negative positions" exercises D -1. The example exercises D 7 when only three shapes remain. Because pos is an int, Java chooses remove(int index) rather than remove(Object).

Total area and largest. totalArea() adds up s.area() without knowing which kind of shape each s is. In the example, after the deletion it computes π + 2.25 + 5 = 10.3916, printed as 10.39. largest() replaces the current best only on a strictly greater area. In the test "Tie for largest", three shapes of area 12 remain once the circle is deleted. The first of them, the 3 by 4 rectangle, is chosen. On an empty collection the method returns null, and main prints Largest: none.

The command loop. main calls readLine() until it returns null, skips blank lines and dispatches each command with an arrow-style switch. Output goes through a PrintWriter with a single flush() at the end. With empty input the loop never runs and nothing is printed, as the test "Empty input" requires. In the test "Empty collection", each of the four commands prints its empty-case message on one line.

Code and tests on GitLab

Test cases

CaseInputExpected outputActual outputResult
Statement exampleC 1 R 2 3 S 1.5 T 4 2.5 L D 1 D 7 L A M0: Circle(r=1.00) area=3.14 1: Rectangle(b=2.00 h=3.00) area=6.00 2: Square(s=1.50) area=2.25 3: Triangle(b=4.00 h=2.50) area=5.00 ERROR position 7 0: Circle(r=1.00) area=3.14 1: Square(s=1.50) area=2.25 2: Triangle(b=4.00 h=2.50) area=5.00 Total: 3 shapes, area 10.39 Largest: Triangle(b=4.00 h=2.50) area=5.000: Circle(r=1.00) area=3.14 1: Rectangle(b=2.00 h=3.00) area=6.00 2: Square(s=1.50) area=2.25 3: Triangle(b=4.00 h=2.50) area=5.00 ERROR position 7 0: Circle(r=1.00) area=3.14 1: Square(s=1.50) area=2.25 2: Triangle(b=4.00 h=2.50) area=5.00 Total: 3 shapes, area 10.39 Largest: Triangle(b=4.00 h=2.50) area=5.00OK
Empty collectionL A M D 0(empty) Total: 0 shapes, area 0.00 Largest: none ERROR position 0(empty) Total: 0 shapes, area 0.00 Largest: none ERROR position 0OK
Empty input(empty)(empty)(empty)OK
One element and negative positionsS 2 M D -1 D 0 L ALargest: Square(s=2.00) area=4.00 ERROR position -1 (empty) Total: 0 shapes, area 0.00Largest: Square(s=2.00) area=4.00 ERROR position -1 (empty) Total: 0 shapes, area 0.00OK
Tie for largestR 3 4 T 6 4 C 2 R 2 6 M D 2 M ALargest: Circle(r=2.00) area=12.57 Largest: Rectangle(b=3.00 h=4.00) area=12.00 Total: 3 shapes, area 36.00Largest: Circle(r=2.00) area=12.57 Largest: Rectangle(b=3.00 h=4.00) area=12.00 Total: 3 shapes, area 36.00OK

Actual outputs: code compiled with Java 21.0.12.1 (Temurin) and run in an isolated container on 9 October 2026.

Complexity

Let n be the number of shapes at a given moment. Adding is amortised O(1) thanks to ArrayList. Deleting at position i costs O(n − i), because the later elements must shift left. Listing, summing areas and finding the largest are each O(n): a single pass in which every polymorphic call takes constant time. largest() recomputes best.area() at each comparison, which is still O(1) per iteration.

With k commands in the input, the worst case overall is O(k · n). Memory is O(n): one reference per shape in the list, plus the one or two double fields of each object. Dynamic binding adds no meaningful cost. The JVM resolves the call through the class's method table, and in practice the JIT compiler often inlines it.

Common mistakes

  • Writing the full format in every subclass's toString(), which duplicates code, instead of defining it once in Shape and overriding only what varies.
  • Implementing totalArea() or largest() with chains of instanceof and casts. It works, but it defeats polymorphism, and the statement forbids it explicitly.
  • Checking only pos >= size() and forgetting negative positions, so D -1 throws an exception instead of printing the error line.
  • Redefining area() in Square with its own side field, or declaring base as private in Rectangle so the subclass cannot read it.
  • Calling String.format without Locale.ROOT. On a machine with a comma-decimal locale it prints 3,14 and the output no longer matches.
  • Using >= when searching for the largest, which returns the last of the tied shapes instead of the first.

Variants

Sort the collection by area

Add a command O that sorts the shapes from smallest to largest area while keeping tied shapes in their relative order. One line is enough: elements.sort(Comparator.comparingDouble(Shape::area)). List.sort is stable, and the method reference Shape::area is also dispatched dynamically for each object. Alternatively, make Shape implement Comparable<Shape>. More practice of this kind is in the top 10 Java exam exercises.

Add the perimeter as a second abstract method

Declare public abstract double perimeter() in Shape and append per=%.2f to toString(). The compiler will then demand an implementation in Circle, Rectangle and Triangle. For the triangle you must fix its kind; for an isosceles triangle, each slanted side is Math.hypot(base / 2, height). Square inherits the method unchanged once again. This is a good way to test whether a student understands what an abstract method obliges subclasses to do.

Article generated with AI.larebelion

Byline

· Chief editor · English edition · London

“If you have to ask what each shape is before adding up areas, you wrote a disguised switch, not polymorphism.”

Comentarios

Publicar un comentario