在学习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