课程重点:
- 字符串概念
- 字符串的内存理解
- 字符串的常用方法
- 可变字符串的常用方法
13.1. 字符串的简介
13.1.1. 字符串的概念
字符串, 其实是由若干个字符组成的一个有序序列。 用String来表示一个字符串。
字符串中的内容, 用双引号括起来。 在双引号中, 字符的数量不限制, 可以是0个, 可以是1个, 也可以是多个。
String str1 = "hello world";
13.1.2. 字符串的内存分析
字符串, 是一个引用数据类型。 但是字符串的引用, 和之前在面向对象部分的引用有一点差别。
差别: 类的对象, 是在堆上开辟的空间。 字符串, 是在 常量池 中开辟的空间。 (常量池, 是在方法区中的一个子空间)
- String str = "hello world";
- 此时, "hello world", 是在 常量池 中开辟的空间。 str里面存储的, 其实是常量池中的某一个内存的地址。
- 当 str = "30"; 的时候, 其实, 并不是修改了 str指向的空间中的内容。 因为常量池空间特性, 一个空间一旦开辟完成了, 里面的值是不允许修改的。 此时, 是在常量池中开辟了一块新的空间, 存储了 "30", 并把这个新的空间的地址给str赋值了。
- 字符串类型, 之所以选择在常量池中进行空间的开辟, 而不是在堆上。 原因是需要使用 享元原则 。
// 当第一次使用到"hello world"这个字符串的时候, 常量池中并没有这块内存。 // 此时, 就需要去开辟一块新的空间, 存储为 "hello world", 并且把空间的地址赋值给了str1。String str1 = "hello world";// 当再次使用到 "hello world" 这个字符串的时候, 常量池中现在是有这块空间的。// 此时, 就不需要在开辟新的空间了, 直接将现有的这个空间地址赋值给 str2。String str2 = "hello world";// 即: str1 和 str2 现在都指向 "hello world"System.out.println(str1 == str2);
- String str = new String("hello world");
- String是一个Java中用来描述字符串的类, 里面是有构造方法的。
- 通过String类提供的构造方法, 实例化的字符串对象, 在堆上开辟的空间。 在堆空间中, 有一个内部维护的属性, 指向了常量池中的某一块空间。
// 在堆上开辟了一个String对象的空间, 把这个堆上空间的地址给了str1// 在堆空间中, 有一个内部的属性, 指向了常量池中的 "hello world"String str1 = new String("hello world");// 在堆上开辟了一个String对象的空间, 把这个堆上空间的地址给了str2// 在堆空间中, 有一个内部的属性, 指向了常量池中的 "hello world"String str2 = new String("hello world");System.out.println(str1 == str2);// false: 因为此时 str1和str2 里面存储的是两块堆空间的地址。System.out.println(str1.equals(str2)); // true: 因为在String类中,已经重写过equals方法了, 重写的实现为比较实际指向的常量池中的字符串。
13.1.3 字符串拼接的内存分析
- 直接使用两个字符串字面量进行拼接
- 其实, 就是直接将两个由双引号直接括起来的字符串进行拼接 。 类似于 String str = "hello" + "world"; 。
- 这里, 直接在常量池中进行空间操作。 将常量池中拼接之后的结果, 地址给 str 进行赋值。
- 使用一个字符串变量和其他的进行拼接
- 这里的拼接, 不是在常量池中直接完成的。
- 在这个拼接的过程中, 隐式的实例化了一个String类的对象, 在堆上开辟了空间。 堆上空间内部维护了一个指向了常量池中拼接结果的一个属性。 这个堆上的空间地址给左侧的引用进行了赋值。
13.2. 字符串的常用方法
13.2.1. 字符串的构造方法
13.2.1.1. 字符串构造方法列举
13.2.1.2. 示例代码
/*** @Description 字符串的构造方法*/public class StringMethod1 { public static void main(String[] args) { // 1. 无参构造, 实例化一个空的字符串对象。 所谓的空字符串,其实是 "", 并不是null。 String s1 = new String(); // String s1 = ""; System.out.println(s1); // 2. 通过一个字符串, 实例化另外一个字符串。 String s2 = new String("hello"); System.out.println(s2); char[] arr1 = { 'h', 'e', 'l', 'l', 'o' }; // 3. 通过一个字符数组, 实例化一个字符串。将字符数组中的所有的字符拼接到一起。 String s3 = new String(arr1); System.out.println(s3); // 4. 通过一个字符数组, 实例化一个字符串。 将字符数组中的指定范围的字符拼接到一起。 String s4 = new String(arr1, 2, 3); System.out.println(s4); byte[] arr2 = { 97, 98, 99, 100, 101, 102, 103, 104 }; // 5. 将一个字节数组中的每一个字节,转成对应的字符,再拼接到一起,组成一个字符串。 String s5 = new String(arr2); System.out.println(s5); // 6. 将一个字节数组中的offset位开始,取length个字节,将每一个字节,转成对应的字符,再拼接到一起,组成一个字符串。 String s6 = new String(arr2, 2, 4); System.out.println(s6); }}
13.2.2. 字符串的非静态方法
13.2.2.1. 前言
因为字符串, 是常量。 任何的修改字符串的操作, 都不会对所修改的字符串造成任何的影响。 所有的对字符串的修改操作, 其实都是实例化了新的字符串对象。 在这个新的字符串中, 存储了修改之后的结果。 并将这个新的字符串以返回值的形式返回。 所以, 如果需要得到对一个字符串修改之后的结果, 需要接收方法的返回值。
13.2.2.2. 常用的非静态方法
13.2.2.3. 示例代码
import java.util.Arrays;/** * @Description 字符串常用的非静态方法 */public class StringMethod2 { public static void main(String[] args) { // 1. 字符串的拼接,效率比加号拼接高 String ret1 = "hello".concat("world"); System.out.println(ret1); // helloworld // 2. 字符串截取 String ret2 = "hello world".substring(3); System.out.println(ret2); // lo world String ret3 = "hello world".substring(3, 8); System.out.println(ret3); // lo wo // *. 字符序列截取 CharSequence charSequence = "hello world".subSequence(3, 8); System.out.println(charSequence); // 3. 用新的字符替换原字符串中所有的旧的字符 String ret4 = "hello world".replace('l', 'L'); System.out.println(ret4); // heLLo worLd // 4. 用新的字符序列替换原字符串中所有的旧的字符序列 String ret5 = "hello world".replace("ll", "~"); System.out.println(ret5); // he~o world // 5. 转成字符数组 char[] ret6 = "hello world".toCharArray(); System.out.println(Arrays.toString(ret6)); // [h, e, l, l, o, , w, o, r, l, d] // 6. 转成字节数组 byte[] ret7 = "hello world".getBytes(); System.out.println(Arrays.toString(ret7)); // [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] // 7. 获取某一个字符在一个字符串中第一次出现的下标。 int ret8 = "hello world".indexOf('L'); System.out.println(ret8); // 2 // 8. 获取某一个字符在一个字符串中从fromIndex位开始往后,第一次出现的下标。 int ret9 = "hello world".indexOf('l', 4); System.out.println(ret9); // 9 // 9. 获取某一个字符在一个字符串中最后一次出现的下标。 int ret10 = "hello world".lastIndexOf('o'); System.out.println(ret10); // 7 // 10. 获取某一个字符在一个字符串中,从fromIndex位开始往前,最后一次出现的下标 int ret11 = "hello world".lastIndexOf('o', 5); System.out.println(ret11); // 4 // 11. 字符串大小写字母转变 System.out.println("hello WORLD".toUpperCase()); System.out.println("hello WORLD".toLowerCase()); // 12. 判断一个字符串中, 是否包含另外一个字符串。 System.out.println("hello world".contains("loo")); // 需求:判断一个字符串中是否包含某一个字符 // 答案:或者这个字符在字符串中出现的下标,如果不是-1,说明包含。 // 13. 判断一个字符串, 是否是以指定的字符串作为开头。 System.out.println("哈利波特与魔法石.mp4".startsWith("哈利波特")); System.out.println("哈利波特与魔法石.mp4".endsWith(".mp4")); // 14. 去除一个字符串首尾的空格 System.out.println(" hello world ".trim()); // 15. 判断两个字符串的内容是否相同 System.out.println("hello world".equals("HELLO WORLD")); System.out.println("hello world".equalsIgnoreCase("HELLO WORLD")); // true // 16. 比较两个字符串的大小 // > 0: 前面的字符串 > 参数字符串 // == 0: 两个字符串大小相等 // < 0: 前面的字符串 < 参数字符串 // 比较逻辑: // 依次比较每一位的字符的大小。 // 如果某一次的字符比较可以分出大小,本次的比较结果将作为整体的字符串的比较结果。 int result = "hello world".compareTo("hh"); System.out.println(result); }}
13.2.3. 字符串的静态方法
13.2.3.1. 常用的静态方法
13.2.3.2. 示例代码
/** * @Description */public class StringMethod3 { public static void main(String[] args) { // 将若干个字符串拼接到一起,在拼接的时候,元素与元素之间以指定的分隔符进行分隔 String str1 = String.join(", ", "lily", "lucy", "uncle wang", "polly"); System.out.println(str1); float score = 100; String name ="xiaoming"; int age = 19; // 大家好,我叫xiaoming,今年19岁了,本次考试考了100分。 /** * 常见占位符: * %s : 替代字符串 -> %ns: 凑够n位字符串,如果不够,补空格 * %d : 整型数字占位符 -> %nd: 凑够n位,如果不够补空格。 * %f : 浮点型数字占位符 -> %.nf: 保留小数点后面指定位的数字 * %c : 字符型占位符 */ String str2 = String.format("大家好,我叫%11s,今年%03d岁了,本次考试考了%.6f分。", name, age, score); System.out.println(str2); }}
13.3. StringBuffer和StringBuilder类
13.3.1. 概念
都是用来操作字符串的类。
字符串都是常量, 所有的操作字符串的方法, 都不能直接修改字符串本身。 如果我们需要得到修改之后的结果, 需要接收返回值。
StringBuffer和StringBuilder不是字符串类, 是用来操作字符串的类。 在类中维护了一个字符串的属性, 这些字符串操作类中的方法, 可以直接修改这个属性的值。 对于使用方来说, 可以不去通过返回值获取操作的结果。
在StringBuffer或者StringBuilder类中, 维护了一个字符数组。 这些类中所有的操作方法, 都是对这个字符数组进行的操作。
13.3.2. 常用方法
13.3.3. 示例代码
/** * @Description */public class Test { public static void main(String[] args) { // 1. 构造方法 StringBuilder sb = new StringBuilder("hello world"); // 2. 增: 在一个字符串后面拼接其他的字符串 sb.append('!'); // 3. 增: 在指定的下标位插入一条数据 sb.insert(3, "AAAAA"); // 4. 删: 删除字符串中的 [start, end) 范围内的数据 sb.delete(3, 5); // 5. 删: 删除指定位的字符 sb.deleteCharAt(6); // 6. 截取一部分的字符串,这个操作不会修改到自己,如果希望得到截取的部分,需要接收返回值。 String sub = sb.substring(4, 6); // 7. 替换,将字符串中 [start, end) 范围内的数据替换成指定的字符串 sb.replace(3, 6, "l"); // 8. 修改指定下标位的字符 sb.setCharAt(0, 'H'); // 9. 将字符串前后翻转 sb.reverse(); System.out.println(sb); }}
13.3.4. 区别
StringBuffer和StringBuilder从功能上来讲, 是一模一样的。 但是他们两者还是有区别的:
- StringBuffer是线程安全的。
- StringBuilder是线程不安全的。
使用场景:
- 当处于多线程的环境中, 多个线程同时操作这个对象, 此时使用StringBuffer。
- 当没有处于多线程环境中, 只有一个线程来操作这个对象, 此时使用StringBuilder。
13.3.5. 备注
但凡是涉及到字符串操作的使用场景, 特别是在循环中对字符串进行的操作。 一定不要使用字符串的方法, 用StringBuffer或者StringBuilder的方法来做。
由于字符串本身是不可变的, 所以String类所有的修改操作, 其实都是在方法内实例化了一个新的字符串对象, 存储拼接之后的新的字符串的地址, 返回这个新的字符串。 如果操作比较频繁, 就意味着有大量的临时字符串被实例化、被销毁, 效率极低。 StringBuffer、StringBuilder不同, 在内部维护了一个字符数组, 所有的操作都是围绕这个字符数组进行的操作。 当需要转成字符串的时候, 才会调用 toString() 方法进行转换。 当频繁用到字符串操作的时候, 没有中间的临时的字符串出现, 效率较高。
/** * @Description String、StringBuffer、StringBuilder 拼接效率比较 */public class Test2 { public static void main(String[] args) { // 需求: 进行字符串的拼接 100000 次。 String str = ""; StringBuffer buffer = new StringBuffer(); StringBuilder builder = new StringBuilder(); int times = 100000; // String类的拼接 long time0 = System.currentTimeMillis(); for (int i = 0; i < times; i++) { str += i; } long time1 = System.currentTimeMillis(); // StringBuffer的拼接 for (int i = 0; i < times; i++) { buffer.append(i); } long time2 = System.currentTimeMillis(); for (int i = 0; i < times; i++) { builder.append(i); } long time3 = System.currentTimeMillis(); System.out.println("String: " + (time1 - time0)); System.out.println("StringBuffer: " + (time2 - time1)); System.out.println("StringBuilder: " + (time3 - time2)); }}