Java 7之基础类型第5篇 - Java字符串类型

由于日常见到的大部分文本都是基于字符串来表示和处理的,所以Java提供了强大的API支持,方便程序编写人员对文本的处理。下面就来研究一下String类的源代码。


1、Java常见问题解答


首先来看一下字符串常见的几个问题:


(1)String s = new String("xyz");创建了几个String 对象?      

      两个或一个,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多少遍,都是缓冲区中的那一个,类似与缓存Byte数值的-128~127数值。New String每写一遍,就创建一个新的对象,那个常量”xyz”对象的内容来创建出一个新String对象。如果以前就用过’xyz’,这句代表就不会创建”xyz”自己了,直接从缓冲区拿。
      所以,凡是通过构造器创建的对象都会进行内存分配,所以他就不会指向缓存池中已有的对象而指向新的对象,这样就会造成缓存池中存在多个值相同的字符串对象,浪费资源。所以一般要直接为字符串指定值即可。
      这里需要介绍一下缓存池:为了节省内存,提高资源的复用,jvm引入了常量池这个概念,它属于方法区的一部分,作用之一就是存放 编译期间生产的各种字面量和符号引用。 方法区的垃圾回收行为是比较少出现的,该区中的对象基本不会被回收,可以理解成是永久存在的。

(2)String s="a"+"b"+"c"+"d";创建了几个String对象?       

      一个,因为Javac在做编译时已经对这些字符串进行了合并操作,预先做了优化处理。

(3)String name = "ab";  name = name + "c";两条语句总共创建了多少个字符串对象?       

       创建了两个对象,这两个对象都会放到缓存池中,只是name的引用由"ab"改变为"abc"了。我们在这样用的时候,还需要考虑其他问题,如这个程序会造成内在泄漏,因为缓存池中的在缓存池中的字符串是不会被垃圾回收机制回收的,基本都是常驻内存,所以过多使用String类,可能会出现内存溢出。

(4)字符串比较

        String s1 = "a";
        String s2 = s1 + "b";
        String s3 = "ab"; 
        System.out.println(s2 == s3);//false

       可以看到s2与s3的引用并不相同。由于s2字符串在编译时并不能进行确定,所以首先进入缓存池中的有s1和s3,随后会创建一个新的s2字符串对象,两者当然不一样了。

如果程序的字符串连接表达式中没有使用变量或者调用方法,那么该字符串变量的值就能够在编译期间确定下来,并且将该字符换缓存在缓冲区中,同时让该变量指向该字符串;否则将无法利用缓冲区,因为使用了变量和调用了方法之后的字符串变量的值只能在运行期间才能确定连接式的值,也就无法在编译期间确定字符串变量的值,从而无法将字符串变量增加到缓冲区并加以利用。

如果要对s1与s2加上final关键字后,结果就为true了。因为会在编译期进行优化处理。


所以如果有字符串拼接之类的操作,建议使用StringBuilder类型或StringBuffer类。


2、String类变量及构造函数


下面来看一下JDK 7中String类的两个重要的变量:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}
在JDK 6中其实还有另外的两个变量offset和count:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence{
    private final char value[]; // 用来存储字符串转换而来的字符数组
    private final int offset; // 字符串起始字符在字符数组的位置
    private final int count; // 字符串分解成字符数组后字符的数目
}

以上的两段代码可以看出

  1. String类是被final修饰的,从安全角度来说,通过final修饰后的String类是不能被其他类继承的,在最大程度上的保护了该类,从效率上来说,提高了该类的效率,因为final修饰后会比没有用final修饰的快。
  2. value[],或者JDK 6中的offet, count也是被final修饰的,这时候三个变量的值必须在编译期间就被确定下来,并且在运行期间不能再被修改了。因此,每次我们每次进行字符串修改、拼接的时候,并不能直接修改当前String对象的值,只能重新创建一个新的对象。
  3. 创建String对象的时候,String对象还使用字符数组(char[])来存储我们的字符串, 在Java中String类其实就是对字符数组的封装

下面来看一下String类的一个构造函数,如下:

public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
}
使用如上的构造函数使用original.value的形式来创建一个字符串,但是通常不建议这样做,可能会创建两个字符串对象,消耗太大。继续来看其他的一些常用构造函数。

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

    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

    public String(int[] codePoints, int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > codePoints.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }

        final int end = offset + count;

        // Pass 1: Compute precise size of char[]
        int n = count;
        for (int i = offset; i < end; i++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                continue;
            else if (Character.isValidCodePoint(c))
                n++;
            else throw new IllegalArgumentException(Integer.toString(c));
        }

        // Pass 2: Allocate and fill in char[]
        final char[] v = new char[n];

        for (int i = offset, j = 0; i < end; i++, j++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                v[j] = (char)c;
            else
                Character.toSurrogates(c, v, j++);
        }

        this.value = v;
    }
