字符串
- 1.String
- 1.1 String源码
- 1.2 String 常用方法
- 2.StringBuffer
- 3.StringBuilder
- 4.理解 String、StringBuilder、StringBuffer
String类是Java中一个比较特殊的类,字符串即String类,它不是Java的基本数据类型之一,但可以像基本数据类型一样使用,声明与初始化等操作都是相同的,是程序经常处理的对象,所以学好String的用法很重要
1.String
1.1 String源码
JDK 版本 1.8 ,String 内部实际存储结构为 char 数组,源码如下:
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
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
String 没有继承任何接口,不过实现了三个接口,分别是 Serializable、Comparable、CharSequence 接口。
- Serializable :这个序列化接口没有任何方法和域,仅用于标识序列化的语意。
- Comparable:实现了 Comparable 的接口可用于内部比较两个对象的大小
- CharSequence:字符串序列接口,CharSequence 是一个可读的 char 值序列,提供了 length(), charAt(int index), subSequence(int start, int end) 等接口,StringBuilder 和 StringBuffer 也继承了这个接口
String 中有一个用于存储字符的 char 数组value[],这个数组存储了每个字符。另外一个就是 hash 属性,它用于缓存字符串的哈希码。因为 String 经常被用于比较,比如在 HashMap 中。如果每次进行比较都重新计算其 hashcode 的值的话,那无疑是比较麻烦的,而保存一个 hashcode 的缓存无疑能优化这样的操作
我们看到String 对象是由final 修饰的,一旦使用 final 修饰的类不能被继承、方法不能被重写、属性不能被修改。而且 String 不只只有类是 final 的,它其中的方法也是由 final 修饰的,也由于 String 的不可变性,类似字符串拼接、字符串截取等操作都会产生新的 Strign 对象
大家知道,下面这些字符串分别创建了几个对象?
1、String s1 = "aaa";
2、String s2 = "bbb" + "ccc";
3、String s3 = s1 + "bbb";
4、String s4 = new String("aaa");
1、s1 创建了几个对象。字符串在创建对象时,会在常量池中看有没有 aaa 这个字符串;如果没有此时还会在常量池中创建一个;如果有则不创建。我们默认是没有的情况,所以会创建一个对象
2、 s2 创建了几个对象呢?我们用idea添加JDK自带的反编译工具javap来看看反汇编代码:
编译器做了优化String s2 = “bbb” + "ccc"会直接被优化为bbbccc。也就是直接创建了一个 bbbccc 对象
3、s3 创建了几个对象呢?
我们可以看到,s3 执行"+“操作会创建一个StringBuilder对象然后执行初始化,执行”+"号相当于是执行new StringBuilder.append()操作,所以:
String s3 = new StringBuilder().append(s1).append("bbb").toString();
// Stringbuilder.toString() 方法也会创建一个 String
所以 s3 执行完成后,相当于创建了 3 个对象
4、 s4 创建了几个对象,在创建这个对象时因为使用了 new 关键字,所以肯定会在堆中创建一个对象。然后会在常量池中看有没有 aaa 这个字符串;如果没有此时还会在常量池中创建一个;如果有则不创建。所以可能是创建一个或者两个对象,但是一定存在两个对象
1.2 String 常用方法
charAt:返回指定位置上字符的值
getChars: 复制 String 中的字符到指定的数组
equals: 用于判断 String 对象的值是否相等
indexOf: 用于检索字符串
substring: 对字符串进行截取
concat: 用于字符串拼接,效率高于 +
replace:用于字符串替换
match:正则表达式的字符串匹配
contains: 是否包含指定字符序列
split: 字符串分割
join: 字符串拼接
trim: 去掉多余空格
toCharArray: 把 String 对象转换为字符数组
valueOf: 把对象转换为字符串
2.StringBuffer
StringBuffer 对象代表一个可变的字符串序列,当一个 StringBuffer 被创建以后,通过 StringBuffer 的一系列方法可以实现字符串的拼接、截取等操作。一旦通过 StringBuffer 生成了最终想要的字符串后,就可以调用其toString方法来生成一个新的字符串
比如:
StringBuffer b = new StringBuffer("111");
b.append("222");
System.out.println(b);
StringBuffer 是线程安全的,我们可以通过它的源码可以看出:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuffer所有方法直接使用synchronized关键字加锁,从而保证了线程安全性,对于synchronized的实现原理,大家可以看我多线程那篇博客,对synchronized的实现原理详解
3.StringBuilder
StringBuilder 其实是和 StringBuffer 几乎一样,只不过 StringBuilder 是非线程安全的。并且,为什么 + 号操作符使用 StringBuilder 作为拼接条件而不是使用 StringBuffer 呢?
我猜测:加锁是一个比较耗时的操作,而加锁会影响性能,所以 String 底层使用 StringBuilder 作为字符串拼接
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
4.理解 String、StringBuilder、StringBuffer
我们上面说到,使用+连接符时,JVM 会隐式创建 StringBuilder 对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意,如下这段代码:
String s = "aaaa";
for (int i = 0;i<10000;i++){
s+="bbb";
}
这是一段很普通的代码,只不过对字符串 s 进行了 + 操作,我们通过反编译代码来看一下:
// 经过反编译后
String s = "aaa";
for(int i = 0; i < 10000; i++) {
s = (new StringBuilder()).append(s).append("bbb").toString();
}
在每次进行循环时,都会创建一个StringBuilder对象,每次都会把一个新的字符串元素bbb拼接到aaa的后面,所以,执行几次后的结果如下:
每次都会创建一个 StringBuilder ,并把引用赋给 StringBuilder 对象,因此每个 StringBuilder 对象都是强引用, 这样在创建完毕后,内存中就会多了很多 StringBuilder 的无用对象
这样由于大量 StringBuilder 创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个 StringBuilder 对象调用append()方法手动拼接:
StringBuilder builder = new StringBuilder("aaa");
for (int i = 0; i < 10000; i++) {
builder.append("bbb");
}
builder.toString();
这段代码中,只会创建一个 builder 对象,每次循环都会使用这个 builder 对象进行拼接,因此提高了拼接效率
从设计角度理解:
String 在 JDK1.6 之后提供了intern()方法,intern 方法是一个native方法,它底层由 C/C++ 实现,intern 方法的目的就是为了把字符串缓存起来,在 JDK1.6 中却不推荐使用 intern 方法,因为 JDK1.6 把方法区放到了永久代(Java 堆的一部分),永久代的空间是有限的,除了Fullgc外,其他收集并不会释放永久代的存储空间。JDK1.7 将字符串常量池移到了堆内存中
下面我们来看一段代码,来认识一下intern方法:
public static void main(String[] args) {
String a = new String("ab");
String b = new String("ab");
String c = "ab";
String d = "a";
String e = new String("b");
String f = d + e;
System.out.println(a.intern() == b);
System.out.println(a.intern() == b.intern());
System.out.println(a.intern() == c);
System.out.println(a.intern() == f);
}
结果:
false、true、true、false
和你预想的一样吗?为什么会这样呢?我们先来看一下 intern 方法的官方解释
从JDK 1.7开始去永久代,字符串常量池已经被转移至 Java 堆中,开发人员也对 intern 方法做了一些修改。因为字符串常量池和 new 的对象都存于 Java 堆中,为了优化性能和减少内存开销,当调用 intern 方法时,如果常量池中已经存在该字符串,则返回池中字符串;否则直接存储堆中的引用,也就是字符串常量池中存储的是指向堆里的对象
所以我们对上面的结论进行分析:
String a = new String("ab");
String b = new String("ab");
System.out.println(a.intern() == b);
输出为什么是false:
a.intern 返回的是常量池中的 ab,而 b 是直接返回的是堆中的 ab。地址不一样,肯定输出 false
所以第二个:
System.out.println(a.intern() == b.intern());
也就没问题了吧,它们都返回的是字符串常量池中的 ab,地址相同,所以输出 true
然后来看第三个:
System.out.println(a.intern() == c);
a 不会变,因为常量池中已经有了 ab ,所以 c 不会再创建一个 ab 字符串,这是编译器做的优化,为了提高效率
下面来看最后一个:
System.out.println(a.intern() == f);
StringBuilder 和 StringBuffer 的扩容问题
首先先注意一下 StringBuilder 的初始容量:
public StringBuilder() {
super(16);
}
StringBuilder 的初始容量是 16,当然也可以指定 StringBuilder 的初始容量。
在调用 append 拼接字符串,会调用 AbstractStringBuilder 中的 append 方法
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
上面代码中有一个ensureCapacityInternal方法,这个就是扩容方法,我们跟进去看一下:
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
这个方法会进行判断,minimumCapacity 就是字符长度 + 要拼接的字符串长度,如果拼接后的字符串要比当前字符长度大的话,会进行数据的复制,真正扩容的方法是在newCapacity中:
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
扩容后的字符串长度会是原字符串长度增加一倍 + 2,如果扩容后的长度还比拼接后的字符串长度小的话,那就直接扩容到它需要的长度 newCapacity = minCapacity,然后再进行数组的拷贝