Skip to content

UUIDの比較とRFC 4122準拠に関する問題

問題の概要

UUID(Universally Unique Identifier)の比較において、Javaの標準実装がRFC 4122規格に完全に準拠していないという問題があります。RFC 4122では、UUIDを128ビットの符号なし整数として扱い、フィールドごとに符号なしで比較することが求められています。

しかし、JavaのUUID.compareTo()メソッドは、内部的に符号付きlongとして比較を行うため、特定のケースで正しくない比較結果を返す可能性があります。

具体例

java
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()メソッドは以下のように実装されています:

java
@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()メソッドを使用すると、簡単に符号なし比較を実装できます。

java
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以前のバージョンでは、以下のように符号なし比較を実装できます。

java
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);
    }
};

使用例

作成した比較器は、ソートやコレクションの操作で使用できます。

java
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()はネイティブメソッドとして最適化されており、高頻度の比較操作でも効率的に動作します。