diff --git a/src/java.base/share/classes/java/util/UUID.java b/src/java.base/share/classes/java/util/UUID.java index d1c81badba6..fa66d199239 100644 --- a/src/java.base/share/classes/java/util/UUID.java +++ b/src/java.base/share/classes/java/util/UUID.java @@ -59,21 +59,21 @@ import jdk.internal.util.ByteArrayLittleEndian; * {@code UUID}. The bit layout described above is valid only for a {@code * UUID} with a variant value of 2, which indicates the Leach-Salz variant. * - *

The version field holds a value that describes the type of this {@code - * UUID}. There are four different basic types of UUIDs: time-based, DCE - * security, name-based, and randomly generated UUIDs. These types have a - * version value of 1, 2, 3 and 4, respectively. + *

See + * RFC 9562: Universally Unique Identifiers (UUIDs) for the complete specification, + * including the UUID format, layouts, and algorithms for creating {@code UUID}s. * - *

For more information including algorithms used to create {@code UUID}s, - * see RFC 4122: A - * Universally Unique IDentifier (UUID) URN Namespace, section 4.2 - * "Algorithms for Creating a Time-Based UUID". + *

There are eight defined types of UUIDs, each identified by a version number: + * time-based (version 1), DCE security (version 2), name-based with MD5 (version 3), + * randomly generated (version 4), name-based with SHA-1 (version 5), reordered time-based (version 6), + * Unix epoch time-based (version 7), and custom-defined layout (version 8). * - * @spec https://www.rfc-editor.org/info/rfc4122 - * RFC 4122: A Universally Unique IDentifier (UUID) URN Namespace + * @spec https://www.rfc-editor.org/rfc/rfc9562.html + * RFC 9562 Universally Unique IDentifiers (UUIDs) * @since 1.5 */ public final class UUID implements java.io.Serializable, Comparable { + /** * Explicit serialVersionUID for interoperability. */ @@ -178,6 +178,62 @@ public final class UUID implements java.io.Serializable, Comparable { return new UUID(md5Bytes); } + /** + * Creates a type 7 UUID (UUIDv7) {@code UUID} from the given Unix Epoch timestamp. + * + * The returned {@code UUID} will have the given {@code timestamp} in + * the first 6 bytes, followed by the version and variant bits representing {@code UUIDv7}, + * and the remaining bytes will contain random data from a cryptographically strong + * pseudo-random number generator. + * + * @apiNote {@code UUIDv7} values are created by allocating a Unix timestamp in milliseconds + * in the most significant 48 bits, allocating the required version (4 bits) and variant (2-bits) + * and filling the remaining 74 bits with random bits. As such, this method rejects {@code timestamp} + * values that do not fit into 48 bits. + *

