Java之String重点解析

  1. String s = new String("abc")这段代码建立了几个对象呢?s=="abc"这个判断的结果是什么?s.substring(0,2).intern()=="ab"这个的结果是什么呢?
  2. s.charAt(index) 真的能表示出全部对应的字符吗?
  3. "abc"+"gbn"+s直接的字符串拼接是否真的比使用StringBuilder的性能低?

前言

很高兴碰见你~html

Java中的String对象特性,与c/c++语言是很不一样的,重点在于其不可变性。那么为了服务字符串不可变性的设计,则衍生出很是多的相关问题:为何要保持其不可变?底层如何存储字符串?如何进行字符串操做才拥有更好的性能?等等。此外,字符编码的相关知识也是很是重要;毕竟,如今使用emoij是再正常不过的事情了。java

文章的内容围绕着不可变 这个重点展开:c++

  1. 分析String对象的不可变性;
  2. 常量池的存储原理以及intern方法的原理
  3. 字符串拼接的原理以及优化
  4. 代码单元与代码点的区别
  5. 总结

那么,咱们开始吧~git

不可变性

理解String的不可变性,咱们能够简单看几行代码:面试

String string = "abcd";
String string1 = string.replace("a","b");
System.out.println(string);
System.out.println(string1);

输出:
abcd
bbcd

string.replace("a","b")这个方法把"abcd"中的a换成了b。经过输出能够发现,原字符串string并无发生任何改变,replace方法构造了一个新的字符串"bbcd"并赋值给了string1变量。这就是String的不可变性。正则表达式

再举个栗子:把"abcd"的最后一个字符d改为a,在c/c++语言中,直接修改最后一个字符便可;而在java中,须要从新建立一个String对象:abca,由于"abcd"自己是不可变的,不能被修改。编程

String对象值是不可变的,一切操做都不会改变String的值,而是经过构造新的字符串来实现字符串操做。api

不少时候很难理解,为何Java要如此设计,这样不是会致使性能的降低吗?回顾一下咱们平常使用String的场景,更多的时候并无直接去修改一个string,而是使用一次,则被抛弃。但下次,极可能,又再一次使用到相同的String对象。例如日志打印:安全

Log.d("MainActivity",string);

前面的"MainActivity"咱们并不须要去更改他,可是却会频繁使用到这个字符串。Java把String设计为不可变,正是为了保持数据的一致性,使得相同字面量的String引用同个对象。例如:网络

String s1 = "hello";
String s2 = "hello";

s1s2引用的是同个String对象。若是String可变,那么就没法实现这个设计了。所以,咱们能够重复利用咱们建立过的String对象,而无需从新建立他。

基于重复使用String的状况比更改String的场景更多的前提下,Java把String设计为不可变,保持数据一致性,使得同个字面量的字符串能够引用同个String对象,重复利用已存在的String对象。

在《Java编程思想》一书中还提到另外一个观点。咱们先看下面的代码:

public String allCase(String s){
    return string.toUpperCase();
}

allCase方法把传入的String对象所有变成大写并返回修改后的字符串。而此时,调用者的指望是传入的String对象仅仅做为提供信息的做用,而不但愿被修改,那么String不可变的特性则很是符合这一点。

使用String对象做为参数时,咱们但愿不要改变String对象自己,而String的不可变性符合了这一点。

存储原理

因为String对象的不可变特性,在存储上也与普通的对象不同。咱们都知道对象建立在 上,而String对象其实也同样,不同的是,同时也存储在 常量池 中。处于堆区中的String对象,在GC时有极大可能被回收;而常量池中的String对象则不会轻易被回收,那么则能够重复利用常量池中的String对象。也就是说, 常量池是String对象得以重复利用的根本缘由

常量池不轻易垃圾回收的特性,使得常量池中的String对象能够一直存在,重复被利用。

往常量池中建立String对象的方式有两种: 显式使用双引号构造字符串对象、使用String对象的intern()方法 。这两个方法不必定会在常量池中建立对象,若是常量池中已存在相同的对象,则会直接返回该对象的引用,重复利用String对象。其余建立String对象的方法都是在堆区中建立String对象。举个栗子吧。

