文章目录
- String
- 字符串的创建
- 为什么说String是不可变的?
- 创建后的字符串存储在哪里?
- 字符串的拼接
- String类的常用方法
 
- StringBuilder & StringBuffer
- 使用方法
- 验证StringBuffer和StringBuilder的线程安全问题
 
- 总结
- 三者区别
- 什么情况下用+运算符进行字符串连接比调用StringBuffer/StringBuilder 对象的 append 方法连接字符串性能更好?
 
String
String不是基本数据类型,是引用类型。
字符串的创建
- 方法一:直接赋值String s01 = "helloWorld";
- 方法二:通过new对象创建String s02 = new String("helloWorld");
- 方法三:拼接创建String s03 = "hello"+"World";
- 方法四:通过数组创建byte[] arr = {10,20,30,40}; String s04 = newString(arr);
为什么说String是不可变的?
-  String类型是不可变的 其类是由final修饰的,这就导致了在修改字符串内容是会创建新的String对象,会增加内存的使用和开销。 
 (下方为源码截图) 如果需要修改字符串而不产生新的对象,我们可以使用StringBuffer 或者StringBuilder 
-  String类型是线程安全的 因为String是不可变的,意味着一旦创建了String对象,字符串的内容就不能被更改。任何对字符串的修改都会生成一个新的String对象 
-  为什么需要设置成不可变的? 因为java设计者认为字符串使用是比较频繁的,设置为不可变的可以允许多个客户端同时共享这一字符串,从而保证了线程安全。 
创建后的字符串存储在哪里?
-  字符串创建后存在于运行时常量池中。 
 (在jdk1.7以前,运行时常量池存在于方法区中。从jdk1.7开始,就存在于堆中。)
-  当创建一个字符串对象时,如果字符串内容在常量池中已经存在,JVM会返回常量池中的引用;如果不存在,JVM会在常量池中创建一个新的字符串对象 
字符串的拼接
-  我们先看一下代码 String s01 = "aaa"; String s02 = "bbb";String ss01 = "aaabbb"; String ss02 = s01+s02; System.out.println(ss01==ss02); // (1)结果分析看下方String ss03 = ss02.intern(); System.out.println(ss01==ss03); // (2)结果分析看下方String ss04 = "aaa"+"bbb"; System.out.println(ss01==ss04); // (3)结果分析看下方String ss05 = s01.concat(s02); System.out.println(ss01==ss05); // (4)结果分析看下方
-  (1)运行结果为:false ① s01和s02为字符串变量 
 ② 字符串变量拼接是会现在在堆中new一个对象,并将地址赋值给ss02,然后再让ss02指向常量池。
 ③ 因为无论连接后的数据是否在常量池,ss02执行的都是堆空间中的地址,故为false
-  (2)运行结果为:true String.intern()用于优化字符串的内存使用,可以减少内存消耗。它可以确保相同内容的字符串只存储一次。 
 即,intern()方法的作用是把字符串加入到常量池中,然后返回这个字符串在常量池中的地址。
-  (3)运行结果为:true ① 字符串相加时,都是静态字符串,相加的结果会添加到常量池中。 
 ② 如果常量池中有则返回其引用;如果没有则创建该字符串再返回其引用。
 ③ “aaabbb”在创建ss01时就已经存在常量池中了,所以ss04在创建的时候会发现已经存在"aaabbb"了,返回的是同一个引用,即ss01和ss04都是"aaabbb"在常量池中的引用,故是相等的。
-  (4)运行结果为:false 追加的字符串如果不是空,则会产生一个新的对象。 
 下方为concat方法的源码。即如果追加的字符串是空,返回当前对象,如果不是空,则会创建一个新的对象,而新对象是在堆中创建的,故ss05的地址是在堆中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);}
