深入String类不可变特性

一.从代码层面分析

二.从内存结构角度分析

三.可以通过反射来修改char数组的值

四.一个例题

一.从代码层面分析:

string源码中真正存储元素的容器是final修饰的char数组,而且没有提供set方法来提供对它的修改,这是首先在代码层面上保证了不可变性。

而如StringBuilder和StringBuffer的char数组是提供了set方法,可以修改的。(真正的容器char数组是放在二者的父类AbstractStringBuilder中,而且它们的大部分方法都是使用的AbstractStringBuilder类的方法。)

String类中还有substring, replace, replaceAll, toLowerCase等方法可以获取改变后的字符串,它不是在源字符串的基础上改变的,而是在方法内部创建了一个新的String对象,赋值给引用。

例:
public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }

二.从内存结构角度分析:

String引用指向的对象的内容(对象内存地址所存的内容)是不能改变的,但String引用(变量)是可以改变的,可以让其指向另外一个字符串。(不能修改字符串的内容,但可以修改字符串的引用)

例:
String s1 = "hello";
s1 = "world";

这个代码中,开始s1指向的是堆中的一块内存地址内容是“hello”,执行到第二句代码时,s1引用重新指向了堆中新创建的另一块内存地址,内容是“world”。

所以String引用只要重新指向其他的字符串,那么除非新指向的这个字符串已经在字符串常量池中存在了,否则就会在堆中新创建一块内存空间。(jdk1.8中字符串常量池也是在堆中)

三.可以通过反射来修改char数组的值

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

String s1 = "a bc";
System.out.println(s1);// a bc

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

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

//获取s1对象上的value属性的值
char[] value = (char[]) field.get(s1);
//修改value中的值
value[1] = '_';
System.out.println(s1);// a_bc

在这个过程中,s始终引用的同一个String对象,但是在反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。

四.一个例题

指出下列程序运行的结果:

public class Example{
    String str=new String("tarena");
    char[]ch={'a','b','c'};
    public static void main(String args[]){
        Example ex=new Example();
        ex.change(ex.str,ex.ch);
        System.out.print(ex.str+" and ");
        System.out.print(ex.ch);
    }
    
    public void change(String str,char ch[]){
   //引用类型变量,传递的是地址,属于引用传递。
        str="test ok";
        ch[0]='g';
    }
}

结果是:tarena and gbc

分析:

因为是引用类型变量,所以传递的是地址,属于引用传递。即实参和形参开始是指向同一个内存地址的。

从前面对String类不可变特性的分析可知,这个题把str引用传递时,它在change方法内重新指向了一个新创建的内存地址。所以执行到这里时str引用和之前的已经不一样了,但这个因为是在方法内部的属于局部引用,会随着方法结束而消失,所以当这个方法执行完后,str又变成之前指向"tarena"字符串的引用。

输出的str不变也是由于String的不可变特性,这个char数组没有这个特性,它的实参和形参一直是指向同一个内存地址的,所以它会改变。

上述例子对StringBuilder的测试结果如下:

public class Example{
    StringBuilder str = new StringBuilder("tarena");
    char[]ch={'a','b','c'};
    public static void main(String args[]){
        Example ex=new Example();
        ex.change(ex.str,ex.ch);
        System.out.print(ex.str+" and ");
        System.out.print(ex.ch);
    }

    public void change(StringBuilder str,char ch[]){
        // StringBuilder没有str = "a";这种赋值操作
        //str = new StringBuilder("hello");// 不变,因为是new新创建的,在堆中是新建一个内存空间
        // 结果会改变,因为使用append是在原来的StringBuilder对象上改变的
        str.append("_hello");
        ch[0]='g';
    }
}

参考博客如下:

  1. https://blog.csdn.net/weixin_43490440/article/details/101164669
  2. https://blog.csdn.net/zhangjg_blog/article/details/18319521
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值