String类的不可变性

来源 | 简书| 作者 | 指尖上的榴莲

一.原理(为什么说String类是不可变的)

1.什么是不可变对象

如果一个对象在创建之后就不能再改变它的状态,那么这个对象是不可变的(Immutable)。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型变量的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

2.final关键字的作用

如果要创建一个不可变对象,关键一步就是要将所有的成员变量声明为final类型。所以下面简单回顾一下final关键字的作用:

  • final修饰类,表示该类不能被继承,俗称断子绝孙类,该类的所有方法自动地成为final方法
  • final修饰方法,表示子类不可重写该方法
  • final修饰基本数据类型变量,表示该变量为常量,值不能再修改
  • final修饰引用类型变量,表示该引用在构造对象之后不能指向其他的对象,但该引用指向的对象的状态可以改变

3.String类不可变性的分析

先看下面这段代码:

String s = "abc";    //(1)
System.out.println("s = " + s);

s = "123";    //(2)
System.out.println("s = " + s);

打印结果为:

s = abc
s = 123

看到这里,你可能对String是不可变对象产生了疑惑,因为从打印结果可以看出,s的值的确改变了。其实不然,因为s只是一个String对象的引用,并不是String对象本身。 当执行(1)处这行代码之后,会先在方法区的运行时常量池创建一个String对象"abc",然后在Java栈中创建一个String对象的引用s,并让s指向"abc",如下图所示:

图1

当执行完(2)处这行代码之后,会在方法区的运行时常量池创建一个新的String对象"123",然后让引用s重新指向这个新的对象,而原来的对象"abc"还在内存中,并没有改变,如下图所示:

图2

4.String类不可变性的原理

要理解String类的不可变性,首先看一下String类中都有哪些成员变量。在JDK1.8中,String的成员变量主要有以下几个:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {    
    /** The value is used for character storage. */
    private final char value[];    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

首先可以看到,String类使用了final修饰符,表明String类是不可继承的。 然后,我们主要关注String类的成员变量value,value是char[]类型,因此String对象实际上是用这个字符数组进行封装的。再看value的修饰符,使用了private,也没有提供setter方法,所以在String类的外部不能修改value,同时value也使用了final进行修饰,那么在String类的内部也不能修改value,但是上面final修饰引用类型变量的内容提到,这只能保证value不能指向其他的对象,但value指向的对象的状态是可以改变的。通过查看String类源码可以发现,String类不可变,关键是因为SUN公司的工程师,在后面所有String的方法里都很小心的没有去动字符数组里的元素。所以String类不可变的关键都在底层的实现,而不仅仅是一个final。

5.String对象真的不可变吗

上面提到,value虽然使用了final进行修饰,但是只能保证vaue不能指向其他的对象,但value指向的对象的状态是可以改变的,也就是说,可以修改value指向的字符数组里面的元素。因为value是private类型的,所以只能使用反射来获取String对象的value属性,再去修改value指向的字符数组里面的元素。通过下面的代码进行验证:

String s = "Hello World";
System.out.println("s = " + s);//获取String类中的value属性

Field valueField = String.class.getDeclaredField("value");//改变value属性的访问权限

valueField.setAccessible(true);//获取s对象上的value属性的值

char[] value = (char[]) valueField.get(s);//改变value所引用的数组中的第6个字符

value[5] = '_';
System.out.println("s = " + s);

打印结果为:

s = Hello World
s = Hello_World

在上述代码中,s始终指向同一个String对象,但是在反射操作之后,这个String对象的内容发生了变化。也就是说,通过反射是可以修改String这种不可变对象的。

二.设计目标(为什么String要设计成不可变的)

在Java中,将String设计成不可变的是综合考虑到内存、同步、数据结构及安全等各种因素的结果,下文将为各种因素做一个小结。

1.运行时常量池的需要

String s = "abc";

执行上述代码时,JVM首先在运行时常量池中查看是否存在String对象“abc”,如果已存在该对象,则不用创建新的String对象“abc”,而是将引用s直接指向运行时常量池中已存在的String对象“abc”;如果不存在该对象,则先在运行时常量池中创建一个新的String对象“abc”,然后将引用s指向运行时常量池中创建的新String对象。

String s1 = "abc";
String s2 = "abc";

执行上述代码时,在运行时常量池中只会创建一个String对象"abc",这样就节省了内存空间。示意图如下所示:

图3

2.同步

因为String对象是不可变的,所以是多线程安全的,同一个String实例可以被多个线程共享。这样就不用因为线程安全问题而使用同步。

3.允许String对象缓存hashcode

查看上文JDK1.8中String类源码,可以发现其中有一个字段hash,String类的不可变性保证了hashcode的唯一性,所以可以用hash字段对String对象的hashcode进行缓存,就不需要每次重新计算hashcode。所以Java中String对象经常被用来作为HashMap等容器的键。

4.安全性

如果String对象是可变的,那么会引起很严重的安全问题。比如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为String对象是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变String引用指向的对象的值,造成安全漏洞。

原文发布于微信公众号 - java技术学习之道(javajsxxzd)

原文发表时间:2018-05-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏微服务生态

Scala学习笔记(一)

lazy val forLater = someTimeConsumingOperation()

541
来自专栏IMWeb前端团队

正则里exec跟match的区别

还在傻傻分不清这两个函数该用哪个?来来来,本文给你一个清晰的思路~! 一、 exec match分别是谁家的方法 exec是RegExp类的方法 match...

1908
来自专栏数据之美

python基础(5):深入理解 python 中的赋值、引用、拷贝、作用域

在 python 中赋值语句总是建立对象的引用值,而不是复制对象。因此,python 变量更像是指针,而不是数据存储区域, ? 这点和大多数 OO 语言类似吧,...

2027
来自专栏我的技术专栏

C++ 异常机制分析

994
来自专栏coding for love

JS原生引用类型解析7-Promise类型

ES6引入了一个全新的对象Promise,用于表示一个异步操作的最终状态(完成或失败),以及其返回的值。Promise最直接的好处就是链式调用,另外在错误捕获上...

441
来自专栏编程

C语言编程精华——《指针》

C语言编程精华--《指针》 使用指针有两方面的优势: 1、用指针能够更好完成某些任务; 2、有些任务只能是用指针才能够完成。 一、指针定义 1、计算机的内存 计...

1690
来自专栏Java爬坑系列

C\C++中声明与定义的区别

  声明和定义是完全同的概念,声明是告诉编译器“这个函数或者变量可以在哪找到,它的模样像什么”。而定义则是告诉编译器,“在这里建立变量或函数”,并且为它们分配内...

17710
来自专栏塔奇克马敲代码

第3章 字符串、向量和数组

1666
来自专栏梧雨北辰的开发录

Swift学习:构造器(下)

本篇主要介绍Swift中构造器的一些特殊用法 一、可失败的构造器 顾名思义,这是用于我们构造过程可能失败情况的构造器。失败的原因可能是给构造器传入无效的参数值,...

2437
来自专栏海天一树

小朋友学C语言(5):常量和变量

先动手编写程序: #include <stdio.h> int main() { int a = 1; printf("a = %d\n", a...

3169

扫码关注云+社区