Java Array of Shapes- Object-Oriented Graphics Programming Guide
What Is a Java Array of Shapes?
It's exactly what it sounds like. An array that holds shape objects. But here's why it matters: shapes in Java are classes, and different shapes share common properties. A circle, rectangle, and triangle all have position, color, and the ability to draw themselves.
When you group them in a single array, you treat them uniformly while each one behaves according to its actual type. That's polymorphism doing the heavy lifting.
The Core Building Blocks
Before you write a single line of code, you need three things:
- A base Shape class with common properties and methods
- Concrete shape classes that extend the base class
- A polymorphic array that holds instances of those concrete classes
That's the entire architecture. Everything else is implementation details.
The Shape Superclass
Your base class defines what every shape has and can do. Keep it minimal:
- Position coordinates (x, y)
- Color
- A draw() method that subclasses override
- Maybe calculateArea() if your program needs it
Concrete Shape Classes
Each shape extends Shape and provides its own implementation. Circle overrides draw() to paint a circle. Rectangle overrides draw() to paint a rectangle. Same method name, different behavior.
This is the whole point. You call shapeArray[i].draw() and the right thing happens for each shape type.
Creating the Array of Shapes
The syntax is straightforward:
Shape[] shapes = new Shape[10];
You declare an array of type Shape, not Circle or Rectangle. That's the trick. The array holds Shape references, but those references point to actual Circle, Rectangle, or Triangle objects.
shapes[0] = new Circle(50, 50, 20, Color.RED);
shapes[1] = new Rectangle(100, 100, 80, 40, Color.BLUE);
shapes[2] = new Triangle(150, 50, 30, Color.GREEN);
All three are assigned to the same array despite being different classes. Java handles this because all three are Shapes.
Iterating and Drawing
This is where it gets useful. One loop draws every shape:
for (int i = 0; i < shapes.length; i++) {
if (shapes[i] != null) {
shapes[i].draw(g);
}
}
The Graphics object g gets passed to each shape's draw method. Circle.draw() uses g.drawOval(). Rectangle.draw() uses g.drawRect(). You don't need to know which is which.
Polymorphism in Action
Here's the critical part most tutorials skip over. When you call shape.draw(g), Java doesn't decide at compile time which draw() to run. It looks at the actual object type at runtime and calls the right version.
This means your drawing loop doesn't change when you add new shape types. New shape class? Add it to the array. The loop stays the same.
This is the real value of this pattern. Your code stays maintainable as shape complexity grows.
Common Mistakes
Forgetting to Override Methods
If your Shape class has a draw() method and subclasses don't override it, every shape draws identically. That's probably not what you want.
Null Checks
Arrays of objects are initialized with null values. Attempting to call methods on a null reference throws an exception. Always check before drawing.
Using Primitive Arrays
Don't try to store shapes in an int[] or double[]. Those hold numbers, not objects. You need Shape[] or a similar reference type array.
Forcing Type Checks
If you find yourself writing instanceof checks in your drawing loop, you've already lost the polymorphism benefit. Redesign your approach instead.
Approach Comparison
| Approach | Flexibility | Complexity | Best For |
|---|---|---|---|
| Separate arrays per shape type | Low | Low | Simple programs, fixed shape set |
| Array of Shapes (polymorphic) | High | Medium | Dynamic shape collections |
| ArrayList of Shapes | Highest | Medium | Unknown number of shapes, frequent add/remove |
| ShapeManager class | High | High | Complex applications with layers, selection, grouping |
For most graphics programs, the ArrayList of Shapes approach hits the sweet spot. You get polymorphism plus dynamic sizing. Arrays work fine when you know the exact count upfront.
Getting Started
Here's a minimal working example to copy and run:
import java.awt.*;
import javax.swing.*;
class Shape {
int x, y;
Color color;
Shape(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
}
void draw(Graphics g) {
g.setColor(color);
}
}
class Circle extends Shape {
int radius;
Circle(int x, int y, int radius, Color color) {
super(x, y, color);
this.radius = radius;
}
@Override
void draw(Graphics g) {
super.draw(g);
g.fillOval(x - radius, y - radius, radius * 2, radius * 2);
}
}
class Rectangle extends Shape {
int width, height;
Rectangle(int x, int y, int width, int height, Color color) {
super(x, y, color);
this.width = width;
this.height = height;
}
@Override
void draw(Graphics g) {
super.draw(g);
g.fillRect(x, y, width, height);
}
}
To use this with a JFrame:
Shape[] shapes = {
new Circle(100, 100, 30, Color.RED),
new Rectangle(200, 100, 60, 40, Color.BLUE),
new Circle(300, 120, 20, Color.GREEN)
};
for (Shape s : shapes) {
s.draw(g);
}
That's it. Three shapes, one loop, no type checking.
When to Use This Pattern
This approach works well when:
- You're drawing multiple shape types in the same component
- You need to add new shape types without rewriting drawing logic
- You're building a graphics editor or game with different object types
- You want a single loop to handle all rendering
Skip this pattern if you only have one shape type or if shapes share no common behavior. Inheritance only adds value when there's actually shared behavior to exploit.