说到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 变量赋值为另一个字符串,对原来的字符串对象是没有任何改变的。
我猜有不少同学在回答这个问题的时候,会答说是因为string底层的这个value 数组被 final 修饰,所以 String 不可变,这个说法其实不正确。
我们来看一个例子:
这个例子中,我们的 demo 变量使用了 final 修饰,但是我们仍然改变了其内容。所以,final 并不能保证对象的一个不可变性。
基础数据类型:一旦初始化,便不能改变其值。
引用类型:一旦初始化,便不能改变其引用,也就是不能指向一个新的对象,但是仍然可以修改引用指向的对象内容。
保证value一旦被初始化,就不可改变其引用。
成员变量的访问权限为 private,同时没有提供方法将字段暴露出来,想要修改只能通过 String 提供的方法。
一旦初始化之后,String 类中的方法就不会去改动 value 中的元素,需要的话都是直接新建一个 String 对象。
这个设计主要是避免有人定义一个子类继承 String,然后重写 String 的方法,将这个子类设计成可变对象。我们知道在 java 中,有父类引用指向子类对象这种用法,这种情况下,我们需要一个String 对象,可能返回的是String 子类的对象,这会导致 String 看起来是可变的。所以 java 直接将 String定义成不可继承,避免出现这种情况。
不只是 String 类,其实所有的不可变类大致的设计思想都是按这四步来。后续如果我们自己想要设计一个不可变类,也可以按这四点来设计。
String 是 Java 中最基础也是最长使用的类,经常用于存储一些敏感信息,例如用户名、密码、网络连接等。因此,String 类的安全性对于整个应用程序至关重要。
我们来看下面这个例子:
private static void dangerousOperation(MyString myString) throws InterruptedException {
if (!securityCheck(myString)) {
System.out.println("校验失败");
return;
}
// 一些七的八的操作
doSomething();
// 执行危险操作
dangerous(myString);
}
我们通过一个方法来模拟一个危险的一个系统操作。
首先在这个方法的入口会进行一个安全检查。如果检查失败,会直接返回。
然后接着我们会最终去执行这个比较危险的操作。
如果此时这个方法的参数是可变对象,那么它可能在通过安全检查的时候,是一个合法的入参。但是当最终执行到下面的危险操作时,他可能被调用方给修改了,变成一个不合法的参数。但是这个时候他已经通过检查了,所以我们没办法对他进行拦截,最终可能会导致我们的系统被攻击或者存在安全隐患。
通过使用常量池,内容相同的字符串可以使用同一个对象,从而节省内存空间。如果 String 是可变的,试想一下,当字符串常量池中的某个字符串对象被很多地方引用时,此时修改了这个对象,则所有引用的地方都会改变,这可能会导致预期之外的情况。
典型的使用字符串常量池的场景:json 工具类,fastjson、jackson 等。
String 对象是不可修改的,如果线程尝试修改 String 对象,会创建新的 String,所以不存在并发修改同一个对象的问题。
String 被广泛应用于 HashMap、HashSet 等哈希类中,当对这些哈希类进行操作时,例如 HashMap 的 get/put,hashCode 会被频繁调用。
由于不可变性,String 的 hashCode 只需要计算1次后就可以缓存起来,因此在哈希类中使用 String 对象可以提升性能。
对于不可变性,Java 之父詹姆斯高斯林在一次采访中谈过这个话题,他表示:只要可以,他就会使用不可变性。可以看出他对不可变性的评价非常高。
至于不可变的好处,高斯林主要谈到了几个观点:
1)不可变对象往往更不容易出问题;
2)安全性问题;
3)缓存。
这三点在我们之前的内容里也基本都提到了,原文如下,有兴趣的可以去看一下。
https://www.artima.com/articles/james-gosling-on-java-may-2001#part13
文章开头题目的答案是 true。
解释:当这段代码被编译之后,这两个被双引号修饰的 jionghui 字符串字面量,由于它们的值是相同的,所以它们会指向同一个符号引用。
当这个符号引用被解析时,我们会在字符串常量池中创建一个 jionghui 字符串。最终这两个字符串字面量都会指向我们字符串常量池里面的这个 jionghui,所以他们其实指向的是同一个字符串对象。
因为他们的引用是相同的,所以这个地方输出结果的是 true。
看过我上一个文章/视频的同学应该不难理解。如果你不理解,或者说你对字符串常量池、符号引用有一些疑问,你可以去看一下我的上一个文章/视频。我在上一个文章/视频里有详解介绍字符串常量池和符号引用的相关内容。
全网最实用的 IDEA Debug 调试技巧(超详细案例)
Java 基础高频面试题(2021年最新版)
Java 集合框架高频面试题(2021年最新版)
面试必问的 Spring,你懂了吗?
面试必问的 MySQL,你懂了吗?