Stack Heap and Class Design

exercises
tutorial
java
memory-model
recursion
class-design

Applied class exercises for week 3, covering 2026-03-12-object-oriented-programming-ii and reinforcing java-mutability (why reference types behave differently from primitives when passed to methods).

On the HTML site, fill in each blank with your answer (as a quoted string, e.g. "1"), then click Run Code to check it. In the PDF, the Working callout is shown as a static answer key instead (interactive checking needs a browser).

Question 1 — Tracing the call stack and heap

Understanding the runtime call stack and object heap clarifies unexpected behaviour of reference types. Consider:

class Counter {
    private int number;
    public Counter() { this.reset(); }
    public void increment(int amount) { this.number += amount; }
    public void increment() { this.number++; }
    public void reset() { this.number = 0; }
    public int getValue() { return this.number; }
}

class DebugMe {
    static void f(Counter counter, int param) {
        counter.increment();
        param++;
    }

    public static void main(String[] args) {
        Counter first = new Counter();
        int second = 0;
        f(first, second);
        System.out.println(first.getValue());
        System.out.println(second);
    }
}

What does main print (first line, then second line)?

First line →

Second line →

State of the call stack/heap just before f finishes executing:

main
args {}
first Counter{number: 1}
second 0
f
counter (same Counter object as first)
param 1

first and counter are two references to the same Counter object on the heap, so counter.increment() inside f is visible through first after f returns — first.getValue() prints 1. But param is a int, passed by value: param++ only changes f’s local copy, not main’s second — so second still prints 0. This is why mutating an object through a reference parameter is visible to the caller, but reassigning/incrementing a primitive parameter is not.

Question 2 — Designing a Vertex class

The Triangle class below represents a triangle as a 2D array of coordinates (vertexes[0] = x-coordinates, vertexes[1] = y-coordinates) — leading to a fiddly implementation:

public class Triangle {
    private double[][] vertexes;

    public Triangle(double a, double b, double c) {
        double dividend = (square(a) - square(b) - square(c));
        double cx = dividend / (-2 * c);
        double cy = Math.sqrt(square(b) - square(cx));
        this.vertexes = new double[][]{
            new double[]{0, c, cx},
            new double[]{0, 0, cy}
        };
    }

    public double perimeter() {
        double x0 = vertexes[0][0], x1 = vertexes[0][1], x2 = vertexes[0][2];
        double y0 = vertexes[1][0], y1 = vertexes[1][1], y2 = vertexes[1][2];
        return distance(x0, y0, x1, y1) + distance(x1, y1, x2, y2) + distance(x2, y2, x0, y0);
    }

    public Triangle scale(double multiplier) {
        double[][] scaledUp = new double[2][3];
        for (int x = 0; x < 2; x++) {
            for (int y = 0; y < 3; y++) {
                scaledUp[x][y] = vertexes[x][y] * multiplier;
            }
        }
        return new Triangle(scaledUp);
    }
}

Design a Vertex class (signatures only, no implementation needed) that could simplify this — a triangle is really a collection of three vertices, each with an x and y coordinate.

class Vertex {
    Vertex(); // Construct a new vertex at (0, 0)
    Vertex(double, double);
    double distance(Vertex); // distance to another vertex
    Vertex scale(double); // multiply the coordinates by the given amount
}

Rewriting Triangle to use three Vertex fields instead of a raw 2D array:

public class Triangle {
    private Vertex a;
    private Vertex b;
    private Vertex c;

    public Triangle(double a, double b, double c) {
        double dividend = (square(a) - square(b) - square(c));
        double cx = dividend / (-2 * c);
        double cy = Math.sqrt(square(b) - square(cx));
        this.a = new Vertex();
        this.b = new Vertex(c, 0);
        this.c = new Vertex(cx, cy);
    }

    public Triangle(Vertex a, Vertex b, Vertex c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    public double perimeter() {
        return a.distance(b) + b.distance(c) + c.distance(a);
    }

    public Triangle scale(double multiplier) {
        return new Triangle(a.scale(multiplier), b.scale(multiplier), c.scale(multiplier));
    }
}

Introducing Vertex moves the x/y bookkeeping into a single-responsibility class, so Triangle’s own methods read as “distance between vertices” and “scale each vertex” rather than juggling raw array indices.

Question 3 — Recursive call stack (bonus)

class Reverser {
    static void reverse(int[] arr) {
        int n = arr.length;
        if (n == 1) {
            return;
        }
        int[] rest = new int[n - 1];
        for (int i = 1; i < n; i++) {
            rest[i - 1] = arr[i];
        }
        arr[n - 1] = arr[0];
        reverse(rest);
    }

    public static void main(String[] args) {
        int[] numbers = {42, 24, 11};
        reverse(numbers);
    }
}

Sketch the state of the call stack and heap just prior to the third call to reverse.

main
args {}
numbers {42, 24, 42}
reverse (1st call)
arr {42, 24, 42}
n 3
rest {24, 24}
reverse (2nd call)
arr {24, 24}
n 2
rest {11}

Each recursive call copies everything except the first element into a new, smaller array (rest), and overwrites the last slot of the current array with its own first element — so by the time the base case (n == 1) is reached, the original array has been rebuilt back-to-front in place.