有两种方式可以构造一个String对象:
String s1 = "dog";
String s2 = new String("dog");
①第一种构造方式是直接从字符串常量池中取得一个字符串对象"dog",然后s1指向常量池中对应的位置。
需要注意的是,当试图从字符串常量池中取得"dog"对象时,发现没有,则会在池中创建该对象。当下一次又要从池中获取"dog"对象时,可以直接取得。这是一种缓存思想。
②第二种构造方式是在堆中申请一块区域,使用构造器创建一个String对象。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
......
//使用构造器创建对象
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
}
在第二种创建方式中,我们调用了构造方法,首先会进行参数传递,也就是将"dog"赋值给original。此时又进行了第一种创建方式,但是现在可以直接从字符串常量池中取得“dog”对象。然后,在构造方法中,进行对象的浅拷贝。也就是说它们的成员char型数组是同一个。
String类中持有一个char型数组,该数组类型被限制为 private final,即数组内容不可改变。
/** The value is used for character storage. */
private final char value[];
由于String内部所持有的字符序列不可变,那么其提供的所有的拼接、剪裁字符串的操作,实际上都返回了一个新的String对象,而不是原对象。
String的不可变性是字符串常量池设计的基础:由于字符串是不可变的,所以无需担心常量池中的数据会发生冲突,每个字符串对象都是独一无二的。
字符串常量池所处的位置:
此外,String类本身也是不可以被继承的,它被声明为final class。
①ASCII字符集。
ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)主要用于对ASCII字符集进行单字节编码。ASCII字符集仅覆盖了控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。
它使用7位表示一个字符,这样只能支持128个字符。而后扩展到使用8位表示一个字符,共可表示256个字符。
②Unicode字符集。
由于ASCII字符集支持的字符太少,因此出现了Unicode字符集。Unicode把所有语言都统一到一套编码里,在表示一个Unicode的字符时,通常会用“U+”然后紧接着一组十六进制的数字来表示这一个字符。
在基本多文种平面(Basic Multilingual Plane, BMP, 简称为“零号平面”)里的所有字符,要用4位十六进制数,即U+0000到U+FFFF。比如”汉“对应的数字是U+6c49(十进制27721),”字“对应的数字是U+5b57(十进制23383)。这里的数字也就是上面所说的”代码点“。
零号平面有一个专用区:0xE000-0xF8FF,有6400个码位。零号平面的0xD800-0xDFFF,共2048个码位,是一个被称作代理区(Surrogate)的特殊区域。
在零号平面以外的字符则需要使用5位或6位十六进制数表示,即U+10000到U+10FFFF的数字。
在Unicode中,有多种方式可以将数字表示成程序中的数据,包括:UTF-8、UTF-16、UTF-32。UTF,全称Unicode Transformation Format,意思是Unicode转换格式,即怎样将Unicode定义的数字转换成程序数据。
1)UTF-8
UTF-8以字节为单位对Unicode进行编码。编码方式如下:
Unicode编码(十六进制) | UTF-8字节流(二进制) |
---|---|
000000-00007F | 0xxxxxxx |
000080-0007FF | 110xxxxx 10xxxxxx |
000800-00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000-10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
UTF-8是一种可变长字符编码,也是一种前缀码。
UTF-8使用1到4个字节为每个字符编码:
java代码的验证:
通过对字符串调用getBytes()方法,实际上是对该字符串进行编码,然后将编码结果存放在byte数组中。该方法中默认的字符编码方式是 UTF-8,当然也可以传入其他的编码方式。
String str = "汉";
byte[] bytes = str.getBytes();
//bytes数组 = {-26,-79,-119}
再将字节数组解码成字符串,这里默认的解码方式也是UTF-8:
String str = "汉";
byte[] bytes = str.getBytes();
String s = new String(bytes);//“汉"
注意:以什么方式编码的,就要以什么方式解码。
2) UTF-16
UTF-16编码以16位无符号整数为单位。
如果Unicode编码不超过0xFFFF,则其UTF-16编码就是Unicode编码对应的16位无符号整数;
如果Unicode编码 (假设为U) 超过0xFFFF,我们先计算U’=U-0x10000,然后将U’写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
3) UTF-32
UTF-32编码以32位无符号整数为单位。Unicode的UTF-32编码就是其对应的32位无符号整数。
java使用Unicode字符集。
java中的char类型是16位无符号基本数据类型,用于存储Unicode字符。char类型使用UTF-16编码,也即是说Unicode编码不超过0xFFFF只需要使用一个char;如果超出了0xFFFF,则使用2个char。汉字可以存储在一个char中。
String str = "汉";
byte[] bytes = str.getBytes();
在 jdk8 中,String中使用char数组存储字符序列,因此,字符串"汉"仅需使用一个char来存。
当字符串str调用getBytes方法时,将对char数组以UTF-8的方式进行编码,将"汉"对应的Unicode编码重新使用三字节编码,得到 -26 -79 -119。
在 jdk9 中,String中改用byte数组来存储字符序列。因为很多拉丁系语言的字符,使用16位char会造成了一定的空间浪费。使用byte来存储,可以更加紧凑,带来更小的内存占用和更快的操作速度。并且,底层的改变对java字符串的行为没有任何大的影响,这个特性对于绝大部分应用来说是透明的,绝大部分情况不需要修改已有代码。
// since java 9.0
private final byte[] value;
现在,我们通过代码来看看是如何将Unicode字符存储在byte数组中的。
String s1 = "a";
String s2 = "汉";
debug模式下,s1的byte数组值为{97},s2的byte的数组值为{73, 108}。
"a"的Unicode编码为0x61,对应字节的十进制为97,直接将这个单字节存放在1个byte中。
而"汉"的Unicode编码为0x6c49,对应字节的十进制数为108 73,直接将这个16位双字节的Unicode编码存入byte数组中,但是存储时低位字节73放在了低端地址,高位字节108放在了高端地址,这是一种小端模式(Little endian)。
而在调用getBytes方法将Unicode数字通过UTF-16编码为字节数组时,前两位的-2,-1,即十六进制下的FEFF(补码),FEFF表示存储采用大端模式,而FFFE表示使用小端模式。因此,这里采用的是大端模式,先存储了高位字节108,后存储了低位字节73。
String s1 = "a";
String s2 = "汉";
byte[] bytes1 = s1.getBytes("UTF-16");//{-2,-1,0,97}
byte[] bytes2 = s2.getBytes("UTF-16");//{-2,-1,108,73}
然而,原char数组实现方式下,字符串的最大长度就是数组本身的长度。当使用byte数组时,数组长度相同的情况下,存储能力则退化一倍。
equals方法用于判断两个字符串的字符序列是否相同。
public boolean equals(Object anObject) {
//首先判断本字符串与要比较的对象是否地址相同,如果地址相同,则为同一个对象
if (this == anObject) {
return true;
}
//其次,判断传入的对象是否是String类型的,必须都是String类型的对象才可以比较
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;
}
忽略大小写比较两个字符串是否相等。
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
根据字符的编码顺序,从左往右比较两个字符串谁大谁小。如果某个字符串是另一个字符串的前缀子串,则返回调用者和被比较者的长度之差。
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;
}
比较字符串是否以某前缀开始,或者以某后缀结束,都可以得益于下面方法。
判断从某个偏移位置开始,是否以传入的前缀开始。
比较前缀可以调用startsWith(prefix, 0);比较后缀可以调用startsWith(suffix, value.length - suffix.value.length)。
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
//取得原字符串的偏移位置
int to = toffset;
char pa[] = prefix.value;
//传入的前缀子串的起始位置
int po = 0;
int pc = prefix.value.length;
//如果偏移位置小于0,或者偏移位置开始+前缀子串的长度已经超出了原字符串的长度,则直接返回false
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
//一切就绪后,就可以开始一个字符一个字符地比较了,直到出现两个字符不一致,就返回false
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
1)寻找子串在原串中第一次出现的位置。String类中使用了朴素的字符串匹配方式,一个一个位置依次匹配。
/**
* @param source 原字符串
* @param sourceOffset 原字符串的偏移位置
* @param sourceCount 原字符串的长度
* @param target 目标字符串
* @param targetOffset 目标字符串的偏移位置
* @param targetCount 目标字符串的长度
* @param fromIndex 从哪个位置开始搜索.
*/
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
//如果起始搜索位置超出了原字符串的长度
if (fromIndex >= sourceCount) {
//如果目标字符串长度为0,则返回原串的长度,否则返回-1
return (targetCount == 0 ? sourceCount : -1);
}
//纠正起始搜索位置
if (fromIndex < 0) {
fromIndex = 0;
}
//如果目标子串长度为0,则返回起始搜索位置
if (targetCount == 0) {
return fromIndex;
}
char first = target[targetOffset];
//目标子串最多只可能出现在原串起始位置加上两串长度之差的位置max,也就是说最多只需要遍历到原串的max位置
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
//先找到第一个相同字符的位置
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
//找到了第一个相同的字符,现在开始判断剩余的字符
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) {
//找到了目标子串在原串中第一次出现的位置。
return i - sourceOffset;
}
}
}
return -1;
}
2)寻找子串在原串中最后一次出现的位置。依然使用了朴素的字符串匹配方式,从最后一个相同的元素开始一一匹配。
static int lastIndexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
//可能的最后起始位置
int rightIndex = sourceCount - targetCount;
if (fromIndex < 0) {
return -1;
}
if (fromIndex > rightIndex) {
fromIndex = rightIndex;
}
/* Empty string always matches. */
if (targetCount == 0) {
return fromIndex;
}
//目标子串的最后一个位置
int strLastIndex = targetOffset + targetCount - 1;
//目标子串的最后一个元素
char strLastChar = target[strLastIndex];
//原串中的可能的最前位置
int min = sourceOffset + targetCount - 1;
//起始搜索位置
int i = min + fromIndex;
//寻找最后一个匹配的元素
startSearchForLastChar:
while (true) {
//找到最后一个匹配的元素的位置
while (i >= min && source[i] != strLastChar) {
i--;
}
if (i < min) {
return -1;
}
//匹配剩余位置的元素,j为原串对应子串长度下末尾的前一个位置
int j = i - 1;
//start为原串对应子串长度下的第一个位置的前一个位置
int start = j - (targetCount - 1);
//目标子串的倒数第二个位置
int k = strLastIndex - 1;
while (j > start) {
//发现剩下的元素无法匹配时,重新寻找最后一个匹配的元素的位置
if (source[j--] != target[k--]) {
i--;
continue startSearchForLastChar;
}
}
//剩下元素都匹配时,整个子串匹配成功,返回原串中的对应位置
return start - sourceOffset + 1;
}
}
1)字符替换
replace方法首先找到第一个需要替换的字符的位置,然后把前面所有的字符复制到新的数组中,然后从第一个需要替换的位置开始,依次判断是否需要替换成新的字符。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
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];
}
while (i < len) {
char c = val[i];
//依次判断是否需要替换成新的字符
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
2)使用正则表达式分割字符串
public String[] split(String regex, int limit) {
//首先判断是否可以采用快速方式,下面两种情况符合一种即可:
//(1)如果传入的分割字符串只有一个字符,并且不是不属于正则表达式的元字符集".$|()[{^?*+\\"
//(2)如果传入的分割字符串是两个字符,并且第一个字符是反斜杠,第二个字符不是ascii数字或ascii字母
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))
{
int off = 0;
int next = 0;
//返回数组的长度是否受限制,当limit大于0时,表示数组的长度为limit,分割将在limit限制下结束;当limit等于0时,表示数组长度无限制,将会尽最大努力分割
boolean limited = limit > 0;
//用ArrayList盛装分割出来的子串
ArrayList<String> list = new ArrayList<>();
//通过调用indexOf方法,找到的ch的位置赋值给next,off是字符串开始搜索时的偏移量
while ((next = indexOf(ch, off)) != -1) {
//如果分割不受限制
if (!limited || list.size() < limit - 1) {
//将[off,next)这段子串放入list容器
list.add(substring(off, next));
//然后off更新为next的下一位置,开始下一轮查找
off = next + 1;
} else {
//由于限制数组长度,分割将要结束,把剩余的子串作为最后的子串,放入list
list.add(substring(off, value.length));
//更新off
off = value.length;
//跳出循环
break;
}
}
//如果没有匹配到,则返回这个字符串的复制品
if (off == 0)
return new String[]{
this};
// 不受限制的情况下,找不到下一个匹配位置了,把剩余的子串作为最后一个子串放入list
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// 构建结果数组,首先取得list的尺寸
int resultSize = list.size();
//如果数组长度不受限制
if (limit == 0) {
//如果list中存在空串"",则不会为空串创建数组空间
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
//构造结果数组
String[] result = new String[resultSize];
//list调用subList方法取得子容器,然后将子容器中的元素转换为数组形式返回
return list.subList(0, resultSize).toArray(result);
}
//如果不可以用快速方式,则需要使用pattern进行分割,
//首先将regex解析成一个pattern,然后再对字符串继续分割
return Pattern.compile(regex).split(this, limit);
}
3)String类中还提供将字符串两边的空格去除的方法trim()
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
//去掉左侧空格,st指向新串的第一个字符
while ((st < len) && (val[st] <= ' ')) {
st++;
}
//去除右侧空格,len指向新串的最后一个字符的下一位置
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
String类中提供了很多将其他类型转换为String的静态方法,大部分都可以直接调用已有的接口。
比如一个对象转换为String:
public static String valueOf(Object obj) {
//直接调用对象的toString方法
return (obj == null) ? "null" : obj.toString();
}
char型数组转换为String:
public static String valueOf(char data[]) {
//调构造器
return new String(data);
}
int转换为String:
public static String valueOf(int i) {
//调用Integer类提供的方法
return Integer.toString(i);
}
情形1:将一个堆中的String对象"a"和一个从字符串常量池中取得的字符串"b"进行拼接,或者将两个堆中的String对象"a"和"b"进行拼接。
String s1 = new String("a")+ "b";
String s2 = new String("a")+ new String("b");
jdk8中,以上代码在执行时,实际上是先创建了一个StringBuilder对象,依次将加号前后的字符串append进自身,然后调用toString方法形成得到一个新的String对象"ab"然后返回。
但值得注意的是,StringBuilder的toString方法。
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
该方法调用String类的构造器String(char value[], int offset, int count),其构造的String对象中持有的字符序列实际上是StringBuilder中value的一个副本,并且也没有把String字符串放入常量池中。
因此,现在常量池中没有字符串"ab"。
注意:jdk9 中没有使用StringBuilder,而是利用了invokeDynamic指令(实际是利用了MethodHandle,统一了入口),将字符串拼接的优化与javac生成的字节码解耦。
情形2:
String s3 = "aa" + "bb" + "cc";
上述代码在javac编译期,就会被优化为直接从字符串常量池中取出字符串"aabbcc"。
该方法被调用的时候,如果字符串常量池中已经存在该字符串,则直接从取常量池中的字符串对象返回;
如果池中不存在该字符串,那么就把这个字符串对象放入池中,更准确地说,是在常量池中引用堆中的这个字符串,并返回。
public native String intern();
通过下面代码验证:
1 String s1 = new String("a")+ new String("b");
2 s1.intern();
3 String s2 = "ab";
4 System.out.println(s1 == s2);//true
intern有何作用?
如果程序中需要创建大量的字符串,并且很多字符串是重复的,那么使用intern()将可以节省空间,因为重复的数据都会从常量池中取得。
比如:
public class InternTest{
static final int MAX_COUNT = 1000 * 1000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args){
Integer[] data = new Integer[]{
1,2,3,4,5};
for(int i = 0; i < MAX_COUNT; ++i){
//下面做法将会创建MAX_COUNT个String对象,并将地址存入arr数组中
//arr[i] = new String(String.valueOf(data[i % data.length]));
//使用intern后,节约大量堆空间
arr[i] = new String(String.valueOf(data[i % data.length])).intern();
/*前5次循环,创建String对象,常量池引用这些堆中创建的String对象,然后返回常量池中的字符串
* 5轮循环过后,依然创建这些字符串,但不用再放入常量池,因为已存在,现在只返回常量池的字符串,而堆中新创 * 建的字符串,将会因为没有被引用,会被垃圾收集器清理。现在只需要维护5个String对象。
*/
}
}
}