金九银十,收下这份 Java String 面试题
请点赞关注,你的支持对我意义重大。
Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。
前言
大家好,我是小彭。
过去两年,我们在掘金平台上发布 JetPack 专栏文章,小彭也受到了大家的意见和鼓励。最近,小彭会陆续搬运到公众号上。
在每种编程语言里,字符串都是一个躲不开的话题,也是面试常常出现的问题。在这篇文章里,我将总结 Java 字符串中重要的知识点 & 面试题 ,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
学习路线图:
1. C 和 Java 中字符串和字符数组的对比
1.1 内存表示不同
- 在 C 语言中,字符串和字符数组相同。字符串本质上是以
\0
为结束符的字符数组字符数组,因此字符串和字符数组在本质上相同,都是一块连续的内存空间,以需要转义\0
为结束符。C 语言是不关心 char[] 里存储字符的编码方式的,只有通过程序的上下文确定; - 在 Java 中,字符串和字符数组不同。字符串是 String 对象,而字符数组是数组对象,均不需要结束符。如果是数组对象,对象内存区域中有一个字段表示数组的长度,而 String 相当于字符数组的包装类。内部包装了一个基于
UTF-16 BE
编码的字符数组(从 Java 9 开始变为字节数组)。其他字符编码输入的字节流在进入 String 时都会被转换为UTF-16 BE
编码。
java.lang.String
public final class String {
private final char value[];
private int hash;
...
}
1.2 char 类型的数据长度
- 在 C 语言中,char 类型占 1 字节,分为有符号与无符号两种;
- 在 Java 中,char 类型占 2 字节,只有无符号类型。
语言 | 类型 | 存储空间(字节) | 最小值 | 最大值 |
---|---|---|---|---|
Java | char | 2 | 0 | 65535 |
C | char(相当于signed char) | 1 | -128 | 127 |
C | signed char | 1 | -128 | 127 |
C | unsigned char | 1 | 0 | 255 |
2. 为什么 Java 9 String 内部将 char 数组改为 byte 数组?
Java String 的内存表示本质上是基于 UTF-16 BE
编码的字符数组。UTF-16 是 2 个字节或 4 个字节的变长编码,这意味着即使是 UniCode 字符集的拉丁字母,使用 ASCII 编码只需要一个字节,但是在 String 中需要两个字节的存储空间。
为了优化存储空间,从 Java 9 开始,String 内部将 char 数组改为 byte 数组,String 会判断字符串中是否只包含拉丁字母。如果是的话则采用单字节编码(Latin-1),否则使用 UTF-16 编码。
String.java (since Java 9)
private final byte coder;
static final boolean COMPACT_STRINGS;
static {
COMPACT_STRINGS = true;
}
byte coder() {
return COMPACT_STRINGS ? coder : UTF16;
}
@Native static final byte LATIN1 = 0;
@Native static final byte UTF16 = 1;
不同编码实现的简单区别如下:
编码格式 | 编码单元长度 | BOM | 字节序 |
---|---|---|---|
UTF-8-无BOM | 1 ~ 4 字节 | 无 | 大端序 |
UTF-8 | 1 ~ 4 字节 | EF BB BF | 大端序 |
UTF-16-无BOM | 2 / 4 字节 | 无 | 大端序 |
UTF-16BE(默认) | 2 / 4 字节 | FE FF | 大端序 |
UTF-16LE | 2 / 4 字节 | FF FE | 小端序 |
UTF-32-无BOM | 4 字节 | 无 | 大端序 |
UTF-32BE(默认) | 4 字节 | 00 00 FE FF | 大端序 |
UTF-32LE | 4 字节 | FF EE 00 00 | 小端序 |
关于字符编码的更多内容,见: 计算机基础:今天一次把 Unicode 和 UTF-8 说清楚
3. String & StringBuilder & StringBuffer 的区别
3.1 效率
String 是不可变的,每次操作都会创建新的变量,而另外两个是可变的,不需要创建新的变量;另外,StringBuffer 的每个操作方法都使用 synchronized 关键字保证线程安全,增加了更多加锁 & 释放锁的时间。因此,操作效率的简单排序为:StringBuilder > StringBuffer > String。
3.2 线程安全
String 不可变,所以 String 和 StringBuffer 都是线程安全的,而 StringBuilder 是非线程安全的。
类型 | 操作效率 | 线程安全 |
---|---|---|
String | 低 | 安全(final) |
StringBuffer | 中 | 安全(synchronized) |
StringBuilder | 高 | 非安全 |
4. 为什么 String 设计为不可变类?
4.1 如何让 String 不可变?
《Effective Java》中 可变性最小化原则,阐述了不可变类的规则:
- 1、不对外提供修改对象状态的任何方法;
- 2、保证类不会被扩展(声明为 final 类或 private 构造器);
- 3、声明所有域为 final;
- 4、声明所有域为 private;
- 5、确保对于任何可变性组件的互斥访问。
以上规则 String 均满足。
4.2 为什么 String 要设计为不可变?
- 1、不可变类 String 可以避免修改后无法定位散列表键值对: 假设 String 是可变类,当我们在 HashMap 中构建起一个以 String 为 Key 的键值对时,此时对 String 进行修改,那么通过修改后的 String 是无法匹配到刚才构建过的键值对的,因为修改后的 hashCode 可能是变化的。而不可变类可以规避这个问题。
- 2、线程安全: 不可变对象本质是线程安全,不需要同步;
提示: 反射可以破坏 String 的不可变性。
5. String + 的实现原理
String +
操作符是编译器语法糖,编译后会被替换为 StringBuilder#append(...)
语句,例如:
示例程序
// 源码:
String string = null;
for (String str : strings) {
string += str;
}
return string;
// 编译产物:
String string = null;
for(String str : strings) {
StringBuilder builder = new StringBuilder();
builder.append(string);
builder.append(str);
string = builder.toString();
}
// 字节码:
0 aconst_null
1 astore_1
2 aload_0
3 astore_2
4 aload_2
5 arraylength
6 istore_3
7 iconst_0
8 istore 4
10 iload 4
12 iload_3
13 if_icmpge 48 (+35)
16 aload_2
17 iload 4
19 aaload
20 astore 5
22 new #7 <java/lang/StringBuilder>
25 dup
26 invokespecial #8 <java/lang/StringBuilder.<init>>
29 aload_1
30 invokevirtual #9 <java/lang/StringBuilder.append>
33 aload 5
35 invokevirtual #9 <java/lang/StringBuilder.append>
38 invokevirtual #10 <java/lang/StringBuilder.toString>
41 astore_1
42 iinc 4 by 1
45 goto 10 (-35)
48 aload_1
49 areturn
可以看到,如果在循环里直接使用字符串 +
,会生成非常多中间变量,性能非常差。应该在循环外新建一个 StringBuilder
,在循环内统一操作这个对象。
6. String 对象的内存分配
6.1 "abc" 与 new String("abc") 的区别
"abc"
=> 虚拟机首先检查 运行时常量池 中是否存在 "abc",如果存在则直接返回,否则在字符串常量池中创建 "abc" 对象并返回。因此,多次声明使用的是同一个对象;new String("abc")
=> 在编译过程中,Javac 会将 "abc" 加入到 Class 文件常量池 中。在类加载时期,Class 文件常量池会被加载进运行时常量池。在调用 new 字节码指令时,虚拟机会在堆中新建一个对象,并且引用常量池中的 "abc" 对象。
6.2 String#intern() 的实现原理
如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回常量池中的这个字符串;否则,先将此 String 对象包含的字符串拷贝到常量池中,在常量池中的这个字符串。
从 JDK 1.7 开始,String#intern()
不再拷贝字符串到常量池中,而是在常量池中生成一个对原 String 对象的引用,并返回。
// 举例:
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
// 输出结果为:
JDK1.6以及以下:false false
JDK1.7以及以上:false true
7. 为什么 String#haseCode() 要使用 31 作为因子?
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
- 原因 1 - 31 可以被编译器优化 $31 * i = (i << 5) - i$,位运算和减法运算的效率比乘法运算高。
- 原因 2 - 31 是一个质数: 质数是只能被 1 和自身整除的数,使用质数作为乘法因子获得的散列值,在将来进行取模时,得到相同 index 的概率会降低,即降低了哈希冲突的概率。
- 原因 3 - 31 是一个不大不小的质数: 质数太小容易造成散列值聚集在一个小区间,提供散列冲突概率;质数过大容易造成散列值超出 int 的取值范围(上溢),丢失部分数值信息,散列冲突概率不稳定。
我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐]私信我提问。
金九银十,收下这份 Java String 面试题的更多相关文章
- 想要金九银十面试通关,不懂 Java多线程肯定是不行的!
作者 | 纳达丶无忌 如果对什么是线程.什么是进程仍存有疑惑,请先 Google 之,因为这两个概念不在本文的范围之内. 用多线程只有一个目的,那就是更好的利用 CPU 的资源,因为所有的多线程代码都 ...
- 金九银十想去跳槽面试?那这份Java面经你真得看看了,写的非常详细!
前言 前两天在和朋友吃饭的时候聊到时间这个东西是真的过的好坏啊,金三银四仿佛还在昨天.一眨眼金九银十又快到了,对程序员来说这两个是一年最合适的跳槽涨薪环节了,今年的你已经做好准备了吗?不妨看看这篇文章 ...
- 不等"金九银十",金风八月,我早已拿下字节跳动的offer
字节跳动,我是在网上投的简历,之前也投过一次,简历都没通过删选,后来让师姐帮我改了一下简历,重新投另一个部门,获得了面试机会.7月23日,中午HR打电话过来预约了下午4点半面试,说会在线写代码,让我准 ...
- 备战金九银十,Java研发面试题(Spring、MySQL、JVM、Mybatis、Redis、Tomcat)[带答案],刷起来!
八月在即,马上就是"金九银十",又是跳槽招聘季.咱们这行公认涨薪不如跳槽加的快.但不建议频繁跳槽,还是要学会融合团队,抓住每个机会提升技能. 苏先生在这里给大家整理了一套各大互联网 ...
- “金九银十”已过,总结我的天猫、蚂蚁、头条面试经历(Java岗)
跳槽时时刻刻都在发生,但是我建议大家跳槽之前,先想清楚为什么要跳槽.切不可跟风,看到同事一个个都走了,自己也盲目的开始面试起来(期间也没有准备充分),到底是因为技术原因(影响自己的发展,偏移自己规划的 ...
- 金九银十跳槽高峰,面试必备之 Redis + MongoDB 常问80道面试题
前言 有着“金九银十”之称的招聘旺季已经开启,跳槽高峰期也如约而至. 本文为主要是 Redis + MongoDB 知识点的攻略,希望能帮助到大家. 内容较多,大家准备好耐心和瓜子矿泉水. Redis ...
- 金九银十已到!Cookie 和 Session的这些知识你必须知道,面试必问!
前言 会话:一次会话中包含多次请求和响应 注:一次会话表示浏览器第一次给服务器发送请求,会话建立,直到有一方断开为止 功能:在一次会话的多次请求间共享数据 方式: (1) 客户端会话技术:Cookie ...
- apk开发环境!多亏这份《秋招+金九银十-腾讯面试题合集》跳槽薪资翻倍!再不刷题就晚了!
开头 最近很多网友反馈:自己从各处弄来的资料,过于杂乱.零散.碎片化,看得时候觉得挺有用的,但过个半天,啥都记不起来了.其实,这就是缺少系统化学习的后果. 为了提高大家的学习效率,帮大家能快速掌握An ...
- Java程序员备战“金九银十”必备的面试技巧(附携程Java岗面试题)
一.面试前的准备 1.1 如何准备一场面试1.1.1 如何获取大厂面试机会1.1.2 面试必知 ①. 准备介绍自己 ②. 关于着装 ③ .随身带上自己的成绩单和简历 ④. 如果笔试就提前刷一些笔试题 ...
随机推荐
- RocketMQ消息的顺序与重复
1.如何保证消息的顺序 原因:生产者将消息发给topic,topic分发给不同的队列再给多个消费者并发消费,难以保证顺序. 方案:topic和队列之间加入MessageQueueSelector.将一 ...
- js 表面学习 - 认识结构2
单行注释以 // 开头. 多行注释以 /* 开头,以 */ 结尾. 任何位于 /* 和 */ 之间的文本都会被 JavaScript 忽略. JavaScript 数据类型 JavaScript 变量 ...
- Linux目录结构和文件类型
文件系统目录结构 根(/)是所有文件的入口,类似于倒状的树 以 . 开头的文件为隐藏文件 文件路径之间用/分隔,包括路径在内文件名称最长4095个字节 文件名除了斜杠和NUL都可以,文件名的最大长度是 ...
- Work Center View * cannot be used for report assignment. Please deselect错误解决方法
by zyi
- 异常概念&异常体系和异常分类
异常概念 异常:指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止. 在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象.Java处 ...
- HTML及HTTP协议
web服务的过程: 浏览器发请求 --> HTTP协议 --> 服务端接收请求 --> 服务端返回响应 --> 服务端把HTML文件内容发给浏览器 --> 浏览器渲染页面 ...
- 可变参数和Collections集合工具类
可变参数 /** * 可变参数:jdk1.5后出现的新特性 * 使用前期: * 当方法的参数列表数据类型已经确定的时候但是参数的个数不确定的时候就可以使用可变参数 * 使用格式:定义方法的时候使用 * ...
- SpringBoot集成文件 - 如何使用POI导出Word文档?
前文我们介绍了通过Apache POI导出excel,而Apache POI包含是操作Office Open XML(OOXML)标准和微软的OLE 2复合文档格式(OLE2)的Java API.所以 ...
- linux rz上传失败
最近rz上传文件时出现了一次文件上传失败的情况,故搜集了以下资料加强学习 rz -ary --o-sync -a 表示使用ascii码格式传输文件,如果是Dos格式的文件,会转换为unix格式 -r ...
- odoo 14 一些常见问题集
1 # 当你往tree或者form视图中增加action的时候 2 # 记住!千万别重名 3 # 一旦重名,Export.Delete.Archive.Unarchive都会消失不见 4 # tree ...