JavaSE-9-String

Author Avatar
ZJax 07月 04,2021
  • 在其它设备中阅读本文章

9.1 String 基础

  • String 类又称作不可变字符序列

    我们发现字符串内容全部存储到 value[] 数组中,而变量 value 是 final 类型的,也就是常量(即只能被赋值一次)。 这就是“不可变对象”的典型定义方式。

    我们发现在前面学习 String 的某些方法,比如:substring() 是对字符串的截取操作,但本质是读取原字符串内容生成了新的字符串。

  • String 位于 java.lang 包中,Java 程序默认导入 java.lang 包下的所有类。

  • Java 字符串就是 Unicode 字符序列,例如字符串 "Java" 就是4个 Unicode 字符 'J'、'a'、'v'、'a' 组成的。

  • Java 没有内置的字符串类型,而是在标准 Java 类库中提供了一个预定义的类 String,每个用双引号括起来的字符串都是 String 类的一个实例。

  • Java 允许使用符号"+"把两个字符串连接起来。

注意:当"+"运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。

String s1 = "Hello";
String s2 = "World! ";
String s = s1 + s2; // HelloWorld!

在遇到字符串常量之间的拼接时,编译器会做出优化,即在编译期间就会完成字符串的拼接。因此,在使用 == 进行 String 对象之间的比较时,我们需要特别注意。

public class TestString2 {
    public static void main(String[] args) {
        // 编译器做了优化,直接在编译的时候将字符串进行拼接
        String str1 = "hello" + " java"; // 相当于str1 = "hello java";
        String str2 = "hello java";
        System.out.println(str1 == str2); // true
        String str3 = "hello";
        String str4 = " java";
        // 编译的时候不知道变量中存储的是什么,所以没办法在编译的时候优化
        String str5 = str3 + str4;
        System.out.println(str2 == str5); // false
    }
}

9.2 String 类和常量池

在 Java 的内存分析中,我们会经常听到关于“常量池”的描述,实际上常量池也分了以下三种:

  1. 全局字符串常量池(String Pool)

    全局字符串常量池中存放的内容是在类加载完成后存到 String Pool 中的,在每个 JVM 中只有一份,存放的是字符串常量的引用值(在堆中生成字符串对象实例)。

  2. class 文件常量池(Class Constant Pool)

    class 常量池是在编译的时候每个 class 都有的,在编译阶段,存放的是常量(文本字符串、final 常量等)和符号引用。

  3. 运行时常量池(Runtime Constant Pool)

    运行时常量池是在类加载完成之后,将每个 class 常量池中的符号引用值转存到运行时常量池中,也就是说,每个 class 都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

String str3 = new String("bcd"); // str3 指向非String Pool的堆区
String str4 = str3.intern(); // str4 指向String Pool
String str5 = "bcd"; // str5 指向String Pool,且与str4的地址值相同
System.out.println(str3 == str4); // false
System.out.println(str4 == str5); // true
System.out.println(str3 == str5); // false

首先经过编译之后,在该类的 class 常量池中存放一些符号引用; 然后类加载之后,将 class 常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中 str1 所指向的“abc”实例对象); 然后将这个对象的引用(对象的地址)存到全局 String Pool中; 最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用(地址),也就是直接查询 String Pool,保证 String Pool 里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

现在就很容易解释整个程序的内存分配过程了。 首先,在堆中会有一个“abc”实例,全局 String Pool 中存放着“abc”的一个引用值,然后在运行第二句的时候会生成两个实例,一个是“def”的实例对象,并且 String Pool 中存储一个“def”的引用值,还有一个是 new 出来的一个“def”的实例对象,与上面那个是不同的实例。当在解析 str3 的时候查找 String Pool,里面有“abc”的全局驻留字符串引用,所以 str3 的引用地址与之前的那个已存在的相同,str4 是在运行的时候调用 intern() 函数,返回 String Pool 中“def”的引用值,如果没有就将 str2 的引用值添加进去,在这里,String Pool 中已经有了“def”的引用值了,所以返回上面在 new str2 的时候添加到 String Pool 中的 “def”引用值,最后str5在解析的时候也是指向存在于 String Pool 中的“def”的引用值,那么这样一分析之后,结果就容易理解了。

9.3 String 类常用的方法


  1. String 类的下述方法能创建并返回一个新的 String 对象:concat()、 replace()、substring()、 toLowerCase()、 toUpperCase()、trim()。
  2. 提供查找功能的有关方法:endsWith()、 startsWith()、 indexOf()、lastIndexOf()。
  3. 提供比较功能的方法:equals()、equalsIgnoreCase()、compareTo()。
  4. 其它方法:charAt() 、length()。

