news 2026/10/15 1:03:03

JDK源码精读:Long包装类的缓存、拆箱与溢出陷阱

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JDK源码精读:Long包装类的缓存、拆箱与溢出陷阱

1. 为什么Long的源码值得单独拎出来读

很多人读JDK源码,第一反应是去啃HashMap、ConcurrentHashMap、AQS这些“大件”,觉得Long这种包装类太简单,扫一眼API文档就够了。我一开始也这么想,直到有一次在排查一个线上数据对不上的问题时,才发现自己对Long的理解漏洞百出——缓存边界、拆箱时机、equals和==的差异、parseLong和valueOf的底层行为,这些细节在真实项目里全都会咬人。

Long这个类看起来只是把long包了一层,但它身上浓缩了Java包装类设计的几乎所有典型问题:缓存机制、自动装箱拆箱、进制转换、位运算、溢出处理、字符串解析。把Long读透,等于把一整套包装类的设计思路摸清楚了,后面再看Integer、Short、Byte基本就是举一反三。

这篇文章面向的是已经会写Java、但没系统读过Long源码的开发者。我会从字段结构一路拆到parseLong的每一位运算,把“为什么这么设计”讲清楚,同时穿插我自己踩过的坑。读完之后,你至少能回答这几个问题:Long.valueOf(127)和Long.valueOf(128)到底差在哪、Long的缓存能不能调、parseLong为什么不用Integer.parseInt那套写法、Long的hashCode为什么那么算。

2. Long的骨架:字段、常量与继承关系

2.1 类声明里藏着的两个信息

Long的类声明是这样的:

public final class Long extends Number implements Comparable<Long>

这里有两个点值得说。第一,final修饰,意味着它不能被继承,这是所有包装类的共同选择——包装类的语义必须稳定,不允许子类改写equals、hashCode这些基础行为。第二,它继承了Number并实现了Comparable,前者提供了intValue()、longValue()、doubleValue()这一组拆箱方法,后者提供了自然排序能力,这也是它能直接丢进TreeMap、Collections.sort的前提。

Number是个抽象类,里面定义了六个xxxValue()抽象方法。Long对它们的实现基本都是强转或截断,比如intValue()就是(int)value,doubleValue()就是(double)value。这里有个容易忽略的点:从Long转int是可能丢精度的,因为long是64位,int只有32位,高位会被直接砍掉。我在一个统计项目里就遇到过,把Long类型的累计值直接intValue()之后变成了负数,排查了半天才发现是溢出截断。

2.2 那个被很多人忽略的value字段

Long内部只有一个实例字段:

private final long value;

final保证不可变,这是包装类能安全用作HashMap的key、能放心共享的前提。所有对Long的操作,本质上都是围绕这个value做运算,然后返回一个新的Long或者一个基本类型。

这里要提醒一句:不可变意味着每次运算都会产生新对象。如果你在一个循环里频繁做Long的加减,会产生大量临时对象。我在一个批量对账的场景里,原本用Long做累加,十万条数据跑下来GC压力明显,后来改成基本类型long累加、最后再装箱,性能提升非常直观。这不是说Long不能用,而是要知道它的代价在哪。

2.3 常量与缓存边界

Long里定义了几个关键常量:

public static final long MIN_VALUE = 0x8000000000000000L; public static final long MAX_VALUE = 0x7fffffffffffffffL; public static final int SIZE = 64; public static final int BYTES = SIZE / Byte.SIZE;

MIN_VALUE是-2^63,MAX_VALUE是2^63 - 1,这两个值在溢出判断里会反复用到。SIZE是64,表示位数;BYTES是8,表示字节数。这两个常量在做序列化、内存估算的时候很有用。

真正关键的是缓存相关的部分:

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); } }

这段代码的意思是:Long在类加载时会预先创建-128到127这256个Long对象,放在一个静态数组里。当你调用Long.valueOf()且值落在这个区间时,直接返回缓存对象,不会新建。

注意:这个缓存区间是固定的,不能像Integer那样通过-XX:AutoBoxCacheMax调整。Long的缓存上限写死在代码里,就是127。这一点很多人会搞混,以为Long也能调缓存范围。

为什么只缓存这个区间?因为JDK设计者认为这个范围的值在大多数程序里出现频率最高,缓存它们能显著减少对象创建。而更大的值如果也缓存,会占用大量内存,得不偿失。这个取舍思路在所有包装类里是一致的。

3. valueOf与自动装箱:缓存到底在什么时候生效

3.1 valueOf的完整逻辑

Long.valueOf(long)的实现非常短:

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); }

逻辑很直白:落在[-128, 127]就查缓存,否则新建。这里的offset = 128是为了把-128映射到数组下标0,127映射到下标255。

