java String類,底層自學自看 筆記(實時更新) 1
提示:本文章是基於jdk1.7,對於一些常見類底層學習的公開筆記,學藝不精,發現錯誤請評論提出。
按照jdk,String類自上而下的順序挨個學習研究
package java.lang; import java.io.ObjectStreamField; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Formatter; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException;
一:首先String型別實現了三個介面
Serializable:序列化,(可被轉為二進位制,實現永久儲存)
Comparable:自然排序(String提供了比較方法)
CharSequence:可讀序列()
二:擁有兩個私有實體類
private final char value[]; 字元陣列
分析:String型別的儲存是以字元陣列的方式進行儲存。
private int hash; 雜湊 生成的hash碼 private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];(宣告一個類的序列化欄位)
三:方法
1
public String() {
this.value = new char[0];
}
可以 String i = new String(); 的方式建立一個字串物件,他會建立一個0長度的char型別陣列
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
注:一個沒有什麼用的構造器,字串本身就是不可變的,直接 ‘=’ 就可以。
從記憶體意義上講,也許設計之初是,將一個不會再用到的字串呼叫此方法進行替換,不佔用新的記憶體,也許想法是達到節省記憶體的目的,但是顯然,這不符合String的特性。
之前都是熱身,現在開始稍微,有點難度-.-
2
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
注:這個方法是new String時傳入一個Char型別陣列。
然後使用Arrays.copyOf,copy整個陣列,生成一個新的一模一樣的字元陣列返回給String。
且此處也不適合使用=直接記憶體賦值,容易產生一些不可預知的事情。
3
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
注:這個其實去上面的是一樣的功能,只是增加了控制copy的起點和字元個數。
4
public String(int[] codePoints, int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);//起點不能小於0
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);//個數不能小於0
}
if (offset > codePoints.length - count) {//不允許起點+個數大於了總長
throw new StringIndexOutOfBoundsException(offset + count);
}
final int end = offset + count;//結束位置
int n = count;//需要獲取的個數
for (int i = offset; i < end; i++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))//判斷是否是有效的hash點,簡單說就是是否小與2的16次方
continue;
else if (Character.isValidCodePoint(c))//簡單說這個是判斷他右移16位是否小於17
n++;
else throw new IllegalArgumentException(Integer.toString(c));
}
//這個for迴圈 就是判斷 這個hashcode碼值是否在 2的16 到 2的22之間,如果在 就n+1,就是增加一個位置
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型別指定起點和位置的字元拿出來展示出來。
但以底層的形式來看,輸入5個不見得一定會返回5個,也可能是6個或者更多。
我猜測應該是防止碼值擴充套件而導致該方法無法使用,增加了該方法的擴充套件性。
2018-11-14 15:52- -1.0.0//該方法解讀存在修改可能,記錄下版本
接下來是幾個已經被廢棄的。
5
//功能:從8位整數值的陣列的子陣列中分配一個新的構造。
//hibyte:每個16位Unicode碼單元的前8位
@Deprecated //廢棄註解
public String(byte ascii[], int hibyte, int offset, int count) {
checkBounds(ascii, offset, count);//這個方法就是驗證起點和個數的和不能超過最大值,否則異常
char value[] = new char[count];//建立一個新的,長度是count
if (hibyte == 0) {
for (int i = count; i-- > 0;) {
value[i] = (char)(ascii[i + offset] & 0xff);
}
} else {
hibyte <<= 8;
for (int i = count; i-- > 0;) {
value[i] = (char)(hibyte | (ascii[i + offset] & 0xff)); 做了二進位制非運算
}
}
//該if執行的是如果給了Unicode碼
this.value = value;
}
//官方給的廢棄原因是-- 此方法沒有正確地將位元組轉換為字元
注:通過觀察,在hibyte有值的情況下確實沒有正確的將位元組轉化為字元,筆者並沒有看懂做二進位制非運算這部操作的意義和用途,儘管他實現了分配新的構造。通過實驗生成的是亂碼
猜測:該方法的目的可能是改變記憶體地址之類的,但是錯誤的直接改變內裡儲存的值(廢棄)
最新猜測:
2018-11-14 16:46- -1.0.0//該方法解讀存在修改可能,記錄下版本
@Deprecated//廢棄 this引用的就是上面那個方法,所以廢棄了
//該方法就是預設從頭到尾
public String(byte ascii[], int hibyte) {
this(ascii, hibyte, 0, ascii.length);
}
6
//這就是一個簡單的私有的工具類
//起始位置和需要的長度不能小於0
//其實值+需要值不能大於總長度
//之後再出現這個方法將不再進行解釋
private static void checkBounds(byte[] bytes, int offset, int length) {
if (length < 0)//
throw new StringIndexOutOfBoundsException(length);
if (offset < 0)
throw new StringIndexOutOfBoundsException(offset);
if (offset > bytes.length - length)
throw new StringIndexOutOfBoundsException(offset + length);
}
7
//charsetName 要轉碼的名字
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);//轉碼操作之後裡面會copy一個新的你要的char[]返回
}
注:這個是String提供的,一般對於亂碼問題才會用到的方法,先將字串轉成原始的byte然後轉碼。
同時提供了預設從頭至尾的轉碼,預設為ISO-8859-1
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);//呼叫了上述方法,從起始到結尾
}
8
//charset:編碼的實體類,和上面的功能是一樣的
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}
//以及他的另一種形式
public String(byte bytes[], Charset charset) {
this(bytes, 0, bytes.length, charset);
}
9
//及直接生成預設編碼的構造方法,該decode的底層預設是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型別轉碼,物件的形式轉碼,以及簡化版轉碼。
應用:如果直接以byte陣列的形式建立String,那麼直接就是utf-8了,非常的方便。
10
//將StringBuffer轉為String,StringBuffer和StringBuilder的底層都是char[] ,而且Buffer除了對其方法做了大量的synchronized之外二者並沒有什麼區別。
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());//傳統的char型別複製
}
}
Arrays.copy(char[],length);複製的陣列,長度
他的底層核心:
System.arraycopy(char[],int,char[],int,int);
底層是System.arraycopy(現有陣列(char[]),現有陣列起點(int),要複製內容進去的目標陣列(char[]),目標陣列複製內容的起點 , 複製的長度);
複製的長度一般都是現有陣列和目標陣列長度比較的較小值。
Math.min(one(int), two(int));會返回一個較小的one或者two
Math.min()的底層實現也很簡單
public static int min(int a, int b) {
return (a <= b) ? a : b;
}
下一個方法是
11
//和buffer一樣,不浪費時間了-.-
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
12
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}//這是指定是否允許該字串被斷開,被拆分等操作。
//官方所希望的功能顯然並沒有完成,這是一個毫無功能的構造方法,也許以後的版本會完善
//String型別本身也是進行某種操作後會生成新的String,這個功能也顯得不大可能
13
//這個是擷取String,前面就有這個方法,這個this呼叫的也是序號3的方法
@Deprecated
String(int offset, int count, char[] value) {
this(value, offset, count);
}//感覺沒什麼用,僅僅只是換了下引數的順序,廢棄也是正常的
//java公司出的原始碼書籍也提到過,構造方法僅僅換個引數是他們不推薦做的,因為這樣做增加了程式碼體積
,實際上也沒有任何功能上的豐富和加強。
構造方法結束
14
//獲取字串的長度
public int length() {
return value.length;//value是char[]型別也是獲取陣列的長度就是string型別的長度
}
15
public boolean isEmpty() {
return value.length == 0;
}
//空格不算是空,依舊有長度。
//如果value為null會報錯
//如果為""則是0
16
//返回指定位置的字元
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {//不能小於0或者大於他的長度,否則會丟擲異常
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
//索引是從0開始的
17
//返回指定位置的code值
public int codePointAt(int index) {
if ((index < 0) || (index >= value.length)) {//驗證
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointAtImpl(value, index, value.length);
}
//Character.codePointAtImpl該工具也不僅僅是做了返回操作,還是驗證了一些東西
static int codePointAtImpl(char[] a, int index, int limit) {
char c1 = a[index++];//這裡是先返回index,再加1
if (isHighSurrogate(c1)) {//十六進位制比較:c1>='\uD800' && c1 < \uDBFF +1. 簡單說就是在 55296和56319+1 之間①
if (index < limit) {
char c2 = a[index];//高位在左邊,低位在右邊
if (isLowSurrogate(c2)) {//大於等於56320和<57343+1之間
return toCodePoint(c1, c2);
}
}
}
return c1;//如果是普通的字元直接返回碼點
}
①:是補充字元驗證,比如說一個特殊字元,它在char陣列儲存要佔兩個位置來儲存,那麼在這裡就用到了。因為標識這個特殊字元同時需要兩個位置來儲存
注:如果該位置是補充字元,那麼就會尋找低位字元,然後做了一步toCodePoint運算,就是把之前分離儲存的字元再逆運算組合起來,都是一些int型別二進位制的位運算,在此不過多研究。
18
//返回輸入位置的前一個字元。
public int codePointBefore(int index) {
int i = index - 1;
if ((i < 0) || (i >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointBeforeImpl(value, index, 0);
}//和上一個比,就是輸出你要的前一個,沒啥好說的
static int codePointBeforeImpl(char[] a, int index, int start) {
char c2 = a[--index];//這是直接給c2輸出減完後的
if (isLowSurrogate(c2)) {
if (index > start) {
char c1 = a[--index];
if (isHighSurrogate(c1)) {
return toCodePoint(c1, c2);
}
}
}
return c2;
}
19
//返回指定起點終點字元所對應字串的個數
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);
}
//驗證
public static int codePointCount(char[] a, int offset, int count) {
if (count > a.length - offset || offset < 0 || count < 0) {
throw new IndexOutOfBoundsException();
}
return codePointCountImpl(a, offset, count);//調下面那個方法,未做任何操作
}
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])) {//遇到補碼 i+1 長度減1
n--;
i++;
}
}
return n;
}
注:這應該是面向框架,或者內部呼叫的,於實際應用,一般用不上
20
//返回偏移量的方法
public int offsetByCodePoints(int index, int codePointOffset) {
if (index < 0 || index > value.length) {
throw new IndexOutOfBoundsException();
}
return Character.offsetByCodePointsImpl(value, 0, value.length,
index, codePointOffset);
}
//由指定索引開始,正數向左便宜,負數向右偏移。返回之後的字元在char陣列中的第幾位。
//解釋一下,比如果abcde中的c佔了兩個字元還儲存,那麼這個方法(0,3)就是4,索引 1是a,2是b,3和4是c
//用到的非常少,對這個方法進行追溯,發現只有一個sun公司自己開發的包用過,還只用過一次。
21
//複製字串
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);//這是一個java層面最底層的複製陣列的方法,
//所有複製陣列,及底層是陣列的字串複製的話底層都會是這個方法。
//第一個引數,要被複制的陣列物件
//第二個引數,複製的起點
//第三個引數,複製到另一個數組裡的目標陣列
//第四個引數,開始複製的起點
//第五個,複製的長度
}
22
//與上個方法功能相同
//引數1:複製的起點 2: 複製的重點 3:目標陣列 4:目標陣列複製進去時的起點
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);
//上個方法已經詳細說明,此處不再過多討論。
}
23
//這是一個廢棄的方法,遠古時期jdk1.1用到的,遠古時期的東西一直遺留到了現在。
@Deprecated//廢棄
public void getBytes(int srcBegin, int srcEnd, byte 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);
}
int j = dstBegin;
int n = srcEnd;
int i = srcBegin;
char[] val = value; /* avoid getfield opcode */
while (i < n) {
dst[j++] = (byte)val[i++];
}
}
//試了一下這個方法,各種問題,比方說只有為0的複製進去了。官方解釋此方法不能正確的將字元轉化為位元組,
//這就可能是因為jvm的機制更改,底層功能的更改,因為此方法表面上看並沒有轉位元組的操作
24
//比較常用的一個方法,返回指定名稱字元編碼的byte陣列
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, value, 0, value.length);//看似簡單 操作很深(1)
}
(1)
static byte[] encode(String charsetName, char[] ca, int off, int len)
throws UnsupportedEncodingException
{
StringEncoder se = deref(encoder);//encode是ThreadLocal類,實現資料的隔離。
//使用ThreadLocal的作用就是,當為該字串進行操作轉碼的時候,防止該物件發生了某些改變,就會發生很多意料之外的錯誤
String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;//如果不指定,預設的字元編碼
if ((se == null) || !(csn.equals(se.requestedCharsetName())
|| csn.equals(se.charsetName()))) {
se = null;
try {
Charset cs = lookupCharset(csn);//該方法底涉及的很深,大部分是驗證,所有關鍵的操作都放到了C語言,所以也沒有
//必要寫出來,但是實現的東西很簡單。就是把輸出的字串生成對應的字元編碼物件
if (cs != null)
se = new StringEncoder(cs, csn);
//這是一個生成StringEncode的靜態工程①
} catch (IllegalCharsetNameException x) {}
if (se == null)
throw new UnsupportedEncodingException (csn);//嚴謹的驗證,一般不會跑到這裡
set(encoder, se);//這一步就是把StringEncoder物件放進ThreadLocal裡。encoder就是ThreadLocal,同時也是
//StringEncoder物件的一個實體類。由StringEncoder物件自己呼叫
}
return se.encode(ca, off, len);②//準備工作完成,開始最後一步
}
(1)①
private StringEncoder(Charset cs, String rcn) {
this.requestedCharsetName = rcn;
this.cs = cs;
this.ce = cs.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
//看似高深,其實這是為物件賦值的另一種形式,也可稱為工廠模式,就相當於set,是底層眾多初始化物件的其中一種手段
//每個方法都是返回自己本身,這樣賦值之後就可以繼續調其方法。好處,就是簡單明瞭。
//在建立之初就可以直接賦值,並且後期維護時可以非常簡單。可以直接看到到底初始化了哪些資料
this.isTrusted = (cs.getClass().getClassLoader0() == null);//載入class
}
(1)②
byte[] encode(char[] ca, int off, int len) {
int en = scale(len, ce.maxBytesPerChar());//簡單的東西就不展現程式碼了,這一步僅僅是兩個引數相乘。
//第一個引數是字串的長度,第二個是每個字元生成的最大位元組數,得到的就是該字串的最大位元組數的長度
byte[] ba = new byte[en];//建立一個這麼長的byte陣列
if (len == 0)
return ba;//如果長度為0 就不存在轉碼,直接返回
if (ce instanceof ArrayEncoder) {
int blen = ((ArrayEncoder)ce).encode(ca, off, len, ba);
return safeTrim(ba, blen, cs, isTrusted);//如果為true,就跳出了jdk,很坑
//分析一下吧:ce是CharsetEncoder抽象類,ArrayEncoder是一個介面,相互之間沒有介面或者繼承關係,
//應該是 與本次邏輯無關的一部 判斷,大概是 其他地方呼叫會用到,因為應該是 進不來這一步
//看不到ArrayEncoder的原始碼,只能知道這麼多了
} else {
ce.reset();//對CharsetEncoder的一步初始化操作,只是設定他的屬性state為0
ByteBuffer bb = ByteBuffer.wrap(ba);//byte型別資料讀取緩衝區,專案沒用到過,所以具體解釋不清
//不過字面意思很好理解,就是一個提供緩衝的類
CharBuffer cb = CharBuffer.wrap(ca, off, len);//顧名思義,char型別緩衝區
try {
CoderResult cr = ce.encode(cb, bb, true);①
if (!cr.isUnderflow())
cr.throwException();
cr = ce.flush(bb);
if (!cr.isUnderflow())
cr.throwException();
} catch (CharacterCodingException x) {
// Substitution is always enabled,
// so this shouldn't happen
throw new Error(x);
}
return safeTrim(ba, bb.position(), cs, isTrusted);
}
}
(1)②①
public final CoderResult encode(CharBuffer in, ByteBuffer out,
boolean endOfInput){
//endOfinput 傳的固定的true
//另外兩個就是 把資料扔進換成 然後帶過來
int newState = endOfInput ? ST_END : ST_CODING;
//ST_END為2,ST_CODING為3:前面的為true,所以 newState為2
if ((state != ST_RESET) && (state != ST_CODING)
&& !(endOfInput && (state == ST_END)))
throwIllegalStateException(state, newState);
state = newState;//state由0變為2
for (;;) {//傾向於c語言的習慣。題外話:其實jdk的程式碼規範也沒那麼嚴謹,依舊能看出來,誰是誰寫的程式碼
//就是普通的for迴圈,從編譯的角度講,while的程式碼體積要大一點點,所以for要好一點點
//程式碼規範其實和程式碼效率並沒有那麼的貼合,儘管別的國家的程式碼規範實施的很好,但是中國程式設計師寫的程式碼顯然比外國
//效率是要高的。首先外國寫的專案幾乎沒什麼高併發,可在中國幾千幾萬還算是正常的,一般程式設計師都能應付
CoderResult cr;
try {
cr = encodeLoop(in, out);//編碼迴圈,c語言實現的,返回的類,是c對記憶體地址資料處理後對產生結果的總結。
} catch (BufferUnderflowException x) {
throw new CoderMalfunctionError(x);
} catch (BufferOverflowException x) {
throw new CoderMalfunctionError(x);
}
if (cr.isOverflow())
return cr;
if (cr.isUnderflow()) {
if (endOfInput && in.hasRemaining()) {
cr = CoderResult.malformedForLength(in.remaining());
// Fall through to malformed-input case
} else {
return cr;
}
}
CodingErrorAction action = null;
if (cr.isMalformed())
action = malformedInputAction;
else if (cr.isUnmappable())
action = unmappableCharacterAction;
else
assert false : cr.toString();
if (action == CodingErrorAction.REPORT)
return cr;
if (action == CodingErrorAction.REPLACE) {
if (out.remaining() < replacement.length)
return CoderResult.OVERFLOW;
out.put(replacement);
}
if ((action == CodingErrorAction.IGNORE)
|| (action == CodingErrorAction.REPLACE)) {
// Skip erroneous input either way
in.position(in.position() + cr.length());
continue;
}
assert false;
//斷言,執行的此處就肯定是錯誤的,也是意料之外發生的事情,但是肯定不是預期的,是錯的,斷言拋異常
//一些列12345的各種判斷,沒寫備註,無法搞清楚在判斷啥,但也算是到最底層了。這個getByte終於結束了
}
}
2018年11月14日 17:26:00,第一次更新1-12
2018年11月15日 11:56:25,第二次更新13-17
2018年11月17日 17:29:38,第三次更新18-23