ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java字面量全面解析:六类字面量、常量池与面试考点

Java字面量全面解析:六类字面量、常量池与面试考点 如果你让我从 Java 里挑一个最容易被低估的基础概念我第一个提名字面量。整数、小数、字符串、字符、布尔、空值这些从你好世界那天起就天天出现在代码里的东西全都是字面量。但别小看它面试现场十个有八个倒在这一串问题上String s new String(abc)创建了几个对象a和a到底差在哪为什么0.1 0.2算出来不是0.3为什么Integer i 100时i j是true变成200就变false这篇不是带背八股而是把六类字面量的语法规则、类型判定、常量池底子和面试常见答法一次性讲透。不管是刚学 Java 的新手、准备校招社招的朋友还是带团队想给新人讲清楚原理的人都可以拿它当一份可收藏的实操笔记。我会尽量把“为什么”也讲了而不只是告诉你“是什么”。1. 字面量为什么值得单独花时间类型在编译期就被焊死了1.1 从表达式的最底端看起字面量就是代码里“写死”的那些值程序本质上就是数据加运算。数据从哪里来一部分来自外部输入参数、文件、数据库、网络请求另一部分就是直接写在源码里的常量。这种“直接写在源码里代表一个固定值”的语法单元就是字面量。比如int age 18; // 18 是整数int字面量 double price 99.9; // 99.9 是小数double字面量 char grade A; // A 是字符字面量 String name Java; // Java 是字符串字面量 boolean active true; // true 是布尔字面量 Object obj null; // null 是空值字面量注意字面量和变量的区别。变量是一个“有名有地址的存储位置”值可以变字面量是“裸的值”没有名字也没有办法被赋值修改。它和new出来的对象更不一样new是运行期行为对象的创建需要走构造器、分配内存字面量则是编译器直接读到的“常量原文”它的值在编译阶段就已经确定下来并被写进 class 文件的常量池里。为什么这个概念值得单独拎出来讲因为后面所有关于常量池、比较、自动装箱、浮点精度的问题根子都在字面量上。你把字面量的规则吃透了那些面试题就不再是死记硬背的答案而是可以推出来的结论。1.2 编译器一见面就定生死每个字面量的“身份”是语法规则决定的Java 是静态类型语言字面量从被编译器读入的那一刻起类型就不是写代码的人“说了算”而是由语法规则自动决定没有任何“我声明它是什么它就是什么”的自由没有小数点的数字默认是int加了L才是long带小数点的数字默认是double加了f/F才是float单引号包起来的是char双引号包起来的是Stringtrue和false只能是booleannull是唯一的空值字面量它的类型是特殊的 null type。这就是为什么System.out.println(10)和System.out.println(10L)看似差不多实际调用的却是完全不同的重载方法前者走println(int)后者走println(long)。编译器在看到字面量的瞬间就已经确定了它的基本类型后续所有运算、重载解析、自动转换都在这个类型基础上展开。var关键字Java 10也没有改变这套规则。var a 10;推断出的类型依然是intvar b 3.14;推断出的类型就是doublevar c hi;推断出的类型就是String。它只是把“写类型”这个操作交给了编译器类型判定的依据仍然是字面量本身。理解这一层你才能接着往下看为什么数字字面量会有那些匪夷所思的边界问题为什么字符串字面量会进池子为什么null的行为和任何普通值都不一样。2. 整数与小数字面量进制、边界、精度一张表讲清楚2.1 整数字面量有四种进制还有下划线分隔写法很多人以为整数就只是十进制其实 Java 里整数字面量一共支持四种写法进制前缀示例十进制值十进制无100100八进制0010064十六进制0x0x64100二进制0b0b1100100100八进制现在见得少了但老代码、文件权限等场景仍可能出现至少要能看懂看到0123要知道这是八进制它的值是83而不是123。这种“看起来像十进制实际不是”的写法是实实在在的坑尤其容易出现在定义常量时。十六进制在位运算、颜色值、内存地址场景很常见0xFF一眼就能看出是 255。二进制写法则多见于掩码和位运算0b0001_0000比记十六进制更直观。Java 7 开始数字字面量里可以插入下划线来提升可读性这是一个很少有人用但我特别推荐大家用起来的功能long creditCard 1234_5678_9012_3456L; int phone 138_0000_0000; double pi 3.14_15;下划线只是给人看的编译器完全不 care它只负责把它剥掉再解析。但下划线不是想放哪儿就放哪儿规则的记忆方式就一句话不能在数字开头和结尾、不能紧邻小数点、不能紧邻进制前缀、不能紧邻类型后缀。下面这些都是编译错误int a _100; // 错误不能放在开头 int b 100_; // 错误不能放在结尾 double c 3._14; // 错误不能紧邻小数点 double d 3.14_; // 错误不能紧邻小数点 long e 123_L; // 错误不能紧邻类型后缀 long f 0x_123L; // 错误不能紧邻进制前缀可以不用但见过要认识。面试官真考这个的时候你答出来就是加分项。2.2long和int的边界是选择题和机考题的重灾区int的范围是-2147483648到2147483647long的范围是-9223372036854775808到9223372036854775807。字面量在写的时候必须落在对应范围内否则编译器直接报错。最经典的一个问题是int x 2147483648;为什么编译不过因为2147483648已经超出int范围编译器眼中它就不再是一个合法的 int 字面量。你想保存这个数必须显式加上L后缀写成long x 2147483648L;。有个容易混淆的边界题int min -2147483648;能不能编译能。这是 Java 语法里专门留的口子一元负号运算符可以把2147483648这个超出范围的十进制字面量当作int的最小值处理。但注意单写2147483648仍然是非法的-2147483648能过是因为负号参与之后结果落回了int范围。实际写代码我更建议直接用Integer.MIN_VALUE语义清晰也不会让读代码的人犯嘀咕。同理想表示long的最大值正确写法是9223372036854775807L差一位变成9223372036854775808L同样直接报错。这里还要提醒一个细节l后缀小写字母 L太容易和数字1混淆了团队规范里都会强制用大写的L。你自己写的时候也别图省事用l。2.3 小数字面量默认是double想用float必须带后缀小数字面量的默认类型是double不是float。很多初学者以为“只要是小数就是 float”这个理解是错的。3.14在 Java 里是double要写成float必须加f/Fdouble d1 3.14; // 正确默认 double double d2 3.14d; // 正确显式写 d float f1 3.14; // 错误double 不能隐式转成 float float f2 3.14f; // 正确float f1 3.14;报错的原因不是“精度缺失”而是“double 到 float 是窄化转换”编译器不允许直接隐式转换。与之相对float f 10;却能编译通过因为int到float是拓宽转换虽然也可能损失精度但 Java 语法允许它隐式发生。小数字面量还支持科学计数法写法是1e3、2.5E-2这种形式。注意1e3的类型是double值是1000.0不是int。1e10这个数字也经常有人写错它本质是 double 字面量不要试图存进 int 变量或者至少别用错类型。double a 1e3; // 1000.0 double b 1.5e-3; // 0.0015 float c 1.5e3f; // 1500.0f也可以加 f2.4 为什么0.1 0.2不等于0.3字面量精度问题的根源如果你只把它当“面试段子”记后面遇到浮点数相关的换算还是会翻车。它的原理其实不复杂计算机用二进制存储小数但大部分十进制小数无法用有限的二进制位精确表示。就像十进制里1/3是0.3333...永远除不尽一样二进制里0.1也是一个无限循环小数。IEEE 754 标准下的 float 和 double 只能保存有限位于是存下来的0.1并不是精确的0.1。double a 0.1; double b 0.2; System.out.println(a b); // 0.30000000000000004这不是 Java 的 bug所有支持 IEEE 754 的语言都会这样。C、JavaScript、Python 一个都跑不掉。实际业务里判断两个小数是否相等不能直接要么给一个误差范围double result 0.1 0.2; boolean ok Math.abs(result - 0.3) 1e-9;要么金额计算直接用BigDecimal但坑在于必须用字符串构造BigDecimal x new BigDecimal(0.1); BigDecimal y new BigDecimal(0.2); BigDecimal z x.add(y); // 0.3如果图省事写new BigDecimal(0.1)那你传进去的其实是已经丢过精度的 double 值还不如直接加误差判断。这个细节我在代码审查里见过太多次值得单独提醒。3. 字符与字符串字面量单引号和双引号之间的整套学问3.1 字符字面量必须满足的“三条规定”字符字面量用单引号必须同时满足三条规则里面恰好是“一个字符”不能是空的不能在单引号里塞多个字符。这里的“一个字符”在 TCP 中文语境下还有一层隐蔽的含义——Java 的char是一个 UTF-16 编码单元长度固定 16 位最经典的例子就是 emoji。char c1 a; // 正确 char c2 中; // 正确中 在 BMP 内单码元 char c3 ; // 错误这个 emoji 需要两个代理码元一个 char 装不下 String s ; // 正确字符串没有这个限制char在内存里本质上是一个无符号的 16 位整数范围0到65535可以参与算术和比较。a b是truea 1的结果是98类型是int而不是char。要还原成字符必须强转char ch a; System.out.println(ch 1); // 98int System.out.println((char)(ch 1)); // b需要强转这条性质平时写代码可能体会不深但在做字符统计、大小写转换、循环输出字母表的时候它就是最底层的工作原理。3.2 字符串字面量为什么是“引用类型姿势”字符串字面量是双引号包起来的一串字符String是引用类型字符串字面量不是简单地把字符排出来它会被编译到 class 文件的字符串常量池中。同一份字节码里内容相同的字符串字面量只会有一份实例所以abc写十遍运行时它们本质上是同一个对象。这个机制带来的第一个结论就是编译期折叠String s1 abc; String s2 a b c; System.out.println(s1 s2); // true因为编译阶段 s2 已经折叠成 abc注意这里必须是“字面量与字面量相加”编译器才能在编译期直接算出结果。一旦掺入变量性质就变了String a a; String s3 a bc; // 运行时通过 StringBuilder 生成新对象 System.out.println(s3 abc); // false但如果变量本身是final的编译器也能在编译期把它代入并完成折叠final String a a; String s4 a bc; System.out.println(s4 abc); // truefinal 常量参与编译期计算这些细节是面试常考点但真正写业务代码时我更建议只记住一个原则字符串内容比较永远走equals()不要依赖。只有在你明确想判断“是不是同一个对象”时才用。3.3 字符与字符串最容易混淆的几种写法因为a和a只差一个引号很多人会在不该用的时候用错。下面这张对照表建议直接收藏写法类型说明非法空字符字面量编译错误achar16 位无符号整数可参与运算aString字符串常量池中的引用ab非法单引号里只能一个字符abString字符串长度不限可为空 String包含一个空格的字符串null引用空值字面量后文细说一个很经典的输出题a b和a b结果完全不同。前者char和char相加会提升成int结果是195后者char和字符串相加会自动拼接成字符串ab。而空字符串和null又是完全不同的东西是一个长度为 0 的字符串对象null是“没有任何对象”。混淆它们的后果体现在所有调用上.length()返回0null.length()直接抛 NPE。3.4 转义字符和 Unicode 转义的隐藏坑字符串和字符字面量里经常用到转义字符最常用的几个必须背下来转义含义\n换行\t制表符\双引号\单引号\\反斜杠\b退格\r回车\f换页写 Windows 路径时最容易翻车。比如你想表示C:\Users\admin\test.txt直接写String path C:\Users\admin\test.txt; // 错误这里面的\U和\t都会被当成转义序列解析\U还不是合法转义编译器会直接报错。更诡异的是\u在 Java 里是特殊的 Unicode 转义语法上必须紧跟四位十六进制数字于是C:\u1234就成了一个 Unicode 字符。路径建议用正斜杠C:/Users/admin/test.txt或者把反斜杠写成双反斜杠C:\\Users\\admin\\test.txt。还有个冷知识般的坑Java 的 Unicode 转义是在词法分析之前处理的所以它连注释都不放过。如果注释里出现了\u000a换行符的 Unicode 转义编译器会先把这段改成真正的换行然后这行注释就“断头”了后面的内容会被当成正常代码编译报一堆莫名其妙的错误。遇到这种情况先往转义字符那边想。4. 布尔与空值两个“只有一种答案”的字面量反而最容易翻车4.1 布尔字面量true和false之外的边界问题布尔字面量只有两个true和false。Java 没有像 C 语言那样把0当作false、非0当作trueif (1)在 Java 里是编译错误boolean和int之间不存在隐式转换。这是 Java 刻意做的设计目的就是让条件判断的语义严格、无歧义。实际写布尔变量时最容易出现的问题不是字面量本身而是自动拆箱。比如Boolean flag null; if (flag) { // NPE自动拆箱时 flag 是 null }Boolean是包装类型if (flag)会触发自动拆箱null无法拆成基本类型boolean直接抛 NPE。类似的还有Integer n null; if (n 0)同样会炸。用包装类型做判断之前一定要先判空或者直接用Boolean.TRUE.equals(flag)这种带着空值容忍的比较方式。还有一个写代码风格的问题if (flag true)是很多新人会犯的冗余写法。flag本身就是boolean直接if (flag)就够了。反过来if (!flag)也比if (flag false)清晰得多。4.2null到底是什么类型为什么所有引用类型都能接住它null是空值字面量它不属于任何自定义类它的类型是唯一的 null type。这个类型没有名字你永远写不出一个类型叫nullType的变量但编译器在内部确实会把它当作一种特殊类型处理。也正因为如此null可以赋值给任何引用类型包括类、接口、数组String s null; Object o null; int[] arr null; Comparable c null;但null不能赋值给基本类型int n null;是编译错误。这个区别看起来简单实际代码里因为“想表达没有值”而把int改成Integer的情况特别常见这就是 int 和 Integer 的取舍问题业务里确实可能出现“无值”要用包装类型但代价就是前面说的拆箱 NPE 风险。null也不是对象它不是某个“空对象”的实例对它做任何成员访问都会抛 NullPointerException。null instanceof String的结果是false不是true也不是异常——instanceof对null的操作是安全的。null和空字符串、空集合完全不是一回事。String s 是“有字符串长度为 0”List list null是“连容器都不存在”而new ArrayList()是“有容器但里面没数据”。三者混为一谈是最常见的错误之一。4.3 重载方法传null的歧义题null作为参数传给重载方法时会出现一个很经典的选择题void test(String s) {} void test(Integer i) {}调用test(null)能编译吗不能。因为String和Integer没有继承关系编译器找不到哪一个“更具体”于是直接判定为歧义报编译错误。但如果你这样写void test(String s) {} void test(Object o) {}test(null)就能编译并且会选String版本。原因是String是Object的子类编译器选择“最具体”的实参匹配。这个点面试闪电题经常出现建议代码里尽量避免写出这种靠 null 决定语义的重载。实际开发中我更推荐这样处理 null入参校验用Objects.requireNonNull(param)在方法入口快速拦截 null让错误尽早暴露返回值设计宁可返回空集合或Optional也少返回null替调用方省一层判空字符串判空abc.equals(str)比str.equals(abc)安全得多前者天然能处理str为null的情况。5. 面试连环问字面量、常量池、自动装箱我按源码思路拆一遍5.1 第一串new String(abc)到底创建了几个对象这道题几乎每个 Java 面试都会出现。拆解它需要先分清两层概念字面量abc的处理发生在编译期和类加载阶段new的执行发生在运行期。如果字符串常量池里还没有abc那么new String(abc)会创建两个对象一个是在常量池里的abc字面量对象另一个是new出来的、在堆上的新 String 对象。两个对象的内容一样但不是同一个对象。如果常量池里已经有abc了那么new String(abc)只创建一个堆对象字面量那个是复用之前的。所以标准答案不是“两个”而是“分情况池里没有就两个有就一个”。很多候选人只说“两个”面试官一追问“池里已经有了呢”就露馅。记得把intern()也一起理解new String(abc).intern()返回的是常量池里的对象如果池里还没有则先写入池再返回。5.2 第二串Integer i 100时为什么i j是true200就变false先看这段代码Integer i1 100; Integer i2 100; System.out.println(i1 i2); // true Integer i3 200; Integer i4 200; System.out.println(i3 i4); // false原因在自动装箱。Integer i 100底层实际调用的是Integer.valueOf(100)而valueOf有一个内置缓存-128到127范围内的 Integer 对象在类加载时就提前创建好了取值直接返回同一个缓存实例。100落在缓存范围内i1和i2拿到的是同一个对象比较的是引用所以是true。200超出缓存范围valueOf只能每次new一个新对象i3和i4指向堆上不同对象就是false。这个机制不是只对 Integer 生效。Byte、Short、Long都缓存-128到127Character缓存0到127Boolean因为只有两个值直接缓存了TRUE和FALSE两个实例。面试考到这里通常会追加一句“那我写new Integer(100)呢”答案也很明确显式new一定创建新对象不走缓存永远不可能是true。所以比较包装类型的值老老实实用equals()。5.3 第三串字符串与equals的“四条判断题”字符串比较也是字面量问题的重灾区规律整理成一张表String a abc; String b abc; System.out.println(a b); // true同一个常量池对象 String c new String(abc); System.out.println(a c); // false堆上新对象 System.out.println(c.intern() a); // trueintern 回常量池 String d a bc; System.out.println(a d); // true编译期折叠 String e a; String f e bc; System.out.println(a f); // false运行时拼接把这条逻辑捋顺之后字符串那几条经典面试题基本都能推出来。“字符串逆序”“字符串排序”这类操作题也建立在字符串不可变这个本质上String本身没有reverse()方法要拐个弯用StringBuilder.reverse()因为每次“修改”字符串都会产生新对象。5.4 让面试官觉得你懂原理的答题口径面试答这类题最忌讳只报答案。正确的姿势是先搭一个“编译期/运行期”的框架哪些事情发生在编译期哪些事情发生在运行期再落到具体机制上。比如答字符串题你可以这样说“字符串字面量在编译期会被折叠、进入 class 文件常量池类加载时被解析到运行时常量池我如果new String(abc)池里的对象是字面量解析产生的new 则是运行期在堆里再创建一个对象两者不为同一个引用。JDK 7 以后字符串常量池移到了堆里JDK 9 以后 String 底层实现从char[]换成了byte[]但对外 API 行为不变。”能主动把版本演进说清楚通常比背三遍答案更让面试官有印象。答自动装箱题则要把落脚点放在valueOf和缓存范围上不要只说“100 和 200 不一样”这种经验结论。如果想把原理看透自己做个实验最踏实。写个最简单的类用javap -verbose看常量池能看到CONSTANT_String_info、CONSTANT_Integer_info这些条目比从博客上看十遍都记得牢。javap -verbose Main.class我自己的习惯是凡是涉及字面量的疑点一律写个小 Demo 跑一遍再打开字节码对着常量池看。面试题背得再熟不如自己亲眼看到那一行行CONSTANT_String_info来得踏实。带新人时我也常讲字面量这张表比背一百道八股有用得多它是面试题的根也是代码可读性的底线。希望这篇整理能帮你把整数、小数、字符串、字符、布尔、空值这六关一次打通。
返回列表