Skip to content

JDK 17 源码阅读 - 数据类型 - 01 - Byte

🏷️ Java Java 17 JDK 17 源码阅读

JDK 版本

以下代码均基于 JDK 17 版本。

首先看一下 Byte 类的定义:

java
@jdk.internal.ValueBased
public final class Byte extends Number implements Comparable<Byte>, Constable {
}

Byte 继承了 Number 抽象类,另外还实现了 Comparable<Byte>Constable 接口。

Number 抽象类定义了将当前类型转换为几种基础数值类型的方法,其代码如下:

java
public abstract class Number implements java.io.Serializable {
    public Number() {super();}
    public abstract int intValue();
    public abstract long longValue();
    public abstract float floatValue();
    public abstract double doubleValue();
    public byte byteValue() { return (byte)intValue(); }
    public short shortValue() { return (short)intValue(); }
}

Byte 类中的实现为:

java
@IntrinsicCandidate
public byte byteValue() { return value; }
public short shortValue() { return (short)value; }
public int intValue() { return (int)value; }
public long longValue() { return (long)value; }
public float floatValue() { return (float)value; }
public double doubleValue() { return (double)value; }
@java.io.Serial
private static final long serialVersionUID = -7183698231559129828L;

Byte 类型是范围最小的数值类型,所以向上强转时不会有溢出的问题。另外 serialVersionUID 静态字段是序列化时用来标记当前类的版本号的。

Comparable 是一个泛型接口,仅仅包含一个方法,其代码如下:

java
public interface Comparable<T> {
    public int compareTo(T o);
}

这个接口虽然只有一个方法,但在 Java 中很多地方都会用到。这个方法不管 T 是什么类型,返回的始终是一个 int 类型的结果。当结果为正数时,表示当前对象比参数 o 大,为负数时表示当前对象比参数 o 小,为零时表示两者相等。

compareTo 方法的实现需要满足如下约定:

  1. x.compareTo(y) == -y.compareTo(x)
  2. x.compareTo(y) > 0y.compareTo(z) > 0,则 x.compareTo(z) > 0
  3. x.compareTo(y) == 0,则 x.compareTo(z) 的结果和 y.compareTo(z) 的结果相同

Byte 类中的实现为:

java
public int compareTo(Byte anotherByte) {
    return compare(this.value, anotherByte.value);
}
public static int compare(byte x, byte y) {
    return x - y;
}

Constable 是 Java 12 引入的,表示这是一种可常量化的类型,其实例可以名义上描述自身为一个 ConstantDesc

java
public interface Constable {
    Optional<? extends ConstantDesc> describeConstable();
}

Byte 类中的实现为:

java
@Override
public Optional<DynamicConstantDesc<Byte>> describeConstable() {
    return Optional.of(DynamicConstantDesc.ofNamed(BSM_EXPLICIT_CAST,
        DEFAULT_NAME, CD_byte, intValue()));
}

上面代码中的三个常量都定义在 ConstantDescs 类下:

java
public static final DirectMethodHandleDesc BSM_EXPLICIT_CAST
        = ofConstantBootstrap(CD_ConstantBootstraps, "explicitCast",
        CD_Object, CD_Object);
public static final String DEFAULT_NAME = "_";
public static final ClassDesc CD_byte = ClassDesc.ofDescriptor("B");

虽然 Byte 类型的定义代码上没有显式继承 Object 类,但在 Java 中所有引用类型都是 Object 类型的子类。接下来是 Byte 类重写的继承自 Object 类的 hashCodeequalstoString 方法:

java
@Override
public int hashCode() { return Byte.hashCode(value); }
public static int hashCode(byte value) { return (int)value; }

public boolean equals(Object obj) {
    // 顺便提一下,这里可以使用新的 instanceof 语法优化一下写法
    // if (obj instanceof Byte b) {
    //     return value == b.byteValue();
    // }
    if (obj instanceof Byte) {
        return value == ((Byte)obj).byteValue();
    }
    return false;
}

public String toString() { return Integer.toString((int)value); }

其中 hashCode 方法需要满足以下约定:

  1. 在 Java 应用程序的一次执行过程中,在对同一个对象上多次调用 hashCode 方法时,只要该对象的 equals 比较中使用的信息没有被修改,它就必须始终返回相同的整数。这个整数不需要在同一个应用程序的不同执行之间保持一致。

    这条约定可以联想到 Object.hashCode() 方法。这是个原生方法,在 JVM 实现中其结果是基于对象的地址计算出来的,正好满足这个约定。
    当然,Byte 类的 hashCode 方法中直接返回了 value,因此也满足这个约定。

  2. 如果根据 equals 方法两个对象相等,那么对这两个对象中调用 hashCode 方法必须产生相同的整数结果。

  3. 如果根据 equals 方法两个对象不相等,并不要求对这两个对象中的 hashCode 方法必须产生不同的整数结果。然而,程序员应该意识到,对于不相等的对象产生不同的整数结果可能会提高哈希表的性能。

