String、StringBuffer、StringBuilder学习笔记

StringBuffer和StringBuilder


String类是不可变的类型,也就是尝试对字符串进行修改的时候,不会对原字符串进行修改,而是在原字符串的基础上进行修改,然后生成一个新的字符串存放在常量池中。

对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象

  1. String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。在早期的JVM实现版本中,被final修饰的方法会被转为内嵌调用以提升执行效率。而从Java SE5/6开始,就渐渐摈弃这种方式了。因此在现在的Java SE版本中,不需要考虑用final去提升方法调用效率。只有在确定不想让该方法被覆盖时,才将方法设置为final。
  2. 上面列举出了String类中所有的成员属性,从上面可以看出String类其实是通过char数组来保存字符串的。

String str="hello world"和String str=new String(“hello world”)的区别:

public class Main {
         
    public static void main(String[] args) {
        String str1 = "hello world";
        String str2 = new String("hello world");
        String str3 = "hello world";
        String str4 = new String("hello world");
         
        System.out.println(str1==str2);
        System.out.println(str1==str3);
        System.out.println(str2==str4);
    }
}

执行结果如下所示:

false
true
false

接下来从JVM角度简单解释一下原因:

在class文件中有一部分 来存储编译期间生成的 字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。

String str1 = “hello world”;和String str3 = “hello world”; 都在编译期间生成了 字面常量和符号引用,运行期间字面常量"hello world"被存储在运行时常量池(当然只保存了一份)。通过这种方式来将String对象跟引用绑定的话,JVM执行引擎会先在运行时常量池查找是否存在相同的字面常量,如果存在,则直接将引用指向已经存在的字面常量;否则在运行时常量池开辟一个空间来存储该字面常量,并将引用指向该字面常量。

众所周知,通过new关键字来生成对象是在堆区进行的,而在堆区进行对象生成的过程是不会去检测该对象是否已经存在的。因此通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。

​ 那么为了节省内存,提高效率,如果可以在原来字符串的基础上直接进行修改操作,这样会更加方便,StringBuffer和StringBuilder就是可以修改的字符序列。

定义与区别

​ 首先对来看String,StringBuffer,StringBuilder的继承关系图,如下所示。
在这里插入图片描述

在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,所以如果需要对字符串进行修改推荐使用 StringBuffer。

StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的不能同步访问)。

由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。

在初始化的时候,默认是16个字节的空间,但是可以变长,具体参考源码,这里主要对于其构造函数进行简单学习。

/**
     * Constructs a string buffer with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuffer() {
        super(16);
    }

    /**
     * Constructs a string buffer with no characters in it and
     * the specified initial capacity.
     *
     * @param      capacity  the initial capacity.
     * @exception  NegativeArraySizeException  if the {@code capacity}
     *               argument is less than {@code 0}.
     */
    public StringBuffer(int capacity) {
        super(capacity);
    }

“+=”优化

String中“+=”在编译时会被优化,首先查看以下代码

package varString;

public class demo02 {

    public static void main(String[] args) {

        // 开始时间
        long start = System.currentTimeMillis();
        String string = "";
        // StringBuffer string = new StringBuffer();
        // StringBuilder string = new StringBuilder();
        for (int i = 0; i < 99999; i++) {
            string += i;
            // string.append(i);
        }
        System.out.println(string);

        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end - start));


    }
}

将编译生成的class文件进行反编译,得到以下代码:

// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://kpdus.tripod.com/jad.html
// Decompiler options: packimports(3) fieldsfirst ansi space 
// Source File Name:   demo02.java

package varString;

import java.io.PrintStream;

public class demo02
{

	public demo02()
	{
	}

	public static void main(String args[])
	{
		long start = System.currentTimeMillis();
		String string = "";
		for (int i = 0; i < 0x1869f; i++)
			string = (new StringBuilder()).append(string).append(i).toString();

		System.out.println(string);
		long end = System.currentTimeMillis();
		System.out.println((new StringBuilder()).append("用时:").append(end - start).toString());
	}
}

从反编译得到的代码可知,“+=”操作被优化为了StringBuilder中的append,

string += i;
string = (new StringBuilder()).append(string).append(i).toString();

上述过程中产生了10000个对象,但是如果采用下列代码:

package varString;

public class demo02 {

    public static void main(String[] args) {

        // 开始时间
        long start = System.currentTimeMillis();
        // String string = "";
        // StringBuffer string = new StringBuffer();
        StringBuilder string = new StringBuilder();
        for (int i = 0; i < 99999; i++) {
            // string += i;
            string.append(i);
        }
        System.out.println(string);

        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end - start));


    }
}

上述代码中只产生了一个StringBuilder对象,相比于前面的String方法,会有很大的优化空间。

常用函数

以下是 StringBuffer 类支持的主要方法:

方法功能
public StringBuffer append(String s)将指定的字符串追加到此字符序列。
public StringBuffer reverse()将此字符序列用其反转形式取代
public delete(int start, int end)移除此序列的子字符串中的字符。
public insert(int offset, int i)int 参数的字符串表示形式插入此序列中。
insert(int offset, String str)str 参数的字符串插入此序列中。
replace(int start, int end, String str)使用给定 String 中的字符替换此序列的子字符串中的字符。

以下列表列出了 StringBuffer 类的其他常用方法:

方法功能
int capacity()返回当前容量。
char charAt(int index)返回此序列中指定索引处的 char 值。
void ensureCapacity(int minimumCapacity)确保容量至少等于指定的最小值。
void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)将字符从此序列复制到目标字符数组 dst
int indexOf(String str)返回第一次出现的指定子字符串在该字符串中的索引。
int indexOf(String str, int fromIndex)从指定的索引处开始,返回第一次出现的指定子字符串在该字符串中的索引。
int lastIndexOf(String str)返回最右边出现的指定子字符串在此字符串中的索引。
int lastIndexOf(String str, int fromIndex)返回 String 对象中子字符串最后出现的位置。
int length()返回长度(字符数)。
void setCharAt(int index, char ch)将给定索引处的字符设置为 ch
void setLength(int newLength)设置字符序列的长度。
CharSequence subSequence(int start, int end)返回一个新的字符序列,该字符序列是此序列的子序列。
String substring(int start)返回一个新的 String,它包含此字符序列当前所包含的字符子序列。
String substring(int start, int end)返回一个新的 String,它包含此序列当前所包含的字符子序列。
String toString()返回此序列中数据的字符串表示形式。

StringBuffer类和StringBuilder类所支持的方法基本一致

接下来其中一些简单的方法进行演示。

public class RunoobTest{
    public static void main(String args[]){
        StringBuilder sb = new StringBuilder(10);
        sb.append("Runoob..");
        System.out.println(sb);  
        sb.append("!");
        System.out.println(sb); 
        sb.insert(8, "Java");
        System.out.println(sb); 
        sb.delete(5,8);
        System.out.println(sb);  
    }
}

在这里插入图片描述

效率测试

package varString;

public class demo02 {

    public static void main(String[] args) {

        // 开始时间
        long start = System.currentTimeMillis();
        // String string = "";
        // StringBuffer string = new StringBuffer();
        StringBuilder string = new StringBuilder();
        for (int i = 0; i < 99999; i++) {
            // string += i;
            string.append(i);
        }
        System.out.println(string);

        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end - start));


    }
}

通过上述代码对String +=,StringBuffer,StringBuilder的速度进行简单测试,+=会使用47s的时间,StringBuffer会使用25ms的时间,StringBuilder会使用18ms的时间。

线程安全

在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的

如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。

(一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞)

总结一下

  1. String:适用于少量的字符串操作的情况
  2. StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
  3. StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

面试题

下面这段代码的输出结果是什么?

String a = “hello2”;   String b = “hello” + 2;   System.out.println((a == b));

输出结果为:true。原因很简单,“hello”+2在编译期间就已经被优化成"hello2",因此在运行期间,变量a和变量b指向的是同一个对象。

反编译代码:

	public static void main(String args[])
	{
		String a = "hello2";
		String b = "hello2";
		System.out.println(a == b);
	}

下面这段代码的输出结果是什么?

String a = “hello2”;   String b = “hello”; String c = b + 2; System.out.println((a == c));

输出结果为:false。由于有符号引用的存在,所以 String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,因此这种方式生成的对象事实上是保存在堆上的。因此a和c指向的并不是同一个对象。

反编译得到的代码:

	public static void main(String args[])
	{
		String a = "hello2";
		String b = "hello";
		String c = (new StringBuilder()).append(b).append(2).toString();
		System.out.println(a == c);
	}

下面这段代码的输出结果是什么?

String a = “hello2”;   final String b = “hello”; String c = b + 2; System.out.println((a == c));

输出结果为:true。对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = “hello” + 2;

反编译得到的代码:

	public static void main(String args[])
	{
		String a = "hello2";
		String b = "hello";
		String c = "hello2";
		System.out.println(a == c);
	}

下面这段代码输出结果为false,这里面虽然将b用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此a和c指向的不是同一个对象。

public class Main {
    public static void main(String[] args) {
        String a = "hello2";
        final String b = getHello();
        String c = b + 2;
        System.out.println((a == c));
    }
     
    public static String getHello() {
        return "hello";
    }
}

String str = new String(“abc”)创建了多少个对象?

反编译代码如下所示:

	public static void main(String args[])
	{
		String str = new String("abc");
	}

​ 很显然,new只调用了一次,也就是说只创建了一个对象。

而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念 该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。

因此,这个问题如果换成 String str = new String(“abc”)涉及到几个String对象?合理的解释是2个。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值