当咱们经过new String()的方法或者调用String对象的实例方法,如string.substring()方法,会在堆区中建立一个String对象。而当咱们使用双引号建立一个字符串对象,如String s = "abc",或调用String对象的intern()方法时,会在常量池中建立一个对象,以下图所示:

image.png

还记得咱们文章开头的问题吗?

  • String s = new String("abc"),这句代码建立了几个对象?"abc"在常量池中构造了一个对象,new String()方法在堆区中又建立了一个对象,因此一共是两个。
  • s=="abc"的结果是false。两个不一样的对象,一个位于堆中,一个位于常量池中。
  • s.substring(0,2).intern()=="ab" intern方法在常量池中构建了一个值为“ab"的String对象,"ab"语句不会再去构建一个新的String对象,而是返回已经存在的String对象。因此结果是true。

只有显式使用双引号构造字符串对象、使用String对象的intern()方法 这两种方法会在常量池中建立String对象,其余方法都是在堆区建立对象。每次在常量池建立String对象前都会检查是否存在相同的String对象,若是是则会直接返回该对象的引用,而不会从新建立一个对象。

关于intern方法还有一个问题须要讲一下,在不一样jdk版本所执行的具体逻辑是不一样的。在jdk6之前,方法区是存放在永生代内存区域中,与堆区是分割开的,那么当往常量池中建立对象时,就须要进行深拷贝,也就是把一个对象完整地复制一遍并建立新的对象,以下图:

image.png

永生代有一个很严重的缺点:容易发生OOM 。永生代是有内存上限的,且很小,当程序大量调用intern方法时很容易就发生OOM。在JDK7时将常量池迁移出了永生代,改在堆区中实现,jdk8之后使用了本地空间实现。jdk7之后常量池的实现使得在常量池中建立对象能够进行浅拷贝,也就是不须要把整个对象复制过去,而只须要复制对象的引用便可,避免重复建立对象,以下图:

image.png

观察这个代码:

String s = new String(new char[]{'a'});
s.intern();
System.out.println(s=="a");

在jdk6之前建立的是两个不一样的对象,输出为false;而jdk7之后常量池中并不会建立新的对象,引用的是同个对象,因此输出是true。

jdk6以前使用intern建立对象使用的深拷贝,而在jdk7以后使用的是浅拷贝,得以重复利用堆区中的String对象。

经过上面的分析,String真正重复利用字符串是在使用双引号直接建立字符串时。使用intern方法虽然能够返回常量池中的字符串引用,可是自己已经须要堆区中的一个String对象。于是咱们能够得出结论:

尽可能使用双引号显式构建字符串;若是一个字符串须要频繁被重复利用,能够调用intern方法将他存放到常量池中。

字符串拼接

字符串操做最多的莫过于字符串拼接了,因为String对象的不可变性,若是每次拼接都须要建立新的字符串对象就太影响性能了。所以,官方推出了两个类: StringBuffer、StringBuilder 。这两个类能够在不建立新的String对象的前提下拼装字符串、修改字符串。以下代码:

StringBuilder stringBuilder = new StringBuilder("abc");
stringBuilder.append("p")
        .append(new char[]{'q'})
        .deleteCharAt(2)
        .insert(2,"abc");
String s = stringBuilder.toString();

拼接、插入、删除均可以很快速地完成。所以,使用StringBuilder进行修改、拼接等操做来初始化字符串是更加高效率的作法。StringBuffer和StringBuilder的接口一致,但StringBuffer对操做方法都加上了synchronize关键字,保证线程安全的同时,也付出了对应的性能代价。单线程环境下更加建议使用StringBuilder。

拼接、修改等操做来初始化字符串时使用StringBuilder和StringBuffer能够提升性能;单线程环境下使用StringBuilder更加合适。

通常状况下,咱们会使用+来链接字符串。+在java通过了运算符重载,能够用来拼接字符串。编译器也对+进行了一系列的优化。观察下面的代码:

String s1 = "ab"+"cd"+"fg";
String s2 = "hello"+s1;