+ * Monotonicity (each subsequent value being greater than the last) is a primary characteristic + * of {@code UUIDv7} values. This is due to the {@code timestamp} value being part of the {@code UUID}. + * Callers of this method that wish to generate monotonic {@code UUIDv7} values are expected to + * ensure that the given {@code timestamp} value is monotonic. + * + * + * @param timestamp the number of milliseconds since midnight 1 Jan 1970 UTC, + * leap seconds excluded. + * + * @return a {@code UUID} constructed using the given {@code timestamp} + * + * @throws IllegalArgumentException if the timestamp is negative or greater than {@code (1L << 48) - 1} + * + * @since 26 + */ + public static UUID ofEpochMillis(long timestamp) { + if ((timestamp >> 48) != 0) { + throw new IllegalArgumentException("Supplied timestamp: " + timestamp + "does not fit within 48 bits"); + } + + SecureRandom ng = Holder.numberGenerator; + byte[] randomBytes = new byte[16]; + ng.nextBytes(randomBytes); + + // Embed the timestamp into the first 6 bytes + randomBytes[0] = (byte)(timestamp >>> 40); + randomBytes[1] = (byte)(timestamp >>> 32); + randomBytes[2] = (byte)(timestamp >>> 24); + randomBytes[3] = (byte)(timestamp >>> 16); + randomBytes[4] = (byte)(timestamp >>> 8); + randomBytes[5] = (byte)(timestamp); + + // Set version to 7 + randomBytes[6] &= 0x0f; + randomBytes[6] |= 0x70; + + // Set variant to IETF + randomBytes[8] &= 0x3f; + randomBytes[8] |= (byte) 0x80; + + return new UUID(randomBytes); + } + private static final byte[] NIBBLES; static { byte[] ns = new byte[256]; @@ -320,6 +376,7 @@ public final class UUID implements java.io.Serializable, Comparable { *

  • 2 DCE security UUID *
  • 3 Name-based UUID *
  • 4 Randomly generated UUID + *
  • 7 Unix Epoch time-based UUID * * * @return The version number of this {@code UUID} @@ -336,16 +393,13 @@ public final class UUID implements java.io.Serializable, Comparable { * The variant number has the following meaning: * * * @return The variant number of this {@code UUID} - * - * @spec https://www.rfc-editor.org/info/rfc4122 - * RFC 4122: A Universally Unique IDentifier (UUID) URN Namespace */ public int variant() { // This field is composed of a varying number of bits. diff --git a/test/jdk/java/util/UUID/UUIDTest.java b/test/jdk/java/util/UUID/UUIDTest.java index 9fbd6dc1788..cb447a05656 100644 --- a/test/jdk/java/util/UUID/UUIDTest.java +++ b/test/jdk/java/util/UUID/UUIDTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -47,6 +47,7 @@ public class UUIDTest { randomUUIDTest(); randomUUIDTest_Multi(); nameUUIDFromBytesTest(); + testOfEpochMillisTimestamp(); stringTest(); versionTest(); variantTest(); @@ -146,6 +147,44 @@ public class UUIDTest { } } + private static void testOfEpochMillisTimestamp() { + // Should not throw for valid currentTimeMillis() timestamp + long timestamp = System.currentTimeMillis(); + try { + UUID u = UUID.ofEpochMillis(timestamp); + if (u == null) { + throw new AssertionError("Generated UUID should not be null for timestamp: " + timestamp); + } + } catch (Exception e) { + throw new AssertionError("Unexpected exception with timestamp " + timestamp, e); + } + + // Should not throw for the 48-bit long + long value = 0xFEDCBA987654L; + try { + UUID u = UUID.ofEpochMillis(value); + if (u == null) { + throw new AssertionError("Generated UUID should not be null for 48-bit long: " + value); + } + } catch (Exception e) { + throw new AssertionError("Unexpected exception with 48-bit long " + value, e); + } + + // Should throw for negative timestamp + value = -0xFEDCBA987654L; + try { + UUID.ofEpochMillis(value); + throw new AssertionError("Expected IllegalArgumentException with negative timestamp: " + value); + } catch (IllegalArgumentException expected) {} + + // Should throw for timestamp > 48 bits + value = 1L << 48; + try { + UUID.ofEpochMillis(value); + throw new AssertionError("Expected IllegalArgumentException with timestamp > 48 bits: " + value); + } catch (IllegalArgumentException expected) {} + } + private static void stringTest() throws Exception { for (int i = 0; i < COUNT; i++) { UUID u1 = UUID.randomUUID(); @@ -187,6 +226,15 @@ public class UUIDTest { throw new Exception("nameUUIDFromBytes not type 3: " + test); } + long timestamp = System.currentTimeMillis(); + test = UUID.ofEpochMillis(timestamp); + if (test.version() != 7) { + throw new Exception("ofEpochMillis not type 7: " + test); + } + if (test.variant() != 2) { + throw new Exception("ofEpochMillis not variant 2: " + test); + } + test = UUID.fromString("9835451d-e2e0-1e41-8a5a-be785f17dcda"); if (test.version() != 1) { throw new Exception("wrong version fromString 1");