zoukankan      html  css  js  c++  java
  • 5、String、StringBuffer、StringBuilder

    String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类(不可变类),被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

    StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,它是 Java 1.5 中新增的,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。

    StringBuilder 在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选线程不安钱

    要知道String 是 Immutable 的,字符串操作不当可能会产生大量临时字符串,以及线程安全方面的区别。

    还要关注几个地方:

    1.通过 String 和相关类,考察基本的线程安全设计与实现,各种基础编程实践。

    2.考察 JVM 对象缓存机制的理解以及如何良好地使用。

    3.考察 JVM 优化 Java 代码的一些技巧。

    4.String 相关类的演进,比如 Java 9 中实现的巨大变化。

    1. 字符串设计和实现考量

    String 是 Immutable 类的典型实现,原生的保证了基础线程安全,因为你无法对它内部数据进行任何修改,这种便利甚至体现在拷贝构造函数中,由于不可变,Immutable 对象在拷贝时不需要额外复制数据。如调用函数时的值传递

    StringBuffer,它的线程安全是通过把各种修改数据的方法都加上 synchronized 关键字实现的,非常直白。其实,这种简单粗暴的实现方式,非常适合我们常见的线程安全类实现,不必纠结于 synchronized 性能之类的,有人说“过早优化是万恶之源”,考虑可靠性、正确性和代码可读性才是大多数应用开发最重要的因素。

    为了实现修改字符序列的目的,StringBuffer 和 StringBuilder 底层都是利用可修改的数组(char数组,JDK 9 以后是 byte数组),二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized。

    (byte数组:字节数组,如int类型可以表示为byte[4],int类型为4个字节,byte数组的一项为一个字节,可以表示8bit位,也就是最大是-128-127范围的有符号整数)

    (char占两个字节,大小为0-65535,为16bit位的无符号类型,不能表示负数)

    (char可以表示中文字符,byte不可以)

    这个内部数组应该创建成多大的呢?如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行 arraycopy。

    如:

    String strByConcat = "aa" + "bb" + "cc" + "dd";

    反编译后回发现,在JDK8中,字符串拼接操作会自动被 javac 转换为 StringBuilder 操作。而在 JDK 9 里面则是因为 Java 9 为了更加统一字符串操作优化,提供了 StringConcatFactory,作为一个统一的入口。javac 自动生成的代码,虽然未必是最优化的,但普通场景也足够了,你可以酌情选择。

    2. 字符串缓存(常量池)

    如果能避免创建重复字符串,可以有效降低内存消耗和对象创建开销

    String 在 Java 6 以后提供了 intern() 方法(JVM会把字符串添加进常量池中,并返回其在常量池中的地址),目的是提示 JVM 把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用 intern() 方法的时候,如果已经有缓存(字符串常量池)的字符串,就会返回缓存(字符串常量池)里的实例,否则将其缓存起来(放入字符串常量池)。一般来说,JVM 会将所有的类似“abc”这样的文本字符串,或者字符串常量之类缓存起来。

    一般使用 Java 6 这种历史版本,并不推荐大量使用 intern,因为被缓存的字符串是存在所谓 PermGen (永久代,存在于方法区,当时的字符串常量池还在方法区中)里的,也就是臭名昭著的“永久代”,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以,如果使用不当,OOM 就会光顾。在后续版本中,这个缓存被放置在堆中(字符串常量池移到堆中),这样就极大避免了永久代占满的问题,甚至永久代在 JDK 8 中被 MetaSpace(元数据区,存在于本地内存)替代了。而且,默认缓存大小也在不断地扩大中。

    Intern 是一种显式地排重机制,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的;另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践。


    由于String在Java世界中使用过于频繁,Java为了避免在一个系统中产生大量的String对象,引入了字符串常量池。其运行机制是:创建一个字符串时,首先检查池中是否有值相同的字符串对象,如果有则不需要创建直接从常量池中刚查找到的对象引用;如果没有则新建字符串对象,返回对象引用,并且将新创建的对象放入常量池中。但是,通过new方法创建的String对象是不检查字符串常量池的,而是直接在堆区或栈区创建一个新的对象,也不会把对象放入常量池中,通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。上述原则只适用于通过直接量给String对象引用赋值的情况。

    举例:String str1 = "123"; //通过直接量赋值方式,放入字符串常量池
    String str2 = new String(“123”);//通过new方式赋值方式,不放入字符串常量池,放在堆中。在堆区进行对象生成的过程是不会去检测该对象是否已经存在的

    注意:String提供了intern()方法。调用该方法时,如果常量池中包括了一个等于此String对象的字符串(由equals方法确定,内容相同),则返回常量池中的字符串引用。否则,将此String对象添加到常量池中,并且返回此池中对象的引用。

    几张图轻松理解String.intern():

    https://blog.csdn.net/soonfly/article/details/70147205

    1)对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"I"+"love"+"java"; 的字符串相加,在编译期间便被优化成了"Ilovejava"。这个可以用javap -c命令反编译生成的class文件进行验证。对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。

     JDK 1.6  与 JDK 1.7中intern()得区别:

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

    举例说明:

    String str2 = new String("str")+new String("01");
    str2.intern();
    String str1 = "str01";
    System.out.println(str2==str1);

    在JDK 1.7下,当执行str2.intern();时,因为常量池中没有“str01”这个字符串,所以会在常量池中生成一个对堆中的“str01”的引用(注意这里是引用 ,就是这个区别于JDK 1.6的地方。在JDK1.6下是生成原字符串的拷贝),而在进行String str1 = “str01”;字面量赋值的时候,常量池中已经存在一个引用,所以直接返回了该引用,因此str1和str2都指向堆中的同一个字符串,返回true。若在JDK1.6下,执行str2.intern();时,会在常量池中生成这个“str01”字符串,之后String str1 = “str01”;字面量赋值的时,str1指向常量池中的这个字符串,str2指向堆中的对象,此时应该会返回false。

    String str2 = new String("str")+new String("01");
    String str1 = "str01";
    str2.intern();
    System.out.println(str2==str1);

    将中间两行调换位置以后,因为在进行字面量赋值(String str1 = “str01″)的时候,常量池中不存在,所以str1指向的常量池中的位置,而str2指向的是堆中的对象,再进行intern方法时,对str1和str2已经没有影响了,所以返回false。

    若  String str3 = str2.intern(); 那么str3 == str1 将返回true。因为 str2.intern()时,常量池已经存在"str01"字符串,则会返回该字符串在常量池中的地址引用给str3,故相等。

    3.String 自身的演化

    在历史版本中,String是使用 char 数组来存数据的,这样非常直接。但是 Java 中的 char 是两个 bytes 大小,拉丁语系语言的字符,根本就不需要太宽的 char,这样无区别的实现就造成了一定的浪费。

    Java 9 中,引入了 Compact Strings 的设计,对字符串进行了大刀阔斧的改进。将数据存储方式从 char 数组,改变为一个 byte 数组加上一个标识编码的所谓 coder,并且将相关字符串操作类都进行了修改。另外,所有相关的 Intrinsic 之类也都进行了重写,以保证没有任何性能损失。虽然底层实现发生了这么大的改变,但是 Java 字符串的行为并没有任何大的变化,所以这个特性对于绝大部分应用来说是透明的,绝大部分情况不需要修改已有代码。在通用的性能测试和产品实验中,我们能非常明显地看到紧凑字符串带来的优势,即更小的内存占用、更快的操作速度

    例题:

    1. 下面这段代码的输出结果是什么?

      String a = "hello2";   String b = "hello" + 2;   System.out.println((a == b));

      输出结果为:true。原因很简单,"hello"+2在编译期间就已经被优化成"hello2",因此在运行期间,变量a和变量b指向的是同一个对象。

    2.下面这段代码的输出结果是什么?

      String a = "hello2";    String b = "hello";       String c = b + 2;       System.out.println((a == c));

      输出结果为:false。由于有符号引用的存在,所以  String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,因此这种方式生成的对象事实上是保存在堆上的,实际上是使用StringBuilder.append来完成。因此a和c指向的并不是同一个对象。

    3.下面这段代码的输出结果是什么?

      String a = "hello2";     final String b = "hello";       String c = b + 2;       System.out.println((a == c));

      输出结果为:true。对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = "hello" + 2; 

    4.下面这段代码输出结果为:

     1 public class Main {
     2     public static void main(String[] args) {
     3         String a = "hello2";
     4         final String b = getHello();
     5         String c = b + 2;
     6         System.out.println((a == c));
     7     }
     8      
     9     public static String getHello() {
    10         return "hello";
    11     }
    12 }

    输出结果为false。这里面虽然将b用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此a和c指向的不是同一个对象。

    5.下面这段代码的输出结果是什么?

    6.String str = new String("abc")创建了多少个对象?

    这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念  该段代码执行过程类的加载过程是有区别的。类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中又在堆中创建了一个String对象。

      因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个

      个人觉得在面试的时候如果遇到这个问题,可以向面试官询问清楚”是这段代码执行过程中创建了多少个对象还是涉及到多少个对象“再根据具体的来进行回答。

    String a=new String("b"+"c")会创建几个对象?

    https://blog.csdn.net/netot/article/details/53324560

    7.下面这段代码1)和2)的区别是什么?

    http://www.cnblogs.com/dolphin0520/p/3778589.html

    Q:下列程序的输出结果: 
    String s1 = “abc”; 
    String s2 = “a”; 
    String s3 = “bc”; 
    String s4 = s2 + s3; 
    System.out.println(s1 == s4); 
    A:false,因为s2+s3不会在编译器优化,实际上是使用StringBuilder.append来完成,会生成不同的对象,堆中生成。

     Q:下列程序的输出结果: 
    String s = new String(“abc”); 
    String s1 = “abc”; 
    String s2 = new String(“abc”); 
    System.out.println(s == s1.intern()); 
    System.out.println(s == s2.intern()); 
    System.out.println(s1 == s2.intern()); 
    A:false,false,true。因为s和s2是在堆中新生成的字符串,而s1则会在常量池中生成“abc”,之后执行s2.intern()时,因为常量池中已经有“abc”了,故s2.intern()会返回指向常量池中“abc”的引用,所以s1 == s2.intern()为true

  • 相关阅读:
    dede list调用 内容模型 附件
    dedecms list标签调用附加表字段--绝对成功
    织梦系统站点首页、列表、文章页等页面点击数调用方法
    apache include 文件包含引用的方法 报错 [an error occurred while processing this directive]
    windows 2008 中IIS7.0以上如何设置404错误页面
    织梦Fatal error: Call to a member function GetInnerText()
    dedecms清空栏目后,新建ID不从1开始的解决方法
    js禁止
    AngularJs表单验证
    submile 安装,汉化,插件
  • 原文地址:https://www.cnblogs.com/xuan5301215/p/9072739.html
Copyright © 2011-2022 走看看