还有一个重载版本valueOf(String s),它内部调用parseLong(s, 10),然后再走valueOf(long)。也就是说,从字符串创建Long同样会走缓存。这一点在写代码时值得留意:Long.valueOf("100")和Long.valueOf("1000"),前者返回的是缓存对象,后者是新对象。

3.2 自动装箱背后调用的就是valueOf

当你写Long a = 100L;的时候,编译器会自动插入Long.valueOf(100L)。所以自动装箱是否命中缓存,完全取决于值是否在[-128, 127]。

这就引出了那个经典面试题:

Long a = 127L; Long b = 127L; System.out.println(a == b); // true Long c = 128L; Long d = 128L; System.out.println(c == d); // false

a == b为true是因为两者都指向缓存里的同一个对象;c == d为false是因为128超出了缓存范围,各自new了一个。这个坑我在实际项目里见过不止一次,有人用==比较两个Long的ID,小数据量测试时全对,一上生产数据量大了就出问题。

实操建议:比较两个Long的值,永远用equals或者先longValue()再比。不要用==,除非你非常确定它们来自缓存且值在范围内。

3.3 缓存带来的一个隐蔽问题

缓存对象是共享的,虽然Long不可变所以共享本身安全,但如果你用Long作为锁对象,就可能出问题。比如:

Long lock = 100L; synchronized (lock) { ... }

如果另一个地方也用Long lock2 = 100L;,它们其实是同一个对象,会互相阻塞。这种问题极其隐蔽,因为看起来是两个独立的变量。我的建议是:永远不要用包装类做锁对象,用专门的Object实例或者ReentrantLock。

4. parseLong:一个字符一个字符啃出来的解析逻辑

4.1 为什么不用Integer.parseInt那套

Long.parseLong(String)最终会调用parseLong(String s, int radix)。这个方法的核心是一个循环,逐字符解析。很多人会问:为什么不直接复用Integer的解析逻辑?答案是范围不同。int是32位,long是64位,溢出判断的边界完全不一样,而且Long要处理更大的数值,不能简单套用。

4.2 解析过程拆解

方法开头先做了一堆边界检查:字符串非空、radix在[2, 36]之间、长度大于0。然后处理正负号,确定negative标志和第一个有效字符的位置。

接下来是核心循环,我把它简化一下:

long result = 0; long limit = negative ? Long.MIN_VALUE : -Long.MAX_VALUE; long multmin = limit / radix; while (i < len) { 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; }

这里有个非常巧妙的设计:整个累加过程用的是负数。为什么?因为Long.MIN_VALUE的绝对值比Long.MAX_VALUE大1,如果用正数累加,处理Long.MIN_VALUE时会溢出。用负数累加,Long.MIN_VALUE就能被正确表示。这个技巧在Integer、Short里也是一样的。

溢出判断分两步:先判断result < multmin,确保乘radix之后不会越界;再判断result < limit + digit,确保减去当前位之后不会越界。这两步缺一不可,顺序也不能反。

4.3 我踩过的一个进制坑

parseLong支持2到36进制。有一次我处理一批带前缀的编号,想当然地用了Long.parseLong("0x1F", 16),结果直接抛异常。原因是parseLong不识别0x前缀,它只认纯数字字符。要处理带前缀的字符串,得用Long.decode("0x1F"),它会自动识别0x、0X、#、0这些前缀。

Long.parseLong("1F", 16); // 31,正确 Long.parseLong("0x1F", 16); // 抛NumberFormatException Long.decode("0x1F"); // 31,正确

这个区别在解析配置文件、处理十六进制ID的时候特别容易踩。

4.4 性能上的一个细节

parseLong内部用的是Character.digit(char, int),这个方法会处理各种Unicode数字字符,比直接c - '0'要慢一些。如果你在极端性能敏感的场景下解析大量十进制数字,自己写一个只处理ASCII数字的解析器会更快。但绝大多数业务场景下,parseLong的性能完全够用,没必要过早优化。

5. 位运算与进制转换:Long身上的工具方法

5.1 位运算方法族

Long提供了一组静态位运算方法,这些方法在写底层代码、做标志位管理时非常有用:

方法作用典型场景
highestOneBit(long)返回最高位1对应的值找小于等于n的最大2的幂
lowestOneBit(long)返回最低位1对应的值位图、权限判断
bitCount(long)统计1的个数汉明重量计算
numberOfLeadingZeros(long)前导零个数二分查找、位图定位
numberOfTrailingZeros(long)末尾零个数找最低位1的位置
reverse(long)位反转某些加密、哈希算法
rotateLeft/rotateRight循环移位哈希、加密