编写一个测试程序,如下:
        char data[] = {'a', 'b', 'c'};  
        String str1 = new String(data);  
        String str2 = new String(data,0,2);  
        
        char data1[] = {0x4E2D, 0x56FD};  
        String str3 = new String(data1);
        System.out.println(str1);//abc
        System.out.println(str2);//ab
        System.out.println(str3);//中国
还有以byte[]数组为参数创建字符串的构造函数,其中最主要的两个如下:
    public String(byte bytes[], int offset, int length) {
        checkBounds(bytes, offset, length);
        char[] v  = StringCoding.decode(bytes, offset, length);
        this.offset = 0;
        this.count = v.length;
        this.value = v;
    }
    public String(byte bytes[], int offset, int length, String charsetName) throws UnsupportedEncodingException {
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        checkBounds(bytes, offset, length);
        char[] v = StringCoding.decode(charsetName, bytes, offset, length);
        this.offset = 0;
        this.count = v.length;
        this.value = v;
    }
    public String(byte bytes[], int offset, int length, Charset charset) {
        if (charset == null)
            throw new NullPointerException("charset");
        checkBounds(bytes, offset, length);
        char[] v = StringCoding.decode(charset, bytes, offset, length);
        this.offset = 0;
        this.count = v.length;
        this.value = v;
    }
编写测试程序如下:
byte[] ascBytes = {(byte)0x61, (byte)0x62, (byte)0x63};                    // ASCII的'a','b','c'字符
System.out.println(new String(ascBytes,0,2));                              // abc
System.out.println(new String(ascBytes,0,2,Charset.forName("ISO-8859-1")));// abc
System.out.println(new String(ascBytes,0,2,"ISO-8859-1"));                 // abc
其他的一些使用byte数组创建字符串的构造函数其实最终都是调用如上两个构造函数。另外还提供了StringBuffer和StringBuilder转String字符串的构造函数,如下:
public String(StringBuffer buffer) {
        synchronized(buffer) {
            this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
        }
    }

    public String(StringBuilder builder) {
        this.value = Arrays.copyOf(builder.getValue(), builder.length());
    }


3、String字符串长度


接下来就看一看String类常用的API了。

   /**
     * Returns the length of this string.
     * The length is equal to the number of <a href="Character.html#unicode">Unicode
     * code units in the string.
     */
    public int length() {
        return value.length;
    }
如上的方法是返回字符串的长度,但是需要注意的是:

String类中的length()方法也是对字符串进行char统计,也就是计算代码单元数量(代码单元有可能大于正真的字符数量,如果字符串中有增补字符的话)。如果要计算代码点数量,必须用str.codePointCount(0,str.length())方法。

 public int codePointCount(int beginIndex, int endIndex) {
        if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
            throw new IndexOutOfBoundsException();
        }
        return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex);
    }
static int codePointCountImpl(char[] a, int offset, int count) {
        int endIndex = offset + count;
        int n = count;
        for (int i = offset; i < endIndex; ) {
            if (isHighSurrogate(a[i++]) && i < endIndex && isLowSurrogate(a[i])) {
                n--;
                i++;
            }
        }
        return n;
    }

由此可见,用char类型来处理字符串在有些情况下是不准确的,我们最好使用字符串类型中处理代码点的方法,不要使用处理char类型(一个代码单元)的方法。


4、截取字符串字串


来看一下JDK 6中的字符串截取的方法substring():

    public String substring(int beginIndex, int endIndex) {
	if (beginIndex < 0) {
	    throw new StringIndexOutOfBoundsException(beginIndex);
	}
	if (endIndex > count) {
	    throw new StringIndexOutOfBoundsException(endIndex);
	}
	if (beginIndex > endIndex) {
	    throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
	}
	return ((beginIndex == 0) && (endIndex == count)) ? this :new String(offset + beginIndex, endIndex - beginIndex, value);
    }
    // Package private constructor which shares value array for speed.
    String(int offset, int count, char value[]) {
	this.value = value;
	this.offset = offset;
	this.count = count;
    }
char[] value 数组被共享了。而截取字串的操作是通过偏移量offset和长度count来实现的。这样就会造成内存泄漏问题,例如需要多次截取大字符串中的很小一部分时。
public class LeakTest {
    public static void main(String...args) {
        List<String> handler = new ArrayList<String>();
        for(int i = 0; i < 100000; i++) {
            // HugeStr h=new HugeStr();   //  保存了对每个HugeStr类中str的强引用,所以一直无法释放
            ImprovedHugeStr h = new ImprovedHugeStr();
            handler.add(h.getSubString(1, 5));
        }
    }
}

