如果你让我从 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,双引号包起来的是String; true和false只能是boolean;null是唯一的空值字面量,它的类型是特殊的 null type。
这就是为什么System.out.println(10)和System.out.println(10L)看似差不多,实际调用的却是完全不同的重载方法:前者走println(int),后者走println(long)。编译器在看到字面量的瞬间就已经确定了它的基本类型,后续所有运算、重载解析、自动转换,都在这个类型基础上展开。
var关键字(Java 10+)也没有改变这套规则。var a = 10;推断出的类型依然是int,var b = 3.14;推断出的类型就是double,var c = "hi";推断出的类型就是String。它只是把“写类型”这个操作交给了编译器,类型判定的依据仍然是字面量本身。
理解这一层,你才能接着往下看:为什么数字字面量会有那些匪夷所思的边界问题,为什么字符串字面量会进池子,为什么null的行为和任何普通值都不一样。
2. 整数与小数字面量:进制、边界、精度,一张表讲清楚
2.1 整数字面量有四种进制,还有下划线分隔写法
很多人以为整数就只是十进制,其实 Java 里整数字面量一共支持四种写法:
| 进制 | 前缀 | 示例 | 十进制值 |
|---|---|---|---|
| 十进制 | 无 | 100 | 100 |
| 八进制 | 0 | 0100 | 64 |
| 十六进制 | 0x | 0x64 | 100 |
| 二进制 | 0b | 0b1100100 | 100 |
八进制现在见得少了,但老代码、文件权限等场景仍可能出现,至少要能看懂:看到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到2147483647,long的范围是-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/F:
double 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'是true,'a' + 1的结果是98,类型是int而不是char。要还原成字符必须强转:
char ch = 'a'; System.out.println(ch + 1); // 98,int 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"); // true,final 常量参与编译期计算这些细节是面试常考点,但真正写业务代码时,我更建议只记住一个原则:字符串内容比较永远走equals(),不要依赖==。==只有在你明确想判断“是不是同一个对象”时才用。
3.3 字符与字符串最容易混淆的几种写法
因为'a'和"a"只差一个引号,很多人会在不该用的时候用错。下面这张对照表建议直接收藏:
| 写法 | 类型 | 说明 |
|---|---|---|
'' | 非法 | 空字符字面量,编译错误 |
'a' | char | 16 位无符号整数,可参与运算 |
"a" | String | 字符串常量池中的引用 |
'ab' | 非法 | 单引号里只能一个字符 |
"ab" | String | 字符串,长度不限,可为空 |
" " | String | 包含一个空格的字符串 |
null | 引用 | 空值字面量,后文细说 |
一个很经典的输出题:'a' + 'b'和'a' + "b"结果完全不同。前者char和char相加会提升成int,结果是195;后者char和字符串相加会自动拼接成字符串"ab"。
而空字符串""和null又是完全不同的东西:""是一个长度为 0 的字符串对象,null是“没有任何对象”。混淆它们的后果体现在所有调用上:"".length()返回0,null.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当作true,if (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是true,200就变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到127,Character缓存0到127,Boolean因为只有两个值,直接缓存了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); // true:intern 回常量池 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来得踏实。带新人时我也常讲:字面量这张表,比背一百道八股有用得多,它是面试题的根,也是代码可读性的底线。希望这篇整理能帮你把整数、小数、字符串、字符、布尔、空值这六关一次打通。