一、背景
字符串的不可变性可以说是面试中的一个常见的“简单的” 问题。
常见的回答如:
字符串创建后不可改变。
字符串的不可变性是指字符串的字符不可变。
String 的 value 字符数组声明为 final 保证不可变。
真的是这样吗?
下面我们再思考两个问题:
- 那么字符串的不可变究竟是指什么?
- 是如何保证的呢?
下面看一个奇怪的现象:在程序一段程序的最后执行下面的语句居然打印了 “aw” 为什么?
// 前面代码省略
System.out.println("ab");
建议大家先思考,然后再看下文。
二、案例
你认为下面的示例代码的输出结果是啥?
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str = "ab";
System.out.println("str=" + str);
Class stringClass = str.getClass();
Field field = stringClass.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = 'w';
System.out.println("str=" + str );
}
}
输出结果为:
str=ab
str=aw
是不是和有些同学想的有些不一样呢?
字符串的字符数组可以通过反射进行修改,导致字符串的“内容”发生了变化。
我们再多打印一些:
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str = "ab";
System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
Class stringClass = str.getClass();
Field field = stringClass.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = 'w';
System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
}
}
输出结果为:
str=ab,1638215613,3105
str=aw,1638215613,3105
通过这个例子我们可以看出,String 字符串对象的 value 数组的元素是可以被修改的。
简单看下 java.lang.System#identityHashCode
的源码:
/**
* Returns the same hash code for the given object as
* would be returned by the default method hashCode(),
* whether or not the given object's class overrides
* hashCode().
* The hash code for the null reference is zero.
*
* @param x object for which the hashCode is to be calculated
* @return the hashCode
* @since JDK1.1
*/
public static native int identityHashCode(Object x);
native 方法,该函数给出对象唯一的哈希值(不管是否重写了 hashCode 方法)。
可知,对象没有变。
那么,我们知道 String 的哈希值是通过字符串的字符数组计算得来的(JDK8),那为啥两次 hashCode 函数返回值一样呢?
我们再仔细看下 java.lang.String#hashCode
源码:
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
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
//省略其他
}
发现在第一次调用 hashCode 函数之后,字符串对象内通过 hash 这个属性缓存了 hashCode的计算结果(只要缓存过了就不会再重新计算),因此第二次和第一次相同。
那么如何保证不可变性的呢?
首先将 String 类声明为 fianl 保证不可继承。
然后,所有修改的方法都返回新的字符串对象,保证修改时不会改变原始对象的引用。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
其次,字符串字面量都会指向同一个对象。
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 字符串字面量
String str = "ab";
System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
Class stringClass = str.getClass();
Field field = stringClass.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = 'w';
System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
// 字符串字面量
System.out.println("ab");
}
可以看到打印结果为:
str=ab,1638215613,3105
str=aw,1638215613,3105
aw
很多人不理解,为啥 System.out.println("ab");
打印 aw ?
是因为字符串字面量都指向字符串池中的同一个字符串对象(本质是池化的思想,通过复用来减少资源占用来提高性能)。
A string literal is a reference to an instance of class
String
(§4.3.1, §4.3.3).字符串字面量是指向字符串实例的一个引用。
Moreover, a string literal always refers to the same instance of class
String
. This is because string literals - or, more generally, strings that are the values of constant expressions (§15.28) - are “interned” so as to share unique instances, using the methodString.intern
.字符串字面量都指向同一个字符串实例。
因为字面量字符串都是常量表达式的值,都通过String.intern
共享唯一实例。
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
对象池中存在,则直接指向对象池中的字符串对象,否则创建字符串对象放到对象池中并指向该对象。
因此可以看出,字符串的不可变性是指引用的不可变。
虽然 String 中的 value 字符数组声明为 final,但是这个 final 仅仅是让 value的引用不可变,而不是为了让字符数组的字符不可替换。
由于开始的 ab 和最后的 ab 属于字面量,指向同一个字符串池中的同一个对象,因此对象的属性修改,两个地方打印都会受到影响。
三、思考
很多简单的问题并没有看起来那么简单。
大家在看技术博客,在读源码的时候,一定要有自己的思考,多问几个为什么,有机会多动手实践。
大家在学习某个技术时要养成本质思维,即思考问题的本质是什么。
面试的时候,简单的问题要回答全面又有深度,不会的问题要回答出自己的思路,这样才会有更多的机会。
想了解更多开发和避坑技巧,经验,学习方法少走弯路,
欢迎关注本人的慕课专栏:
再学经典:《Effective Java》独家解析
解锁大厂思维:剖析《阿里巴巴 Java 开发手册》