Assignment 2 — Fraction

Finish the Fraction class we wrote together: strengthen its invariant so that every fraction is stored in lowest terms, add the arithmetic that gets to assume it, and make it a real Number.

Due Sep 8, 2026

Note: “AI” below means your favorite chatbot, e.g. Google Gemini, Microsoft Copilot, ChatGPT. Where a step does not say otherwise, AI tools are not allowed, as set out in the AI policy in the syllabus.

Start from the starter repo (link on Discord, #announcements). It holds src/Fraction.java as Monday’s class left it — including an add that is still a stub — and a src/FractionTest.java that says what is missing. Run the tests before you write anything:

javac -d out src/*.java
java -ea -cp out FractionTest

-ea is what turns assertions on; without it the test checks nothing, so it throws and says so instead. In IntelliJ, -ea goes in the VM options of the run configuration.

On your own machine, check your Java version. Fraction.java has a main written as static void main(), with no String[] — the short form we talked about in class. It is a Java 25 entry point: on an older JDK it still compiles, but running Fraction gives “Main method not found in class Fraction”. That is your JDK, not your code, and the lab machines are fine. FractionTest runs either way.

Every check in that file is true of a finished Fraction, so silence means done and a failure names the next thing to do. Today it stops at the zero denominator — the guard we write together on Wednesday — and after that the failures follow the steps below in order.

If you missed Wednesday’s class, that first failure is where to start. Three things go in, and none of them is graded — they are what the class hands you. First the fields become final and the constructor guards its denominator:

private final int num;
private final int den;

public Fraction(int num, int den) {
    if (den == 0) {
        throw new IllegalArgumentException("denominator must not be zero");
    }
    this.num = num;
    this.den = den;
}

Then add, which in the starter returns new Fraction() and so answers 0 / 1 to everything:

public Fraction add(Fraction other) {
    return new Fraction(num * other.den + other.num * den, den * other.den);
}

The test catches the missing guard and the stubbed add. It cannot catch the missing final — a test runs your code, it does not read how you declared it — so put that in yourself. Step 1 below leans on it.

Part 1 — the invariant, and the arithmetic that assumes it

  1. Strengthen the invariant. In class we promised one thing about every Fraction: the denominator is never zero. Promise two more.

    • It is always stored in lowest terms. new Fraction(2, 4) prints 1 / 2.
    • The sign, if there is one, lives on the numerator. new Fraction(1, -2) prints -1 / 2, and new Fraction(-1, -2) prints 1 / 2.

    Both are a constructor’s job, not a caller’s, and the fields are final, so the constructor is the only place they can be. Work out the sign and the common divisor first, then assign each field once. You will want a helper for the divisor; make it private static, because it needs nothing from any particular fraction:

    private static int gcd(int a, int b)

    Resist writing a simplify() that fixes a fraction after the fact. A fraction that needs fixing is one that should never have been built, and a method to fix it is a sign the invariant is in the wrong place.

  2. Add subtract, multiply, and divide. Each takes another Fraction, returns a new one, and modifies nothing.

    • None of the three should contain an if. If you catch yourself writing a check, work out whether a constructor has already made it impossible.
    • divide is the interesting one. What should happen if you divide by zero? You should not have to write a single line to make it happen — but you do have to be able to explain why, so make sure you can.
  3. Fix toString for whole numbers. A fraction whose denominator is 1 should print as 3, not 3 / 1. Note that after step 1 this is the only place a whole number can come from.

  4. Commit and push. The last push before the deadline is the one that counts.

Part 2 — a Fraction is a Number

We do most of this together in class on Wednesday, in pairs on one machine, so what is left afterwards should be finishing and pushing rather than starting. That makes Part 2 the one piece of graded work in this course you may hand in having written it with someone else: partners push the same file, on purpose. Part 1 is your own — and so is Part 2, if you were not there on Wednesday.

  1. Make Fraction extends Number. Number is the abstract class that Integer, Double and Long already extend. It declares four methods and implements none of them, so the moment you write extends Number your class stops compiling until all four exist:

    public int intValue()
    public long longValue()
    public float floatValue()
    public double doubleValue()

    Read that error before you fix it. It is the clearest statement Java ever makes of what a superclass is: a promise the compiler will not let you make without keeping.

  2. doubleValue() is the toDouble() you already have. Rename it and put @Override above it. The annotation is not decoration — it asks the compiler to check that you really are replacing an inherited method, so a misspelled name fails loudly instead of quietly becoming a new method nobody ever calls.

    The rename also breaks FractionTest.java, which calls toDouble() in three of its checks. Rename those too. Nothing has gone wrong — that is simply what renaming a public method costs, and three checks is a cheap place to meet the bill.

  3. intValue() truncates toward zero. new Fraction(3, 2).intValue() is 1, not 2 — which is what a cast to int does, and what integer division already does, so the obvious line is also the correct one. Work out what you expect from new Fraction(-3, 2).intValue() before you run it, and make sure the code agrees with you.

  4. Make this run, and be ready to explain it:

    Number[] nums = { 1, 2.5, new Fraction(1, 2) };
    for (Number n : nums) {
        System.out.println(n.doubleValue());
    }

    Nothing in that loop knows what a Fraction is. It knows only that anything extending Number can be asked for a double, which is the entire idea. The 1 and the 2.5 turned into an Integer and a Double on the way into the array; [CJ] 5.4 — Object Wrappers and Autoboxing is where that is explained, and it is this week’s reading for exactly this reason.

Optional — a hierarchy of your own

Ungraded, and there is no starter repo: you make the repository yourself, the way you made java-scratch in class. When it runs, paste its URL into the Journal on Brightspace. That is the whole submission.

Part 2 put Fraction into a hierarchy someone else had already designed, and that hid three things. Number holds no data, so you never wrote super(...). You never declared an abstract class yourself. And the design was settled before you arrived: Number had already chosen what the parent declares and what it leaves to each subclass — which, in a hierarchy of your own, is the first thing you have to decide.

Write one abstract class and at least two concrete subclasses:

  1. The abstract class holds a field and guards it in its constructor. Every subclass reaches that constructor through super(...), so the check runs once on behalf of all of them. You have not written super(...) yet — Number has no state to pass it.

  2. Each subclass guards its own fields, after the super(...) call. An invariant lives with the field it constrains: the parent cannot check a dimension it does not hold, and a subclass cannot skip the parent’s check, because super(...) has to come first. Between them, every object is born correct — which is Part 1 one level up.

  3. At least one abstract method, answered differently by each subclass. Abstract for the same reason Number declares doubleValue() and does not implement it: there is no sensible version for the parent to give.

  4. Every field final.

  5. A main that puts your subclasses into an array of the parent type and loops over it, exactly as the Number[] loop does. Nothing inside the loop may name a subclass.

Before you finish, leave the super(...) call out once and read the error. Java writes a no-argument super() for you when you do not write one yourself, so the complaint is that the parent has no constructor taking no arguments — a call you never typed, failing on a line you did not write.

If you would rather not invent a domain: Shape holding a name, with Circle and Rectangle under it and an abstract double area(), where the parent checks the name and each subclass checks its own dimensions.

Done looks like: the loop printing one line per object, every line different, with no subclass named inside it — and the repository’s URL in the Journal.

Two subclasses is the whole of it. This is an evening, not a second assignment.

What else to do (optional part)

These problems are ungraded: they carry no points, and skipping them costs you nothing.

  1. Add equals, so that two Fractions with the same value are equal. Use the signature Java expects:

    public boolean equals(Object o)

    Then notice how little work it takes, and why: step 1 means 1/2 and 2/4 are the same two numbers by the time anyone can compare them. If you had skipped normalizing, this method would have to do the arithmetic instead. (Java also wants a hashCode whenever you write equals. We will come back to that.)

  2. [Consulting AI allowed] Find two fractions whose sum add reports incorrectly, with no error and no exception — the answer is simply wrong. You will not need very large numbers. Once you have found a pair, ask AI what Math.addExact and Math.multiplyExact do, and what it would cost to use them here.

  3. [Consulting AI allowed but keep the setup small] Extend FractionTest.java to cover what it leaves out: subtract, multiply, divide, the failure in divide, and Part 2. Those are the parts of your solution nothing is currently checking, so this is the item that tells you whether the rest of the assignment is right. Follow the shape already in the file — as in A1, automated means the program checks itself on every run and stays silent unless something breaks.

  4. Write one line of Javadoc above each public method — what a caller needs to know, not what the code does. This is not graded, but Presentation is worth 10% of your grade and is you explaining this code out loud.