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:

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:

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:

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.