Chapter13 -- 字符串

基本内容

  • 不可变的String
  • String中的+操作符
  • StringBuilder和StringBuffer
  • String在内存中的位置

1 不可变的String

String的对象是不可变的,如下:

String str = "abc";
str = "def";

在上述代码中,实际上分别创建了abc和def两个字符串,str只是由指向abc的引用变为指向def的引用。

我们来看下Java中String类的源代码

/** The value is used for character storage. */
private final char value[];

我们可以看到,其实String类在Java内部是以final修饰的字符数组形式存储的。这也就表明了,String只会初始化一次,并且不可被继承。

我们通过源代码再来看下对String进行分割,合并等操作后String是否会发生变化。

  //分割字符串
    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }

  //合并字符串
    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

可以看出,其实在这些操作后都生成了新的字符串。

2 String中的+操作符

Java中是不允许程序员对操作符重载的,但是Java自身对+操作符进行了重载。
在String的操作中,+操作符表示字符串连接。

String str = "java";
String str2 = "hello " + str;
System.out.println(str2);
console:
hello java

我们反编译上述代码

  0: ldc           #16  // String java
  2: astore_1
  3: new           #18  // class java/lang/StringBuilder
  6: dup
  7: ldc           #20  // String hello
  9: invokespecial #22  // Method java/lang/StringBuilder."":(Ljava/lang/String;)V
  12: aload_1
  13: invokevirtual #25 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  16: invokevirtual #29 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

我只截取了反编译后的部分内容。
可以看出在Java的内部实现中,+操作是以StringBuilder的形式实现的。

注意:

  • ldc,将int, float或String型常量值从常量池中推送至栈顶,在这里是查找java字符串推送至栈顶。
  • astore_1,将栈顶引用型数值存入第二个本地变量,在这里就是将java存储到本地变量。
  • new,创建一个对象,并将其引用值压入栈顶。
  • dup,复制栈顶数值并将复制值压入栈顶。
  • invokespecial,调用方法,注意观察注释中调用的方法名。
  • aload_1,将第二个引用类型本地变量推送至栈顶

其实这个过程就是:

  • 在常量池中查找java字符串,并推送至栈顶。
  • 将java存储到第二个本地变量中。
  • 创建StringBuilder类型对象,并推送至栈顶。
  • 在常量池中查找hello字符串,并推送至栈顶。
  • 调用方法初始化StringBuilder
  • 调用StringBuilder的append()方法,合并字符串。
  • 调用StringBuilder的toString()方法,将StringBuilder转换为String。

在String使用+连接字符串的时候,创建了很多String对象,这无疑会影响到效率。

3 StringBuilder和StringBuffer

使用StringBuilder/StringBuffer的意义无非就是为了提高操作字符串的效率。
我们先看下源码中关于这两个类的声明。

public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence{}
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{}

都是继承自抽象类AbstractStringBuilder,都熟悉点了Serializable和CharSequence接口。
其实不仅仅是类的声明,StringBuilder/StringBuffer其实是在功能上是完全相同的,只是StringBuffer中的方法大多被synchronized修饰,因此是线程安全的,而其实不仅仅是类的声明,StringBuilder不是线程安全的。

我们使用《Thinking in Java》中的例子来探究下StringBuiler是否真的能提升效率。
我们先提供两个方法

public String stringBuilderAppend(String[] strArray) {
  StringBuilder stringBuilder = new StringBuilder();
  for(int i = 0; i < strArray.length; i++) {
    stringBuilder.append(strArray[i]);
  }
  return stringBuilder.toString();
}

public String stringAppend(String[] strArray) {
  String str = "";
  for(int i = 0; i < strArray.length; i++) {
    str += strArray[i];
  }
  return str;
}

我们反编译后上述代码

public java.lang.String stringBuilderAppend(java.lang.String[]);
    Code:
      10: goto          24
      13: aload_2
      14: aload_1
      15: iload_3
      16: aaload
      17: invokevirtual #19                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      20: pop
      21: iinc          3, 1
      24: iload_3
      25: aload_1
      26: arraylength
      27: if_icmplt     13

  public java.lang.String stringAppend(java.lang.String[]);
    Code:
       5: goto          32
       8: new           #16                 // class java/lang/StringBuilder
      11: dup
      12: aload_2
      13: invokestatic  #37                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
      16: invokespecial #43                 // Method java/lang/StringBuilder."":(Ljava/lang/String;)V
      19: aload_1
      20: iload_3
      21: aaload
      22: invokevirtual #19                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      25: invokevirtual #23                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      28: astore_2
      29: iinc          3, 1
      32: iload_3
      33: aload_1
      34: arraylength
      35: if_icmplt     8
}

