String对象不可变吗

本篇内容主要讲解“String对象不可变吗”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“String对象不可变吗”吧!

创新互联专业为企业提供和平网站建设、和平做网站、和平网站设计、和平网站制作等企业网站建设、网页设计与制作、和平企业网站模板建站服务,十余年和平做网站经验,不只是建网站,更提供有价值的思路和整体网络服务。

注:本文基于JDK8

String相信是Java中很基础的一个类,也是很多初级面试中很喜欢问的,包括String在JVM中的池化(常量池)、String的intern方法、如何避免内存溢出(主要存在JDK1.7之前)等,而String是创建后就不可变的对象从最开始学习Java我们就会从各种书籍、教程、面试宝典上看到,然而事实真的如此吗?你对这个不可变理解的正确吗?

首先我们看以下代码:

String text = "hello : ";
text += "JoeKerouac";
System.out.println(text);
 

上述代码运行结果相信大多数人都会知道,最终会打印出"hello : JoeKerouac",而这是我们最常用的“改变”字符串的方法,为什么改变加引号呢?是因为这个方法其实并没有真正改变字符串本身,而是重新创建了一个新的String对 象,然后将text引用指向了这个新建的对象,原String对象"hello : "并没有被改变,似乎String确实是无法改变的,然而真的就没办法改变吗?首先我们来看String的class文件,可以看到用来存储字符串的实际数据的char数组是final类型的,而该数组也并没有相应的get方法,看起来是很完美,那如果用反射去更改呢?下面我们来上代码:

import java.lang.reflect.Field;

public class StringTest {
    public static void main(String[] args) {
        String text = "JoeKerouac";
        change(text);
        System.out.println(text);
    }

    /**
     * 更改字符串值,将字符串的第一个字符更改为j
     *
     * @param txt
     *            要更改的字符串
     */
    static void change(String txt){
        try {
            Field valueField = txt.getClass().getDeclaredField("value");
            valueField.setAccessible(true);
            char[] value = (char[]) valueField.get(txt);
            value[0] = 'j';
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            System.err.println("当前系统不允许反射调用");
        } catch (NoSuchFieldException e) {
        }
    }
}
 

上述代码运行后你猜结果是什么?是"joeKerouac",是的,你没猜错,是"joeKerouac"而不是"JoeKerouac",下面我们再来看一个例子:

import java.lang.reflect.Field;

public class StringTest {
    public static void main(String[] args) {
        String text = "JoeKerouac";
        change(text);
        String textCopy = "JoeKerouac";
        System.out.println(textCopy);
    }

    /**
     * 更改字符串值,将字符串的第一个字符更改为j
     *
     * @param txt
     *            要更改的字符串
     */
    static void change(String txt){
        try {
            Field valueField = txt.getClass().getDeclaredField("value");
            valueField.setAccessible(true);
            char[] value = (char[]) valueField.get(txt);
            value[0] = 'j';
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            System.err.println("当前系统不允许反射调用");
        } catch (NoSuchFieldException e) {
        }
    }
}
 

上述代码运行后结果是什么呢?机智点儿的小伙伴应该猜出来了,还是"joeKerouac",可能还有人会一脸懵逼,上个例子还能看的懂,是用反射将text对象更改了,所以结果是"joeKerouac",可是这个明明textCopy是在运行完change方法后才声明出来的,怎么也被修改了呢?这是因为这两个字符串的字面值一样,所以实际上他们指向的String对象都是同一个,这是Java对String池化了(当然,这个不是本文重点),所以当text更改后textCopy实际也就被更改了 ,同时也说明textCopy的声明虽然在change方法调用的后一行,但是他在change方法调用前就与text一样通过某种手段链接到了同一个String对象上。

看到这里可能有的同学觉得这就结束了,String对象确实被我们用反射改变了,但是此时内存是怎么样的呢?内存中的"JoeKerouac"这个最早声明的字符串是被改变了还是只是跟文章开头的代码段一样仅仅是重新创建了一个对象,然 后让字符串的引用指向了这个新对象,原对象没有更改呢?其实大概我们也是可以猜出来的,我们是用反射更改的String中的value数组,并没有使用公共API去更改,从逻辑上来说JVM是无感知的,他不应该也没有时机去创建一个新>的字符串然后返回,那么事实是否如此呢?下面我们来看下面这个例子:

import java.lang.reflect.Field;

public class StringTest {
    public static void main(String[] args) throws InterruptedException {
        String text = "JoeKerouac";
        System.out.println("now text is : " + text);
        //在此时做一次heap dump
        Thread.sleep(1000 * 15);
        change(text);
        System.out.println("now text is : " + text);
        //在此时重新heap dump一下
        Thread.sleep(1000 * 60 * 60);
    }

    /**
     * 更改字符串值,将字符串的第一个字符更改为j
     *
     * @param txt
     *            要更改的字符串
     */
    static void change(String txt) {
        try {
            Field valueField = txt.getClass().getDeclaredField("value");
            valueField.setAccessible(true);
            char[] value = (char[]) valueField.get(txt);
            value[0] = 'j';
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            System.err.println("当前系统不允许反射调用");
        } catch (NoSuchFieldException e) {
        }
    }
}
 

在上述的两个注释处分别做heap dump操作,推荐使用JVisual VM,方便后续操作,heap dump完毕后使用JVisual VM对两次dump出来的文件进行分析,可以使用JVisual VM中的OQL控制台查询,具体查询语句如下:

select {instance: s, content: s.toString(), id: objectid(s)} from java.lang.String s where s.toString() == 'joeKerouac' || s.toString() == 'JoeKerouac'
 

最后结果应该是第一次dump文件会有一个JoeKerouac字符串,第二次dump文件会有一个joeKerouac字符串,而且两个字符串的ID是一样的,说明我们上面的猜测是正确的,JVM并没有去创建一个新的字符串返回,而我们确实做到了更>改String的值,真正的更改内存值。

我们从第三段代码可以看出使用该种方法可以做到更改用户编码时定义的字符串,假如在你的系统里有一个password字符串,而你恰巧在某个地方用这个方法将这个字符串更改了,别人要是不知道估计调好久都找不到哪儿的原因,明 明声明的对着的,但是打印就是不对,哈哈~ 另外如果你要是某天遇到这个问题说不定就是某个调皮的小鬼给你这样搞了一波~

最后,我们看到String并不是如传说那样一成不变的,依托于Java强大的反射系统,即使是String这样的不可变对象也会改变~(PS:有些安全性要求高的系统会限制反射的使用)

到此,相信大家对“String对象不可变吗”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!


本文题目:String对象不可变吗
网页URL:http://hbruida.cn/article/gcdgeh.html