String
String概述
java.lang.String类代表字符串。
- Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。
- String类声明为final, 不可被继承。
- String内部定义了**
private final char[] value
**用于存储字符串数据(JDK8
)。 - String类实现了Serializable接口:表示字符串是支持序列化的。
- String类实现了Comparable接口:表示String可以比较大小。
String类的声明以及属性如下:
String实例化
String类的实例化可以分为字面量赋值和**new + 构造器()**两种分式。
字面量赋值
由“”
引起的字面量都做为String类
的实例,可以直接赋值给String
类的引用变量。
String str = "hello";
在Java中由
“”
引起的字面量在编译期会以CONSTANT_String
和CONSTANT_Utf8
的形式被保存**Class
文件的常量池中,在类加载阶段这些字面量会进入当前类的运行时常量池**,当字面量第一被使用时(ldc指令推入栈顶),才会以字符串对象的形式在堆中创建对象,并在字符串常量池(StringTable)保存该对象的引用。
关于字面量进入字符串常量池的具体细节请参阅此处: Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的?
上面的str
中实际保存的是字符串常量池中的“hello”的引用。对此我们可以使用javap -verbose
指令进行查看。
构造器实例化
String类重载的构造器有很多,大概有十几种重载的构造器,Java的官方文档中可以查询到,这里不再列出。需要注意的是文档中标注Deprecated
的构造器表示已经弃用,不建议使用。
对于这么多的构造器我们不需要全部了解,只需要掌握常用一些构造器即可,其它的构造器用到时,再去查阅文档。
常用构造器
String(String original)
该构造器中的参数为一个字符串字面量。
String str = new String("hello");
这种实例化方式与字面量直接赋值的区别在于:
-
字面量赋值时
String
类引用变量会直接指向字符串常量池中的字符串。 -
**String(String original)**实例化时,会先在堆空间中新建一个
String
类的对象,str
会指向堆中新创建的这个String
对象,这个对象的value
属性中保存了常量池中字符串对象的value
属性。
上述代码的反汇编:
String(String original
)构造器中的操作:
String(char[] value)
该构造器中的参数为一个char[]
类型数组,构造器会调用Arrays.copyOf()
方法新建一个参数数组的拷贝,并将拷贝后的字符数组首元素地址赋值给this.value
属性。
// 举例:
char[] chars = {'a', 'b', 'c'};
String str = new String(chars);
System.out.println(str); // abc
通过这种方式实例化String
类对象时,str
指向了堆中新建的String
对象,这个对象的value
属性指向了拷贝后的字符数组。
String(char[] value, int offset, int count)
该构造器会将参数数组中,从offset
下标开始,长度为count
的子字符数组拷贝至新字符串当中。
// 举例:
@Test
public void test() {
char[] chars = {'a', 'b', 'c', 'd', 'e', 'f'};
String str = new String(chars, 3, 3);
System.out.println(str); // def
// 当指定的索引或者长度大于参数数组的最大索引时会抛出字符串索引越界异常
str = new String(chars,3,4);
}
String
类中常用的构造器还有许多,其它的构造器在后面用到时在进行介绍。
两种实例化方式的内存图
String注意点
字符串判断相等
对于字符串判断相等,不能使用==
运算符,这个操作符只能用于判断两个字符串是否存放在同一个位置上,尽管存放在同一个位置上的字符串一定相等,但是内容相等的字符串也会存放在不同的位置上。
public void test(){
String str1 = "hello";
String str2 = new String("hello");
System.out.println(str1 == str2) // 输出结果为: false
}
在上文已经介绍过两种实例化方式的内存布局,此处不再赘述。
如果需要判断字符串是否相等,应该使用String类重写的**equals()
方法**。
public void test(){
String str1 = "hello";
String str2 = new String("hello");
System.out.println(str1.equals(str2)); // 输出结果为: true
}
String
类重写equals()
方法的内部实现:
对于一个字符串引用和一个字符串常量的比较,建议调用字符串常量的equals
()方法,可以有效防止空指针异常。
public void test(){
String str1 = new String("hello");
System.out.println("hello".equals(str1)); // 输出结果为: true
}
字符串拼接
在Java
中字符串的拼接可以使用 +
、+=
运算符或者concat()
方法。
+
和 +=
运算符拼接字符串
据Java编程思想
中的描述,用于String
的+
和 +=
是Java
中仅有的两个重载过的运算符。重载的意思是,当一个运算符用于特殊类时被赋予了特殊的意义。对于String
类来说,+
和+=
运算符会将运算符左右的操作数进行拼接。在进行拼接时,实际上是利用了StringBuilder
类对象进行字符串拼接。如果操作数是基本数据类型,会先将基本数据类型转换为所对应的字符数组。如果是引用数据类型则会调用该类的toString()
方法进行拼接。
我们看下面的例子:
例1:
对于字面量的字符串拼接和引用变量的字符串拼接最终返回的引用是不同的。
public static void main(String[] args) {
String s1 = "ab";
String s2 = "cd";
String s3 = "ab" + "cd";
String s4 = s1 + s2;
System.out.println(s3); // "abcd"
System.out.println(s4); // "abcd"
System.out.println(s3 == s4); // false
}
从第6和第7行输出结果来看,s3和s4的内容相同。但是在判断两个引用的地址时输出结果为false,也就是说s3和s4指向的不是不一块内存空间。原因如下:
-
如果是字面量的方式进行拼接字符串,那么在编译期间就会进行对字面量进行运算并将新字符串的信息Class文件的常量池中,当运行时
“abcd”
以字符串形式放入字符串常量池中,s3接收的到的就是常量池中“abcd”
字符串的引用。 -
而如果是涉及到变量的拼接字符串,那么在运行期间
+
运算符实际上在底层创建了一个StringBuilder
类的对象(后面会介绍StringBuilder类),并调用该对象的append()
方法将字符串添加进对象中,然后再转换为一个String
类的对象并返回该对象的引用。所以s4其实是指向了堆空间中新建的String
类对象。
内存图如下:
这一块我进源码看了一下,里面太复杂了差点没绕出来。StringBuilderd
的append()
和toString()
方法最终都调用了native
的System.arraycopy()
方法进行数组拷贝,这块应该属于JVM的内容了属实超纲了,如果有不对的地方还请大佬们指正。
例2:
如果使用final
来修饰s1
和s2
,那么结果又不一样:
public static void main(String[] args) {
final String s1 = "ab";
final String s2 = "cd";
String s3 = "ab" + "cd";
String s4 = s1 + s2;
System.out.println(s3); // "abcd"
System.out.println(s4); // "abcd"
System.out.println(s3 == s4); // true
}
从字节码文件可以看到,第4行和第5行所执行的操作相同。这里一块在查阅相关资料后得知应该是涉及到一个常量折叠的概念:
将常量表达式的值求出来作为常量嵌在最终生成的代码中,这种优化叫做常量折叠。 — 转自知乎RednaxelaFX大佬
大佬的文章中指出在Java
中符合常量折叠的情况:
- 八大基本数据类型和String类的字面量。
static final
修饰的基本数据类型字段和String类型的静态字段(以常量表达式初始化的才算)final
修饰的基本数据类型和String类型的局部变量。(也是以常量表达式初始化的才算)- 以常量表达式为操作数的算数和关系运算表达式,以及对于String的
+
拼接运算符。
对于上述的情况,编译器在遇到相关运算符是,就必须检查其操作数都是常量表达式,如果是的话就必须在编译时对该运算符做常量折叠。
大佬的原文:对于一个很复杂的常量表达式,编译器会算出结果再编译吗?
很明显,对于上面的代码中第4行和第5行都满足于常量折叠的情况,因此编译器在编译期对运算进行了优化,并将运算结果的字符串信息保存在了Class
文件的常量池中,所以在运行期间的操作和字符串字面量赋值时相同,最终s3
和s4
的指向也相同。
例3:
如果对final
修饰的String
局部变量进行初始化时不是常量表达式,则不会发生常量折叠情况。
public static void main(String[] args){
String s1 = "ab";
String s2 = "cd";
String s3 = "ab" + "cd";
final String fs1 = s1; // 对fs1的赋值是一个变量
final String fs2 = s2; // 对fs2的赋值时一个变量
String s4 = fs1 + fs2; // 做运算时,尽管fs1和fs2被final修饰,但它们的值是一个变量所以不满住常量折叠
System.out.println(s3 == s4); // false
}
再来看下反汇编代码:
从最终的输出结果和反汇编代码来看,s4 = fs1 + fs2
这行代码并没有被编译器优化,所以s3
中保存的是“abcd”
常量字符串的引用,s4
则指向了堆中新创建的字符串对象。
concat()
方法拼接字符串
使用concat()
方法拼接字符串,该方法只接受String
类的参数,也就是其它数据类型需要调用该类对应的toString()
方法才能作为参数。我们来看下该方法内部的实现:
从其内部方法的实现可以看出,每一次concat()
都需要在堆空间中新创建一个数组,同时也需要在堆空间中创建一个String
类对象,即使是两个常量字符串进行拼接,也会在堆中创建一个新的字符串对象,并将两个常量字符串的内容拷贝进去。
public static void main(String[] args){
String s1 = "ab" + "cd"; // s1 指向常量池中的字符串对象
String s2 = "ab".concat("cd"); // s2 指向堆中的字符串对象
System.out.println(s1 == s2); // false
}
内存图如下:
两种字符串拼接的效率对比
在上面介绍的两种字符串拼接方式中,使用+
运算符进行拼接时,实际是创建了StringBuilder
和String
类2个对象,并且还会将StringBuilder
底层数组中的内容拷贝到新数组中。而如果使用concat()
进行字符串拼接时,实际上只创建了一个对象和一个数组。因此从效率上来说,concat()
方法进行字符串拼接时的效率应该要高于+
运算符拼接字符串。
对此进行如下测试:
public class StringTest1 {
public static void main(String[] args) {
String s1 = "";
String s2 = "";
String s3 = "abc";
long start = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
s1 += s3;
}
long end = System.currentTimeMillis();
System.out.println("使用运算符拼接: " + (end - start));
start = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
s2.concat(s3);
}
end = System.currentTimeMillis();
System.out.println("使用concat拼接: " + (end - start));
}
}
输出的结果非常明显。concat
的效率原高于+
运算符拼接字符串。不过这不能说明concat
的效率就高于了StringBuilder
,主要原因是因为+
运算符的底层实现并不适合在循环中进行大量拼接,每次循环都得创建2个对象,如果是对象不是字符串类型还得先将其转换为字符串类型。所以效率相对于concat
要低。不过对于大量的字符串拼接,一般也不会使用concat
,后面介绍StringBuilder
时,会了解到当正确使用了StringBuilder
它的效率才是最高的,这也是编译器对于+
的底层优化选择使用StringBuilder
的原因。
intern()手动入池
intern()方法是Java提供的一个将字符串手动添加进字符串常量池的方法。以下是JDK8文档中的解释:
我英文不好,文中大概的意思是,当该方法被调用时,如果池中已经存在一个等价于当前字符串的对象(通过equals方法决定是否相同),那么返回池中这个字符串。否则将这个字符串对象添加进常量池中,并返回这个这个字符串的引用。
说实话,这个解释说的比较模糊(可能是我没看懂😂),我在网上查了一些资料后总结出比较靠谱的解释是:
当该方法被调用时,字符串常量池中如果已经存在指向内容相同字符串的引用,则返回这个引用,如果没有找到指向相同字符串的引用,则将堆中对象的引用存入字符串常量池中,并返回这个引用。也就是说字符串常量池中其实存的只是字符串的一个引用,并没有存放该对象的实例。
针对于两种情况,分别看一下下面的例子:
例1:当字符串常量池中已经存在引用时
public static void main(String[] args){
String s1 = "hello";
String s2 = new String("hel") + new String("lo");
s2.intern();
System.out.println(s1 == s2); // false, s1存放的是"hello"的引用, s2存放的依然是堆中对象的引用,s2.intern()会返回, 常量池中"hello"的引用, 但不会改变s2原本的值。
}
还是看反汇编把,比我干打字要直观一点🤣
内存图大概如下(画的有点乱…):
例2:当字符串常量池不存在引用时。
public static void main(String[] args){
String s2 = new String("hel") + new String("lo");
s2.intern(); // s2所在对象的引用入常量池
String s1 = "hello"; // 保存了常量池中的引用,与s2指向同一个对象
System.out.println(s1 == s2); // true
}
在之前画的图里面都将指向字符串常量对象的引用变量直接指向了字符串常量池中,其实不太准确,因为字符串常量池中保存的都是字符串常量对象的引用,而这个对象其实是在堆空间中存放的。不过一开始我也没清楚了解到这些内容,并且网上很多图也是这样画的,我想应该是这样作图可能更加直观把。
字符串的不可变性
String
类被声明为final
,代表了这个类无法被继承,而存放字符串的底层数组引用value
也被声明为了private final char[]
,所以我们通过正常的手段是也无法从外部访问到这个value
这个属性,也不能改变value
的指向。
不可变性的体现:
- 当对String类引用变量重新赋值时,改变的是该变量所指向的字符串对象,而不是原对象中value数组中的内容。
- 当对现有的字符串进行拼接字符串操作时,会产生新的字符串对象,原字符串不会改变。
- 当调用String的replace()方法时,替换字符时,也时重新开辟了新的内存空间,并使得引用指向了这块空间。
虽然无法通过正常手段访问value
,但是也可以通过一些特殊手段访问到这个私有属性,只要能访问到该属性拿到它所指向的数组引用,就可以修改其中的内容,方法就是使用反射机制来获取运行时类的属性。
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s1 = "abc";
System.out.println(s1); // abc
char[] chars = {'1', '2', '3'};
Field value = String.class.getDeclaredField("value"); // 获取运行时类的value属性
value.setAccessible(true); // 设置可以访问私有属性
value.set(s1, chars); // 将s1中value指向的数组改为chars
System.out.println(s1); //123, value的指向被改变
char[] chars1 = (char[]) value.get(s1); // 获取value指向的这个数组的引用
chars1[0] = '2'; // 改变这个数组的内容
System.out.println(s1); // 223, value所指向数组的内容也发生改变
}
上面的内容也只是一个扩展了解,既然Java的开发工程师们如此小心的将value
封装起来,目的不就是不想让我们去其中的值嘛,所以如果没有什么特殊需求,不建议使用这种方式去破坏字符串的不可变性。并且反射的主要作用也不是拿来做这个的。
String的常用方法
字符串比较
方法名 | 功能 |
---|---|
boolean equals(Object anObject) | 比较字符串的内容是否相同。 |
boolean equalsIgnoreCase(String anotherString) | 与equals方法类似, 忽略大小写。 |
int compareTo(String another) | 比较两个字符串的大小。 |
@Test
public void test1(){
String s1 = "HelloWorld";
// 1. equals() 比较字符串内容是否相同
System.out.println("HelloWorld".equals(s1)); // true
System.out.println("helloworld".equals(s1)); // false,比较大小写
// 2. equalsIgnoreCase() 与equals方法类似, 忽略大小写
System.out.println("helloworld".equalsIgnoreCase(s1)); // true
System.out.println("HELLOWORLD".equalsIgnoreCase(s1)); // true
// 3. compareTo 比较两个字符串的大小
/*
比较规则是基于每个字符的Unicode编码:
如果当前字符串 > 参数字符串,返回大于0的数字
如果当前字符串 < 参数字符串,返回小于0的数字
如果两个字符串完全相同,返回0
*/
System.out.println("ABC".compareTo("ABE")); // -2
System.out.println("aBC".compareTo("ABE")); // 32
System.out.println("ABC".compareTo("ABC")); // 0
}
String
重写后的compareTo
方法
字符串查找
方法名 | 功能 |
---|---|
boolean contains(CharSequence s) | 判断当前字符串中是否包含指定的char值序列。 |
char charAt(int index) | 返回index索引处的字符,return value[index] |
int indexOf(String str) | 返回参数字符串在此字符串中第一次出现处的索引。 |
int indexOf(String str, int fromIndex) | 返回参数字符串在此字符串中第一次出现处的索引,从指定的索引开始。 |
int lastIndexOf(String str) | 返回参数字符串在此字符串中最右边出现处的索引,从右至左查找。 |
int lastIndexOf(String str, int fromIndex) | 返回参数字符串在此字符串中最后一次出现处的索引,从指定的索引开始从右至左查找。 |
注: indexOf
和lastIndexOf
方法如果未找到返回值为-1 。
@Test
public void test2(){
// 1. contains(CharSequence s) 判断一个字符串中是否包含指定char值序列, 严格要求大小写
String s1 = "HelloWorld";
System.out.println(s1.contains("llo")); // true
System.out.println(s1.contains("LLO")); // false
// 2. charAt(index) 通过索引获取字符
System.out.println(s1.charAt(0)); // H
System.out.println(s1.charAt(1)); // e
System.out.println(s1.charAt(2)); // l
s1.charAt(-1); // StringIndexOutOfBoundsException
// 3. indexOf(String str): 返回指定子字符串在此字符串中第一次出现处的索引, 未找到返回-1
System.out.println(s1.indexOf("Hel")); // 0
System.out.println(s1.indexOf("lo")); // 3
System.out.println(s1.indexOf("abc")); // -1
// 4. indexOf(String str, int fromIndex): 返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始查找。
String s2 = "HelloHelloWorld";
System.out.println(s2.indexOf("He",1)); // 5
System.out.println(s2.indexOf("He",6)); // -1
// 5. lastIndexOf(String str): 返回指定子字符串在此字符串中最右边出现处的索引。
System.out.println(s2.lastIndexOf("He")); // 5
// 6. lastIndexOf(String str, int fromIndex): 返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始往左查找
System.out.println(s2.lastIndexOf("He",4)); // 0
// 什么情况下 indexOf(str)和lastIndexOf(str)返回值相同?
// 1. 字符串中只存在一个str子串
// 2. 字符串中不存在str子串,返回-1
}
方法名 | 功能 |
---|---|
boolean endsWith(String suffix) | 测试此字符串是否以指定的后缀结束 |
boolean startsWith(String prefix) | 测试此字符串是否以指定的前缀开始 |
boolean startsWith(String prefix, int toffset) | 测试此字符串从指定索引开始的子字符串是否以指定前缀开始 |
@Test
public void test3(){
// 1. endsWith(String suffix) // 测试此字符串是否以指定的后缀结尾, 区分大小写
String s1 = "helloworld";
System.out.println(s1.endsWith("d")); // true;
System.out.println(s1.endsWith("rld")); // true;
System.out.println(s1.endsWith("LD")); // false
// 2. startWith(String prefix) // 测试此字符串是否以指定的前缀开始,区分大小写
System.out.println(s1.startsWith("hel")); // true;
System.out.println(s1.startsWith("HEL")); // false;
// 3. startsWith(String prefix, int toffset) 测试此字符串从指定索引开始的子字符串是否以指定前缀开始
String s2 = "012345";
System.out.println(s2.startsWith("34")); // false
System.out.println(s2.startsWith("34",3)); // true
}
字符串替换
方法名 | 功能 |
---|---|
String replace(char oldChar, char newChar) | 将原字符串中所有为oldChar的字符替换为newChar字符串,并返回这个新字符串。 |
String replace(CharSequence target, CharSequence replacement) | 使用给定的replacement 序列替换此字符串中所有匹配target 的子字符串,返回新字符串。 |
String replaceAll(String regex, String replacement) | 使用给定的replacement 替换此字符串所有匹配给定的正则表达式的子字符串,返回新字符串。 |
String replaceFirst(String regex, String replacement) | 使用给定的replacement 替换此字符串匹配给定的正则表达式的第一个子字符串,返回新字符串。 |
注意:字符串不可变性,原字符串的value
不会被修改。
@Test
public void test4(){
// 1. replace(char oldChar, char newChar): 返回一个新的字符串, 它是通过用 newChar 替换此字符串中出现的【所有】 oldChar 得到的。
String s1 = "我爱北京天安门, 我爱北京天安门";
String s2 = s1.replace('我', '你');
System.out.println(s1); // "我爱北京天安门, 我爱北京天安门", 源字符串不变
System.out.println(s2); // "你爱北京天安门, 你爱北京天安门" , 所有oldChar都会被替换
// 2. replace(CharSequence target, CharSequence replacement): 使用给定的`replacement`序列替换此字符串中所有匹配`target`的子字符串。
String s3 = s1.replace("天安", "东直");
System.out.println(s3); // "我爱北京东直门, 我爱北京东直门"
// 3. replaceAll(String regex, String replacement) : 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
String str = "12hello34world5java7891mysql456";
// 把字符串中的数字替换成 ","
String s4 = str.replaceAll("\\d+", ",");
System.out.println(s4); // ",hello,world,java,mysql,"
// 如果结果中开头和结尾有 "," 的话去掉
System.out.println(s4.replaceAll("^,|,$", "")); // "hello,world,java,mysql"
}
字符串分割
方法名 | 功能 |
---|---|
String[ ] split(String regex) | 根据给定正则表达式的匹配拆分此字符串,返回由分割后的子串组成的数组。 |
String[ ] split(String regex, int limit) | 根据匹配给定的正则表达式来拆分此字符串, 最多不超过limit个,如果超过了,剩下的全部都放到数组最后一个元素中。 |
@Test
public void test5(){
// 1. String[] split(String regex)
String s1 = "123,456,789";
String[] strs = s1.split(",");
for(String s : strs){
System.out.println(s);
}
/*输出结果
123
456
789
*/
// 按非小写字母进行分割
String[] strs2 = "abc.()%3def&*@CCCXX#ghi".split("[^a-z]+");
for(String s : strs2){
System.out.println(s);
}
/* 输出结果:
abc
def
ghi
*/
// 2. String[ ] split(String regex, int limit)
String[] strs1 = s1.split(",", 2);
for(String s : strs1){
System.out.println(s);
}
/* 输出结果:
123
456,789 // 超过limit的被放在一组
*/
}
字符串截取
方法名 | 功能 |
---|---|
String substring(int beginIndex) | 返回一个新的字符串, 它是此字符串的从beginIndex 开始截取到最后的一个子字符串。 |
String substring(int beginIndex, int endIndex) | 返回一个新字符串, 它是此字符串从beginIndex 开始截取到endIndex (不包含)的一个子字符串。 |
@Test
public void test6(){
// 1. substring(int beginIndex) 返回一个新的字符串, 它是此字符串的从beginIndex开始截取到最后的一个子字符串
String s7 = "我爱北京天安门";
String s8 = s7.substring(2);
System.out.println(s7); // "我爱北京天安门" s7不变
System.out.println(s8); // "北京天安门"
// 2. substring(int beginIndex, int endIndex), 返回一个新字符串, [beginindex,endIndex) 左闭右开区间
String s9 = s7.substring(2, 4);
System.out.println(s9); // 北京
}
其它常用方法
方法名 | 功能 |
---|---|
int length() | 返回字符串的长度,return value.length |
boolean isEmpty() | 判断是否是空字符串,“” 表示长度为0的空字符串,而不是null |
String toLowerCase() | 将字符串中的字母转换为小写 |
String toUpperCase() | 将字符串中的字母转换为大写 |
String trim() | 返回字符串的副本, 忽略前导空白和尾部空白 |
boolean matches(String regex) | 判断此字符串是否匹配给定的正则表达式 |
@Test
public void test7(){
// 1. length() 获取字符串长度
String s1 = "HelloWorld";
System.out.println(s1.length()); // 10
// 2. isEmpty() 字符串是否为空
System.out.println(s1.isEmpty()); // false
System.out.println("".isEmpty()); // true;
// 3. toLowerCase() 将字符串中的字母转换为小写
String s2 = s1.toLowerCase();
System.out.println(s1); // "HelloWorld"
System.out.println(s2); // "helloworld"
// 4. toUpperCase() 将字符串的字母转换为大写
s2 = s1.toUpperCase();
System.out.println(s1); // "HelloWorld"
System.out.println(s2); // "HELLOWORLD"
// 5. trim() 返回字符串的副本, 忽略前导空白和尾部空白
String s3 = " he ll o wo rld ";
String s4 = s3.trim();
System.out.println(s3); // " he ll o wo rld "
System.out.println(s4); // "he ll o wo rld"
// 6. matches(String regex): 判断此字符串是否匹配给定的正则表达式。
String s2 = "12345";
// 判断str字符串中是否全部有数字组成,即有1-n个数字组成
boolean matches = s2.matches("\\d+");
System.out.println(matches); // true
String tel = "0571-4534289";
// 判断这是否是一个杭州的固定电话
boolean result = tel.matches("0571-\\d{7,8}");
System.out.println(result); // true
}
String与其他类型之间的转换
String与基本数据类型、包装类之间的转换。
String
—> 基本数据类型、包装类:调用包装类的静态方法parseXxx(str)
String s1 = "123";
String s2 = "TRue";
int in = Integer.parseInt(s1);
boolean bo = Boolean.parseBoolean(s2);
System.out.println(in); // 123
System.out.println(bo); // true
- 基本数据类型、包装类 —>
String
: 调用String重载的**valueOf()
**方法
// valueOf()方法
int num = 123;
boolean bool = true;
String s3 = String.valueOf(num);
String s4 = String.valueOf(true);
System.out.println(s3); // "123"
System.out.println(bo); // "true"
// 也可以用 + "" 转换
String s5 = 456 + "";
System.out.println(s5); // "456"
String 与 char[ ] 之间的转换
String ---> char[]
: 调用String的**toCharArray()
**方法
String s1 = "abc123";
char[] chars = s1.toCharArray();
for (int i = 0; i < chars.length; i++) {
System.out.print(chars[i]); // "abc123"
}
System.out.println();
char[] ---> String
:调用String(char[])
构造器
char[] arr = new char[]{'h','e','l','l','o'};
String s2 = new String(arr);
System.out.println(s2); // "hello"
String 与 byte[ ]之间的转换
String
于byte[]
的之间的转换也叫做编码和解码,编码和解码时需要字符编码相同,否则会出现乱码情况。关于默认的字符编码,在IDEA种可以进行设置,我这里默认是utf8。
- 编码:
String ---> byte[]
: 调用String的**getBytes()
**方法
@Test
public void test() throws UnsupportedEncodingException { // getBytes(String charsetName) 抛出的异常
String s1 = "abcdef";
byte[] bytes = s1.getBytes();
System.out.println(Arrays.toString(bytes)); // [97, 98, 99, 100, 101, 102]
// 使用默认的字符编码,进行编码(utf-8)
String s2 = "中国";
byte[] bytes1 = s2.getBytes();
System.out.println(Arrays.toString(bytes1)); // [-28, -72, -83, -27, -101, -67]
// 说明:utf-8中一个汉字使用3个字节表示
// getBytes(charsetName)使用指定字符编码
byte[] gbks = s2.getBytes("gbk"); // 使用gbk字符编码
System.out.println(Arrays.toString(gbks)); // [-42, -48, -71, -6]
// 说明:gbk中一个汉字使用2个字节表示
}
- 解码:
byte[] ---> String
: 调用String的构造器
@Test
public void test() throws UnsupportedEncodingException {
// 使用默认的字符编码,进行编码(utf-8)
String s2 = "中国";
byte[] bytes1 = s2.getBytes();
// 使用默认的字符编码,进行解码
// String(byte[] byte)
String s3 = new String(bytes1);
System.out.println(s3); // 中国
// 使用gbk进行编码
byte[] gbks = s2.getBytes("gbk");
// 使用默认的字符编码,进行解码
String s4 = new String(gbks);
System.out.println(s4); // �й� 乱码, 原因编码集和解码集不一致!
// 使用gbk字符编码,进行解码
// String(byte[] bytes, String charsetName)
String s5 = new String(gbks, "gbk");
System.out.println(s5); // 中国
}
编码:字符串 —> 字节 (看得懂的字符 —> 看不懂的二进制数据)
解码:字节 —> 字符串 (看不懂的二进制数据 --> 看得懂的字符)
注意:在解码时,要求解码使用的字符编码必须与编码时使用的字符编码一致,否则出现乱码。
StringBuffer与StringBuilder
概述
StringBuffer
和StringBuilder
是两个类似于String
的字符串缓冲区。两个类同样是被final
修饰的,即不可以被继承。两个类都继承于AbstractStringBuilder
抽象类,与String
最大的区别在于底层的value
数组引用是非final
修饰的,也就是其指向的内容是可以改变的。StringBuffer
是JDK1.0时期已经存在的一个类,这个类是一个线程安全的类。而StringBuilder
是JDK5.0增加的一个类,该类提供一个与StringBuffer
兼容的 API,不过是一个线程不安全的类。StringBuilder
设计用作StringBuffer
的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。因此从效率上来说StringBuilder
是要优于StringBuffer
的。
实例化
StringBuffer
与StringBuilder
不可以直接使用字符串字面量来进行实例化,只允许使用构造器进行实例化。两个类中提供的构造器相同,这里以StringBuilder
为例。
-
StringBuilder()
构造一个其中不带字符的字符串缓冲区,其初始容量为 16 个字符。
StringBuilder builder = new StringBuilder();
-
StringBuffer(CharSequence seq)
构造一个字符串生成器,它包含与指定的
CharSequence
相同的字符,该构造器参数可以是CharSequence
实现类的实例,也就是StringBuffer,StringBuilder都可以作为构造器参数。
StringBuilder builder = new StringBuilder();
StringBuffer buffer = new StringBuffer();
// StringBuilder和StringBuilder作为参数时会调用这个构造器
StringBuilder builder1 = new StringBuilder(builder);
StringBuilder builder2 = new StringBuilder(buffer);
-
StringBuffer(int capacity)
构造一个不带字符,但具有指定初始容量的字符串缓冲区。
StringBuilder builder = new StringBuilder(100);
-
StringBuffer(String str)
构造一个字符串缓冲区,并将其内容初始化为指定的字符串内容。
StringBuilder builder = new StringBuilder("abc");
自动扩容
append()方法
StringBuffer
与StringBuilder
可以使用append()
方法在底层value
数组的有效字符后进行新的字符追加,append()
方法定义了大量的重载方法,可以将任何数据类型的元素添加进来,当value
数组的容量不够时,就会进行扩容。
append()
方法的重载方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-045S0ivj-1632792675363)(D:\课件\笔记\java\02-Java进阶\02-常用类\image-20210927230853047.png)]
举例:
public static void main(String[] args) {
StringBuilder builder = new StringBuilder(); // 初始容量为16
builder.append(111); // 添加整型
builder.append(12.3); // 添加浮点型
builder.append('a'); // 添加字符
builder.append(false); // 添加布尔型
builder.append("string"); // 添加字符串
char[] chars = {'a', 'b', 'c','d','e','f'};
builder.append(chars); // 添加数组
builder.append(chars,2,3); // 指定数组起始偏移量和添加的个数。从数组索引2开始相后添加3个字符
System.out.println(builder); // 11112.3afalsestringabcdefcde
}
自动扩容机制
上述的添加操作,当内容超过原数组容量时,就会进行自动扩容。下面以参数为String时,进行举例:
实例化建议
从自动扩容的底层实现来看,我们在实例化StringBuffer
或者StringBuffer
应该优先选择使用:new StringBuffer(int capacity)
或 new StringBuffer(int capacity)
,特别是在缓冲区的容量大致确定的情况下,指定容量进行初始化只会创建一次数组,之后的append()
操作都是直接在数组count
的索引位置开始向后添加字符,效率最高。
常用方法
增加
append()
方法,在上面已经介绍。
删除
方法名 | 功能 |
---|---|
StringBuilder delete(int start,int end) | 删除[start,end)区间内的内容 |
public void test1(){
StringBuilder s1 = new StringBuilder("abc");
// 1. delete(int start,int end) 删除指定区间内的字符串[start,end)
System.out.println(s1.delete(1,4)); // "a1" 返回新的字符串,删除"bc1"
System.out.println(s1); // "a1" 源字符串也被改变
}
修改
方法名 | 功能 |
---|---|
StringBuilder replace(int start, int end, String str) | 把[start,end)区间内的内容替换为str |
void setCharAt(int n ,char ch) | 将指定索引处的的字符改为ch |
public void test2(){
StringBuilder s2 = new StringBuilder("abcdef");
// 1. replace(int start, int end, String str) // 将指定区间内的字符串替换为str(长度可以不相同) [start,end) 左闭右开区间
s2.replace(1,3,"xxx"); // axxxdef
System.out.println(s2); // accccccaaaa
// 2. setCharAt(int index, char ch) 将指定索引处的的字符改为ch
s2.setCharAt(4,'X');
System.out.println(s2); // axxxXef
}
查找
方法名 | 功能 |
---|---|
int indexOf(String str) | 返回str字符串第一次出现位置的索引,找不到返回-1 |
char charAt(int n ) | 返回指定索引处的字符 |
public void test3(){
// 1. indexOf(String str) 返回str字符串第一次出现位置的索引,找不到返回-1
StringBuilder s2 = new StringBuilder("abcdef");
System.out.println(s2.indexOf("cde")); // 2
System.out.println(s2.indexOf("xxx")); // -1
// 2. charAt(int n ) 返回指定索引处的字符
System.out.println(s2.charAt(0)); // a
System.out.println(s2.charAt(1)); // b
System.out.println(s2.charAt(2)); // c
}
插入
方法名 | 功能 |
---|---|
StringBuilder insert(int offset, xxx) | 在指定位置插入xxx,xxx代表如下数据类型 |
public void test4(){
StringBuilder s2 = new StringBuilder("abcdef");
s2.insert(3,"hello");
System.out.println(s2); // abchellodef
s2.insert(4,false);
System.out.println(s2); // abchfalseellodef
//...
}
其它
方法名 | 功能 |
---|---|
public int length() | 返回字符串长度(实际使用的长度) |
String substring(int start,int end) | 反回一个从[start,end)区间内的字符串 |
StringBuilder reverse() | 把当前字符序列逆转 |
public void test5(){
StringBuffer s2 = new StringBuffer("abcdef");
// 1. reverse() 反转字符串
s2.reverse();
System.out.println(s2); // fedcba
// 2. subString(int start, int end) // 返回一个从start开始得到end前结束的左闭右开区间字符串。
String s3 = s2.substring(2, 5);
System.out.println(s3); // "dcb"
System.out.println(s2); // "fedcba", 源字符串不会改变
// 3. length() 返回字符串长度(实际使用的长度)
System.out.println(s2.length()); // 6
}
String 与 StringBuffer、StringBuilder之间的转换
String ---> StringBuffer、StringBuilder
:调用StringBuffer、StringBuilder构造器。
String s1 = "abc";
StringBuffer buffer = new StringBuffer(s1);
StringBuilder builder = new StringBuilder(s1);
StringBuffer、StringBuilde --> String
:调用String构造器;StringBuffer、StringBuilde的 toString() 方法。
// 调用String()构造器 接上面
String s2 = new String(buffer);
String s3 = new String(builder);
// 调用toString()方法
String s4 = buffer.toString();
String s5 = builder.toString();
String、StringBuffer、StringBuilder 三者的异同
相同
三者都是用来存储与操作字符串的类,底层都使用了char[]
数组存放字符串。
不同
实例化不同
-
String的实例化方式可以是字面量赋值和构造器实例化两种。
-
StringBuffer和StringBuilder只能通过构造器的方式进行实例化。
可变性不同
- String是不可变的字符序列。
- StringBuffer和StringBuilder是可变的字符串序列。
线程安全性不同
- String因为不可变性,所以线程安全。
- StringBuffer中的方法都是同步方法(
synchronized
修饰的),因此是线程安全的。 - StringBuilder中的方法都是普通方法,因此是线程不安全的。
关于线程的问题不是本文介绍的内容,如果不太理解可以想象有一个厕所,厕所有把锁,谁进去就把门锁住,这样大家只能排队上厕所,而不会发生你在上厕所,隔壁王叔叔进来串门的情况。
效率不同
- String因为其不可变性,效率最低。
- StringBuffer支持线程安全,开销更大,所以效率较低。
- StringBuilder因为线程不安全,开销更小,所以效率最高。
String、StringBuffer、StringBuilder三者效率对比
在介绍String
的字符串拼接时,我们经过测试得出,在进行大量重复的字符串拼接操作时,String的concat()
方法效率远远高于+
运算符,但是最后又说到编译器之所以采用StringBuilder
优化是因为它的效率才是最高的,那么我们来看看为什么。
我们采用4个字符串变量作为拼接的内容,防止+
加运算符在编译期间被优化。
public static void main(String[] args) {
String s1 = "ab";
String s2 = "cd";
String s3 = "ef";
String s4 = "hi";
String s5 = s1 + s2 + s3 + s4;
String s6 = s1.concat(s2).concat(s3).concat(s4);
}
来看看这两个代码底层发生了什么:
第13~40行属于String s5 = s1 + s2 + s3 + s4;
所进行操作。
第42~56行属于String s6 = s1.concat(s2).concat(s3).concat(s4);
所进行的操作。
单从这个行数来看,貌似是concat
所进行的操作更少。但是我们可以仔细看分析一下这几行代码:
-
对于
s5
的初始化操作,从开始到结束,只new
了一个StringBuilder
,然后是4次append()
,并且没有扩容产生新的数组,最后又new
了一个String
,底层创建了一个数组将内容拷贝进来。所以这个过程实际上创建了2个对象,一个StringBulder
和一个String
。 -
而对于
s6
的初始化操作,从开始到结束,调用了3次concat
方法,我们知道每一次concat
底层都会新建一个数组,并以该数组为参数新建一个String
对象,所以这个过程实际上创建了3个String
对象。
按这样分析,对s5
的赋值应该是要更优一些。但是这差距也不明显啊?
我们可以想一下,假如一个类有好几个字段,在
toString()
的时候需要将字段的值进行拼接,那么随着字段的增加+
运算符其实从头至尾只创建了2个对象 (需要扩容时才会创建新数组),而concat()
是有多少拼接的内容就要创建多少个String
。如果我有一个数组中存放的全是这个类的对象,并且我想循环打印信息,那么单从toString()
所产生对象的角度来看,+
是O(n)
,concat
是O(n^2)
。
同时+
可以拼接任何类型的数据,其原因就是底层调用了append()
的方法,而concat()
只能接收String
类型的参数,因此从通用性上来说,+
也要更好一些。
好家伙,这一下整懵了,之前说concat
拼接效率高,现在又说+
运算符要更优,那我该如何选择?
其实把扯这么多只是想说明concat()
在做字符串拼接时,并不是最优选择。
-
如果是在一行上的拼接,也就是拼接字符的数量确定时(例如
toString
中对字段的拼接),我们可以使用+
运算,因为底层的优化很好的利用了StringBuilder
的特性,并且使用也更加简单。 -
而对于大量重复的拼接操作(涉及循环),我们应该考虑使用
StringBuilder
或StringBuffer
,此时如果使用+
就会产生大量对象,使用concat()
也会产生很多对象,而使用StringBuilder
或StringBuilder
时,我们将新建的对象放在循环外面,并且给定初始容量,那么其实整个操作的过程中就只会有2个对象的产生。
测试:
public static void main(String[] args) {
String s1 = "";
String s2 = "";
StringBuilder builder = new StringBuilder();
StringBuffer buffer = new StringBuffer();
String s3 = "abcdefg"; // 拼接内容
long start;
long end;
// 测试 + 拼接效率
start = System.nanoTime();
for (int i = 0; i < 20000; i++) {
s1 += s3;
}
end = System.nanoTime();
System.out.println("使用+拼接: " + (end - start) / 1000 / 1000 + "ms"); // 单位ms
// 测试 concat() 拼接效率
start = System.nanoTime();
for (int i = 0; i < 20000; i++) {
s2.concat(s3);
}
end = System.nanoTime();
System.out.println("使用concat拼接: " + (end - start) / 1000 / 1000 + "ms");
// 测试 StringBuffer 拼接效率
start = System.nanoTime();
for (int i = 0; i < 20000; i++) {
buffer.append(s3);
}
end = System.nanoTime();
System.out.println("使用StringBuffer拼接: " + (end - start) / 1000 / 1000 + "ms");
// 测试 StringBuilder 拼接效率
start = System.nanoTime();
for (int i = 0; i < 20000; i++) {
builder.append(s3);
}
end = System.nanoTime();
System.out.println("使用StringBuilder拼接: " + (end - start) / 1000 / 1000 + "ms");
}
测试结果:
这是在没有给定StringBuffer
与StringBuilder
初始容量的情况下测出的结果,相信如果能预估出一个StringBuffer
或者StringBuilder
所需的容量时,效率会更高。
字符串相关练习题
/**
* 一些练习题:
*/
class MyStringUtil {
// 模拟trim方法
public static String myTrim(String str) {
if (str.isEmpty()) {
return "";
}
char[] chars = str.toCharArray();
int begin = 0;
int end = chars.length - 1;
while (true) {
if (chars[begin] == ' ') {
begin++;
}
if (chars[end] == ' ') {
end--;
}
if (chars[begin] != ' ' && chars[end] != ' ') {
break;
}
}
return str.substring(begin, end + 1);
}
// reverse字符串, 指定字符串版本
public static String reverseStr(String mainStr, String subStr) {
if (mainStr == null || subStr == null) {
return null;
}
int begin = mainStr.indexOf(subStr);
if (begin == -1) {
return mainStr;
}
int end = begin + subStr.length() - 1;
char[] chars = mainStr.toCharArray();
while (begin < end) {
chars[begin] ^= chars[end];
chars[end] ^= chars[begin];
chars[begin] ^= chars[end];
begin++;
end--;
}
return new String(chars);
}
// reverse字符串, 指定位置版本[start,end)
public static String reverseStr(String str, int start, int end) {
if (str == null || end == 0) {
return str;
}
// 创建一个StringBuffer对象,长度指定为str.length()
StringBuffer s = new StringBuffer(str.length());
// 将str指定位置前的字符串添加到s中
s.append(str.substring(0, start)); // [0,start)
// 将str中从end - 1 开始到start的字符反向添加到s中
for (int i = end - 1; i >= start; i--) {
s.append(str.charAt(i));
}
// 从end开始到str末尾的字符串添加到s中
s.append(str.substring(end));
return s.toString();
}
// 获取一个字符串在另一个字符串中出现的次数
public static int getStrAppearNum(String mainStr, String subStr) {
if (mainStr == null || subStr == null) {
return 0;
}
if (mainStr.length() < subStr.length() || subStr.length() == 0) {
return 0;
}
int start = mainStr.indexOf(subStr);
int end = mainStr.lastIndexOf(subStr);
int count = 0;
while (start < end) {
count += 2;
// int rightOffset = begin + subStr.length();
// int leftOffset = end - subStr.length();
start = mainStr.indexOf(subStr, start + subStr.length());
end = mainStr.lastIndexOf(subStr, end - subStr.length());
}
if (start == end && start != -1) {
count++;
}
return count;
}
/*获取两个字符串中最大相同子串(只有一个最大相同子串版本)
* 比如:
* str1 = "abcwerthelloyuiodef";str2 = "cvhellobnm"
*
* 1. 两个字符串中的最大相同子串,长度一定不会超过较短的字符串。
* 2. 所以可以从较短字符串查找,长度依次递减,并且遍历该长度下,每一种可能的组合(使用整体位移方式)
* 3. 使用subString()方法来截取长度递减后,每一种可能的字符串。
* 4. 并使用contains(sub)方法判断子串是否在较大字符串中。
* 5. 如果发现这个子串存在于较大字符串中,则返回这个字符串
* 6. 如果这个字符不存在于较大字符串中,则继续下次循环
* 7. 因为子串长度每次递减1,而子串是由较短字符串截取得来,所以从较短字符串长度开始一直到长度为0结束。
*/
//
public static String getMaxSameString(String str1, String str2) {
if(str1 == null || str2 == null){
return null;
}
// 判断较长字符串和较短字符串
String longerStr = (str1.length() >= str2.length()) ? str1 : str2;
String shorterStr = (str1.length() < str2.length()) ? str1 : str2;
int length = shorterStr.length();
for(int i = 0; i < length; i++){ // 子串的截取次数不能超过较短字符串的长度,超过了子串长度就为0,无意义
// 使用两个索引作为子串的头和尾,每次获取子串也是依靠这两个索引获得,并且两个索引每次遍历都是向右移动1,直到右索引 == length 时,说明当前长度下的所有可能子串遍历完毕
for(int l = 0,r = length - i; r <= length;l++,r++){
// 获取shorterStr在[l,r)的子串
String subStr = shorterStr.substring(l, r);
if(longerStr.contains(subStr)){
return subStr;
}
}
}
return null;
}
/*获取两个字符串中最大相同子串(有多个最大相同子串版本)
*
* 1. 总体逻辑和获取一个最大相同子串的逻辑类似,只不过当存在多个最大相同子串时,需要用数组来接收
* 2. 但是数组的长度不好确定,所以先用StringBuilder类的对象接收每一个相同的子串,并在中间使用","分割
* 3. 当内层循环找到一个相同子串时, 不应该立即退出循环, 而是应该将该字符串append到StringBuilder字符串中
* 4. 接着再继续循环,将两个索引往右移动获取相同长度下其他可能的子串进行对比, 如果有相同子串,依然需要append到StringBuilder字符串中
* 5. 当内层循环结束时,需要判断StringBuilder字符串的长度是不是为0,如果为0则说明还没找到相同子串,如果不为0,则说明已经找到相同子串,则不需要再继续循环
* 6. 退出循环后,需要将StringBuilder字符串, 转换为String类的字符串,并且分割成String[] 字符数组,并返回。
*/
public static String[] getMaxSameStringMul(String str1, String str2) {
if(str1 == null || str2 == null){
return null;
}
// 前面的逻辑和上面代码逻辑相同
// // 判断较长字符串和较短字符串
String longerStr = (str1.length() >= str2.length()) ? str1 : str2;
String shorterStr = (str1.length() < str2.length()) ? str1 : str2;
// 创建一个StringBuild的对象,用来接收不同的子串
StringBuilder sameStr = new StringBuilder();
int len = shorterStr.length();
for(int i = 0; i < len; i++)
{
for(int l = 0, r = len - i;r <=len;l++,r++ ){
// 获取shorterStr在[l,r)的子串
String subStr = shorterStr.substring(l, r);
if(longerStr.contains(subStr)){
// 使用StringBuilder的append方法将这个相同子串添加到StringBuilder的字符串中
sameStr.append(subStr + ","); // 后面加"," 为后面分割成字符数组做准备
// 此时不能退出循环,而是将两个索引继续像右移动,判断在当前长度下还存不存在相同的子串
}
}
// 当结束内层for循环后,判断如果sameStrs的长度不为0,则说明已经有子串被添加其中,则不需要再遍历
if(sameStr.length() != 0){
break;
}
}
// 将StringBuilder字符串转换为String字符串,再把末尾的","去掉, 然后剩下的内容再按照","分割成字符串放入数组中
// $ 是正则表达式匹配字符串结束位置
String[] strs = sameStr.toString().replaceAll(",$","").split(",");
return strs;
}
}
public class Practice {
public static void main(String[] args) {
// 测试trim方法
String str = " abc asdas if ";
String s1 = MyStringUtil.myTrim(str);
System.out.println(s1); // "abc asdas if"
// 测试reverse, 指定字符串版本
String str1 = "abcdefg";
String s2 = MyStringUtil.reverseStr(str1, "cdef");
System.out.println(s2); // "abfedcg"
// 测试reverse, 指定位置版本
String s3 = MyStringUtil.reverseStr(str1, 2, 6);
System.out.println(s3); // "abfedcg"
// 测试getStrAppearNum方法
String str2 = "abkkcadkabkebfkabkskab";
int count = MyStringUtil.getStrAppearNum(str2, "ab");
System.out.println(count); // 4
// 测试getMaxSameString方法(只有一个最大相同子串版本)
String str3 = "abcwerthelloyuiodef";
String maxSameString = MyStringUtil.getMaxSameString(str3, "cvhellobnm");
System.out.println(maxSameString); // "hello"
// 测试getMaxSameString方法(有多个最大相同子串版本)
String str4 = "abclllllwerthelloyuiodefabcde";
String[] maxSameStrings = MyStringUtil.getMaxSameStringMul(str4, "cvhellobnlllllmabcde");
System.out.println(Arrays.toString(maxSameStrings)); // [hello, lllll, abcde]
}
}