深刻理解Java經常使用類----String

     Java中字符串的操做可謂是最多見的操做了,String這個類它封裝了有關字符串操做的大部分方法,從構建一個字符串對象到對字符串的各類操做都封裝在該類中,本篇咱們經過閱讀String類的源碼來深刻理解下這些字符串操做背後的原理。主要內容以下:數組

  • 繁雜的構造器
  • 屬性狀態的經常使用函數
  • 獲取內部數值的經常使用函數
  • 比較大小的相關函數
  • 局部操做等經常使用函數

1、繁雜的構造器
     在學會操做字符串以前,咱們應先了解下構造一個字符串對象的方式有幾種。先看第一種構造器:函數

private final char value[];

public String() {
   this.value = "".value;
}

String源碼中第一個私有域就是value這個字符數組,該數組被聲明爲final表示一旦初始化就不能被改變。也就是說一個字符串對象其實是由一個字符數組組成的,而且該數組一旦被初始化則不能更改。這也很好的解釋了String對象的一個特性:不可變性。一經賦值則不能改變。而咱們第一種構造器就很簡單,該構造器會將當前的string對象賦值爲空(非null)。this

接下來的幾種構造器都很簡單,實際上都是操做了value這個數組,但都不是直接操做,由於它不可更改,因此通常都是複製到局部來實現的各類操做。編碼

//1
public String(String original) {
     this.value = original.value;
     this.hash = original.hash;
}
//2
public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
//3
public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // 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);
    }

不管是第一種的傳入一個String類型,仍是第二種的直接傳入char數組的方式,都是轉換爲爲當前將要建立的對象中value數組屬性賦值。至於第三種方法,對傳入的char數組有要求,它要求從該數組索引位置爲offset開始的後count個字符組成新的數組做爲參數傳入。該方法首先作了幾個極端的判斷並增設了對應的異常拋出,核心方法是Arrays.copyOfRange這個方法,它纔是真正實現字符數組拷貝的方法。操作系統

該方法傳入三個參數,形參value,起始位置索引,終止位置索引。在該方法中主要作了兩件事情,第一,經過起始位置和終止位置獲得新數組的長度,第二,調用本地函數完成數組拷貝。線程

System.arraycopy(original, from, copy, 0,Math.min(original.length - from, newLength));

雖然該方法是本地方法,可是咱們大體能夠猜出他是如何實現的,無非是經過while或者for循環遍歷前者賦值後者。咱們看個例子:code

public static void main(String[] args){
        char[]  chs = new char[]{'w','a','l','k','e','r'};
        String s = new String(chs,0,3);
        System.out.println(s);
    }
輸出結果:wal

能夠看見這是一種[ a,b)形式,也就是說索引包括起始位置,但不包括終止位置,因此上例中只截取了索引爲0,1,2並無包括3,這種形式的截取方式在String的其餘函數中也是常見的。對象

以上介紹的構建String對象的方式中,基本都是屬於操做它內部的字符數組來實現的,下面的幾種構造器則是經過操做字節數組來實現對字符串對象的構建,固然這些操做會涉及到編碼的問題。下面咱們看第一個有關字節數組的構造器:索引

public String(byte bytes[], int offset, int length, String charsetName)
            throws UnsupportedEncodingException {
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(charsetName, bytes, offset, length);
    }

該方法首先保證charsetName不爲null,而後調用checkBounds方法判斷offset、length是否小於0,以及offset+length是否大於bytes.length。而後調用一個核心的方法用於將字節數組按照指定的編碼方式解析成char數組,咱們能夠看看這個方法:圖片

static char[] decode(String charsetName, byte[] ba, int off, int len)
        throws UnsupportedEncodingException
    {
        StringDecoder sd = deref(decoder);
        String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
        if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
                              || csn.equals(sd.charsetName()))) {
            sd = null;
            try {
                Charset cs = lookupCharset(csn);
                if (cs != null)
                    sd = new StringDecoder(cs, csn);
            } catch (IllegalCharsetNameException x) {}
            if (sd == null)
                throw new UnsupportedEncodingException(csn);
            set(decoder, sd);
        }
        return sd.decode(ba, off, len);
    }

首先經過deref方法獲取對本地解碼器類的一個引用,接着使用三目表達式獲取指定的編碼標準,若是未指定編碼標準則默認爲 ISO-8859-1,而後緊接着的判斷主要是:若是未能從本地線程相關類中獲取到StringDecoder,或者與指定的編碼標準不符,則手動建立一個StringDecoder實例對象。最後調用一個decode方法完成譯碼的工做。相比於該方法,咱們更經常使用如下這個方法來將一個字節數組轉換成char數組。

public String(byte bytes[], String charsetName)
            throws UnsupportedEncodingException {
        this(bytes, 0, bytes.length, charsetName);
    }

只指定一個字節數組和一個編碼標準便可,固然內部調用的仍是咱們上述的那個構造器。固然也能夠不指定任何編碼標準,那麼則會使用默認的編碼標準:UTF-8

