Java漫谈-String上

本文首发:windCoder.com

由于具体关注的内容的特殊性,如无特殊注明,本文讨论均基于Java8。

不可变

String对象是不可变的。每次修改都是创建了一个全新的String对象,以包含修改后的字符串内容,最初的String对象在原处丝毫未动。

对一个方法而言,参数是为该方法提供信息的,而不是想让该方法改变自己的。

  • String类是final的,不可被继承。
  • String类的本质是字符数组char[], 并且其值不可改变。即:private final char value[];
  • String类对象有个特殊的创建的方式,直接赋值,如'''String x = "abc"```, 字面量(String Literals)"abc" 就表示一个字符串对象,变量 x 指向其该字符串对象的地址,即是一个引用。
  • JVM存在一个String Pool(String池/字符串常量池/全局字符串池,也有叫做string literal pool),1.7之前处于方法区中,之后被分离出来放在了堆中。
  • 两个有用的类StringBuffer和StringBuilder。前者线程安全,但比后者速度较慢。
  • 1.8新出了一个StringJoiner类,,用于构造由分隔符分隔的字符序列,并可选择性地从提供的前缀开始和以提供的后缀结尾。

重载“+”

内部并不是创建n个String对象,而是创建了一个StringBuilder对象,通过其append()方法连接,最后调用toStrong()方法返回。

当为类似String s = "a" + "b" + "c";的单行操作时,编译器会执行优化,在编译时直接合成一个“abc”。

该操作适用于单行“+”操作,不适用于循环(如for等)。因为在循环中,每次循环会生成一个新的个StringBuilder对象。

循环时的手动优化:在外创建StringBuilder对象,在循环内部执行append()方法拼接字符串。

StringBuilder 是JavaSE5引入的,之前都是StringBuffer。后者是线程安全的,因此开销会大些,所以在javaSE5及以后中,字符串操作应该还会更快一点。

创建

创建方式

创建字符串的方式很多,归纳起来有三类:

  • 使用new关键字创建字符串,比如String s1 = new String("abc");
  • 直接指定。比如String s2 = "abc";
  • 使用串联生成新的字符串。比如String s3 = "ab" + "c";

分析创建

下面一起看下在创建与运行时内部具体发生了些什么。

示例1

public class StringDemo1 {
    public static void main(String[] args) {
        String s1 = new String("123");
    }
}

当仅运行这段代码期间,涉及用户声明的几个String变量?

答案很简单

一个,就是String s。

涉及的实例/对象呢?

先说答案

两个,一个是字符串字面量"123"所对应的、驻留(intern)在一个全局共享的字符串常量池中的实例,另一个是通过new String(String)创建并初始化的、内容与"123"相同的实例。

至于原因,要从StringDemo1类的编译说起:

当编译完成,会生成StringDemo1.class文件,该文件中,"123"会被提取并放置在class常量池中,当JVM加载类时会通过读取该class常量池创建并驻留一个String实例作为常量来对应"123"字面量(其引用存储在String Pool中,未注明时以下均称“字符串池”或“常量池”),这是一个全局共享的,只有当字符串池中没有相同内容的字符串时才需要创建

当执行main方法中的new语句时,JVM会执行的字节码类似:

0: new           #2                  // class java/lang/String
3: dup
4: ldc           #3                  // String 123
6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1

这之中出现过多少次new java/lang/String就是创建了多少个String对象,即代码String s1 = new String("123");执行一次只会创建一个实例对象

下面是RednaxelaFX对于这段字节码含义的描述:

在JVM里,“new”字节码指令只负责把实例创建出来(包括分配空间、设定类型、所有字段设置默认值等工作),并且把指向新创建对象的引用压到操作数栈顶。此时该引用还不能直接使用,处于未初始化状态(uninitialized);

如果某方法a含有代码试图通过未初始化状态的引用来调用任何实例方法,那么方法a会通不过JVM的字节码校验,从而被JVM拒绝执行。

能对未初始化状态的引用做的唯一一种事情就是通过它调用实例构造器,在Class文件层面表现为特殊初始化方法<init>

实际调用的指令是invokespecial,而在实际调用前要把需要的参数按顺序压到操作数栈上。

在上面的字节码例子中,压参数的指令包括dup和ldc两条,分别把隐藏参数(新创建的实例的引用,对于实例构造器来说就是“this”)与显式声明的第一个实际参数("123"常量的引用)压到操作数栈上

最终如图:

image.png

黑线表示String对象的内容指向。

示例2

public class StringDemo2 {
    public static void main(String[] args) {
        String s1 = new String("123");
        String s2 = "123";
    }
}

这里我们看下String s2 = "123";的字节码:

10: ldc           #3                  // String 123
12: astore_2

由此可见s2直接引用的是字符串常量池中的对象。故该实例中依旧是生成了2个实例对象。如图:

image.png

黑线同实例1中的,红线为s2引用的指向,因为常量池中已经存在"123",所以不会再创建。s2会通过查询常量池获取池中"123"的地址并指向。

若再加一个String s3 = new String("123");呢?此时只会再创建一个实例对象,从而一共是3个。从而有了如下:

public class StringDemo2 {
    public static void main(String[] args) {
        String s1 = new String("123");
        String s2 = "123";
        String s3 = new String("123");
        PrintUtill.println(s1==s2);
        PrintUtill.println(s2==s3);
        PrintUtill.println(s1==s3)
    }
}

结果为:

false
false
false

StringJoiner用法简介

StringJoiner类是Java8的一个新类(还有一个新类Optional可用来解决空指针的问题),可以通过指定分隔符拼接字符串,功能与String.join方法类似,同时可选择性地从提供的前缀开始和以提供的后缀结尾。这里简单展示用法,不做过多讨论。

StringJoiner sj = new StringJoiner(":", "[", "]");
sj.add("www").add("windcoder").add("com");
String desiredString = sj.toString();
PrintUtill.println(desiredString);

执行结果:

[www:windcoder:com]

String.join()内部实现则用了StringJoiner,其源码如下:

    public static String join(CharSequence delimiter, CharSequence... elements) {
        Objects.requireNonNull(delimiter);
        Objects.requireNonNull(elements);
        // Number of elements not likely worth Arrays.stream overhead.
        StringJoiner joiner = new StringJoiner(delimiter);
        for (CharSequence cs: elements) {
            joiner.add(cs);
        }
        return joiner.toString();
    }

反编译指令

基础命令

javap反编译指令可查看编译后的.class文件的字节码信息,这里是做简单的使用记录,不做过多讨论:

javap -c Concatenation

若想查看更详细的常量池等信息,可添加-verbose选项,即:

javap -c -verbose Concatenation

-c 输出类中各方法的未解析的代码,即构成 Java 字节码的指令。

-verbose 输出堆栈大小、各方法的 locals 及 args 数,以及class文件的编译版本。

如当想反编译上面的StringDemo1.class文件,执行如下命令即可:

javap -c  StringDemo1.class

指令简说

dup 复制栈顶数值(数值不能是long或double类型的)并将复制值压入栈顶

ldc 将int, float或String型常量值从常量池中推送至栈顶。

invokespecial 调用实例构造器<init>方法, 私有方法和父类方法

官方对dup的解释(6.5.dup)如下:

Duplicate the top value on the operand stack and push the duplicated value onto the operand stack.

The dup instruction must not be used unless value is a value of a category 1 computational type (§2.11.1).

官方对ldc推送String的描述如下,由此也可看出字符串常量池中的存储的String属于引用,当ldc推送时,其实推送的也是引用:

The index is an unsigned byte that must be a valid index into the run-time constant pool of the current class (§2.6). The run-time constant pool entry at index either must be a run-time constant of type int or float, or a reference to a string literal, or a symbolic reference to a class, method type, or method handle (§5.1).

.....

if the run-time constant pool entry is a reference to an instance of class String representing a string literal (§5.1), then a reference to that instance, value, is pushed onto the operand stack.(6.5.ldc)

.....

参考资料

  1. 请别再拿“String s = new String("xyz");创建了多少个String实例”来面试了吧
  2. The SCJP Tip Line Strings, Literally
  3. JEP 122:删除永久世代
  4. JDK 8 Milestones
  5. JVM指令详解(上)
  6. jvm 几个invoke 指令
  7. JDK 8 Features
  8. JDK 7 Features
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容