Skip to content

JDK 17 源码阅读 - 数据类型 - 03 - Integer

🏷️ Java JDK 17 源码阅读

JDK 版本

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

toString

相比之前的看的数据类型,Integer 类型首先是多了一段 char[] 的常量数组,这个数组包括了数值字符串所允许的所有数字(可以看到最多支持 36 进制的数字)。这个字段在将字符串转换成数字,或者反过来将数字转换为字符串时会使用到。

java
static final char[] digits = {
    '0' , '1' , '2' , '3' , '4' , '5' ,
    '6' , '7' , '8' , '9' , 'a' , 'b' ,
    'c' , 'd' , 'e' , 'f' , 'g' , 'h' ,
    'i' , 'j' , 'k' , 'l' , 'm' , 'n' ,
    'o' , 'p' , 'q' , 'r' , 's' , 't' ,
    'u' , 'v' , 'w' , 'x' , 'y' , 'z'
};

ByteShort 类型的 toString() 方法都是直接调用的 Integer.toString(int i) 方法,这里详细读一下其实现。

下面是 toString() 方法的具体实现。

java
public String toString() {
    return toString(value);
}
@IntrinsicCandidate
public static String toString(int i) {
    int size = stringSize(i);
    // 是否启用紧凑字符串(默认启用)
    if (COMPACT_STRINGS) {
        // 启用紧凑字符串时使用 LATIN1 编码,每个字符占用一个字节
        byte[] buf = new byte[size];
        getChars(i, size, buf);
        return new String(buf, LATIN1);
    } else {
        // 禁用紧凑字符串时使用 UTF16 编码,每个字符占用两个字节
        byte[] buf = new byte[size * 2];
        StringUTF16.getChars(i, size, buf);
        return new String(buf, UTF16);
    }
}

首先用到了 stringSize() 方法,这个方法的作用是计算一个数字的字符串长度。

java
static int stringSize(int x) {
    // d 变量记录符号位的长度
    int d = 1;
    if (x >= 0) {
        d = 0;
        // 将数值统一为负数进行处理
        x = -x;
    }
    // p 初始值是 -10,后面会每次乘以 10,用于比较。
    int p = -10;
    // i 变量表示数字部分的长度
    // 由于 integer 的最大值和最小值是 2147483647 和 -2147483648,
    // 其字符部分长度最大就是 10,所以这里最多循环 9 遍。
    for (int i = 1; i < 10; i++) {
        // 如果小于比较用的 p 的大小,则返回已统计的长度。
        if (x > p)
            return i + d;
        // 乘以 10,用于下一个循环的比较。
        p = 10 * p;
    }
    // 如果循环结束,则相当于 i = 10,直接返回 10 加上符号位的长度。
    return 10 + d;
}

之后使用了一个标志位常量 COMPACT_STRINGS,这个常量在 Integer 类中是静态的,默认值是 true,但是这个值可以在应用启动时通过 -XX:-CompactStrings 参数将其禁用。这个标志是在 JDK 9 中正式引入的,其目的是减少字符串的内存占用。默认启用。在 JDK 8 及之前,字符默认使用 UTF 16 存储,即一个字符占用两个字节,改成 LATIN1 后可以减少一半的内存占用。从 toString(int i) 的方法体中可以看到启用时 buf 数组的长度是 size,而禁用时则是 size * 2

java
static final boolean COMPACT_STRINGS;
static {
    COMPACT_STRINGS = true;
}

调试中禁用 CompactStrings

IDEA 中可以通过在 运行/调试配置 的 虚拟机选项 中添加 -XX:-CompactStrings 参数来禁用该标志位。

接下来是 getChars() 方法,这个方法的作用是将数字对应的数字依次写入字符数组。

java
static int getChars(int i, int index, byte[] buf) {
    int q, r;
    int charPos = index;
    // 记录符号位,并将数值统一为负数进行后续处理;
    boolean negative = i < 0;
    if (!negative) {
        i = -i;
    }

    // Generate two digits per iteration
    // 从左向右依次每次计算两位数字对应的 ASCII 码值,并写入到 `buf` 数组中;
    while (i <= -100) {
        q = i / 100;
        r = (q * 100) - i;
        i = q;
        buf[--charPos] = DigitOnes[r];
        buf[--charPos] = DigitTens[r];
    }
    // `> -100` 时退出循环,因为此时可能不足两位,所以不能使用循环中的每次两位的方法处理。 
    // We know there are at most two digits left at this point.
    // 先计算余下数字的个位;
    q = i / 10;
    r = (q * 10) - i;
    buf[--charPos] = (byte)('0' + r);

    // 如果有十位,则加上,否则跳过;
    // Whatever left is the remaining digit.
    if (q < 0) {
        buf[--charPos] = (byte)('0' - q);
    }
    // 最后加上可能有的符号位;
    if (negative) {
        buf[--charPos] = (byte)'-';
    }
    // 返回下标位置(这里应该始终为 0)。
    return charPos;
}

