Java字符串理解01-String、String Builder、String Buffer

总体来看

  • StringBuffer是线程安全的,另外两个都不是线程安全的。因为StringBuffer所有的方法都用synchronized修饰了。

  • 这个三个中StringBuilder在字符串拼接上,StringBuilder最快,StringBuffer次之,String最慢。为什么?

  • String是不可变的,所以在拼接字符串都是通过StringBuffer新建临时变量,进行拼接再将原来的变量指向拼接好的字符串。所以String比StringBuffer慢。

  • StringBuilder和StringBuffer之中的方法是一样的,但是StringBuilder的方法没有synchronized修饰,考虑的线程安全,一定会牺牲一些性能的。所以StringBuilder比StringBuffe快。

StringBuilder

  1. StringBuilder总体架构图
    Java字符串理解01-String、String Builder、String Buffer_第1张图片
    从这个图中我们可以得到最宝贵的信息就是,char [] value 既不是私有也不是final的。这就可以得知,StringBuilder是可变的。

  2. 构造函数

 public StringBuilder() {
        super(16);
    }
 public StringBuilder(int capacity) {
        super(capacity);
    }
public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

 public StringBuilder(CharSequence seq) { 
        this(seq.length() + 16);
        append(seq);
    }

super(str.length() + 16)方法就是AbstractStringBuilder的构造函数,因为StringBulider是继承这个类的。调用的是父类的构造方法进行初始化。

 AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
  1. append()家族

append()方法是我们用的最多的一个方法,进行字符串的构造基本都需要它。之前我在做项目中要拼接sql语句,都是用的append()来操作的,这个方法可以识别空格,可以使用+来连接连个字符串,拼接字符串很方便。

append()方法重载很多,因为拼接的组件有很多类型的,系统肯定要提供完整的方法体系。但是方法的结构都是一样的。如下:

public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

其实吧真正实现是在AbstractStringBuilder类中。count是已经初始化的容量中,实际使用了多少容量。每一字符占一个容量。
思路:

  • 先判断原来字符串长度+新增加字符串长度是否在之前初始化容量之内
  • 然后将从新分配char [] value 的容量=value.length+str.length
  • 最后调用getChars(0, len, value, count)将新的字符串拼接到value的后面。
public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
    
 private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }
private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }  
  1. 添加’null’
    应该不需要多说,这个方法就是将null当成字符串进行拼接了。
 private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

添加boolean类型的

public AbstractStringBuilder append(boolean b) {
        if (b) {
            ensureCapacityInternal(count + 4);
            value[count++] = 't';
            value[count++] = 'r';
            value[count++] = 'u';
            value[count++] = 'e';
        } else {
            ensureCapacityInternal(count + 5);
            value[count++] = 'f';
            value[count++] = 'a';
            value[count++] = 'l';
            value[count++] = 's';
            value[count++] = 'e';
        }
        return this;
    }
  1. 删除指定长度的子字符串
    思路:
  • 判断边界值,主要是截止位置,最长也只是count的值
  • 进行拷贝了
    借鉴:
    编程时一定要考虑边界值,也就是边缘测试,在我们的代码中药体现出来。(两边边界判断,特殊情况判断)
public AbstractStringBuilder delete(int start, int end) {
        if (start < 0)
            throw new StringIndexOutOfBoundsException(start);
        if (end > count)
            end = count;
        if (start > end)
            throw new StringIndexOutOfBoundsException();
        int len = end - start;
        if (len > 0) {
            System.arraycopy(value, start+len, value, start, count-end);
            count -= len;
        }
        return this;
    }

6 获取子字符串
从源代码中我们可以看到,StringBuilder最终返回的是String字符串对象,也就是不变得。 在构造中是可变的,一旦构造完成,就返回一个不可变的字符对象。

public String substring(int start, int end) {
        if (start < 0)
            throw new StringIndexOutOfBoundsException(start);
        if (end > count)
            throw new StringIndexOutOfBoundsException(end);
        if (start > end)
            throw new StringIndexOutOfBoundsException(end - start);
        return new String(value, start, end - start);
    }

撸了一会的源码,个人觉得吧,大多数方法都是先构造长度,长度构造完毕之后使用以下两个方法进行拼接就行了。不必深究这个源码,得到的东西还是很有限的。

  System.arraycopy(value, start+len, value, start, count-end)
	getChars(0, len, value, count);
	