equals 方法则需要满足以下约定:

  1. 自反:对于任何非空引用值 xx.equals(x) 应该返回 true
  2. 对称:对于任何非空引用值 xy,如果 x.equals(y) 返回 true,那么 y.equals(x) 也应该返回 true
  3. 传递:对于任何非空引用值 xyz,如果 x.equals(y) 返回 truey.equals(z) 返回 true,那么 x.equals(z) 应该返回 true
  4. 一致:对于任何非空引用值 xy,只要在对象的 equals 比较中使用的信息没有被修改,多次调用 x.equals(y) 应该始终返回 true 或者始终返回 false
  5. 对于任何非空引用值 xx.equals(null) 应该返回 false

可以看出 equals 方法和 hashCode 方法的约定有很多类似和关联的地方。hashCode 常用来进行离散,以方便存储和查找,但是 hashCode 可能会产生冲突,因此在比较时仍然需要使用 equals 方法进行判断,不过在调用 equals 方法前先比较 hashCode 可以极大的提高比较效率。这也就是为什么 hashCode() 方法会有第 2 和 第 3 条约定的原因。因此,通常情况下,如果重写了 equals 方法,则必须也要重写 hashCode 方法,以同时满足这些约定,否则可能会导致一些意想不到的问题。

Byte 类内部创建了一个嵌套类 ByteCache,这个类的作用是缓存所有的 Byte 对象实例,其代码如下:

java
private static class ByteCache {
    private ByteCache() {}

    static final Byte[] cache;
    static Byte[] archivedCache;

    static {
        final int size = -(-128) + 127 + 1;

        // Load and use the archived cache if it exists
        CDS.initializeFromArchive(ByteCache.class);
        if (archivedCache == null || archivedCache.length != size) {
            Byte[] c = new Byte[size];
            byte value = (byte)-128;
            for(int i = 0; i < size; i++) {
                c[i] = new Byte(value++);
            }
            archivedCache = c;
        }
        cache = archivedCache;
    }
}

其中第 11 行代码的处理是优先从 CDS(Class Data Sharing)中加载缓存数据。

初始化 cache 字段的处理也比较有意思,用了 3 个 Byte[] 数组变量,分别是 ccachearchivedCache。其中 archivedCache 字段是 JVM 中指定的用来加载缓存的字段。从如下的 JVM 源码可以看到几个基本数据类型对应的包装类使用的缓存都是这个字段名。

cpp
static ArchivableStaticFieldInfo closed_archive_subgraph_entry_fields[] = {
  {"java/lang/Integer$IntegerCache",              "archivedCache"},
  {"java/lang/Long$LongCache",                    "archivedCache"},
  {"java/lang/Byte$ByteCache",                    "archivedCache"},
  {"java/lang/Short$ShortCache",                  "archivedCache"},
  {"java/lang/Character$CharacterCache",          "archivedCache"},
  {"java/util/jar/Attributes$Name",               "KNOWN_NAMES"},
  {"sun/util/locale/BaseLocale",                  "constantBaseLocales"},
};

另外的 c 变量是在 archivedCache 没有值时创建的。为什么不直接将 archivedCache 初始化为 new Byte[size] 然后在对其赋值呢?虽然这里两种写法可能没什么区别,但还是推荐使用这种防御性写法。万一初始化的代码同时有两个线程在执行,那么当前的写法可以大概率地避免未初始化完成的 archivedCache 被访问(由于 archivedCache 字段没有使用 volatile 修饰,仍然有可能出问题,不过这里是静态类的静态代码块,理论上不会出现并发执行的情况)。

下面的 valueOf 方法就是基于 ByteCache 实现的,这样通过这种方法不管创建多少个 Byte 变量都不需要创建新的 Byte 对象实例。

java
@IntrinsicCandidate
public static Byte valueOf(byte b) {
    final int offset = 128;
    return ByteCache.cache[(int)b + offset];
}
public static Byte valueOf(String s, int radix)
    throws NumberFormatException {
    return valueOf(parseByte(s, radix));
}
public static Byte valueOf(String s) throws NumberFormatException {
    return valueOf(s, 10);
}

第二个 valueOf 方法的重载调用了 parseByte 转型方法,Byteparse 方法都是基于 Integer 类型的对应方法实现的,先转换为整型,然后再转换为 byte 型。当转换的整型结果超出 byte 范围时,也会抛出 NumberFormatException 异常,Integer.parseInt 方法本身也有可能抛出这个异常。

