Problem overview
A drawing app keeps rectangles, squares and triangles together. You write one class per kind of shape, all extending a common class Shape, plus two methods in Solution that work on a Shape[]:
totalPerimeter(shapes)returns the sum of all the perimeters.largestShape(shapes)returns thename()of the shape with the largest perimeter. On a tie, the first one in the array wins.
The perimeters are 2 * (width + height) for Rectangle, 4 * side for Square and a + b + c for Triangle. A plain Shape is named "Shape" and has perimeter 0.
For a Rectangle(3, 4), a Square(5) and a Triangle(3, 4, 5), the perimeters are 14, 20 and 12. The total is 46 and the largest is the square, so the result is 46 Square.
Approach: ask the object, never check its class
The two methods in Solution must not test what kind of shape they hold. They only call perimeter() and name(), the two methods that Shape declares. Java runs the version that belongs to the object's real class, so a Square object computes 4 * side even when the code only knows it has a Shape. This is polymorphism: one call, many behaviours.
The payoff is that a shape added later, say a Circle with its own perimeter(), works with totalPerimeter and largestShape without touching them.
Both methods make one pass over the array, with a running total in the first one and a running best in the second.
Step by step
- Give
Shapeits default methods.name()returns"Shape"andperimeter()returns 0. The subclasses override both, but the common type needs them so that callers can use them. - Add the fields.
Rectanglekeepswidthandheight,SquarekeepssideandTrianglekeepsa,bandc, all aspublic intfields.perimeter()needs them later, so each object has to remember its sizes. - Fill the constructors. The parameters have the same names as the fields, so
this.width = width;stores the parameter in the field. - Override
name()andperimeter()in each class. Mark both with@Overrideso the compiler checks that the parent has them. - Write
totalPerimeter. Start anint totalat 0, loop over the shapes with a for-each loop and addshape.perimeter(). - Write
largestShape. Start withlargest = shapes[0](the array is never empty), loop over all the shapes, and replacelargestwhen a shape's perimeter is strictly greater. Returnlargest.name().
The solution in Java
class Shape {
public String name() {
return "Shape";
}
public int perimeter() {
return 0;
}
}
class Rectangle extends Shape {
public int width;
public int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public String name() {
return "Rectangle";
}
@Override
public int perimeter() {
return 2 * (width + height);
}
}
class Square extends Shape {
public int side;
public Square(int side) {
this.side = side;
}
@Override
public String name() {
return "Square";
}
@Override
public int perimeter() {
return 4 * side;
}
}
class Triangle extends Shape {
public int a;
public int b;
public int c;
public Triangle(int a, int b, int c) {
this.a = a;
this.b = b;
this.c = c;
}
@Override
public String name() {
return "Triangle";
}
@Override
public int perimeter() {
return a + b + c;
}
}
class Solution {
public int totalPerimeter(Shape[] shapes) {
int total = 0;
for (Shape shape : shapes) {
total += shape.perimeter();
}
return total;
}
public String largestShape(Shape[] shapes) {
Shape largest = shapes[0];
for (Shape shape : shapes) {
if (shape.perimeter() > largest.perimeter()) {
largest = shape;
}
}
return largest.name();
}
}
Details worth knowing
- Strictly greater keeps the first on a tie. With
>, a later shape with an equal perimeter never replaces the current one. With>=the last tied shape would win. - The loop can start at the first shape. Comparing
shapes[0]with itself changes nothing, so a for-each over the whole array is fine. - The name belongs to the object. A
Rectangle(1, 1)has the same sides as aSquare(1), and both have perimeter 4, but the first reports"Rectangle"and the second"Square". - The constructors set the fields directly. The subclass constructors don't call
super(...)explicitly:Shapehas no constructor of its own, so Java calls its default one for you.
Adding a new kind of shape
To see why the rule matters, add a class the lesson doesn't have. This Pentagon extends Shape and overrides the same two methods:
class Pentagon extends Shape {
public int side;
public Pentagon(int side) {
this.side = side;
}
@Override
public String name() {
return "Pentagon";
}
@Override
public int perimeter() {
return 5 * side;
}
}
With the array { new Square(2), new Pentagon(3) }, the perimeters are 8 and 15. Calling totalPerimeter and largestShape on it gives 23 Pentagon, and neither method needed a single change.
The defaults in Shape ("Shape" and 0) are never used with these three subclasses, because each one overrides both methods. They exist so the common type has the two methods that callers rely on. A plain new Shape() is allowed here and reports Shape with perimeter 0.
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. They keep only a couple of variables, so the extra space is O(1).
The largest possible values are small: a perimeter is at most about 4 * 10000 for one shape, and with 100 shapes the total stays far below the int limit.
Testing
Running the solution on three small lists gives:
| Shapes | Result |
|---|---|
Rectangle(3, 4), Square(5), Triangle(3, 4, 5) | 46 Square |
Rectangle(2, 4), Square(3), Triangle(4, 4, 4) | 36 Rectangle |
Square(1), Rectangle(1, 1), Triangle(2, 2, 3) | 15 Triangle |
Square(7) | 28 Square |
In the second row all three perimeters are 12, so the first shape, the rectangle, wins.
Common mistakes
- Checking the class. Code like
if (shape instanceof Square)insidetotalPerimeterworks for today's shapes but breaks the moment a new kind appears. Callperimeter()and let the object decide. - Forgetting
this.in a constructor. Writingwidth = width;assigns the parameter to itself and leaves the field at 0. - Returning the perimeter instead of the name.
largestShapereturns aString, the result oflargest.name(), not the number. - Forgetting the multiplication by 2 in the rectangle.
width + heightis only half of the way around. - Using
>=in the comparison. That returns the last shape among equal ones, and the second example would printTriangleinstead ofRectangle.
