String类源码阅读
String
初学java的时候被问最多的一个问题就是String在java里是对象还是基本类型, 现在很明显, 这是个类, String其实也就是个字符数组
private final char value[];
构造器
有一说一 这构造器估计是最多的一个类吧?
// 无参构造器, 原英文注释也说这个构造器是不必要的, 因为String本身的value就是final修饰的
// 它不可变, 所以直接String a = ""就好了, 除非你想要一个新的对象
// 这里的""之所以有value是因为""其实在内存中也是一个String对象
public String() {
this.value = "".value;
}
// 可以理解为深拷贝一个String对象, 我们最常用的String a = new String("123")就是这个构造器
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// 传入一个char数组, 这里的copyOf是浅拷贝操作
// 下面有一个关于这个构造器的实验
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
// 这个构造器赋值的value是传入的value的offset下标开始, 到offset+count结束
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);
}
// copyOfRange方法也是一个浅拷贝
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
// 这个构造器入参是一个int数组
public String(int[] codePoints, int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= codePoints.length) {
this.value = "".value;
return;
}
}
// 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];
// 这个方法将c右移16位, 检测这个c高十六位是否为0
if (Character.isBmpCodePoint(c))
continue;
// 因为int是32位的而char是16位的, 所以一个int是可以用两个char表示的
// 这里是判断int的高16位如果比最大的unicode要小, 就说明这个int需要用两个char表示
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;
// 这一步就是将这个c用两个char来储存
else
Character.toSurrogates(c, v, j++);
}
this.value = v;
}
// 这里就是将byte用charsetName编码储存了
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
}
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
// 检测数组越界
checkBounds(bytes, offset, length);
// 进入StringCoding类的decode方法, 将bytes从offset到offset+length用charsetName
// 编码返回一个char[]
this.value = StringCoding.decode(charsetName, bytes, offset, length);
}
测试入参为char[]的构造器
// 这里的输出是123, 证明了虽然是copyOf浅拷贝对value赋值的, 但是由于value是final修饰的
// 所以改变原数组不会对String有影响
public static void main(String[] args) {
char[] val = new char[]{'1', '2', '3'};
String b = new String(val);
val[0] = '2';
System.out.println(b);
}
由于String构造器是在是太多了, 所以只挑一些重要的
init和cinit
也是历史遗留问题, 顺便在这里讲一下init和cinit的区别, 我们都知道init是对象实例化的时候所调用的构造器方法
public class StringTest {
public static void main(String[] args) {
String a = new String();
}
}
用javap来看看字节码
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/String
3: dup
4: invokespecial #3 // Method java/lang/String."<init>":()V
7: astore_1
8: return
可以看见我们new一个对象的这个操作执行的是init方法
而与之对应的还有个cinit, 这个cinit是在虚拟机对类进行加载的时候所使用的构造器,
在jvm的类加载阶段中是有一个初始化阶段的, 下面是深入理解java虚拟机一书中的原文
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通 过程序编码制定的主观计划去初始化类变量和其他资源。我们也可以从另外一种更直接的形式来表 达:初始化阶段就是执行类构造器clinit()方法的过程。clinit()并不是程序员在Java代码中直接编写 的方法,它是Javac编译器的自动生成物
这里可以看见cinit其实可能跟我们无关, 它是编译器生成的产物, 在这个阶段jvm会给类中的静态变量赋值(这里的静态变量在准备阶段会赋初始值)
getChars && getBytes
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);
}
// arraycopy方法将value从srcBegin开始到srcEnd复制到dst
// 下标位置是dstBegin到dstBegin + srcEnd - srcBegin
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
// 常用的应该是这个
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
// 就是进入StringCoding类的encode方法用charsetName来解码
return StringCoding.encode(charsetName, value, 0, value.length);
}
equals
在object中的equals是比较引用是否相等, 而String中则将equals方法重写了
public boolean equals(Object anObject) {
// 引用相等, 可以判断是同一个对象
if (this == anObject) {
return true;
}
// 传入的如果不是String类型, 那肯定不相等
if (anObject instanceof String) {
// 直接转为String
String anotherString = (String)anObject;
int n = value.length;
// 如果字符串长度不相等, 那两个字符串一定不相等
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 对比字符串每一位是否相等, 只要不等就是false
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
compareTo
比较两个字符串的大小
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;
// 这里从0开始遍历, 返回下标数字的大小
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
// 如果c1大于c2, 那么返回的是正数
if (c1 != c2) {
return c1 - c2;
}
k++;
}
// 如果上面一直都是相等, 那么用长度比较
return len1 - len2;
}
返回正数就是当前的字符串大, 返回负数就是入参大
hashCode
这里是计算hashCode的方法, 之前也写过一篇相同hashCode的String但是值不同的文章
两个字符串的hashCode相同但这两个字符串不相等的情况
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;
}
可知
str.charAt(0) * 31^(n-1) + str.charAt(1) * 31^(n-2) + … + str.charAt(n-1)
所以这个hashCode与object的不一样, 在object中hashCode是一个本地方法, 是根据内存地址计算得出的
至于String为什么要重写hashCode, 这里我有一个解释: 如果不重写hashCode那么我们使用equals判断出来相等的String他的hashCode可能就不相等, 这样的话在HashMap中他们就可能不在同一个桶中, 所以就不可能使用HashMap, 这就是HashMap的key一定要是重写过这两个方法的原因
indexOf
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.
return -1;
}
// Character.MIN_SUPPLEMENTARY_CODE_POINT这个值是65535
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;
// 从起始点开始寻找ch, 返回下标
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
经过我自己的测试后发现:BmpCodePoint代码点是65535是2的16次方,刚好是两个字节(即一个字)的大小。在超出两个字节后只能算是有效的代码点,并非是BmpCodePoint代码点。从代码中也可看出,BmpCodePoint代码点的整数是可以直接强转成char类型的。在java中char类型刚好占2个字节,在2个字节以内的整数都可以直接强转换成char类型!
这里需要了解Unicode字符的原理,以及代码点的概念!
————————————————
版权声明:本文为CSDN博主「岁月丶丿静好」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013035972/article/details/51954177/
这里是转载的一段为什么要对这个ch进行判断的原因
lastIndexOf与indexOf一样, 仅仅只是从后往前遍历而已
subString
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);
}
// 使用了一个三元表达式这里的语义可以理解为
// if(beginIndex == 0 && endIndex == value.length) this;
// else new String(value, beginIndex, subLen)
// 其实就是判断我要获得的子字符串是不是就是当前字符串, 如果不是才new一个回去
// 这个构造器上面也有讲到
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
concat
将str接到this的后面
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
// 获取buf, 这里的copyOf是将value浅拷贝到一个新的数组中返回, 数组长度是len + otherLen
char buf[] = Arrays.copyOf(value, len + otherLen);
// 这里使用了getChars方法, 将自己的value放入buf, 位置是len~len+otherLen
str.getChars(buf, len);
// 使用拼接好的char[]来构造一个新的String返回
return new String(buf, true);
}
replace
public String replace(char oldChar, char newChar) {
// 如果新的跟老的一样就无需替换
if (oldChar != newChar) {
int len = value.length;
int i = -1;
// 因为value是final修饰的, 所以这里必须开一个新的数组进行修改操作
char[] val = value; /* avoid getfield opcode */
// 寻找oldChar在val中的位置
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
// 这里是在val中寻找到了这个oldChar进入的if语句
if (i < len) {
char buf[] = new char[len];
// 在i之前都无oldChar出现过
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
// 在i之后可能有多个oldChar出现, 所以用循环寻找替换
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
// 替换结束之后用buf生成一个新的String对象返回
return new String(buf, true);
}
}
return this;
}
matches
这里是正则表达式相关的内容, 以后再看
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
contains
这里直接是使用indexOf方法的, 因为indexOf返回的是第一次搜索到的下标
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
split
split也是一个常用的api, 根据输入的字符串分割字符串返回一个字符串数组
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;
// 如果regex的长度是1, 那么regex不可以是".$|()[{^?*+\\"这些字符
// 或者regex的长度是2, 那么第二个字符不属于0-9,a-z,A-Z
// 满足上述两条的一条并且满足ch属于某一范围时才可以进入if
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<>();
// ch在String中还存在, 我们这里假设limit就是0
while ((next = indexOf(ch, off)) != -1) {
// !limited是true
if (!limited || list.size() < limit - 1) {
// list中加入off到next这一段, 达成分割目的
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
// 把最后一段加入list
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0) {
// 如果list不为空并且list的末尾字符串长度为0, 就把大小-1
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
// 这里就是放入list中不为空的元素
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
// 这里应该是regex是个正则表达式, 就调用正则的分割
return Pattern.compile(regex).split(this, limit);
}
intern
最后讲讲intern, 之前有一个面试题是这样的
String a = "ab";
String b = new String("ab");
System.out.println(a == b);
- 输出什么
- 输出false
- 那么对b进行什么操作可以输出true
- b.intern()
intern这个方法在String中是个本地方法
public native String intern();
学习过jvm之后就会明白, String a = “ab” 这里的ab是分配在常量池里的, 而new String是分配在堆上的一个新对象, 在java8中, 移除了永久代这个概念
到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta- space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
所以常量池是被放入了java堆中, 所以b调用intern方法就会将自己的value在常量池中分配, 如果常量池中已经存在则直接引用, 不存在就创建, 所以这个时候a和b做==判断就会返回true, 他们实质指向的都是常量池中的"ab"
后记
String中的方法实在是太多, 理解了jvm之后去看源码真的能透彻很多, 就比如intern, 若是不理解java虚拟机规范中的运行时数据区域, 实在会不明白这个操作