String StringBuffer StringBuilder之间的区别

博客主要探讨了String、StringBuffer、StringBuilder的区别。执行速度上,StringBuilder最快,String最慢;安全性上,String和StringBuilder线程非安全,StringBuffer线程安全。还分析了String不可继承的原因、字符串拼接问题,以及重写equals为何要重写hashCode方法。

在学习String、StringBuffer、StringBuilder三者时,首先给出必要的结论,后面详细分析。
在执行速度上: StringBuilder > StringBuffer > String .这是在一般情况下的结果
在安全性上: String、StringBuilder 线程非安全 || StringBuffer 线程安全
问题1:为什么String是不可继承的?
在String源码对于String类的定义:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];
 
    /** The offset is the first index of the storage that is used. */
    private final int offset;
 
    /** The count is the number of characters in the String. */
    private final int count;
 
    /** Cache the hash code for the string */
    private int hash; // Default to 0
 
    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
 
    ...... 
}

从源码中可以看出,String类是被final修饰,因此该类不会被继承(其值也不会被改变),且值在编译时期就会被认定为常量来处理。因此,String变量在使用时,只是将引用指向了该变量。从上面的代码可以看出,成员变量基本都是采用final修饰。所以,我们经常会被这样的一个代码所迷惑:

package StringTest;

public class StringTest2 {

	public static void main(String[] args) {
		String s1 = "a";
		String s2 = "b";
		String s3 = s1 + s2;
		String s4 = "ab";
		String s5 = new String("a");
		String s6 = s5 + "b";
		System.out.println(s3.equals(s4)); //true
		System.out.println(s3 == s4); //false
		System.out.println(s1 == s5); //false
		System.out.println(s3 == s6); //false

	}
}

从这个例子我们可以看出来:equals比较的是最终的两个对象的值,s3 和 s4的值是一样 ,但是其在内存中是不同的两个对象,其实String s3 = s1 + s2;在底层中的实现过程:s1+s2在编译时期是不能被直接解析的,而是底层通过StringBuilder的append()在堆中创建一个值为"ab"的对象,并将s3指向该对象,而s4的引用是在常量池中,所以当作 == 比较时,这两个对象是不同的,但是两个对象的值是相同的,即equals()对象所返回的为true;
其实这样好理解一点:当两个String对象进行拼接时,如果在编译时期不能够直接确定该值,则比较两个对象的内存地址时,一般都是不同的,但是两者equals()的值是相等的。
因此,在进行String的各种拼接的比较在这里应该算是比较清晰了,主要还是讲由于String类的不可改变导致的String类的不可变性,从而引出字符串的拼接问题。
final关键字的讲解可以参考:
https://www.cnblogs.com/liun1994/p/6691094.html
问题2:String、StringBuilder、StringBuffer区别?
在回顾三者的区别,我想通过一端代码来说明:

package StringTest;

public class Test {

	public static void main(String[] args) {
		StringTest();
		StringBufferTest();
		StringBuilderTest();
	}

	public static void StringTest() {
		String s = "";
		Long startTime = System.currentTimeMillis();
		for (int i = 0; i < 100000; i++) {
			s = s + "add";
		}
		Long endTime = System.currentTimeMillis();
		System.out.println("StringTest 总共用时:" + (endTime - startTime));
	}

	public static void StringBufferTest() {
		
		StringBuffer sb = new StringBuffer();
		Long startTime = System.currentTimeMillis();
		for(int i = 0;i<100000;i++) {
			sb.append("add");
		}
		Long endTime = System.currentTimeMillis();
		System.out.println("StringBufferTest 总共用时:" + (endTime - startTime));

	}

	public static void StringBuilderTest() {
		StringBuilder sb = new StringBuilder();
		Long startTime = System.currentTimeMillis();
		for(int i = 0;i<100000;i++) {
			sb.append("add");
		}
		Long endTime = System.currentTimeMillis();
		System.out.println("StringBuilderTest 总共用时:" + (endTime - startTime));
	}
}
	运行结果:
			StringTest 总共用时:10163
			StringBufferTest 总共用时:3
			StringBuilderTest 总共用时:2

我感觉从这个简单的测试,可以看出效率问题,String的效率很低,但看不出这个StringBuffer和StringBuilder之间的效率问题。但是将循环的次数增多的时候,即可以看出效率还是很明显的,StringBuilder比StringBuffer高很多。其次,StringBuffer是使用了同步锁,在单线程情况下是看不出问题,当加入多线程时,是可以看出问题的 。

package StringTest;

import java.util.concurrent.CountDownLatch;

/*
 * 测试StringBuffer 与StringBuilder之间的安全性问题
 * 程序主要是测试字符串被反转的次数,如果是奇数次则是“BBBBAAAA”,如果是偶数次线程安全的话是不变的,通过奇数次与偶数次的判断 可以看出哪一个是线程安全的,哪一个是线程非安全的。
 * */
class StringBufferTaskThread extends Thread {
	private Object obj = null;
	private CountDownLatch countDownLatch;// 记载运行线程数

	public StringBufferTaskThread(StringBuffer sbuffer, CountDownLatch countDownLatch) {
		this.obj = sbuffer;
		this.countDownLatch = countDownLatch;
	}

	public StringBufferTaskThread(StringBuilder sbuilder, CountDownLatch countDownLatch) {
		this.obj = sbuilder;
		this.countDownLatch = countDownLatch;
	}

	@Override
	public void run() {
		for (int i = 0; i < 3; i++) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "开始了");
			if (obj instanceof StringBuffer) {
				((StringBuffer) obj).reverse();
			} else if (obj instanceof StringBuilder) {
				((StringBuilder) obj).reverse();
			}
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName() + "退出了");
		countDownLatch.countDown();
	}
}

