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
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)prints1 / 2. - The sign, if there is one, lives on the numerator.
new Fraction(1, -2)prints-1 / 2, andnew Fraction(-1, -2)prints1 / 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 itprivate 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.- It is always stored in lowest terms.
Add
subtract,multiply, anddivide. Each takes anotherFraction, 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. divideis 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.
- None of the three should contain an
Fix
toStringfor whole numbers. A fraction whose denominator is 1 should print as3, not3 / 1. Note that after step 1 this is the only place a whole number can come from.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.
Make
Fraction extends Number.Numberis the abstract class thatInteger,DoubleandLongalready extend. It declares four methods and implements none of them, so the moment you writeextends Numberyour 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.
doubleValue()is thetoDouble()you already have. Rename it and put@Overrideabove 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 callstoDouble()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.intValue()truncates toward zero.new Fraction(3, 2).intValue()is1, not2— which is what a cast tointdoes, and what integer division already does, so the obvious line is also the correct one. Work out what you expect fromnew Fraction(-3, 2).intValue()before you run it, and make sure the code agrees with you.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
Fractionis. It knows only that anything extendingNumbercan be asked for adouble, which is the entire idea. The1and the2.5turned into anIntegerand aDoubleon 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:
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 writtensuper(...)yet —Numberhas no state to pass it.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, becausesuper(...)has to come first. Between them, every object is born correct — which is Part 1 one level up.At least one
abstractmethod, answered differently by each subclass. Abstract for the same reasonNumberdeclaresdoubleValue()and does not implement it: there is no sensible version for the parent to give.Every field
final.A
mainthat puts your subclasses into an array of the parent type and loops over it, exactly as theNumber[]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.
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/2and2/4are 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 ahashCodewhenever you writeequals. We will come back to that.)[Consulting AI allowed] Find two fractions whose sum
addreports 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 whatMath.addExactandMath.multiplyExactdo, and what it would cost to use them here.[Consulting AI allowed but keep the setup small] Extend
FractionTest.javato cover what it leaves out:subtract,multiply,divide, the failure individe, 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.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.