String的Tricky的地方
简单而言,java中的直接用字符串初始化的Spring是指向常量池的。而常量池中的内容是不可改变的。(这句话千万别理解成String对象都在常量池。正常生成的String对象也存在于堆)。
public class Sandbox4 extends Sandbox2{
String s1 = "test1";
public static void main(String[] args) {
Sandbox4 a= new Sandbox4();
a.ChangeString(a.s1);
System.out.print(a.s1);
}
void ChangeString(String s){
s = "test2";
}
}
如果你强行要改变他的值。JVM回在常量池中新生成一个对象。而java中都是穿值的。上面的程序中进入方法的s是一个引用。方法改变了引用的值,而不是引用所指向对象的值。所以程序的释出仍然是test1。即使你采用new String的方法声明s1也结果不会改变的。JAVA API中已经明确说了String中的内容不能改变。只能新建一个对象替换。
另外的一点是,当你使用字符串字面量给String赋值时,有可能新生成一个对象(比如说上面的例子)。也有可能不新生成一个对象,而是指向常量池中已经存在的对象。
public class Sandbox4 extends Sandbox2{
String s1 = "test1";
public static void main(String[] args) {
Sandbox4 a= new Sandbox4();
String s2 = "test1";
System.out.print("s1==s2:"+(a.s1==s2));
}
}
但是,如果将上述换成new String的方法来定义引用,则是两个不同的对象。
如果你想生成一个非常量的String。请使用StringBuffer类。
这里还要牵扯一个问题。就是final。
当书写final Object obj类似的定义时,obj指向的对象并不是不可以变的!而只是obj指向的对象不能变成别的对象。所以跟上面说的情况不同。不能说String是默认final修饰的。这是两回事。
java中的是比较他们的内存地址。object中的equals就是用实现的。当然可以重写。hashcode是用来均匀映射的。理论上hashcode相等不代表什么。
但是,在hashmap等的实现中这样却不行。事实上,我们在不能允许两个equals相等的对象hashcode不同。所以,如果你重写了equals方法,而且会用到集合框架。请重写hashcode。你重写的equals方法一般是要求两个对象中部分或者全部字段相等。显然,用这些字段计算hashcode就好啦。
在JDK1.8中,常量池被分到了metaSpace。
String不可变原理
String的底层是字符数组。
private final char[] value;
数组引用被设置成了final。我们知道final只在编译期间有用,而且数组引用的final不代表数组中每个元素也是final。所以,String可以被反射改变。
public static void testReflection() throws Exception {
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
}
但是,不是说String直接字面常量赋值会存储于常量池吗?万能的反射才不管方法区是啥!
除了把value设置为final,JDK作者还将String的class设置为final以免继承者改变类的逻辑,最终的保障就是小心翼翼写代码,不暴露任何可以修改value的方法。
itern方法
在JDK源代码里,intern就是这么一个简单的系统调用。如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。
public native String intern();
public class Test {
public static void main(String argv[])
{
String s2 = new String("HelloWorld");
System.out.println(s2 == s2.intern());//输出false,因为s2在堆上,而s2.intern返回常量池中的对象
}
}
使用这个方法主要目的是节省内存空间。
intern()方法在JDK1.6中的作用是:比如String s = new String(“SEU_Calvin”),再调用s.intern(),此时返回值还是字符串"SEU_Calvin",表面上看起来好像这个方法没什么用处。但实际上,在JDK1.6中它做了个小动作:检查字符串池里是否存在"SEU_Calvin"这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会把"SEU_Calvin"添加到字符串池中,然后再返回它的引用。
目的
-
不可变对象可以提高String Pool的效率和安全性。如果你知道一个对象是不可变的,那么需要拷贝这个对象的内容时,就不用复制它的本身而只是复制它的地址,复制地址(通常一个指针的大小)需要很小的内存效率也很高。对于同时引用这个“ABC”的其他变量也不会造成影响。java的classloader机制需要使用String类型,所以,保证String的不能修改极为重要。
-
不可变对象对于多线程是安全的,因为在多线程同时进行的情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况。
String的性能问题
以前,String类一直被诟病存在拼接时巨大的性能问题。实际上,String的+是语法糖,内部是新建StringBuilder对象完成拼接后再转String。会导致新建和销毁大量对象。所有请避免频繁使用String进行字符串拼接。
String、StringBuffer和StringBuilder在本质上都是字符数组,不同的是,在进行连接操作时,String每次返回一个新的String实例,而StringBuffer和StringBuilder的append方法直接返回this,所以这就是为什么在进行大量字符串连接运算时,不推荐使用String,而推荐StringBuffer和StringBuilder。
StringBuilder的append直接使用了其父类的append的方法。
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;
}
而StringBuffer,则是升级直接搞成了synchronized修饰一下就完事了。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
总说StringBuilder线程不安全。不安全在哪呢?你显然不能多线程的操作StringBuider对象。但是当大多数时候我们拼接一个字符串时,都是单线程堆栈的操作环境。所以没啥问题。
进一步深入
String的机制可以说是JAVA最恶心的地方之一。
JDK6,JDK7,JDK8的String类的行为都不相同。真是令人恐怖。
推荐下一篇
https://blog.csdn.net/define_us/article/details/82223291