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.java — abstract 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
Personends 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
Diseaseclass — and every place in the rest of your code should refer to them through that class. No more magic numbers scattered throughSimulationandPerson. - 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
Diseaseitself 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)