Java 中的String 字符串类(8千字超全详解)

String 类

1. String 类的基本概念

(1)String 类对象用于保存字符串,也就是一组字符序列。字符串常量是一组用双引号括起来的字符序列,如:“你好”,"hello"等。
(2)String 类对象与字符串常量的区别:String 类对象存储在堆内存中,而字符串常量存储在方法区的常量池中;存放在常量池中的字符串常量会有一个地址,通常该地址会返回给指向该字符串常量的String 类对象的 value 属性;但是注意:字符串常量本身就是一个存储在常量池中的String 类对象。
(3)字符串的字符使用Unicode 字符编码,一个字符(无论是字母还是汉字)都占两个字节。

  • 前3点后面会再深入解释,概念有点绕。

(4)String 类实现了接口 Serializable【说明了 String 类对象可以串行化:可以在网络传输】;String 类还实现了接口 Comparable [说明了String 对象可以比较大小]。

  • 如下图:
    在这里插入图片描述

(5)String 类是 final 类,不能被其他的类继承。
(6)String 类中有属性 private final char value[],这是一个 char 类型的常量数组,用于存放字符串中的每一个字符。
(7)注意:value 数组 是一个 final 类型, 不可以被修改,即数组 value 不能再指向新的数组地址,但是数组中单个字符内容是可以改变的。

  • 代码说明:

		final char[] value = {
    
    'a','b','c'};
        char[] v2 = {
    
    't','o','m'};
        value[0] = 'H';// 可以改变数组中的元素的值
        //value = v2; 错误,不可以修改 value地址
        

2. String 类对象的创建方式

(1)方式一:直接赋值 String s = “lineryue”;
(2)方式二:调用构造器 String s2 = new String(“lineryue”);

  • 两种创建方式的流程分析
  • 方式一:直接赋值 String s = “lineryue”;

