String---详解

String

  • String实现了Serializable接口:表示字符串是支持序列化的。
  • 实现了Comparable接口:表示String可以比较大小
  • String声明为final的, 不可被继承
  • String在jdk8及以前内部定义了final char value[],value用于存储字符串数据,jdk9时改为byte[]

String的不可变性

  • 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
  • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  • 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。

字符串常量池

String str="i"与 String str=new String(“i”)一样吗?

  • String str="i"会将起分配到常量池中,常量池中没有重复的元素,如果常量池中存中i,就将i的地址赋给变量,如果没有就创建一个再赋给变量。
  • String str=new String(“i”)会将对象分配到堆中,即使内存一样,还是会重新创建一个新的对象。
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

    //字面量
    String a = "aa";
    
  • 如果不是用双引号声明(字面量方式)的String对象,可以使用String提供的intern()方法

  • 字符串常量池中是不会存储相同内容的字符串的

  • String Pool 是一个固定大小的Hashtable,默认值大小长度是1009。如果放进StringPool的String非常多, 就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String. intern时性能会大幅下降。

  • 使用-XX:StringTableSize可设置StringTable的长度

  • jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设 置没有要求

  • jdk7中,StringTable的长度默认值是60013

  • jdk8开始,1009是StringTable长度可设置的最小值

  • Java 6及以前,字符串常量池存放在永久代

  • Java 7中Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内

    1、永久代默认情况下比较小

    2、永久代的回收效率较低,垃圾回收频率低

    • 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样在进行调优应用时仅需要调整堆大小就可以了
    • 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String. intern()
  • Java8元空间,字符串常量在


String拼接操作

  1. 常量与常量的拼接(+)结果在常量池,原理是编译期优化
  2. 常量池中不会存在相同内容的常量。
  3. 只要其中有一个是变量,结果就在堆中,变量拼接的原理是***StringBuilder***
  4. 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
  5. 注意字符串拼接操作不一定使用StringBuilder
    • 情况一:拼接符号左右两边都是字符串常量:"a"+"b"
    • 情况二:拼接符号左右两边都是常量引用:final String a= "a";final String b="b"; a+b;
@Test
public void test1(){
  String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
  String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2
  /*
   * 最终.java编译成.class,再执行.class
   * String s1 = "abc";
   * String s2 = "abc"
   */
  System.out.println(s1 == s2); //true
  System.out.println(s1.equals(s2)); //true
}
@Test
public void test2(){
  String s1 = "javaEE";
  String s2 = "hadoop";

  String s3 = "javaEEhadoop";
  String s4 = "javaEE" + "hadoop";//编译期优化
  //如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop
  String s5 = s1 + "hadoop";
  String s6 = "javaEE" + s2;
  String s7 = s1 + s2;

  System.out.println(s3 == s4);//true
  System.out.println(s3 == s5);//false
  System.out.println(s3 == s6);//false
  System.out.println(s3 == s7);//false
  System.out.println(s5 == s6);//false
  System.out.println(s5 == s7);//false
  System.out.println(s6 == s7);//false
  //intern():判断字符串常量池中是否存在javaEEhadoop值,如果存在,则返回常量池中javaEEhadoop的地址;
  //如果字符串常量池中不存在javaEEhadoop,则在常量池中加载一份javaEEhadoop,并返回次对象的地址。
  String s8 = s6.intern();
  System.out.println(s3 == s8);//true
}

'+'和append的区别