StringBuffer

  • 这个类的方法和StringBulider的方法一样,唯一不同的是每个方法都是现成安全的。

String

1. 变量的不可变性

  • String类型变量是不可变的,这一点很重要,也就是String变量一旦初始化,该变量的值就不能改变了
  • 看下面代码
   String str=new String("tarena");
    char[]ch={'a','b','c'};
    public static void main(String args[]){
        Example ex=new Example();
        ex.change(ex.str,ex.ch);
        System.out.println(ex.str+" and ");
        System.out.println(ex.ch);
    }
    public void change(String str,char ch[]){
        //引用类型变量,传递的是地址,属于引用传递。
        System.out.println("数组:"+ch);
        str="test ok";
        ch[0]='g';
    }
}

//输出的结果如下
数组:[C@39a054a5
tarena and 
gbc

从上面代码能够得到知识点

  • Java引用类型变量,传参传的是地址,而不是副本,也就是你做的任何修改都是在原来数据上进行修改。
  • String类型变量的不可变性,change方法对str变量的修改没有用

为什么String是不可变的呢?

  1. 要弄懂这个问题,需要进入源码来看看。
public final class String
    implements java.io.Serializable, Comparable<String>, 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

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
}

以上是String源码中的属性,private final char value[]就是String用来存储字符的,用一个字符数组就可以存储一串字符串了。
其中最重要的就是,这个字符数组是final类型,也就是不可变类型。不可引用类型:一旦初始化,所引用的地址不会改变

  1. 看一下String的构造函数
  public String() {
        this.value = "".value;
    }
 public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
 public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

从上面的构造函数可知,String字符串的结构如下图
Java字符串理解01-String、String Builder、String Buffer_第2张图片
图片来源

  • 结合上面源码就可以理解为什么String类型变量时不可变了,一个String变量初始化时会将其字符串中的每个字符存在其封装的字符数组中(private final char value[]),然而该数字时private final 类型,没有提供改变的接口。所以String就是不可变的了。
  1. 其它辅助知识
    在jvm中,方法区有个常量池,常量池中存储包括已经初始化的字符常量。不可变的字符常量的好处就是,一旦初始化,存储在常量池中,那这个常量就是可复用的。也就是说,我们新建一个字符变量时,会先在常量池中搜索字符串常量是否存在,存在的话将引用连接到该字符常量,不需要新建对象。只有当不存在时才新建对象。

  2. 关于为什么可以对字符串进行拼接,替换。
    其实本质上对字符串的操作,不是对其本身进行操作,而是新建一个String变量,对副本进行操作。

常用方法

  1. String中的equal()方法

这个方法思路还是很简单,就是将两个字符串封装的字符数组每一位进行比较,如何每一位的字符都相同,那么这两个字符串就相等。

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
  1. compare家族系列
public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }
   public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }
 public int compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
    }

这一类方法就是比较两个字符串的每一位,区别就是有的忽略大小写,如果有哪一位不同,就返回两个字符在顺序上的差值。

实现思想都是比较两个字符串的字符数组,因为只有字符数据才能一位一位的进行比较。再这之前肯定还有些额外的判断,比如两个字符串的长度之类的。

  1. startsWith(String prefix, int toffset)
  • prefix:要比较的以哪个字符串开头
  • toffset:从哪个位置开始进行比较(其实我们一般都是从头开始)
public boolean startsWith(String prefix, int toffset) {
        char ta[] = value;
        int to = toffset;
        char pa[] = prefix.value;
        int po = 0;
        int pc = prefix.value.length;
        // Note: toffset might be near -1>>>1.
        if ((toffset < 0) || (toffset > value.length - pc)) {
            return false;
        }
        while (--pc >= 0) {
            if (ta[to++] != pa[po++]) {
                return false;
            }
        }
        return true;
    }

这个方法就是从头开始比较了。

 public boolean startsWith(String prefix) {
        return startsWith(prefix, 0);
    }

思路:

  • 看了这么多比较,我发现String中的比较的思路都是一样的,你把这个方法和前面的Compare家族的方法进行比较,也就是多了个toffset约束,试想toffset=0,不就是一模一样了吗?
  • 这也给我们编程一些提示:如果遇到要比较两个变量是否相同都将其 数组化,这样就可以对每一位进行比较了。
  1. endsWith(String suffix)
