"细思极恐"的String类(不可变性)

String类(自己的一些总结)

今天在CSDN上看到了一道关于String类的demo,让我意识到是时候好好的研究一下String类了,似乎对它的操作总能产生一些出乎意料的输出结果,于是在网上找了些资料并结合自己遇到的题目进行了一些总结,欢迎指正其中的错误。题目如下:
    public static void main(String[] args) {
        StringBuffer sb1 = new StringBuffer("Hello");
        StringBuffer sb2 = new StringBuffer("World");
        change(sb1,sb2);
        System.out.println(sb1 + " " + sb2);
        String s1 = "Hello";
        String s2 = "World";
        change(s1,s2);
        String s3 = s1;
        s3 = s2;
        System.out.println(s1 + " " + s2);
        StringBuilder ssss = new StringBuilder("123");
        ssss.toString();
    }   
    public static void change(StringBuffer sb1, StringBuffer sb2) {
        sb1 = sb2;
        sb1.append(sb2);
    }
    public static void change(String s1, String s2) {
        s1 = "123";
        s2 = s1 + s2;
    }

最后的结果为:Hello WorldWorld
Hello World

如果你对这个结果感到很疑惑,那么请你看完这篇文章,因为你对String的理解真的还不够透彻。

String的不可变

大多数人都知道String是不可变的,但多数也仅停留于此,并不能深刻的理解这句话。请看如下代码:

String s = "Hello";
s = "hello";
System.out.println(s);

输出结果:hello

相信我们都能推测这个结果,但是String不是不可变的吗?而这里我们给输出s却变成了hello,那是不是就说明String是可变的呢?这里所说的不可变指的是创建的"Hello"字符串是在内存中不可变的。画一个简图:

String.png
当"Hello"在堆中被创建之后,就不可变,只能改变s所存的地址值,才能完成s="hello"的赋值。

String作为函数参数传递

而为初步理解上面的那道题,单纯的了解String的不可变性是不够的,我们还要知到String在作为函数值传递时发生了什么,其实,String与基本数据类型作为参数传递时,传递的只是值的一个副本,在函数中对数据进行操作,不会改变原有的值,改变的只是副本值。就以这道题为例子,它的简单内存分析图如下:

仅看Stirng类的操作

1.当为调用change方法时:

String2.png

2.当开始运行change方法时:

String3.png

函数参数传递,就是将main方法中的s1,s2的地址值,传递给change方法中的s1,s2的地址值。

3.change方法的操作:

String4.png

s1="123";和s2=s1+s2;要特别注意是对change方法的s1,s2操作。

4.结束调用:

String2.png

最后你会惊奇的发现change方法就像一个挑梁小丑一样,一顿操作但是,对main方法的s1,s2没有造成任何“伤害”。

对于StringBuffer的操作则不用多说,与正常对象一致,这里不做详细解释。sb1=sb2,并不会改变堆内存中村的值,sb1.append(sb2);会改变堆中的值,因此输出发生改变。

java只支持值传递(面试题)

值得注意的事,java机制是只支持值传递,java认为,即使传递的是引用,也把他看成地址值看待,所以就是值传递,实际上可以说是引用传递了,只不过传递的是地址值


到此,我们应该已经对String不可变形,有了一定的了解,我也因此对String类产生了浓厚的兴趣,于是我开始查找String类为什么具有不可变性

String类不可变性(源码如何实现)

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

/** 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

在在JDK1.9后,String类又做了一些改动,将字符串使用字节数组存储了,当然这部影响研究其不可变性的原理

 @Stable
    private final byte[] value;
    /*
     * LATIN1
     * UTF16
     */
    private final byte coder;

    private int hash; // Default to 0
   
    private boolean hashIsZero; // Default to false;

    static final boolean COMPACT_STRINGS;

    static {
        COMPACT_STRINGS = true;
    }

    

由以上的代码可以看出, 在Java中String类其实就是对字符数组的封装。

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

总结:

设计 作用
主要字段都是private的 String类的外部无法直接修改String
没有提供setXXX等公共方法来修改这些值 String类的外部无法间接修改String
主要字段都是final的 String类内部也不能被改变
类被final修饰 无法继承覆写String类中的方法

当然上面的讨论,均是在不考率反射的前提下,其实我们可以利用反射来直接改变String类堆内存的值:

public class Stringdemo {
    public static void main(String[] args) throws Exception {   
        //创建字符串"Hello World", 并赋给引用s
        String s = "Hello World"; 
        
        System.out.println("s = " + s); //Hello World
        
        //获取String类的反射对象
        Class<String> clazz = String.class;
        
        //获取String类中的value字段
        Field valueFieldOfString = clazz.getDeclaredField("value");
        
        //改变value属性的访问权限
        valueFieldOfString.setAccessible(true);
        
        //获取s对象上的value属性的值
        byte[] value = (byte[]) valueFieldOfString.get(s);
        
        //改变value所引用的数组中的第5个字符
        value[5] = '_';
        
        System.out.println("s = " + s);  //Hello_World
    }
}

结果:

s = Hello World
s = Hello_World

可以通过结果看到,我们改变了String的值,笔者用的是JDK9以上,如果使用jDK9以下版本,应将byte[] value = (byte[]) valueFieldOfString.get(s); 改为char[] value = (char[]) valueFieldOfString.get(s);即可运行。

————————————————

引用了:

CSDN博主「昨夜星辰_zhangjg」的原创文章
原文链接:https://blog.csdn.net/zhangjg_blog/article/details/18319521

为什么java要将String设为不可变

1.字符串常量池的需要

