String详解 & 等值判断
1、String 为什么是不可变的?
可变性
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,所以String 对象是不可变的。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}
🐛 修正 : 被
final关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final关键字修饰的数组保存字符串并不是String不可变的根本原因,因为这个数组保存的字符串是可变的(final修饰引用类型变量的情况)。
String真正不可变有下面2点原因:
- 保存字符串的数组被
final修饰且为私有的,并且String类没有提供/暴露修改这个字符串的方法String类被final修饰导致其不能被继承,进而避免了子类破坏String不可变
因为虽然value是不可变的,也只是value这个引用地址不可变。挡不住
因为value变量只是stack上的一个引用,而数组的本体结构其实是在heap堆,String类里的value用final修饰,只是说stack里的这个叫value的引用地址不可变,没有说堆里array本身数据不可变。
2、String、StringBuffer、StringBuilder 的区别?
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
此外,AbstractStringBuilder 还提供了其他基本操作,如 expandCapacity、insert、indexOf 等公共方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
补充:在 Java 9 之后,
String、StringBuilder与StringBuffer的实现改用 byte 数组存储字符串。
线程安全性
String 中的对象是不可变的,也就可以理解为常量,因此线程安全。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
操作少量的数据:适用
String单线程操作字符串缓冲区下操作大量数据:适用
StringBuilder多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
3、字符串拼接用 “+” ,还是 StringBuilder or StringBuffer?
Java 语言本身并不支持运算符重载,“+” 和 “+=” 是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符!
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;
对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,而是重复创建新的 StringBuilder 对象。
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {
s += arr[i];
}
System.out.println(s);
StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象。

