String深入浅出

本文详细介绍了Java中String类的基本特性,包括字符串的不可变性、JDK1.8和1.9底层实现的区别、字符串常量池的工作原理以及内存分配。讨论了字符串拼接的效率问题,重点解析了intern()方法的使用,并通过实例展示了其在不同JDK版本中的行为差异。此外,文章还涉及了字符串对象的创建和内存占用,以及与性能优化相关的知识点。
摘要由CSDN通过智能技术生成

  • String ,字符串常量,String类中使用 final 关键字修饰字符数组保存字符串 private final char value[],String对象是不可变的,也就理解为常量,线程安全。如果要操作少量的数据用 String。
  • StringBuffer,字符串变量,对方法加了同步锁或者对调用的方法加了同步锁,线程安全。多线程操作字符串缓冲区下操作大量数据用StringBuffer。
  • StringBuilder,字符串变量,非线程安全的,单线程操作字符串,缓冲区下操作大量数据用 StringBuilder。

一、String的基本特性


  • String :字符串,使用一对双引号(“”)引起来表示。
    • String s1 = “abc”; // 字面量的定义方式
    • String s2 = new String(“abc”);
  • String 类声明为 final的,不可被继承。
  • String 类实现了
    • Serializable 接口:表示字符串 是支持序列化的。
    • Comparable 接口:表示String可以比较大小。
  • String 在jdk8及以前内部定义了 final char[] value 用于存储字符串数据。jdk9时修改为 byte[]
  • 字符串常量池中是不会存储相同内容的字符串的

1.1、String的不可变性

String的不可变性

String :代表不可变的字符序列。简称:不可变性

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

通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

String为什么不可变,有什么好处?

  1. 字符数组被 private关键字修饰,并且String没有暴露和提供任何修改字符数组的方法。
  2. String类被 final 关键字修饰,代表其不可被继承,从而杜绝了 子类覆盖父类行为的可能。

在这里插入图片描述

好处:

  • 只有String不可变了,字符串常量池才能发挥作用。
    用字面量创建字符串时,字符串常量池会返回已有对象的引用,如果字符串可变,那引用的值就可以随时修改并能随时影响到其他的引用,从而数据会发生各种错误,这样常量池还谈何复用呢?
  • String 不可变,保证了它的哈希码也不可变。因此计算一次哈希码后就可以将其缓存,再用到时就无需计算了,性能更高,也正是由于其哈希码不会变,所以能够放心地去使用和哈希计算相关的对象,比如 HashMap、HashSet。

笔试题

在这里插入图片描述


1.2、String在JDK1.8和1.9中底层的差别

String在JDK1.8和1.9中底层实现的那点事儿

String在JDK1.8中的底层实现,如下:(char数组)

private final char value[];

String在JDK1.9中的底层实现,如下:(byte数组)

@Stable //表示下方属性 最多被赋值1次!
private final byte[] value;

为什么呢?

  • 开发者发现人们在使用字符串的时候,多数使用的是拉丁文,而拉丁文所占字节数为1。JDK1.8底层实现是使用的char数组,一个char类型所占字节数为2,所以在char数组存储拉丁文的时候浪费了一个字节的内存空间。往往我们在使用字符串的时候都是大量使用的,浪费一个字节的内存空间必然会导致GC频繁,性能低下。所以开发者在JDK1.9将char数组改为了byte数组!
    在这里插入图片描述

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

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

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

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

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

  • 在jdk7中,StringTable的长度默认值是 60013,StringTableSize设置没有要求。

  • jdk8开始,StringTableSize可设置的最小值是:1009



二、String的内存分配


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

Java 7 中 ,将字符串常量池的位置调整到Java

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

StringTable (字符串常量池)为什么要调整?

  1. permSize默认比较小,因为永久代空间比较小
  2. 永久代垃圾回收频率低
    jdk7 中将 字符串常量池 放到了堆空间中。因为永久代的回收效率很低,在Full GC的时候才会触发。而 full gc是老年代的空间不足、永久代不足时才会触发。这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。

放到堆里,能及时回收内存



三、字符串拼接操作


  1. 常量常量的拼接结果在常量池,原理是编译期优化
    字符串常量(“a”) 或者 常量引用(final String s = “a”)
  2. 常量池中不会存在相同内容的常量
  3. 只要其中有一个是变量,结果就在****中。变量拼接的原理是 StringBuilder
  4. 如果拼接的结果调用 intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。若有的话,返回此对象地址。

代码演示:

@Test
public void test5(){
    String s1 = "javaEEhadoop";
    String s2 = "javaEE";
    String s3 = s2 + "hadoop";
    System.out.println(s1 == s3);//false

    final String s4 = "javaEE";//s4:常量
    String s5 = s4 + "hadoop";
    System.out.println(s1 == s5);//true

}

