当前位置: 代码网 > it编程>编程语言>Java > Java中单引号与双引号的底层硬核解析

Java中单引号与双引号的底层硬核解析

2026年08月15日 Java 我要评论
面试官问:‘1’+‘2’ 等于多少?如果你回答“12”,那么恭喜你,成功掉进了面试官挖的坑里!在 java 中,单引号('

面试官问:‘1’+‘2’ 等于多少?如果你回答“12”,那么恭喜你,成功掉进了面试官挖的坑里!

在 java 中,单引号(' ')和双引号(" ")看似只是符号的差异,实则代表了两种完全不同的数据类型和内存模型

很多工作 3-5 年的老手,依然会在拼接 sql、处理 json 或进行字符运算时被这两个符号坑得怀疑人生。今天这篇文章,我们从内存位数、运算本质、jvm 存储三个维度,彻底把它们扒个底朝天。

一、核心本质:一句话定生死

先记住这句“保命口诀”:

  • 单引号:包裹的是 char(字符型),属于基本数据类型
  • 双引号:包裹的是 string(字符串型),属于引用数据类型

虽然 string 底层也是由 char[](java 8)或 byte[](java 9+)构成的,但它们在 jvm 中的地位有着云泥之别。

二、内存与位数硬核对比(重点)

结合你关心的内存布局,我们来看看它们在 jvm 中的真实占用:

对比维度单引号 ' ' (char)双引号 " " (string)
数据类型基本类型(primitive)引用类型(reference)
内存大小固定 16 bit(2 字节)至少 16 字节(对象头 12b + 引用 4b)+ 底层数组开销
存储位置栈内存(直接存数值)堆内存(对象)+ 栈内存(存地址引用)
默认值'\u0000'(空字符,数值为 0)null(空指针)
能否存储空编译报错('' 是非法的)合法("" 代表空字符串对象)

关于 char 的 2 字节(16 bit):java 使用 unicode(utf-16)编码,char 可以存储包括中文在内的绝大多数常用字符(如 '中')。但对于某些生僻字或 emoji(如 “😂”),需要 2 个 char(代理对)才能存下,这一点在遍历字符串时极容易踩坑。

三、面试高频致命陷阱:+号运算逻辑

这是面试中 90% 的人都会答错的点。两者的“加法”逻辑截然不同:

1. 单引号char做加法 =数值运算(ascii/unicode 码点相加)

因为 char 本质是一个无符号整数,参与 + 运算时会自动类型提升int

system.out.println('a' + 1);   // 输出:66(因为 'a' 的码点是 65,65+1=66)
system.out.println('1' + '2'); // 输出:99(字符 '1' 码点 49 + 字符 '2' 码点 50 = 99)

2. 双引号string做加法 =字符串拼接

只要 + 号两端有一个string 类型,java 编译器就会将其视为字符串拼接,并调用 stringbuilder 进行 append。

system.out.println("a" + 1);   // 输出:"a1"(1 被自动转为字符串)
system.out.println("1" + "2"); // 输出:"12"

3. 终极混合题(必看!)

system.out.println('a' + "b"); // 输出:"ab"(因为遇到双引号,转为字符串拼接)
system.out.println('a' + 'b' + "c"); // 先算 'a'+'b' = 97+98 = 195,再拼 "c",输出:"195c"

四、生产环境中的“血泪坑”

坑 1:sql 注入与拼接混乱

新手拼接动态 sql 时,常常被单双引号嵌套绕晕:

// ❌ 极易出错且存在 sql 注入风险
string sql = "select * from user where name = '" + username + "'"; 

username 包含单引号时,sql 直接报错或被注入。解决方案:放弃拼接,使用 preparedstatement

坑 2:判断空字符(npe 与编译错误)

char c = '';    // ❌ 编译报错:空字符常量非法
string s = "";  // ✅ 合法,代表一个空字符串对象

// 实际业务判断:
if (str != null && !"".equals(str)) { // 正确判空方式
    // ...
}

坑 3:遍历字符串中的 emoji 乱码

由于 char 只有 16 bit,无法容纳 “😂”(u+1f602)这个码点。

string emoji = "😂";
system.out.println(emoji.length()); // 输出:2(因为底层占用了 2 个 char)
char c = emoji.charat(0);           // 拿到的是高代理项,打印出来是乱码

正确做法:使用 codepoint 系列方法遍历。

这是开发中最容易被忽视的坑。我们先用一段代码直观对比 charat()codepoint 遍历的区别:

错误示范:使用charat()遍历

public class codepointdemo {
    public static void main(string[] args) {
        string str = "hi😂世界𠮷";  // 包含 emoji 和生僻字
        
        system.out.println("=== charat() 遍历(错误方式)===");
        for (int i = 0; i < str.length(); i++) {
            char c = str.charat(i);
            system.out.println("索引 " + i + ":" + c);
        }
    }
}

输出结果:

=== charat() 遍历(错误方式)===
索引 0:h
索引 1:i
索引 2:?   ← 乱码!取到了高代理项
索引 3:?   ← 乱码!取到了低代理项
索引 4:世
索引 5:界
索引 6:?   ← 乱码!取到了高代理项
索引 7:?   ← 乱码!取到了低代理项

分析"😂""𠮷" 各占 2 个 charcharat() 把它们拆成了独立的代理项,导致输出乱码。同时,str.length() 返回的是 8(字符单元数),而不是真正的字符个数 5

正确示范:使用codepoint系列方法遍历

public class codepointdemo {
    public static void main(string[] args) {
        string str = "hi😂世界𠮷";
        
        system.out.println("=== codepoint 遍历(正确方式)===");
        int len = str.codepointcount(0, str.length()); // 获取真正的字符个数
        system.out.println("实际字符个数(码点数):" + len);
        
        int index = 0;
        while (index < str.length()) {
            int codepoint = str.codepointat(index);
            // 判断当前码点是否占用 2 个 char(即是否是代理对)
            int charcount = character.charcount(codepoint);
            
            system.out.println("索引 " + index + 
                               ",码点:u+" + integer.tohexstring(codepoint).touppercase() + 
                               ",字符:" + new string(character.tochars(codepoint)) +
                               ",占用 char 数:" + charcount);
            
            index += charcount; // 跳过已处理的所有 char
        }
    }
}

输出结果:

=== codepoint 遍历(正确方式)===
实际字符个数(码点数):5
索引 0,码点:u+48,字符:h,占用 char 数:1
索引 1,码点:u+69,字符:i,占用 char 数:1
索引 2,码点:u+1f602,字符:😂,占用 char 数:2
索引 4,码点:u+4e16,字符:世,占用 char 数:1
索引 5,码点:u+754c,字符:界,占用 char 数:1
索引 6,码点:u+20bb7,字符:𠮷,占用 char 数:2

正确遍历:每个 unicode 码点(包括 emoji 和生僻字)都能完整输出,codepointcount() 返回的是 5(真正的字符个数)。

封装工具方法(生产环境直接复用)

/**
 * 将字符串安全地按 unicode 码点拆分为字符数组
 * 适用于包含 emoji、生僻字的任意字符串
 */
public static list<string> splitcodepoints(string str) {
    list<string> result = new arraylist<>();
    int index = 0;
    while (index < str.length()) {
        int codepoint = str.codepointat(index);
        result.add(new string(character.tochars(codepoint)));
        index += character.charcount(codepoint);
    }
    return result;
}

// 使用示例
string str = "hi😂世界𠮷";
list<string> chars = splitcodepoints(str);
system.out.println(chars); // 输出:[h, i, 😂, 世, 界, 𠮷]

五、性能层面的思考(gc 角度)

  • char 是“零开销”:它只是一块 16 bit 的内存空间,不涉及 gc(垃圾回收)。
  • string 是“重资产”:即使你写一个 string s = "a";,jvm 也要创建完整的 string 对象和底层的 byte[] 数组。如果你在循环中使用 += 拼接字符串,会产生大量临时垃圾对象,频繁触发 young gc。

性能建议

如果你只需要存储确定的一个字符(如性别标识 'm'/'f'、状态码 '1'-'5'),请优先使用 charbyte,而不是 string。这在构建几十万级的对象列表时,能节省上百 mb 内存。

六、有没有办法“优雅”地互相转换?

虽然两者不同,但在日常开发中频繁互转:

// char -> string
string s1 = string.valueof('a');
string s2 = character.tostring('a');

// string -> char (取第一个字符)
string str = "hello";
char c = str.charat(0); // 'h'

// 注意:如果字符串为空 "",charat(0) 会抛出 indexoutofboundsexception

总结:一张图记住所有区别

特征单引号 '双引号 "
类型char(基本)string(引用)
内存位数固定 16 bit至少 128 bit(16字节)以上
存储位置
+ 号含义加法运算(码点相加)字符串拼接
可否为空否(编译报错)是("")
推荐场景单字符常量、标志位文本消息、json、sql

最后留个思考题:下面这段代码输出什么?(欢迎在评论区留下你的答案!)

system.out.println("result: " + 'a' + 'b');
system.out.println('a' + 'b' + " result");

提示:答案不是 "result: ab""ab result" 哦!

原理解析:

  1. "result: " + 'a' + 'b':表达式从左到右执行。第一个 + 左边是 string 类型,因此 'a' 被自动转为字符串,结果为 "result: a";接着再拼接 'b',最终输出 result: ab
  2. 'a' + 'b' + " result":第一个 + 左右两边都是 char没有 string 参与,所以做的是数值加法'a'(码点65)+ 'b'(码点66)= 131(int)。

然后 131 + " result",遇到 string 才转为字符串拼接,最终输出 131 result

口诀:从左到右算,遇string才拼接;没遇stringchar就当数字加。

string str = "a😂b";
system.out.println(str.length());
system.out.println(str.codepointcount(0, str.length()));
system.out.println(str.charat(1));
system.out.println(str.codepointat(1));

提示:答案不是 3, 3, '😂', 128514 哦!因为 charat(1) 取到的是代理项,输出是乱码。

原理解析(重点!)

我们先把字符串 "a😂b" 在内存中的存储结构画出来:

逻辑字符unicode 码点底层 char 存储char 索引(下标)
'a'u+0041\u00410
'😂'u+1f602高代理项 \ud83d(占1个char)1
低代理项 \ude02(占1个char)2
'b'u+0042\u00423

逐行分析:

  1. str.length()4:length() 返回的是 char 单元的数量"a😂b" 底层共有 4 个 char(a占1个,😂占2个代理项,b占1个)。
  2. str.codepointcount(0, str.length())3:codepointcount() 返回的是 unicode 码点的个数(即真正的“人类可读字符数”)。'a''😂''b' 共 3 个。
  3. str.charat(1) → 输出高代理项(乱码):索引 1 正好是 '😂' 的高代理项 \ud83d。这是一个未被分配的保留字符,不是独立可打印字符。单独打印它时,控制台无法识别,通常会显示为乱码(? 或方块)。
  4. str.codepointat(1)128514:从索引 1 开始,codepointat() 会智能地识别代理对:发现索引 1 是高代理项,自动往后读索引 2 的低代理项,合并计算出完整码点。'😂' 的码点是 u+1f602,对应的十进制整数就是 128514(十六进制 0x1f602 转十进制)。

额外验证小技巧

如果你想把 codepointat(1) 的结果转回字符,可以这样验证:

int cp = str.codepointat(1);        // 128514
string emoji = new string(character.tochars(cp));
system.out.println(emoji);          // 输出:😂

以上就是java中单引号与双引号的底层硬核解析的详细内容,更多关于java单引号与双引号的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com