Java中的String类真的不可变吗?

Posted 小小怪下士 XIA

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的String类真的不可变吗?相关的知识,希望对你有一定的参考价值。

其实在Java中,String类被final修饰,主要是为了保证字符串的不可变性,进而保证了它的安全性。那么final到底是怎么保证字符串安全性的呢?接下来就让我们一起来看看吧。

一. final的作用

1.  final关键词修饰的类不可以被其他类继承,但是该类本身可以继承其他类,通俗地说就是这个类可以有父类,但不能有子类。

1

2

3

final class MyTestClass1

    // ...

2.  final关键词修饰的方法不可以被覆盖重写,但可以被继承使用。

1

2

3

4

5

class MyTestClass2

    final void myMethod()

        // ...

    

3.  final关键词修饰的基本数据类型被称为常量,只能被赋值一次。  

1

2

3

class MyTestClass3

    final int number = 100;

4.  final关键词修饰的引用数据类型变量,其值为地址值,该地址值不能改变,但该地址对应的数据对象可以被改变(其实这一点就和我们今天要说的内容有关了,在后面我会结合案例跟大家重点解释,大家一定要打起精神仔细学习哦)。

5.  final关键词修饰的成员变量,需要在创建对象前就赋值,否则会报错(即需要在定义时直接赋值)。

综上所述,我们可以知道,final在Java中是一个非常有用的关键字,主要可以提高我们代码的稳定性和可读性。当然,我们今天要讲解的重点是被final修饰的String类,所以接下来我们还是把目光转回到String身上来,看看String都有哪些特性吧!

二. 被final修饰的String类

为了让大家更好地理解String的不可变性,首先我要给各位简要地讲一下String的源码设计。从下面的这段源码中,我们可以搞清楚很多底层的设计思路,接下来就请大家跟着我一起来看看String的核心源码吧。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

/**

 * ......其他略......

 *

 * Strings are constant; their values cannot be changed after they

 * are created. String buffers support mutable strings.

 * Because String objects are immutable they can be shared. For example:

 *

 * ......其他略......

 *

 */

public final class String

    implements java.io.Serializable, Comparable<String>, CharSequence

     

    ......

我先把上面的源码及其注释,给大家作一个简单的解释:

● final:请参考第1小节对final特点的介绍;

● Serializable:用于序列化;

● Comparable<String>:默认的比较器;

● CharSequence: 提供对字符序列进行统一、只读的操作。

从这段源码及其注释中,我们可以得到下面这些结论:

● String类用final关键字修饰,说明String不可被继承;

● String字符串是常量,字符串的值一旦被创建,就不能被改变;

● String字符串缓冲区支持可变字符串;

● String对象是不可变的,它们是可以被共享的。

三. String的不可变性

在学习了上面的这些核心源码之后,接下来,我们可以通过一个案例来实践验证一番,看看String字符串的内容到底能不能改变。这里有个代码案例,如下图所示:

在上述的案例结果中,大家可以看出,s的内容竟然发生了改变?!但我们不是一直说String是不可变的吗?这是咋回事?大家先别急,我们继续往下看。

要想弄明白这个问题,我们首先得知道一个知识点:引用和值的区别!

在上面的代码中,我们先是创建了一个 "yiyige" 为内容的字符串引用s,如下图:

 s其实先是指向了value对象,而value对象又指向了存储 "y,i,y,i,g,e" 字符的字符数组。但因为value被final修饰,所以value的值不可被更改。因此,上面代码中改变的其实是s的引用指向,而不是改变了String对象的值!

换句话说,上面实例中s的值,其实只是value的引用地址,并不是String的内容本身。当我们执行 s = "yyg" 语句时,Java会创建一个新的字面量对象 "yyg",而原来的 "yiyige" 字面量对象其实依然存在于内存的intern缓存池中。

在这里,String对象的改变,实际上是通过内存地址的“断开-连接”变化来完成的。在这个过程中,原字符串中的内容并没有发生任何的改变。String s = "yiyige" 和 s = "yyg"这两行代码,实质上是开辟了2个内存空间,s只是由原来指向 "yiyige" 变为指向 "yyg" 而已,而其原来的字符串内容,是没有发生改变的,如下图所示。

因此,我们在以后的开发中,如果要经常修改字符串的内容,请尽量少用String!因为如果字符串的指向经常的“断开-连接”,就会大大降低性能,我建议大家使用StringBuilder 或 StringBuffer 进行替换。

我们继续把上面的代码深入地分析一下。在Java中,因为数组也是对象, 所以value中存储的也只是一个引用,它指向一个真正的数组对象。在执行了String s = “yiyige”; 这句代码之后,真正的内存布局应该是下图这样的:

因为value是String封装的字符数组,value中所有的字符都属于String这个对象。而由于value是private的,没有提供setValue等公共方法来修改这个value值,所以我们在String类的外部是无法修改value值的,也就是说字符串一旦初始化就不能再被修改。

