UUIDの比較とRFC 4122準拠に関する問題
問題の概要
UUID(Universally Unique Identifier)の比較において、Javaの標準実装がRFC 4122規格に完全に準拠していないという問題があります。RFC 4122では、UUIDを128ビットの符号なし整数として扱い、フィールドごとに符号なしで比較することが求められています。
しかし、JavaのUUID.compareTo()
メソッドは、内部的に符号付きlongとして比較を行うため、特定のケースで正しくない比較結果を返す可能性があります。
具体例
UUID uuid1 = UUID.fromString("b533260f-6479-4014-a007-818481bd98c6");
UUID uuid2 = UUID.fromString("131f0ada-6b6a-4e75-a6a0-4149958664e3");
System.out.println(uuid1.compareTo(uuid2) < 0); // true を返す(RFC的には false であるべき)
この場合、Javaの実装ではtrue
を返しますが、RFC 4122に従った比較ではfalse
を返すべきです。
RFC 4122の要求事項
RFC 4122では、UUIDの比較について次のように規定しています:
RFC 4122の比較ルール(引用)
字句的等価性のルール: UUIDの各フィールドをセクション4.1.2の表に示されている符号なし整数とみなす。次に、UUIDのペアを比較するには、対応するフィールドをデータ型に従い、重要度の順に算術比較する。2つのUUIDが等しいのは、すべての対応するフィールドが等しい場合に限る。
UUIDは、この文書で定義されているように、辞書的順序で並べることもできる。UUIDのペアについて、UUIDが異なる最も重要なフィールドで最初のUUIDの方が大きい場合、最初のUUIDは2番目のUUIDの後になる。UUIDが異なる最も重要なフィールドで2番目のUUIDの方が大きい場合、2番目のUUIDは最初のUUIDの前になる。
Java実装の問題点
JavaのUUID.compareTo()
メソッドは以下のように実装されています:
@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);
}
問題は、Long.compare()
が符号付きlongとして比較を行う点にあります。UUIDの最上位ビットが負の数として解釈される場合、数値比較が正しく行われません。
公式のスタンス
この問題は公式に認識されており、JDKのバグトラッキングシステムでJDK-7025832として報告されています。
互換性の問題
このバグは「won't fix」(修正しない)としてマークされています。これは、compareTo
メソッドの動作がJavaバージョン間で一貫していることが非常に重要であるためです。既存のコードとの後方互換性を維持するため、実装は変更されません。
解決策:RFC準拠の比較器の実装
RFC 4122に準拠した比較が必要な場合は、カスタムのComparator<UUID>
を実装することを推奨します。
Java 8以降の場合
Java 8で導入されたLong.compareUnsigned()
メソッドを使用すると、簡単に符号なし比較を実装できます。
Comparator<UUID> rfcComparator = (a, b) -> {
int mostSigBits = Long.compareUnsigned(
a.getMostSignificantBits(),
b.getMostSignificantBits()
);
return mostSigBits != 0 ? mostSigBits :
Long.compareUnsigned(
a.getLeastSignificantBits(),
b.getLeastSignificantBits()
);
};
Java 7以前の場合
Java 8以前のバージョンでは、以下のように符号なし比較を実装できます。
Comparator<UUID> rfcComparator = new Comparator<UUID>() {
@Override
public int compare(UUID a, UUID b) {
int cmp = compareUnsigned(
a.getMostSignificantBits(),
b.getMostSignificantBits()
);
return cmp != 0 ? cmp :
compareUnsigned(
a.getLeastSignificantBits(),
b.getLeastSignificantBits()
);
}
private int compareUnsigned(long a, long b) {
return Long.compare(a + Long.MIN_VALUE, b + Long.MIN_VALUE);
}
};
使用例
作成した比較器は、ソートやコレクションの操作で使用できます。
List<UUID> uuids = Arrays.asList(
UUID.fromString("b533260f-6479-4014-a007-818481bd98c6"),
UUID.fromString("131f0ada-6b6a-4e75-a6a0-4149958664e3")
);
// RFC準拠でソート
uuids.sort(rfcComparator);
// 個別の比較
int result = rfcComparator.compare(uuid1, uuid2);
まとめ
Javaの標準UUIDクラスはRFC 4122の比較規則に完全には準拠していませんが、これは意図的な設計判断によるものです。既存のコードベースとの互換性を維持するため、この動作は変更されません。
RFC 4122に準拠した比較が必要な場合は、Long.compareUnsigned()
メソッドを使用したカスタム比較器を実装することを推奨します。これにより、符号なし整数としての正しい比較が保証されます。
パフォーマンス考慮点
カスタム比較器を使用する場合、パフォーマンスへの影響はごくわずかです。Long.compareUnsigned()
はネイティブメソッドとして最適化されており、高頻度の比較操作でも効率的に動作します。