1. 创建字符串
常见的构造 String 的方式:
// 方式一
String str = "Hello Bit";
// 方式二
String str2 = new String("Hello Bit");
// 方式三
char[] array = {'a', 'b', 'c'};
String str3 = new String(array);
在官方文档上(https://docs.oracle.com/javase/8/docs/api/index.html)可以看到 String 还支持很多其他的构造方式,用到的时候去查就可以了。
注意事项:
(1)“hello” 这样的字符串字面值常量,类型也是 String。
(2)String 也是引用类型,String str = “Hello”; 这样的代码内存布局如下:
回忆 “引用”
之前在讲数组的时候就提到了引用的概念。
引用类似于 C 语言中的指针,只是在栈上开辟了一小块内存空间保存一个地址。但是引用和指针又
不太相同,指针能进行各种数字运算(指针+1之类的),但是引用不能,这是一种 “没那么灵活” 的指针。
另外,也可以把引用想象成一个标签,“贴” 到一个对象上,一个对象可以贴一个标签,也可以贴多个。如果一个对象上面一个标签都没有,那么这个对象就会被 JVM 当做垃圾对象回收掉。
Java 中,数组、String 以及自定义的类都是引用类型。
由于 String 是引用类型,因此对于以下代码:
String str1 = "Hello";
String str2 = str1;
内存布局如图:
那么,是不是修改 str1,str2 也会随之变化呢?
public class Test{
public static void main(String[] args) {
String str1 = "Hello";
String str2 = str1;
System.out.println(str2);
str1 = "World";
System.out.println(str2);
}
}
我们发现,“修改” str1 之后,str2 也没发生变化,还是 “Hello”?
事实上,str1 = “World” 这样的代码并不算 “修改” 字符串,而是让 str1 这个引用指向了一个新的String 对象。
2. 字符串比较相等
2.1 字符串比较的剖析
大部分编程语言中,比较两个字符串内容的相等,主要使用 ==。
但是 C 和 Java 语言例外,因为 C 语言中的字符串实际是字符数组,两个字符数组用 == 来比较,会隐式转换成指向数组首元素的指针,实际上比较的是两个指针指向是否一致,并不是比较字符串内容。
在 Java 语言中,如果针对引用类型使用 ==,是在比较两个引用保存的地址是否相等。
如果现在有两个 int 类型的变量,判断其相等可以使用 == 完成。
public class Test{
public static void main(String[] args) {
int x = 10;
int y = 10;
System.out.println(x == y);
}
}
如果在 String 类对象上使用 == 呢?
代码1:
public class Test{
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
}
}
看起来貌似没什么问题,再换个代码试试,发现情况不太妙。
代码2:
public class Test{
public static void main(String[] args) {
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2);
}
}
分析两种创建 String 类的方式差异。
代码1内存布局:
str1 和 str2 是指向同一个对象的,此时如 “Hello” 这样的字符串常量是保存在 字符串常量池 中。
关于字符串常量池
如 “Hello” 这样的字符串字面值常量,也是需要一定的内存空间来存储的。这样的常量具有一个特点,就是不需要修改(常量嘛)。所以如果代码中有多个地方引用都需要使用 “Hello” 的话,就直接引用到常量池的这个位置就行了,而没必要把 “Hello” 在内存中存储两次。
代码2内存布局:
通过 String str1 = new String(“Hello”); 这样的方式创建的 String 对象相当于在堆上另外开辟了空间来存储 “Hello” 的内容,也就是内存中存在两份 “Hello”。
String 使用 == 比较字符串,并不是在比较字符串内容,而是比较两个引用是否指向同一个对象(比较两个引用中保存的地址是否相等)。
关于对象的比较
面向对象编程语言中,涉及到对象的比较,有三种不同的方式,比较身份、比较值、比较类型。
在大部分编程语言中,== 是用来比较比较值的,但是在 Java 中是用来比较身份的。
如何理解比较值和比较身份呢?
可以想象一个场景,快递储物柜有很多格子,每个格子里都放着东西。
例如,“第二行, 左数第五列” 这个柜子和 “第二行,右数第二列” 这个柜子是同一个柜子,就是 身份相同。如果身份相同,那么里面放的东西一定也相同(值一定也相同)。
例如,“第一行,左数第一列” 这个柜子和 “第一行,左数第二列” 这两个柜子不是同一个柜子,但是柜子打开后,发现里面放着的是完全一模一样的两双鞋子,这个就是 值相同。
2.2 equals 方法
Java 中想要比较字符串的内容,必须采用 String 类提供的 equals 方法。
public class Test{
public static void main(String[] args) {
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1.equals(str2));
}
}
equals 方法使用的技巧:
比较 str 和 “Hello” 两个字符串是否相等,应该如何来写呢?
String str = new String("Hello");
// 方式一
System.out.println("Hello".equals(str));
// 方式二
System.out.println(str.equals("Hello"));
更推荐使用 “方式一”,因为一旦 str 是 null,方式二的代码会抛出异常,而方式一不会。
public class Test{
public static void main(String[] args) {
String str = null;
// 方式一
System.out.println("Hello".equals(str));
// 方式二
System.out.println(str.equals("Hello"));
}
}
注意事项: “Hello” 这样的字面值常量,本质上也是一个 String 对象,完全可以使用 equals 等 String 对象的方法。
3. 字符串常量池
在上面的例子中,String 类的两种实例化操作,直接赋值以及 new 一个新的 String 类对象。
3.1 直接赋值
public class Test{
public static void main(String[] args) {
String str1 = "hello";
String str2 = "hello";
String str3 = "hello";
System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str2 == str3);
}
}
内存布局:
为什么没有开辟新的堆内存空间呢?
String 类的设计使用了共享设计模式。
在 JVM 底层实际上会自动维护一个对象池(字符串常量池)。
(1)如果采用了直接赋值的方式进行 String 类的对象实例化操作,那么该实例化对象(字符串内容)将自动保存到这个对象池中。
(2)如果下次继续使用直接赋值的方式声明 String 类对象,此时对象池中如果有指定内容,将直接进行引用。如果没有,则开辟新的字符串对象,然后将其保存在对象池中以供下次使用。
理解 “池” (pool)
“池” 是程序中的一种重要的提升效率的方式,目的就是为了降低开销,提高效率。本质是把频繁使用过的东西提前保存好,以备用到的时候,随时可以使用。
在未来的学习中,我们会遇到各种 “内存池”,“线程池”,“数据库连接池” …
3.2 采用构造方法
类对象使用构造方法实例化是标准做法。分析如下程序:
String str = new String("hello");
这种做法有两个缺点:
(1)如果使用 String 构造方法就会开辟两块堆内存空间,并且其中一块堆内存将成为垃圾空间(字符串常量 “hello” 也是一个匿名对象,用了一次之后就不再使用了,就成为垃圾空间,会被 JVM 自动回收掉)。
(2)字符串共享问题,同一个字符串可能会被存储多次,浪费空间。
String 类的 intern 方法:
可以使用 String 的 intern 方法来手动把 String 对象加入到字符串常量池中。
public class Test{
public static void main(String[] args) {
String str1 = new String("hello");
String str2 = "hello";
System.out.println(str1 == str2);
// 使用 intern 方法
String str3 = new String("hello").intern();
System.out.println(str2 == str3);
}
}
内存布局:
调用 intern 方法,拿着当前字符串里的内容在字符串常量池中寻找,看当前内容是否存在于池中。如果存在,直接返回池中的地址;如果不存在,则把当前字符串的内容加到字符串常量池中,返回池中的地址。
面试题:请解释 String 类中两种对象实例化的区别
(1)直接赋值:只会开辟一块堆内存空间,并且该字符串对象可以自动保存在对象池中以供下次
使用。
(2)构造方法:会开辟两块堆内存空间,不会自动保存在对象池中,可以使用 intern() 方法手动入
池。
综上,一般采取直接赋值的方式创建 String 对象。
4. 理解字符串不可变
4.1 字符串不可变
Java 的 String 是一种不可变对象,它的内容不可改变。
String 类的内部实现也是基于 char[ ] 来实现的,但是 String 类并没有提供 set 方法之类的来修改内部的字符数组。
形如下面的代码:
public class Test{
public static void main(String[] args) {
String str = "hello";
str = str + " world";
str += "!!!";
System.out.println(str);
}
}
如 += 这样的操作,表面上好像修改了字符串,其实并没有。内存的变化如下:
+= 之后 str 打印的结果改变,但并不是 String 对象本身发生改变,而是 str 引用到了其他的对象。
回顾引用:
引用相当于一个指针,里面保存的内容是一个地址。要区分清楚当前的修改到底是修改了地址对应内存的内容,还是引用中保存的地址改变了。
为什么 Java 的 String 为什么要设计成不可变的?(不可变对象的优点?)
1、方便放入字符串对象池,如果 String 可变,池中的内容发生变化,就会影响到所有引用这个池对象的结果。
2、不可变对象是线程安全的。
3、不可变对象更方便缓存 hash code,作为key 时可以更高效地保存到 HashMap 中。
注意事项: 如下代码不应该在实际开发中出现,会产生大量的临时对象,效率比较低。
public class Test{
public static void main(String[] args) {
String str = "Hello";
for (int i = 0; i < 10; i++) {
str += i;
}
System.out.println(str);
}
}
4.2 必须要修改字符串
例如,有字符串 str = “Hello”,想改成 str = “hello”,该怎么办?
1、常见办法:借助原字符串,创建新的字符串。
public class Test{
public static void main(String[] args) {
String str = "Hello";
str = "h" + str.substring(1);
System.out.println(str);
}
}
2、特殊办法:使用 “反射” 这样的操作可以破坏封装,访问一个类内部的 private 成员。
IDEA 中 “ctrl + 鼠标左键” 跳转到 String 类的定义,可以看到内部包含了一个 char[ ],保存了字符串的内容。
import java.lang.reflect.Field;
public class Test{
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str = "Hello";
// 获取 String 的类对象
// 根据 value 字段,这个 value 和 String 源码中的 value 是匹配的,在类对象中拿到对应的字段
Field valueField = String.class.getDeclaredField("value");
// 将这个字段的访问属性设为 true,让 value 这个 private 的成员也能被访问的
valueField.setAccessible(true);
// 将 str 中的 value 属性获取到
char[] value = (char[]) valueField.get(str);
// 修改 value 的值
value[0] = 'h';
System.out.println(str);
}
}
关于反射
反射是面向对象编程的一种重要特性,有些编程语言也称为 “自省”。
指的是程序运行过程中,获取/修改某个对象的详细信息(类型信息,属性信息等),相当于让一个对象更好地 “认清自己”。
5. 字符、字节与字符串
5.1 字符与字符串之间的转换
6. 字符串常见操作
6.1 字符串比较
6.1.1 equals() 方法 - 区分大小写的比较(比较相等)
1、equals() 方法
String str1 = "hello";
String str2 = "Hello";
System.out.println(str1.equals(str2));
2、==
String 使用 == 比较字符串,并不是在比较字符串内容,而是比较两个引用是否指向同一个对象(比较两个引用中保存的地址是否相等)。
6.1.2 equalsIgnoreCase() 方法 - 不区分大小写的比较(比较相等)
equalsIgnoreCase() 方法
public class Test{
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "hello";
System.out.println(str1.equals(str2));
System.out.println(str1.equalsIgnoreCase(str2));
}
}
6.1.3 compareTo() 方法 - 比较大小
compareTo() 方法 :该方法返回一个整型,该数据会根据大小关系返回三类内容。
(1)相等:返回0;
(2)小于:返回内容小于0;
(3)大于:返回内容大于0。
注意: 这里的返回结果不是 -1 / +1,而是大于、小于0的数字。
字符串的比较大小规则:字典序
两个字符串,先比较首个字符的大小(根据 unicode 的值来判定),如果相同,就依次比较后面字符的大小,直到分出大小。
public class Test{
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "hello";
System.out.println(str1.compareTo(str2));
System.out.println("ABC".compareTo("aBC"));
System.out.println("A".compareTo("a"));
System.out.println("ABCLMN".compareTo("ABCDEF"));
System.out.println("A".compareTo("A"));
System.out.println("刘".compareTo("王"));
}
}
6.2 字符串查找
6.2.1 contains() 方法
判断一个子字符串是否存在
public class Test{
public static void main(String[] args) {
String str1 = "Hello World";
String str2 = "World";
String str3 = "World!";
System.out.println(str1.contains(str2));
System.out.println(str1.contains(str3));
}
}
6.2.2 indexOf() 方法
indexOf(String str):从头开始查找指定字符串的位置,查到后返回该位置的开始索引,如果查不到返回 -1。
indexOf(String str, int fromIndex):从指定位置开始查找子字符串位置。
public class Test{
public static void main(String[] args) {
String str1 = "Hello World";
String str2 = "World";
String str3 = "Hi";
System.out.println(str1.indexOf(str2));
System.out.println(str1.indexOf(str3));
System.out.println(str1.indexOf(str2, 4));
System.out.println(str1.indexOf(str2, 7));
System.out.println(str1.indexOf(str3, 2));
}
}
如果存在多个相同的子字符串,返回最左侧的结果的下表(从左往右查找,找到第一个结果就直接返回)。
public class Test{
public static void main(String[] args) {
String str1 = "Hello World World";
String str2 = "World";
System.out.println(str1.indexOf(str2));
}
}
6.2.3 lastIndexOf() 方法
lastIndexOf(String str):从后向前查找指定字符串的位置,查到后返回该位置的开始索引,如果查不到返回 -1。
lastIndexOf(String str, int fromIndex):从指定位置由后向前查找子字符串位置。
public class Test{
public static void main(String[] args) {
String str1 = "Hello World World";
String str2 = "World";
String str3 = "Hi";
int result = str1.lastIndexOf(str2);
System.out.println(result);
result = str1.lastIndexOf(str2, result - 1);
System.out.println(result);
System.out.println(str1.lastIndexOf(str3));
}
}
6.2.4 startsWith() 方法
startsWith(String prefix):判断是否以指定字符串开头,返回 true / false。
startsWith(String prefix, int toffset):从指定位置开始判断是否以指定字符串开头,返回 true / false。
public class Test{
public static void main(String[] args) {
String str1 = "Hello Hello World";
String str2 = "Hello";
String str3 = "Hi";
System.out.println(str1.startsWith(str2));
System.out.println(str1.startsWith(str3));
System.out.println(str1.startsWith(str2, 2));
System.out.println(str1.startsWith(str2, 6));
}
}
6.2.5 endsWith() 方法
endsWith(String suffix):判断是否以指定字符串结尾,返回 true / false。
public class Test{
public static void main(String[] args) {
String str1 = "Hello Hello World";
String str2 = "World";
String str3 = "Hello";
System.out.println(str1.endsWith(str2));
System.out.println(str1.endsWith(str3));
}
}
startsWith() 与 endsWith() 的典型用法
1、判断某个链接的协议类型,会使用 startsWith()
如:https://www.baidu.com/
2、判断某个文件的类型,会使用 endsWith() 判断扩展名
如:Test.java
6.3 字符串替换
6.3.1 replaceAll() / replaceFirst() 方法
replaceAll(String regex, String replacement):替换所有的指定内容。
replaceFirst(String regex, String replacement):替换首个内容。
注意事项: 使用一个指定的新的字符串替换掉已有的字符串数据。由于字符串是不可变对象,替换操作不修改当前字符串,而是产生一个新的字符串。
public class Test{
public static void main(String[] args) {
String str1 = "Hello Hello World";
String str2 = "Hello";
String result1 = str1.replaceAll(str2,"Big");
System.out.println("result1:" + result1);
System.out.println("str1:" + str1 + "\n");
String result2 = str1.replaceFirst(str2,"Big");
System.out.println("result2:" + result2);
System.out.println("str1:" + str1);
}
}
6.4 字符串拆分 - split() 方法
一个完整的字符串,按照指定的分隔符,划分为若干个子字符串,再把这些部分放在一个数组中。
split(String regex):按照指定的分隔符,将字符串全部拆分。
split(String regex, int limit):按照指定的分隔符,将字符串部分拆分,该数组长度就是 limit。
import java.util.Arrays;
public class Test{
public static void main(String[] args) {
String str1 = "Hello World Java C++";
// 按照空格拆分
String[] result1 = str1.split(" ");
System.out.println(Arrays.toString(result1));
String[] result2 = str1.split(" ", 2);
System.out.println(Arrays.toString(result2));
}
}
拆分是特别常用的操作,需要重点掌握。另外,正则表达式中有许多特殊字符代表特殊含义,作为分割符可能无法正确切分,需要加上转义字符。
import java.util.Arrays;
public class Test{
public static void main(String[] args) {
String str1 = "192.168.0.1";
// 拆分 IP 地址
// 正则表达式见到 '.' 是当成特殊符号对待
// 正则表达式见到 '\.' 才是当成 '.' 对待
// Java 的字符串中又将 '\' 当成 Java 的转义字符,为了表示一个原始的 '\',需要再次转义
String[] result1 = str1.split("\\.");
System.out.println(Arrays.toString(result1));
}
}
注意事项:
(1)字符 ’ | ‘,’ * ‘,’ + ’ 都得加上转义字符,前面加上 ’ \ '。
(2)如果是字符 ’ . ',那么就得写成 ’ \\. '。
(3)如果一个字符串中有多个分隔符,可以用 ’ | ’ 作为连字符。
6.5 字符串截取 - substring() 方法
从一个完整的字符串中截取出部分内容。
substring(int beginIndex):从指定索引截取到结尾。
substring(int beginIndex, int endIndex):截取部分内容。
public class Test{
public static void main(String[] args) {
String str = "HelloWorld!!!";
// [begin, end) 前闭后开
System.out.println(str.substring(5));
System.out.println(str.substring(5,10));
}
}
注意事项:
(1)索引从0开始;
(2)注意前闭后开区间的写法,substring(0, 5) 表示包含 0 号下标的字符,不包含 5 号下标。
6.6 其他操作方法
这些操作同样都是创建了新的字符串,没有修改原有字符串,因为字符串是不可变对象。
trim():去掉字符串开头和结尾的空白字符(空格、换行、回车、制表符、翻页符等)。
toUpperCase():字符串转大写。
toLowerCase():字符串转小写。
length():获取字符串长度。
注意: 数组长度使用 数组名称.length 属性,而 String 中使用的是 length() 方法。
intern():字符串入池操作。
concat():字符串连接,等同于 “+”,不入池。
isEmpty():判断是否为空字符串,但不是 null,而是长度为 0。
注意:String str1 = “”; 是空字符串;而 String str2 = null; 是空引用。
public class Test{
public static void main(String[] args) {
// 去掉字符串开头和结尾的空白字符
String str1 = " hello world ";
System.out.println("[" + str1 + "]");
System.out.println("[" + str1.trim() + "]");
System.out.println("");
// 大小写转换,只转换字母
String str2 = " HELLO%$$%@#$%world 哈哈哈";
System.out.println(str2.toUpperCase());
System.out.println(str2.toLowerCase());
System.out.println("");
// 求字符串长度
String str3 = " hello%$$%@#$%world 哈哈哈";
System.out.println("str3.length = " + str3.length());
System.out.println("");
// 观察 isEmpty() 方法
String str4 = "Hi";
String str5 = "";
String str6 = new String("Java");
System.out.println(str4.isEmpty());
System.out.println(str5.isEmpty());
System.out.println(str6.isEmpty());
System.out.println("hello".isEmpty());
System.out.println("".isEmpty());
System.out.println(new String().isEmpty());
}
}
7. StringBuffer 和 StringBuilder
首先来回顾下 String 类的特点:任何的字符串常量都是 String 对象,而且 String 的常量一旦声明不可改变,如果改变对象内容,改变的只是其引用的指向而已。
由于 String 的不可更改特性,为了方便字符串的修改,提供 StringBuffer 和 StringBuilder 类。
StringBuffer 和 StringBuilder 大部分功能是相同的。
在 String 中使用 “+” 来进行字符串连接,但是这个操作在 StringBuffer 类中需要更改为 append() 方法。
public class Test{
public static void main(String[] args) {
// 1、String 的 += 会产生新的 String 对象,在循环中使用是比较低效的
String str = "hello";
for (int i = 0; i < 100; i++) {
str += i;
}
System.out.println(str);
// 2、使用 StringBuffer
StringBuffer SB = new StringBuffer("hello");
for (int i = 0; i < 100; i++) {
SB.append(i);
}
System.out.println(SB);
}
}
常用方法
reverse():字符串翻转。
delete(int start, int end):删除指定范围的数据( [begin, end) 前闭后开)。
insert(int offset, 各种数据类型 b):插入数据。
public class Test{
public static void main(String[] args) {
// 使用 StringBuffer
StringBuffer stringBuffer1 = new StringBuffer("hello");
// reverse()
// 直接修改本身
stringBuffer1.reverse();
System.out.println(stringBuffer1.toString());
// delete()
StringBuffer stringBuffer2 = new StringBuffer("hello");
stringBuffer2.delete(2, 4);
System.out.println(stringBuffer2.toString());
// insert()
StringBuffer stringBuffer3 = new StringBuffer("hello");
stringBuffer3.insert(4, "World");
System.out.println(stringBuffer3.toString());
}
}
String、StringBuffer、StringBuilder 的区别
(1)String 的内容不可修改,StringBuffer 与 StringBuilder 的内容可以修改。
(2)StringBuffer 与 StringBuilder 大部分功能是相似的。
(3)StringBuffer 采用同步处理,属于线程安全操作;而 StringBuilder 未采用同步处理,属于线程不安全操作。