文章目录
- 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。