public class StringTest1 {
    public static void main(String[] args) {
        String s1 = "core Java";
        String s2 = "Core Java";
        System.out.println(s1.charAt(3)); // 提取下标为3的字符
        System.out.println(s2.length()); // 字符串的长度
        System.out.println(s1.equals(s2)); // 比较两个字符串是否相等
        System.out.println(s1.equalsIgnoreCase(s2)); // 比较两个字符串(忽略大小写)
        System.out.println(s1.indexOf("Java")); // 字符串s1中是否包含Java
        System.out.println(s1.indexOf("apple")); // 字符串s1中是否包含apple
        String s = s1.replace(' ', '&'); // 将s1中的空格替换成&
        System.out.println("result is :" + s);
    }
}
public class StringTest2 {
    public static void main(String[] args) {
        String s = "";
        String s1 = "How are you?";
        System.out.println(s1.startsWith("How")); // 是否以How开头
        System.out.println(s1.endsWith("you")); // 是否以you结尾
        s = s1.substring(4); // 提取子字符串:从下标为4的开始到字符串结尾为止
        System.out.println(s);
        s = s1.substring(4, 7); // 提取子字符串:下标[4, 7) 不包括7
        System.out.println(s);
        s = s1.toLowerCase(); // 转小写
        System.out.println(s);
        s = s1.toUpperCase(); // 转大写
        System.out.println(s);
        String s2 = "  How old are you!! ";
        s = s2.trim(); // 去除字符串首尾的空格。注意:中间的空格不能去除
        System.out.println(s);
        System.out.println(s2); // 因为String是不可变字符串,所以s2不变
    }
}

9.4 StringBuffer 和 StringBuilder

StringBuffer 和 StringBuilder 非常类似,均代表可变的字符序列。 这两个类都是抽象类 AbstractStringBuilder 的子类,方法几乎一模一样。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;
    ...
} 

显然,内部也是一个字符数组,但这个字符数组没有用 final 修饰,随时可以修改。因此,StringBuilder 和 StringBuffer 称之为“可变字符序列”。那两者有什么区别呢?

  1. StringBuffer JDK1.0 版本提供的类,线程安全,做线程同步检查, 效率较低。
  2. StringBuilder JDK1.5 版本提供的类,线程不安全,不做线程同步检查,因此效率较高。 建议采用该类。

常用方法列表:

方法解释
public StringBuilder append(…)可以为该 StringBuilder 对象添加字符序列,仍然返回自身对象。
public StringBuilder delete(int start,int end)可以删除从 start 开始到 end-1 为止的一段字符序列,仍然返回自身对象。
public StringBuilder deleteCharAt(int index)移除此序列指定位置上的 char,仍然返回自身对象。
public StringBuilder insert(…)可以为该 StringBuilder 对象在指定位置插入字符序列,仍然返回自身对象。
public StringBuilder reverse()用于将字符序列逆序,仍然返回自身对象。
public String toString()返回此序列中数据的字符串表示形式。
public int indexOf(String str)以下方法均与 String 中的方法类似
public int indexOf(String str,int fromIndex)
public String substring(int start)
public String substring(int start,int end)
public int length()
char charAt(int index)

9.5 不可变和可变字符序列使用陷阱

String 使用的陷阱

String 一经初始化后,就不会再改变其内容了。对 String 字符串的操作实际上是对其副本(原始拷贝)的操作,原来的字符串一点都没有改变。比如:

String s ="a";创建了一个字符串 s = s+"b";实际上原来的"a"字符串对象已经丢弃了,现在又产生了另一个字符串 s+"b"(也就是"ab")。

如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的时间和空间性能,甚至会造成服务器的崩溃。

相反,StringBuilder 和 StringBuffer 类是对原字符串本身操作的,可以对字符串进行修改而不产生副本拷贝或者产生少量的副本。因此可以在循环中使用。

String 和 StringBuilder 在频繁字符串修改时效率测试

/**
 * @author ZJax
 * @date 2021年05月15日 21:25
 */
public class TestStringBuilder {
    public static void main(String[] args) {
        /* 使用String进行字符串的拼接 */
        String str8 = "";
        // 本质上使用String拼接, 但是每次循环都会生成一个String对象
        long num1 = Runtime.getRuntime().freeMemory(); // 获取系统剩余内存空间
        long time1 = System.currentTimeMillis(); // 获取系统的当前时间
        for (int i = 0; i < 5000; i++) {
            str8 = str8 + i;  // 相当于产生了5001个对象
        }
        long num2 = Runtime.getRuntime().freeMemory();
        long time2 = System.currentTimeMillis();
        System.out.println("String占用内存 : " + (num1 - num2));
        System.out.println("String占用时间 : " + (time2 - time1));

        /* 使用StringBuilder进行字符串的拼接 */
        StringBuilder sb1 = new StringBuilder();
        long num3 = Runtime.getRuntime().freeMemory();
        long time3 = System.currentTimeMillis();
        for (int i = 0; i < 5000; i++) {
            sb1.append(i);
        }
        long num4 = Runtime.getRuntime().freeMemory();
        long time4 = System.currentTimeMillis();
        System.out.println("StringBuilder占用内存 : " + (num3 - num4));
        System.out.println("StringBuilder占用时间 : " + (time4 - time3));
    }
}

要点

  1. String:不可变字符序列。
  2. StringBuffer:可变字符序列,并且线程安全,但是效率低。
  3. StringBuilder:可变字符序列,线程不安全,但是效率高(一般用它)。