• 【Java】String、StringBuilder和StringBuffer


    【String】

    首先,从String类的定义入手,可以看到String类是由final修饰,即不可变的,一旦创建出来就不可修改,因此首先明确,字符串的拼接、截取等操作都会产生新的字符串对象。

     

    观察以下几种创建Stirng的语句

    1 String s1 = "hello ";
    2 String s2 = "world";
    3 String s3 = s1 + s2;
    4 String s4 = "hello " + "world";
    5 String s5 = new String("hello world");

    1> 对于s1,直接通过字符串常量创建,会先查找常量池中是否有 hello 这个字符串,有的话不创建,没有的话新建,因此会在常量池中创建一个对象。

    2> 对于s2,同上,在常量池中创建一个对象。

    3> 对于s3,字符串拼接操作,查看反汇编代码,字符串拼接实际上是new StringBuilder(),然后执行其append()方法,最后执行toString,过程中创建了3个对象。

    4> 对于s4,直接通过常量字符串拼接,会被优化为创建 hello world,在常量池中创建对象。

    5> 对于s5,new操作一定会在堆中创建对象,然后查找常量池中是否有 hello world 这个字符串,没有就在常量池中创建一个。

    对于s3、s4、s5,字符串的内容都是hello world,但对于s3和s5,引用的都是堆中创建的字符串对象,s4引用的则直接是常量池中的对象,因此它们三个的引用都是不同的,如下

    intern

    通过上面的验证,知道直接通过常量定义的String对象是位于常量池中,而通过new显式创建或者拼接隐式创建的String对象是位于堆中。

    除了常用的操作字符串方法,JDK还提供了 inter() 方法,该方法的官方说明如下,作用是当方法调用时,如果常量池中已经包含一个和这个String相等(内容相同,即equals方法)的String,就会返回常量池中的对象,否则,这个对象会被添加到常量池,然后返回她的引用。简单来说其作用是把字符串缓存到常量池中。

    通过几个例子来验证intern的作用

    【StringBuilder和StringBuffer】

    StringBuilder和StringBuffer都表示可变的字符串序列,可以通过其提供的一序列方法实现字符串的拼接、截取,观察源码会发现两个类的核心代码基本一致,二者都继承自抽象类AbstractStringBuilder。

    • 二者的共同点是均提供了字符串的拼接、截取等操作,无需每次变动都创建新的对象,一次创建,最后只需通过toString生成最终的字符串对象即可。

    • 二者的区别在于在字符串拼接等操作上StringBuffer使用synchronized关键字修饰方法,多线程情况下保证了线程安全,当然相比StringBuilder,也降低了性能。

    通过源码来看动态拼接字符串逻辑,StringBuilder和StringBuffer都是调用父类的append方法实现,因此二者实现逻辑是一致的,其中count是用来记录value数组已经使用的长度,即在现有的value数组后面拼接上新的str字符串。因此在StringBuilder中,代码中的 count+=len 不是原子操作,多线程操作的情况下,例如当前count为5,可能出现多个线程拿到的count都是5,分别执行累加操作后再赋给count,得到的count只是6,此时便出现了线程不安全的情况。

    附:虽然上面提到,通过 + 号拼接String对象时,java也会隐式地创建StringBuilder对象进行拼接,通常情况下并不会造成性能效率损失,但在需要大量循环拼接字符串时,使用+拼接,会在每次循环时都创建一个StringBuilder对象,循环结束后,内存中会多出许多无用的StringBuilder对象,因此这种情况,建议在循环体外创建StringBuilder对象,循环调用其append()方法拼接,如此只需创建一个StringBuilder对象。

    扩容

    首先注意二者的初始容量都是16,当然也可以通过参数指定初始容量

    1 public StringBuilder() {
    2         super(16);
    3     }
    4 public StringBuffer() {
    5         super(16);
    6     }

    在字符串拼接时,都会调用父类AbstractStringBuilder的append方法,方法源码如下

     1 public AbstractStringBuilder append(String str) {
     2         if (str == null) {
     3             return appendNull();
     4         }
     5         int len = str.length();
     6         ensureCapacityInternal(count + len);
     7         putStringAt(count, str);
     8         count += len;
     9         return this;
    10     }

    其中的 ensureCapacityInternal 方法,就是在拼接前进行扩容

    1 private void ensureCapacityInternal(int minimumCapacity) {
    2         // overflow-conscious code
    3         int oldCapacity = value.length >> coder;
    4         if (minimumCapacity - oldCapacity > 0) {
    5             value = Arrays.copyOf(value,
    6                     newCapacity(minimumCapacity) << coder);
    7         }
    8     }

    其中的minimumCapacity就是扩容后的字符串长度,超过目前容量的话,调用newCapacity方法执行扩容,可以看出每次扩容后的容量是原容量的2倍加2,如果仍不够,就直接扩容到需要的长度。

     1 private int newCapacity(int minCapacity) {
     2         // overflow-conscious code
     3         int oldCapacity = value.length >> coder;
     4         int newCapacity = (oldCapacity << 1) + 2;
     5         if (newCapacity - minCapacity < 0) {
     6             newCapacity = minCapacity;
     7         }
     8         int SAFE_BOUND = MAX_ARRAY_SIZE >> coder;
     9         return (newCapacity <= 0 || SAFE_BOUND - newCapacity < 0)
    10             ? hugeCapacity(minCapacity)
    11             : newCapacity;
    12     }
  • 相关阅读:
    Android UI组件之自定义控件实现IP地址控件
    封装一个类搞定90%安卓客户端与服务器端交互
    深入理解 RecyclerView 系列之:ItemDecoration
    Android开发技巧——设置系统状态栏颜色
    Activity,Fragment的状态保存
    Activity生命周期函数、onSaveInstanceState()和onRestoreInstanceState()的介绍
    Android Fragment生命周期
    恢复云数据库MySQL的备份文件到自建数据库遇到的报错
    如何在宿主机上查看kvm虚拟机的IP
    批量分发公钥脚本
  • 原文地址:https://www.cnblogs.com/iUtopia/p/14726326.html
Copyright © 2020-2023  润新知