目录
String 字符串
String、StringBuffer、StringBuilder 的区别?
1、可变性
String
是不可变的。
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组
保存字符串,不过没有使用 final
和 private
关键字修饰,最关键的是这个 AbstractStringBuilder
类还提供了很多修改字符串的方法比如 append
方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
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;
}
//...
}
2、线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf
等公共方法。StringBuffer
对方法加了同步锁
或者对调用的方法加了同步锁,所以是线程安全
的。StringBuilder
并没有对方法进行加同步锁
,所以是非线程安全
的。
3、性能
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。
StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。
相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
4、对于三者使用的总结:
- 1、操作少量的数据: 适用
String
- 2、单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 3、多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
String 为什么是不可变的?
我们知道被 final
关键字修饰的类
不能被继承,修饰的方法
不能被重写,修饰的变量是基本数据类型
则值不能改变,修饰的变量是引用类型
则不能再指向其他对象。因此,final
关键字修饰的数组保存字符串并不是 String
不可变的根本原因,因为这个数组保存的字符串是可变的(final
修饰引用类型变量的情况)。
String 真正不可变有下面几点原因:
- 1、保存字符串的数组被
final
修饰且为私有的
,并且String
类没有提供/暴露修改这个字符串的方法。 - 2、
String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。
字符串常量池的作用了解吗?
字符串常量池
是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa == bb); // true
String s1 = new String(“abc”);这句话创建了几个字符串对象?
会创建 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。
2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
jdk 1.8 中
// 字符串常量池中已存在字符串对象“abc”的引用
String s1 = "abc";
// 下面这段代码只会在堆中创建 1 个字符串对象“abc”
String s2 = new String("abc");
String中的intern 方法有什么作用?
String.intern()
是一个 native
(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
示例代码(JDK 1.8) :
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true
String 类型的变量和常量做“+”运算时发生了什么?
1、先来看字符串不加 final
关键字拼接的情况(JDK1.8):
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
对于 String str3 = "str" + "ing";
编译器会给你优化成 String str3 = "string";
。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型( byte、boolean、short、char、int、float、long、double)以及字符串常量。
- final 修饰的基本数据类型和字符串变量
- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
2、不过,字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。
final String str1 = "str";
final String str2 = "ing";
// 下面两个表达式其实是等价的
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
被 final
关键字修改之后的 String
会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。
如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。
示例代码(str2
在运行时才能确定其值):
final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {
return "ing";
}
字符串小知识点积累
-
1、字符串的拆分
public static void main(String[] args) { // 现在有一个包含很多非字母的字符串,怎么进行拆分(其中包含两个空格符) String str = "$bo*y gi!r#l "; // 匹配非字母的字符进行分割 String[] words = str.split("[^A-Za-z]+"); StringBuilder sb = new StringBuilder(); for (String word : words) { sb.append(word).append(" "); } System.out.println(sb.toString().trim()); // bo y gi r l }
-
2、一个字符串,怎么判断这个字符包含:字母大小写、空格字符、数字字符或者其他字符
// 方法一:普通写法 public static void main(String[] args) { String str = "1qazxsw23 edcvfr45tgbn hy67uj m,ki89ol.\\/;p0-=\\][ "; char[] line = str.toCharArray(); for (char c : line) { if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { // 包含大小写 // @todo } else if (c == ' ') { // 包含空格 // @todo } else if (c >= '0' && c <= '9') { // 包含0-9的数字 // @todo } else { // 其他字符 // @todo } } } // 方法二:正则匹配 String str1 = str.replaceAll("[^a-zA-Z]", ""); // 包含字母大小写 // 上行的另外一种写法:String s1=str.replaceAll("[A-Z]+|[a-z]+", ""); String str2 = str.replaceAll("[^ ]", ""); // 包含空格字符 String str3 = str.replaceAll("[^0-9]", ""); // 包含0-9的数字字符 // 上行的另外一种写法:String str3=str.replaceAll("[0-9]+", ""); String str4 = str.replaceAll("[a-zA-Z0-9 ]", "");// 替换大小写数字以及空格 System.out.println(str1.length()); System.out.println(str2.length()); System.out.println(str3.length()); System.out.println(str4.length()); // 方法三:利用Character的API方法 String str = ""; for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); if (Character.isLetter(c)) { // 是否包含字母 // todo } else if (Character.isDigit(c)) { // 是否包含数字 // todo } else if (Character.isSpaceChar(c)) { // 是否包含空格 // todo } else { // todo } }
-
3、字符中所有出现的数字前后加上符号“*”,其他字符保持不变
// Jkdi234klowe90a3 转化成 Jkdi*234*klowe*90*a*3* public static void main(String[] args) { String input = "Jkdi234klowe90a3"; String s = input.replaceAll("([0-9]+)", "*$1*"); System.out.println(s); // Jkdi*234*klowe*90*a*3* }
String.format()函数
public static void main(String[] args) {
String out1 = String.format("format结果: %s%s%s","厦门","福州","泉州");
System.out.println("out1 = "+out1); // out1 = format结果: 厦门福州泉州
}