字符串常量池(String pool, String intern pool) 是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串
如下面的代码所示,将会在堆内存中只创建一个实际String对象。

String s1 = "Hello"; 
String s2 = "Hello"; 
String s3 = "Hel"+"lo"; 
  • 注:对于s3,在编译时就会将"Hel"+"lo"自动相连,并在常量池中查找

可画出内存图:

String5.png

2.允许String对象缓存HashCode

Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。
字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存,这也是一种性能优化手段,意味着不必每次都去计算新的哈希码。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
在String类的定义中有如下代码:

private int hash;//用来缓存HashCode

3.安全性

String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了其他类,那么会对你的数据库造成不可知的破坏。


引用了:
CSDN博主「不能说的秘密go」的原创文章
原文链接:https://blog.csdn.net/canot/article/details/103754370


其实除了不可变性,String类还有一些其他很有意思(面试题会考)的知识,如上面提到的字符串常量池。

(equals,==)针对于String

==: 比较的是两个String对象的地址值是否相同

equals:比较的是两个String对象的是否相同

我们知道,对于Object类中的equals方法,equals(),==两者等价,之所以String类这么"典型"是因为String类重写了equals方法,上源码:

    public boolean equals(Object anObject) {
        if (this == anObject) { 
            return true;
        }
        if (anObject instanceof String) {
            String aString = (String)anObject;
            if (!COMPACT_STRINGS || this.coder == aString.coder) {
                return StringLatin1.equals(value, aString.value);
            }
        }
        return false;
    }

//StringLatin1.equals()
 public static boolean equals(byte[] value, byte[] other) {
        if (value.length == other.length) {
            for (int i = 0; i < value.length; i++) {
                if (value[i] != other[i]) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

简单来讲,判断步骤为:

  1. 判断两者地址值是否相同

  2. 判断要比较的对象是否属于String类

  3. if (!COMPACT_STRINGS || this.coder == aString.coder),这里是关于jdk9的新特性,String类存在了byte[]中,coder存的是字符串的编码集,COMPACT_STRINGS默认为true,表示这种特性为开启状态。

    //源码
    static final boolean COMPACT_STRINGS;
    static {
        COMPACT_STRINGS = true;
    }
    

    扩展:

    coder方法(源码)判断COMPACT_STRINGS为true的话,则返回coder值,否则返回UTF16;isLatin1方法(源码)判断COMPACT_STRINGS为true且coder为LATIN1(ISO-8859-1)则返回true。

    诸如charAt、equals、hashCode、indexOf、substring等等一系列方法都依赖isLatin1方法来区分对待是StringLatin1还是StringUTF16。

  4. 判断内容长度是否相等

  5. 判断内容是否相等

    注:除了第3条,这些通常也是我们重写equals的判断步骤。

字符串常量池

先上题目:

String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = "hel"+"lo";
String s5 = "hel"+new String("lo");
String s6a = "hel";
String s6b = "lo";
String s6 = s6a+s6b;

//下列判断结果分别为true还是false
/*
s1 == s2;
s1 == s3;
s1 == s4;
s1 == s5;
s1 == s6; 
s1.equals(s3);
s1.equals(s5);
s1.equals(s6);
答:
true
false
true
false
false
true
true
true
*/

我感觉把这些搞懂,再来做考察字符串常量池的面试题只要认真思考都能拿下。

1.对于s1 == s2 == s4;在上面介绍为什么java要将String设为不可变时已经说过,在此不再缀叙。

2.对于s3,使用new方式初始化字符串,会在堆上分配一个存放字符串对象的空间,然后查找常量池中是否存在内容为 “hello” 的字符串对象,如果存在则返回引用,不存在则创建再返回引用,此时内存分配情况为:

String6.png

番外知识:public String intern(); //手动入池。

String7.png

3.再解释s5,s6我们先要了解一下“+”,理解它作为Java的字符串连接符,所做的操作。当我们看到String s5 = "hel"+new String("lo");时一定能知道s5 = hello,因为我们能非常直观的理解连接的意思,却往往没有想过,+是如何实现连接的。

让我们看看编译器是怎么处理 + 操作符的:

例:

String str0 = "a";
String str1 = str0 + "b";

下面是这一个程序片段编译后的字节码指令:

public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String a
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String b
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_2
      23: return

(画外音)作为一个菜鸡,我是没太看懂。

如上可知:+拼接操作实际上被编译器理解成了这个样子:

String str0 = "a";
StringBuilder sb = new StringBuilder();
sb.append(str0).append("b");
String str1 = sb.toString();

再看StringBuilder的toString方法源码:

public String toString() {
        // Create a copy, don't share the array
        return isLatin1() ? StringLatin1.newString(value, 0, count)
                          : StringUTF16.newString(value, 0, count);
    }

//以StringLatin1.newString(value, 0, count)为例
public static String newString(byte[] val, int index, int len) {
        if (len == 0) {
            return "";
        }
        return new String(Arrays.copyOfRange(val, index, index + len),
                          LATIN1);
    }

看到newString方法里使用构造方法即new String()初始化字符串,我相信你应该明白s5,s6为何与s1做 == 判断会为false了,其实他们都隐式的调用了构造函数(个人的理解)生成字符串与s3一样会在堆内存中开辟了两处内存空间。

再此注意String s4 = "hel"+"lo";在编译时就会自动被理解为s4 = "hello";

字符串池的优缺点

优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;

缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。

这些是我通过看别人的博客,和结合自己的理解总结的知识点,以后关于String、StringBuffer、StringBuilder的相关知识还会持续更新在这里,未完待续。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355