深入解析 Java 使用==或equals()比较字符串

Java 字符串常量

Java 字符串在面试、笔试中都是常考知识点,下面进行相关知识点讲解。

例子

1
2
3
4
5
6
7
8
9
String strOne = "testString";
String strTwo = "testString";
System.out.println(strOne.equals(strTwo));
System.out.println(strOne == strTwo);
/** output 输出
true
true
*/

结果分析

String的equals()是比较字符串的内容。故equals得到true很容易理解。

==是比较内存地址的,第一个true说明strOne 和strTwo的地址相同。

为什么呢?

java有常量池,存储所有的字符串常量。

String strOne = “testString”
java首先会在常量池查找是否有 “testString”这个常量,发现没有,于是创建一个 “testString”,然后将其赋给strOne。

String strTwo= “String”;
java同样会在常量池中查找 “testString”,这次常量池中已经有strOne 创建的 “testString”,故不创建新的常量,将其地址赋给strTwo。

如此,strOne和strTwo便有了相同的地址。

new建立String对象

Java 字符串对象创建有两种形式:

  • 上文的字符串常量,如String str = "testString"
  • 使用用new这种标准的构造对象的方法,如String str = new String("testString")

举例说明 1

1
2
3
4
5
6
7
8
9
String strOne = "testString";
String strThree = new String("testString");
System.out.println(strOne.equals(strThree));
System.out.println(strOne==strThree);
/** output 输出
true
false
*/

举例说明 2

1
2
3
4
5
6
7
8
9
String strOne = new String("testString");
String strThree = new String("testString");
System.out.println(strOne.equals(strThree ));
System.out.println(strOne==strThree);
/** output 输出
true
false
*/

分析原因

而用new String(“testString”)创建的两个字符串,用equals()比较相等,用==比较则不相等。

为什么呢?

new String()每次会在堆中创建一个对象,每次创建的对象内存地址都不同,故==不相等。但字符串内容是相同的,故equals()相等。

String intern() 方法

看下官方文档的说明,大致意思是:

如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回。

1
2
3
4
5
6
7
8
9
10
11
12
public String intern()
Returns a canonical representation for the string object.
A pool of strings, initially empty, is maintained privately by the class String.
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java™ Language Specification.
Returns:
a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.

举例说明 1

1
2
3
4
5
6
7
String strOne = "testString";
String strThree = new String("testString");
System.out.println(strOne==strThree.intern());
/** output 输出
true
*/

String strOne = "testString" 这行代码使得常量池中已经存在了"testString",intern() 从字符串常量池中查询到当前字符串已经存在,返回常量池中的字符串

举例说明 2

1
2
3
4
5
6
7
String strOne = new String("testString");
String strThree = new String("testString");
System.out.println(strOne.intern()==strThree.intern());
/** output 输出
true
*/

如文档里所说 s.intern() == t.intern() is true if and only if s.equals(t) is true

strOne.intern() 先查找常量池中是否存在当前字符串,发现不存在,于是会把字符串放入常量池中。strThree.intern()也先从常量池中查找,找到了刚才放入的字符串,所以两者相等。

底层实现

new 创建的String对象使用intern方法,intern方法,若不存在就会将当前字符串放入常量池中。

intern() 底层实现也是维护了一个hash表,保持字符串。

1
2
3
4
5
6
7
8
9
10
11
oop StringTable::intern(Handle string_or_null, jchar* name,
int len, TRAPS) {
unsigned int hashValue = java_lang_String::hash_string(name, len);
int index = the_table()->hash_to_index(hashValue);
oop string = the_table()->lookup(index, name, len, hashValue);
// Found
if (string != NULL) return string;
// Otherwise, add to symbol to table
return the_table()->basic_add(index, string_or_null, name, len,
hashValue, CHECK_NULL);
}

查看源码可知,intern() 是使用 jni 调用c++实现的StringTable的intern方法, StringTable的intern方法跟Java中的HashMap的实现是差不多的, 只是不能自动扩容。默认大小是1009。

注意,数据量规模很大时,如果程序将很多字符串常量(类名、方法名、key值等)存入intern()的常量池,当池大小超过了默认值后,性能可能会急剧下降。

String 真的不可变么

不可继承

源码可以看到 public final class String,所以 String 也不允许继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
public String() {
this.value = "".value;
}
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*
* @param original
* A {@code String}
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

不可改变

如果对一个已有字符串进行修改,不会在原来内存地址上修改数据,而是重新指向一个新的对象。

1
2
3
4
5
6
7
String x = "123456";
x.substring(0, 3);
System.out.println(x);
/** output 输出
123456
*/

使用 substring 改变字符串,也是返回一个新的字符串,而不是原地修改。

利用反射改变字符串

String 内部使用 private final char value[]; 来保存值,而且也没有暴露关于value的引用。虽然数组value一旦赋值后,不允许修改,但是这仅仅是不能指向新的数组,数组的内容其实可以修改。

我们可以利用反射得到 value ,从而改变 value 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String x = "123456";
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
f.set(x, "helloworld".toCharArray());
// 可以利用set改变value的值,final char value[] 不起作用?
System.out.println(x);
char[] value = (char[]) f.get(x);
// 得到value的引用,修改数据中的某个元素
value[0] = 'H';
value[1] = 'E';
System.out.println(x);
/** output 输出
helloworld
HElloworld
*/

看下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String x = "123456";
String y = "123456";
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
f.set(x, "helloworld".toCharArray());
System.out.println(x);
System.out.println(y);
char[] value = (char[]) f.get(x);
value[0] = 'H';
value[1] = 'E';
System.out.println(x);
System.out.println(y);
/** output 输出
helloworld
helloworld
HElloworld
HElloworld
*/

xy 本来都是"123456",我们只利用反射改变了x的值,却发现y的值也跟着变了,因为这两者都是指向常量值中的字符串,所以修改一个,另外一个也变了。

不推荐实际生产环境中使用反射改变字符串,违背String不可变行

参考文章