在学习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: BBABABAAStringBuffer一直都是很正常的,但是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
