Java基础——字符串之String类和StringBuilder类

本文探讨了Java中String类的只读特性及其对性能的影响,通过实例分析证明String对象不可变性,并介绍编译器如何优化字符串拼接。同时,讲解了StringBuilder类在字符串拼接上的优势,以及其内部工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.引言


很多人觉得C/C++难,Java则相对简单,其中有一个原因就是,C/C++处理字符串那真的是会让很多人头疼,比如在C/C++中对字符串的初始化定义为:

char str[10] = "java";
char *str = "java";
char str[10]={'j','a','v','a','\0'};

一看到数组、指针,就让很多人犯愁了。而又例如字符串的拼接,在C/C++中是通过strcat(str1,str2)实现的,但是使用这个方法,必须得清楚知道str1拥有足够的空间容纳str2,否则会造成不能完整将str2拼接到str1上。总之,挺麻烦的,不是?而Java则对字符串相关的处理方法进行了很高级的封装,Java使用者也能很轻松地对字符串进行一系列操作,相比于C/C++,简直是如鱼得水。
当然,本人在此并不是比较C/C++和Java谁好谁不好。本篇文章主要讲讲Java中涉及到字符串的String类StringBuilder类

二.String类


1.String类的定义

如下代码所示:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //其他成员变量和方法
    }

第一,可以注意到final修饰符,说明String类不能被继承。
第二,成员变量char value[]用于存储字符串中的每一个字符。

2.String对象的只读特性

String对象是不可变的,具有只读特性。

这句话看似无关痛痒,其实在实际的工程项目中,这一特性对性能必然有很大的影响,只是在大多数的开发过程中,我们并不在意。
那如何说明String对象的只读特性呢?又如何说明这一特性对性能的影响呢?我们One by one的回答。

2.1证明只读特性

在《Thinking in Java》第13章《字符串》中,作者举例说明:

package String;
public class Immutable {
    public static String upcase(String s) {
        return s.toUpperCase();
    }
    public static void main(String[] args) {
        String q = "howdy";
        System.out.println(q);
        String qq = upcase(q);
        System.out.println(qq);
        System.out.println(q);
    }
}
输出:
howdy
HOWDY
howdy

作者的解释:当把q传给upcase()方法时,实际传递的是引用的一个拷贝。其实,每当把String对象作为方法的参数时,都会复制一份引用,而该引用所指的对象其实一直待在单一的物理位置上,从未动过。回到upcase()的定义,传入其中的引用有了名字s,只有upcase()运行的时候,局部引用s才存在。一旦upcase()运行结束,s就消失了。当然了,upcase()的返回值,其实只是最终结果的引用。这足以说明,upcase()返回的引用已经指向了一个新的引用,而原本的q则还在原地。

个人认为这个例子并不能完整地说明String对象的只读特性。我的例子如下:

public static void main(String[] args) {
    String s = "abc";
    String t = "JAVA";
    System.out.println(s);
    System.out.println(t);
    String ss = s.toUpperCase();
    String tt = t.toUpperCase();
    System.out.println(ss);
    System.out.println(s);
    System.out.println(tt);
    System.out.println(t);
    System.out.println(ss == s);
    System.out.println(tt == t);
}
输出:
abc
JAVA
ABC
abc
JAVA
JAVA
false
true

大家都知道在Java中“==”比较的是两个对象的内存地址,从上面的例子可以看出来,如果原String对象的值未被修改,则返回的就是原来的对象,如果原对象被修改了,就会返回一个新的String对象。可参考String类中所有修改String值的方法,比如toUpperCase()方法中大致结构就是:

if(不需要修改) return this; //返回本身
else return new String(修改后的值);//返回一个新的String对象

2.2只读特性的影响

如各种资料可见,最好的例子是字符串拼接
最常用的方式就是重载“+”StringBuilder.append()方法。可能一般情况下,大多数Java开发人员都喜欢用“+”,
因为最简单,最方便,比如:

public static void main(String[] args) {
    String a = "喜欢";
    String b = "我" + a + "Java" + 31;
}

