JavaでUUID.compareToの挙動が思ってたのと違った
UUIDはv6,v7,v8とかで時間ベースのソートが出来るようになった。べんりー
なのでUUIDの上下関係が大事になったんだけどJavaのUUID.compareToはそこまで考慮されていなさそうだった。
たとえば2つのUUIDを用意してみる。
00000000-0000-7dda-8cc3-13f289aa2814
- 1970/01/01とかでつくったUUID
e677d1fe-e676-7224-be39-f88d1cd436d8
- 9999/12/31とかでつくったUUID
- こっちのほうが大きくなってほしい
まあ、見た目通り文字列で比較すればe677d1fe-e676-7224-be39-f88d1cd436d8
のほうが大きい。
ちなみに postgres で比較するとそのとおりの挙動になる
neko=> select ('00000000-0000-7dda-8cc3-13f289aa2814')::uuid < ('e677d1fe-e676-7224-be39-f88d1cd436d8')::uuid; ?column? ---------- t (1 row)
でも、Java(21)でUUIDオブジェクトとして比較すると逆になる
# jshell 2023ms 2024年02月22日 15時49分21秒 | Welcome to JShell -- Version 21.0.2 | For an introduction type: /help intro jshell> UUID max = UUID.fromString("e677d1fe-e676-7224-be39-f88d1cd436d8"); max ==> e677d1fe-e676-7224-be39-f88d1cd436d8 jshell> UUID min = UUID.fromString("00000000-0000-7dda-8cc3-13f289aa2814"); min ==> 00000000-0000-7dda-8cc3-13f289aa2814 jshell> max.compareTo(min) $3 ==> -1
ちなみに文字列にして比較すると(当たり前だけど)見た目通りの挙動になる。
jshell> max.toString().compareTo(min.toString()) $4 ==> 53
なぜかと言うとOpenJDKの実装では前半と後半にわけて(符号付きの)longとして保持している。
public UUID(long mostSigBits, long leastSigBits) { this.mostSigBits = mostSigBits; this.leastSigBits = leastSigBits; }
で、compareTo
も実装されていていて、最初に前半のlongで比較している
@Override public int compareTo(UUID val) { // The ordering is intentionally set up so that the UUIDs // can simply be numerically compared as two numbers int mostSigBits = Long.compare(this.mostSigBits, val.mostSigBits); return mostSigBits != 0 ? mostSigBits : Long.compare(this.leastSigBits, val.leastSigBits); }
で、このmostSigBits
の値が一定の大きさを超えたUUIDではマイナスになる(まぁ、それはそう)。
例えば上記のものは内部的には下記のような感じ
max = {UUID@3077} "e677d1fe-e676-7224-be39-f88d1cd436d8" mostSigBits = -1839771030039137756 leastSigBits = -4739483847872989480
ちなみに変換ロジックはこれ
public static UUID fromString(String name) { if (name.length() == 36) { char ch1 = name.charAt(8); char ch2 = name.charAt(13); char ch3 = name.charAt(18); char ch4 = name.charAt(23); if (ch1 == '-' && ch2 == '-' && ch3 == '-' && ch4 == '-') { long msb1 = parse4Nibbles(name, 0); long msb2 = parse4Nibbles(name, 4); long msb3 = parse4Nibbles(name, 9); long msb4 = parse4Nibbles(name, 14); long lsb1 = parse4Nibbles(name, 19); long lsb2 = parse4Nibbles(name, 24); long lsb3 = parse4Nibbles(name, 28); long lsb4 = parse4Nibbles(name, 32); if ((msb1 | msb2 | msb3 | msb4 | lsb1 | lsb2 | lsb3 | lsb4) >= 0) { return new UUID( msb1 << 48 | msb2 << 32 | msb3 << 16 | msb4, lsb1 << 48 | lsb2 << 32 | lsb3 << 16 | lsb4); } } } return fromString1(name); }
まぁ、Java のUUIDは4くらいまでしか対象にしていない気がするのでしょうがないような気がするし、これが仕様なのかバグなのかは人によって捉え方が違うと思う。
ただ、UUIDv7とかのロジックで生成したUUIDをJava上でオブジェクトとして比較すると順番が思ったのと違うことになるというお話でした。 僕はとりあえずtoString()して比較するようにします。