一、String
✏️ Java 中用 java.lang.String
类代表字符串
✏️ 底层用char[]
存储字符数据。从 Java9 开始,底层使用 byte[]
存储字符数据
public class TestDemo { public static void main(String[] args) { String bigBrother = "林哥"; // char[] value = {'林', '哥'}; } }
🌿 字符串的底层是
char[]
,但是 char 数组和字符串不能等价。char 数组是 char 数组,字符串是字符串。🌿 C 语言中是可以把 char 数组和字符串等价的
✏️ 所有的 字符串字面量(如:"林哥"
)都是 String 类的实例
✏️ String 对象创建完毕后,String 对象的字符内容是不可以修改的
🌿 String 对象的引用变量的指向是可以修改的
public class TestDemo { public static void main(String[] args) { String s = "林哥"; s += "love"; s = "666"; test(s); // s = 666 System.out.println("s = " + s); } private static void test(String str) { // str 是局部变量, 生命周期只在 test 方法中 str += 123; } }
二、字符串常量池
✏️ 字符串常量池:String Constant Pool
✏️ 从 Java7 开始,字符串常量池是在堆空间(之前是在方法区)
🌿 当遇到字符串字面量的时候,会先在 SCP 中检索
🌿 看 SCP 中是否存在与字面量内容一样的字符串对象 A ❓
🌿 如果有,返回字符串对象 A
🌿 否则,创建一个新的字符串对象 B,并将其加入到 SCP 中,然后返回 B
public class TestDemo { public static void main(String[] args) { String s1 = "林哥"; String s2 = "林哥"; // true System.out.println(s1 == s2); } }
三、字符串的初始化
public class TestDemo { public static void main(String[] args) { String s1 = "林哥"; String s2 = new String("林哥"); // 字符串对象 s3 的值和字符串对象 s1 的值是一样的 // 底层指向的都是同一个 char 数组 String s3 = new String(s1); String s4 = new String(s2); char[] cs = {'林', '哥'}; String s5 = new String(cs); String s6 = new String(s5); } }
String s1 = "林哥"; String s2 = new String("林哥");
✏️ 上面的两行代码中,字符串对象 s1 底层的 char 数组 value 和字符串对象 s2 底层的 char 数组是同一个
✏️ 如下图所示:String 的构造方法会把传入的字符串对象的 value 直接赋值给当前对象(当前创建的新字符串对象)的 value
可通过 debug 的方式验证...
✏️ 上图:字符串对象 s1 底层的 value 的 id 号是 541
✏️ 字符串对象 s2 底层的 value 的 id 号也是 541
—
✏️ 给 String 对象的构造方法传入字符数组的时候,底层会拷贝一份该字符数组,然后才赋值给当前创建的 String 对象的 char 数组 value
四、String 类的 intern 方法
先看下面的代码,思考打印结果:
public class TestDemo { public static void main(String[] args) { String s0 = "林哥"; String s1 = s0.intern(); // true System.out.println(s1 == s0); String s2 = new String(s0); // false System.out.println(s2 == s0); String s3 = s2.intern(); System.out.println(s3 == s0); // true System.out.println(s3 == s2); // false } }
✏️ 当字符串的
intern()
方法被调用的时候,如果字符串常量池中已经包含一个字符串对象的内容等于调用intern()
的字符串对象的内容,则返回字符串常量池中的字符串对象。否则,把调用intern()
的字符串对象添加到字符串常量池中,并返回该字符串对象(调用intern()
的字符串对象)
String s3 = s2.intern();
✏️ s2 调用了
intern()
方法,如果字符串常量池中存在与 s2 内容一样的字符串对象 s 的话,返回字符串常量池中的 s 对象;否则,将 s2 添加到字符串常量池中,并返回 s2
只有遇到字符串字面量的时候,才会在字符串常量池中检索
public class TestDemo { public static void main(String[] args) { int a = 1, b = 2, c = 3; String s1 = String.format("%d_%d_%d", a, b, c); String s2 = String.format("%d_%d_%d", a, b, c); System.out.println(s1 == s2); // output: false /* 只有【字符串字面量】才会在字符串常量池中找检索 */ } }
public class TestDemo { public static void main(String[] args) { int a = 1, b = 2, c = 3; String s1 = String.format("%d_%d_%d", a, b, c); String s2 = String.format("%d_%d_%d", a, b, c); System.out.println(s1 == s2); // output: false /* 会把 s1 放入字符串常量池,并返回 s1 指向对象 s3 和 s1 指向的是同一个对象 */ String s3 = s1.intern(); /* 会把被 s1 指向的字符串常量池中的字符串对象返回 s1 和 s4 指向的是同一个对象 */ String s4 = s2.intern(); /* 返回字符串常量池中的字符串对象, 若 SCP 中没有, 创建一个放入 SCP, 并返回 */ String s5 = "1_2_3"; System.out.println(s1 == s3); // true System.out.println(s1 == s4); // true System.out.println(s1 == s5); // true } }
五、字符串常用方法
public class TestDemo { public static void main(String[] args) { // trim: 去除左右的空格 String s1 = " 111 2222 ".trim(); String s = "hAve A niCe Day"; // toUpperCase: 把小写字母转为大写字母 String s2 = s.toUpperCase(); // toLowerCase: 把大写字母转换为小写字母 String s3 = s.toLowerCase(); // contains: 是否包含某个子字符串片段 boolean contains = "Have a nice Day".contains("nice"); // startsWith: 是否以某个子字符串片段开头 boolean startsWith = "Love is love.".startsWith("love"); // endsWith: 是否以某个子字符串片段结尾 boolean endsWith = "Love is love".endsWith("love"); // 将字符串分隔为字符串数组 String[] split = "Today_we_have_a_new_student".split("_"); // output: [Today, we, have, a, new, student] System.out.println(Arrays.toString(split)); } }
字符串的截取:
public class TestDemo { public static void main(String[] args) { String nice = "goodMorning"; // 从下标 4 位置开始截取, 截取到最后 String s1 = nice.substring(4); // output: s1 = Morning System.out.println("s1 = " + s1); String s = "believe"; // 从下标 2 位置开始截取, 截取到下标为 5 的位置(不包括下标5位置的字符) // 左闭右开 String s2 = s.substring(2, 5); // s2 = lie System.out.println("s2 = " + s2); } }
indexOf: 定位字符串所在索引
public class TestDemo { public static void main(String[] args) { int result1 = "hello".indexOf("e"); int result2 = "hello".indexOf("a"); System.out.println(result1); // 1 System.out.println(result2); // -1 } }
🌿 如果字符串中不包含该子字符串,返回 -1
lastIndexOf: 定位字符串所在索引
public class TestDemo { public static void main(String[] args) { // 从左往右开始定位字符串索引 int result1 = "hello log".indexOf("lo"); // 从右往左开始定位字符串索引(但数的时候还是从左往右数) int result2 = "hello log".lastIndexOf("lo"); System.out.println(result1); // 3 System.out.println(result2); // 6 } }
字符串替换:
public class TestDemo { public static void main(String[] args) { String s1 = "I live you"; String s2 = s1.replace("live", "like"); // s1 = I live you System.out.println("s1 = " + s1); // s2 = I like you System.out.println("s2 = " + s2); } }
六、StringBuilder 和 StringBuffer
✒️ 需要进行大量的字符串改动操作(如拼接、替换)的时候,使用 String 会非常消耗内存、会降低程序性能
✒️StringBuilder 或 StringBuffer 进行大量字符串拼接或替换的时候,程序性能和内存的消耗特别小
public class TestDemo { public static void main(String[] args) { String s = "1"; s += "2"; s += "3"; s += "4"; // s = 1234 System.out.println("s = " + s); } }
🌿 上面代码,每一次的拼接操作都会产生一个新的字符串对象
🌿 由于字符串内容的不可变性,每次的拼接操作必定都会产生一个新的字符串对象(如上图),会存在大量的字符串对象的创建和销毁操作
public class TestDemo { public static void main(String[] args) { StringBuilder sb = new StringBuilder("1"); sb.append("2"); sb.append("3"); sb.append("4"); // 1234 System.out.println(sb.toString()); // 链式调用 StringBuffer sBuffer = new StringBuffer("1") .append("2") .append("3") .append("4"); // 1234 System.out.println(sBuffer); } }
🌿 使用 StringBuilder 和 StringBuffer 则会非常高效
🌿 StringBuilder 和 StringBuffer 的使用方式(API)都一样
🌿 区别:StringBuilder 线程不安全;StringBuffer 线程安全
✏️ 常用方法:append、insert、delete、replace、reverse
✏️ StringBuider 或 StringBuffer 与 String 并不存在继承关系
✏️ StringBuilder、StringBuffer、String 都实现了 CharSequence 接口
✏️ StringBuilder 的 append 原理:动态数组
动态数组的实现博主后期在学习,后期再写文章
结束,如有错误请不吝赐教