String扫盲贴

字符串操作是最常见的操作。在Java中,往往使用String类来进行各种字符串操作。
而对于String这个类,其实隐含不少特性。对此,自己最近梳理了一遍。

字符串创建

常用方式主要两种:

String a = "123"

String b = new String("123");

第一种方式,”123“直接存储在常量池;第二种方式实际创建了两个对象,第一个对象是”123“字符串在常量池中,第二个对象是在java堆中的String对象。

不可变

翻看jdk源码,java.lang.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  
    
    ...
    ...

以上代码在jdk7,8都是一致的。

String类用final修饰,从java的final关键字语义上看,说明String不可继承。

其次,String的底层其实用了一个final类型的char数组来存储,说明该字段一旦创建后就不能改变。

其中有个很容易引起迷惑的地方,必须要弄清楚:String对象本质是引用,我们所说的不可变是指引用指向的对象内容不可变,并不是引用不可变。而String类中涉及修改的方法(substring、replace、toLowerCase等)都是创建一个新的字符串,并把这个它重新赋给引用。这就说明引用是重新指向了一个新的的字符串,但原来的字符串依旧存在内存里。

虽然说String不可变,但也不是绝对不可变,可以通过反射机制进行修改。然而大多情况不需要也没必要用到反射,这里就不详细讨论了

具体可参考Java中的String为什么是不可变的? -- String源码分析
里面阐述比较详细了。

字符串‘+’操作

以前的文章中很多都说字符串‘+’操作会导致性能低效,要用StringBuilder或StringBuffer。但其实现在的JVM已经优化得足够强大,

例如以下代码

public class StringTest{
    public static void main(String[] args) {
        String a = "hello";
        String b = "abc" + "def" + 47 + a;
    }

通过 javap -c 反编译出来的字节码如下:

public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hello
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: ldc           #5                  // String abcdef47
      12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      15: aload_1
      16: invokevirtual #6                  // 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
}

我们可以看到,字符串的‘+’操作其实在实际执行过程中,就是一个StringBuilder的append操作。

再看这段代码:

public class StringTest{
    public static void main(String[] args) {
        String a = "";
        for (int i=0;i<10;i++) {
            a = a + i;
        }
    }
}

对应的字节码:

public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String
       2: astore_1
       3: iconst_0
       4: istore_2
       5: iload_2
       6: bipush        10
       8: if_icmpge     36
      11: new           #3                  // class java/lang/StringBuilder
      14: dup
      15: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      18: aload_1
      19: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: iload_2
      23: invokevirtual #6                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      26: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      29: astore_1
      30: iinc          2, 1
      33: goto          5
      36: return
}

对于循环内的字符串拼接,虽然还是转化为StringBuilder,但是情况有点不同,留意8:和 33:之间的代码就是每一次循环所执行的操作,可以看到每一次的循环都创建了一个新的StringBuilder对象。

所以总结来说,对于普通的一次性的‘+’操作,可以放心使用;但循环下的‘+’,因为每一次都要new 一个StringBuilder而导致性能降低,因此还是先自己定义一个StringBuilder,然后每次循环通过append操作来完成

字符串常量池

JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当创建字符串常量时,JVM会首先检查字符串常量池。如果字符串已经存在池中,就返回池中的实例引用。如果字符串不在池中,就会实例化一个字符串并放到池中

在JDK6中,字符串常量池在永久代分配内存;而JDK7开始,常量池已经在Java堆上分配内存。

而字符串常量池本质是个固定容量的HashMap。 Java7和8可以通过 -XX:StringTableSize 设置其map size。
在Java6到Java7u40之前-XX:StringTableSize的默认大小是1009;7u40之后扩大到60013。

-XX:+PrintStringTableStatistics 会在程序终止时打印字符串常量池的使用情况

String.intern

String.intern是把双刃剑,用时需谨慎,切记切记!!!

String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法。

源码注释:当调用 intern 方法时,如果常量池中已经存在该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。
在这里jdk6和jdk7表现有点区别:

  1. jdk6会直接生成一个新的字符串对象到常量池中,并返回该对象引用
  2. jdk7因为常量池不在Perm区,不需要重新生成对象,而是直接存储堆中的引用

关于stirng.intern的更深入分析可看
深入解析String#intern
以及
详细可看白衣大神的String.intern() 祛魅

substring()

jdk6与jdk7中的实现方式不一样。

jdk6调用substring()虽然会创建一个新的字符串对象,但里面的char[] 仍然指向原来的那个,
因此对一个很长很长的字符串进行截取后,可能导致内存泄露。

jdk7中substring()方法在堆中真正的创建了一个新的数组,原字符数组没有被引用后就被GC回收了.因此避免了上述问题.

如有纰漏,敬请指出~

参考:
String.intern in Java 6, 7 and 8 – string pooling
JDK6和JDK7中的substring()方法

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

推荐阅读更多精彩内容

  •   需要说明的一点是,这篇文章是以《深入理解Java虚拟机》第二版这本书为基础的,这里假设大家已经了解了JVM的运...
    Geeks_Liu阅读 13,912评论 5 44
  • 缘起 开始介绍 intern()方法前,先看一个简单的 Java程序吧!下面是一段 Java代码,代码内容比较简单...
    LilacZiyun阅读 2,678评论 6 17
  • #海底两万里#【伙伴共读第155天】 今晚读《教学勇气――漫步教师心灵》第四章。 优质教育总是重视...
    维C多阅读 1,093评论 0 0
  • 先看效果 做起来很简单。就是用三角函数,算出五边形五个定点的坐标,然后根据左边连线就行了。根据看图。 具体代码如下...
    shada阅读 2,635评论 0 1
  • 听见墙角的花开 低微呢喃 绿苔爬过荒阶 破土开疆 阳光刺破落木 小蚁匆忙 蝶虫互语 嗅一阵清风松香 四野张望 好一...
    离离青青草阅读 187评论 1 2