Start learning free

LESSON 564 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Shapes Perimeter in Kotlin: Polymorphism with Open Classes

Practice O(n) Read the full explanation

A drawing app keeps rectangles, squares and triangles in one list. Write the shape types so that code holding that list can ask any shape for its name and its perimeter (the length all the way around it) without knowing which kind of shape it is. That is polymorphism: one call, shape.perimeter(), and each kind of shape runs its own version.

Every shape class inherits from the class Shape, the common type. Shape has two methods:

  • name() returns the kind of shape as text: "Shape" for a plain Shape.
  • perimeter() returns the length around the shape: 0 for a plain Shape.

Each shape class overrides both:

  • Rectangle(width, height): its name is "Rectangle", its perimeter 2 × (width + height).
  • Square(side): its name is "Square", its perimeter 4 × side.
  • Triangle(a, b, c): its name is "Triangle", its perimeter a + b + c.

In Kotlin Shape and its methods are open, so the shape classes can extend it (: Shape()) and override them. Each shape class gets its sizes as val properties of its constructor.

Then write two methods of the class Solution, as in the course's other lessons. Both get the shapes as Array<Shape>:

  • totalPerimeter(shapes) returns the sum of the perimeters of all the shapes.
  • largestShape(shapes) returns the name of the shape with the largest perimeter. If several shapes share the largest perimeter, the one that comes first wins.

Both functions must work through the common type's methods only: never check which kind a shape is. Then a new kind of shape, added later, works with them unchanged.

The starter code already has every class header, constructor included, and every method header: write the method bodies. For each test, the harness reads the shapes, builds them in the order given, calls totalPerimeter and largestShape, and prints the two results on one line, separated by a space.

 

Example 1:

Input:
    Rectangle(3, 4)
    Square(5)
    Triangle(3, 4, 5)
Output: 46 Square
Explanation: The perimeters are 2 × (3 + 4) = 14, 4 × 5 = 20 and 3 + 4 + 5 = 12. Their total is 46, and the square's 20 is the largest.

Example 2:

Input:
    Rectangle(2, 4)
    Square(3)
    Triangle(4, 4, 4)
Output: 36 Rectangle
Explanation: All three perimeters are 12: 2 × (2 + 4), 4 × 3 and 4 + 4 + 4. On a tie the shape that comes first in the list wins, so the answer is the rectangle.

Example 3:

Input:
    Square(1)
    Rectangle(1, 1)
    Triangle(2, 2, 3)
Output: 15 Triangle
Explanation: The perimeters are 4, 4 and 7, so the last shape, the triangle, is the largest. A 1 by 1 rectangle has the same sides as a square, but it is still a Rectangle: each object answers name() for its own class.

 

Constraints:

  • Every test has between 1 and 100 shapes: the list is never empty.
  • Every size (width, height, side, and each side of a triangle) is an integer from 1 to 104.
  • A triangle's three sides always make a real triangle: each side is shorter than the other two together.

Note:

Each function looks at every shape once: O(n) time for n shapes, and no extra space.

