一直很喜欢一句话,如果你不能向一个六岁的孩子解释清楚,那么其实你自己根本没弄懂。这句话很适合我们这些技术人,程序员的世界里不应该出现模糊的概念,遇到一些拿捏不准的东西需要及时理解透彻。
String类是我们每天需要使用到无数次的对象,工作中直接拿来用就好了,很少会再去细细的琢磨琢磨String,这里就带大家一起琢磨琢磨String。
String的内存分配
String是一个不可变的对象,存在方法区的常量池中,使用了享元模式,可以被共享使用,以减少对象的创建。
1 String a = "hello";
2 String b = "hello";
3 String c = new String("hello");
4 String d = new String("hello");
下面解释一下上面四行代码的内存分配:
- 第一行会先在在常量池中寻找是否存在对象”hello”,如果不存在,就在常量池中创建一个”hello”对象,并在栈中创建”hello”对象的引用a;如果存在,则直接在栈中创建”hello”对象的引用a;
- 第二行会在常量池中找到对象”hello”,在栈中创建”hello”对象的引用b;
- 第三行使用了String的构造方法,所以会在堆中新创建一个String对象,然后在栈中那个创建该对象的引用c;
- 第四行和第三行一样,会现在堆中创建新的对象,再在栈中创建引用;
所以很好理解的就是a==b,a!=c,c!=d,因为a和b的地址都是常量池中的”hello”对象,c的地址是堆中新创建的String对象,d的地址也是堆中新创建的String对象。
工作中我们很少会使用构造方法去创建String对象,同样的也不建议使用构造方法创建String对象。
我们通过String的内存分配,了解到每创建一个新的String对象都可能会在方法区中创建一个新的对象,所以工作中遇到频繁字符串拼接的时候如果使用下面这样的方式拼接,可能会创建很多无用的临时对象到常量池中。
String a = "hello ";
String b = "world";
String c = "!";
String hello = a + b + c;
当然了,不要想当然的以为下面这段代码创建d对象时也会一个个的拼接,在常量池中创建各种临时对象。没有对象参与拼接,全是字符串进行拼接的情况下,对于JVM来说d和e是等价的,这个算是JVM的一个小优化。
String d = "hello " + "world" + "!";
String e = "hello world!";
StringBuffer和StringBuild
由于字符串的频繁拼接会导致在常量池中创建很多无用的对象,所以这里就需要引入StringBuffer和StringBuild。
先说一下StringBuffer,如下代码在对StringBuffer进行拼接的时候,不会生成新的对象,只会修改这个StringBuffer对象的长度和内容,所以使用StringBuffer拼接字符串可以避免在常量池中创建无用对象的问题。
String hello = new StringBuffer("hello ").append("world").append("!").toString();
这里再区分一下StringBuffer和StringBuild,StringBuffer会对修改方法进行同步,因此多个线程同时去操作一个StringBuffer对象时,所有的方法都是序列化进行的,可以在多线程场景下使用。
总结一下就是StringBuffer线程安全,StringBuild线程不安全,但是StringBuild速度比StringBuffer快,所以
- String:适用于少量的字符串操作的情况
- StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
- StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
说到这里还要说一个编码习惯问题,很多人即使知道了使用String来拼接字符串会存在的一些问题,但是问题不会直接体现出来,所以还是习惯使用Sting来拼接字符串。这就是编码习惯不好的原因,养成一个好的编码习惯会减少很多未知的问题。
intern方法
这里先不着急引入intern方法,先来看一段代码,使用String来作为Synchronized锁的问题
public class StringSyncTest {
public static void main(String[] args) {
String sync1 = "lock";
String sync2 = new String("lock");
MyThread t1 = new MyThread(sync1);
t1.setName("t1");
MyThread t2 = new MyThread(sync2);
t2.setName("t2");
t1.start();
t2.start();
}
public static void syncString(String sync) throws InterruptedException{
synchronized (sync) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " method syncString in");
Thread.sleep(2000);
System.out.println(threadName + " method syncString out");
}
}
}
class MyThread extends Thread {
private String sync;
public MyThread(String sync) {
this.sync = sync;
}
@Override
public void run() {
try {
StringSyncTest.syncString(sync);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
返回值
t1 method syncString in
t2 method syncString in
t2 method syncString out
t1 method syncString out
可以看出我们使用字符串来作为synchronized的条件的时候,锁失效了,因为sync1是在常量池中的对象,syn2是在堆中的对象,sync1!=sync2。所以不推荐使用String来作为锁的条件。
但是如果必须要使用一个String对象来作为锁的条件,又怎么处理呢?这里就需要引入intern方法了,这里引用一段jdk源码中对intern方法的描述的翻译,intern用来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后 返回引用。
所以我们只需要修改一下syncString方法,对实参sync添加一个intern方法,就能保证所有的sync都是指向常量池中的引用了,从而解决用String来作为Synchronized对象的问题了。
public static void syncString(String sync) throws InterruptedException{
sync.intern();
synchronized (sync) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " method syncString in");
Thread.sleep(2000);
System.out.println(threadName + " method syncString out");
}
}
扫码关注,不迷路