append的方式比拼接字符串更加高效

  • 使用append的方式,从始至终只需要创建一个stringbuilder对象

  • 使用字符串拼接,每次都需要创建stringbuilder对象、String对象,占用内存过多

  • 比如在循环体里面:

    String str = "";
    for(int i=0; i<10000;i++){
        str += i;
    }
    

    每一条语句,都会生成一个新的StringBuilder,多到没谱!

  • StringBuilder和StringBuffer,字符串是存放在 char[] 中的,char[]是存放在堆中的。
    相比String每次+都重新创建一个String对象,重新开辟一段内存不同,StringBuilder和StringBuffer的append都是直接把String对象中的char[]的字符直接拷贝到StringBuilder和StringBuffer的char[]上,效率比String的+高得多。当然,当StringBuilder和StringBuffer的char[]长度不够时,也会重新开辟一段内存。

  • StringBuilder的内部有一个char[], 在调用StringBuilder的无参构造方法时其内部char[]的默认长度是16。当我们调用StringBuilder的append方法时,其实就是不断的往char[]里填东西的过程。

  • StringBuilder默认长度是16,然后,如果要append第17个字符,怎么办?
    答案是采用 Arrays.copyOf()成倍复制扩容!
    扩容的性能代价是很严重的:一来有数组拷贝的成本,二来原来的char[]也白白浪费了要被GC掉。可以想见,一个129字符长度的字符串,经过了16,32,64, 128四次的复制和丢弃,合共申请了496字符的数组,在高性能场景下,这几乎不能忍。

由此可见,合理设置一个初始值多重要。使用之前先仔细评估一下要保存的字符串最大长度。

所以,在实际开发中,如果基本确定需要添加字符串的长度,就可以自定义长度实例化stringbuilder

StringBuilder a = new StringBuilder(10)
  • StringBuffer和StringBuilder的功能基本一样,只是StringBuffer是线程安全的,而StringBuilder不是线程安全的。因此,StringBuilder的效率会更高。

intern()方法

public native String intern();

1、如果不是用双引号声明的String对象,可以使用String提供的intern方法: intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

  • 比如: String myInfo = new String("I love u").intern();
  • 也就是说,如果在任意字符串上调用String. intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。

2、因此,下列表达式的值必定是true:

("a" + "b" + "c").intern()== "abc"; 通俗点讲,Interned String就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)。

new String会创建几个对象?

  1. new String(“ab”)会创建几个对象?

    • 两个;看字节码就行。

       0 new #2 <java/lang/String>
       3 dup
       4 ldc #3 <ab>
       6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
       9 astore_1
      10 return
      
    • 1:new决定的:在堆空间中分配一块内存,创建一个对象;

    • 2:字符串常量池里面放一个ab。

  2. new String(“a”) + new String(“b”)会创建几个对象?

    0 new #2 <java/lang/StringBuilder>
     3 dup
     4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
     7 new #4 <java/lang/String>
    10 dup
    11 ldc #5 <a>
    13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
    16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    19 new #4 <java/lang/String>
    22 dup
    23 ldc #8 <b>
    25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
    28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
    31 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
    34 astore_1
    35 return
    
    1. new StringBuilder();

    2. new String(“a”)

    3. 常量池里面的a

    4. new String(“b”)

    5. 常量池里面的b

    6. StringBuilder的toString()方法:new String(“ab”)

      toString()方法的调用,在字符串常量池中,没有生成"ab"

相关面试题

/**
 * 如何保证变量s指向的是字符串常量池中的数据呢?
 * 有两种方式:
 * 方式一: String s = "shkstart";//字面量定义的方式
 * 方式二: 调用intern()
 *         String s = new String("shkstart").intern();
 *         String s = new StringBuilder("shkstart").toString().intern();
 */
public class StringIntern {
  public static void main(String[] args) {
    String s = new String("1");
    String s1 = s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
    String s2 = "1";
    //s  指向堆空间"1"的内存地址
    //s1 指向字符串常量池中"1"的内存地址
    //s2 指向字符串常量池已存在的"1"的内存地址  所以 s1==s2
    System.out.println(s == s2);//jdk6:false   jdk7/8:false
    System.out.println(s1 == s2);//jdk6: true   jdk7/8:true
    System.out.println(System.identityHashCode(s));//491044090
    System.out.println(System.identityHashCode(s1));//644117698
    System.out.println(System.identityHashCode(s2));//644117698

    //s3变量记录的地址为:new String("11")
    String s3 = new String("1") + new String("1");
    //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!

    // jdk6:创建了一个新的对象"11",也就有新的地址。
    // jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址
    s3.intern();
    //s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
    String s4 = "11";
    System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
  }
}

拓展

