String 为什么不可变?不可变有什么好处?
共 3159字,需浏览 7分钟
·
2022-05-23 18:14
前言
说到String的不可变性,我猜肯定有同学要说可以通过反射来修改。所以我们在分享之前,在这边先出一个反射的题目,大家看看能不能答对。
题目
String name = "jionghui";
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(name);
value[0] = 'a';
System.out.println("jionghui" == name);
大家可以思考一下这个题目,我会在文末给出答案和解析。
不可变是什么意思
不可变类(immutable):类的实例一旦创建后,其内容(状态)就不可改变。
简单理解就是:一个对象一旦被创建后,整个对象就是不可变的。包括属任何性和状态。
可能有同学会拿下面这段代码来说,这不是变了吗?
public void testFinal() {
String str = "程序员囧辉";
str = "屌丝囧辉";
}
我们看下第2行代码,这行代码中有两部分组成。
等号左边:一个局部变量 str,类型是 String,这个变量是放在栈上的。
等号右边:一个字符串对象,放在堆中。
我们说的不可变,指的是字符串对象。
我们通过第3行代码,将这个 str 变量赋值为另一个字符串,对原来的字符串对象是没有任何改变的。
final修饰value数组?
我猜有不少同学在回答这个问题的时候,会答说是因为string底层的这个value 数组被 final 修饰,所以 String 不可变,这个说法其实不正确。
我们来看一个例子:
这个例子中,我们的 demo 变量使用了 final 修饰,但是我们仍然改变了其内容。所以,final 并不能保证对象的一个不可变性。
final修饰变量的含义
基础数据类型:一旦初始化,便不能改变其值。
引用类型:一旦初始化,便不能改变其引用,也就是不能指向一个新的对象,但是仍然可以修改引用指向的对象内容。
为什么不可变?
1)value使用final修饰
保证value一旦被初始化,就不可改变其引用。
2)没有暴露成员变量
成员变量的访问权限为 private,同时没有提供方法将字段暴露出来,想要修改只能通过 String 提供的方法。
3)内部方法不会改动 value
一旦初始化之后,String 类中的方法就不会去改动 value 中的元素,需要的话都是直接新建一个 String 对象。
4)类使用final修饰,不可继承
这个设计主要是避免有人定义一个子类继承 String,然后重写 String 的方法,将这个子类设计成可变对象。我们知道在 java 中,有父类引用指向子类对象这种用法,这种情况下,我们需要一个String 对象,可能返回的是String 子类的对象,这会导致 String 看起来是可变的。所以 java 直接将 String定义成不可继承,避免出现这种情况。
不只是 String 类,其实所有的不可变类大致的设计思想都是按这四步来。后续如果我们自己想要设计一个不可变类,也可以按这四点来设计。
不可变的好处?为什么这么设计?
1)安全性
String 是 Java 中最基础也是最长使用的类,经常用于存储一些敏感信息,例如用户名、密码、网络连接等。因此,String 类的安全性对于整个应用程序至关重要。
我们来看下面这个例子:
private static void dangerousOperation(MyString myString) throws InterruptedException {
if (!securityCheck(myString)) {
System.out.println("校验失败");
return;
}
// 一些七的八的操作
doSomething();
// 执行危险操作
dangerous(myString);
}
我们通过一个方法来模拟一个危险的一个系统操作。
首先在这个方法的入口会进行一个安全检查。如果检查失败,会直接返回。
然后接着我们会最终去执行这个比较危险的操作。
如果此时这个方法的参数是可变对象,那么它可能在通过安全检查的时候,是一个合法的入参。但是当最终执行到下面的危险操作时,他可能被调用方给修改了,变成一个不合法的参数。但是这个时候他已经通过检查了,所以我们没办法对他进行拦截,最终可能会导致我们的系统被攻击或者存在安全隐患。
2)节省空间——字符串常量池
通过使用常量池,内容相同的字符串可以使用同一个对象,从而节省内存空间。如果 String 是可变的,试想一下,当字符串常量池中的某个字符串对象被很多地方引用时,此时修改了这个对象,则所有引用的地方都会改变,这可能会导致预期之外的情况。
典型的使用字符串常量池的场景:json 工具类,fastjson、jackson 等。
3)线程安全
String 对象是不可修改的,如果线程尝试修改 String 对象,会创建新的 String,所以不存在并发修改同一个对象的问题。
4)性能
String 被广泛应用于 HashMap、HashSet 等哈希类中,当对这些哈希类进行操作时,例如 HashMap 的 get/put,hashCode 会被频繁调用。
由于不可变性,String 的 hashCode 只需要计算1次后就可以缓存起来,因此在哈希类中使用 String 对象可以提升性能。
Java 之父的观点
对于不可变性,Java 之父詹姆斯高斯林在一次采访中谈过这个话题,他表示:只要可以,他就会使用不可变性。可以看出他对不可变性的评价非常高。
至于不可变的好处,高斯林主要谈到了几个观点:
1)不可变对象往往更不容易出问题;
2)安全性问题;
3)缓存。
这三点在我们之前的内容里也基本都提到了,原文如下,有兴趣的可以去看一下。
https://www.artima.com/articles/james-gosling-on-java-may-2001#part13
题目答案
文章开头题目的答案是 true。
解释:当这段代码被编译之后,这两个被双引号修饰的 jionghui 字符串字面量,由于它们的值是相同的,所以它们会指向同一个符号引用。
当这个符号引用被解析时,我们会在字符串常量池中创建一个 jionghui 字符串。最终这两个字符串字面量都会指向我们字符串常量池里面的这个 jionghui,所以他们其实指向的是同一个字符串对象。
因为他们的引用是相同的,所以这个地方输出结果的是 true。
看过我上一个文章/视频的同学应该不难理解。如果你不理解,或者说你对字符串常量池、符号引用有一些疑问,你可以去看一下我的上一个文章/视频。我在上一个文章/视频里有详解介绍字符串常量池和符号引用的相关内容。
推荐阅读
最近我将面试:阿里、字节、美团、快手、拼多多等大厂的高频面试整理出来,并按大厂的标准给出自己的解析。
群里有不少同学看完拿下了阿里、美团等大厂 Offer,希望能助你一臂之力,早日拿下大厂 Offer。
获取方式:关注公众号回复【面试】即可领取,更多大厂面试真题解析 PDF 整理中。