4、字符串常量池的技术了解吗?
java中常量池的概念主要有三个:全局字符串常量池,class文件常量池,运行时常量池。我们这里所说字符串常量池的就是全局字符串常量池,对这个想弄明白的同学可以看这篇Java中几种常量池的区分。
jvm为了提升性能和减少内存开销,避免字符的重复创建,其维护了一块特殊的内存空间,即字符串常量池。当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对String类型专门开辟的一块区域,主要目的是为了避免字符串的重复创建!
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa==bb);// true
字符串常量池的位置也是随着jdk版本的不同而位置不同。在jdk7之前,常量池的位置在永久代(方法区)中,此时常量池中存储的是。在jdk7中,常量池的位置在堆中,此时,常量池存储的就是了。
5、String str="aaa" 与 String str=new String("aaa")一样吗?
- 使用
String a = “aaa” ;,程序运行时会在常量池中查找”aaa”字符串,若没有,会将”aaa”字符串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。 - 使用String b = new String("aaa");`,程序会在堆内存中开辟一片新空间存放新对象,同时会将”aaa”字符串放入常量池,相当于创建了两个对象,无论常量池中有没有”aaa”字符串,程序都会在堆内存中开辟一片新空间存放新对象。
6、intern()函数
intern()函数:
intern函数的作用是将对应的符号常量进入特殊处理,在JDK1.6以前 和 JDK1.7以后有不同的处理:
在JDK1.6中,intern的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量;如果没有找到,则将该字符串常量加入到字符串常量区,也就是在字符串常量区建立该常量;
在JDK1.7中,intern的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量;如果没有找到,说明该字符串常量在堆中,则把堆区该对象的引用加入到字符串常量池中,以后别人拿到的是该字符串常量的引用,实际存在堆中
演示代码:
@Test
public void test() {
String s = new String("2");
s.intern();
String s2 = "2";
System.out.println(s == s2);
String s3 = new String("3") + new String("3");
s3.intern();
String s4 = "33";
System.out.println(s3 == s4);
}
运行结果:
// 在jdk6中输出是 false false
false
false
// 在jdk7中输出是 false true
false
true
JDK1.6
String s = new String("2");创建了两个对象,一个在堆中的StringObject对象,一个是在常量池中的“2”对象。 s.intern();在常量池中寻找与s变量内容相同的对象,发现已经存在内容相同对象“2”,返回对象2的地址。 String s2 = "2";使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象"2"的地址。 System.out.println(s == s2);从上面可以分析出,s变量和s2变量地址指向的是不同的对象,所以返回false
String s3 = new String("3") + new String("3");创建了两个对象,一个在堆中的StringObject对象,一个是在常量池中的“3”对象。中间还有2个匿名的new String("3")我们不去讨论它们。 s3.intern();在常量池中寻找与s3变量内容相同的对象,没有发现“33”对象,在常量池中创建“33”对象,返回“33”对象的地址。 String s4 = "33";使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象"33"的地址。 System.out.println(s3 == s4);从上面可以分析出,s3变量和s4变量地址指向的是不同的对象,所以返回false
JDK1.7
String s = new String("2");创建了两个对象,一个在堆中的StringObject对象,一个是在堆中的“2”对象,并在常量池中保存“2”对象的引用地址。 s.intern();在常量池中寻找与s变量内容相同的对象,发现已经存在内容相同对象“2”,返回对象“2”的引用地址。 String s2 = "2";使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回对象“2”的引用地址。 System.out.println(s == s2);从上面可以分析出,s变量和s2变量地址指向的是不同的对象,所以返回false
String s3 = new String("3") + new String("3");创建了两个对象,一个在堆中的StringObject对象,一个是在堆中的“3”对象,并在常量池中保存“3”对象的引用地址。中间还有2个匿名的new String("3")我们不去讨论它们。 s3.intern();在常量池中寻找与s3变量内容相同的对象,没有发现“33”对象,将s3对应的StringObject对象的地址保存到常量池中,返回StringObject对象的地址。 String s4 = "33";使用字面量创建,在常量池寻找是否有相同内容的对象,发现有,返回其地址,也就是StringObject对象的引用地址。 System.out.println(s3 == s4);从上面可以分析出,s3变量和s4变量地址指向的是相同的对象,所以返回true。
7、equals() 与 == 的区别
== 对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,
==比较的是值 - 对于引用数据类型来说,
==比较的是对象的内存地址
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals() 不能用于比较两个基本数据类型的变量,只能用来比较两个对象类型的变量是否相等;equals()隶属于Object类,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。
Object 类 equals() 方法:
public boolean equals(Object obj) {
return (this == obj);
}
equals() 方法存在两种使用情况:
- 类没有重写
equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是Object类equals()方法。 - 类重写了
equals()方法 :自定义类时,一般我们都重写equals()方法来比较两个对象实例中的属性是否相等,若它们的属性相等,则返回 true。
举个例子(这里只是为了举例。实际上,你按照下面这种写法的话,像 IDEA 这种比较智能的 IDE 都会提示你将 == 换成 equals() ):
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 创建‘ab’,并放到常量池中
String bb = "ab"; // 虚拟机会先从常量池中查找,找到则直接赋值给bb
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
String类equals()方法:
public boolean equals(Object anObject) {
if (this == anObject) { //先比较对象地址是否相同,相同则两个对象值一定也相同
return true;
}
if (anObject instanceof String) { //逐个字符比较形参字符串值与当前字符串值是否相同
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
8、介绍下hashCode()?
hashCode() 的作用是获取哈希码,也称为散列码。
它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据 “键” 快速的检索出对应的 “值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
9、为什么要有 hashCode?
以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。
但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
10、hashCode()、equals()两种方法是什么关系?
hashCode() 有什么用呢?
hashCode() 的作用是获取哈希码(int 类型),也称为散列码,这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都有 hashCode() 函数。另外需要注意的是: Object 的 hashCode() 方法是native方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”(可以快速找到所需要的对象),这其中就利用了散列码!
为什么要有 hashCode?
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?
下面这段内容摘自《Head First Java》:
当你把对象加入
HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode值作比较,如果没有相符的hashCode,HashSet会假设对象没有重复出现,就会直接让其加入。但是如果发现有相同hashCode值的对象,这时会调用equals()方法来检查hashCode相等的对象是否真的相同:如果两者相同,HashSet就不会让其加入;如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals的次数,相应就大大提高了执行速度。
那为什么 JDK 还要同时提供这两个方法呢?
这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考HastSet添加元素的过程)!
我们在前面也提到了HastSet添加元素的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同,也就是说 hashCode 帮助我们大大缩小了查找成本!
那为什么不只提供 hashCode() 方法呢?
这是因为两个对象的hashCode 值相等并不代表两个对象就相等!
总结:
hashCode()与equals()的相关规定:
如果两个对象相等,则hashcode一定也是相同的;
两个对象相等,对两个对象分别调用equals方法都返回true;
两个对象有相同的hashcode值,它们也不一定是相等的;
11、那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的呢?
因为 hashCode() 所使用的哈希算法针对不同的对象有时计算出相同的哈希码,越糟糕的哈希算法越容易碰撞(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。
总结下来就是 :
- 如果两个对象的
hashCode值相等,那这两个对象不一定相等(哈希碰撞) - 如果两个对象的
hashCode值相等并且equals()方法也返回true,我们才认为这两个对象相等 - 如果两个对象的
hashCode值不相等,我们就可以断定这两个对象不相等
12、为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode 值必须是相等,也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等!
如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等!
思考 :自定义的对象,如果重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题呢?
答案 :使用HashMap, 就必须同时重写hashcode()和equals(),以保证key的唯一性,否则可能导致HashMap不能正常的运作!