public class StringIntern1 {
  public static void main(String[] args) {
    String s3 = new String("1") + new String("1");//new String("11")
    //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
    String s4 = "11";//在字符串常量池中生成对象"11"
    String s5 = s3.intern();//去常量池里面找有没有"11",没有的话就指向那个引用,有的话就使用有的
    System.out.println(s3 == s4);//false
    System.out.println(s5 == s4);//true
  }
}

总结String中Intern()的使用

jdk6中,将这个字符串对象尝试放入串池

  • 如果串池中有,则不会放入,返回已有的地址
  • 如果没有,将此字符串对象复制一份放入,并返回对象地址

jdk7后,将这个字符串对象尝试放入串池

  • 如果有,不会放入,返回已有的地址
  • 如果没有,将字符串对象的引用地址复制一份放入,返回引用地址
public class StringExer1 {
  public static void main(String[] args) {
    String s = new String("a") + new String("b");//new String("ab")
    //在上一行代码执行完以后,字符串常量池中并没有"ab"

    String s2 = s.intern();//jdk6中:在串池中创建一个字符串"ab"
    //jdk7后:串池中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回

    System.out.println(s2 == "ab");//jdk6:true  jdk7后:true
    System.out.println(s == "ab");//jdk6:false  jdk7后:true
  }
}
public class StringExer1 {
  public static void main(String[] args) {
    String x = "ab";
    String s = new String("a") + new String("b");//new String("ab")
    String s2 = s.intern();

    System.out.println(s2 == x);//true 
    System.out.println(s == x);//false
  }
}
public class StringExer2 {
  public static void main(String[] args) {
    String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成"ab"
    //        String s1 = new String("a") + new String("b");执行完以后,不会在字符串常量池中会生成"ab"
    s1.intern();
    String s2 = "ab";
    System.out.println(s1 == s2); //false
  }
}

1、两个String拼接,因为最后stringBuilder调用了toString方法,所以返回的是一个new String()地址

2、如果常量池里面没有拼接后的值,调用intern方法,常量池中放的是new String()的地址

3、如果常量池有拼接后的值,调用intern方法,就会返回常量池这个值的地址


intern()空间效率

大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用 intern()方法,就会明显降低内存的大小。

G1中的String去重操作

  • 背景:对许多Java应用(有大的也有小的)做的测试得出以下结果:
    • 堆存活数据集合里面String对象占了25%
    • 堆存活数据集合里面重复的String对象有13.5%
    • String对象的平均长度是45
  • 许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用里面**,Java堆中存活的数据集合差不多25%是String对象**。更进一步,这里面差不多一半String对象是重复的,重复的意思是说: string1. equals (string2)=true堆上存在重复的string对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的String对象进行去重,这样就能避免浪费内存。

实现

  • 当垃圾收集器工作的时候,会访问堆上存活的对象。对每一个访问的对象都会检查是否是候选的要去重的String对象。
  • 如果是,把这个对象的一个引用插入到队列中等待后续的处理。一个去重的线程在后台运行,处理这个队列。处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它引用的String对象。
  • 使用一个hashtable来记录所有的被String对象使用的不重复的char数组。 当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组。
  • 如果存在,String对象会被调整引用那个数组,释放对原来的数组的引用,最终会被垃圾收集器回收掉。
  • 如果查找失败,char数组会被插入到hashtable,这样以后的时候就可以共享这个数组了。

命令行选项

  • UseStringDeduplication (bool) :开启String去重,默认是不开启的,需要手动开启。
  • PrintStringDedupl icationStatistics (bool) :打印详细的去重统计信息,
  • StringDedupl icationAgeThreshold (uintx) :达到这个年龄的string对象被认.为是去重的候选对象

今日推歌

----《从前说》

从前对妈妈说谎
翻山越岭只为能与你拥抱一场
幻想 披上红妆
后来你娶了理想 我嫁给了户对门当
爱与不爱又何妨
从前说无惧山海
用九百九十九张车票换有你的未来
只要你在
后来你把我归还人海
哭着说欠我的未来
下辈子再爱

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星回昭以烂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值