public String(byte bytes[], int offset, int length) {
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(bytes, offset, length);
    }

固然還能夠更簡潔:

public String(byte bytes[]) {
        this(bytes, 0, bytes.length);
    }

可是通常用於轉換字節數組成字符串的構造器仍是使用由字節數組和編碼標準組成的兩個參數的構造器。

以上爲String類中大部分構造器的源代碼,有些源碼和底層操做系統等方面知識相關聯,理解不深,見諒。下面咱們看看有關String類的其餘一些有關操做。

2、屬性狀態的經常使用函數
     該分類的幾個函數仍是相對而言較爲簡單的,主要有如下幾個函數:

//返回字符串的長度
public int length() {
        return value.length;
}
//判斷字符串是否爲空
public boolean isEmpty() {
        return value.length == 0;
    }
//獲取字符串中指定位置的單個字符
public char charAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];
}

有關字符串屬性的函數大體就這麼些,相對而言比較簡單,下面看看獲取內部數值的經常使用函數。

3、獲取內部數值的經常使用函數
     此分類下的函數主要有兩大類,一個是返回的字符數組,一個是返回的字節數組。咱們首先看返回字符數組的方法。

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

該函數用於將當前String對象中value字符數組的起始索引位置srcBegin到終止索引位置srcEnd拷貝到目標數組dst中,其中dst數組的起始位置爲dstBegin索引處。看個例子:

public static void main(String[] args){
        String str = "hello-walker";
        char[] chs = new char[6];
        str.getChars(0,5,chs,1);
        for(int a=0;a<chs.length;a++){
            System.out.println(chs[a]);
        }
    }

結果以下:

這裏寫圖片描述

咱們指定從str 的[0,5)共五個字符組成一個數組,從chs數組索引爲1開始,一個個複製到chs裏。有關獲取獲取字符數組的函數就這麼一個,下面咱們看看獲取字節數組的函數。

public byte[] getBytes(String charsetName)
            throws UnsupportedEncodingException {
        if (charsetName == null) throw new NullPointerException();
        return StringCoding.encode(charsetName, value, 0, value.length);
    }

這個函數的核心方法,StringCoding.encode和上述的StringCoding.decode很類似,只不過一個提供編碼標準是爲了解碼成字符串對象,而另外一個則是提供編碼標準爲了將字符串編碼成字節數組。有關getBytes還有一些重載,但這些重載基本每一個都會調用咱們上述列出的這個方法,只是他們省略了一些參數(使用他們的默認值)。

4、判等函數
     在咱們平常的項目中可能常常會遇到equls這個函數,那麼這個函數是否又是和符號 == 具備相同的功能呢?下面咱們看看判等函數:

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

咱們看到該方法中,第一個判斷就使用了符號 == ,實際上等於符號判斷的是:兩個對象是否指向同一內存空間地址(固然若是他們是指向同一內存的,他們內部封裝的數值天然也是相等的)。 從上述代碼中咱們能夠看出,這個equals方法,首先判斷兩個對象是否指向同一內存位置,若是是則返回true,若是不是才判斷他們內部封裝的數組是不是相等的。

public boolean equalsIgnoreCase(String anotherString) {
        return (this == anotherString) ? true
                : (anotherString != null)
                && (anotherString.value.length == value.length)
                && regionMatches(true, 0, anotherString, 0, value.length);
    }

該方法是忽略大小寫的判等方法,核心方法是regionMatches:

public boolean regionMatches(boolean ignoreCase, int toffset,
            String other, int ooffset, int len) {
        char ta[] = value;
        int to = toffset;
        char pa[] = other.value;
        int po = ooffset;
        
        if ((ooffset < 0) || (toffset < 0)
                || (toffset > (long)value.length - len)
                || (ooffset > (long)other.value.length - len)) {
            return false;
        }
        while (len-- > 0) {
            char c1 = ta[to++];
            char c2 = pa[po++];
            if (c1 == c2) {
                continue;
            }
            if (ignoreCase) {
                char u1 = Character.toUpperCase(c1);
                char u2 = Character.toUpperCase(c2);
                if (u1 == u2) {
                    continue;
                }
                if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
                    continue;
                }
            }
            return false;
        }
        return true;
    }

首先是檢錯判斷,簡單判斷下傳入的參數是否小於0等,而後經過不斷讀取兩個字符數組的字符比較是否相等,若是相等則直接跳過餘下代碼進入下次循環,不然分別將這兩個字符轉換爲小寫和大寫兩種形式進行比較,若是相等,依然返回true。equals方法只能判斷二者是否相等,可是對於誰大誰小則無能爲力。 下面咱們看看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 compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
}

這裏的compare方法是CASE_INSENSITIVE_ORDER類的一個內部類。

爲了避免讓文章篇幅過長,本篇暫時結束,下篇將介紹最多見的一些有關字符串操做的函數源碼,總結的很差,望海涵!

相關文章
相關標籤/搜索