(1)首先在方法区的常量池中查看是否存在一个存储了 “lineryue” 的空间;
(2)如果存在,就在栈空间中声明一个对象名 s ,然后将该空间的地址返回给 s ,s 最终指向的是常量池的空间地址(说明 s 是可变的,同时该字符串常量也是一个对象;
(3)如果不存在,就在常量池中开辟一个空间,将 “lineryue” 存储进去,然后在栈空间中声明一个对象名 s ,再将该空间的地址返回给 s ,s 最终指向的是常量池的空间地址(说明 s 是可变的),同时该字符串常量也是一个对象。

  • 方式二:调用构造器 String s2 = new String(“lineryue”);

(1)首先在堆内存中开辟了一个空间,这个空间是真正的String 类对象,该空间里面存在一个char 数组类型的 value 属性,value 指向了常量池;
(2)接着String 类对象在方法区的常量池中查看是否存在一个存储了 “lineryue” 的空间;
(3)如果存在,就将该空间的地址返回给 value ,value 最终指向的是常量池的空间地址;(注意:value 是 final 类型的)
(4)如果不存在,就在常量池中开辟一个空间,将 “lineryue” 存储进去,再将该空间的地址返回给 value ,value 最终指向的是常量池的空间地址;
(5)最后,在栈内存中声明一个对象名 s2 ,将堆内存中 String 类对象空间的地址返回给 s2 ,s2 最终指向的是堆空间中的String 类对象地址。

  • 创建String 类对象的内存分布图:

在这里插入图片描述

  • 小结:两种创建String 类对象的方式,虽然都是创建了字符串 “lineryue” ,但创建的途径是不同的,上面的图和流程分析是非常重要的,把它熟记在心。

3. String 类对象经典例题

(1)观察下面代码,输出结果是什么?


		String a = "abc";
        String b = "abc";
        System.out.println(a.equals(b));// T;equals() 是比较两个对象的字符串是否相同,"abc"和"abc" 是相同的。
        System.out.println(a == b);// T;“==” 是比较两个对象的地址是否相同,根据创建String类对象的流程,a 和 b都指向常量池中的同一个地址,所以 a == b。  

		String s1 = new String("bcde");
        String s2 = new String("bcde");
        System.out.println(s1.equals(s2));// T;equals() 是比较两个对象的字符串是否相同,"bcde"和"bcde" 是相同的。
        System.out.println(s1 == s2); // F;只要有new ,都是在堆内存中创建一个新对象,s1 和 s2 分别指向了两个对象空间,地址不同,所以错误。
      

(2)观察下面代码,输出结果是什么?


		String a = "hsp"; //a 指向 常量池中的 String类对象“hsp”
        String b = new String("hsp");//b 指向堆中的String类对象,再由该对象的 value 属性指向常量池中的“hsp”。
        System.out.println(a.equals(b)); // T;"hsp" 和 "hsp"相同
        System.out.println(a == b); // F;对象名a 和 b指向的地址不同

(3)观察下面代码,输出结果是什么?

        String a = "hsp"; 
        String b = new String("hsp");

   /* 当某个String 类对象调用intern() 方法时,
   		如果常量池中已经存在一个等于此String对象的字符串(用equals 方法确定是否等于),
   		则该方法返回常量池中该字符串的地址;
   		否则,将String对象的字符串 添加到常量池中,该方法返回存储字符串的地址;*/
   		
        System.out.println(a == b.intern()); // T;b.intern() 返回了常量池中的存储"hsp"的地址,因此a 指向的地址和 b.intern() 返回的地址相同。
        System.out.println(b == b.intern()); // F;b 对象名指向的是堆内存中的对象地址,和 b.intern() 返回的不同。

(4)观察下面代码,输出结果是什么?

public class StringExercise04 {
    
    
    public static void main(String[] args) {
    
    
        Person p1 = new Person();
        p1.name = "hspedu";
        Person p2 = new Person();
        p2.name = "hspedu";

        System.out.println(p1.name.equals(p2.name)); // T;两者都是"hspedu",正确
        System.out.println(p1.name == p2.name); // T;两者都指向了常量池中的"hspedu",地址相同,正确
        System.out.println(p1.name == "hspedu"); // T;p1.name指向了常量池中的"hspedu"的地址,"hspedu"本身就是该地址,两者相同,正确
    }
}

class Person {
    
    
    public String name;
}
  • 小结:在字符串对象的比较时,注意使用(==)还是 equals() 方法,前者比较的是对象的地址,后者比较的是字符串的内容。需要牢牢把握创建String类对象的两种方式的流程,这是本质,多绕的题都会迎刃而解。

4. String 类对象的特性

(1)String 类是一个 final 类,代表不可变的字符序列。
(2)String类对象是不可变的,指的是一个String 类对象一但被创建,该对象的内容就不可以再改变(例如:属性 value );但对象的引用(对象名)可以改变,也就是说对象名可以再指向另外的对象。

  • 思考1,对于String类对象不可变的理解:
    • 在 String 类的源码中,主要的成员变量只有两个:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
  • 分析

    • 可以知道,String 类就是对char 数组 value 的封装,这里先不用管 hash 属性;String类使用 private final 修饰 value,且没有提供修改value的 setValue 方法,这代表了 一但String 类对象被创建,该对象就不可以再更改其 value 的值了。 所以,我们可以认为 String 类对象是不可变的。(使用反射时有例外,这个在反射的章节再讲解)
  • 思考2,对于String 类对象的引用(对象名)可以改变的理解:

  • 看下面代码,输出结果是什么?
		
		String s = "abc";
		System.out.println("s=" + s);// 输出:s=abc
		
		s = "123";
		System.out.println("s=" + s);// 输出:s=123
  • 分析:
    • 上面代码中,执行语句 String s = “abc”,在常量池中创建了一个String类 对象"abc",并将该对象的地址返回给了对象名 s ;再执行语句 s = “123”,并不是将刚才的"abc" 修改为"123",而是在常量池中又创建了一个 String 类对象 “123”,并将新对象的地址返回给了 s ;
    • 所以,改变的是对象的引用 s ,而"abc" 这个 String 类的对象还存在于常量池中,只不过是没有对象名引用它了。

5. String 类对象经典面试题

(1) 看下面代码,创建了几个对象?


	String s = "hello" + "abc";
  • 答案: 只创建了一个对象;编译器会自动优化代码,上面代码在优化后等价于 String s = “helloabc”;因此只是在常量池中创建了一个 String 类对象。

(2) 看下面代码,创建了几个对象?


	String a = "hello"; 
    String b = "abc";// a 和 b 都指向常量池中的对象
    String c = a + b;// c 指向堆内存中的对象
  • 答案:
    • 创建了三个对象;先在常量池中创建了 “hello” 和 “abc” 这两个对象,然后 String c = a + b 这一语句,底层先创建了一个 StringBuilder 类对象,然后使用该对象的 append 方法,将 “hello” 和 “abc” 拼接在一起,在常量池中创建了 “helloabc”,但最终返回了一个堆中的 String 类对象,该对象的 value 指向常量池的 “helloabc”,而 c 指向了堆内存中的对象。

(3) 看下面代码,创建了几个对象?输出结果是什么?


        String s1 = "hspedu";  //s1 指向池中的 “hspedu”
        String s2 = "java"; // s2 指向池中的 “java”
        String s3 = s1 + s2;// s3 指向堆中的对象,该对象再指向池中的“hspedujava”
        String s5 = "hspedujava"; // s5 指向池中的 “hspedujava”
        String s6 = s3.intern();// s6 指向池中的 “hspedujava”
        
        System.out.println(s3 == s5); // F
        System.out.println(s5 == s6); // T
        System.out.println(s5.equals(s6));// T

(4) 看下面代码,输出结果是什么?


class Test1 {
    
    
    String str = new String("hsp");
    final char[] ch = {
    
    'j', 'a', 'v', 'a'};

    public void change(String str, char[] ch) {
    
    
        str = "java";
        ch[0] = 'h';
    }

    public static void main(String[] args) {
    
    
        Test1 ex = new Test1();
        ex.change(ex.str, ex.ch);
        System.out.print(ex.str + " and ");
        System.out.println(ex.ch);// 可以直接打印出char数组中的元素
    }
}
  • 答案: hsp and hava;
    • 本题中是引用传递,即把对象的引用复制了一份,复制品也指向了对象,两个引用不会相互影响,但是他们共同指向了同一个对象空间。在 change 方法栈中,执行了 str = “java”; 即复制的引用指向了新的常量池中的字符串 “java”;change 方法栈退出后,main 方法中原有的 str 引用还是指向原来的 “hsp” 对象,因此 ex.str 输出的是 hsp 。
    • 而同样是引用传递,在 change 方法栈中 数组 ch 内的元素的值被改变了,则 main 方法栈中 ex.ch 输出的结果也会被影响。本题画内存图分析会思路会清晰,如果博主的解释不够清楚,可以在评论区提出问题。

6. String 类中的常用方法

  • 代码实现:
public class StringMethod01 {
    
    
    public static void main(String[] args) {
    
    

        //1. equals :比较内容是否相同,区分大小写
        String str1 = "hello";
        String str2 = "Hello";
        System.out.println(str1.equals(str2));

        // 2.equalsIgnoreCase :忽略大小写的判断内容是否相等
        String username = "johN";
        if ("john".equalsIgnoreCase(username)) {
    
    
            System.out.println("Success!");
        } else {
    
    
            System.out.println("Failure!");
        }

        // 3.length :获取字符的个数,字符串的长度
        System.out.println("韩顺平".length());

        // 4.indexOf :获取字符在字符串对象中第一次出现的索引,索引从0开始,如果找不到,返回-1
        String s1 = "wer@terwe@g";
        int index = s1.indexOf('@');
        System.out.println(index);// 3
        System.out.println("weIndex=" + s1.indexOf("we"));// 0

        // 5.lastIndexOf :获取字符在字符串中最后一次出现的索引,索引从0开始,如果找不到,返回-1
        s1 = "wer@terwe@g@";
        index = s1.lastIndexOf('@');
        System.out.println(index);//11
        System.out.println("ter的位置=" + s1.lastIndexOf("ter"));// 4

        // 6.substring :截取指定范围的子串
        String name = "hello,张三";

        // 下面name.substring(6) 从索引6开始截取后面所有的内容
        System.out.println(name.substring(6));// 截取后面的字符

        // name.substring(0,5)表示从索引0开始截取,截取到索引 5-1 = 4 位置
        System.out.println(name.substring(2,5));// llo

    }
}
  • 代码实现:
public class StringMethod02 {
    
    
    public static void main(String[] args) {
    
    

        // 1.toUpperCase:转换成大写
        String s = "heLLo";
        System.out.println(s.toUpperCase());// HELLO

        // 2.toLowerCase :转换成小写
        System.out.println(s.toLowerCase());// hello

        // 3.concat :拼接字符串
        String s1 = "宝玉";
        s1 = s1.concat("林黛玉").concat("薛宝钗").concat("together");
        System.out.println(s1);// 宝玉林黛玉薛宝钗together

        // 4.replace 替换字符串中的字符
        s1 = "宝玉 and 林黛玉 林黛玉 林黛玉";
        // 在s1中,将 所有的 林黛玉 替换成薛宝钗
        // 解读: s1.replace() 方法执行后,返回的结果才是替换过的.
        // 注意对 s1没有任何影响
        String s11 = s1.replace("宝玉", "jack");
        System.out.println(s1);//宝玉 and 林黛玉 林黛玉 林黛玉
        System.out.println(s11);//jack and 林黛玉 林黛玉 林黛玉

        // 5.split: 分割字符串, 对于某些分割字符,我们需要 转义比如 | \\等
        String poem = "锄禾日当午,汗滴禾下土,谁知盘中餐,粒粒皆辛苦";
        //解读:
        // 1. 以 , 为标准对 poem 进行分割 , 返回一个数组
        // 2. 在对字符串进行分割时,如果有特殊字符,需要加入转义符 \
        String[] split = poem.split(",");
        poem = "E:\\aaa\\bbb";
        split = poem.split("\\\\");
        System.out.println("==分割后内容===");
        for (int i = 0; i < split.length; i++) {
    
    
            System.out.println(split[i]);
        }

        // 6.toCharArray :转换成字符数组
        s = "happy";
        char[] chs = s.toCharArray();
        for (int i = 0; i < chs.length; i++) {
    
    
            System.out.println(chs[i]);
        }

        // 7.compareTo :比较两个字符串的大小,如果前者大,
        // 则返回正数,后者大,则返回负数,如果相等,返回0
        // 解读:
        // (1) 如果长度相同,并且每个字符也相同,就返回 0
        // (2) 如果长度相同或者不相同,但是在进行比较时,可以区分大小
        //     就返回 if (c1 != c2) {
    
    
        //                return c1 - c2;
        //            }
        // (3) 如果前面的部分都相同,就返回 str1.len - str2.len
        String a = "jcck";// len = 3
        String b = "jack";// len = 4
        System.out.println(a.compareTo(b)); // 返回值是 'c' - 'a' = 2的值

        // 8.format : 格式字符串
        // 占位符有: %s 字符串 %c 字符 %d 整型 %.2f 浮点型

        String name = "john";
        int age = 10;
        double score = 56.857;
        char gender = '男';

        // 将所有的信息都拼接在一个字符串.
        String info =
                "我的姓名是" + name + "年龄是" + age + ",成绩是" + score + "性别是" + gender + "。希望大家喜欢我!";

        System.out.println(info);// 很麻烦,使用占位符


        // 解读
        // 1. %s , %d , %.2f %c 称为占位符
        // 2. 这些占位符由后面变量来替换
        // 3. %s :表示后面由 字符串来替换
        // 4. %d :是整数来替换
        // 5. %.2f :表示使用小数来替换,替换后,只会保留小数点两位, 并且进行四舍五入的处理
        // 6. %c :使用char 类型来替换
        String formatStr = "我的姓名是%s 年龄是%d,成绩是%.2f 性别是%c.希望大家喜欢我!";

        String info2 = String.format(formatStr, name, age, score, gender);

        System.out.println("info2=" + info2);
    }
}

总结

  • 本文详细总结讲解了 String 类(字符串) 的知识,并深入解释了 String类 的注意事项和细节,举了很多很多的例子来讲解。String 类(字符串)是Java 的重难点,理解并掌握这一部分的知识,需要有较好的面向对象的基础。小白博主已经尽力整理了,希望小伙伴们看后能有所收获!
  • 最后,如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!我们下一篇博文见吧!

猜你喜欢

转载自blog.csdn.net/weixin_45395059/article/details/125760234