因为上述循环中每次处理两个数字,所以使用了两个 byte[] 常量,分别存储一个两位数十位和个位数字对应的字符的 ASCII 码值。至于为什么每次处理两位数字,可能是基于时间和空间的综合考量。

java
static final byte[] DigitTens = {
    '0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
    '1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
    '2', '2', '2', '2', '2', '2', '2', '2', '2', '2',
    '3', '3', '3', '3', '3', '3', '3', '3', '3', '3',
    '4', '4', '4', '4', '4', '4', '4', '4', '4', '4',
    '5', '5', '5', '5', '5', '5', '5', '5', '5', '5',
    '6', '6', '6', '6', '6', '6', '6', '6', '6', '6',
    '7', '7', '7', '7', '7', '7', '7', '7', '7', '7',
    '8', '8', '8', '8', '8', '8', '8', '8', '8', '8',
    '9', '9', '9', '9', '9', '9', '9', '9', '9', '9',
    } ;

static final byte[] DigitOnes = {
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    } ;

上面的 toString(int i) 方法是转换为十进制字符串,Integer 类还提供了一个重载的静态方法,可以指定进制。

java
public static String toString(int i, int radix) {
    // 这两个常量分别是 2 和 36,超出范围时默认转换为十进制字符串。
    if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
        radix = 10;

    /* Use the faster version */
    // 如果使用的是十进制,则直接调用 toString(int i) 方法
    if (radix == 10) {
        return toString(i);
    }
    // 这里同样是判断是否使用紧凑字符串,启用时使用 Latin1 编码
    if (COMPACT_STRINGS) {
        // 转换为二进制时字符串最长为 32 位,加上符号位,一共 33 个字节
        byte[] buf = new byte[33];
        boolean negative = (i < 0);
        // 从后往前填充字符,最大的下标为 32
        int charPos = 32;
        // 统一
        if (!negative) {
            i = -i;
        }
        // 这里是核心部分,首先计算余数,结果就是当前所在下标的数字,
        // 然后从 digits 常量数组获取对应的字符,
        // 然后对进制进行整除,循环直到商小于进制时结束
        while (i <= -radix) {
            buf[charPos--] = (byte)digits[-(i % radix)];
            i = i / radix;
        }
        // 最后将剩余的数字直接转换为字符
        buf[charPos] = (byte)digits[-i];
        // 如果是负数的话再加上符号位
        if (negative) {
            // 为了避免多执行一次 -- 操作,charPos 在这里才会减少 1
            buf[--charPos] = '-';
        }
        // 使用 Latin1 编码将字节数组转换为字符串
        return StringLatin1.newString(buf, charPos, (33 - charPos));
    }
    // 未启用紧凑字符串时使用 UTF16 编码
    return toStringUTF16(i, radix);
}

toStringUTF16 方法和上面的 toString(int i, int radix) 方法类似,只是编码方式不同。

java
private static String toStringUTF16(int i, int radix) {
    // 因为是 UTF16 编码,所以数组长度需要 * 2
    byte[] buf = new byte[33 * 2];
    boolean negative = (i < 0);
    int charPos = 32;
    if (!negative) {
        i = -i;
    }
    while (i <= -radix) {
        // 每个字符占 2 个字节
        StringUTF16.putChar(buf, charPos--, digits[-(i % radix)]);
        i = i / radix;
    }
    StringUTF16.putChar(buf, charPos, digits[-i]);

    if (negative) {
        StringUTF16.putChar(buf, --charPos, '-');
    }
    return StringUTF16.newString(buf, charPos, (33 - charPos));
}

这里可以顺便看一下 StringUTF16.putChar 方法的实现:

java
@IntrinsicCandidate
// intrinsic performs no bounds checks
static void putChar(byte[] val, int index, int c) {
    assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
    // 下标左移一位,相当于 * 2
    index <<= 1;
    val[index++] = (byte)(c >> HI_BYTE_SHIFT);
    val[index]   = (byte)(c >> LO_BYTE_SHIFT);
}

可以看到 index 下标做了 * 2 的操作,因为 UTF16 编码占 2 个字节,然后将高位和低位分别写入。HI_BYTE_SHIFTLO_BYTE_SHIFT 是两个常量,默认分别是 0 和 8,即高位偏移 0,低位偏移 8,也就是高低位是反过来的。以字符串 "7b" 为例,对应的 byte[] 应该为 [0, 55,0, 98],但实际上是 [55, 0, 98, 0]

java
// 默认为 false
private static native boolean isBigEndian();

static final int HI_BYTE_SHIFT;
static final int LO_BYTE_SHIFT;
static {
    if (isBigEndian()) {
        HI_BYTE_SHIFT = 8;
        LO_BYTE_SHIFT = 0;
    } else {
        HI_BYTE_SHIFT = 0;
        LO_BYTE_SHIFT = 8;
    }
}

toUnsignedString(int i, int radix) 的功能是生成 i 对应的无符号数对应的字符串。radix 是进制。主要功能是通过调用 Long 的同名方法实现的,以后再看。

java
public static String toUnsignedString(int i, int radix) {
    return Long.toUnsignedString(toUnsignedLong(i), radix);
}
public static long toUnsignedLong(int x) {
    return ((long) x) & 0xffffffffL;
}

toHexString(int i) 方法的功能是生成 i 对应的十六进制字符串,另外两个分别是八进制和二进制。

java
public static String toHexString(int i) {
    return toUnsignedString0(i, 4);
}
public static String toOctalString(int i) {
    return toUnsignedString0(i, 3);
}
public static String toBinaryString(int i) {
    return toUnsignedString0(i, 1);
}

toUnsignedString0(int val, int shift) 方法的 shift 参数表示的是几个 bit 转换为一个 char,这里是 16 进制,所以 shift 为 4。相应的,如果是 8 进制的话,shift 为 3,4 进制的话 shift 为 2,2 进制的话 shift 为 1。

java
private static String toUnsignedString0(int val, int shift) {
    // assert shift > 0 && shift <=5 : "Illegal shift value";
    // 根据前导 0 的个数,计算最终需要生成的字符串长度
    int mag = Integer.SIZE - Integer.numberOfLeadingZeros(val);
    int chars = Math.max(((mag + (shift - 1)) / shift), 1);
    // 根据是否启用紧凑字符串,选择不同的实现
    if (COMPACT_STRINGS) {
        // 根据计算出的长度初始化字节数组
        byte[] buf = new byte[chars];
        // 计算对应的字符并写入字节数组
        formatUnsignedInt(val, shift, buf, chars);
        // 使用 Latin1 编码将字节数组转换为字符串
        return new String(buf, LATIN1);
    } else {
        // 根据计算出的长度初始化字节数组(这里是 2 倍的长度)
        byte[] buf = new byte[chars * 2];
        // 计算对应的字符并写入字节数组
        formatUnsignedIntUTF16(val, shift, buf, chars);
        // 使用 UTF16 编码将字节数组转换为字符串
        return new String(buf, UTF16);
    }
}

numberOfLeadingZeros 方法是用来计算 i 的二进制表示中前导 0 的个数,算法相当巧妙。

java
@IntrinsicCandidate
public static int numberOfLeadingZeros(int i) {
    // HD, Count leading 0's
    if (i <= 0)
        // 0 的二进制表示是 32 个 0
        // 负数时因为符号位是 1,所以没有前导 0
        return i == 0 ? 32 : 0;
    // 此时最多 31 个前导 0
    int n = 31;
    // 1 << 16 表示 2^16 = 65536,此时高位的 16 位中至少有一个 1。
    // 所以 n 减去 16,并将高位的 16 位移到低位(以使后续的算法在相同的位上操作)。
    if (i >= 1 << 16) { n -= 16; i >>>= 16; }
    // 后面的几步和上面类似,依次判断高位的 8 位、4 位、2 位
    if (i >= 1 <<  8) { n -=  8; i >>>=  8; }
    if (i >= 1 <<  4) { n -=  4; i >>>=  4; }
    if (i >= 1 <<  2) { n -=  2; i >>>=  2; }
    // 最后一步相当于判断最后 2 位中的高位的 1 位是否为 1
    return n - (i >>> 1);
}