这些方法的实现都是纯位运算,没有循环,效率极高。比如highestOneBit用的是连续右移再或的经典技巧:

public static long highestOneBit(long i) { i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); i |= (i >> 32); return i - (i >>> 1); }

先把最高位1右边全部填成1,再减去右移一位的结果,就只剩下最高位那个1。这个技巧值得记住,面试和实际优化都用得上。

5.2 进制转换:toHexString、toOctalString、toBinaryString

这三个方法把long转成对应进制的字符串。它们的实现思路类似,都是通过位掩码和移位逐段处理。以toHexString为例,它每次取低4位,查一个预定义的字符数组,然后右移4位,直到值为0。

这里有个细节:它们处理的是无符号表示。也就是说,Long.toHexString(-1L)返回的是"ffffffffffffffff",而不是带负号的字符串。如果你需要带符号的十六进制表示,得自己处理。

Long.toHexString(-1L); // "ffffffffffffffff" Long.toBinaryString(-1L); // 64个1 Long.toOctalString(-1L); // "1777777777777777777777"

我在做协议解析的时候,经常用toHexString把long转成十六进制看字节分布,比手动算方便得多。但要注意它不会补前导零,如果需要固定长度,得自己String.format("%016x", value)。

5.3 toString的实现与缓存

Long.toString(long)内部调用toString(long, 10)。这个方法有个小优化:对于小数字(绝对值小于某个阈值),会走一个更快的路径,直接用查表法生成字符串。大数字才走通用的除法循环。

另外,Long里还缓存了toString的结果吗?答案是没有。Integer有一个IntegerCache用于valueOf,但toString的结果并不缓存。每次调用toString都会新建字符串。所以在高频日志场景下,如果同一个Long要反复转字符串,可以考虑自己缓存。

6. equals、hashCode与compare:比较逻辑的正确姿势

6.1 equals的实现

public boolean equals(Object obj) { if (obj instanceof Long) { return value == ((Long)obj).longValue(); } return false; }

逻辑很简单:先判断类型,再比较值。注意它用的是instanceof而不是getClass(),这意味着如果有一个Long的子类(虽然Long是final的,不可能有子类),也会被认作相等。这是JDK里equals的常见写法。

6.2 hashCode为什么那么算

public int hashCode() { return Long.hashCode(value); } public static int hashCode(long value) { return (int)(value ^ (value >>> 32)); }

这个算法把64位的值的高32位和低32位异或,然后截断成32位。为什么这么做?因为HashMap的桶下标是基于hashCode的,如果直接用低32位,高位信息就丢了,容易产生碰撞。异或一下,高位信息就混进来了,分布更均匀。

这个技巧在Long、Double、UUID等64位值的hashCode里都能看到。理解了这个,你就明白为什么自己写hashCode时也要把高位混进来。

6.3 compare与compareUnsigned

Long.compare(long x, long y)的实现是:

public static int compare(long x, long y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); }

注意它没有用x - y,因为减法可能溢出。比如x = Long.MAX_VALUE,y = -1,x - y会溢出成负数,导致比较结果错误。用比较运算符虽然多几次判断,但绝对安全。

compareUnsigned则是把两个long当作无符号数比较。它的实现是:

public static int compareUnsigned(long x, long y) { return compare(x + Long.MIN_VALUE, y + Long.MIN_VALUE); }

加上MIN_VALUE相当于把有符号数的范围平移到无符号数的顺序上,这个技巧很巧妙。在处理哈希值、ID这类不关心符号的场景下,compareUnsigned比compare更合适。

实操提醒:Long的compareTo调用的是compare,所以TreeSet<Long>、TreeMap<Long, ?>默认用的是有符号比较。如果你放进去的是哈希值这种可能为负的数,排序结果可能不符合预期,需要自己传Comparator。

7. 那些年我在Long上踩过的坑

7.1 拆箱空指针

这是最经典的坑:

Long value = null; long v = value; // NullPointerException

自动拆箱时如果对象为null,直接NPE。这个坑在三目运算符里尤其隐蔽:

Long a = null; Long b = 10L; long result = true ? a : b; // NPE

因为三目运算符的类型对齐规则,a和b会被拆箱成long再运算,a为null就炸了。我见过一个项目里,这种写法在测试环境因为数据恰好不为空一直没暴露,上线后遇到空值直接崩。

防御方式:三目运算符里如果两个分支类型不一致,显式保证不出现null,或者用Optional、Objects.requireNonNullElse这类工具。

7.2 Long和long混用导致的比较错误

Long a = 1000L; long b = 1000L; System.out.println(a == b); // true

这里a == b为true,因为a会自动拆箱成long再和b比较。但如果是:

Long a = 1000L; Long b = 1000L; System.out.println(a == b); // false

两个Long比较就是引用比较,1000超出缓存范围,所以是false。一个操作数是基本类型时走值比较,两个都是包装类型时走引用比较,这个规则一定要记牢。

7.3 溢出无声无息

long的加减乘都可能溢出,而且不会抛异常:

long max = Long.MAX_VALUE; long result = max + 1; // 变成 Long.MIN_VALUE

在做金额、计数这类场景时,溢出会导致数据完全错误。JDK 8之后可以用Math.addExact、Math.multiplyExact,溢出时抛ArithmeticException:

try { long result = Math.addExact(max, 1); } catch (ArithmeticException e) { // 处理溢出 }

我在对账系统里就把所有关键的累加换成了addExact,虽然有一点性能开销,但能第一时间发现异常,比事后排查数据对不上要划算得多。

7.4 序列化时的兼容性

Long实现了Serializable,序列化时写的是long值。但如果你把Long和long混在同一个字段上(比如版本升级时改了类型),反序列化可能出问题。更稳妥的做法是保持类型稳定,或者用自定义的序列化策略。

8. 从Long看包装类的设计套路

把Long读完,你会发现JDK包装类的设计有一套固定套路:不可变字段 + 缓存 + 静态工厂 + 位运算工具 + 严格的解析逻辑。这套套路在Integer、Short、Byte、Character上几乎一模一样,只是缓存范围和位宽不同。

理解这套套路的价值在于,当你需要自己写一个值对象时,可以借鉴这些设计:用final字段保证不可变、用静态工厂控制实例创建、用缓存优化高频值、用位运算提升性能、用严格的边界检查防止溢出。这些不是Long独有的技巧,而是JDK沉淀下来的通用工程经验。

我个人在读完Long之后,最大的收获不是记住了某个API,而是养成了一个习惯:看到包装类就想到缓存边界,看到比较就想到拆箱,看到累加就想到溢出。这三个条件反射,帮我避开了不少生产事故。如果你还没系统读过Long,建议从valueOf和parseLong这两个方法入手,把每一行都推演一遍,收获会比泛读十个类还大。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 1:02:58

Vue3+Webpack5+Element Plus中后台工程化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 0:50:01

Codex 让浏览器自己干活:从 MCP 接入到批量任务落地的完整链路

在多数团队里&#xff0c;真正吃掉时间的不是写代码&#xff0c;而是每天重复的网页手工操作&#xff1a;登录后台、翻页筛选、导出报表、再粘到表格里核对&#xff0c;一套动作十几分钟&#xff0c;一天重复好几轮。 传统 Playwright 自动化脚本解决过一部分问题&#xff0c;但…

作者头像 李华
网站建设 2026/10/15 0:47:50

superpowers:用SKILL.md让Claude Code变身高效率AI开发助手

1. 先搞清楚&#xff1a;superpowers 到底是什么如果你平时用 Claude Code 比较多&#xff0c;可能早就遇到了一个让我当初很头疼的问题&#xff1a;底层模型确实聪明&#xff0c;但你在对话里让它"做个深度调研""写一套完整的单测""把这次的改动整理…

作者头像 李华
网站建设 2026/10/15 0:41:12

西安奕致格客服热线上线:怎么联系、该打哪个号、咨询前准备什么

活动策划项目的沟通有一个共同特点&#xff1a;问题往往集中在筹备期的最后几周集中出现。场地确认、流程调整、物料变更、现场衔接&#xff0c;每一件都需要快速找到对的人。过去&#xff0c;西安奕致格文化传媒有限公司&#xff08;简称奕致格&#xff09;对外的沟通主要依赖…

作者头像 李华
网站建设 2026/10/15 0:40:37

Traefik 实战详解:从动态服务发现到 HTTPS 自动证书的容器网关

1. 先搞清楚 Traefik 到底是个什么东西很多同学第一次听到 Traefik 这个名字&#xff0c;是从 Docker 或者 Kubernetes 的部署教程里瞟到的。当时我在某公司做容器化改造&#xff0c;第一个月就被这个工具折腾得不轻。现在回头看&#xff0c;它确实值得单独写一篇完整的使用详解…

作者头像 李华
网站建设 2026/10/15 0:34:19

Haskell finite-fields 库实战:有限域算术从原理到 Reed-Solomon 编码

简介&#xff1a;这是一份面向Haskell开发者与密码学、代数几何学习者的有限域算术实现库&#xff0c;聚焦在类型安全与工程可用性之间寻找平衡。包内已实现通用素数字段、小素数字段&#xff08;p < 2^31&#xff09;、基于Conway多项式预计算表的小型Galois域&#xff0c;…

作者头像 李华