水平有限,有誤的地方望批評指正。
文章從兩大方面講解,1、介紹,2、源碼解析
String類代表了java中的字符串,
一、String介紹
1、不可變類,線程安全。
不可變類在java語言中使用final關鍵字實現,final有三個作用,簡單的說修飾的類不可繼承,方法不可重寫,變量不可修改。而String類和存儲字符數據的Char數組就是用final修飾的,因此string類不可繼承,內部數據(char數組)不能修改,對String進行替換,裁剪,連接都是新生成一個String對象,因此String是不可變類,不可變類都是線程安全的,最典型的就是JAVA中的包裝類Integer,Long等。爲什麼不可變類是線程安全的呢?因爲String對象是無狀態對象,無狀態對象可以理解爲狀態不能改變的對象,這裏的狀態也可以理解爲對象裏的數據。
2、可共享
jdk7運行時常量池存儲在方法區,也就是永久代(hotspot),jdk8永久代被移除,運行時常量池存儲在本地內存的元空間中,運行時常量池存儲了字符串常量的引用,字符串常量存儲在堆中。java就是使用常量池來實現字符串共享的,下面我們看下例子
public static void main(String[] args) {
String a = "test";
String b = "test";
System.out.println(a == b);
String c = new String("test");
System.out.println(a == c);
}
結果是一個true,一個false。a和b都指向了常量池中的引用,因此他們的地址相同的,a==b也就爲true,而c指向的是在堆上新創建的對象的引用,和a不是指向同一個字符串地址,因此a==c爲false,javap對應的指令
大家查看劃線的幾個地方,從上往下第一處,第二處,第四處都是從常量池獲取字符傳test的引用,而第三處是new返回的引用,所以a和b地址一樣,a,b和c的地址不一樣。
3、簡單高效
程序中最常見的就是字符串拼接操作,java中可以直接使用+運算符來表示字符串拼接,我們看以下代碼。
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = s1 + s2 + "c";
System.out.println(s3);
}
查看編譯後的指令
1、創建一個StringBuilder對象
2、調用StringBuilder的構造函數
3、3,4,5步驟都是調用StringBuilder的append方法將s1,s2,和"c"拼接起來。
結論:字符串+運算被java編譯器編譯後變成新建一個StringBuilder對象,然後每個+會編譯成append方法,因此我們在一般情況下可以直接使用+拼接字符串,java爲什麼要這麼做呢,就是因爲性能,如果不使用StringBuilder拼接過程會產生很多中間String對象。當然有些情況下需要顯式使用StringBuilder,看下面代碼。
public static void main(String[] args) {
String s1 = "a";
for (int i = 0; i < 10; i++) {
s1 = s1 + i;
}
System.out.println(s1);
}
javap查看指令
1、1彈出棧頂兩個元素進行比較,如果第二個元素大於等於第一個元素就跳轉到36代碼處,36代碼處就是跳出for循環輸出結果System.out的地方,棧中彈出第一個元素是10第二個是常量i(先入後出),說明i大於等於10則循環結束。
2、2和3表示創建StringBuilder對象,4,5表示調用append拼接s1和i
3、6表示將拼接後的結果存儲在索引爲1的局部變量中
4、7表示將局部變量索引爲2的變量加1也就是1
5、8表示跳轉到指令5處,進行下一次比較和循環
結論:分析指令後我們發現for循環中使用+進行字符串拼接,編譯器編譯後每次都會在for循環內部生成一個StringBuilder對象,然後調用append拼接字符串,如果for循環過大會產生大量StringBuilder對象,引起性能問題,所以我們在循環外新建StringBuilder,循環內調用append操作,代碼比較簡單我們就不做展示了。
二、源碼解析
1、String實現了Serializable,Comparable<String>,CharSequence代表String是可序列化,可比較,字符序列。
2、主要屬性
char value[] 字符存儲數組
private int hash hash值
static final Comparator<String> CASE_INSENSITIVE_ORDER 忽略大小寫的比較器
3、常用方法
int hashCode() 計算對象的hashcode
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) { //1
char val[] = value;
for (int i = 0; i < value.length; i++) { //2
h = 31 * h + val[i];
}
hash = h; //3
}
return h; //3
}
1、如果h不爲0或者value的長度爲0則直接返回h,h不爲0的情況說明h已經被計算過直接返回,所以hashCode只會計算一次。
2、遍歷char數組中的每個字符值加上h*31,數組中的每個元素都參加運算讓結果更加準確,h*31使hashcode值更加分散,乘以31也可以被優化成位數操作,計算效率更快。
3、給h賦值後返回
static String join(CharSequence delimiter, CharSequence... elements) elements使用delimiter分隔開拼接後返回
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter); //1
Objects.requireNonNull(elements); //1
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter); //2
for (CharSequence cs: elements) { //3
joiner.add(cs);
}
return joiner.toString(); //4
}
1、驗證參數是否正確
2、創建分隔符爲delimiter的StringJoiner
3、遍歷可變長字符串參數,對每個字符串調用joiner.add,joiner.add內部由一個StringBuilder的value屬性,每次add就會使用value添加分隔符然後再添加字符串,value爲空則添加前綴再添加字符串,此處無前綴。
4、調用joiner.toString()返回,如果沒有後綴則返回value.toString,否則添加後綴後再返回toString,此處無後綴
static String format(String format, Object... args)
public Formatter format(Locale l, String format, Object ... args) {
ensureOpen();
// index of last argument referenced
int last = -1; //1
// last ordinary index
int lasto = -1; //1
FormatString[] fsa = parse(format); //2
for (int i = 0; i < fsa.length; i++) {
FormatString fs = fsa[i];
int index = fs.index();
try {
switch (index) {
case -2: // fixed string, "%n", or "%%" //3
fs.print(null, l);
break;
case -1: // relative index //4
if (last < 0 || (args != null && last > args.length - 1))
throw new MissingFormatArgumentException(fs.toString());
fs.print((args == null ? null : args[last]), l);
break;
case 0: // ordinary index //5
lasto++;
last = lasto;
if (args != null && lasto > args.length - 1)
throw new MissingFormatArgumentException(fs.toString());
fs.print((args == null ? null : args[lasto]), l);
break;
default: // explicit index //6
last = index - 1;
if (args != null && last > args.length - 1)
throw new MissingFormatArgumentException(fs.toString());
fs.print((args == null ? null : args[last]), l);
break;
}
} catch (IOException x) {
lastException = x;
}
}
return this; //7
}
1、last,lasto都指向參數索引位置。
2、parse(format)解析字符串,將分割後的普通字符串和格式化字符串添加到FormatString[]中,FormatString是個接口,有兩個實現類FixedString代表普通字符串,索引index爲-2,FormatSpecifier代表格式化字符,用來格式化參數,index有三種取值,0代表正常索引,也就是不指定索引位置的格式化字符,例如%s,%d,-1代表相對位置,取last位置的參數使用此格式化字符進行格式化,例如%<s,%<d, default代表顯式指定參數索引的參數,例如%1$s,%2$s
3、遍歷FormatString,index爲-2表示普通字符串,則直接使用Formatter裏的屬性a(StringBuilder)進行拼接
4、index爲-1,格式化字符使用相對位置,則使用上一個索引last的參數調用print進行格式化,格式化後的字符串使用屬性a拼接。
5、index爲0,格式化字符使用順序位置,lasto加1,指向當前位置的後一個參數,last賦值,對下一個位置字符串調用print格式化後,使用a拼接
6、default,格式化字符使用指定參數位置,使用index-1指向的參數調用print格式化,格式化後用a拼接
7、返回a.toString
String concat(String str) 當前字符串拼接str返回拼接後的字符串
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) { //1
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen); //2
str.getChars(buf, len); //3
return new String(buf, true); //4
}
1、如果字符串參數長度爲0,直接返回當前字符串。
2、Arrays.copyOf創建一個大小爲當前字符串長度加上參數字符串長度的數組,包含value的值,方法內部是新建一個大小爲len+otherLen的數組,然後調用System.arraycopy將value數組的內容複製到新建的數組中,並返回新數組,System.arraycopy是本地方法,由c,c++提供更高效的實現。
3、將Str字符的數組數據放入新數組len開頭到len+Str.length處,內部也是調用System.arraycopy。
4、新建字符串,構造參數爲新數組buf。
String[] split(String regex, int limit) 根據regrex分隔字符串,limit爲分隔字符串數量的限制
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)) //1
{
int off = 0; //2
int next = 0; //2
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) { //3
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) //4
return new String[]{this};
// Add remaining segment
if (!limited || list.size() < limit) //5
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0) { //6
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result); //7
}
return Pattern.compile(regex).split(this, limit); //1
}
1、判斷regrex是否是一個字符的字符串,.並且不是$|()[{^?*+\\字符之一,或者含有兩個字符的字符串第一個字符是反斜槓,第二個字符不是ascll數字或字母,否則採用正則表達式處理
2、off表示截取起始位置,next字符匹配的位置,limited是否有分隔字符串數量的限制
3、調用while循環,尋找字符匹配的位置,如果到達分隔數量的限制,直接將剩餘字符串添加到list中,否則截取off到next的字符串,也就是起始位置或上一個分隔符到當前分隔符的字符串。
3、如果off爲0,說明沒有匹配的字符,直接返回字符串
4、如果沒有分隔數量的限制或者沒有到達分隔數量的限制則添加剩餘字符串
5、如果沒有分隔數量的限制則去除尾部空字符串,4步驟有可能添加空字符串
6、字符串列表轉爲字符串數組後並返回。
String replace(char oldChar, char newChar) 替換所有oldchar字符爲newChar
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) { //1
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) { //2
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) { //3
buf[j] = val[j];
}
while (i < len) { //4
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
1、如果兩個字符相等直接返回當前字符串
2、找到第一個oldChar和newChar相等的位置i,新建char數組將小於i的字符放入新數組中
3、從i開始遍歷,如果i下標的字符和oldChar相等,則新數組下標i賦值newChar,否則新數組下標賦值原來的字符。
4、返回新建的String,參數數組爲新建的buf.
String substring(int beginIndex, int endIndex) 截取beginIndex到endIndex的字符串
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) { //1
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) { //1
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) { //1
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen); //2
}
1、判斷參數是否合法
2、直接調用String的構造函數創建新字符串,構造函數中使用Arrays.copyOfRange複製數據。
String trim() 清空字符串中前後的空格
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) { //1
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) { //2
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this; //3
}
1、第一個while循環計算索引從0開始第一個字符不爲空格的索引
2、第二個while循環計算索引從len開始第一個不爲空格的索引
3、存在空格則調用subString返回截取的去掉前後空格的字符串,否則返回當前字符串。
其他的方法代碼比較簡單,就不再贅述了,大家有時間可以自己看一下。
public boolean startsWith(String prefix, int toffset)
public boolean endsWith(String suffix)
lastIndexOf(int ch)
public int indexOf(int ch)
public boolean equals(Object anObject)
下篇文章給大家講解StringBuilder和StringBuffer