图片 8

关于 String

什么是不可变对象?

众所周知, 在Java中, String类是不可变的。那么到底什么是不可变的对象呢?
可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

1. String 不可变

  • 不可变对象

    对象在创建完成之后,其状态不能再被改变,则该对象即为不可变对象
  • 对象不可变具体内容

    不能改变对象内的成员变量
    基本数据类型的值不能改变
    引用类型的变量不能指向其他对象
    引用类型所指向的对象状态也不能改变
  • 例如

    String s = "ABC"; 
    System.out.print(s);  // ABC
    s = "123";  
    System.out.print(s);  // 123
    
  • 执行结果

    "ABC"
    "123"
    
  • 释义

对象:在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。
引用:只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。

  • s只是一个引用,它指向了一个具体的对象,当s=“123”;
    这句代码执行过之后,又创建了一个新的对象“123”,
    而引用s重新指向了这个新的对象,原来的对象“ABC”还在内存中存在,并没有改变。
  • 不能直接操作对象本身,所有的对象都由一个引用指向,必须通过这个引用才能访问对象本身,包括获取成员变量的值,改变对象的成员变量,调用对象的方法等
  • 效率和安全
Java 将 String 设成不可变最大的原因是效率和安全
  • 提高字符串常量池的效率和安全性
  • 如果一个对象是不可变的
    ,需要拷贝的对象的内容时就不用复制它本身,而只是复制它的地址,复制地址(通常一个指针的大小)需要很小的内存,效率也很好
  • 对于引用同一个对象的其他变量也不会造成影响
  • 对于多线程是安全的。多线程同时进行时,一个可变对象的值很可能被其他线程改变

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

区分对象和对象的引用

对于Java初学者, 对于String是不可变对象总是存有疑惑。看下面代码:

String s = "ABCabc";
System.out.println("s = " + s);

s = "123456";
System.out.println("s = " + s);

打印结果为:

s = ABCabc
 s = 123456

首先创建一个String对象s,然后让s的值为“ABCabc”,
然后又让s的值为“123456”。
从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢?
其实这里存在一个误区:
s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。

也就是说,s只是一个引用,它指向了一个具体的对象,当s=“123456”;
这句代码执行过之后,又创建了一个新的对象“123456”,
而引用s重新指向了这个心的对象,原来的对象“ABCabc”还在内存中存在,并没有改变。内存结构如下图所示:

图片 1

Java和C++的一个不同点是,
在Java中不可能直接操作对象本身,所有的对象都由一个引用指向,必须通过这个引用才能访问对象本身,包括获取成员变量的值,改变对象的成员变量,调用对象的方法等。而在C++中存在引用,对象和指针三个东西,这三个东西都可以访问对象。其实,Java中的引用和C++中的指针在概念上是相似的,他们都是存放的对象在内存中的地址值,只是在Java中,引用丧失了部分灵活性,比如Java中的引用不能像C++中的指针那样进行加减运算。

2. String 源码

  • JDK1.7以后,String 类的成员变量只剩下两个:
![](https://upload-images.jianshu.io/upload_images/3383738-53aef63d77deb9b9.png)

String 类成员变量.png
  • 释义

    • String 类实际上是对字符数组的封装。
    • 成员变量 hash,是该 String 对象的哈希值的缓存。
    • 在 Java 中,数组也是对象,所以 value
      也只是一个引用,它指向一个真正的数组对象。在执行第一句代码时,真正的内存排布如下图:

      图片 2

      String 内存数据.png

  • String 对象不可变
    • String 内部成员变量 value 和 hash 并未对外提供 setter
      方法,且变量 value 是 final 的。这些都说明一旦 String 内部的
      value 被初始化后,便不会再改变,故而 String 对象不会改变。
  • 至于 String 内部其他方法:如 replace()
    在执行后,结果发生的变化,也是同理。即:这些方法执行过程中,会生成新的
    String 对象,该对象将会被赋值给引用,而原来的对象并未改变。如:

    图片 3

    String 新对象.png

  • 值得注意的是,通过反射可以修改对象的值。如:

     public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    
          String s = "Hello World";
          System.out.println("s: " + s);
          // 获取String类中的value字段
          Field valueFieldOfString= String.class.getDeclaredField("value");
          // 设置访问权限
          valueFieldOfString.setAccessible(true);
          // 获取 s 对象上的value属性的值
          char[] value = (char[]) valueFieldOfString.get(s);
          // 改变value所引用的数组中的第 5 个字符
          value[5] = '_';
          System.out.println("s: " + s); // Hello_World
    }
    

