Serguey Asael Shinder: A Set<byte[]> compares arrays by identity, so contains() misses equal bytes
A set of hashes or tokens stored as byte[] looks like it works in every test that reuses the same array object. It stops working the first time the bytes arrive in a fresh array.
Set<byte[]> seen = new HashSet<>();
seen.add(new byte[] {1, 2, 3});
boolean found = seen.contains(new byte[] {1, 2, 3}); // false
Why. Arrays do not override equals or hashCode; they inherit them from Object), whose equals is true only when both references point to the same object. Two arrays with identical contents are two objects, so HashSet, HashMap keys and List.contains all treat them as different. The content comparison exists, but as a static utility you have to call yourself: [Arrays.equals(byte[], byte[])](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Arrays.html#equals(byte%5B%5D,byte%5B%5D)) and [Arrays.hashCode(byte[])](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Arrays.html#hashCode(byte%5B%5D)).
The same applies to char[], int[] and every other array type — and to Map<String[], …> keys built from split strings.
Where it bites: de-duplicating messages by their SHA-256 digest, caching by a binary key, tracking seen nonces or tokens. The test passes because the test reuses one array; production computes a new digest each time, so every lookup misses, the set grows without bound, and the replay check never fires.
![Serguey Asael Shinder: A Set<byte[]> compares arrays by identity, so contains() misses equal bytes](/og/a-set-of-byte-arrays-compares-identity-not-content.png)
The fix: give the key a content-based equals.
ByteBuffer.wrap(bytes)—ByteBuffer.equals) compares the remaining elements, andhashCodedepends on them. Treat the wrapped array as immutable afterwards: changing it changes the key's hash while it sits in the set.- Convert to an immutable value —
HexFormat.of().formatHex(bytes)or Base64 gives aStringkey with correct semantics, at the cost of an extra allocation. - A small record that defines equality itself:
record Digest(byte[] value) {
Digest { value = value.clone(); }
@Override public boolean equals(Object o) {
return o instanceof Digest d && Arrays.equals(value, d.value);
}
@Override public int hashCode() { return Arrays.hashCode(value); }
}
A record does not fix this on its own: its generated equals compares components with their own equals, which for an array is still identity — so the overrides above are required.
A check that finds it. Search for Set<byte[]>, Map<byte[], and List<byte[]> used with contains, remove or indexOf. Each hit is either relying on identity on purpose — rare, and worth a comment — or a lookup that will miss.
No JDK runs on the machine this note was written on; the behaviour described is the one specified in the linked javadoc.