这是《水煮 JDK 源码》系列 的第 11 篇文章,计划撰写100篇关于JDK源码相关的文章
UUID
的全称是 universally unique identifier
,表示通用唯一标识符,UUID
类位于 java.util
包下,自 JDK 1.5
版本新增的,它是一个 final
类,不能被继承,在平常的开发中,通常会使用 UUID
类来生成唯一的标识符,比如下面的代码:
public static void main(String[] args) {
System.out.println(UUID.randomUUID());
}
运行后,输出的结果如下:
981cd0fb-91cf-4d55-ba60-377806e6051a
上面的结果中显示的就是默认的 UUID
字符串,它是由数字、字母和 - 组成,其中数字和字母共占32个字符,- 占4个字符,总共36个字符,当我们运行上面的程序时,实际上会调用 UUID
哪些方法呢?具体如下:
UUID.randomUUID() > new UUID() > toString()
那么 UUID
是如何生成的呢?32个字符又分别代表什么含义呢?下面通过具体的源码来了解一下。
1、UUID 类定义
UUID
类实现了 Serializable
和 Comparable
接口,其定义如下:
public final class UUID implements java.io.Serializable, Comparable<UUID> {
...
}
Serializable
:它是一个标记接口,没有任何方法定义,用于对象的序列化;Comparable
:该接口只有一个compareTo
方法,通常用于对实现它的类的对象进行比较大小;
2、成员变量
UUID
类中定义了2个成员变量,分别代表最高有效64位和最低有效64位,如下:
/** 最高有效64位 */
private final long mostSigBits;
/** 最低有效64位 */
private final long leastSigBits;
mostSigBits
和 leastSigBits
它们是构成 UUID
标识符的重要组成部分。
3、构造函数
UUID
类提供了 2 个构造函数,其定义如下:
/** 私有的构造方法 */
private UUID(byte[] data) {
long msb = 0;
long lsb = 0;
// 通过 assert 断言来判定参数 data 长度是否为 16
assert data.length == 16 : "data must be 16 bytes in length";
for (int i=0; i<8; i++)
msb = (msb << 8) | (data[i] & 0xff);
for (int i=8; i<16; i++)
lsb = (lsb << 8) | (data[i] & 0xff);
this.mostSigBits = msb;
this.leastSigBits = lsb;
}
public UUID(long mostSigBits, long leastSigBits) {
this.mostSigBits = mostSigBits;
this.leastSigBits = leastSigBits;
}
在构造函数中,主要是给成员变量 mostSigBits
和 leastSigBits
赋值,虽然 UUID
提供了 public
构造函数,但是在平时开发中,可能很少直接通过构造函数来创建 UUID
对象,更多的是使用 randomUUID()
方法,下面通过断点方式来看一下这个私有构造函数计算出的 mostSigBits
和 leastSigBits
具体值是多少,如下:
::: hljs-center
:::
4、方法
UUID
类中的方法主要分为静态方法和实例方法,其中静态方法主要用于创建 UUID
实例的,而实例方法主要用于获取 UUID
中的一些基本信息,比如版本号、时间戳、时钟序列等。
4.1 静态方法 - randomUUID()
public static UUID randomUUID() {
// 通过内部静态类 Holder 获取 SecureRandom 对象
SecureRandom ng = Holder.numberGenerator;
// 定义一个长度为 16 的字节数组
byte[] randomBytes = new byte[16];
// 通过 SecureRandom.nextBytes() 方法随机生成 16 个字节数字,并填充至数组中
ng.nextBytes(randomBytes);
// 索引为 6 的字节数表示的是 UUID 的版本号
// 首先清除版本号,然后再设置版本号为 4
randomBytes[6] &= 0x0f; /* clear version */
randomBytes[6] |= 0x40; /* set to version 4 */
// 索引为 8 的字节数表示的是 UUID 的变种编号
// 首先清除该编号,然后设置为 IETF 变种,其值 为 2
randomBytes[8] &= 0x3f; /* clear variant */
randomBytes[8] |= 0x80; /* set to IETF variant */
// 调用私有的构造函数创建 UUID 对象实例
return new UUID(randomBytes);
}
randomUUID()
方法可能是使用最多的一个方法,从实现可以看出,首先它通过内部静态类 Holder
获取了一个随机 SecureRandom
对象,主要用于产生随机数,Holder
类定义如下:
private static class Holder {
static final SecureRandom numberGenerator = new SecureRandom();
}
4.2 静态方法 - nameUUIDFromBytes()
public static UUID nameUUIDFromBytes(byte[] name) {
MessageDigest md;
try {
// 获取 MD5 信息摘要算法
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsae) {
throw new InternalError("MD5 not supported", nsae);
}
// 通过 MD5算法 对传入的 name[] 字节数组进行转换
// 转换后得到长度为 16 的 md5Bytes 新数组
byte[] md5Bytes = md.digest(name);
// 设置版本号为 3
md5Bytes[6] &= 0x0f; /* clear version */
md5Bytes[6] |= 0x30; /* set to version 3 */
md5Bytes[8] &= 0x3f; /* clear variant */
md5Bytes[8] |= 0x80; /* set to IETF variant */
return new UUID(md5Bytes);
}
nameUUIDFromBytes()
方法可以通过一个字节数组来创建 UUID
,首先会通过 MD5
摘要算法对字节数组进行加密转换,得到一个长度为 16 的新字节数组,然后使用新的字节数组构建 UUID
,该方法种会设置版本号为 3,而通过 randomUUID()
方法设置的版本号为 4.
4.3 静态方法 - fromString()
public static UUID fromString(String name) {
// 传入的 name 格式必须是和 UUID.toString() 得到的字符串一致
// 通过 - 对字符串进行分割,需要得到长度为 5 的字符串数组
String[] components = name.split("-");
if (components.length != 5)
throw new IllegalArgumentException("Invalid UUID string: "+name);
for (int i=0; i<5; i++)
components[i] = "0x"+components[i];
// 计算出最高有效64位的值
long mostSigBits = Long.decode(components[0]).longValue();
mostSigBits <<= 16;
mostSigBits |= Long.decode(components[1]).longValue();
mostSigBits <<= 16;
mostSigBits |= Long.decode(components[2]).longValue();
// 计算出最低有效64位的值
long leastSigBits = Long.decode(components[3]).longValue();
leastSigBits <<= 48;
leastSigBits |= Long.decode(components[4]).longValue();
return new UUID(mostSigBits, leastSigBits);
}
除了上面的可以通过字节数组构建 UUID
外,还可以直接通过字符串来构建,但是不是随便的字符串都可以,而是需要和 UUID.toString()
方法得到的字符串格式一致,也就是使用 -
进行分割时,必须得到长度为 5 的字符串数组,否则就会抛出 IllegalArgumentException
异常,然后根据分割的值来计算 mostSigBits
和 leastSigBits
的值,最后再通过 mostSigBits
和 leastSigBits
构建 UUID
对象。
4.4 实例方法
UUID
类提供的实例方法,主要有以下这些:
version()
: 获取当前UUID
的版本信息;variant()
:获取当前UUID
的变体编号;timestamp()
:获取当前UUID
的时间戳;clockSequence()
:获取当前UUID
的时钟序列值;node()
:获取当前UUID
的节点值;toString()
:将UUID
对象转换为字符串,通过这个方法就可以了解UUID
的构成;hashCode()
:获取当前UUID
的哈希值;equals(Object obj)
:用于比较两个UUID
对象是否相同;compareTo(UUID val)
:比较两个UUID
对象值的大小;
(1)基本信息方法
public int version() {
// Version is bits masked by 0x000000000000F000 in MS long
return (int)((mostSigBits >> 12) & 0x0f);
}
public int variant() {
// This field is composed of a varying number of bits.
// 0 - - Reserved for NCS backward compatibility
// 1 0 - The IETF aka Leach-Salz variant (used by this class)
// 1 1 0 Reserved, Microsoft backward compatibility
// 1 1 1 Reserved for future definition.
return (int) ((leastSigBits >>> (64 - (leastSigBits >>> 62)))
& (leastSigBits >> 63));
}
对于版本号和变体编号信息,是所有 UUID
通用的,而对于基于时间的 UUID
,则可以调用下面的 3 个方法获取时间相关的信息,如下:
public long timestamp() {
// 如果 version 版本号不为 1,则直接抛出异常
// 必须是以时间为基准的 UUID
if (version() != 1) {
throw new UnsupportedOperationException("Not a time-based UUID");
}
// 通过高位值进行转换获取时间戳
return (mostSigBits & 0x0FFFL) << 48
| ((mostSigBits >> 16) & 0x0FFFFL) << 32
| mostSigBits >>> 32;
}
public int clockSequence() {
// 如果 version 版本号不为 1,则直接抛出异常
// 必须是以时间为基准的 UUID
if (version() != 1) {
throw new UnsupportedOperationException("Not a time-based UUID");
}
// 通过低位值进行转换获取时钟序列值
return (int)((leastSigBits & 0x3FFF000000000000L) >>> 48);
}
public long node() {
// 如果 version 版本号不为 1,则直接抛出异常
// 必须是以时间为基准的 UUID
if (version() != 1) {
throw new UnsupportedOperationException("Not a time-based UUID");
}
// 通过低位值进行转换获取节点值
return leastSigBits & 0x0000FFFFFFFFFFFFL;
}
(2)toString() 方法
public String toString() {
return (digits(mostSigBits >> 32, 8) + "-" +
digits(mostSigBits >> 16, 4) + "-" +
digits(mostSigBits, 4) + "-" +
digits(leastSigBits >> 48, 4) + "-" +
digits(leastSigBits, 12));
}
通过 toString()
方法,可以得知 UUID
字符串的具体构成,主要是通过 mostSigBits
和 leastSigBits
进行变换得到的,digits()
方法定义如下:
private static String digits(long val, int digits) {
long hi = 1L << (digits * 4);
return Long.toHexString(hi | (val & (hi - 1))).substring(1);
}
UUID
的构成主要分为以下的几个:
* UUID = <time_low> "-" <time_mid> "-"
* <time_high_and_version> "-"
* <variant_and_sequence> "-"
* <node>
* time_low = 4*<hexOctet>
* time_mid = 2*<hexOctet>
* time_high_and_version = 2*<hexOctet>
* variant_and_sequence = 2*<hexOctet>
* node = 6*<hexOctet>
* hexOctet = <hexDigit><hexDigit>
* hexDigit =
* "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
* | "a" | "b" | "c" | "d" | "e" | "f"
* | "A" | "B" | "C" | "D" | "E" | "F"
通过上面的信息可以看出,UUID
由 时间低位
+ 时间中位
+ 时间高位和版本号
+ 变体编号和序列号
+ 节点值
五个部分组成的,可以看一个具体的值
033bd94b-1168-37e4-b0d6-44c3c95e35bf
(3)其他方法
public int hashCode() {
long hilo = mostSigBits ^ leastSigBits;
return ((int)(hilo >> 32)) ^ (int) hilo;
}
public boolean equals(Object obj) {
if ((null == obj) || (obj.getClass() != UUID.class))
return false;
UUID id = (UUID)obj;
// 比较两个 UUID 是否相同,主要比较 mostSigBits 和 leastSigBits 是否相等
return (mostSigBits == id.mostSigBits &&
leastSigBits == id.leastSigBits);
}
public int compareTo(UUID val) {
// 比较两个 UUID 的大小,也是比较 mostSigBits 和 leastSigBits 的大小
return (this.mostSigBits < val.mostSigBits ? -1 :
(this.mostSigBits > val.mostSigBits ? 1 :
(this.leastSigBits < val.leastSigBits ? -1 :
(this.leastSigBits > val.leastSigBits ? 1 :
0))));
}
对于比较两个 UUID
是否相同或者是否相等,都是直接比较 mostSigBits
和 leastSigBits
的值。
5、测试验证
下面通过 3 种不同的方式来创建 UUID
实例,同时输出基本信息。
package com.magic.test;
import java.util.UUID;
public class UUIDTest {
public static void main(String[] args) {
UUID uuid = UUID.randomUUID();
System.out.println(uuid);
System.out.println("version : " + uuid.version());
System.out.println("variant : " + uuid.variant());
UUID uuid1 = UUID.nameUUIDFromBytes("TEST".getBytes());
System.out.println(uuid1);
System.out.println("version : " + uuid1.version());
System.out.println("variant : " + uuid1.variant());
UUID uuid2 = UUID.fromString("033bd94b-1168-37e4-b0d6-44c3c95e35bf");
System.out.println(uuid2);
System.out.println("version : " + uuid2.version());
System.out.println("variant : " + uuid2.variant());
}
}
运行程序,输出信息如下:
d3f83827-8ccd-4ff7-8118-5155db6dbf4b
version : 4
variant : 2
033bd94b-1168-37e4-b0d6-44c3c95e35bf
version : 3
variant : 2
033bd94b-1168-37e4-b0d6-44c3c95e35bf
version : 3
variant : 2
如果直接对上面的创建的 UUID
调用时间相关方法,则会抛出 UnsupportedOperationException
异常,测试代码如下:
System.out.println(uuid2.timestamp());
System.out.println(uuid2.clockSequence());
System.out.println(uuid2.node());
输出的错误信息如下:
Exception in thread "main" java.lang.UnsupportedOperationException: Not a time-based UUID
at java.util.UUID.timestamp(UUID.java:293)
at com.magic.test.UUIDTest.main(UUIDTest.java:26)
通过之前的源码分析可以知道,timestamp()
、clockSequence()
、node()
方法必须是基于时间的 UUID
才能调用,基于时间的 UUID
的版本号为 1
,而上面的 uuid
、uuid1
、uuid2
都不是基于时间的,所以会抛出异常。