1、类的定义
String 类
public final class String implements Serializable, Comparable<String>, CharSequence
String 类被声明为 final 的,意味着它不可以被继承。
- 实现了
Serializable
接口使它可以被序列化 - 实现了
Comparable
接口便于字符串之前的比较 - 实现了
CharSequence
接口,该接口是 char 值的一个可读序列,代表一个有序字符集合。
CharSequence 接口
CharSequence 接口中声明了如下几个方法:
public interface CharSequence {
// 获取字符序列长度
int length();
// 获取某个指定位置的字符
char charAt(int index);
// 获取子序列
CharSequence subSequence(int start, int end);
// 将字符序列转换为字符串
public String toString();
}
对于一个抽象类或者是接口类,不能使用 new 来进行赋值,但是可以通过以下的方式来进行实例的创建:
CharSequence cs = "hello"; // 等价于 CharSequence cs = new String("hello");
CharSequence 就是字符序列,而 String 本质上是通过字符数组实现的!
CharSequence 和 String 都能用于定义字符串,但 CharSequence 的值是可读可写序列,而 String 的值是只读序列。
2、成员变量
// 字符数组,用于存储字符串中的字符
private final char[] value;
// 缓存字符串的hash值,默认是0
private int hash;
- final 修饰 value,表示其一旦被赋值,内存地址是绝对无法修改的
- private 修饰 value,外部绝对访问不到,且 String 类也没用提供任何访问方法
String 是不可变类。即类值一旦被初始化,就不能再被改变了,如果被修改,将会是新的类。也就是说 String s = “a”; s = “b”
时并不是对变量 s 进行了修改,而是重新指向了新的字符串对象。
String 为什么要设计成不可变类呢?
- 不可变对象不能被写,避免了引用传值,所以线程安全。
- 方便使用字符串常量池,节省开销
不可变类的设计通常要遵循以下几个原则:
- 将类声明为 final,所以它不能被继承。
- 将所有的成员声明为私有的,这样就不允许直接访问这些成员。
- 对变量不要提供 setter 方法。
- 将所有可变的成员声明为 final,这样只能对它们赋值一次。
- 通过构造器初始化所有成员,进行深拷贝(deep copy)。
- 在 getter 方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。
常量池
1)class 文件中的常量池
在 class 文件中,除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用。
编写如下代码,并查看其 class 文件内容:
public class Person {
final int age = 10;
String name = "老王";
}
在上图中可以看到,字面量 10 和 “老王” 出现在 Constant pool 列表中。
字面量是用于表达源代码中一个固定值的表示法。数字,字符串等都有字面量表示。
2)运行时常量池
根据《java虚拟机规范》的规定,class 文件的常量池中的信息,将在类加载后进入方法区中的常量池存储。
3、构造方法
1)利用字节数组来生成字符串
byte 是网络传输或存储的序列化形式。在很多传输和存储的过程中需要将 byte[] 数组和 String 进行相互转化。
不带编码
-
String(byte[] bytes)
public String(byte[] bytes) { this(bytes, 0, bytes.length); }
-
String (byte[] bytes, int offset, int length)
public String(byte[] bytes, int offset, int length) { checkBounds(bytes, offset, length); // 将字节数组解码为字符数组 this.value = StringCoding.decode(bytes, offset, length); }
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 + length > bytes.length) throw new StringIndexOutOfBoundsException(offset + length); }
如果没有指明解码使用的字符集,StringCoding 的 decode方法首先调用系统的默认编码格式;
如果没有指定编码格式(charsetName),则默认使用ISO-8859-1
编码格式进行编码操作。
带编码
字符集(charset)是一个系统支持的所有抽象字符的集合。常见的字符集有 ascii字符集、Unicode字符集。
字符编码(charsetName)是对字符集的一套编码规则,将具体的字符进行“数字化”,便于计算机理解和处理。例如常用的 UTF-8 字符编码就是对 Unicode 字符集的一种具体编码规范。
- String (byte[] bytes, Charset charset)
- String (byte[] bytes, int offset, int length, Charset charset)
- String (byte[] bytes, String charsetName)
- String (byte[] bytes, int offset, int length, String charsetName)
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);
}
其他逻辑不变,参数从 Charset 换成 String,即为后两种的实现
可以看到利用字节数组的核心逻辑是:
char[] value = StringCoding.decode(charsetName, bytes, offset, length);
char[] value = StringCoding.decode(charset, bytes, offset, length);
char[] value = StringCoding.decode(bytes, offset, length);
StringCoding 类的 decode 方法如下:
private final static ThreadLocal<SoftReference<StringDecoder>> decoder
= new ThreadLocal<>();
static char[] decode(String charsetName, byte[] ba, int off, int len)
throws UnsupportedEncodingException {
// 从线程级缓存中获取反序列化器
StringDecoder sd = deref(decoder);
String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
// 缓存中没有反序列化器,或者虽然有,但是之前反序列化的字符集与这次不同,则重新生成decoder
if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
|| csn.equals(sd.charsetName()))) {
sd = null;
try {
Charset cs = lookupCharset(csn);
if (cs != null)
sd = new StringDecoder(cs, csn);
} catch (IllegalCharsetNameException x) {}
if (sd == null)
throw new UnsupportedEncodingException(csn);
// 将decoder放入线程级缓存,以备下次使用
set(decoder, sd);
}
// 完成反序列化
return sd.decode(ba, off, len);
}
// 从缓存中获取反序列器,此处使用了软引用,便于jvm在内存不足时,释放该缓存
private static <T> T deref(ThreadLocal<SoftReference<T>> tl) {
SoftReference<T> sr = tl.get();
if (sr == null)
return null;
return sr.get();
}
// 判断字符集是否支持,并加载字符集处理类
private static Charset lookupCharset(String csn) {
if (Charset.isSupported(csn)) {
try {
return Charset.forName(csn);
} catch (UnsupportedCharsetException x) {
throw new Error(x);
}
}
return null;
}
// 将对象的软引用放入线程级缓存
private static void set(ThreadLocal tl, Object ob) {
tl.set(new SoftReference(ob));
}
就是利用了线程级缓存来缓存 decoder,这样就不必每次都实例化新的 decoder,同时线程级缓存也确保了反序列化的操作是线程安全的。其中 ThreadLocal 和 SoftReference 结合的用法值得借鉴。
2)利用字符数组来生成字符串
-
String(char[] value)
public String(char[] value) { this.value = Arrays.copyOf(value, value.length); }
-
String(char[] value, int offset, int 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 + count > value.length) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); }
3)利用字符串来创建一个新的字符串
-
String(String original)
public String(String original) { this.value = original.value; this.hash = original.hash; }
因为 String 一旦定义之后是不可以改变的,不用担心改变源 String 的值会影响到目标 String 的值。
所以可以直接将源 String 中的 value 和 hash 两个属性直接赋值给目标 String。
4)利用 StringBuffer/StringBuilder 来创建一个新的字符串
-
String(StringBuffer buffer)
-
String(StringBuilder builder)
public String(StringBuffer buffer) { synchronized(buffer) { this.value = Arrays.copyOf(buffer.getValue(), buffer.length()); } }
参数从 StringBuilder 变为 StringBuilder 即为后者的实现
这两种方法都较少用,一般都是通过它们的 toString 方法来获得字符串对象。
5)保护型构造方法
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
这是 java7 后加入的新特性。从代码中可以看出,该方法和 String(char[] value) 有两点区别:
- 该方法多了一个 boolean 类型的参数 share。其实这个参数在方法体中根本没被使用,也给了注释,目前不支持使用 false,只使用 true。那么可以断定,加入这个 share 的只是为了区分于 String(char[] value) 方法,构成方法重载。
- 具体的方法实现不同。String(char[] value) 方法在创建 String 的时候会用到会用到 Arrays 的 copyOf 方法将 value 中的内容逐一复制到 String 当中,而这个 String(char[] value, boolean share) 方法则是直接将 value的引用赋值给 String 的 value。也就是说,这个方法构造出来的 String 和参数传过来的 char[] value 共享同一个数组。
那么,为什么 java 会提供这样一个方法呢?
- 性能好。一个是直接给数组赋值(相当于直接将 String 的 value 的指针指向 char[] 数组),一个是逐一拷贝。显然直接赋值更快。
- 节约内存。共享内部数组所以节约内存
- 安全。该方法之所以设置为默认的访问权限,是因为一旦该方法设置为 public,在外面可以访问的话,如果构造方法没有对 arr 进行拷贝,那么其他人就可以在字符串外部修改该数组,由于它们引用的是同一个数组,因此对 arr 的修改就相当于修改了字符串,那就破坏了字符串的不可变性。甚至会导致内存泄漏(内存泄漏是指不用的内存无法被释放)。
4、其他方法
对于数组的操作,必定需要使用 offset(初始偏移量)和 count( 长度)来进行标记和记录。
String 类的大多数方法都是围绕着 value、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);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
实现 CharSequence 的方法
-
charAt(int index):返回指定索引处的 char 值
public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; }
-
length():返回此字符串的长度
public int length() { return value.length; }
-
subSequence(int beginIndex, int endIndex):返回一个新的字符序列,它是此序列的一个子序列
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); }
使用 String(value, beginIndex, subLen) 方法创建一个新的 String 并返回,这个方法会将原来的 char[] 中的值逐一复制到新的 String 中,两个数组并不是共享的,虽然这样做损失一些性能,但是有效地避免了内存泄漏。
重写 Object 的方法
equals()
对于字符串来说,往往是希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象.
@Override
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;
}
可以看到,代码逻辑是先宏观比较,再微观比较,这种写法很大程度上提高了比较的效率,值得学习。
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;
}
hashCode 的实现其实就是使用数学公式:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
在这段代码中,值得注意的是:
1、将整数值与char值相加会得到什么?
根据 java 基本类型间的强制转换规则,char 型将会被转换为 int 型,然后与 int 类型的值相加。那么 char 在转换为 int 时该如何取值呢?其实这里利用了码位(code point)的概念,通过如下的程序来看一下:
String str = "abc123中国";
for (int i = 0; i < str.length(); i++) {
System.out.println((int)str.charAt(i) + "," + str.codePointAt(i));
}
运行结果:
97,97
98,98
99,99
49,49
50,50
51,51
20013,20013
22269,22269
由此可知,char 字符转换为整型时,其值为其在 Unicode 字符集中的码位。
码位(code point):码位是表示一个字符在码空间中的数值。可以对具体的字符集进行字符编码。
例如:ascii 包含 128个 码位(范围是 0-127),数字 0 的码位是 48。
2、31是怎么来的?
在 Java Effective 中有提及:
之所以选择 31,是因为它是一个奇素数(质数)。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与 2 相乘等价于移位运算。使用素数的好处并不明显,但是习惯上都使用素数来计算散列结果。31 有个很好的特性。即用移位和减法来代替乘法,可以得到更好的性能:31 * i == (i << 5) - i。现代的 VM 可以自动完成这种优化。
3、内容相同的两个字符串的哈希值是否一定相等?
int a = "Aa".hashCode(); // 2112
int b = "BB".hashCode(); // 2112
不同的输入得到了相同的哈希值,这叫 哈希碰撞或哈希冲突。所以:
- 哈希值相同的字符串,字面值不一定相同
- 字面值相同的字符串,哈希值一定相同
- 字面值相同的字符串,内存地址不一定相同
在存储数据计算 hash 地址的时候,通常希望尽量减少 hash 冲突。如果使用相同 hash 地址的数据过多,那么这些数据所组成的 hash 链就很长(HashMap采用了链地址法,即数组+链表的数据结构),从而降低了查询效率!所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的 hash 地址越大,所谓的 “冲突” 就越少,查找起来效率也会提高。
toString()
// 返回此对象本身(它已经是一个字符串!)
public String toString() {
return this;
}
比较
-
compareTo():来源 Comparable 接口。按字典顺序比较两个字符串
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; }
-
compareToIgnoreCase(String str):按字典顺序比较两个字符串,不考虑大小写
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator(); public int compareToIgnoreCase(String str) { return CASE_INSENSITIVE_ORDER.compare(this, str); }
String 类的 compareToIgnoreCase 方法实际上是调用了 其静态内部类 CaseInsensitiveComparator 的 compare 方法。
private static class CaseInsensitiveComparator implements Comparator<String>, java.io.Serializable { // use serialVersionUID from JDK 1.2.2 for interoperability private static final long serialVersionUID = 8575799808933029326L; public int compare(String s1, String s2) { int n1 = s1.length(); int n2 = s2.length(); int min = Math.min(n1, n2); for (int i = 0; i < min; i++) { char c1 = s1.charAt(i); char c2 = s2.charAt(i); if (c1 != c2) { c1 = Character.toUpperCase(c1); c2 = Character.toUpperCase(c2); if (c1 != c2) { c1 = Character.toLowerCase(c1); c2 = Character.toLowerCase(c2); if (c1 != c2) { // No overflow because of numeric promotion return c1 - c2; } } } } return n1 - n2; } /** Replaces the de-serialized object. */ private Object readResolve() { return CASE_INSENSITIVE_ORDER; } }
可以看到,它只是在 String 类的 compare 方法上多了一层大小写转换操作而已。
-
contentEquals(CharSequence cs) 和 contentEquals(StringBuffer sb)
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; } private boolean nonSyncContentEquals(AbstractStringBuilder sb) { char v1[] = value; char v2[] = sb.getValue(); int n = v1.length; if (n != sb.length()) { return false; } for (int i = 0; i < n; i++) { if (v1[i] != v2[i]) { return false; } } return true; }
- 如果是 StringBuffer或StringBuilder。先判断长度是否一样长,再判断内容是否一致。
- 如果是 String 类,则调用其内部是 equals 方法。
- 如果是其他 CharSequence 子类,判断逻辑同第1点。
public void stringTest() { String str = "hello"; System.out.println(str.contentEquals("hello")); //true System.out.println(str.contentEquals(new StringBuffer("hello"))); //true }
字符串乱码(getBytes)
- byte[] getBytes() :使用平台默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组
- byte[] getBytes(Charset charset):使用给定的 charset 将此 String 编码到 byte 序列
- byte[] getBytes(String charsetName):使用指定的字符集将此 String 编码为 byte 序列
String str ="nihao 你好";
byte[] bytes = str.getBytes("ISO-8859-1");
System.out.println(new String(bytes)); // nihao ??
把代码修改成 System.out.println(new String(bytes,"ISO-8859-1"));
是否就可以了?这是不行的。
乱码的问题的根源主要是两个:字符集不支持复杂汉字、二进制进行转化时字符集不匹配。
对于 String 来说,getBytes 和 new String 两个方法都会使用到编码,ISO-8859-1 不支持汉字,把这两处的编码替换成 UTF-8 后,打印出的结果就正常了。
这系列方法的底层核心实现是字节数组带编码的核心实现形式
连接(concat)
concat 源码
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);
}
void getChars(char dst[], int dstBegin) {
// byte[] src = new byte[]{2,4,0,0,0,0,0,10,15,50}
// byte[] dest = new byte[5];
// System.arrayCopy(src,0,dest ,0,5);//src=[2,4,0,0,0]
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
数组拷贝
System.arraycopy
int[] arr = {1,2,3,4,5};
int[] copied = new int[10];
System.arraycopy(arr, 0, copied, 1, 5);
System.out.println(Arrays.toString(copied));
运行结果
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 1, 2, 3, 4, 5, 0, 0, 0, 0]
Arrays.copyOf
int[] arr = {1,2,3,4,5};
int[] copied = Arrays.copyOf(arr, 10);
System.out.println(Arrays.toString(copied));
copied = Arrays.copyOf(arr, 3);
System.out.println(Arrays.toString(copied));
运行结果
[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
[1, 2, 3]
两者的区别在于,Arrays.copyOf()
不仅仅只是拷贝数组中的元素,在拷贝元素时,会创建一个新的数组对象。而 System.arrayCopy
只拷贝已经存在数组元素。
实际上 Arrays.copyOf()
的底层还是调用了 System.arrayCopyOf()
方法:
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}
查找(indexOf)
// 返回指定子字符串中第一次出现str的索引
public int indexOf(String str) {
return indexOf(str, 0);
}
// 返回指定字符串中第一次出现str的索引,从fromIndex开始
public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
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++) {
//取到第一个字符相同的位置
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) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
这个方法首先会查找子字符串的头字符在此字符串中第一次出现的位置,再以此位置的下一个位置开始,然后将子字符串的字符依次和此字符串中字符进行比较,如果全部相等,则返回这个头字符在此字符串中的位置;如果有不相等的,则继续在剩下的字符串中查找,继续进行上面的过程,直到查找到子字符串或没有找到返回-1为止。
该用 static 修饰是因为
AbstractStringBuilder
类的indexOf(String str, int fromIndex)
会调用这个方法,为了方便其他类调用该方法所以用 static 修饰。
合并(join)
String.join("-", "Java", null, "cool"); //Java-null-cool
String.join(",", "a").join(",", "b"); //b
此方法是静态方法。方法有两个入参,参数一是合并的分隔符,参数二是合并的数据源,数据源支持数组和 List,在使用的时候,有两个不太方便的地方:
- 不支持依次 join 多个字符串,比如第二例中第一次 join 的值被第二次 join 覆盖了;
- 如果 join 的是一个 List,无法自动过滤掉 null 值
可以使用 Guava 的 API 来解决上述问题:
// 依次 join 多个字符串,Joiner 是 Guava 提供的 API
Joiner joiner = Joiner.on(",").skipNulls();
String result = joiner.join("hello",null,"china");
log.info("依次 join 多个字符串:{}",result);
List<String> list = Lists.newArrayList(new String[]{"hello","china",null});
log.info("自动删除 list 中空值:{}",joiner.join(list));
// 输出的结果为;
// 依次 join 多个字符串:hello,china
// 自动删除 list 中空值:hello,china
底层调用的是 AbstractStringBuilder
类的 append 方法。
切割(split)
String s =":boo:and:foo:";
s.split(":") 结果:["", "boo", "and", "foo", ""]
// 第二个参数limit,这个数值n如果 >0 则会执行切割 n-1 次,数组长度不会大于切割次数
s.split(":", 2) 结果:["", "boo:and:foo:"]
s.split(":", 5) 结果:["", "boo", "and", "foo", ""]
// 如果参数limit是非正数,则执行切割到无限次,数组长度也可以是任何数值,保留空字符串
s.split(":", -2) 结果:["", "boo", "and", "foo", ""]
// 如果参数limit等于0,则会执行切割无限次并且去掉该数组最后的所有空字符串
s.split(":", 0) 结果:["", "boo", "and", "foo"]
// 默认第二个参数为0
s.split("o") 结果:[":b", "", ":and:f", " ", ":"]
s.split("o",2) 结果:[":b", "o:and:foo:"]
从拆分结果中可以看到,空值是拆分不掉的,仍然成为结果数组的一员,如果想删除空值,只能自己拿到结果后再做操作,但 Guava(Google 开源的技术工具) 提供了一些可靠的工具类,可以快速去掉空值,如下:
String a =",a, , b c ,";
// Splitter 是 Guava 提供的 API
List<String> list = Splitter.on(',')
.trimResults()// 去掉空格
.omitEmptyStrings()// 去掉空值
.splitToList(a);
log.info("Guava 去掉空格的分割方法:{}",JSON.toJSONString(list)); // ["a","b c"]
替换(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;
}
- 先判断替换字符和被替换字符是否相同,相同则返回源数据,否则往下进行
- 查询原数组中是否存在要替换的字符,如果没有则返回源数据,否则往下进行
- 新建一个和原长度一样的数组,将第一个需要被替换的字符之前的元素存放进来
- 从第一个要被替换的字符开始,判断其是否为要替换的字符,如果是则用新值替换,否则保持原样
public void testReplace() {
String str ="hello word !!";
log.info("替换之前 :{}",str);
// 替换全部字符串或字符
str = str.replace('l','d');
log.info("替换所有字符 :{}",str);
// 基于正则表达式替换全部字符串或字符
str = str.replaceAll("d","l");
log.info("替换全部 :{}",str);
// 基于正则表达式只替换第一次出现的字符串或字符
str = str.replaceFirst("l","");
log.info("替换第一个 l :{}",str);
}
输出的结果是:
替换之前 :hello word !!
替换所有字符 :heddo word !!
替换全部 :hello worl !!
替换第一个 :helo worl !!
String 类中的方法还有很多,比如获取子字符串的 substring
方法等等。其原理不外乎围绕着字符数组 value
、下标 offset
、字符串长度 count
这几个变量来展开,万变不离其宗。
转换(copyValueOf 与 valueOf)
valueOf
将变量转换成 String 类型。
Object对象
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";
}
public static String valueOf(char c) {
char data[] = {c};
return new String(data, true);
}
public static String valueOf(int i) {
return Integer.toString(i);
}
public static String valueOf(long l) {
return Long.toString(l);
}
public static String valueOf(float f) {
return Float.toString(f);
}
public static String valueOf(double d) {
return Double.toString(d);
}
copyValueOf
public static String copyValueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String copyValueOf(char data[]) {
return new String(data);
}
可以看到这两个函数是等同于
String valueOf(char data[], int offset, int count);
String valueOf(char data[]);
这两个函数的,那么当初为什么要设计这样两个copy函数呢?
这是因为在早期 String 构造器在实现上是不会拷贝数组的,而是直接将参数的 char[] 数组作为 String 的 value 属性。所以执行 test[0] = ‘A’; 时会导致早期的字符串发生变化。为了避免这个问题,提供了 copyValueOf 方法,每次都拷贝成新的字符数组来构造新的 String 对象。但是现在的 String 对象,在构造器中就通过拷贝新数组实现了,所以这两个方面在本质上已经没区别了。
字符数组的内容会被拷贝,字符数组中的子串的修改将不会影响返回的字符串。
所以以下情况修改 data 并不会影响 s1 与 s2 的值:
char[] data = "123456789";
String s1 = String.valueOf(data); // s1 = "123456789"
String s2 = String.copyValueOf(data); // s2 = "123456789"
data[0] = '9';
System.out.println(s1); // s1 = "123456789"
System.out.println(s1); // s2 = "123456789"
intern()
public native String intern();
该方法返回一个字符串对象的内部化引用。String 类维护了一个初始为空的字符串的对象池,当 intern 方法被调用时,如果对象池中已经包含这一个相等的字符串对象则返回对象池中的实例,否则添加字符串到对象池并返回该字符串的引用。该方法开发中很少用到。
5、其他
反射修改 String
String 在通常意义上虽然被认为是”不可变“的,但是仍然可以利用反射来改变 String 的值,如下:
String java = "java";
System.out.println("old value:" + java);
try {
Field field = java.getClass().getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(java);
value[0] = 'g';
System.out.println("new value:" + java);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
运行结果:
old value:java
new value:gava
String 对 “+” 的重载
public static void main(String[] args) {
String string = "hello";
String string2 = string + "world";
}
反编译查看
public static void main(String args[]){
String string = "hollis";
String string2 = (new
StringBuilder(String.valueOf(string))).append("chuang").toString();
}
String.valueOf 和 Integer.toString 的区别
下面三种方式都能将一个 int 类型的变量变成 String 类型,他们的区别是什么呢?
int i = 5;
String i1 = "" + i;
String i2 = String.valueOf(i);
String i3 = Integer.toString(i);
- 第三行和第四行没有任何区别,因为 String.valueOf(i) 也是调用 Integer.toString(i) 来实现的。
- 第二行其实首先基于原字符串创建了一个 StringBuilder 对象,然后依次调用 append 方法、toString 方法。