资讯动态

JDK源码深度剖析:Long类的缓存、解析与无符号运算陷阱

发布时间:2026/10/16 1:50:13 来源:尧图企业网站定制
1. 为什么值得花时间啃Long的源码很多人第一次翻JDK源码都是从String或者ArrayList开始的这很正常毕竟日常写业务代码跟它们打交道最多。但如果你问我哪个类的源码最能体现“细节里的魔鬼”我会毫不犹豫地推荐Long。这个类看起来简单得不行不就是对基本类型long的一个包装嘛有什么好研究的我一开始也这么想直到有一次在做一个涉及大量ID生成和序列化的项目时被Long的缓存机制、parseLong的边界处理、以及compare和compareTo的区别狠狠教育了一顿才意识到这个类远没有表面看起来那么“朴素”。Long是java.lang包下的一个不可变包装类它把基本类型long包装成对象同时提供了大量静态工具方法用于解析、转换、比较和位运算。它实现了ComparableLong、Number接口并且从Java 8开始还支持了无符号运算相关的方法。这个类几乎出现在每一个Java应用的每一层代码里从数据库主键映射到JSON序列化从缓存Key生成到分布式ID传递无处不在。但正因为太常见很多人对它的理解停留在“会用”的层面一旦遇到性能瓶颈或者诡异的边界问题就不知道从何下手了。这篇文章适合所有写Java的人看。不管你是刚入行的新手还是写了五六年业务代码的老手只要你还在用Long这里面就有你大概率没注意到的细节。我会从设计思路、核心字段、缓存机制、解析方法、比较逻辑、无符号支持、序列化注意事项这几个维度把Long的源码掰开揉碎讲清楚。每一部分我都会告诉你“为什么这么设计”以及“在实际写代码时怎么用才不会踩坑”。文章里涉及的所有代码示例和参数计算你都可以直接复制到本地跑一遍验证。2. Long类的整体设计与核心字段拆解2.1 不可变性与final修饰的深层考量打开Long的源码第一眼看到的就是public final class Long extends Number implements ComparableLong。这个final不是随便加的。Long被设计成不可变类意味着一旦一个Long对象被创建它内部包裹的long值就永远不会改变。这个设计决策背后有三个层面的考虑。第一是线程安全。不可变对象天然线程安全多个线程同时读取同一个Long对象不需要任何同步措施。你在并发环境里传递Long类型的参数或者把它放进ConcurrentHashMap的Key里完全不用担心值被意外修改。第二是缓存友好。因为值不可变所以可以安全地缓存常用对象这就是后面要讲的LongCache存在的前提。第三是作为HashMap的Key时的稳定性。如果Long是可变的那么一个对象放进HashMap之后如果它的hashCode变了你就再也找不到它了。Long内部真正存储值的字段只有一个private final long value;。这个字段被final修饰保证了引用不可变。整个类没有任何setter方法所有看似“修改”的操作比如、-、*实际上都是创建了一个新的Long对象。这一点在写循环累加的时候要特别注意如果你在循环里用Long做累加器每一次sum i都会产生一个新的Long对象在大循环里这就是性能杀手。我见过一个项目在百万级循环里用Long做计数GC日志里全是Long对象的分配和回收改成long之后性能直接提升了一个数量级。2.2 核心常量与位宽定义Long类里定义了几个非常关键的常量理解它们对后面看解析和转换方法至关重要。public static final int SIZE 64; public static final int BYTES SIZE / Byte.SIZE; public static final long MAX_VALUE 0x7fffffffffffffffL; public static final long MIN_VALUE 0x8000000000000000L; public static final int MAX_VALUE_RADIX 36;SIZE是64表示一个long值在二进制下占64位。BYTES是8表示占8个字节。MAX_VALUE是2^63 - 1也就是9223372036854775807十六进制表示是0x7fffffffffffffffL。MIN_VALUE是-2^63也就是-9223372036854775808十六进制是0x8000000000000000L。注意MIN_VALUE的绝对值比MAX_VALUE大1这个不对称性在写取绝对值、比较、解析相关代码时经常导致边界Bug。MAX_VALUE_RADIX是36表示Long在进制转换时支持的最大进制是36也就是0-9加上a-z。这个限制来源于Character.digit方法的实现超过36就没有字符可以表示了。你在用Long.toString(long, int)或者Long.parseLong(String, int)的时候如果传入的radix超过36或者小于2都会抛出NumberFormatException。2.3 Number抽象类的继承关系Long继承自Number这意味着它必须实现intValue()、longValue()、floatValue()、doubleValue()、byteValue()、shortValue()这几个方法。这些方法看起来简单但里面藏着窄化转换的陷阱。intValue()的实现是return (int)value;直接把64位的long截断成32位的int。如果你有一个Long值是4294967296L也就是2^32调用intValue()得到的是0因为低32位全是0。这种截断不会抛异常但会静默地丢失数据。我在排查一个数据同步问题时遇到过这个坑上游系统用Long传了一个超过int范围的值下游系统用intValue()接收结果数据对不上查了半天才发现是截断导致的。doubleValue()的实现是return (double)value;这里有一个精度问题。long有64位double的尾数只有52位所以当一个long值的绝对值超过2^53时转换成double会丢失精度。比如Long.MAX_VALUE转成double之后是9.223372036854776E18它和原始值已经不相等了。如果你在做金额计算或者ID比较时混用了long和double一定要小心这个精度陷阱。3. LongCache缓存机制与对象创建策略3.1 缓存范围为什么是-128到127Long类内部有一个静态内部类LongCache代码如下private static class LongCache { private LongCache(){} static final Long cache[] new Long[-(-128) 127 1]; static { for(int i 0; i cache.length; i) cache[i] new Long(i - 128); } }这个缓存数组覆盖了从-128到127共256个值。为什么选这个范围因为这是Java语言规范里对包装类缓存的最小要求。Byte的全部值都在这个范围内Short、Integer、Long都至少缓存这个范围。选这个范围的逻辑是这些值在大多数程序里出现频率最高缓存它们可以用少量内存换取大量的对象复用。但这里有一个非常容易踩的坑Long的缓存范围是固定的不像Integer可以通过-XX:AutoBoxCacheMax参数调整。也就是说你无法通过JVM参数扩大Long的缓存范围。如果你在代码里大量使用Long.valueOf(1000L)每次都会创建新对象因为1000超出了缓存范围。3.2 valueOf与自动装箱的字节码真相Long.valueOf(long l)的实现是public static Long valueOf(long l) { final int offset 128; if (l -128 l 127) { return LongCache.cache[(int)l offset]; } return new Long(l); }当你写Long a 100L;的时候编译器会自动插入Long.valueOf(100L)的调用。因为100在缓存范围内所以a拿到的是缓存里的同一个对象。但如果你写Long b 200L;每次都会new Long(200)产生不同的对象。这就引出了一个经典面试题Long a 100L; Long b 100L; a b是true还是false答案是true因为都在缓存范围内指向同一个对象。但如果换成200L答案就是false。这个行为在写代码时如果没注意用比较Long值就会出Bug。我个人的习惯是只要是比较Long的值一律用equals或者longValue()绝不用。3.3 缓存机制带来的内存与性能权衡缓存256个Long对象每个对象大约16字节对象头12字节加8字节的long值对齐后16字节总共大约4KB内存。这4KB换来的是常用小数值的对象复用减少了GC压力。在大量使用小Long值的场景下这个优化效果很明显。但缓存也有代价。如果你在代码里频繁使用超出缓存范围的Long值比如用时间戳作为Long那么每次自动装箱都会创建新对象。一个典型的反例是在循环里用Long做累加Long sum 0L; for (int i 0; i 1000000; i) { sum i; }这段代码会创建一百万个Long对象因为每次sum i都会先拆箱sum加上i再装箱成新的Long。如果sum的值超出了127每次都是新对象。改成long sum 0L;就完全没有这个问题。这个优化我在多个项目里做过效果立竿见影。4. 解析方法parseLong与valueOf的边界处理4.1 parseLong的核心解析逻辑Long.parseLong(String s, int radix)是整个类里最复杂的方法之一。它的核心逻辑是遍历字符串的每一个字符把字符转换成对应进制的数字然后累加到结果中。但这里面有大量的边界检查和溢出处理。public static long parseLong(String s, int radix) throws NumberFormatException { if (s null) { throw new NumberFormatException(null); } 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); } // ... 省略具体解析逻辑 }首先检查null然后检查radix是否在2到36之间。这两个检查是必须的因为后面的解析逻辑依赖radix的有效性。如果radix不合法直接抛异常避免后续计算出错。4.2 溢出检测的巧妙实现解析过程中最精妙的部分是溢出检测。因为long的最大值是9223372036854775807在累加过程中如果结果超过这个值就会溢出。JDK的实现方式是在每次累加前用负数来比较long result 0; boolean negative false; int i 0; if (s.charAt(0) -) { negative true; i; } long limit negative ? Long.MIN_VALUE : -Long.MAX_VALUE; long multmin limit / radix; while (i s.length()) { int digit Character.digit(s.charAt(i), radix); if (digit 0) { throw new NumberFormatException(...); } if (result multmin) { throw new NumberFormatException(...); } result * radix; if (result limit digit) { throw new NumberFormatException(...); } result - digit; } return negative ? result : -result;这段代码用负数来累加因为Long.MIN_VALUE的绝对值比Long.MAX_VALUE大1用负数可以统一处理正负两种情况。multmin是limit / radix在每次乘以radix之前先检查result是否小于multmin如果小于说明乘以radix之后一定会溢出。然后在减去digit之前检查result是否小于limit digit如果小于说明减去digit之后会超过limit。这两个检查保证了整个解析过程不会溢出。我第一次看这段代码的时候觉得有点绕后来自己用正数写了一遍溢出检测发现要处理MIN_VALUE的情况非常麻烦才理解JDK为什么用负数。这个技巧在你自己写解析器的时候可以直接借鉴。4.3 parseLong与valueOf的分工Long.valueOf(String s)内部调用的是Long.parseLong(s, 10)然后对结果调用Long.valueOf(long)。也就是说valueOf(String)会先解析成long再根据值是否在缓存范围内决定是否复用缓存对象。而parseLong只返回基本类型long不涉及对象创建。在实际使用中如果你只需要基本类型long用parseLong如果你需要Long对象用valueOf。但要注意valueOf(String)在解析失败时抛出的异常和parseLong是一样的都是NumberFormatException。另外parseLong还有一个重载版本parseLong(CharSequence s, int beginIndex, int endIndex, int radix)从Java 9开始提供支持解析字符序列的子串避免了先substring再解析的额外对象创建。5. 比较逻辑与无符号运算支持5.1 compare与compareTo的区别Long类里有两个比较方法静态的compare(long x, long y)和实例的compareTo(Long anotherLong)。compare的实现是public static int compare(long x, long y) { return (x y) ? -1 : ((x y) ? 0 : 1); }这个实现看起来简单但它是所有比较逻辑的基础。compareTo的实现是compare(this.value, anotherLong.value)。注意compareTo的参数是Long类型如果传入null会抛出NullPointerException。而compare的参数是基本类型long不存在null问题。在写排序逻辑时我通常用Long.compare(a, b)而不是a.compareTo(b)因为前者不需要担心null而且少一次方法调用。如果你用Comparator可以用Comparator.comparingLong(Long::longValue)这样也是基于基本类型的比较效率更高。5.2 无符号运算的方法族从Java 8开始Long增加了一系列无符号运算方法包括compareUnsigned、divideUnsigned、remainderUnsigned、toUnsignedString、parseUnsignedLong等。这些方法把long的64位全部当作数值位不再保留符号位。compareUnsigned的实现是public static int compareUnsigned(long x, long y) { return compare(x Long.MIN_VALUE, y Long.MIN_VALUE); }这个技巧很巧妙加上Long.MIN_VALUE相当于把有符号数的范围平移使得无符号比较可以用有符号比较来实现。比如-1L的无符号表示是0xFFFFFFFFFFFFFFFF加上MIN_VALUE之后变成0x7FFFFFFFFFFFFFFF也就是Long.MAX_VALUE这样它就和正常的正数比较逻辑一致了。divideUnsigned和remainderUnsigned的实现更复杂因为不能直接用/和%需要手动实现无符号除法。JDK用了经典的移位减法算法这里不展开但你要知道这两个方法在需要处理无符号大数时非常有用。5.3 无符号转换的实际应用场景无符号运算在实际项目中用得不多但在某些特定场景下非常关键。比如处理哈希值时哈希值通常是无符号的32位或64位整数用有符号的long表示时负值其实是高位为1的无符号数。如果你要把哈希值转成字符串展示用toUnsignedString才能得到正确的无符号表示。另一个场景是处理来自网络协议或文件格式的二进制数据这些数据通常是无符号的。比如一个64位的无符号整数用long接收后如果需要打印或比较就必须用无符号方法。我在处理一个自定义二进制协议时就因为没有用compareUnsigned导致高位为1的值比较结果错误排查了很久才发现问题。6. 序列化与JSON处理中的Long陷阱6.1 Long的序列化机制Long实现了Serializable接口它的序列化很简单就是把内部的long value写出去。但这里有一个细节Long的serialVersionUID是4290774380558885855L这个值是JDK固定的。如果你自己写了一个类似的类没有显式声明serialVersionUID反序列化时可能会因为版本不匹配而失败。在RPC框架或者缓存系统里Long经常作为传输对象的一部分。如果序列化协议对Long的处理不一致比如一个系统用Java原生序列化另一个系统用JSON就可能出现精度问题。JSON标准里数字是没有类型区分的JavaScript的Number类型只能安全表示2^53 - 1以内的整数。如果一个Long值超过这个范围用JavaScript解析就会丢失精度。6.2 JSON序列化的大数精度问题这个问题在前后端交互中极其常见。后端返回一个Long类型的ID比如1234567890123456789前端用JavaScript接收JSON.parse之后得到的数字是1234567890123456800最后几位变了。这不是Java的Bug而是JavaScript的Number类型限制。解决方案有几种一是后端把Long序列化成字符串前端按字符串处理二是使用支持大数的JSON库比如json-bigint三是在前后端约定ID不超过2^53 - 1。我个人的经验是只要涉及分布式ID或者雪花算法生成的ID一律用字符串传输避免任何精度问题。在Java端如果你用Jackson可以通过配置WRITE_NUMBERS_AS_STRINGS或者对特定字段加JsonSerialize(using ToStringSerializer.class)来实现。如果是FastJSON可以用JSONField(serializeUsing ToStringSerializer.class)。这些配置在项目初期就要定好后期再改成本很高。6.3 数据库映射中的类型选择在数据库里Long通常对应BIGINT类型。但有些数据库的BIGINT是无符号的范围是0到2^64 - 1而Java的long是有符号的范围是-2^63到2^63 - 1。如果你从无符号BIGINT里读了一个超过Long.MAX_VALUE的值JDBC驱动会怎么处理不同的驱动行为不一样有的会抛异常有的会截断有的会返回负数。我在一个项目里遇到过这个问题数据库里存了一个无符号BIGINT的最大值Java端用Long接收结果变成了-1。后来改成用BigInteger接收才解决。所以如果你的数据库用了无符号BIGINT一定要确认JDBC驱动的行为或者干脆在数据库层面就用有符号BIGINT避免这个坑。7. 常见问题与排查技巧实录7.1 Long比较的经典错误最常见的错误就是用比较两个Long对象。前面讲过缓存范围内的值用可能返回true超出范围返回false这种不确定性是Bug的温床。我建议在代码规范里明确禁止用比较任何包装类型一律用equals或者先拆箱。另一个错误是用equals比较Long和Integer。Long.equals的实现是public boolean equals(Object obj) { if (obj instanceof Long) { return value ((Long)obj).longValue(); } return false; }如果传入的是Integerinstanceof Long返回false直接返回false不会抛异常。这看起来没问题但如果你期望的是数值相等就会得到错误的结果。比如Long.valueOf(1).equals(Integer.valueOf(1))返回false。这种跨类型的比较错误在泛型代码里容易出现因为泛型擦除后类型信息丢失运行时才暴露问题。7.2 解析异常的处理策略parseLong抛出的NumberFormatException是一个运行时异常很多人在写代码时直接让它往上抛结果在接口层返回了500错误。更好的做法是在解析前做校验或者捕获异常后返回默认值。我通常会在工具类里封装一个parseLong(String s, long defaultValue)方法解析失败时返回默认值而不是抛异常。这样在配置读取、参数解析等场景下更健壮。但要注意如果是用户输入的关键参数解析失败应该明确报错而不是静默使用默认值否则会掩盖问题。还有一个细节parseLong不接受前导号吗实际上它是接受的。parseLong(123)返回123。但它不接受空格parseLong( 123)会抛异常。如果你从配置文件或用户输入里读取的值可能带空格记得先trim()。7.3 性能相关的注意事项Long的性能问题主要集中在对象创建上。除了前面说的循环累加还有几个场景要注意。一是用Long作为HashMap的Key时如果Key的值经常变化且超出缓存范围每次put都会创建新对象。二是用Long做Stream操作时装箱拆箱的开销会累积。三是用Long做方法参数时如果调用频率极高可以考虑重载一个long参数的版本。我做过一个简单的基准测试在一个千万次的循环里用long累加耗时约10毫秒用Long累加耗时约200毫秒差距是20倍。这个差距在低延迟系统里是不可接受的。所以我的原则是能用基本类型就用基本类型只有在必须用对象的时候才用包装类型。常见问题排查思路解决方案比较返回false检查值是否超出缓存范围改用equals或longValue()解析大数精度丢失检查是否经过JavaScript处理序列化为字符串传输无符号值比较错误检查是否用了有符号比较改用compareUnsigned循环累加性能差检查是否用了Long做累加器改用基本类型long数据库读取值为负数检查数据库字段是否无符号改用BigInteger或调整字段类型8. 从Long源码中学到的设计思想Long的源码里有一个很值得玩味的设计toString方法的实现。它没有用简单的除法取余而是用了更高效的算法特别是对于小数值做了优化。这种在细节上追求性能的态度是JDK源码的一贯风格。另一个让我印象深刻的是hashCode的实现public static int hashCode(long value) { return (int)(value ^ (value 32)); }这个实现把64位的高32位和低32位异或然后取低32位作为哈希值。这样做的原因是HashMap的桶数组长度通常是2的幂哈希值的高位在索引计算中会被丢弃所以把高位异或到低位可以保留高位的信息减少哈希冲突。这个技巧在你自己写hashCode时也可以借鉴。Long还提供了sum、max、min这些静态方法它们主要用于Stream的reduce操作。比如LongStream.sum()内部就是用Long.sum实现的。这些方法看起来简单但它们的参数和返回值都是基本类型long避免了装箱拆箱的开销。这种在API设计上区分基本类型和包装类型的做法值得我们在写工具类时学习。最后再分享一个小技巧如果你需要把long转成字符串但不想用Long.toString可以用String.valueOf(long)它内部也是调用Long.toString但语义更清晰。如果你需要把long转成其他进制的字符串用Long.toBinaryString、Long.toOctalString、Long.toHexString它们比Long.toString(value, radix)更快因为内部做了针对性优化。这些方法在写日志、调试、生成短链接等场景下很实用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