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不能正常的运作!