Week 4: Inheritance and Polymorphism I

04.05.2026

Slides

Download Week 4 Slides (PDF) →

Code Examples

Four reference packages. The first two give you the tools (enums + Random); the next two show inheritance end to end (a plain extends hierarchy and an abstract base class); the last is the W3 SIR starter that this week’s assignment evolves.

Code/Week4/src/at/ac/univie/gis/week4/
├── cardinal/
│   ├── CardinalDirection.java       enum: type-safe constants
│   ├── CardinalDirectionTest.java   tiny enum demo
│   └── RandomLocations.java         java.util.Random: nextInt/Double/Boolean/Gaussian + seeding
├── watercourse/
│   ├── Watercourse.java             parent class with describe()
│   ├── Spring.java                  plain helper (composition)
│   ├── River.java                   extends Watercourse; super(...) + override
│   └── WatercourseExample.java      ArrayList<Watercourse>, dynamic dispatch, instanceof
├── geometry/
│   ├── Geometry.java                ABSTRACT base; abstract getArea(), concrete describe()
│   ├── Circle.java                  concrete subclass
│   ├── Rectangle.java               concrete subclass
│   └── GeometryExample.java         ArrayList<Geometry> of mixed shapes
└── sir/
    ├── Point.java                   2D point with distanceTo
    ├── Person.java                  SUSCEPTIBLE / INFECTIOUS / REMOVED
    ├── Disease.java                 stub for assignment task 3
    └── Main.java                    W3 SIR starter — your W4 work lands here

1. cardinal/ — enums and Random

CardinalDirection.java — An enum: a small fixed set of named, type-safe constants. The compiler will not let you pass an invalid direction the way it would let you pass an invalid int.

package at.ac.univie.gis.week4.cardinal;

/**
 * An enum representing the four cardinal directions.
 * Enums provide type-safe constants - you cannot accidentally use an invalid direction.
 * Compare this to using plain integers (1=North, 2=South...) which are error-prone.
 */
public enum CardinalDirection {
    NORTH, SOUTH, EAST, WEST
}

CardinalDirectionTest.java — Access an enum value with ClassName.VALUE. Enum values can be printed, compared with ==, and passed around like any object.

package at.ac.univie.gis.week4.cardinal;

public class CardinalDirectionTest {

    public static void main(String[] args) {
        // Access an enum value using ClassName.VALUE
        System.out.println(CardinalDirection.WEST);
    }
}

RandomLocations.java — Two takeaways. (1) java.util.Random gives four flavours of randomness — nextInt, nextDouble, nextBoolean, nextGaussian — where Math.random() only gives one. (2) Passing a seed to the constructor makes the sequence reproducible: the same seed always produces the same numbers. This is the tool the W4 assignment’s “reproducible placement” task is built on.

package at.ac.univie.gis.week4.cardinal;

import java.util.Random;

public class RandomLocations {

    public static void main(String[] args) {

        // --- (1) The four methods on Random --------------------------------

        // No seed → time-based, different every run.
        Random rng = new Random();

        // Random integer in [0, 501)  → e.g. an x-coordinate on a 500-wide grid.
        int x = rng.nextInt(501);

        // Random double in [0.0, 1.0) → multiply by area width to place a person.
        double y = rng.nextDouble() * 500;

        // Random true/false with equal probability.
        boolean coin = rng.nextBoolean();

        // Sample from the standard normal distribution N(0, 1) — handy for noise.
        double noise = rng.nextGaussian();

        System.out.println("x: " + x);
        System.out.println("y: " + y);
        System.out.println("coin: " + coin);
        System.out.println("noise: " + noise);

        // Math.random() is essentially shorthand for new Random().nextDouble().
        // Fine for one-off rolls, but you cannot seed it.
        System.out.println("Math.random(): " + Math.random());

        // --- (2) Seeded Random for reproducible runs -----------------------

        // Two generators built from the SAME seed produce the SAME sequence.
        // Run this main method twice and these four numbers stay identical.
        Random seeded = new Random(42);
        System.out.println("\nSeeded run (always the same):");
        for (int i = 0; i < 4; i++) {
            System.out.println("  " + seeded.nextDouble());
        }
    }
}