STEP 1 OF 16
    Ctrl + Enter to check

    KOTLIN · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Shapes Perimeter in Kotlin, explained

    Problem overview

    A drawing app keeps rectangles, squares and triangles in one list. You write the shape types so that code holding that list can ask any shape for its name and its perimeter, the length all the way around it, without knowing which kind of shape it is. That idea is called polymorphism: one call, shape.perimeter(), and each kind of shape runs its own version.

    There are two parts:

    • the classes: a parent Shape (name "Shape", perimeter 0) and three children that override both methods, Rectangle(width, height), Square(side) and Triangle(a, b, c);
    • two methods of Solution: totalPerimeter(shapes) returns the sum of all perimeters, and largestShape(shapes) returns the name of the shape with the largest perimeter, the earliest one on a tie.

    For a rectangle 3 by 4, a square of side 5 and a triangle with sides 3, 4 and 5, the perimeters are 14, 20 and 12. The total is 46 and the largest is the square.

    Approach

    Both methods take an Array<Shape>. Every element has the parent type, so the only calls the code can make are the ones Shape offers: name() and perimeter(). That is also exactly what the task demands. Never check which class a shape is; ask it. Then a new kind of shape, say a circle added next year, works with both methods unchanged, as long as it extends Shape and overrides the two methods.

    Each method needs one pass over the array. totalPerimeter adds as it goes. largestShape remembers the best shape so far and replaces it only when it meets a strictly larger perimeter, which keeps the earliest shape on a tie.

    Step by step

    1. Write Shape with its two open methods: name() returns "Shape" and perimeter() returns 0. A plain shape has no sides, and these defaults are what makes the common type have both methods.
    2. In Rectangle, name() returns "Rectangle" and perimeter() returns 2 * (width + height). The class header already keeps width and height as val properties.
    3. In Square, the name is "Square" and the perimeter is 4 * side.
    4. In Triangle, the name is "Triangle" and the perimeter is a + b + c.
    5. In totalPerimeter, start var total = 0, loop over the shapes and add shape.perimeter(). Return total after the loop.
    6. In largestShape, start with var largest = shapes[0]. The array is never empty, so the first element exists. Loop over all shapes and, when shape.perimeter() > largest.perimeter(), set largest = shape.
    7. Return largest.name(). The object that won knows its own name.

    The solution in Kotlin

    open class Shape {
        open fun name(): String {
            return "Shape"
        }
        open fun perimeter(): Int {
            return 0
        }
    }
    class Rectangle(val width: Int, val height: Int) : Shape() {
        override fun name(): String {
            return "Rectangle"
        }
        override fun perimeter(): Int {
            return 2 * (width + height)
        }
    }
    class Square(val side: Int) : Shape() {
        override fun name(): String {
            return "Square"
        }
        override fun perimeter(): Int {
            return 4 * side
        }
    }
    class Triangle(val a: Int, val b: Int, val c: Int) : Shape() {
        override fun name(): String {
            return "Triangle"
        }
        override fun perimeter(): Int {
            return a + b + c
        }
    }
    class Solution {
        fun totalPerimeter(shapes: Array<Shape>): Int {
            var total = 0
            for (shape in shapes) {
                total += shape.perimeter()
            }
            return total
        }
        fun largestShape(shapes: Array<Shape>): String {
            var largest = shapes[0]
            for (shape in shapes) {
                if (shape.perimeter() > largest.perimeter()) {
                    largest = shape
                }
            }
            return largest.name()
        }
    }
    

    Kotlin details worth knowing:

    • Writing val inside a class header, as in Square(val side: Int), declares the constructor parameter and a read-only property in one go.
    • Each child writes : Shape() to call the parent's constructor, and every method it replaces needs override. The parent's methods need open, because Kotlin classes and methods are final by default.
    • In for (shape in shapes), the variable has type Shape. The call shape.perimeter() runs the version of the object's real class.

    A run

    Called on three arrays of type Array<Shape>, the two methods give:

    ShapestotalPerimeterlargestShape
    Rectangle(3, 4), Square(5), Triangle(3, 4, 5)46Square
    Rectangle(2, 4), Square(3), Triangle(4, 4, 4)36Rectangle
    Square(1), Rectangle(1, 1), Triangle(2, 2, 3)15Triangle

    In the second row every perimeter is 12, so the first shape, the rectangle, wins the tie. In the third row a 1 by 1 rectangle has the same sides as a square, but it still answers Rectangle, because each object reports its own class. A variable of the parent type works the same way:

    val s: Shape = Square(5)
    println("${s.name()} ${s.perimeter()}")
    println("${Shape().name()} ${Shape().perimeter()}")
    
    Square 20
    Shape 0

    Adding a new kind of shape

    To see why the two methods never check classes, add a shape of your own in a file of your own:

    class Hexagon(val side: Int) : Shape() {
        override fun name(): String {
            return "Hexagon"
        }
        override fun perimeter(): Int {
            return 6 * side
        }
    }
    

    With the array Square(5), Hexagon(4), Triangle(3, 4, 5), totalPerimeter returns 56 and largestShape returns Hexagon, and neither method was edited. Only the new class had to be written, which is the benefit of calling methods through the common type.

    Complexity

    Each method looks at every shape once and calls methods that take constant time, so both run in O(n) time for n shapes, with O(1) extra space: a couple of variables.

    Edge cases and common mistakes

    • Using >= instead of >. It lets a later shape replace an equal earlier one, which breaks the tie rule.
    • Checking the class. Code like "if it is a Square, then 4 * side" defeats the point and fails for any new kind of shape.
    • Forgetting open or override. Without them the compiler rejects the child classes.
    • Starting largest at 0. It must hold a shape, not a number, because the answer is a name. Starting from shapes[0] is safe because the list is never empty.
    • A single shape. The loop compares the first shape with itself, changes nothing, and returns its name. That is correct.
    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free