说明:

  • s1 == s3
    • s1 执行后已将 javaEEhadoop 存放在常量池中。
    • s3 等于 变量s2 拼接 “hadoop”,因为s2是变量,所以拼接结果是放在堆中。
    • s1、s3引用的对象在内存中的位置不同,故为 false。
  • s1 == s5
    • s5 等于 常量引用s4 拼接 “hadoop”,因为s4是常量,所以在s4指向了常量池中"javaEEhadoop"对象
    • s1、s5 引用的同一个对象,即常量池中的 “javaEEhadoop” ,故为 true。

StringBuilder的append()的方式添加字符串 和 String的字符串拼接方式 执行效率比较

体会执行效率:通过StringBuilder的append()的方式添加字符串的效率要远高于使用String的字符串拼接方式!
详情:

  • ① StringBuilder的append()的方式:自始至终中只创建过一个StringBuilder的对象
    使用String的字符串拼接方式:创建过多个StringBuilder和String的对象
  • ② 使用String的字符串拼接方式:内存中由于创建了较多的StringBuilder和String的对象,内存占用更大;如果进行GC,需要花费额外的时间。


五、intern()的使用


/**
 * Returns a canonical representation for the string object.
 * <p>
 * A pool of strings, initially empty, is maintained privately by the
 * class {@code String}.
 * <p>
 * When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.
 * <p>
 * It follows that for any two strings {@code s} and {@code t},
 * {@code s.intern() == t.intern()} is {@code true}
 * if and only if {@code s.equals(t)} is {@code true}.
 * <p>
 * All literal strings and string-valued constant expressions are
 * interned. String literals are defined in section 3.10.5 of the
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * @return  a string that has the same contents as this string, but is
 *          guaranteed to be from a pool of unique strings.
 */
public native String intern();

首先查看官方API的解释:intern

public String intern()

返回字符串对象的规范化表示形式。

一个初始时为空的字符串池,它由类 String 私有地维护。

当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。

它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

所有字面值字符串和字符串赋值常量表达式都是内部的。

返回:

一个字符串,内容与此字符串相同,但它保证来自字符串池中。


Intern()的使用

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

  • 比如:String myInfo = new String("abc").intern();

也就是说,如果在任意字符串上调用 String.intern 方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是 true:

("a" + "b" + "c").intern() == "abc"

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

jdk6 vs jdk7/8

总结String的intern()的使用:

  • Jdk1.6中,将这个字符串对象尝试放入字符串常量池。
    • 如果字符串常量池中有,则并不会放入。返回已有的字符串常量池中的对象的地址。
    • 如果没有,会把 此对象复制一份,放入字符串常量池,并返回字符串常量池中的对象地址。
  • Jdk1.7中,将这个字符串对象尝试放入字符串常量池。
    • 如果字符串常量池中有,则并不会放入。返回已有的字符串常量池中的对象的地址。
    • 如果没有,会把 此对象的引用地址复制一份,放入字符串常量池,并返回字符串常量池中的对象地址。

面试题

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

通过看字节码,就知道是两个。

  • 一个对象是:new 关键字在堆空间创建的
  • 另一个对象是:字符串常量池中的对象。

2、new String(“a”) + new String(“b”) 呢?

  1. 对象1:new StringBuilder()
  2. 对象2:new String(“a”)
  3. 对象3:常量池中的"a"
  4. 对象4:new String(“b”)
  5. 对象5:常量池中的"b"
  6. 对象6:StringBuilder的toString()中又 new String(“ab”)

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

关于 intern()的面试难题

在这里插入图片描述

String s = new String("1"); 
s.intern();                     
String s2 = "1";                    
System.out.println(s == s2);       

说明:

  1. 在执行完第一条语句之后,创建了两个对象
    • 一个对象是:new 关键字在堆空间创建的
    • 另一个对象是:字符串常量池中的对象。
  2. 执行s.intern(); 语句的时候,常量池中包含了该对象故返回了该对象的引用,但并没有变量去接收。故仅仅是返回了字符串常量池中的引用。s仍指向的是堆中空间中创建的对象。
  3. 第三条语句,因常量池中有"1",故引用常量池对象。
String s3 = new String("1") + new String("1");  
s3.intern();                    
String s4 = "11";               
System.out.println(s3 == s4);   //jdk6:false  jdk7/8:true

说明:

  1. String s3 = new String(“1”) + new String(“1”);
    根据以上字符串拼接操作中的知识我们知道,其处理结果是放进了。常量池中并没有"11"

  2. s3.intern(); 因为常量池中没有包含该对象,故在字符串常量池中生成"11",但并没有变量去接收

    • jdk6: 在字符串常量池中创建了一个新的对象"11",也就有新的地址。
    • jdk7: 此时常量中并没有创建"11",而是创建一个指向堆空间中new String(“11”)的地址。因为这里jdk7之后,字符串常量池是放在了堆中。这里有一个空间节省策略,字符串常量池中并没有创建字符串,而是创建了一个引用,指向 堆中的"11"
  3. s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址。

  4. s3 == s4

    • jdk6:

      • s3 指向的是 堆中
      • s4 指向的是 字符串常量池

      故为 fase

    • Jdk7/8

      • s3 和 s4 指向的是同一个 堆中的对象

      故为 true

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值