2. watercourse/extends, super, override, instanceof

Watercourse.java — Plain parent class. Holds a length and a default describe() that subclasses are free to override.

package at.ac.univie.gis.week4.watercourse;

/**
 * A simple base class representing a watercourse (any body of flowing water).
 * Serves as the parent class in an inheritance hierarchy.
 */
public class Watercourse {

    private double length;

    public Watercourse(int length) {
        this.length = length;
    }

    public double getLength() {
        return length;
    }

    public void setLength(double length) {
        this.length = length;
    }

    /**
     * Returns a short description of this watercourse.
     * Subclasses can OVERRIDE this method to add their own information,
     * and call super.describe() to keep the parent's contribution.
     */
    public String describe() {
        return "Watercourse of length " + length;
    }
}

Spring.java — A plain helper class held by River via composition. A river HAS-A spring; a river is not a kind of spring.

package at.ac.univie.gis.week4.watercourse;

public class Spring {

    private String name;

    public Spring(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

River.java — A River IS-A Watercourse. The constructor uses super(length) to hand the length up to the parent, and describe() is overridden so that super.describe() is augmented — not replaced — with the spring name.

package at.ac.univie.gis.week4.watercourse;

/**
 * A River is a specific kind of Watercourse that also has a Spring (origin).
 *
 * Demonstrates two inheritance ideas at once:
 *   - super(length) in the constructor hands data up to the parent.
 *   - describe() is OVERRIDDEN below, and uses super.describe() to add to —
 *     not replace — the parent's behaviour.
 */
public class River extends Watercourse {

    private Spring origin;

    public River(int length, Spring origin) {
        super(length);
        this.origin = origin;
    }

    public Spring getOrigin() {
        return origin;
    }

    public void setOrigin(Spring origin) {
        this.origin = origin;
    }

    @Override
    public String describe() {
        return super.describe() + ", originating at spring " + origin.getName();
    }
}

WatercourseExample.java — An ArrayList<Watercourse> holds both Watercourse and River objects because River IS-A Watercourse. describe() dispatches at runtime to the real object’s implementation. instanceof with a pattern variable (w instanceof River r) is the safe way to ask “is this actually a River?” before reaching for River-only data.

package at.ac.univie.gis.week4.watercourse;

import java.util.ArrayList;

public class WatercourseExample {

    public static void main(String[] args) {

        ArrayList<Watercourse> watercourses = new ArrayList<>();

        watercourses.add(new Watercourse(1246));
        watercourses.add(new Watercourse(541));
        watercourses.add(new Watercourse(1496));
        watercourses.add(new Watercourse(996));
        // A River fits in the same list because it extends Watercourse.
        watercourses.add(new River(766, new Spring("Rheinquelle")));

        for (Watercourse w : watercourses) {
            // Dynamic dispatch: each object decides which describe() runs.
            System.out.println(w.describe());

            // Only Rivers carry a Spring. Use instanceof to ask the question
            // safely before reaching for River-only data.
            if (w instanceof River r) {
                System.out.println("  spring object: " + r.getOrigin().getName());
            }
        }
    }
}

3. geometry/abstract classes

Geometry.javaabstract on the class blocks new Geometry(...) — a generic geometry is not a real shape. abstract on getArea() declares a contract: every concrete subclass must implement it. The concrete describe() calls the abstract getArea() — that call resolves to the subclass’s implementation at runtime.

package at.ac.univie.gis.week4.geometry;

/**
 * An ABSTRACT base class for geometric shapes.
 *
 * 'abstract' on the class means: you cannot do `new Geometry(...)`.
 * Geometry is not a real shape — every real shape is a Circle or Rectangle
 * or some other concrete subclass.
 *
 * 'abstract' on getArea() means: every subclass MUST provide its own
 * implementation. The compiler will refuse to compile a subclass that
 * forgets to implement getArea().
 */
public abstract class Geometry {

    private String label;

    public Geometry(String label) {
        this.label = label;
    }

    public String getLabel() {
        return label;
    }

    /**
     * Abstract method — no body, just a contract.
     * Each subclass fills this in with its own area formula.
     */
    public abstract double getArea();

    /**
     * A concrete method on an abstract class is allowed.
     * It can call the abstract method — the call resolves to the
     * subclass implementation at runtime (dynamic dispatch).
     */
    public String describe() {
        return label + " has area " + getArea();
    }
}

Circle.java & Rectangle.java — Two concrete subclasses, each supplying its own area formula. Without getArea(), the file would not compile.

package at.ac.univie.gis.week4.geometry;

public class Circle extends Geometry {

    private double radius;

    public Circle(String label, double radius) {
        super(label);
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}
package at.ac.univie.gis.week4.geometry;

public class Rectangle extends Geometry {

    private double width;
    private double height;

    public Rectangle(String label, double width, double height) {
        super(label);
        this.width = width;
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }
}

GeometryExample.java — You can declare ArrayList<Geometry> even though Geometry is abstract; you cannot new Geometry(...). Each shape’s own area formula runs at the right moment via dynamic dispatch.

package at.ac.univie.gis.week4.geometry;

import java.util.ArrayList;

public class GeometryExample {

    public static void main(String[] args) {

        ArrayList<Geometry> shapes = new ArrayList<>();
        shapes.add(new Circle("c1", 3.0));
        shapes.add(new Rectangle("r1", 4.0, 5.0));
        shapes.add(new Circle("c2", 1.5));

        // Polymorphism: same call, different formulas chosen per object.
        for (Geometry g : shapes) {
            System.out.println(g.describe());
        }

        // Uncomment to see the compile error abstract classes give you:
        // Geometry g = new Geometry("nope");
    }
}

4. sir/ — the W3 SIR starter (W4 assignment base)

Your starting point for this week’s assignment. Point and Person are the W3 versions; Disease is an empty stub (task 3); Main still uses Math.random() and a deterministic state flip. The file headers point at exactly what each task changes.

package at.ac.univie.gis.week4.sir;

public class Point {

    private double x, y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "Point{x=" + x + ", y=" + y + '}';
    }

    public double distanceTo(Point other) {
        return Math.sqrt(Math.pow(x - other.getX(), 2) + Math.pow(y - other.getY(), 2));
    }

    public double getX() { return x; }
    public void setX(double x) { this.x = x; }
    public double getY() { return y; }
    public void setY(double y) { this.y = y; }
}
package at.ac.univie.gis.week4.sir;

/**
 * Represents a person in the SIR (Susceptible-Infectious-Removed) model.
 *
 * --- Week 4 assignment notes ---
 *
 *   - move() currently uses Math.random(). Mandatory task 1 (reproducible
 *     placement) means every random draw in your simulation should come from
 *     a controlled, seeded source.
 *
 *   - changeState() currently flips state deterministically. Mandatory task 2
 *     (stochastic infection) means SUSCEPTIBLE -> INFECTIOUS should only
 *     happen when a probability roll succeeds.
 *
 *   - The state constants below could move to Disease as part of task 3, but
 *     that is itself a design choice — they could equally well stay here.
 *
 * Note: The states are modeled as integer constants here. Using an enum would
 * be cleaner — see the CardinalDirection example in the cardinal/ package.
 */
public class Person {

    public static final int SUSCEPTIBLE = 1;
    public static final int INFECTIOUS = 2;
    public static final int REMOVED = 3;

    private Point location;
    private int state = SUSCEPTIBLE;

    public Person(Point location, int state) {
        this.location = location;
        this.state = state;
    }

    public int changeState() {
        if (state == SUSCEPTIBLE) {
            state = INFECTIOUS;
        } else if (state == INFECTIOUS) {
            state = REMOVED;
        }
        return state;
    }

    public void move() {
        location.setX(Math.random() * 100);
        location.setY(Math.random() * 100);
    }

    public Point getLocation() { return location; }
    public void setLocation(Point location) { this.location = location; }
    public int getState() { return state; }

    public String getStateName() {
        switch (state) {
            case SUSCEPTIBLE: return "SUSCEPTIBLE";
            case INFECTIOUS:  return "INFECTIOUS";
            case REMOVED:     return "REMOVED";
            default:          return "UNKNOWN";
        }
    }

    @Override
    public String toString() {
        return "Person{location=" + location + ", state=" + getStateName() + '}';
    }
}

Disease.java — Intentionally empty. Mandatory task 3 asks you to put the infection probability and infection radius here; the stretch goal asks you to make Disease abstract and add concrete subclasses (e.g. airborne vs contact-based). The geometry/ package shows that pattern end to end.

package at.ac.univie.gis.week4.sir;

public class Disease {

    // Your design goes here.
}

Main.java — The W3 simulation: builds a hard-coded population and steps the first (infectious) person against the rest with a fixed distance threshold of 5. Most of your W4 changes land here — how to feed in a seeded Random, how to do a real probability roll, and how to read the radius and probability from Disease instead of from magic numbers.

package at.ac.univie.gis.week4.sir;

import java.util.ArrayList;
import java.util.Iterator;

public class Main {

    ArrayList<Person> population;

    public Main() {
        population = new ArrayList<Person>();
        population.add(new Person(new Point(10, 10), Person.INFECTIOUS));
        population.add(new Person(new Point(11, 12), Person.SUSCEPTIBLE));
        population.add(new Person(new Point(12, 13), Person.SUSCEPTIBLE));
        population.add(new Person(new Point(21, 19), Person.SUSCEPTIBLE));
        population.add(new Person(new Point(15, 11), Person.SUSCEPTIBLE));
        population.add(new Person(new Point(17, 16), Person.SUSCEPTIBLE));
        population.add(new Person(new Point(23, 22), Person.SUSCEPTIBLE));
    }

    public static void main(String[] args) {
        Main SIRrun = new Main();
        SIRrun.SIRstep();
    }

    /**
     * A single SIR step: the first person (infectious) infects anyone within distance 5.
     * Simplified — only the first person is checked as a source.
     */
    public void SIRstep() {
        Iterator<Person> SIRcheck = population.iterator();

        Person temp = null;
        if (SIRcheck.hasNext())
            temp = SIRcheck.next();

        while (SIRcheck.hasNext()) {
            Person toCheck = SIRcheck.next();
            System.out.println("\nBefore: " + toCheck);

            if (temp.getLocation().distanceTo(toCheck.getLocation()) < 5)
                toCheck.changeState();

            System.out.println("After: " + toCheck);
        }
    }

    /**
     * TODO: Compare ALL pairs of persons, not just the first one against the rest.
     */
    public void SIRstepAll() {
        // TBD - Exercise for students
    }
}

Assignment

Week 4 Assignment

Reading: Chapters on inheritance and abstract classes.

Coding: Evolve your W3 SIR code with this week’s tools.

  • Reproducible random placement. Every Person ends up at a random location inside the rectangular study area. Two back-to-back runs of your simulation should produce the same starting layout — so you can debug a weird run. (You decide how.)
  • Stochastic infection. When an infectious person is within the infection radius of a susceptible person, the susceptible becomes infectious with the given probability — sometimes yes, sometimes no, in proportion to that probability. The upgrade from W3: there your “risk” was a number you returned; here the state actually changes (or doesn’t). Removed people don’t participate.
  • One place for the disease parameters. The infection probability and the infection radius should live in one designated location — a Disease class — and every place in the rest of your code should refer to them through that class. No more magic numbers scattered through Simulation and Person.
  • More advanced: support multiple kinds of disease (e.g. an airborne one and a contact-based one) whose transmission probabilities differ. Your simulation should be able to run with any one of them. And the generic Disease itself should not be something you can instantiate directly — there’s no such thing as a “generic disease,” only specific ones.

Submission: Upload FirstNameLastNameW4.zip with your .java files
Deadline: Sunday at 5:00 PM (Vienna Time)