-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathPair.java
More file actions
157 lines (136 loc) · 6.69 KB
/
Pair.java
File metadata and controls
157 lines (136 loc) · 6.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Objects;
/**
* An immensely useful class that for some reason is missing from the Java
* standard library. (Google "why Java has no Pair class" for a lively debate.)
*
* The original version of this file contained a hand-written Pair with
* manually coded getFirst(), getSecond(), equals(), hashCode(), and toString().
* That was 70 lines of careful boilerplate. As of Java 16, a record does
* all of that in a single line:
*
* record Pair<T, U>(T first, U second) {}
*
* That's it. The compiler generates:
* - a constructor Pair(T first, U second)
* - accessor methods first() and second() (not getFirst/getSecond)
* - equals() that compares both components
* - hashCode() that combines both components
* - toString() that shows both components
*
* All final, all correct, all free. The rest of this file exists to explain
* what's happening under the hood and to show what you would still need to
* write by hand if you wanted to customize the behaviour.
*
* @author Ilkka Kokkarinen
*/
public final class Pair<T, U> {
// A generic class can have static members, but they cannot use the type
// parameters in any way (because of type erasure — at runtime, there is
// only one Pair class, not one per type argument combination).
private static int count = 0;
public static int getCount() { return count; }
// The components. Marked final — a Pair, like a record, is immutable.
private final T first;
private final U second;
public Pair(T first, U second) {
count++;
this.first = first;
this.second = second;
}
public T first() { return first; } // Record-style accessors (no "get" prefix).
public U second() { return second; }
// --- toString, equals, hashCode ---
// A record would generate all three of these automatically.
// We write them here so students can see what the compiler does for them.
@Override
public String toString() {
return "Pair[first=" + first + ", second=" + second + "]";
}
@Override
public boolean equals(Object other) {
// Java 16+ pattern matching for instanceof: tests the type AND binds
// a variable in one step. No separate cast needed.
if (other instanceof Pair<?, ?> that) {
// Objects.equals handles nulls safely — returns true if both are
// null, false if only one is, and delegates to .equals() otherwise.
return Objects.equals(this.first, that.first)
&& Objects.equals(this.second, that.second);
}
return false;
}
@Override
public int hashCode() {
// Objects.hash (Java 7+) computes a well-distributed combined hash code
// from any number of fields. It replaces the manual bit-shifting and
// XOR gymnastics that we used to write by hand. Under the hood, it
// uses Arrays.hashCode with a prime multiplier (31), which is the same
// algorithm that the compiler generates for records.
return Objects.hash(first, second);
}
// -----------------------------------------------------------------------
// THE RECORD ALTERNATIVE
// -----------------------------------------------------------------------
// If you don't need the static counter or any other customization, you
// can replace this entire 80-line class with:
//
// record SimplePair<T, U>(T first, U second) {}
//
// Let's define one and prove it works identically:
record SimplePair<T, U>(T first, U second) {}
// -----------------------------------------------------------------------
// CONVENIENCE FACTORY METHOD (Java 5+ style, popularized by Java 9 Map.of)
// -----------------------------------------------------------------------
// A static factory method that infers the type arguments, so callers can
// write Pair.of("hello", 42) instead of new Pair<String, Integer>("hello", 42).
public static <T, U> Pair<T, U> of(T first, U second) {
return new Pair<>(first, second);
}
// -----------------------------------------------------------------------
// DEMO: hash code quality test using War and Peace
// -----------------------------------------------------------------------
public static void main(String[] args) throws IOException {
Path path = Path.of(args.length > 0 ? args[0] : "warandpeace.txt");
if (!Files.exists(path)) {
// Create a sample file so the demo is self-contained.
Files.writeString(path, """
Prince Vasili came out first he could not restrain a triumphant smile
The little princess came after him she could not see for tears
The whole household was plunged in confusion and dismay
War and peace peace and war the eternal dance of nations
""".repeat(100)); // repeat to get a decent word count
}
// Count how many distinct hash codes we get from (word, index) pairs.
// The closer this is to the total word count, the better the hash function.
var seenHand = new HashSet<Integer>(); // hand-written Pair
var seenRecord = new HashSet<Integer>(); // record SimplePair
int wordNo = 0;
for (String line : Files.readAllLines(path)) {
for (String word : line.split("\\s+")) {
if (word.isEmpty()) continue;
seenHand.add(Pair.of(word, wordNo).hashCode());
seenRecord.add(new SimplePair<>(word, wordNo).hashCode());
wordNo++;
}
}
System.out.println("Total word occurrences: " + wordNo);
System.out.println("Distinct hash codes (hand-written): " + seenHand.size());
System.out.println("Distinct hash codes (record): " + seenRecord.size());
System.out.println("Pairs created (hand-written only): " + Pair.getCount());
// Demonstrate that the hand-written Pair and the record behave identically.
var p1 = Pair.of("hello", 42);
var p2 = Pair.of("hello", 42);
var p3 = Pair.of("world", 42);
var r1 = new SimplePair<>("hello", 42);
System.out.println("\np1 = " + p1);
System.out.println("r1 = " + r1);
System.out.println("p1.equals(p2): " + p1.equals(p2)); // true
System.out.println("p1.equals(p3): " + p1.equals(p3)); // false
System.out.println("p1.hashCode() == p2.hashCode(): " + (p1.hashCode() == p2.hashCode()));
// The record's toString looks almost identical:
// Pair[first=hello, second=42] vs SimplePair[first=hello, second=42]
}
}