查看: 161|回复: 0

Java基础系列2:深入理解String类

[复制链接]
发表于 2020-2-3 00:55:57 | 显示全部楼层 |阅读模式
Java底子系列2:深入理解String类

String是Java中最为常用的数据范例之一,也是口试中比较常被问到的底子知识点,本篇就聊聊Java中的String。重要包括如下的五个内容:

  • String概览
  • “+”连接符剖析
  • 字符串常量池
  • String.intern()方法剖析
  • String、StringBuffer与StringBuilder
String概览

在Java中,全部类似“ABCabc”的字面值,都是String的实例;String类位于java.lang包下,是Java语言的焦点类,提供了字符串的比较、查找、截取、大小写转换等使用;Java语言为“+”连接符以及对象转换为字符串提供了特别支持,字符串对象可以使用“+”连接其他对象。String的部门源码如下:
  1. public final class String    implements java.io.Serializable, Comparable, CharSequence {    /** The value is used for character storage. */    private final char value[];    /** Cache the hash code for the string */    private int hash; // Default to 0    ...}
复制代码
从上面的源码可以看出:

  • String类被final关键字修饰,意味着String类时不可变类,不能被继承,并且其成员value也是final的,因此字符串一旦创建就不能再修改;
  • String类实现了Serializable、CharSequence、Comparable接口;
  • String实例的值是通过字符数组实现字符串存储的。
“+”连接符剖析

“+”连接符的实现原理

Java语言为“+”连接符以及对象转换为字符串提供了特别的支持。其中字符勾通接是通过StringBuilder及其append方法实现的,对象转换字符串是通过toString方法实现的,toString方法由Object类实现,并可被Java中的全部类继承。用个简朴的例子来验证“+”连接符的实现原理:
  1. // 测试代码public class Test {    public static void main(String[] args) {        int i = 2;        String str = "abc";        System.out.println(str + i);    }}// 反编译后public class Test {    public static void main(String args[]) {        byte byte0 = 10;              String s = "abc";              System.out.println((new StringBuilder()).append(s).append(byte0).toString());    }}
复制代码
由反编译后的代码可以看出,Java使用“+”连接字符串对象时,JVM会创建一个StringBuilder对象,并调用其append方法将字符勾通接,最后调用StringBuilder对象的toString方法返回拼接好的字符串。以是在实际代码编写中,使用“+”来拼接字符串和使用StringBuilder对象的append方法来拼接字符串对象是等价的。
“+”连接符的留意事项

“+”的效率

使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部门情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要留意。因为大量StringBuilder创建在堆内存中,必然会造成效率的损失,以是这种情况建议在循环体外创建一个StringBuilder对象调用append方法手动拼接。
字符串常量的优化

编译时可以剖析为常量值另有一种特别情况,当“+”两端均为编译器确定的字符串常量时,编译器会进行优化,直接将两个字符串拼接好。例如:
  1. String s = "hello" + "world!";// 反编译后String s0 = "helloworld!";
复制代码
  1. /** * 编译期确定 * 对于final修饰的变量,它在编译时被剖析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。 * 以是此时的"a" + s1和"a" + "b"结果是一样的。故结果为true。 */String s0 = "ab"; final String s1 = "b"; String s2 = "a" + s1;  System.out.println((s0 == s2)); // true
复制代码
编译时不可以被剖析为常量值
  1. /** * 编译期无法确定 * 这内里虽然将s1用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定 * 因此s0和s2指向的不是同一个对象,故上面步伐的结果为false。 */String s0 = "ab"; final String s1 = getS1(); String s2 = "a" + s1; System.out.println((s0 == s2)); // false public String getS1() {      return "b";   }
复制代码
综上,“+”连接符对于直接相加的字符串常量效率很高,因为在编译期间便确定了它的值,也就是说形如"hello"+"java"; 的字符串相加,在编译期间便被优化成了"Ilovejava"。对于间接相加(即包含字符串引用,且编译期无法确定值的),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。
字符串常量池

字符串常量池介绍

在Java语言中的8种基本范例和String范例,JVM都为它们提供了一种常量池的概念,常量池就类似于一个Java系统级别提供的缓存。8种基本范例的常量池都是系统协调的,String范例的常量池比较特别,它的重要使用方法有两种:

  • 直接使用双引号声明出来的String对象会直接存储在常量池中;
  • 如果不是双引号声明的String对象,可以使用String提供的intern方法。intern方法是个Native方法,会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。
内存区域

在HotSpot VM中字符串常量池是通过一个StringTable类实现的,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例中只有一份,被全部的类共享;字符串常量由一个一个字符组成,放在了StringTable上。要留意的是,如果放进String Pool的String非常多,就会造成Hash冲突严肃,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。在JDK6及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中的,StringTable的长度是固定的1009;在JDK7版本中,字符串常量池被移到了堆中,StringTable的长度可以通过-XX:StringTableSize=66666参数指定。至于JDK7为什么把常量池移动到堆上实现,缘故起因可能是由于方法区的内存空间太小且不方便扩展,而堆的内存空间比较大且扩展方便。
内存的分配

在JDK6及之前版本中,String Pool里放的都是字符串常量;在JDK7.0中,由于String.intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用。请看如下代码:
  1. String s1 = "ABC";String s2 = "ABC";String s3 = new String("ABC");System.out.println(s1 == s2); // trueSystem.out.println(s1 == s3); // falseSystem.out.println(s1.intern() == s3.intern()); // true
复制代码
由于常量池中不存在两个相同的对象,以是s1和s2都是指向JVM字符串常量池中的"ABC"对象。new关键字一定会产生一个对象,并且这个对象存储在堆中。以是String s3 = new String(“ABC”);产生了两个对象:保存在栈中的s3和保存在堆中的String对象。当执行String s1 = "ABC"时,JVM首先会去字符串常量池中查抄是否存在"ABC"对象,如果不存在,则在字符串常量池中创建"ABC"对象,并将"ABC"对象的地址返回给s1;如果存在,则不创建任何对象,直接将字符串常量池中"ABC"对象的地址返回给s1。由于s1,s2,s3的字符串值都是在常量池中的同一个引用,以是intern()方法的返回值是相当的。
String.intern()方法剖析

String.intern()方法剖析

先来看一下String.intern()方法的代码和注释:
  1. /**     * Returns a canonical representation for the string object.     *
  2.      * A pool of strings, initially empty, is maintained privately by the     * class {@code String}.     *
  3.      * 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.     *
  4.      * 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}.     *
  5.      * All literal strings and string-valued constant expressions are     * interned. String literals are defined in section 3.10.5 of the     * The Java™ Language Specification.     *     * @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();
复制代码
直接使用双引号声明出来的String对象会直接存储在字符串常量池中,如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中,之后再返回。JDK1.7的改动将String常量池 从 Perm 区移动到了 Java Heap区String.intern() 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。
String.intern()的使用

来看看使用和不使用intern()的执行过程,在用new String("ABC")实例化String对象的时候,如果使用了intern方法,那么会先去字符串常量池中去查找是否有值为"ABC"的字符串,找到了就不会创建新的"ABC"字符串,找不到才会去创建新的"ABC"字符串;如果不使用intern方法,则没有去常量池查找的过程,会直接创建新的"ABC"字符串。可以看出二者的区别是:

  • 使用intern(),实际创建的对象数目是少于需要创建的对象数目的,因为会有常量池的字符串共享;但相应的,所需要的常量池的查询斲丧会增加时间损耗;这表现出的是一种空间友好,不需要太多gc来接纳空间;
  • 不使用intern(),实际需要多少对象,就会创建多少对象,因此会有大量的重复值的String对象出现;但相应的,少了查询的斲丧,时间损耗会少一些;这表现出的是一种时间友好。
String、StringBuffer与StringBuilder

类图


https://www.cnblogs.com/file:///Users/wangjinlong/Documents/Dali%E7%8E%8B%E7%9A%84%E6%8A%80%E6%9C%AF%E5%8D%9A%E5%AE%A2/assets/String-class-layout.png

重要区别


  • String是不可变字符序列,StringBuilder和StringBuffer是可变字符序列;
  • StringBuilder是非线程安全的,StringBuffer是线程安全的,其线程安满是通过在成员方法上添加synchronized关键字来实现的;
  • 执行效率上,StringBuilder > StringBuffer > String
总结

综上,我们再通过一个例子来测验以上的学习成果:
  1. String s1 = "AB";String s2 = new String("AB");String s3 = "A";String s4 = "B";String s5 = "A" + "B";String s6 = s3 + s4;System.out.println(s1 == s2); // falseSystem.out.println(s1 == s2.intern()); // trueSystem.out.println(s1 == s5); // trueSystem.out.println(s1 == s6); // falseSystem.out.println(s1 == s6.intern()); // true
复制代码
要理解此标题,需要搞清晰以下三点:

  • 直接使用双引号声明出来的String对象会直接存储在常量池中;
  • String对象的intern方法会得到字符串对象在常量池中对应的引用,如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;
  • 字符串的+使用其本质是创建了StringBuilder对象进行append使用,然后将拼接后的StringBuilder对象用toString方法处置处罚成String对象。
看一下以上的6个String对象在内存的分布情况:
https://www.cnblogs.com/file:///Users/wangjinlong/Documents/Dali%E7%8E%8B%E7%9A%84%E6%8A%80%E6%9C%AF%E5%8D%9A%E5%AE%A2/assets/String-value-inner.png






参考资料】https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.htmlhttps://docs.oracle.com/javase/8/docs/api/https://blog.csdn.net/ifwinds/article/details/80849184
关注我的公众号,获取更多关于口试、技术的文章及福利资源。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?用户注册

x

相关技术服务需求,请联系管理员和客服QQ:2753533861或QQ:619920289
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

帖子推荐:
客服咨询

QQ:2753533861

服务时间 9:00-22:00

快速回复 返回顶部 返回列表