String类的常用方法
String str = "Hello,who are you?";
// (一) 返回指定索引处的char值
char c = str.charAt(10); // 打印结果为:H。如果超出范围则会报错
// (二)将指定字符串连接到此字符串的结尾
String newStr = str.concat("Joy");  // 打印结果为:Hello,who are you?Joy
// (三)测试字符串是否已指定的后缀结束
Boolean flag = str.endsWith("hello"); // 打印结果为:false
// (四)将string编码为byte序列,并将结果存储到一个新的byte数组中
byte[] bytes = str.getBytes(); // bytes = {72,101,108,108,111,44,119,104,111,32,97,114,101,32,121,111,117,63};
// (五)返回指定字符在该字符串中第一次出现的索引  或指定字符串第一次出现的索引
int index = str.indexOf(101);   // 101是e的字符值,其所在的索引位置是 1
int index01 = str.indexOf("you");   // 14
// (六)返回字符串的长度
int len = str.length();  // 18
// (七)替换字符串中的内容,第一个参数为被替换的内容,后一个参数为替换的新内容
String str01 = str.replace("who","where");   // str01打印结果:Hello,where are you?
// (八)拆分字符串
String[] strings = str.split("");  // String[] strings = {H,e,l,l,o,,,w,h,o, ,a,r,e, ,y,o,u,?};
String[] strings1 = str.split(" "); // String[] strings1 = {Hello,who,are,you?};
String[] strings2 = str.split(","); // String[] strings2 = {Hello,who are you?};
// (九)将字符串转换为一个新的字符数组
char[] chars = str.toCharArray(); // char[] chars = {H,e,l,l,o,,,w,h,o, ,a,r,e, ,y,o,u,?};
// (十)忽略字符串的前后空白
String str2 = " hello world  ";
String str3 = str2.trim();   // String str3 = "hello world";
// (十一)大小写转换
String str02 = str.toLowerCase();  // str02 = hello,who are you?
String str03 = str.toUpperCase();   // str03 = HELLO,WHO ARE YOU?
// (十二)判断是否包含指定的字符串序列
Boolean flag1 = str.contains("who");  // flag1 = true;
// (十三)判断字符串是否为空
Boolean flag2 = str.isEmpty();  // flag2 = false;
StringBuilder & StringBuffer
- 针对于String的不可变性,StringBuilder和StringBuffer是可变的,其内容可以随意修改
- StringBuilder和StringBuffer的对象被多次修改后,不陈胜新的未使用对象,即每次都会对对象本身进行操作,故对字符串进行修改推荐使用
使用方法
-  StringBuilder和StringBuffer使用的功能是一样的,这里就一起来整理,不分开了 
-  其他功能同String,再次也就不一一列举了 StringBuilder builder = new StringBuilder("hello");StringBuffer buffer = new StringBuffer("hello");// 追加内容不能为空,可以追加其他基本类型的内容。注意添加后的内容是紧紧挨着的,没有空格的builder.append(3411); buffer.append(3411);builder.append("world,hello"); // 打印结果:helloworldbuffer.append("world,hello");// 在索引2处添加"java",打印结果:hejavalloworldbuilder.insert(2,"java"); buffer.insert(2,"java");// 删除索引2到索引6之间的内容,左闭右开;打印结果:helloworldbuilder.delete(2,6); buffer.delete(2,6);// 反转字符串builder.reverse(); buffer.reverse();// 两者扩容机制是一样的。初始容量为 16+字符串长度。扩容计算公式为:(capacity+1)*2int builderCapacity = builder.capacity(); // 打印结果:21。int bufferCapacity = buffer.capacity();int builderLen = builder.length(); // 打印结果:15int bufferLen = buffer.length();
验证StringBuffer和StringBuilder的线程安全问题
StringBuilder builder = new StringBuilder();
StringBuffer buffer = new StringBuffer();
Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {for(int i=0;i<1000000;i++){builder.append("b");buffer.append("b");}}
});
t1.start();
Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {for(int i=0;i<1000000;i++){builder.append("a");buffer.append("a");}}
});
t2.start();try {t1.join();t2.join();
} catch (InterruptedException e) {e.printStackTrace();
}finally {System.out.println(builder.length());System.out.println(buffer.length());
}
打印结果是buffer的永远是2000000,但是builder的会一直变化
(自己可以拷贝代码试试看哦)
总结
三者区别
- 可变性 or 不可变性 String是不可变的,是只读字符串。 
 StringBuffer和StringBuilder是可变的。
- 安全性 StringBuffer是线程安全的; 
 StringBilder是线程不安全的.
 因为StringBuilder没有被synchronize修饰,属于单线程下的,减少了线程切换的开销,所以其效率要比StringBuffer效率高
- 性能及使用场景 String每次操作都会生成新的对象,性能较低,尤其是在频繁修改字符串的场景下; 
 StringBuilder在单线程环境中,没有同步机制,性能较高。适合在单线程环境中频繁修改字符串的场景;
 StringBuffer在单线程环境中,由于所有方法都是同步的,性能较低。但在多线程环境中,它可以安全地被多个线程使用
什么情况下用+运算符进行字符串连接比调用StringBuffer/StringBuilder 对象的 append 方法连接字符串性能更好?
- 如果使用少量的字符串操作,使用 (+运算符)连接字符串;
- 如果频繁的对大量字符串进行操作,则使用 - 1:全局变量或者需要多线程支持则使用StringBuffer;
- 2:局部变量或者单线程不涉及线程安全则使有StringBuilder。