class HugeStr{
   private String str=new String(new char[10000]);
   public String getSubString(int begin,int end){
        return str.substring(begin,end);
   }
}

class ImprovedHugeStr{
   private String str=new String(new char[10000]);
   public String getSubString(int begin,int end){
        return new String(str.substring(begin,end));
   }
}
由于每个HugeStr类实例都会对str大字符串对象进行强引用,导致无法释放内存并且造成了内存的巨大浪费,最终程序运行会抛出异常:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

在JDK 7中进行了改进,如下:

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);
}

可以看到,使用这个方法进行截取部分子串的操作时,通常会调用:

 new String(value, beginIndex, subLen)
 public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
使用Arrays.copyOfRange()方法返回了一个新的字符串,串中的内容与原串中要截取的子字符串相同。

5、分割字符串


字符串的分割方法有多种,下面来介绍一下。

(1)最常用的就是使用splite()方法。实现源代码如下:
    public String[] split(String regex, int limit) {
        /* fastpath if the regex is a
         (1)one-char String and this character is not one of the
            RegEx's meta characters ".$|()[{^?*+\\", or
         (2)two-char String and the first char is the backslash and
            the second is not the ascii digit or ascii letter.
         */
        char ch = 0;
        if (((regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 && regex.charAt(0) == '\\' &&
              (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && ((ch-'a')|('z'-ch)) < 0 &&
              ((ch-'A')|('Z'-ch)) < 0)) && (ch < Character.MIN_HIGH_SURROGATE ||  ch > Character.MAX_LOW_SURROGATE))
        {
            int off = 0;
            int next = 0;
            boolean limited = limit > 0;
            ArrayList<String> list = new ArrayList<>();
            while ((next = indexOf(ch, off)) != -1) {
                if (!limited || list.size() < limit - 1) {
                    list.add(substring(off, next));
                    off = next + 1;
                } else {    // last one
                    //assert (list.size() == limit - 1);
                    list.add(substring(off, value.length));
                    off = value.length;
                    break;
                }
            }
            // If no match was found, return this
            if (off == 0)
                return new String[]{this};

            // Add remaining segment
            if (!limited || list.size() < limit)
                list.add(substring(off, value.length));

            // Construct result
            int resultSize = list.size();
            if (limit == 0)
                while (resultSize > 0 && list.get(resultSize - 1).length() == 0)
                    resultSize--;
            String[] result = new String[resultSize];
            return list.subList(0, resultSize).toArray(result);
        }
        return Pattern.compile(regex).split(this, limit);
    }

(2)StringTokenizer类实现分割,效率优于splite()方法
StringTokenizer strtk=new StringTokenizer(str,";");
while(strtk.hasMoreTokens()){
    strtk.nextToken();
}
StringTokenizer类在java.util中,有兴趣的可以阅读一下。在Java 7中,这个方法并不鼓励使用,可以使用更加强大的java.util.regex包下的一些类来实现。

(3)虽然subString()会造成内存泄漏,但是由于使用了空间换时间的方法,所以查找的速度还是很快的。
int j=str.indexOf(';');
str.substring(0,j);
str=str.substring(j+1);


6、字符串的hashcode()和equals()


    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;
    }
字符串的hashCode()计算的方法为:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
n为字符串的长度,空字符串的hash值为0。

    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;
    }
比较两个字符串中字符的序列组成是否相等,也就是比较字符串中的内容。如果不覆写这个方法,默认继承的是Object中的equals()方法,代码如下:

    public boolean equals(Object obj) {
        return (this == obj);
    }
使用“==”符号来比较两个引用类型的值,则比较的是引用地址,不准确,所以一定要进行覆写。

关于hashCode()和equals()方法,下面有非常详细的解说。

传送门:



7、缓存字符串


下面还有一个方法需要讲解一下:

public native String intern();
这是一个本地的方法,当调用 intern 方法时,如果缓存池已经包含一个等于此 String 对象的字符串,则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。
       String param = "abc";
       String newStr = new String("abc");
       String param2 = new String("abc");
       newStr.intern();
       param2 = param2.intern();               // param2指向intern返回的常量池中的引用
       System.out.println(param == newStr);    // false
       System.out.println(param == param2);    // true
可以看到,使用intern()方法后,字符串变量的值如果不存在缓冲区中将会被缓存











5、分割字符串

你可能感兴趣的:(String,String类,源代码,java类型,String源代码)