Object object = new Object();
String s3 = s2 + object;
  • 对于s1字符串而言,编译器会把"ab"+"cd"+"fg"直接优化成"abcdefg" ,与String s1 = "abcdefg"; 是等价的。这种优化也就减小了拼接时产生的消耗。甚至比使用StringBuilder更加高效。

  • s2的拼接编译器会自动建立一个StringBuilder来构建字符串。也就至关于如下代码:

    StringBuilder sb = new StringBuilder();
    sb.append("hello");
    sb.append(s1);
    String s2 = sb.toString();

    那么这是否是意味着咱们能够不须要显式使用StringBuilder了,反正编译器都会帮助咱们优化?固然不是,观察下边的代码:

    String s = "a";
    for(int i=0;i<=100;i++){
        s+=i;
    }

    这里有100次循环,则会建立100个StringBuilder对象,这显然是一个很是错误的作法。这时候就须要咱们来显示建立StringBuilder对象了:

    StringBuilder sb = new StringBuilder("a");
    for(int i=0;i<=100;i++){
        sb.append(i);
    }
    String s = sb.toString();

    只须要构建一个StringBuilder对象,性能就极大地提升了。

  • String s3 = s2 + object; 字符串拼接也是支持直接拼接一个普通的对象,这个时候会调用该对象的toString方法返回一个字符串来进行拼接。toString方法是Object类的方法,若子类没有重写,则会调用Object类的toString方法,该方法默认输出类名+引用地址。这看起来没有什么问题,可是有一个大坑:切记不要在toString方法中直接使用+拼接自身 。以下代码

    @Override
    public String toString() {
        return this+"abc";
    }

    这里直接拼接this会调用this的toString方法,从而形成了无限递归。

Java对+拼接字符串进行了优化:

  • 能够直接拼接普通对象
  • 字面量直接拼接会合成一个字面量
  • 普通拼接会使用StringBuilder来进行优化

但同时也有注意这些优化是有限度的,咱们须要在合适的场景选择合适的拼接方式来提升性能。

编码问题

在Java中,通常状况下,一个char对象能够存储一个字符,一个char的大小是16位。但随着计算机的发展,字符集也在不断地发展,16位的存储大小已经不够用了,所以拓展了使用两个char,也就是32位来存储一些特殊的字符,如emoij。一个16位称为一个 代码单元 ,一个字符称为 代码点 ,一个代码点可能占用一个代码单元,也多是两个。

在一个字符串中,当咱们调用String.length() 方法时,返回的是代码单元的数目, String.charAt() 返回也是对应下标的代码单元。这在正常状况下并无什么问题。而若是容许输入特殊字符时,这就有大问题了。要得到真正的代码点数目,能够调用 String .codePointCount 方法;要得到对应的代码点,可调用 String.codePointAt 方法。以此来兼容拓展的字符集。

一个字符为一个代码点,一个char称为一个代码单元。一个代码点可能占据一个或两个代码单元。若容许输入特殊字符,则必须使用代码点为单位来操做字符串。

总结

到此,关于String的一些重点问题就分析完毕了,文章开头的问题读者应该也都知道答案了。这些是面试常考题,也是String的重点。除此以外,关于正则表达式、输入与输出、经常使用api等等也是String相关很重要的内容,有兴趣的读者可自行学习。

但愿文章对你有帮助。

参考资料

  • 《Java编程思想》 java工程师皆知的神书,详细讲解了如何更好运用java来编程,感觉编程思想。
  • 《Java核心技术卷一》 入门书籍,主要讲解如何使用String的api以及一些注意的点。
  • 《深刻理解JVM》对于理解方法区以及常量池有很是大的帮助。
  • 深刻解析String#intern美团技术团队的一篇分析String.intern方法的文章。
  • 感谢网络其余博客的贡献。

全文到此,原创不易,以为有帮助能够点赞收藏评论转发。
笔者才疏学浅,有任何想法欢迎评论区交流指正。
如需转载请评论区或私信交流。

另外欢迎光临笔者的我的博客:传送门

相关文章
相关标签/搜索