此外,value变量是final修饰的,也就是说在String类内部,一旦这个值初始化了,value这个变量所引用的地址就不会改变了,即一直引用同一个对象。正是基于这一层,我们才说String对象是不可变的对象。

所以String的不可变,其实是指value在栈中的引用地址不可变,而不是说常量池中value字符数组里的数据元素不可变。也就是说,value所引用的数组对象里的内容,其实是可以发生改变的。

那么我们又如何改变它呢?这就要通过反射来消除String类对象的不可变性啦!

四. String真的不可变吗?

在上述内容中,我们重点给大家解释了String字符串的可变性。现在大家应该已经知道了,String字符串的内容其实是可变的,不可改变的只是String字符串的对象地址。那么我们到底该怎么让String字符串的内容发生改变呢?在上述我们给大家提到了反射,接下来我们就来看看如何通过反射改变String字符串的内容吧。代码案例如下所示:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

try 

    String str = "yyg";

    System.out.println("str=" + str + ", 唯一性hash值=" + System.identityHashCode(str));

    Class stringClass = str.getClass();

    //获取String类中的value属性

    Field field = stringClass.getDeclaredField("value");

    //设置私有成员的可访问性,进行暴力反射

    field.setAccessible(true);

    //获取value数组中的内容

    char[] value = (char[]) field.get(str);

    System.out.println("value=" + Arrays.toString(value));

    value[1] = 'z';

    System.out.println("str=" + str + ", 唯一性hash值=" + System.identityHashCode(str));

 catch (NoSuchFieldException | IllegalAccessException e)

    e.printStackTrace();

执行结果如下图所示:

  

从上面的结果中我们可以看到,String字符串的字符数组,通过反射进行修改后,字符串的“内容”真的发生了变化!

并且我们又利用底层的java.lang.System#identityHashCode()方法(不管是否重写了hashCode方法),来获取到了该字符串对象的唯一哈希值,该方法获取的hash值与hashCode()方法是一样的。

从结果中,我们可以看到两个字符串的唯一hash值是一样的,这就证明字符串的引用地址没有发生改变。

所以这就说明,我们并不是像之前那样创建了一个新的String字符串,而是真的改变了原有String的内容。

这个代码案例进一步证明了我们上面的结论:String字符串的不可变,指的其实是value对象在栈中的引用地址不可变,而不是说常量池中value里的数据元素不可变!简单地说,就是String字符串的内容其实是可以改变的,不能改表的是它的对象地址而已。

 所以这也就是我们上述所说的final的作用之一:final关键词修饰的引用数据类型的变量,其值为地址值,地址值不能改变,但是地址内的数据对象可以被改变!

五. 总结

至此,我们就把今天的面试题分析完了,现在你明白了吗?最后我再来给大家总结一下今天的重点内容吧:

1.  为什么要用final修饰java中的String类呢?

核心:因为它确保了字符串的安全性和可靠性。

2.  java中的String真的不可变吗?

核心:String字符串的内容其实是可变的,但要通过特殊手段进行实现,不可改变的是String字符串对象的地址。

3.  如何消除String类对象的不可变性?

核心:利用反射来消除String类对象的不可变性。

4.  如果想要保证String的不可变要注意哪些?

● 首先,将 String 类声明为 final类型。这意味着String类是不可被继承的,防止程序员通过继承重写String类的某些方法,使得String类出现“可变的”的情况;

● 然后,重要的字符数组value属性,要被private 和 final修饰。它是String的底层数组,用于存贮字符串内容。又因为数组是引用类型,所以只能限制引用不被改变,也就是说数组元素的值是可以改变的,这在上面的案例中已经证明过了;

● 接着,所有修改的方法都返回新的字符串对象,保证修改时不会改变原始对象的引用;

● 最后,不同的字符串对象都可以指向缓存池中的同一个字符串字面量。

当然Java中的String类使用final修饰”这个概念非常重要,因为它确保了字符串的安全性和可靠性。但是我们也要清楚不可改变的只是它的地址,而不是它的内容,它的内容是可以利用反射来改变的!只不过在一般的描述中,大家都会说String内容不可改变,毕竟很多时候是不允许利用反射这种特殊的功能去进行这样的操作的。

Java中的不可变类

本文与个人博客 zhiheng.me 同步发布,标题: Java中的不可变类

Java中的不可变类

不可变类(Immutable Objects):当类的实例一经创建,其内容便不可改变,即无法修改其成员变量。

可变类(Mutable Objects):类的实例创建后,可以修改其内容。

Java 中八个基本类型的包装类和 String 类都属于不可变类,而其他的大多数类都属于可变类。

与引用不可变的区别

需要特别注意的是,不可变类的不可变是指该类的实例不可变而非指向该实例的引用的不可变。

