关于java:Java之String重点解析

6次阅读

共计 5679 个字符,预计需要花费 15 分钟才能阅读完成。

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

前言

很快乐遇见你~

Java 中的 String 对象个性,与 c /c++ 语言是很不同的,重点在于其 不可变性 。那么为了服务字符串不可变性的设计,则衍生出十分多的相干问题:为什么要放弃其不可变?底层如何存储字符串?如何进行字符串操作才领有更好的性能?等等。此外, 字符编码 的相干常识也是十分重要;毕竟,当初应用 emoij 是再失常不过的事件了。

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

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

那么,咱们开始吧~

不可变性

了解 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 的值,而是通过结构新的字符串来实现字符串操作。

很多时候很难了解,为什么 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() 办法时,会在常量池中创立一个对象,如下图所示:

还记得咱们文章结尾的问题吗?

  • 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 以前,办法区是寄存在永生代内存区域中,与堆区是宰割开的,那么当平常量池中创建对象时,就须要进行深拷贝,也就是把一个对象残缺地复制一遍并创立新的对象,如下图:

永生代有一个很重大的毛病:容易产生 OOM。永生代是有内存下限的,且很小,当程序大量调用 intern 办法时很容易就产生 OOM。在 JDK7 时将常量池迁徙出了永生代,改在堆区中实现,jdk8 当前应用了本地空间实现。jdk7 当前常量池的实现使得在常量池中创建对象能够进行浅拷贝,也就是不须要把整个对象复制过来,而只须要复制对象的援用即可,防止反复创建对象,如下图:

察看这个代码:

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 办法的文章。
  • 感激网络其余博客的奉献。

全文到此,原创不易,感觉有帮忙能够点赞珍藏评论转发。
笔者满腹经纶,有任何想法欢送评论区交换斧正。
如需转载请评论区或私信交换。

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

正文完
 0