java
public static byte parseByte(String s, int radix)
    throws NumberFormatException {
    int i = Integer.parseInt(s, radix);
    if (i < MIN_VALUE || i > MAX_VALUE)
        throw new NumberFormatException(
            "Value out of range. Value:\"" + s + "\" Radix:" + radix);
    return (byte)i;
}

public static byte parseByte(String s) throws NumberFormatException {
    return parseByte(s, 10);
}

这里用到了两个常量 MIN_VALUEMAX_VALUE,分别表示 Byte 类型支持的最小值和最大值。另外几个数值类型 ShortIntegerLong 也有相同名称的常量,分别表示其各自可以允许的最小值和最大值。

java
public static final byte   MIN_VALUE = -128;
public static final byte   MAX_VALUE = 127;

decode 方法支持将十进制、十六进制和八进制的数字字符串转换为 Byte 类型。仍然是基于 Integer 的同名方法实现的。

该方法支持如下前缀:

  • 0x 十六进制
  • 0X 十六进制
  • # 十六进制
  • 0 八进制
java
public static Byte decode(String nm) throws NumberFormatException {
    int i = Integer.decode(nm);
    if (i < MIN_VALUE || i > MAX_VALUE)
        throw new NumberFormatException(
                "Value " + i + " out of range from input " + nm);
    return valueOf((byte)i);
}

这个是 Byte 类用来保存值的 value 字段。这是一个 final 字段,表示其值初始化后不能再被修改。

java
private final byte value;

下面是两个构造函数。通过构造函数创建的 Byte 对象和通过 Byte.valueOf() 方法获取的对象,即使值是一样的,也不是同一个对象。

java
@Deprecated(since="9", forRemoval = true)
public Byte(byte value) {
    this.value = value;
}
@Deprecated(since="9", forRemoval = true)
public Byte(String s) throws NumberFormatException {
    this.value = parseByte(s, 10);
}

下面的几个方法是关于无符号数的,由于 Byte 本身是有符号的,转成有符号数时就可能会溢出,所以只提供了转整形和长整型的方法。

java
public static int compareUnsigned(byte x, byte y) {
    return Byte.toUnsignedInt(x) - Byte.toUnsignedInt(y);
}
public static int toUnsignedInt(byte x) {
    return ((int) x) & 0xff;
}
public static long toUnsignedLong(byte x) {
    return ((long) x) & 0xffL;
}

至于转换的算法,可以说是相当简洁了,只是将 byte 变量转成整形或长整型,然后和 0xff 做按位与运算。

第一步的强制转换的作用是提升数据的位数,这里隐藏的处理是:

  • 如果是正数,前补的二进制都是 0;
  • 如果是负数,前补的二进制都是 1;

第二步的 & 运算就是将超过 byte 长度的二进制位(包括符号位)置零。

这里以 -1 为例说明一下其流程:

  1. byte -1 的 二进制是 1111 1111
  2. 转换为 int 型后的二进制是 1111 1111 1111 1111 1111 1111 1111 1111
  3. & 0xff 的结果为 0000 0000 0000 0000 0000 0000 1111 1111

简单来说就相当于直接将 -1 的二进制值 1111 1111 以忽略符号位的方式直接读取,最终结果就是 255。

顺便提一下为什么 -1 的二进制是 1111 1111,这关系到计算机中负数的存储和负数的计算。

在计算机中负数是以 补码 的形式存储的,而负数的补码是 反码 加一,负数的反码则是 原码 除符号位外按位取反。

十进制数原码反码补码
10000 00010000 00010000 0001
-11000 00011111 11101111 1111
-21000 00101111 11011111 1110

1+(2)=1 为例展示一下使用补码进行负数计算的过程。

000000011+111111102111111111

虽然比较反常识,但一般 CPU 中是只有加法器没有减法器的,计算机科学家们以这种巧妙的将减法运算转换为加法运算的方式实现了减法器的功能。

最后是 Byte 类提供的几个常量,SIZE 表示变量需要的 bit 数,BYTES 表示变量需要的字节数、TYPE 是用来获取其对应的基本类型的 Class

java
public static final int SIZE = 8;
public static final int BYTES = SIZE / Byte.SIZE;
@SuppressWarnings("unchecked")
public static final Class<Short>    TYPE = (Class<Short>) Class.getPrimitiveClass("short");

TYPE 常量的用法可以参考一下 Class 类的 isPrimitive() 方法,这个方法可以判断一个类是否是基本类型。只有包装类的 TYPE 常量的 isPrimitive() 才是 true

java
Integer.TYPE.isPrimitive(); // true
Integer.valueOf(1).getClass().isPrimitive(); // false

下面是判断 Class 是否是基础类型对应的包装类的方法:

java
try {
    return ((Class) clz.getField("TYPE").get(null)).isPrimitive();
} catch (Exception e) {
    return false;
}

以上就是关于 Byte 类源码阅读的全部内容了。