一、String
1.1 创建String
String两种创建方式
String str = "zbx";
// zbx 存储在常量池中,常量池在方法区中
String str2 = new String("java");
// java 存储在堆中
1.2 多个字符串的内存分析
创建字符串
创建一个字符串
String name1 = "zbx";
引用存放在栈中,值存放在常量池中
new 一个字符串
String name2 = new String("bao");
引用存放在栈中,vaule存放在堆中,值存放在常量池中
分析
看String带参的源码
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
original是传入的值,而.value是一个字符数组
private final char value[];
String的引用在栈中,String的值在堆中
但是String的值是一个字符数组,所以堆中存放的是字符数组的引用,而字符数组的值存放在常量池中
如图
如果此时再使用直接赋值的方式创建一个字符串
String name3 = "bao";
先在常量池中进行查,发现此时常量池中已经有“bao”这个字符串了,所以直接指向已有的内存空间
内存图如下
如果使用new的方式来创建一个字符串
String name3 = new String("bao");
则先在常量池中查找,发现有“bao”,然后在堆中开辟一个内存空间,存放“bao”的地址引用,栈中存放new出来的引用
内存图如下
1.3 String常用方法
/**
* @author 张宝旭
*/
public class StringWays {
public static void main(String[] args) {
String string = "this is my love";
int length = string.length(); // 获取字符串的长度
char c = string.charAt(0); // 获取单个字符
int index = string.indexOf("is"); // 查找某个字符串首次出现的下标
int index1 = string.lastIndexOf("i"); // 查找某个字符串最后一次出现的下标
String replace = string.replace("my", "your"); // 替换字符串
String substring = string.substring(0, 7); // 截取字符串
boolean love = string.contains("love"); // 判断某段字符串是否包含在这整个字符串中
String trim = string.trim();// 去掉首尾的空格
char[] chars = string.toCharArray(); // 将字符串转换成字符数组
String string1 = string.toLowerCase(); // 将字符串中的大写字符转换成小写字符
String string2 = string.toUpperCase(); // 将字符串中的小写字符转换成大写字符
boolean love1 = string.endsWith("love"); // 判断字符串是否以某个字符串结尾
String[] s = string.split(" "); // 以某个字符串做分割,存到字符串数组中
}
}
1.4 面试题
第一题
String s1 = "abc";
String s2 = "xyz";
String s3 = s1 + s2;
String s4 = "abc" + "xyz";
String s5 = "zbcxyz";
System.out.println(s3 == s4); // false
System.out.println(s4 == s5); // true
s1、s2都在常量池中,而s3=s1+s2,s3不会放在常量池中(只有字面常量才会放在常量池中),所以s3放在堆中,s4是两个字面常量拼接,所以会放在常量池中,内容为“abcxyz”,s5创建时,现在常量池中找到了相同的,所以两者引用相同
所以s3和s4的引用不同,s4和s5的引用相同
第二题
String s1 = "abc";
String s2 = "xyz";
String s3 = s1 + s2;
String s4 = s3.intern();
String s5 = "abcxyz";
System.out.println(s3 == s4); // true
System.out.println(s4 == s5); // true
使用intern方法如果常量池中没有,则把对象复制一份(或对象引用)放入常量池中, 返回常量池中的对象,如果常量池中存在,则直接返回。
JDK1.7之前是复制一份放入常量池,JDK1.7之后则把对象引用到常量池。
s3存放在堆中,而创建s4时发现常量池中没有,则把s3对象复制一份放入常量池中(此时常量池中存放s3在堆中创建的“abcxyz”的地址),然后返回常量池中的对象,所以s3和s4指向同一个地址引用,结果为true
当创建s5时,发现常量池中有“abcxyz”地址,所以s5指向常量池中的地址,然后此地址再指向堆中“abcxyz”,所以s4和s5都指向常量池中的地址,结果为true
二、StringBuilder
2.1 StringBuilder常用方法
String每一次改变都会创建一个新的对象,而StringBuilder的操作都只需创建一个对象即可
StringBuilder的效率远远高于String
StringBuilder的常用操作
/**
* @author 张宝旭
*/
public class StringBuilderTest {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("my");
stringBuilder.append(" love");// 拼接字符串
stringBuilder.indexOf("l"); // 获取某段字符串首次出现的下标
stringBuilder.lastIndexOf("y"); // 获取某段字符串最后一次出现的下标
stringBuilder.insert(2, "is");// 在指定下标的位置插入字符串
stringBuilder.replace(2, 4, "you"); // 在指定位置替换字符串
stringBuilder.delete(2, 5); // 删除下标为2到5的字符串
int length = stringBuilder.length(); // 获取字符串长度
stringBuilder.reverse(); // 字符串反转
System.out.println(stringBuilder);
}
}
2.2 JVM优化
String的拼接操作,会自动被JVM优化为StringBuilder的操作
public class MyString {
public static void main(String[] args) {
String string = "java";
string += " golang";
string += " C++";
System.out.println("I Love " +string);
}
}
反编译查看JVM的优化
import java.io.PrintStream;
public class MyString
{
public MyString()
{
}
public static void main(String args[])
{
String string = "java";
string = (new StringBuilder()).append(string).append(" golang").toString();
string = (new StringBuilder()).append(string).append(" C++").toString();
System.out.println((new StringBuilder()).append("I Love ").append(string).toString());
}
}
以上看到String的拼接操作都被JVM优化了,为了提高性能
三、StringBuffer
3.1 StringBuffer常用方法
/**
* @author 张宝旭
*/
public class StringBufferTest {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer("name");
stringBuffer.append(" is"); // 拼接字符串
stringBuffer.charAt(0); // 获得某个字符
stringBuffer.delete(4, 7); // 删除指定区间的字符串
stringBuffer.deleteCharAt(1); // 删除指定下标的字符
stringBuffer.indexOf("m"); // 获得指定元素的下标
stringBuffer.insert(1, "a"); // 在指定下标插入元素
stringBuffer.length(); // 获取字符串的长度
stringBuffer.replace(1, 2, "VV"); // 替换指定区间的字符串
String substring = stringBuffer.substring(1, 2);// 截取指定区间字符串,并返回到一个新的字符串中
stringBuffer.reverse(); // 字符串反转
System.out.println(stringBuffer);
System.out.println(substring);
}
}
3.2 线程安全的StringBuffer
StringBuffer和StringBuilder的方法都类似,只是在StringBuffer内部的方法中都添加了synchronized关键字
所以StringBuffer是线程安全的,StringBuilder不是线程安全的
但是StringBuffer的效率比StringBuilder的效率要低
比如如下对比
StringBuilder的append()方法
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuffer的append()方法
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
两者在大致上时相同的,只是StringBuffer添加了synchronized关键字,更加安全,但是效率也更低
四、String、StringBuilder、StringBuffer性能测试
为了让结果更准确,不受常量池中已存在数据的影响,对三者分别做测试(每次运行只测试一种)
/**
* @author 张宝旭
*/
public class StringPerformanceTest {
public static final int COUNT = 10000;
public static void main(String[] args) {
stringTest();
// stringBufferTest();
// stringBuilderTest();
}
/**
* 测试String拼接时间。
*/
public static void stringTest() {
long startTime = System.currentTimeMillis();
String str = "name";
for (int i = 0; i < COUNT; i++) {
str += i;
}
long endTime = System.currentTimeMillis();
System.out.println("String拼接时间: " + (endTime - startTime));
}
/**
* 测试StringBuilder拼接时间。
*/
public static void stringBuilderTest() {
long startTime = System.currentTimeMillis();
StringBuilder stringBuilder = new StringBuilder("name");
for (int i = 0; i < COUNT; i++) {
stringBuilder.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("StringBuilder拼接时间: " + (endTime - startTime));
}
/**
* 测试StringBuffer拼接时间。
*/
public static void stringBufferTest() {
long startTime = System.currentTimeMillis();
StringBuffer stringBUffer = new StringBuffer("name");
for (int i = 0; i < COUNT; i++) {
stringBUffer.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("StringBuffer拼接时间: " + (endTime - startTime));
}
}
当测试次数为10000次时,结果
String拼接时间: 368
StringBuilder拼接时间: 1
StringBuffer拼接时间: 1
当测试次数为一百万次时,结果
String拼接时间: 很久,没有执行完
StringBuilder拼接时间: 46
StringBuffer拼接时间: 57
结论
由以上结果看到,String的拼接效率很低,而StringBuilder和StringBuffer的拼接效率却很快很快,但是由于StringBuffer是线程安全的,加了synchronized关键字,所以性能比StringBuilder略低
建议在不使用多线程的情况下,尽量使用StringBuilder来操作字符串