前言
现实世界中,人们的生活总是离不开电。作为人类活动、机械运转等各种各样最基本的一样东西,在整个世界运转中发挥着重大作用。在Java的世界里,也有它自己的电,几乎在绝大部分地方都能看到它的身影。它就是String 类。String 类由于使用频率高,内容点多,将其分为上下篇做介绍。上篇做源码分解,下篇做常用疑难点分析。
数据结构
在数据结构中,有一种叫‘串’的数据结构,它的底层由数组构成。你可以对这种结构进行操作、查找等等。String类,就是Java语言中对‘串’这种数据结构的实现。它模拟串的数据操作算法,可以实现对串的增删查改。这篇文章我将着重分析String类及其源码实现。掌握了这个类,就掌握了最基本的工具,可以在以后的开发生涯中使用String类时,非常的熟练和心有成竹。
概况图
这个类虽然有很多很多方法,但是可以分析,将其行为归类,分轻重缓急排序。最后重点介绍其常用及重要部分。
本章分析顺序
上述的api看着很多,其实按模块划分以后,大概分为几个方面。如图:
本章重点分析Api常用方法,重点方法,特殊方法三个部分。其所有方法已经按功能被划分为 【增】【删】【查】【改】【分割】【比较】【转换】和【其他】等部分。
到现在为止,做程序员已经是第八个月。随着业务的提炼、项目的训练以及自我的理解,感觉什么都离不开增、删、查、改。细究其原因,发现这些操作其实是对数据结构的基本操作。无论是线性表、树结构、图结构都有这些共性操作。String类作为线性结构中 串的一个JAVA实现,其基本操作也离不开增删查改。在此基础上,延伸出了针对不同数据类型的、针对不同返回值、针对不同入参值的Api。以下分类仅代表个人的理解,可能每个人对有些APi的理解不一样,划分就一样。
String类
(1)增
concat(String str)
(2)删
trim()
(3)查
长度 : length() isEmpty()
字符 : indexOf(int ch) indexOf(int ch, int fromIndex)
indexOf(String str) indexOf(String str, int fromIndex)
lastIndexOf(int ch) lastIndexOf(int ch, int fromIndex)
lastIndexOf(String str) lastIndexOf(String str, int fromIndex)
charAt(int index)
hash值: hashCode()
(4)改
replace(char oldChar, char newChar)
replace(CharSequence target, CharSequence replacement)
replaceFirst(String regex, String replacement)
replaceAll(String regex, String replacement)
(5)分割
割为数组: split(String regex) split(String regex, int limit)
割成子串: substring(int beginIndex) subSequence(int beginIndex, int endIndex)
substring(int beginIndex, int endIndex)
(6)转换
转为字节: getBytes() getBytes(Charset charset)
getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin)
转为字符: toCharArray()
转为字符串: toString()
大小写转换: toUpperCase() toUpperCase(Locale locale)
toLowerCase() toLowerCase(Locale locale)
(7)比较
整串比较: equals(Object anObject) contentEquals(CharSequence cs)
compareTo(String anotherString)
内部类比较器: CaseInsensitiveComparator类
整或子串比较: startsWith(String prefix) startsWith(String prefix, int toffset)
endsWith(String suffix)
contains(CharSequence s)
compareToIgnoreCase(String str)
equalsIgnoreCase(String anotherString)
(8)其他
static join(CharSequence delimiter, CharSequence... elements)
native String intern() // 这个native方法和常量池有关
static valueOf(Object o) // 这个静态valueOf方法对不同的数据类型有同样的操作。
开始之前
重点的Api大概就这些,使用率比较高。看着有点多,但是很多的方法是互相实现的,然后还有一些方法是非常简单的。所以需要分析的方法并不多。当然除了Api 还有构造器和内部私有方法,用以支撑这些Api的。这部分将会糅合在对应的Api方法中分析。
String 的内部属性很简单:内部维持着一个char 数组,和一个hash值,默认为0;
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
其他的,就是基于这个数组延伸出来的这么多功能。
下面来逐步分析源码。
一 、 增
concat(String str)
源码中是这样介绍的:
* Concatenates the specified string to the end of this string.
* 翻译: 将指定字符串追加在末尾
源码实现:
// 整个思路还是很简单的。
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); // 把新数组构建成字符串
}
二、 删
trim()
trim() 方法也被称为修剪方法。在用户的角度里经常有这样的疑问,输入的数据是对的,为什么查不到或者提示有误呢?因为在用户的角度,不会去关心前后是否会存在空格,因为空格在界面上都是看不见的。基于这个操作,trim()方法内部做了实现修整的方法。
源码实现:
public String trim() {
int len = value.length; // 尾指针 (从尾向头查找)
int st = 0; // 头指针 (从头向尾查找)
char[] val = value; /* avoid getfield opcode */
// 判断用的是ASCII码对比,空格或者空格以下的字符将会被清理,顺序从头到尾,获取第一个有效索引。
while ((st < len) && (val[st] <= ' ')) {
st++;
}
// 这里也一样,顺序从尾到头。获取最后一个有效字符的索引。
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
// 根据头指针 和 尾指针的最终位置,对原字符串进行截取,获取的字符串。
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
三、 查
长度 : length() isEmpty()
字符 : indexOf(int ch) indexOf(int ch, int fromIndex)
indexOf(String str) indexOf(String str, int fromIndex)
lastIndexOf(int ch) lastIndexOf(int ch, int fromIndex)
lastIndexOf(String str) lastIndexOf(String str, int fromIndex)
charAt(int index)
这个部分是和查询有关,length() 方法 和 isEmput() 方法很简单,这里不再做介绍。本段重点关注对串中字符的获取。
对字符获取的相关方法有九个,它们之间的关系和作用这里用图结构来表示。
从图上可以很清晰的看到,String类对字符串提供了两种方向的索引,上面IndexOf()方法是从头至尾的顺序遍历。lastIndexOf()是从尾至头的顺序遍历。虽然都有各自实现的底层方法,但是原理是差不多的。所以这里只分析一边indexOf()方法群,另一边lastIndexOf()方法群是差不多的。
在IndexOf里面,有 int 参数类型的 和 String 参数类型的。其区别有点类似Integer的parseInt()方法。下面来分析这两种参数引出的两组方法及底层实现。indexOf() 和 lastIndexOf ()方法都是返回查找字符所在的下标。
charAt(int index)是根据下标寻找相应的字符,和上面反着来的。
(1)indexOf(int ch)
indexOf(int ch)源码:
// 内部默认从下标 0 开始。返回的是一个出现ch的第一个下标。ch是什么呢?
// 源码中有解释,ch是一个Unicode码点,每一个字符在Unicode中都对应一个码点。所以ch其实是一个字符
* @param ch a character (Unicode code point).
*
public int indexOf(int ch) {
return indexOf(ch, 0);
}
indexOf(int ch, int fromIndex) 源码:
// 参数多了一个fromIndex
* @param fromIndex the index to start the search from.
* 翻译: 指定从第几个下标开始
*
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0; //也就是说起点允许为负数,不会报错
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1. -1>>>1 指的是最大值2^31
return -1;
}
//下面以 Character.MIN_SUPPLEMENTARY_CODE_POINT 为界,即补充码点最小值,暂时可以理解为一个临界点。
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i; // 这里通过遍历获得ch码点对应的字符,在串中的下标值。
}
}
return -1;
} else {
// 如果超过临界点,则调用 下面这个方法
return indexOfSupplementary(ch, fromIndex);
}
}
indexOfSupplementary(int ch, int fromIndex)源码:
private int indexOfSupplementary(int ch, int fromIndex) {
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
final char hi = Character.highSurrogate(ch);
final char lo = Character.lowSurrogate(ch);
final int max = value.length - 1;
for (int i = fromIndex; i < max; i++) {
if (value[i] == hi && value[i + 1] == lo) {// 这里也是通过循环遍历 来寻找对应的码点,
return i; // 然后找到字符所在串中的下标。具体的码点相关知识这里不做补充
}
}
}
return -1;
}
(2)indexOf(String str)
indexOf(String str)源码:
* @param str the substring to search for.
* // str指的是目标字符串,其源码内部是引用IndexOf(String str, Int fromIndex)
*
public int indexOf(String str) {
return indexOf(str, 0);
}
indexOf(String str , int fromIndex)源码:
// 其源码内部直接调用的底层方法,因此下面详细分析底层方法。
// fromIndex 是指从哪个下标开始。
public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)源码:【这个暂时没看懂,变量sourceCount和targetCount没怎么明白】
/**
* Code shared by String and StringBuffer to do searches. The
* source is the character array being searched, and the target
* is the string being searched for.
* 翻译: 这是一个由String 和 StringBuffer 共享的方法
*
* @param source the characters being searched. // 源字符串
* @param sourceOffset offset of the source string. // 源串中的偏移量,意思就是从开始寻找的位置偏移后的下标,比如字符串“abcdefg”,开始遍历下标为1,但是偏移量为2,因此开始下标就要从1+2=3开始。
* @param sourceCount count of the source string. // 源串计数变量
* @param target the characters being searched for. // 目标字符串
* @param targetOffset offset of the target string. // 目标串中的偏移量
* @param targetCount count of the target string. // 目标串计数变量
* @param fromIndex the index to begin searching from. // 源串中的开始下标
*/
static int indexOf(char[] source, int sourceOffset, int sourceCount,char[] target,
int targetOffset, int targetCount, int fromIndex) {
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
四、改
重点分析:replace(char oldChar, char newChar)
非重点分析:replace(CharSequence target, CharSequence replacement)
replaceFirst(String regex, String replacement)
replaceAll(String regex, String replacement)
上面四个方法都是替换,针对不同的业务需求,延伸出来的Api。其中这里只讲replace(char oldChar, char newChar)方法。
(1) replace(char oldChar, char newChar)源码:
// 这是一个替换字符操作的Api,不是字符串的操作。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
// 下面遍历,找出目标字符在字符串中的第一个下标 i
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];
}
// 从第一个目标的下标开始再次向后遍历,如果是目标字符,则用newChar 来替换。
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
// 最后将新数组生成字符串,返回。
return new String(buf, true);
}
}
return this;
}
源码大概是这样,但是我看了源码以后,很纳闷!
为什么要搞得这么复杂呢,直接遍历字符串,加个if判断不就完了么?其时间复杂度和空间复杂度都更低一点。百思不得其解之下,我贴出我所表达的这段代码,请大神指出源码这样表达的用意,指点其深奥的地方。
// StringMo 是我模拟String的一个类,内部也是维护着一个字符数组。下面贴出replace方法,我觉得可以直接如下这样:
public StringMo replace(char oldChar,char newChar){
if (oldChar == newChar){
return this;
}
char[] arr = val;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == oldChar){
arr[i] = newChar;
}
}
return new StringMo(val);
}
五、分割
割为数组: split(String regex) split(String regex, int limit)
割成子串: substring(int beginIndex) subSequence(int beginIndex, int endIndex)
substring(int beginIndex, int endIndex)
String的分割有两种,一种是分割成字符串数组,另一种是截取成子串。
分割成数组为split() 方法,有两个重载方法。我们下面来分析一下这个方法。
(1)split(String regex)
// regex 就是正则表达式,或叫做分割条件,如: “a-b-c-d”.split(”-“)得到的就是a b c d四个字符串组成的字符串数组。
public String[] split(String regex) {
return split(regex, 0); // 这个0 是指,数组里面的空字符串单元将会被去掉。详情原因请看下面的方法。
}
(2)split(String regex, int limit)
与split(String regex) 不同的是,多了一个入参 limit,在源码中是这样介绍的。
* @param limit
*
* <p>{@code limit}参数控制应用模式,因此会影响结果的长度阵列。
* 如果极限limit大于零,则模式最多应用limit-1次,数组的长度将不大于limit,
* 并且数组的最后一个条目将包含最后一个匹配分隔符以外的所有输入。
* 如果limit如果为非正,则该模式将被应用可能,数组可以有任意长度。
* 如果limit为零,则该模式将被尽可能多次应用,数组可以任何长度,都将丢弃尾随的空字符串。
*
通俗的讲,limit控制这个方式的模式,模式有三种,分别为 正、非正、零。limit 意思就是最大分割次数,(即使最大可分割次数超过limit 次)。
相关源码如下:
public String[] split(String regex, int limit) {
char ch = 0;
// 条件这一块做了分割,好看点。 整个结构是 (a1 || a2) && b
if (((regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
// a1: 上面这句的意思是,如果正则表达式只有一个字符,并且不是字符串".$|()[{^?*+\\"中的一个,则为true。
(regex.length() == 2 && regex.charAt(0) == '\\' && (((ch = regex.charAt(1))-'0') | ('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0))
// a2: 如果长度为2,并且第一个字符为 \,并且 第二个字符 ( ch - '0' | '9' - ch ) < 0 ,才为true。中间是或运算,计算机中0 为 正,1为负。或运算小于0的条件就是,字符其中一个为负。所以这个意思就是 ch 不能是在 ‘0’ ~ ‘9’(包含)之间 并且 不在‘a’ ~ 'z' 之间,并且不在 'A'~'Z'之间。
&&
(ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE))
// b: 最后要求ch 在Character.MIN_HIGH_SURROGATE 到 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) {
// off初始值为0,next 为字符串中每一次 ch 出现的下标。它会随着偏移值的改变而改变。
/**
* limit 模式为 零 时 或模式为 正 时
*
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
// 将每一个目标串分割出来 放入list中
off = next + 1;
// 计算下一次循环从哪里开始,off可以看做下一次开始遍历的起点。
// 举个例子:“a-ab-abc-d".splict(”-“), off第一次的偏移量是0,next是”-“第一次的下标。
// 所以下一次开始应该是从next+1 ,开始向后索引第二个“-”; 直到next == -1跳出循环
} else { // last one
/**
* limit 模式为正时的最后一段
*
//assert (list.size() == limit - 1);
list.add(substring(off, value.length)); // limit限值下的最后末尾一段装进list中,如果limit = 1,则就是原串。
off = value.length; // 偏移值移到最后
break;
}
}
// If no match was found, return this
// 如果没有进行分割,则返回一个包含原串的数组
if (off == 0)
return new String[]{this};
// Add remaining segment
// 如果是模式零,添加最后剩下的一段。因为索引只到达最后一个目标字符,不到达字符串的末尾。
// 如果是模式正 , 添加了最后一段。与上面的else 不同的是,上面else的情况是指可分割次数 > limit情况下。
// 这个是指 可分割次数 <= limit 的情况下。
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);
}
// Pattern.compile这个部分不做研究
return Pattern.compile(regex).split(this, limit);
}
最后简单总结这两个方法:
split(String regex)其内部调用的 split(String regex,0【int limit】)。0代表一种模式,
如果为0时,字符串将按最大可分割次数来做分割,分割产生的空字符串将会被去掉。
如果大于0时,实际分割情况就得根据 最大可分割次数与limit之间的关系;分为两种
(1)可分割次数 > limit; 将会得到 limit +1长度的子串数组。
(2)可分割次数 <= limit ; 将会得到最大可分割次数的子串数组。
(3)limit = 0 ; 将会得到最大可分割次数的子串,除去空字符串后生成的子串数组。
(3)substring(int beginIndex)
subString 应该是属于截取,它将会从源串中截取一部分作为新字符串。其内部源码如下:
// 代码比较简答,有一个注释特别重要,
* @param beginIndex the beginning index, inclusive.
* 翻译: 下标beginIndex 将会包含在内。
*
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
上面subString就是从指定下标开始截取到最后,并且包含了指定下标的字符。
(4)substring(int beginIndex, int endIndex)
代码差不多,但是注释非常重要。
* @param beginIndex the beginning index, inclusive.
* @param endIndex the ending index, exclusive.
* // 翻译: 起点下标包含在内,结束下标不包含在内。也就是说这是一个左闭右开的方法。
*
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);
}
(5) subSequence(int beginIndex, int endIndex)
内部调用的substring(beginIndex, endIndex);也是左闭右开的区间。
public CharSequence subSequence(int beginIndex, int endIndex) {
return this.substring(beginIndex, endIndex);
}
六、转换
转为字节: getBytes() getBytes(Charset charset) // 转换为字节 或者 按指定编码转换为字节
转为字符: toCharArray() // 转换为char 数组
转为字符串: toString()
大小写转换: toUpperCase() toUpperCase(Locale locale) // 全部转换为大写
toLowerCase() toLowerCase(Locale locale) // 全部转换为小写
这部分比较简单。
七、比较
字符串比较这部分,
整串比较: equals(Object anObject) contentEquals(CharSequence cs)
compareTo(String anotherString)
内部类比较器: CaseInsensitiveComparator类
整或子串比较: startsWith(String prefix) startsWith(String prefix, int toffset)
endsWith(String suffix)
contains(CharSequence s)
compareToIgnoreCase(String str)
equalsIgnoreCase(String anotherString)
先来看整串的比较有哪些。
(1) equals(Object anObject)
它的参数是一个Object,也就是任何对象都可以传过来。其内部的比较分为三层(对象类型、长度、单个字符对比)。
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;
}
(2)contentEquals(CharSequence cs)
它是一个入参是一个字符序列,也就是 String、StringBuffer、StringBuilder 都可以传入。因此它内部做了类型判断。方法和equals()基本一致。
public boolean contentEquals(CharSequence cs) {
// Argument is a StringBuffer, StringBuilder
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// Argument is a String
if (cs instanceof String) {
return equals(cs);
}
// Argument is a generic CharSequence
char v1[] = value;
int n = v1.length;
if (n != cs.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != cs.charAt(i)) {
return false;
}
}
return true;
}
(2)compareTo(String anotherString)
这个方法需要注意的是,它的返回值是一个int,是一个差值。差值是怎么计算的呢?
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; // 差值返回的是从头开始遍历,第一个不同字符的ASCII码差值。
}
k++;
}
return len1 - len2; // 否则返回两者的长度差,其实到了这里,其中一个字符串肯定为另一个字符串的子串。
}
八、 其他
join(CharSequence delimiter, CharSequence... elements)
join(CharSequence delimiter, Iterable<? extends CharSequence> elements)
native String intern() // 这个native方法和常量池有关
static valueOf(Object o) // 这个静态valueOf方法对不同的数据类型有同样的操作。
(1) join()方法
先来看一下join() 方法,有两个重载。其中的参数类型是CharSequence,然后还有可变长度参数列表 elements。
a.
CharSequence 是 java.lang 包下面的一个接口,用来表示字符串的行为,其子类实现一般有三个:String、StringBuffer、StringBuilder。所以CharSequence可以看成是这三个类的父类。当这个父类做参数时,可以传入不同的子类,是Java 多态实现的一种,也叫做向上引用。所以这里CharSequence 做参数时,可以传入String 或 StringBuffer 或者 StringBuilder。
b.
可变长度参数列表是Java 5.0以后的新定义,在参数后面加上三个点,如:Object… ,表示这是需要传入一个Object数组 或者 多个 Object对象参数。
在join(CharSequence delimiter,CharSequence… elements) 方法中,可以传入 join(“-”,“a”,“b”,“c”);
也可以传入 join(“-”,“a”,“b”); 或者可以传入 join(“-”,StringBuffer[]);
join(CharSequence delimiter, CharSequence... elements)
join(CharSequence delimiter, Iterable<? extends CharSequence> elements)
join()方法的作用就是,用第一个入参字符串delimiter,来连接后面参数列表中的各个字符串。它是一个静态方法。相当于是一个工具类。
(2)valueOf() 方法群
它与基本类型的包装类的valueOf类似,用一个入参来构造一个新的字符串,每一个方法都有new,入参包括八种基本数据类型、对象类型等等,这里不再一一罗列。
public static String valueOf(Object obj) { return (obj == null) ? "null" : obj.toString() }
public static String valueOf(char data[]) { return new String(data); }
public static String valueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String valueOf(boolean b) { return b ? "true" : "false";}
...
(3)native String intern()方法
这个方法涉及到常量池。这是一个本地方法。它的注释是这样写的。
/**
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* 翻译: String 的常量池中,最开始是空的,由String类维护着。
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned.
* 翻译: 当intern()方法被调用时,如果常量池中,已经存在一个通过equal方法比较和这个字符串相等的对象了,
* 则返回常量池中的这个对象。
* Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* 翻译:否则,就把这个对象加入到常量池中,然后返回常量池中的这个对象。
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* 翻译:任意两个字符串 s 和 t,如果要满足 s.intern() = t.intern(),则他们一定满足 s.equals(t);
* 不知道我理解得对不对。
* <p>
* All literal strings and string-valued constant expressions are
* interned.
* 翻译:所有的常量表达式都被调用了 intern() 方法。
*
实例演示
char[] s = {'a'};
String t = new String(s);
String m = t.intern();
System.out.println(t == m );
// 结果: ture
这个例子说明了,对象 t 被创建在堆中,常量池 m 只是引用堆中的地址。
实例演示
char[] t = {'a'};
String s = new String(t);
String a = "a";
System.out.println(a == s);
// 结果: false
这个例子说明了,两种不同创建方式,在堆里创建了两个对象。
实例演示
char[] t = {'a'};
String s = new String(t);
s.intern();
String a = "a";
System.out.println(a == s);
// 结果:true
这个例子说明了,通过intern()方法,将s指向的对象,加入了String的常量池。变量a会先去常量池中
寻找,发现有一个a了,于是引用了常量池这个地址,而常量池这个地址就是 s 的地址。
实例演示
String b = "a";
String a = "a";
System.out.println(a == b);
结果: true
// 这个例子说明,变量a 和变量b 都引用的同一个对象,也只生成了一个对象。
这个对象肯定不是变量a的时候生成的,所以一定是变量b的时候生成的。
变量a 没有生成对象,只是引用了变量b 指向的对象。
所以判断出这样直接赋值时,会先去常量池中寻找,如果没有,才会在堆中创建一个新的对象。
实例演示
String b = new String("a");
b.intern();
String a = "a";
结果:false
// 这个例子反向推理,因为是false,变量a会先到常量池查找,发现有一个“a”,但是不是对象b,
所以在执行b.intern( )方法前,常量池中已经有了一个对象,这个对象从哪里来呢?
在变量b 构造对象之前,也就是new之前,需要一个入参,这个入参就是“a”字符串.所以在获得这个入参时,
程序默认执行了 类似: String xxx = "a" 的操作。只不过这个操作是系统执行的,没有xxx变量引用。