String 为什么不可变?不可变有什么好处?

前言

说到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,这个变量是放在栈上的。

等号右边:一个字符串对象,放在堆中。

String 为什么不可变?不可变有什么好处?_第1张图片

我们说的不可变,指的是字符串对象。

我们通过第3行代码,将这个 str 变量赋值为另一个字符串,对原来的字符串对象是没有任何改变的。

final修饰value数组?

String 为什么不可变?不可变有什么好处?_第2张图片

我猜有不少同学在回答这个问题的时候,会答说是因为string底层的这个value 数组被 final 修饰,所以 String 不可变,这个说法其实不正确。

我们来看一个例子:

String 为什么不可变?不可变有什么好处?_第3张图片

这个例子中,我们的 demo 变量使用了 final 修饰,但是我们仍然改变了其内容。所以,final 并不能保证对象的一个不可变性。

final修饰变量的含义

基础数据类型:一旦初始化,便不能改变其值。

引用类型:一旦初始化,便不能改变其引用,也就是不能指向一个新的对象,但是仍然可以修改引用指向的对象内容。

为什么不可变?

1)value使用final修饰

String 为什么不可变?不可变有什么好处?_第4张图片

保证value一旦被初始化,就不可改变其引用。

2)没有暴露成员变量

String 为什么不可变?不可变有什么好处?_第5张图片

成员变量的访问权限为 private,同时没有提供方法将字段暴露出来,想要修改只能通过 String 提供的方法。

3)内部方法不会改动 value

String 为什么不可变?不可变有什么好处?_第6张图片

一旦初始化之后,String 类中的方法就不会去改动 value 中的元素,需要的话都是直接新建一个 String 对象。

4)类使用final修饰,不可继承

String 为什么不可变?不可变有什么好处?_第7张图片

这个设计主要是避免有人定义一个子类继承 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 之父的观点

String 为什么不可变?不可变有什么好处?_第8张图片

对于不可变性,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,你懂了吗?

你可能感兴趣的:(我要进大厂,面试,java,开发语言,面试,职场和发展,后端)