下面这两个写入字节数组的方法的逻辑基本类似,区别就是每个字符占用的字节数不一样。

java
private static void formatUnsignedInt(int val, int shift, byte[] buf, int len) {
    // 字节下标默认为字符串长度
    int charPos = len;
    // 根据 shift 计算对应的进制,shift 为 4 时,进制为 16
    int radix = 1 << shift;
    // 用来做与运算的掩码,16 进制时掩码为 15,对应的二进制为 1111
    int mask = radix - 1;
    do {
        // 因为 charPos 默认为字符串长度,所以需要先减 1
        // val & mask 用来获取低 shift 位的数字值
        // 然后从 digits 数组常量中获取对应的字符
        buf[--charPos] = (byte)Integer.digits[val & mask];
        // 右移 shift 位以进行下一个循环
        val >>>= shift;
    } while (charPos > 0);
}
private static void formatUnsignedIntUTF16(int val, int shift, byte[] buf, int len) {
    int charPos = len;
    int radix = 1 << shift;
    int mask = radix - 1;
    do {
        // 算法和 formatUnsignedInt 基本一样,只有这里写入字符的方式不同
        StringUTF16.putChar(buf, --charPos, Integer.digits[val & mask]);
        val >>>= shift;
    } while (charPos > 0);
}

parseInt

Byte.parseByteShort.parseShort 方法实际上都是调用 Integer.parseInt 方法实现的。这个方法有些长,其中大部分都比较好理解,主要是循环转换时有两个溢出判断有点不太容易看懂。

java
public static int parseInt(String s, int radix)
            throws NumberFormatException
{
    /*
        * WARNING: This method may be invoked early during VM initialization
        * before IntegerCache is initialized. Care must be taken to not use
        * the valueOf method.
        */
    // 判断字符串是否为空
    if (s == null) {
        throw new NumberFormatException("Cannot parse null string");
    }
    // 判断进制是否小于支持的最小进制
    if (radix < Character.MIN_RADIX) {
        throw new NumberFormatException("radix " + radix +
                                        " less than Character.MIN_RADIX");
    }
    // 判断进制是否大于支持的最大进制
    if (radix > Character.MAX_RADIX) {
        throw new NumberFormatException("radix " + radix +
                                        " greater than Character.MAX_RADIX");
    }

    boolean negative = false;
    int i = 0, len = s.length();
    // 统一为负数进行后续运算,这里默认先设置为正数最大值的负值
    int limit = -Integer.MAX_VALUE;

    if (len > 0) {
        // 校验首字符
        char firstChar = s.charAt(0);
        if (firstChar < '0') { // Possible leading "+" or "-"
            if (firstChar == '-') {
                negative = true;
                // 负数时,其 limit 比正数的要小 1,所以要重新设置
                limit = Integer.MIN_VALUE;
            } else if (firstChar != '+') {
                // 既不是 '-' 也不是 '+' 时,则抛出异常
                throw NumberFormatException.forInputString(s, radix);
            }
            // 不能只有 "+" 或 "-"
            if (len == 1) { // Cannot have lone "+" or "-"
                throw NumberFormatException.forInputString(s, radix);
            }
            // 如果是首字符是 "+" 或 "-" 则跳过首字符
            i++;
        }
        // 这个变量是用来判断已经转换的结果在整体进一位后其结果是否溢出,
        // 所以这里需要除以进制。
        int multmin = limit / radix;
        // 转换结果变量初始化为 0
        int result = 0;
        while (i < len) {
            // Accumulating negatively avoids surprises near MAX_VALUE
            // 这里是调用 Character.digit 方法将当前下标位置的单个字符转换为对应进制的数字
            int digit = Character.digit(s.charAt(i++), radix);
            // Character.digit 转换字符时,如果不是当前进制有效的数字,则返回 -1。
            // digit >= 0 时表示当前字符是有效的,此时已经转换的部分需要整体进一位,
            // result < multmin 就是判断在整体进位后其结果是否会溢出。
            if (digit < 0 || result < multmin) {
                throw NumberFormatException.forInputString(s, radix);
            }
            // 将已经转换的部分乘以进制,相当于当前进制左移了一位,右补零。
            result *= radix;
            // 判断再加上本次循环转换的数字后其结果是否会溢出
            if (result < limit + digit) {
                throw NumberFormatException.forInputString(s, radix);
            }
            // 将当前循环转换的数字加到最低位,
            // 因为统一为负数进行运算,所以执行的是减法操作。
            result -= digit;
        }
        // 根据正负标志返回对应的结果
        return negative ? result : -result;
    } else {
        // 字符串为空时
        throw NumberFormatException.forInputString(s, radix);
    }
}

