原创

深入解析String,StringBuilder,StringBuffer异同

温馨提示:
本文最后更新于 2024年07月21日,已超过 271 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我
  • String 字符串常量

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    }
    

    String其实是一经定义就不能改变的char数组。

    下面看下String类中的几个方法:

    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
    
    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }
    
    public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */
    
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }
    

    从上面三个方法可以看出,无论是进行substring,concat还是replace操作,都不会在原String上操作,而是重新生成了一个新的字符串对象。也就是说,进行这些操作后,一开始的字符串是没有改变的。

    在这永远要记住一点:对String对象的任何改变都不会影响到原对象,相关的任何更改操作都会生成新对象。

  • StringBuffer 字符串变量(线程安全)

  • StringBuilder 字符串变量(线程不安全)

  • 既然Java中已经有String类了,为什么还要StringBuilder和StringBuffer类呢?

    • 看这段代码:

      class StringDemo{
      
          public static void main(String[] args){
              String s = "";
              for(int i = 0; i < 6666; i++){
                  s += "fxyh";
              }
          }
      }
      
      • 这句s += "fxyh";的过程相当于将原有的s变量指向新的内容中的对象,然后再进行字符串相加操作,再把这个引用重新指向s变量。
      • 看反编译的代码:String01.png
      • 这段反编译的class文件可以很清除的看出:整个循环的执行过程,并且每次循环都会new出一个StringBuilder对象,然后进行append操作,最后toString方法返回了String对象,也就是说,这个循环完毕后new了6666个对象,想一下,如果这些对象没有被回收,会造成多大的内存浪费。

      • 从反编译后的代码可以看出s += "fxyh";的操作实际自动被JVM优化成了下面这个代码:

        StringBuilder str = new StringBuilder(s);
        str.append("fxyh");
        str.toString();
        
    • 再看下这段代码:
      class StringDemo{
          public static void main(String[] args){
              StringBuilder sb = new StringBuilder();
              for(int i = 0; i < 6666; i++){
                  sb.append("fxyh");
              }
          }
      }
      
      • 看反编译的代码:
        String02.png
      • 从这里可以明显看出,这段代码的for循环,一直是对new 的StringBuilder对象进行append操作,而且这里只new了一次,因此在循环了这么多次后,这段代码所占的资源明显比上面的小很多。
    • 那么既然有了StringBuilder,为啥还要StringBuffer类,看了源码就明白了,StringBuffer类的成员方法和属性和StringBuilder基本一样,但是StringBuffer的成员方法多了一个关键字:synchronized,这个关键字可以在多线程中起到安全保护作用,也就是StringBuffer是线程安全的。
  • 下面看几个测试方法,让我们更深入了解一下。

    package com.fxyh.stringtest;
    
    public class Test1 {
    
        public static void main(String[] args) {
            testString1();
            testStringBuffer();
            testStringBuilder();
            testString2();
            testString3();
        }
    
        public static void testString1() {
            String s = "";
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100000; i++) {
                s += "fxyh";
            }
            long end = System.currentTimeMillis();
            System.out.println("testString1使用时间为:" + (end - start) + "毫秒");
        }
    
        public static void testStringBuffer() {
            StringBuffer sb = new StringBuffer();
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100000; i++) {
                sb.append("fxyh");
            }
            long end = System.currentTimeMillis();
            System.out.println("testStringBuffer使用时间为:" + (end - start) + "毫秒");
        }
    
        public static void testStringBuilder() {
            StringBuilder sb = new StringBuilder();
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100000; i++) {
                sb.append("fxyh");
            }
            long end = System.currentTimeMillis();
            System.out.println("testStringBuilder使用时间为:" + (end - start) + "毫秒");
        }
    
        public static void testString2() {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100000; i++) {
                String s = "I"+"am"+"fxyh";
            }
            long end = System.currentTimeMillis();
            System.out.println("字符串直接相加操作使用时间为:" + (end - start) + "毫秒");
        }
        public static void testString3() {
            String s1 = "I";
            String s2 = "am";
            String s3 = "fxyh";
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100000; i++) {
                String s = s1 + s2 + s3;
            }
            long end = System.currentTimeMillis();
            System.out.println("字符串间接相加操作使用时间为:" + (end - start) + "毫秒");
        }
    
    }
    
    • 测试结果:String03.png

    • 我们刚才说s+="fxyh";JVM会自动优化,看下面的这段代码

      package com.fxyh.stringtest;
      
      public class Test1 {
      
          public static void main(String[] args) {
              testString1();
              testString();
          }
          public static void testString1() {
              String s = "";
              long start = System.currentTimeMillis();
              for (int i = 0; i < 100000; i++) {
                  s += "fxyh";
              }
              long end = System.currentTimeMillis();
              System.out.println("testString1使用时间为:" + (end - start) + "毫秒");
          }
      
          public static void testString(){
              String s = "";
              long start = System.currentTimeMillis();
              for (int i = 0; i < 100000; i++) {
                  StringBuilder sb = new StringBuilder(s);
                  sb.append("fxyh");
                  s=sb.toString();
              }
              long end = System.currentTimeMillis();
              System.out.println("模拟jvm优化操作使用时间为:" + (end - start) + "毫秒");
          }
      }
      
    • 测试结果:String04.png

    • 两个执行时间,基本是一样,同样也验证了s+="fxyh";JVM会自动优化。
  • 下面对上面的执行结果进行一些推理:

    • 对于直接相加的字符串,效率很高,因为在编译的时候,便已经确定了值,即"I"+"am"+"fxyh";这样的字符串相加,在编译的时候被优化成了“Iamfxyh”。
    • 反编译后:String05.png
    • 对于间接相加,形如s1 + s2 + s3;效率要比直接相加底,因为编译器不会对引用变量进行优化。
    • 反编译后:String06.png
    • String、StringBuilder、StringBuffer三者的执行效率:
      • StringBuilder>StringBuffer>String
      • 这个也不是一定的:比如String s = "I" + "am" + "fxyh"的效率就比StringBuilder sb = new StringBuilder().append("I").append("am").append("fxyh");要高。
      • 所以我们要更具适当的情况,选用不同的对象。
      • 当字符串相加操作或者改动少的情况下,建议使用String s = "fxyh";这种形式;
      • 当字符串相加较多的情况下,建议使用StringBuilder,如果是多线程的话,则使用StringBuffer。
  • 再来踩几个坑:

    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;  在编译的时候不会优化成“hello2”,因此这种方式生成的对象是保存在堆上。所以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;
      

个人的一些见解,若有不对的地方,希望大家能帮我指正。

安利一波国内在维护的一款Linux系统-deepin,深度,好用!好用!好用!

正文到此结束