由于String对象的不可变性,那么上面的代码会执行多次“+”:“我”和a相连,产生一个新的String对象,然后再和”Java”相连,再产生一个新的String对象,以此类推。实际开发过程中,一定会遇到拼接多个String对象的时候,那如此可见,这样一行代码,就会产生很多的String类型的中间变量。那对性能的影响体现在何处?这里就要提到Java对象在内存中的真正大小=对象头+实例数据+对齐填充(可参考http://www.cnblogs.com/zhanjindong/p/3757767.html)。我们在多线程开发中经常使用synchronized给对象加锁,那一个对象的锁状态在哪里?就在对象头里。32位HotSpot虚拟机中,对象头的结构如下:
这里写图片描述
图片来源:http://blog.youkuaiyun.com/zhoufanyang_china/article/details/54601311

也意味着,每创建一个String中间变量,都会占用一定内存,比我们想象的还要多。如果可以避免产生这么多的中间变量,岂不是更好?

2.3编译器的优化

首先我们编译一下上面的代码,然后再反编码看一下编译器都干嘛了:
这里写图片描述
从这段反编译后的字节码中可以看出,编译器自动引入了StringBuilder类,因为StringBuilder更高效(为何更高效,请看第三部分)。编译器创建了一个StringBuilder对象用于构造最终的String,然后调用了四次append()方法。也意味着上面的代码等价于:

String b = new StringBuilder().append("我").append(a).append("Java").append(31).toString();

如此看来,编译器会自动优化性能,那我们便可以随意使用重载“+”用于字符串拼接吗?非也,例如:

//String“+”:循环拼接字符串
public static String connectStr(String[] str) {
    String result = "";
    for (String s : str) {
        result += s;
    }
    return result;
}
//StringBuilder:循环拼接字符串
public static String connectStrBuilder(String[] str) {
    StringBuilder stringBuilder = new StringBuilder();
    for (String s : str) {
        stringBuilder.append(s);
    }
    return stringBuilder.toString();
}

同样通过反编译看看编译器都干嘛了:
首先是重载“+”:
这里写图片描述
StringBuilder.append():
这里写图片描述
由此可见,编译器的确对重载“+”方法进行了优化,但是在循环中使用重载“+”,每一次循环都会都产生一个StringBuilder类型的中间变量。OMG,我们不是一直在尽力避免产生不必要的中间变量吗?而使用StringBuilder的append()方法,则简单多了,因为只会在循环之前产生一个StringBuilder对象用于构造最终的String,在循环中,只需要调用append()方法即可。所以一般在处理字符串拼接时,为了性能达到最优,推荐使用StringBuilder的append()方法,然后再通过toString()方法将结果转为String类型,同时StringBuilder也提供了其他一些方法。

三.StringBuilder类


前面已经提到在对字符串的某些处理上,StringBuilder类相比于String类更加高效,比较常见的就是通过append()方法进行字符串拼接,那么为何StringBuilder会更加高效呢?我们来分析一下StringBuilder的定义和append()方法。

//StringBuilder.java
public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
public StringBuilder() {
    super(16);//调用父类的构造函数,并且参数为16,这个参数的意义是初始容量
    }
public StringBuilder(int capacity) {//指定初始容量
    super(capacity);
//其他构造函数等等
    }
@Override
public StringBuilder append(String str) {
    super.append(str);//调用父类AbstractStringBuilder的append()方法
    return this;//返回的是本身
    }
}

再看看AbstractStringBuilder的定义和append()方法

//AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    int count;
    AbstractStringBuilder() {
    }
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];//初始化value数组
    }
    //append()方法
public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);//扩容
    str.getChars(0, len, value, count);//
    count += len;
    return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        //如果需要的最小容量(minimumCapacity = 原来的字符串长度count+需要拼接的字符串长度)已经超过了总的容量(数组value的长度),则进行扩容
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;//原来的容量进行翻倍!
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;//如果翻倍之后的容量依然小于需要的最小容量,则将数组的容量大小设置成需要的最小容量。
            }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

所以,StringBuilder的拼接过程如下,用默认的初始容量16举例:
这里写图片描述

总结起来就是:
(1)StringBuilder初始化时,既可以指定初始化容量(如果你已经大概知道最终的字符串大小,那这样就可以省去扩容过程),也可以按照默认的初始化容量进行初始化。
(2)无论拼接多少次、是否会循环,只会生成一个StringBuilder对象(当然如果你把StringBuilder s = new StringBuilder()这样的语句写在了循环内,那就另说,而且这样写,也不符合一般情况下的逻辑)。

文章内容参考了《Thinking in java》和几篇网上的资料,已在文章给出链接。由于水平有限,文中出现错误与不妥之处在所难免,恳请读者批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值