其余的几个 parseInt 方法的重载和几个 parseUnsignedInt 方法的实现要么大同小异,要么就是调用的 Integer.parseInt 方法,其中 parseUnsignedInt 方法由于可能会超过 Integer.MAX_VALUE,此时会调用 Long.parseLong 方法。

另外两个 valueOf 方法的重载也是通过 parseInt 方法实现的。

java
public static Integer valueOf(String s, int radix) throws NumberFormatException {
    return Integer.valueOf(parseInt(s,radix));
}
    public static Integer valueOf(String s) throws NumberFormatException {
    return Integer.valueOf(parseInt(s, 10));
}

另外 valueOf 方法也是通过对应的 IntegerCache 内部类实现的,和 ShortCache 类似,默认情况也只缓存 -128 到 127 一共 256 个值。不过 IntegerCache 提供了一个 VM 属性 java.lang.Integer.IntegerCache.high 可以自定义缓存上限的数值。可以在启动命令中指定 -Djava.lang.Integer.IntegerCache.high=255 参数来修改,IDEA 中则是在 运行/调试配置 的 虚拟机选项 中添加这个参数。最多允许缓存 Integer.MAX_VALUE 个元素,也就是说这个值最大为 Integer.MAX_VALUE - 129

java
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer[] cache;
    static Integer[] archivedCache;

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                h = Math.max(parseInt(integerCacheHighPropValue), 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        // Load IntegerCache.archivedCache from archive, if possible
        CDS.initializeFromArchive(IntegerCache.class);
        int size = (high - low) + 1;

        // Use the archived cache if it exists and is large enough
        if (archivedCache == null || size > archivedCache.length) {
            Integer[] c = new Integer[size];
            int j = low;
            for(int i = 0; i < c.length; i++) {
                c[i] = new Integer(j++);
            }
            archivedCache = c;
        }
        cache = archivedCache;
        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

byteValue & shortValue

下面这两个强制转换类型的方法有可能由于数据截断导致数值发生变化,使用时需要注意。

java
public byte byteValue() {
    return (byte)value;
}
public short shortValue() {
    return (short)value;
}

getInteger

Integer 类提供了几个用来获取整数型值的系统属性的方法:

java
public static Integer getInteger(String nm) {
    return getInteger(nm, null);
}
public static Integer getInteger(String nm, int val) {
    Integer result = getInteger(nm, null);
    return (result == null) ? Integer.valueOf(val) : result;
}
public static Integer getInteger(String nm, Integer val) {
    String v = null;
    try {
        // 获取系统属性
        v = System.getProperty(nm);
    } catch (IllegalArgumentException | NullPointerException e) {
    }
    if (v != null) {
        try {
            // 将获取到的系统属性值解码为整数
            return Integer.decode(v);
        } catch (NumberFormatException e) {
        }
    }
    return val;
}

使用示例:

java
// 在 VM options 中指定参数:-Dme.iujiajia.hello.year=2024
System.out.println(Integer.getInteger("me.iujiajia.hello.year")); // 2024

decode

上面的 getInteger 方法转型为 Integer 时使用的是 decode 方法,这个方法支持十六进制、八进制和十进制的数字字面量字符串。

  • Ox0X# 代表十六进制;
  • 0 代表八进制;
  • 其余都是十进制。

根据前缀解析出字符串所使用的进制后,再调用 parseInt 方法进行转换。需要注意的是值为 Integer.MIN_VALUE 时的特殊处理。

java
public static Integer decode(String nm) throws NumberFormatException {
    int radix = 10;
    int index = 0;
    boolean negative = false;
    Integer result;

    if (nm.isEmpty())
        throw new NumberFormatException("Zero length string");
    char firstChar = nm.charAt(0);
    // Handle sign, if present
    if (firstChar == '-') {
        negative = true;
        index++;
    } else if (firstChar == '+')
        index++;

    // Handle radix specifier, if present
    if (nm.startsWith("0x", index) || nm.startsWith("0X", index)) {
        index += 2;
        radix = 16;
    }
    else if (nm.startsWith("#", index)) {
        index ++;
        radix = 16;
    }
    else if (nm.startsWith("0", index) && nm.length() > 1 + index) {
        index ++;
        radix = 8;
    }

    if (nm.startsWith("-", index) || nm.startsWith("+", index))
        throw new NumberFormatException("Sign character in wrong position");

    try {
        // 这里仅将去除了符号位后的部分传给了 Integer.valueOf 方法
        // 如果是 Integer.MIN_VALUE, 这里就会由于超出 Integer 的范围而抛出异常
        result = Integer.valueOf(nm.substring(index), radix);
        result = negative ? Integer.valueOf(-result.intValue()) : result;
    } catch (NumberFormatException e) {
        // If number is Integer.MIN_VALUE, we'll end up here. The next line
        // handles this case, and causes any genuine format error to be
        // rethrown.
        // 这里就是为了捕捉 Integer.MIN_VALUE 时的异常
        // 注意这里的拼接,这里相当于移除了表示进制的符号位之后的字符串
        String constant = negative ? ("-" + nm.substring(index))
                                    : nm.substring(index);
        result = Integer.valueOf(constant, radix);
    }
    return result;
}

Integer 类还提供了两个方法用于无符号整数运算: divideUnsignedremainderUnsigned,分别是无符号整数除法和求余。

java
public static int divideUnsigned(int dividend, int divisor) {
    // In lieu of tricky code, for now just use long arithmetic.
    return (int)(toUnsignedLong(dividend) / toUnsignedLong(divisor));
}
public static int remainderUnsigned(int dividend, int divisor) {
    // In lieu of tricky code, for now just use long arithmetic.
    return (int)(toUnsignedLong(dividend) % toUnsignedLong(divisor));
}

highestOneBit & lowestOneBit

highestOneBit 方法返回一个整数,这个整数的二进制表现只有一个 1,这个 1 的位置是参数 i 的二进制表现中最高位的 1 的位置。lowestOneBit 类似,只是这个 1 的位置在最低位。

java
public static int highestOneBit(int i) {
    // MIN_VALUE 对应的二进制为符号位为 1,其余位全为 0
    // numberOfLeadingZeros 方法返回 i 的二进制表现中 0 的数量
    // 无符号右移后相当于将符号位的 1 右移到了 i 对应的二进制中最左边的 1 的位置
    // 最后进行与操作后,则除了这个最高位的 1 之外,其余位都置为了 0
    return i & (MIN_VALUE >>> numberOfLeadingZeros(i));
}
public static int lowestOneBit(int i) {
    // HD, Section 2-1
    // 这个算法实在是太巧妙了!
    // 假设 i 为正数,对应的 -i 的二进制表现为其补码(取反后加 1),
    // 此时只有原最低位的 1 会由于 +1 而从 0 变为 1,
    // 其余位仍是原二进制表现对应位取反后的值。
    // 之后两个数做与运算则只会保留这个最低位的 1,其余位都为 0。
    return i & -i;
}

下面是一些数字对应的运算结果:

txt
  number             binary representation highestOneBit             binary representation  lowestOneBit   binary representation
       0                                 0             0                                 0             0                       0
       1                                 1             1                                 1             1                       1
       2                                10             2                                10             2                      10
     255                          11111111           128                          10000000             1                       1
     256                         100000000           256                         100000000           256               100000000
      -1  11111111111111111111111111111111   -2147483648  10000000000000000000000000000000             1                       1
      -2  11111111111111111111111111111110   -2147483648  10000000000000000000000000000000             2                      10
    -255  11111111111111111111111100000001   -2147483648  10000000000000000000000000000000             1                       1
    -256  11111111111111111111111100000000   -2147483648  10000000000000000000000000000000           256               100000000

numberOfTrailingZeros

前面用到的 numberOfLeadingZeros 是计算前导 0 的数量,numberOfTrailingZeros 是计算尾部 0 的数量。

java
@IntrinsicCandidate
public static int numberOfTrailingZeros(int i) {
    // HD, Count trailing 0's
    // ~i 是对 i 取反
    // 整个运算的效果是把二进制表现最左边的 0 的右边一位置为 1,其余位全为 0。
    // 之后只需要计算出这个 1 的位置即可。
    i = ~i & (i - 1);
    // 这里为什么要执行 i & 32 想了很久,
    // 最后发现这里其实相当于 if (i <= 0) return i == 0 ? 0 : 32;
    // 因为 i < 0 的情况只有在参数为 0 时才会发生,
    // 此时的 i 为 - 1,其二进制表现为 32 个 1,i & 32 就相当于直接返回 32。
    // so,不太确认这里为什么一定做一次与运算。
    if (i <= 0) return i & 32;
    // 如果运行到这里,说明右边至少有 1 个 0
    int n = 1;
    // 这里开始和 numberOfLeadingZeros 方法的逻辑基本类似了。
    // 如果 i > 2^16,说明右边的 16 位全是 0,则尾零的数量 n 应该加 16
    // 只有 i 无符号右移 16 位,以进行后续操作
    if (i > 1 << 16) { n += 16; i >>>= 16; }
    // 依次判断后 8 位、4 位、2 位是否全是 0
    if (i > 1 <<  8) { n +=  8; i >>>=  8; }
    if (i > 1 <<  4) { n +=  4; i >>>=  4; }
    if (i > 1 <<  2) { n +=  2; i >>>=  2; }
    // 因为 n 的初值为 1,所以这里只需要判断剩余 2 位的高位是否为 1 即可。
    // 如果高位为 1,则说明这两位原本都为 0,因为 n 初值为 1,再加上 1 即可;
    // 如果高位为 0,则说明这两位原本只有低位为 0,因为 n 初值为 1,就不需要再加了。
    return n + (i >>> 1);
}

bitCount

bitCount 方法是返回参数 i 的二进制表示中 1 的数量。方法的功能比较好理解,但是代码的算法很难理解。

不太确定是否正确,先说一下我的理解:

  • 首先每两个两个 bit 进行统计,将这两个 bit 中 1 的数量保存到这 2 个 bit 位中;
  • 然后每 4 个 bit 进行统计,将这 4 个 bit 中高 2 位和低 2 两位求和,将结果保存到这 4 个 bit 位。
  • 然后依次统计 8 个 bit,16 个 bit,32 个 bit。
java
@IntrinsicCandidate
public static int bitCount(int i) {
    // HD, Figure 5-2
    // 统计每 2 个 bit 中 1 的数量,将结果保存到这 2 个 bit 位中
    i = i - ((i >>> 1) & 0x55555555);
    // 统计每 4 个 bit 中高 2 位和低 2 位的和,并将高的 2 位置为 0
    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
    // 统计每 8 个 bit 中高 4 位和低 4 位的和,并将高的 4 位置为 0
    i = (i + (i >>> 4)) & 0x0f0f0f0f;
    // 统计每 16 个 bit 中高 8 位和低 8 位的和
    // 这里没有再将高 8 位置为 0,估计是因为即使参与后续运算也不会影响结果
    i = i + (i >>> 8);
    // 统计每 32 个 bit 中高 16 位和低 16 位的和
    i = i + (i >>> 16);
    // 最后这里只保留 32 个 bit 位的低 6 位
    return i & 0x3f;
}

rotateLeft & rotateRight

rotateLeft 是将整数的二进制表现向左旋转 distance 位,从左侧移出的位会重新从右侧进入。rotateRight 则是向相反的方向(右侧)旋转。

java
public static int rotateLeft(int i, int distance) {
    return (i << distance) | (i >>> -distance);
}
public static int rotateRight(int i, int distance) {
    return (i >>> distance) | (i << -distance);
}

算法相当简洁。

左移时右补零,可以使用 << 操作符;
右移时需要左补零,需要使用无符号右移 >>> 操作符。
最后将两个位移结果做一次或运算 | 即可。

reverse & reverseBytes

看方法名 reverse 应该就能大致猜到其作用:将参数 i 的二进制表示进行反转,也就是顺序反过来。

java
public static int reverse(int i) {
    // HD, Figure 7-1
    // 这行代码总共可以分为 5 步来理解:
    // 第一步 i & 0x55555555 是为了保留每 2 个 bit 中的低位,高位置 0
    // 第二部将上一步的结果 << 1 相当于把原来每 2 个 bit 中的低位移到了高位
    // 第三部 i >>> 1 相当于把原来每 2 个 bit 中的高位移到了低位
    // 第四部 将上一步的结果 & 0x55555555 则同样是保留了每 2 个 bit 中的低位,高位置 0
    // 至此在 | 的左边的结果,保留了原 i 中每 2 个 bit 中的低位,但是位置已经移到了高位;
    // | 的右边的结果,保留了原 i 中每 2 个 bit 中的高位,但是位置已经移到了低位
    // 第五步将上述两个结果进行或运算,此时的结果相当于原 i 的二进制表示中每 2 个 bit 的低位和高位都进行了交换
    i = (i & 0x55555555) << 1 | (i >>> 1) & 0x55555555;
    // 这里和上面类似,只是每次对 4 个 bit 中的低 2 位和高 2 位进行交换
    i = (i & 0x33333333) << 2 | (i >>> 2) & 0x33333333;
    // 同样,这里每次对 8 个 bit 中的低 4 位和高 4 位进行交换
    i = (i & 0x0f0f0f0f) << 4 | (i >>> 4) & 0x0f0f0f0f;
    // 至此,在共 64 个 bit 中,每 8 个 bit 内部的二进制表现顺序已经全部反转了
    // reverseBytes 方法则是按照字节(8 个 bit)进行反转
    return reverseBytes(i);
}
@IntrinsicCandidate
public static int reverseBytes(int i) {
    // 这个算法还比较好理解,因为 int 型共 4 个字节,
    // 这个方法就是将这 4 个字节的顺序进行反转。
    // i << 24 将最低的 8 位移到最高位,同时其他位都是 0;
    // (i & 0xff00) << 8 将中间 16 位中的低位移到高位,同时其它位都是 0;
    // (i >>> 8) & 0xff00 则是将中间 16 位的高位移到低位,同时其它位都是 0;
    // i >>> 24 将最高位的 8 位移到最低位,同时其他位都是 0;
    // 最后将上述 4 个结果进行或运算,就完成了对 int 型中 4 个字节的顺序反转。
    return (i << 24)            |
           ((i & 0xff00) << 8)  |
           ((i >>> 8) & 0xff00) |
           (i >>> 24);
}

signum

signum 方法的作用是返回一个整数的符号,如果参数为 0 则返回 0,如果参数为负数则返回 -1,如果参数为正数则返回 1。

java
public static int signum(int i) {
    // HD, Section 2-7
    return (i >> 31) | (-i >>> 31);
}

可以根据参数 i 的值分成以下 3 种情况来理解:

  • 正数
    • i >> 31 :0
    • -i >>> 31:1
    • 结果:1
  • 负数
    • i >> 31 :-1
    • -i >>> 31
      • Integer.MIN_VALUE:1
      • 以外:0
    • 结果:-1
  • 0
    • i >> 31 :0
    • -i >>> 31:0
    • 结果:0

其中比较特殊的是 Integer.MIN_VALUE 的时候,由于 -Integer.MIN_VALUE 仍然是 Integer.MIN_VALUE,所以此时 -i >>> 31 的结果为 1。但是由于 i >> 31 的结果为 -1,其二进制表现为 32 个 1(11111111 11111111 11111111 11111111),所以无论和哪个数字进行 | 或运算,结果都是 -1。

-Integer.MIN_VALUE

之前从来没注意过 -Integer.MIN_VALUE 的结果,没想到还是 Integer.MIN_VALUE。根据负运算的规则,其实际步骤为先按位取反,然后再加 1。
Integer.MIN_VALUE 的二进制表示为 10000000 00000000 00000000 00000000,对其取反则变为 01111111 11111111 11111111 11111111,再加一则变为 10000000 00000000 00000000 00000000,正好还是 Integer.MIN_VALUE 的二进制表示。

另外,Integer.MIN_VALUE - 1 的结果为 Integer.MAX_VALUEInteger.MAX_VALUE + 1 的结果为 Integer.MIN_VALUE

这两个还是比较好理解的:

  • Integer.MIN_VALUE 的二进制表示为 10000000 00000000 00000000 00000000,对其减 1 则为 01111111 11111111 11111111 11111111,也就是 Integer.MAX_VALUE 的二进制表示。
  • Integer.MAX_VALUE 的二进制表示为 01111111 11111111 11111111 11111111,对其加 1 则为 10000000 00000000 00000000 00000000,也就是 Integer.MIN_VALUE 的二进制表示。

以上。