public class Test2 {
	private static final int COUNT_NUMBER = 10000;

	public static void main(String[] args) {

		String str = "AAAABBBB";
		StringBuffer stringBuffer = new StringBuffer(str);
		StringBuilder stringBuilder = new StringBuilder(str);

		// 允许一个或多个线程一直等待,直到其他的线程操作执行完在执行
		CountDownLatch countDownLatch1 = new CountDownLatch(COUNT_NUMBER);
		CountDownLatch countDownLatch2 = new CountDownLatch(COUNT_NUMBER);

		for (int i = 0; i < COUNT_NUMBER; i++) {
			new StringBufferTaskThread(stringBuffer, countDownLatch1).start();
			new StringBufferTaskThread(stringBuilder, countDownLatch2).start();
		}

		try {
			countDownLatch1.await();
			countDownLatch2.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("StringBuffer toString: " + stringBuffer.toString());
		System.out.println("StringBuilder toString: " + stringBuilder.toString());

	}
}
运行结果:
....
....
Thread-19841退出了
Thread-19849退出了
Thread-19854退出了
Thread-19837退出了
StringBuffer toString: AAAABBBB
StringBuilder toString: BBABABAA

StringBuffer一直都是很正常的,但是StringBuilder是会产生错误,所以从线程安全的角度来看,String Buffer是线程安全的,StringBuilder是线程非安全。
问题3:重写equals为什么要重写hashCode方法?
在讲解这个问题时,通过一个简单的例子就可以很清楚的明白,为什么重写equals方法之后,需要重写hashCode方法。

package Test4;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

class Student{
	private String name;
	private int age;
	
	public Student(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	@Override
	public int hashCode() {
		return this.getAge();
	}
	@Override
	public boolean equals(Object obj) {
		if(obj == this) {
			return true;
		}
		if(obj == null) {
			return false;
		}
		if(obj instanceof Student) {
			Student s = (Student) obj;
			if (this.name.equals(((Student) obj).getName())&& this.age==s.getAge()){
				return true;
			}
		}
		return false;
	}
	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + "]";
	}
	
	
}
public class HashCodeTest {

	public static void main(String[] args) {
		Set set = new HashSet<Student>();
		Student s1 = new Student("zhangsan",11);
		Student s2 = new Student("zhangsan",10);
		set.add(s1);
		set.add(s2);
		Iterator iterator = set.iterator();
		while(iterator.hasNext()) {
			System.out.println(iterator.next().toString());
		}
	}
}

当上面的s1、s2的age属性值不一样的时候,此时重写了equals方法,没有重写hashCode方法,按照常理来说,这两个对象是不同的对象,可以正常的添加到set集合中,程序运行之后的结果,也显示两个对象正常添加到集合中;
当将上面的程序修改:重写equals方法,但不重写hashCode(),并且将两个对象的属性值都改成一样,进行测试。

package Test4;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

class Student {
	private String name;
	private int age;

	public Student(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}

	/*
		此处省略get set方法...
	*/
	
	/*
	 * @Override 
	 * public int hashCode() 
	 * {
	 *  	return this.getAge(); 
	 *  }
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj == this) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (obj instanceof Student) {
			Student s = (Student) obj;
			if (this.name.equals(((Student) obj).getName())&& this.age==s.getAge()){
				return true;
			}
		}
		return false;
	}

	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + "]";
	}

}

public class HashCodeTest {

	public static void main(String[] args) {
		Set set = new HashSet<Student>();
		Student s1 = new Student("zhangsan", 11);
		Student s2 = new Student("zhangsan", 11);
		set.add(s1);
		set.add(s2);
		Iterator iterator = set.iterator();
		while (iterator.hasNext()) {
			System.out.println(iterator.next().toString());
		}
	}
}
运行结果:
	Student [name=zhangsan, age=11]
	Student [name=zhangsan, age=11]

两个完全一样的对象竟然同时被加入到了集合中,这不就存在错误了吗?两个完全一样的对象被加入到了set集合中,当重写了hashCode方法之后,结果表明,这两个对象是同一个对象,不会同时被加入到set集合中


	@Override
	public int hashCode() {
		return this.getAge();
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == this) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (obj instanceof Student) {
			Student s = (Student) obj;
			if (this.name.equals(((Student) obj).getName())&& this.age==s.getAge()){
				return true;
			}
		}
		return false;
	}

public class HashCodeTest {

	public static void main(String[] args) {
		Set set = new HashSet<Student>();
		Student s1 = new Student("zhangsan", 11);
		Student s2 = new Student("zhangsan", 11);
		set.add(s1);
		set.add(s2);
		Iterator iterator = set.iterator();
		while (iterator.hasNext()) {
			System.out.println(iterator.next().toString());
		}
	}
}
运行结果:
	Student [name=zhangsan, age=11]

此时是正常的,重写了equals方法,也同时重写了hashCode方法,可以正常判断两个对象是同一个对象。因此,我们必须要做到:当重写equals方法时,必须重写hashCode方法
但是总会遵循一个规律:

    1、equals相等,hashCode一定相等;
    2、equals不相等,hashCode一定不等;
    3、hashCode相等,equals不一定相等;
    4、hashCode不相等,equals一定不相等。

当面对大量的对象比较时,首先会比较hashCode值,如果hashCode值相等才比较equals值,这样会大大降低时间,提升效率。如果连hashCode值都不一样,那两个对象指定是不同的对象。
【参考文章】
https://www.cnblogs.com/goody9807/p/6516374.html
https://www.cnblogs.com/dolphin0520/p/3778589.html

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值