String s = "abc";
System.out.println("s:" + s);  // 输出s:abc
s = "xyz";
System.out.println("s:" + s);  // 输出s:xyz

以上代码显示,不可变类 String 貌似是可以改变值的,但实际上并不是。变量 s 只是一个指向 String 类的实例的引用,存储的是实例对象在内存中的地址。代码中第三行的 “改变” 实际上是新实例化了一个 String 对象,并将 s 的指向修改到新对象上,而原来的对象在内存中并未发生变化,只是少了一个指向它的引用,并且在未来被垃圾回收前它都将保持不变。

public class Immutable {
 
    public static void main(String[] args) {
        String str = new String("abc");
        String str2 = str;
        System.out.println(str == str2); // true
        str2 = "cba";
        System.out.println(str == str2); // false
 
        System.out.println(str == row(str)); // true
        System.out.println(str == other(str)); // false
    }
 
    static private String row(String s){
        return s;
    }
    static private String other(String s){
        s="xyz"; //此处形参 s 指向了新的String对象,引用的地址发生变化
        return s;
    }
}

如此我们看到,对于不可变类的对象,都是通过新创建一个对象并将引用指向新对象来实现 变化 的。

通常,使用关键字 final 修饰的字段初始化后是不可变的,而这种不可变就是指引用的不可变。具体就是该引用所指对象的内存地址是不可变的,但并非该对象不可变。如果该对象也不可变,那么该对象就是不可变类的一个实例。

public class Immutable {
 
    public static void main(String[] args) {
        Immutable immutable = new Immutable();
        final Inner inner = immutable.new Inner();
        inner.value = 123; // 实例可变
        // 下面语句编译错误,inner 是final的,无法让它指向新的对象(改变指向地址)
        // inner = it.new Inner();
        Inner inner2 = inner; // 复制了一份引用,inner和inner2指向同一个对象
        System.out.println(inner); // 将调用 toString 方法输出对象内存地址
        System.out.println(inner2); // inner和inner2具有相同的地址
        System.out.println(inner.value); // 输出 123
        System.out.println(inner2.value); // 输出123
        inner2.value = 321;
        System.out.println(inner); // 输出321
    }
 
    class Inner{
        private int value;
    }
}

不可变类是如何实现的

immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象。

因此,一个不可变类的定义应当具备以下特征:

  1. 所有成员都是 private final 的
  2. 不提供对成员的改变方法,例如:setXXXX
  3. 确保所有的方法不会被重载。手段有两种:使用final Class(强不可变类),或者将所有类方法加上final(弱不可变类)。
  4. 如果某一个类成员不是基本类型(primitive type)或不可变类,必须通过在成员初始化(in)或者getter方法(out)时通过深度拷贝(即复制一个该类的新实例而非引用)方法,来确保类的不可变。
  5. 如果有必要,重写hashCode和equals方法,同时应保证两个用equals方法判断为相等的对象,其hashCode也应相等。

下面是一个示例:

public final class ImmutableDemo {  
    private final int[] myArray;  
    public ImmutableDemo(int[] array) {
    // this.myArray = array; // 错误!
    this.myArray = array.clone(); // 正确
  }  
  public int[] get(){
    return myArray.clone();
  }
}

上例中错误的方法不能保证不可变性,myArray 和形参 array 指向同一块内存地址,用户可以在 ImmutableDemo 实例之外通过修改 array 对象的值来改变实例内部 myArray 的值。正确的做法是通过深拷贝将 array 的值传递给 myArray 。同样, getter 方法中不能直接返回对象本身,而应该是克隆对象并返回对象的拷贝,这种做法避免了对象外泄,防止通过 getter 获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。

对于不可变类,String 是一个典型例子,看看它的源码也有助于我们设计不可变类。

不可变类的优点

不可变类有两个主要有点,效率和安全。

  • 效率

    当一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)只需要很小的内存空间,具有非常高的效率。同时,对于引用该对象的其他变量也不会造成影响。

    此外,不变性保证了hashCode 的唯一性,因此可以放心地进行缓存而不必每次重新计算新的哈希码。而哈希码被频繁地使用, 比如在hashMap 等容器中。将hashCode 缓存可以提高以不变类实例为key的容器的性能。

  • 线程安全

    在多线程情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况同时省去了同步加锁等过程,因此不可变类是线程安全的。

当然,不可变类也有缺点:不可变类的每一次“改变”都会产生新的对象,因此在使用中不可避免的会产生很多垃圾。

以上是关于Java中的String类真的不可变吗?的主要内容,如果未能解决你的问题,请参考以下文章

Java 中不可变对象 String 真的"完全不可改变"吗?

#yyds干货盘点#关于 Java 的可变参数你真的了解吗?

Java中的不可变类

java中的不可变类

Java中常用不可变类

关于Java中的String类的不可变