Class 6 — Monday, September 14
Sections 1 to 3 are a ten-line class you type yourself. From section 4 you are in your own A3 repo.
- A set that loses what you put in it — predict first
- Give it a
hashCode - Break it on purpose — two ways, and only one of them fails
- Now your own
Matrix— this is A3 task 5 add, and the shape it refuses — A3 task 6- Catch it — once
addworks
1. A set that loses what you put in it
Make a file called Point.java in your A3 project, beside Matrix.java, and paste this in. It does no harm if it ends up in your repo.
import java.util.HashSet;
import java.util.Set;
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof Point that)) {
return false;
}
return this.x == that.x && this.y == that.y;
}
public static void main(String[] args) {
Point a = new Point(3, 4);
Point b = new Point(3, 4);
System.out.println(a.equals(b)); // 1st prediction
Set<Point> seen = new HashSet<>();
seen.add(a);
System.out.println(seen.contains(b)); // 2nd prediction
}
}This equals is correct. It is the one you wrote last class, on a different class, and the @Override is there and the compiler is happy with it.
Write down both predictions before you run it. Two lines, true or false.
true, then false.
a.equals(b) says these two points are equal. Then a set holding a is asked whether it contains b, and it says no.
Nothing is broken. equals is right, @Override is right, it all compiles without a warning. A HashSet does not start by calling equals. It starts by calling hashCode, and you never wrote one.
2. Give it a hashCode
Add this to Point, and add java.util.Objects to the imports:
@Override
public int hashCode() {
return Objects.hash(x, y);
}Run it again.
Done looks like: both prediction lines in main printing true, and you can say which line you changed to move the second one.
3. Break it on purpose
Two experiments. Predict each one before you run it.
First, replace the body with a constant:
@Override
public int hashCode() {
return 0;
}Then put Objects.hash(x, y) back, and try a value that changes instead:
private static int counter = 0;
@Override
public int hashCode() {
return counter++;
}return 0 works. Both lines still print true.
The counter does not. The second line is false again.
The rule is one sentence:
Two objects that are equal must return the same hash code.
It says nothing about objects that are not equal, so return 0 keeps the rule — every object agrees with every other object, which includes every pair that is equal. It is correct. It is also slow, because every object lands in one place and the set has to compare them one at a time.
The counter breaks the rule, because the same point gets a different answer depending on when you ask. Two equal objects, two hash codes.
Nothing will ever tell you whether your equals/hashCode play by the rule. Not the compiler, not @Override, not a warning. The rule is four sentences of documentation and the only thing enforcing it is you.
4. Now your own Matrix
Two checks in MatrixTest are waiting for it: hashCodeAgreesWithEquals and aHashSetFindsAnEqualMatrix.
You are hashing a double[][], and there is a trap in it.
Done looks like: both of those checks passing, so the first failure moves down to addAndSubtractWorkCellByCell.
Arrays.hashCode and the checks still fail
Arrays.hashCode hashes an array by asking each element for its own hash code. An element of a double[][] is a double[] — an object, with no hashCode of its own, so it hashes by its address.
Two matrices holding identical numbers hold different arrays, at different addresses, so they get different answers. Section 1 again, one level down.
Arrays.deepHashCode goes all the way in. Read what it does before you use it. Writing the loop yourself is also fine.
hashCode before today
Delete it, and run MatrixTest.
equalsComparesContents still passes. hashCodeAgreesWithEquals and aHashSetFindsAnEqualMatrix do not. Work out why the first one survived before you put it back — that is the whole of section 1 stated as a test failure.
5. add, and the shape it refuses
The add and subtract method each return a new matrix and change neither operand. Both need the two shapes to match, and the same check runs in both. Write it once as a private helper and call it from both.
When the shapes do not match, throw a DimensionMismatchException. Put both shapes in the message. A caller who reads “shapes differ” learns nothing; a caller who reads “2-by-3 and 3-by-2” knows what to fix.
Done looks like: addAndSubtractWorkCellByCell and addAndSubtractRejectAShapeMismatch both passing.
Run the test and fix whatever it names first. It may not be this section — the checks run in the order of the tasks, so the first failure is the next thing to write whatever that is.
Done for today is your first failure moving down at least one check.
6. Catch it
Do this one when addAndSubtractRejectAShapeMismatch passes.
Every method you have written so far throws. None of them has ever been the one to deal with it. Add this to Matrix.java’s main, or to a scratch file:
Matrix twoByTwo = new Matrix(2, 2);
Matrix twoByThree = new Matrix(2, 3);
Matrix sum = twoByTwo.add(twoByThree);
System.out.println(sum);It does not compile. Read the error before you go on — it names the exception and it tells you the two things you are allowed to do about it.
Then do the second one: catch it, and print the message.
Done looks like: the program running to the end and printing the two shapes you put in the message back at you.
catch is worse than no catch
} catch (DimensionMismatchException e) {
}That compiles, and it is the worst thing on this page. The program now carries on with a matrix it never built, and says nothing.
The compiler forced you to make a decision here. Doing nothing is a decision, and it is almost never the right one.
What else to do
[Consulting AI allowed] Add
hashCodeto yourFraction. This implies some research on how to hash a pair of integers.Add
Fractionmethods that enable arithmetics with integers (e.g.myFraction.add(5)). Add tests for it inFractionTest.java.A
Fractioncan represent an integer (e.g.new Fraction(2,1)). In such cases, it is natural to expectnew Fraction(2,1).equals(Integer(2))to hold. Near theFraction.equalsimplementation, write down which fundamental mathematical property of equality the smart implementation ofFraction.equalslike above would brake.