String的面试知识点

String的变化历程

在java6之前,String对象主要有四个成员变量:char[]数组,offset偏移量,count字符数量,hash哈希值;通过offset和count两个属性可以定位char[]数组,共享数组对象,但是有可能会导致内存泄露。占用空间不被释放,比如源字符串很大,subString()一个字符,然后i只调用分割后的,那麽就会会共享原String对象,不会被GC,造成内存泄露。
泄露:调用String对象的substring方法后指向的对象地址并没有发生改变,只是改变的是偏移量,这样的话在GC阶段就有可能造成内存泄露了。
解决:这个里面是通过内存复制的方式重新指向了一个新的地址。
java7/8修复了内存泄漏问题。
java9中维护了一个新的属性coder,标识字符串的
字节编码
;char[]改成byte[],可以减少每一个字符的占用空间,由16字节减少为8字节

不可变性的好处(final)

  • 1.String被设计为不可变是因为String对象是缓存在字符串池中的,
    因此这些缓存的字符串是可以被多个客户端访问的,如果一个客户端的访问影响了别的客户端的行为,这样就存在风险。例如一个客户端把"test"改为"TEST",其他所有的客户端都会跟着受影响。之所以把String进行了缓存处理,是出于性能的考虑。也就是说开发人员认为读比写次数多,索性就直接放方法区公用。因此这个风险只能通过把String设置为不可变来避免。同时,String类被声明为final的。是防止其他人通过继承String类,覆盖父类的方法,会破坏String的不可变性缓存性以及hascode的计算方式

  • 2.为了实现String可以创建HashCode不可变性
    因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。否则hashcode不同,同一个字符串会导致不同entry。

  • 3**.为了线程安全**
    因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

  • 4.String的不可变性为Java的类加载机制的安全性提供了根本的保障。
    如果String是可变的,一个加载"java.io.Writer" 类的请求,可以被改变为"mil.vogoon.DiskErasingWriter",这样安全就没有保障了。
    双亲委派机制保证了string类不被重写,因为肯定是想看root加载类有没有,再往下看。

字符串的创建问题

  1. String str = "abc"的方式会检查对象是否在字符串常量池中,如果在,就直接返回该对象的引用;否则新的字符串将在常量池中被创建。
    2.str = new String(“abc”)每次都会在堆中新建一个对象。

字符串的性能优化

  • 1.两个或者两个以上的字符串常量相加,在预编译的时候“+”会被优化,相当于把两个或者两个以上字符串常量自动合成一个字符串常量.
      字符串常量相加,不会用到StringBuilder对象,有一点要注意的是:字符串常量和字符串是不同的概念,字符串常量储存于方法区(总之就常量池),而字符串储存于堆(heap)。
  • 2.而非纯常量的字符串相加
    像是字符串相加表达式中带变量的那种的话,就是JVM会自动创建一个StringBuilder然后再调用append()方法最后再调用toString()方法返回的方式了,所以在堆中会有个String对象,引用指向的是堆中的对象的地址。
    所以相加出来的结果,是不会被加到常量池中的。
    String s1 = new String("he")+new String("llo"); 
    1,这个代码中,首先,new String(“he”),先在常量池中看,发现没有这个"he"常量,于是建一个,然后再在堆中创建一个String的对象(但没引用,很快被gc的)。
    2,加法**,暗中new了StringBuilder,调用append方法。**
    3.new String(“llo”)一样的道理,堆中一个String对象,常量池中"llo"常量对象。
    4.StringBuilder的append方法搞定后,调用toString()方法,具体是new一个String对象,也就是现在是一个堆中的String对象,内容是"hello",但注意这个hello没有在常量池中创建!!其实可以理解因为没有出现过一次"hello",拼接是通过StringBuilder的append方法完成的。
    总之:对于所有包含new方式新建对象(包括null)和变量形式 的“+”连接表达式,它所产生的新对象都不会被加入字符串池中。
String str = "new String("he");

for(int i=0; i<10; i++) {
      str = str + i;
}

自动优化成

String str = "haha";

for(int i=0; i<10; i++) {
              str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

这样在循环体内一直生成新的StringBuilder对象,性能是比较低的,这种情况下,最好在循环外层定义一个StringBuilder对象,然后使用该对象进行字符串的拼接。

intern()

JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池1.7后则是将在堆上的地址引用复制到常量池。量池被从方法区中移出来到了堆中

String、StringBuffer、StringBuilder区别

  • String,定义字符串常量,每次对字符串的修改,都会返回一个新的字符串对象。如果涉及到字符串变量的拼接,不建议使用String。

  • StringBuilder,主要用于字符串变量的拼接,性能比String的拼接要高线程不安全。适用于单线程或没有线程安全问题的字符串变量拼接。

  • StringBuffer,与StringBuilder类似,不过StringBuffer是线程安全的也就是说任何对字符串的操作,都是加锁的,所以性能比StringBuilder低。适用于多线程下字符串变量的修改。

猜你喜欢

转载自blog.csdn.net/code_mzh/article/details/106841494