String 和 Stringbuild

概述

这一篇想介绍一下 String 这个基本类型 ,包括它的底层实现和一些用法, 一些需要注意的地方.

字符串在内存布局的表示

这一节的内容请仔细看 R 大的这两篇文章

R 大是从编程语言中出发 ,而不是只是单一地从 java 做介绍 (大佬 ,膜拜~~) , 这里我简要得概述一下这两篇主要讲了什么 .

篇一主要介绍 , 字符串在内存中该如何表示的, 阐述了几个方面是设计编程语言时需要考虑的 :

string类型的封装可以在几个维度上表现出差异:
0、“拿在手上”的是什么?
1、字符串元数据与字符串内容打包为整体存放,还是分离存放;
2、不同字符串实例是否共享字符串内容;
3、字符串是否显式记录长度;
4、字符串是否有'\0'结尾(null-terminated),字符串内容是否允许存'\0'(embedded null);
5、外部指针或引用指向字符串的什么位置;
6、字符串的存储容量(capacity)是否可以大于字符串内容的长度(length);
7、是否有对齐要求,结尾是否有padding。

那么第一个问题也就是 表示字符串的虚拟地址空间内 ,到底放的是什么呢 ?

a) 直接是字符串内容?
b) 是指向字符串实体的指针?
c) 是指向字符串实体的“指针的指针”?
d) 是一个代表某个字符串的token?

这里我就不累赘了, 感兴趣的读者可以去了解一下 , 这里介绍和今天相关的内容, java 属于 b) 是指向字符串实体的指针

String s = "foo";
String s1 = s; 

img

以及 c) 是指向字符串实体的“指针的指针” , 也就是我们常说的句柄

img

这里也提一文字介绍到使用句柄的一个好处 :

使用句柄的好处是实现起来可以偷懒。假如有内存管理器需要移动对象(例如说mark-compact或者copying GC),那就得修正所有相关指针。但遍历所有相关指针需要费点功夫,想偷懒的话就可以像这样增加一个间接层,不允许外界直接拥有指向对象的指针,而是让外界持有句柄,句柄可以是指向“句柄表”(handle table)的指针,而句柄表里的元素才真的持有指向对象的指针。要修正指针的时候只要遍历句柄表来修正即可。

句柄的缺点也显而易见多了一个"中间层" 那么就得相应的得有一个空间来储存引用 ,来自<<深入理解java虚拟机>>的图片

img

img

这其中比较好奇的是 , 那么 java 中存在句柄的空间在哪呢 ? 对应的是哪一块的空间 . 应该是常量表 ,

img

img

元数据,字符串内容:整体还是分离?

想想假如让我们来设计 String 这个类 , 那么有以下两种方案

"整体式",例如说32位x86上.NET 4.0的System.String的内存布局是这样的:
"foobar"

img

"分离式" , 64位Oracle JDK7u40/OpenJDK 7u的java.lang.String例子(假定开了压缩指针):
"foobar"

img

深入String

先看一个例子

public class StringTest {

    public static void main(String[] args) {
        String s = "hello2";
        String s2 = "hello2";


        String s1 = "hello" + 2 ;

        String s3 = "hello2";

        String s4 = new String("hello2");

        final  String  s5 = s2 + 2;
        String  s6 = s2 + 2;


        System.out.println(s==s1);// 结果为: true
        System.out.println(s==s3);// 结果为: true
        System.out.println(s==s4);// 结果为: false
        System.out.println(s==s5);// 结果为: false
        System.out.println(s==s6);// 结果为: false

    }

}

我们分析一下上面的例子, 第一个为 true , 这是因为 String 用 + 拼接的话 , 实际是调用了 StringBuilder 的 append 方法, 可以在 String 的 注释中看到

img

StringBuilder 的 append 方法会在 MetaSpace(JDK1.7 之前称为方法区) 里面找如果已经存在字符串了 ,那么就不再创建了 , 直接返回实例 . 就像下面的例子

img

第二个的就像上面的例子 , 在 Constant Table 是同一项 , 返回是 true , 第三个返回 false 也可以用上面的图表示 , 也就是Local Variable Tableonetwo .

第四个可以看这篇文章 , 第四和第五个作为对比

什么是不可变类

下面这段来自[java中的不可变类](https://www.cnblogs.com/zhiheng/p/6653969.html)

先来看一下可变类和不可变类。

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

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

不可变类的优势 :

  • 效率

当一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)只需要很小的内存空间,具有非常高的效率。同时,对于引用该对象的其他变量也不会造成影响。
此外,不变性保证了hashCode 的唯一性,因此可以放心地进行缓存而不必每次重新计算新的哈希码。而哈希码被频繁地使用, 比如在hashMap 等容器中。将hashCode 缓存可以提高以不变类实例为key的容器的性能。

  • 线程安全

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

在《java concurrency in practice》一书给出了一个粗略的定义:对象一旦创建后,其状态不可修改,则该对象为不可变对象。一般一个对象满足以下三点,则可以称为是不可变对象:

  • 其状态不能在创建后再修改;
  • 所有域都是final类型;
  • 其构造函数构造对象期间,this引用没有泄露。

String 为什么是不可变类

例子

String s= "abcd";
s = "abcdel";

图片出处见参考资料。
1297993-20200318141640096-1992387794.jpg

底层

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;

    ....

}

可以看到String 维护的 char数组是private final 的。假如我们要是可以修改char里的内容那么,String 肯定就不是不可变类了,例如下面这种操作。

final int[] value={1,2,3}
value[2]=100; //更改了第三个元素的值

** 但是String中没有提供方法对char 数组进行更改,所以String这个字符串保证了不可变。 **

String 作为不可变类安全优势的例子

public final class  Fa {
    private int num;

    public static String stringAdd(String s){
        s += "bbb";
        return s;

    }

    public static StringBuilder stringBAdd(StringBuilder s){
        s .append("bbb");
        return s;

    }


    public Fa(int num) {
        this.num = num;
    }
}




        String s = "s1";
        String result = Fa.stringAdd(s);
        System.out.println("result : " + s);


        StringBuilder sb = new StringBuilder("sb");
        String sbResult = Fa.stringBAdd(sb).toString();
        System.out.println("sbResult : " + sb);


        结果 : 
        result : s1
		sbResult : sbbbb

所以我们建议在当大量使用是字符串的时候使用 stringbuild 可以提高性能,不然新创建的String 对象会占用堆空间,可能导致频繁GC。

String在java中是不可变长的,一旦初始化就不能修改长度,简单的字符串拼接其实是创建新的String对象,再把拼接后的内容赋值给新的对象,在频繁修改的情况下会频繁创建对象。
而StringBuilder则不会,从头到尾只有一个实例对象,那StringBuilder是怎么实现的呢?
其实StringBuilder在append时并不是用String存储,而是放到一个value的char数组中,字符串是固定长度的,而数组是可以扩容的,这样就不需要不停创建对象了。

真的是不可变类吗

实际可以通过反射来获取char数组,从而知道了String的值,这也是很多开发中密码这类属性不建议使用String而是使用char数组的原因。当然开发中很少使用反射去这样获取。

参考资料

posted @ 2020-03-18 14:38  float123  阅读(311)  评论(0)    收藏  举报