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

一.引言


很多人觉得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.csdn.net/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》和几篇网上的资料,已在文章给出链接。由于水平有限,文中出现错误与不妥之处在所难免,恳请读者批评指正。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java String 是不可变的字符串字符串拼接可以使用加号运算符(+)或 StringBuilder 进行。 使用加号运算符的方法: ``` String str1 = "Hello"; String str2 = "World"; String result = str1 + str2; // HelloWorld ``` 使用 StringBuilder 的方法: ``` StringBuilder sb = new StringBuilder(); sb.append("Hello"); sb.append("World"); String result = sb.toString(); // HelloWorld ``` 使用加号运算符的效率较低,当需要频繁进行字符串拼接时建议使用 StringBuilder。 ### 回答2: Java 字符串是一种重要的数据型,常用于表示文本信息。String Java 字符串操作的主要之一,其提供了丰富的字符串处理方法,如字符串截取、比较等。 字符串拼接是指将多个字符串连接在一起形成一个新的字符串。在 Java ,可以使用加号(+)运算符来实现字符串拼接。当一个字符串和一个非字符串型的值相加时,Java 会将非字符串型的值自动转换为字符串型。例如: ```java String str1 = "Hello"; String str2 = "World"; System.out.println(str1 + str2); // 输出:HelloWorld System.out.println(str1 + 123); // 输出:Hello123 ``` 除了加号运算符,Java 还提供了其他字符串拼接的方式,在 JDK 5 引入了 StringBuilderStringBuffer ,这两个有着相似的功能,都是用来处理字符串的可变对象。 StringBuilderStringBuffer 的区别在于 StringBuffer 是线程安全的,而 StringBuilder 不是,所以在多线程操作字符串时,应该使用 StringBuffer 来避免线程安全问题。StringBuilderStringBuffer 内部都是使用字符数组来实现字符串的拼接,相比加号运算符,使用这些的拼接方式更加高效。 在 JDK 9 Java 又引入了一种新的字符串拼接方式,即使用 StringJoiner ,该适用于将多个字符串、文本或对象拼接为一个字符串。相比 StringBuilderStringBuffer,StringJoiner 更加易读且易于维护。 总的来说,Java 字符串拼接主要有加号运算符、StringBuilderStringBuffer 和 StringJoiner 四种方式,具体使用哪种方式取决于实际需求和场景。在实际开发,应该选择最适合自己需求的方式来进行字符串拼接,以达到更高效、便捷和易维护的目的。 ### 回答3: 在Java字符串是一个非常基础的数据型,它是由String提供支持的。而JavaString具有很多强大的属性和方法,其之一就是字符串拼接,即将两个或多个字符串连接起来形成一个新的字符串。 在Java字符串拼接可以通过"+"运算符实现。例如: String str1 = "Hello"; String str2 = "World"; String str3 = str1 + str2; // 将str1和str2连接成一个新的字符串 当使用"+"运算符时,Java会自动将字符串连接起来,这种方式也被称为字符串的并置运算。 另外,在进行字符串拼接时,可以使用String的concat()方法,该方法将指定的字符串附加到该字符串的末尾。例如: String str1 = "Hello"; String str2 = "World"; String str3 = str1.concat(str2); // 将str2附加到str1的末尾形成新的字符串 以及,对于大量字符串拼接的情况,可以使用StringBuilderStringBufferStringBuilderStringBuffer可以动态地添加字符串内容,避免了大量的字符串拼接带来的性能问题,其使用方法似。 总之,在Java进行字符串拼接是非常基础和常用的操作,掌握它的原理和使用方法对于Java程序员来说非常重要,可以帮助提高代码的效率和质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值