public boolean endsWith(String suffix) {
        return startsWith(suffix, value.length - suffix.value.length);
    }

  • 通过这个判断以什么开头之外,我想以什么字符串结尾一定有思路了。也就是找到开始进行比较的位置,也就是在toffset这个变量上进行改变
  1. hashCode()
    这个方法对我来说感觉很神秘的,看了源码,其实也就是实现了一个计算公式
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
 public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
  1. substring(int beginIndex, int endIndex)
    作用:
    根据起始点返回字符串一个子字符串。
 public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }
  • 这个方法就要好好说一下,之前说过String变量的最大特点是不可变性,那怎么获得字符串呢?
  • 看源码还是很明了的,最后返回的是一个新建的字符串变量,这个变量与原字符串变量没什么关系,人家是独立的一个新的字符串。
    主要是这段代码:
return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);

最后调用了String的构造方法new了一个字符串。

  1. 拼接字符串concat(String str)
    官方说法是将新的字符串拼接到某个字符串的最后面。
    思路:
    把原始的字符串拷贝到新的字符串中,然后要拼接的字符串调用getChars(char dst[], int dstBegin)将拼接的字符串拼接到新的字符串后面,就是要设置拼接点。最后将封装好的字符数组赋值给新的字符串对象。
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);
    }
 void getChars(char dst[], int dstBegin) {
        System.arraycopy(value, 0, dst, dstBegin, value.length);
    }
  1. 复制方法
    @param src the source array.:要拷贝的数组:原数据源
    • @param srcPos starting position in the source array.:原数据源复制起始点
    • @param dest the destination array.:目的数组:要拷贝到的数组
    • @param destPos starting position in the destination data.:目的数组接收的起点
    • @param length the number of array elements to be copied:要拷贝的数据元素的个数
 public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

关于复制,不得不说道这个native方法,这个方法直接调用了Java底层方法,我们也看不到源代码。所以这里只能看看怎么使用该方法,以后要赋值时都可以调用这个方法,这个方法会快很多。

我之前测试这个方法,好像只能转成数组,不能直接用String对象
测试代码

char [] aaa= new String("abcdef").toCharArray();
        char [] bbb= new String("123456").toCharArray();
        System.arraycopy(aaa, 0, bbb,0 , 6);

        for (char c: aaa) {
            System.out.println("aaa值:"+c);
        }
        for (char c: bbb) {
            System.out.println("bbb值:"+c);
        }
测试结果:
//aaa的值
aaa值:a
aaa值:b
aaa值:c
aaa值:d
aaa值:e
aaa值:f
//bbb的值
bbb值:a
bbb值:b
bbb值:c
bbb值:4
bbb值:5
bbb值:6
  1. 替换方法
    功能:
    把String对象中某个字符替换成指定的字符。
    思路:
    把目标字符串出按未进行判断,如果某一位字符等于要替换的字符值,就进行替换。 这其中有个重要思路就是需要一个缓存字符数组来保存替换完成的字符串,因为最后返回的是信息的字符串,该缓存字符数组就作为信息的字符串的封装
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;
    }

  1. trim()
    功能:用来消除字符串前后的空格
    思路:两次扫描,取出空格。分别是从前和从后进行。
    给我的感觉就是,要很好的运用好三目运算,提高代码的精简性。
public String trim() {
        int len = value.length;
        int st = 0;
        char[] val = value;    /* avoid getfield opcode */

        while ((st < len) && (val[st] <= ' ')) {
            st++;
        }
        while ((st < len) && (val[len - 1] <= ' ')) {
            len--;
        }
        return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
    }
  1. char[] toCharArray()

功能:将字符串转换成字符数组
这个方法运用的是很多的,当我们要对字符串的每一位进行处理时,就要用到这个方法来将字符串转换成字符数组。

public char[] toCharArray() {
        // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }

功能:将对选哪个转换成字符串类型,我们经常用这个方法将其他类型的转换成字符串类型。这个方法重载了很多类型。

思路:其实都是调用toString()方法进行转换,我们可以直接调用toString()方法就行了。

  public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

String源码暂时就撸到这里了,其实感觉收回不是特别大,这个源码比较简单,就当学习人家编程习惯吧。

其他知识

1. null和""的区别

“” 是字符常量,是可以调用字符串的所有方法的,但是null不是字符常量,调用方法会报错,但是null可以赋值给所有引用对象。

2. int转String

int s1=1;
String string= String.valueOf(s1)

你可能感兴趣的:(#,Java字符串学习)