1.什么是不可变对象

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

为什么String对象是不可变的?

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

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];

    /** The offset is the first index of the storage that is used. */
    private final int offset;

    /** The count is the number of characters in the String. */
    private final int count;

    /** Cache the hash code for the string */
    private int hash; // Default to 0

在JDK1.7中,String类做了一些改动,主要是改变了substring方法执行时的行为,这和本文的主题不相关。JDK1.7中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

由以上的代码可以看出,
在Java中String类其实就是对字符数组的封装。JDK6中,
value是String封装的数组,offset是String在这个value数组中的起始位置,count是String所占的字符的个数。在JDK7中,只有一个value变量,也就是value中的所有字符都是属于String这个对象的。这个改变不影响本文的讨论。
除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象(可以参考我之前的文章java中数组的特性)。
所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s =
“ABCabc”; 这句代码之后,真正的内存布局应该是这样的:

图片 4

value,offset和count这三个变量都是private的,并且没有提供setValue,
setOffset和setCount等公共方法来修改这些值,所以在String类的外部无法修改String。也就是说一旦初始化就不能修改,
并且在String类的外部不能访问这三个成员。此外,value,offset和count这三个变量都是final的, 也就是说在String类内部,一旦这三个值初始化了,
也不能被改变。所以可以认为String对象是不可变的了。

那么在String中,明明存在一些方法,调用他们可以得到改变后的值。这些方法包括substring,
replace, replaceAll, toLowerCase等。例如如下代码:

String a = "ABCabc";  
System.out.println("a = " + a);  
a = a.replace('A', 'a');  
System.out.println("a = " + a);

打印结果为:

a = ABCabc
a = aBCabc

那么a的值看似改变了,其实也是同样的误区。再次说明, a只是一个引用,
不是真正的字符串对象,在调用a.replace(‘A’, ‘a’)时,
方法内部创建了一个新的String对象,并把这个心的对象重新赋给了引用a。String中replace方法的源码可以说明问题:

图片 5

读者可以自己查看其他方法,都是在方法内部重新创建新的String对象,并且返回这个新的对象,原来的对象是不会被改变的。这也是为什么像replace,
substring,toLowerCase等方法都存在返回值的原因。也是为什么像下面这样调用不会改变对象的值:

String ss = "123456";

System.out.println("ss = " + ss);

ss.replace('1', '0');

System.out.println("ss = " + ss);

打印结果:

ss = 123456
 ss = 123456

2.final关键字的作用

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

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

String对象真的不可变吗?

从上文可知String的成员变量是private final
的,也就是初始化之后不可改变。那么在这几个成员中,
value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗?
比如将数组中的某个位置上的字符变为下划线“_”。
至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。

那么用什么方式可以访问私有成员呢? 没错,用反射,
可以反射出String对象中的value属性,
进而改变通过获得的value引用改变数组的结构。下面是实例代码:

   public static void testReflection() throws Exception {

        //创建字符串"Hello World", 并赋给引用s
        String s = "Hello World"; 

        System.out.println("s = " + s); //Hello World

        //获取String类中的value字段
        Field valueFieldOfString = String.class.getDeclaredField("value");

        //改变value属性的访问权限
        valueFieldOfString.setAccessible(true);

        //获取s对象上的value属性的值
        char[] value = (char[]) valueFieldOfString.get(s);

        //改变value所引用的数组中的第5个字符
        value[5] = '_';

        System.out.println("s = " + s);  //Hello_World
    }

打印结果为:

s = Hello World
 s = Hello_World

在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化,
也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private
final 的,但是这个Wheel对象内部的状态可以改变,
那么就不能很好的保证Car对象不可变。

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”,如下图所示:

图片 6

图1

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

图片 7

图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”,这样就节省了内存空间。示意图如下所示:

图片 8

图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中的String为什么是不可变的? —
String源码分析
为什么String类是不可变的?

发表评论

电子邮件地址不会被公开。 必填项已用*标注