以前都以为string 定义为final的了,在java中是不能修改的,但是今天面试的时候问这个问题,我回答说如果真想修改就修改物理地址中的数据了,然后回头看了下,这个也是可以修改的:
string对象是放一个共享内存池里的,创建string时,先看池里有没有,有就返回,没有则创建。
- package com.leon.demo;
- import java.lang.reflect.Field;
- public class StringDemo {
- public static void main(String[] args) throws Exception {
- String str01 = "aaa";
- String str02 = "aaa";
- String str03 = "bbbb";
- System.out.println(str01.hashCode() + "/" + str02.hashCode() + "/" + str03.hashCode());
- Field field = str02.getClass().getDeclaredField("value");
- field.setAccessible(true);
- field.set(str02,new char[] { 'b', 'b', 'b', 'b'});
- System.out.println(str01 + "/" + str02);
- field = str02.getClass().getDeclaredField("count");
- field.setAccessible(true);
- field.set(str02,4);
- System.out.println(str01 + "/" + str02);
- System.out.println(str01.hashCode() + "/" + str02.hashCode() + "/" + str03.hashCode());
- field = str02.getClass().getDeclaredField("hash");
- field.setAccessible(true);
- field.set(str02,0);
- System.out.println(str01.hashCode() + "/" + str02.hashCode() + "/" + str03.hashCode());
- System.out.println(str01 == str02);
- System.out.println(str02 == str03);
- }
- }
- 96321/96321/3016832
- bbb/bbb
- bbbb/bbbb
- 96321/96321/3016832
- 3016832/3016832/3016832
- true
- false
- 根据Java String的特性,str01和str02指向同一个内存地址,所以str01,str02始终保持一致。
- String不能修改的本质是一个final的字符数组(private final char value[];),但是final只在编译时有效,而运行期间是没有作用的,所以可以通过反射修改数组的值。
- 代码中修改后字符串的hashcode之所以没有变化,是因为String对象会缓存hashcode,去掉缓存就可以看到变化。
- 根据Java String的特性,上面代码创建的所有字符串对象都保存在String Pool中,代码中str02与str03的内容相同,地址却不同,所以String Pool中就存在两个相同的字符串。
public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;
for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}
java为了提高效率, 只再第一次调用hashcode函数时生成一次hash, 并把值存在这个实例里面。 以后即使value数组的值变化了,也不再生成新的hash。
最后,感叹一下,反射机制真是厉害啊!
首先反射机制是不能访问私有成员的.只能访问公有成员.至少我的代码测试了不行。否则还有什么封装性可言?
其次,String不可以变的原因很多.不仅仅是因为value是final,还在于这个类是final的,value是private的,而且这些都只是辅助加强对String的不可变性的保护措施,并不能完全保证不可变。因为java的对象变量存储的是引用.作为数组的vlaue也是引用。final最多只能保证其一直指向同一个数组对象,而要想保证数组本身访问时不改变其每个元素的内容,只能通过其对外提供的方法保证只能读。对外的构造方法传递的char数组必须先拷贝下来,防止外界修改。当然为了节省资源有一个构造方法能够通过直接传递数组对象,不拷贝.但是这样会容易破坏封装性,使得外界能够通过数组对象改变String值。所以这个构造方法是包内访问的。而String又在java.lang包下。java包都是被保护起来了,外界不能修改,即里面的类都是Sun公司写好的,非常安全.
以上这些措施才保证了String的不可变性
附上
三分钟理解Java中字符串(String)的存储和赋值原理
可能很多java的初学者对String的存储和赋值有迷惑,以下是一个很简单的测试用例,你只需要花几分钟时间便可理解。
1.在看例子之前,确保你理解以下几个术语:
栈 :由JVM分配区域,用于保存线程执行的动作和数据引用。栈是一个运行的单位,Java中一个线程就会相应有一个线程栈与之对应。
堆 :由JVM分配的,用于存储对象等数据的区域。
常量池 :在堆中分配出来的一块存储区域,用于存储显式 的String,float或者integer.例如String str="abc"; abc这个字符串是显式声明,所以存储在常量池。
2.看这个例子,用JDK5+junit4.5写的例子,完全通过测试
- import static org.junit.Assert.assertNotSame;
- import static org.junit.Assert.assertSame;
- import org.junit.Test;
- /**
- * @author Heis
- *
- */
- public class StringTest{
- @Test
- public void testTheSameReference1(){
- String str1="abc";
- String str2="abc";
- String str3="ab"+"c";
- String str4=new String(str2);
- //str1和str2引用自常量池里的同一个string对象
- assertSame(str1,str2);
- //str3通过编译优化,与str1引用自同一个对象
- assertSame(str1,str3);
- //str4因为是在堆中重新分配的另一个对象,所以它的引用与str1不同
- assertNotSame(str1,str4);
- }
- }
- 第一个断言很好理解,因为在解析的时候,"abc"被存储在常量池中,str1和str2的引用都是指向常量池中的"abc"。所以str1和str2引用是相同的。
- 第二个断言是由于编译器做了优化,编译器会先把字符串拼接,再在常量池中查找这个字符串是否存在,如果存在,则让变量直接引用该字符串。所以str1和str3引用也是相同的。
- str4的对象不是显式赋值的,编译器会在堆中重新分配一个区域来存储它的对象数据。所以str1和str4的引用是不一样的。