zoukankan      html  css  js  c++  java
  • java基础(六)-----String性质深入解析

    本文将讲解String的几个性质。

    一、String的不可变性

      对于初学者来说,很容易误认为String对象是可以改变的,特别是+链接时,对象似乎真的改变了。然而,String对象一经创建就不可以修改。接下来,我们一步步 分析String是怎么维护其不可改变的性质

    1. 手段一:final类 和 final的私有成员

    我们先看一下String的部分源码:

    1 public final class String
    2     implements java.io.Serializable, Comparable<String>, CharSequence {
    3     /** The value is used for character storage. */
    4     private final char value[];
    5     /** Cache the hash code for the string */
    6     private int hash; // Default to 0
    7     /** use serialVersionUID from JDK 1.0.2 for interoperability */
    8     private static final long serialVersionUID = -6849794470754667710L;
    9 }

      我们可以发现 String是一个final类,且3个成员都是私有的,这就意味着String是不能被继承的,这就防止出现:程序员通过继承重写String类的方法的手段来使得String类是“可变的”的情况。

      从源码发现,每个String对象维护着一个char数组 —— 私有成员value。数组value 是String的底层数组,用于存储字符串的内容,而且是 private final ,但是数组是引用类型,所以只能限制引用不改变而已,也就是说数组元素的值是可以改变的,而且String 有一个可以传入数组的构造方法,那么我们可不可以通过修改外部char数组元素的方式来“修改”String 的内容呢?

    我们来做一个实验,如下:

    1 public static void main(String[] args) {
    2     char[] arr = new char[]{'a','b','c','d'};       
    3     String str = new String(arr);       
    4     arr[3]='e';     
    5     System.out.println("str= "+str);
    6     System.out.println("arr[]= "+Arrays.toString(arr));
    7 }

    运行结果

    1 str= abcd
    2 arr[]= [a, b, c, e]

    结果与我们所想不一样。字符串str使用数组arr来构造一个对象,当数组arr修改其元素值后,字符串str并没有跟着改变。那就看一下这个构造方法是怎么处理的:

    1 public String(char value[]) {
    2     this.value = Arrays.copyOf(value, value.length);
    3 }

    原来 String在使用外部char数组构造对象时,是重新复制了一份外部char数组,从而不会让外部char数组的改变影响到String对象。

    2. 手段二:改变即创建对象的方法

      从上面的分析我们知道,我们是无法从外部修改String对象的,那么可不可能使用String提供的方法,因为有不少方法看起来是可以改变String对象的,如replace()replaceAll()substring()等。我们以substring()为例,看一下源码:

    1 public String substring(int beginIndex, int endIndex) {
    2     //........
    3     return ((beginIndex == 0) && (endIndex == value.length)) ? this
    4             : new String(value, beginIndex, subLen);
    5 }

    从源码可以看出,如果不是切割整个字符串的话,就会新建一个对象。也就是说,只要与原字符串不相等,就会新建一个String对象

    缓存Hashcode

      Java中经常会用到字符串的哈希码(hashcode)。例如,在HashMap中,字符串的不可变能保证其hashcode永远保持一致,这样就可以避免一些不必要的麻烦。这也就意味着每次在使用一个字符串的hashcode的时候不用重新计算一次,这样更加高效。

    在String类中,有以下代码:

    1 private int hash;//this is used to cache hash code.

    以上代码中hash变量中就保存了一个String对象的hashcode,因为String类不可变,所以一旦对象被创建,该hash值也无法改变。所以,每次想要使用该对象的hashcode的时候,直接返回即可。

    二、字符串拼接

    其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码:

    1 String s = "abcd";
    2 s = s.concat("ef");

    其实最后我们得到的s已经是一个新的字符串了。如下图

    s中保存的是一个重新创建出来的String对象的引用.

    那么,在Java中,到底如何进行字符串拼接呢?字符串拼接有很多种方式,这里简单介绍几种比较常用的

    1、使用 + 拼接字符串

     我们先来看一个例子:

     1 public class MyTest {
     2     public static void main(String[] args) {
     3         String s = "Love You";      
     4         String s2 = "Love"+" You";
     5         String s3 = s2 + "";
     6         String s4 = new String("Love You");
     7         System.out.println("s == s2 "+(s==s2));
     8         System.out.println("s == s3 "+(s==s3));
     9         System.out.println("s == s4 "+(s==s4));
    10     }
    11 }

    运行结果:

    1 s == s2  true
    2 s == s3  false
    3 s == s4  false

      是不是对运行结果感觉很不解。别急,我们来慢慢理清楚。首先,我们要知道编译器有个优点:在编译期间会尽可能地优化代码,所以能由编译器完成的计算,就不会等到运行时计算,如常量表达式的计算就是在编译期间完成的。所以,s2 的结果其实在编译期间就已经计算出来了,与 s 的值是一样,所以两者相等,即都属于字面常量,在类加载时创建并维护在字符串常量池中。但 s3 的表达式中含有变量 s2 ,只能是运行时才能执行计算,也就是说,在运行时才计算结果,在堆中创建对象,自然与 s 不相等。而 s4 使用new直接在堆中创建对象,更不可能相等。

      那在运行期间,是如何完成String的+号链接操作的呢,要知道String对象可是不可改变的对象。我们反编译上面例子的calss文件,来看看究竟是怎么实现的:

     1 public class MyTest
     2 {
     3     public MyTest()
     4     {
     5     }
     6     public static void main(String args[])
     7     {
     8         String s = "Love You";
     9         String s2 = "Love You";//已经得到计算结果
    10         String s3 = (new StringBuilder(String.valueOf(s2))).toString();
    11         String s4 = new String("Love You");
    12         System.out.println((new StringBuilder("s == s2 ")).append(s == s2).toString());
    13         System.out.println((new StringBuilder("s == s3 ")).append(s == s3).toString());
    14         System.out.println((new StringBuilder("s == s4 ")).append(s == s4).toString());
    15     }
    16 }

    可以看出,编译器将 + 号处理成了StringBuilder.append()方法。也就是说,在运行期间,链接字符串的计算都是通过 创建StringBuilder对象,调用append()方法来完成的。

    2、concat

    除了使用+拼接字符串之外,还可以使用String类中的方法concat方法来拼接字符串。如:

    1 String wechat = "ChenHao";
    2 String introduce = "每日更新Java相关技术文章";
    3 String hollis = wechat.concat(",").concat(introduce);

    我们再来看一下concat方法的源代码,看一下这个方法又是如何实现的。

     1 public String concat(String str) {
     2    int otherLen = str.length();
     3    if (otherLen == 0) {
     4        return this;
     5    }
     6    int len = value.length;
     7    char buf[] = Arrays.copyOf(value, len + otherLen);
     8    str.getChars(buf, len);
     9    return new String(buf, true);
    10 }

    这段代码首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的String对象并返回。

    通过源码我们也可以看到,经过concat方法,其实是new了一个新的String,这也就呼应到前面我们说的字符串的不变性问题上了。

    三、StringBuffer和StringBuilder

    接下来我们看看StringBufferStringBuilder的实现原理。和String类类似,StringBuilder类也封装了一个字符数组,定义如下:

    1 char[] value;

    String不同的是,它并不是final的,所以他是可以修改的。另外,与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:

    1 int count;

    其append源码如下:

    1 public StringBuilder append(String str) {
    2    super.append(str);
    3    return this;
    4 }

    该类继承了AbstractStringBuilder类,看下其append方法:

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

    append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。

    StringBuffer和StringBuilder类似,最大的区别就是StringBuffer是线程安全的,看一下StringBufferappend方法。

    1 public synchronized StringBuffer append(String str) {
    2    toStringCache = null;
    3    super.append(str);
    4    return this;
    5 }

    该方法使用synchronized进行声明,说明是一个线程安全的方法。而StringBuilder则不是线程安全的。

    效率比较

    既然有这么多种字符串拼接的方法,那么到底哪一种效率最高呢?我们来简单对比一下。

     1 long t1 = System.currentTimeMillis();
     2 String str = "chenhao";
     3 //StringBuffer str = new StringBuffer("chenhao");
     4 for (int i = 0; i &lt; 50000; i++) {
     5    String s = String.valueOf(i);
     6    str += s;
     7    //str=str.concat(s);
     8    //str.append(s);
     9 }
    10 long t2 = System.currentTimeMillis();
    11 System.out.println("+ cost:" + (t2 - t1));

    我们使用形如以上形式的代码,分别测试下五种字符串拼接代码的运行时间。得到结果如下:

    1 + cost:5119
    2 StringBuilder cost:3
    3 StringBuffer cost:4
    4 concat cost:3623

    从结果可以看出,用时从短到长的对比是:

    StringBuilder < StringBuffer < concat < + < StringUtils.join

    StringBufferStringBuilder的基础上,做了同步处理,所以在耗时上会相对多一些,这个很好理解。

    那么问题来了,前面我们分析过,其实使用+拼接字符串的实现原理也是使用的StringBuilder,那为什么结果相差这么多,高达1000多倍呢?

    我们再把以下代码反编译下:

    1 long t1 = System.currentTimeMillis();
    2 String str = "chenhao";
    3 for (int i = 0; i &lt; 50000; i++) {
    4    String s = String.valueOf(i);
    5    str += s;
    6 }
    7 long t2 = System.currentTimeMillis();
    8 System.out.println("+ cost:" + (t2 - t1));

    反编译后代码如下:

     1 long t1 = System.currentTimeMillis();
     2 String str = "chenhao";
     3 for(int i = 0; i &lt; 50000; i++)
     4 {
     5    String s = String.valueOf(i);
     6    str = (new StringBuilder()).append(str).append(s).toString();
     7 }
     8 
     9 long t2 = System.currentTimeMillis();
    10 System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());

    我们可以看到,反编译后的代码,在for循环中,每次都是new了一个StringBuilder,然后再把String转成StringBuilder,再进行append

    而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。

    所以:循环体内,字符串的连接方式,使用StringBuilder 的 append 方法进行扩展。而不要使用+

    总结

    本文介绍了什么是字符串拼接,虽然字符串是不可变的,但是还是可以通过新建字符串的方式来进行字符串的拼接。

    常用的字符串拼接方式有五种,分别是使用+、使用concat、使用StringBuilder、使用StringBuffer以及使用StringUtils.join

    由于字符串拼接过程中会创建新的对象,所以如果要在一个循环体中进行字符串拼接,就要考虑内存问题和效率问题。

    因此,经过对比,我们发现,直接使用StringBuilder的方式是效率最高的。因为StringBuilder天生就是设计来定义可变字符串和字符串的变化操作的。

    但是,还要强调的是:

    1、如果不是在循环体中进行字符串拼接的话,直接使用+就好了。

    2、如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替StringBuilder

  • 相关阅读:
    LintCode "Maximum Gap"
    LintCode "Wood Cut"
    LintCode "Expression Evaluation"
    LintCode "Find Peak Element II"
    LintCode "Remove Node in Binary Search Tree"
    LintCode "Delete Digits"
    LintCode "Binary Representation"
    LeetCode "Game of Life"
    LintCode "Coins in a Line"
    LintCode "Word Break"
  • 原文地址:https://www.cnblogs.com/java-chen-hao/p/10396575.html
Copyright © 2011-2022 走看看