我们只保留两个方法中的循环部分。
注意:

  • goto,无条件跳转到指定位置。
  • if_icmplt,比较栈顶两int型数值大小,当结果小于0时跳转到指定位置。

我们明白了这两项以后就可以看出,String使用+连接字符串的时候,每次都会创建StringBuilder对象,而这肯定是会影响执行效率的。

最后,我想说一些关于StringBuilder和StringBuffer的实现原理的内容。
我们来看它们的父类AbstractStringBuilder的源代码(只截取一部分)。

    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;


    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
    AbstractStringBuilder() {
    }

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

    /**
     * Returns the length (character count).
     *
     * @return  the length of the sequence of characters currently
     *          represented by this object
     */
    @Override
    public int length() {
        return count;
    }

我们可以看到,AbstractStringBuilder内部也是以字符数组实现的。注意count是指实际长度,而capacity指的是容量,注意它们的区别。

/**
     * Ensures that the capacity is at least equal to the specified minimum.
     * If the current capacity is less than the argument, then a new internal
     * array is allocated with greater capacity. The new capacity is the
     * larger of:
     * 
    *
  • The {@code minimumCapacity} argument. *
  • Twice the old capacity, plus {@code 2}. *
* If the {@code minimumCapacity} argument is nonpositive, this * method takes no action and simply returns. * Note that subsequent operations on this object can reduce the * actual capacity below that requested here. * * @param minimumCapacity the minimum desired capacity. */
public void ensureCapacity(int minimumCapacity) { if (minimumCapacity > 0) ensureCapacityInternal(minimumCapacity); } /** * For positive values of {@code minimumCapacity}, this method * behaves like {@code ensureCapacity}, however it is never * synchronized. * If {@code minimumCapacity} is non positive due to numeric * overflow, this method throws {@code OutOfMemoryError}. */ private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } }

以上是扩容的两个方法,原理很简单,大家可以自己思考下。还有个trimToSize()方法,是缩减容量的。

至于StringBuilder和StringBuffer所实现的方法,在这里不多说,大家可以去看API进行学习。

4 String在内存中的位置

之前我在【CoreJava】equals()和==的比较中提到过String的存放位置,但只是稍微讲了下关于两种创建方式存储位置的不同。
我们使用两种种方式创建字符串

String str1 = "abc";
String str2 = new String("abc");

我们反编译上面的代码

0: ldc           #16                 // String abc
2: astore_1
3: new           #18                 // class java/lang/String
6: dup
7: ldc           #16                 // String abc
9: invokespecial #20                 // Method java/lang/String."":(Ljava/lang/String;)V
12: astore_2
13: return

可以看出两种方式都是首先从字符创常量池中寻找abc,但是它们的区别在哪呢?

  • 两种创建方式都是首先在字符串常量池中寻找abc,找不到则创建abc。
  • str1是存储在栈中的地址引用,指向字符串常量池中的abc。
  • 创建str2的时候,首先在堆中创建abc,指向字符串常量池中的abc,再在栈中创建str2,指向堆中的abc。

这就是我们为什么推荐使用

String str = "abc";

这种形式创建字符串了。


字符串的内容也基本结束了,《Thinking in Java》中还补充了正则表达式和格式化输出的一些内容。正则表达式可以作为一整篇文章来讲,而且我自己目前也不太熟练,所以就不在这多说了。格式化输出呢,我用的比较少,平时用的也不多,所以也没有怎么研究过。毕竟脑子有限,还是要有一些侧重的。


欢迎关注个人公众号,搜索:公子照谏或者QCzhaojian
也可以通过扫描二维码关注。
Chapter13 -- 字符串_第1张图片

你可能感兴趣的:(《Thinking,in,Java》读书笔记,string,stringbuilder,stringbuffer)