String、StringBuilder、StringBuffer还傻傻分不清?今天就和大家一起来看下这个三个字符串类。
一、String
1.1 基本介绍
String类代表字符串。Java程序中的所有字符串文字都叫做String类的实例。
通俗点说:String就是用来保存字符串的,比如 "王心凌","123456","hello" 这些都是字符串,字符串中的内容都是被双引号”“
包起来。
1.2 String类的特性
-
String是一个final类,表示不可变的字符序列(其实就是一个char类型的数组)
-
字符串是常量,用双引号包起来表示。值在创建之后就不能被修改了。(这里有些同学肯定会有疑问了,不要急,马上就会说到)
-
String对象的字符内容是存储在一个字符数组中的,也就是第一条的字符序列。
1.3 为什么说String是不可变的
String 是由final
修饰的,一旦使用 final 修饰的类不能被继承、方法不能被重写、属性不能被修改。而且 String 不仅仅类是 final 的,它其中的方法也是由 final 修饰的,换句话说,Sring 类就是一个典型的 Immutable
类。也由于 String 的不可变性,类似字符串拼接、字符串截取等操作都会产生新的 String 对象。
相信有些初学的同学都会有这样的疑问,既然说String的值被创建之后就不能再修改了,那么平时经常使用的字符串拼接、还有String的常用API,比如 subString()、replace()、concat()等等这些不都是对字符串进行修改的吗?下面我们就详细地说一下String的不可变到底是哪种不可变。
首先说一下常量池
这个概念,常量池就是在一块区域中,存放一些预先定义好的值,并且相同的值,只会在第一次使用的时候创建,下次再有人使用这个值,就直接指向常量池中这个值,不会再重新创建。这么做的目的就是为了防止重复创建大量相同的字符串,创建过多的对象不仅占用内存,而且还浪费性能。比如
String str1 = "王心凌"; String str2 = "王心凌";
-
str1去常量池中找一下有没有
"王心凌"
这个字符串常量存在,如果没有就会创建一个新的字符串存放在常量池中 -
str2也会去常量池中找有没有
”王心凌“
这个字符串常量存在,由于在第一次str1使用的时候已经创建了这个字符串,所以str2就直接指向这个常量。
字符串的拼接
知道了常量池的概念之后,我们先来看一下字符串的拼接,本质上是怎么回事。
String str3 = "王心凌"; String str4 = "甜心教主"; //字符串拼接 String str1 = str3 + str4;
-
str3直接指向常量池中已经创建的字符 "王心凌"
-
str4在常量池中没有找到 “甜心教主” 字符串,创建,并且把str4指向这个新创建的字符串
-
str3 和str4 拼接之后的字符串是 “王心凌甜心教主”,str1发现常量池中没有这个字符串,创建新的,然后str1指向新的字符串。
具体的引用关系如下图:
从图中可以看到,当我们为字符串重新赋值的时候,并不是改变原有字符串的内容,而是让字符串变量指向新创建的字符串,也就是指向了常量池中的另一个位置,而原本位置上的那个字符串,是没有动的,还在原位置存放。
调用字符串的API改变字符串变量的值
1.截取方法 subString(int 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);
}
//重点看这里,
//首先,beginIndex为0就表示什么都没截嘛,直接返回原值
//然后就是不为0的时候,直接是new 了一个新的字符串,并没有改变原有的字符串
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
2.替换字符串中的指定字符 replace(CharSequence target, CharSequence replacement)
public String replace(CharSequence target, CharSequence replacement) {
return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
//调用了replaceAll() 方法
this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
}
public String replaceAll(String replacement) {
reset();
boolean result = find();
if (result) {
//创建了一个StringBuffer对象
StringBuffer sb = new StringBuffer();
do {
appendReplacement(sb, replacement);
result = find();
} while (result);
appendTail(sb);
return sb.toString();
}
return text.toString();
}
3.拼接字符串 concat(String str)
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);
//创建了一个新的String对象,并没有改变原值
return new String(buf, true);
}
可以看到,String中的这些改变值的操作其实都是会创建新的字符串对象,其他改变字符串内容的API和上面三个的处理方式都一样,都会去创建新的字符串对象,而不会去改变原有字符串的内容。
二、StringBuffer和StringBuilder
和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。
StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用StringBuffer。
StringBuffer对象代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。
StringBuilder类也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是:StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。
通过源码可以看到,StringBuilder和StringBuffer 都继承了 AbstractStringBuiler类
AbstractStringBuiler中有一个字符数组类型的属性,和String不同的是,这个数组没有被final修饰,所以是可变的。
三、区别
这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面
String:适用于少量的字符串操作的情况 StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况 StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
3.1 速度
首先说运行速度,或者说是执行速度,在这方面运行速度快慢为:
StringBuilder > StringBuffer > String
String最慢的原因:String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的以下面一段代码为例:
String str="甜心教主"; System.out.println(str); str=str+"王心凌"; System.out.println(str);
如果运行这段代码会发现先输出“甜心教主”
,然后又输出“甜心教主王心凌”
,好像是 str 这个对象被更改了,其实,根据上面讲的String是不可变的,我们知道这只是一种假象罢了,JVM 对于这几行代码是这样处理的,首先创建一个 String 对象 str,并把 “甜心教主”
赋值给 str,然后在第三行中,其实 JVM 又创建了一个新的对象也名为 str,然后再把原来的 str 的值和“王心凌”
加起来再赋值给新的 str,而原来的 str 就会被 JVM 的垃圾回收机制(GC)给回收掉了,所以,str 实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以,Java 中对 String 对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
而 StringBuilder 和 StringBuffer 的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比 String 快很多
比如我们这样对字符串进行赋值
String str = "甜心教主" + "王心凌"; StringBuilder stringBuilder = new StringBuilder().append("甜心教主").append("王心凌"); System.out.println(str); System.out.println(stringBuilder.toString());
这样输出结果也是“甜心教主王心凌”
和 “甜心教主王心凌”
,但是 String 的速度却比 StringBuilder的反应速度要快很多,这是因为第1行中的操作和
String str="甜心教主王心凌";
是完全一样的,所以会很快,而如果写成下面这种形式
String str1="甜心教主"; String str2="王心凌"; String str=str1+str2;
那么 JVM 就会像上面说的那样,不断的创建、回收对象来进行这个操作了。速度就会很慢。
3.2 线程安全
如果一个 StringBuffer 对象在字符串缓冲区被多个线程使用时,StringBuffer 中很多方法可以带有synchronized关键字
,所以可以保证线程是安全的,但StringBuilder 的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。
所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder