Java 学习指南 - String

241 阅读7分钟

何为指南?

已经有了这么多技术文章、教程、经典书籍,而我又没有独到的见解,我就述而不作了。本文主要是用我看过的材料,按照一定的知识脉络组织成一篇学习指南,一来方便自己回顾,二来方便大家学习。下面解释一下为什么分成“先知”、“源里”、“API 地图”三个部分。

  1. 先知:在开始实践之前,我们首先要知道这个知识是干嘛的、有什么注意事项。
  2. 源里:隐藏在 API 里面的是源码实现,揭开 API 这层面纱,你就能知其所以然,少犯错,想到更多的技术方案。但是有时候我们没有时间没有动力去看源码,有时候看了也看不出个所以然,不知道看了有什么用。没有时间可以让人划重点,没有动力可以由一个面试题来触发,看不出所以然是因为我们经验不足眼界太窄,可以由一些真实案例来开扩。
  3. API 地图:在日常实践中,我们需要搜寻 API 的使用案例来解决我们的需求。最方便最实用的当然是直接在搜索引擎搜索,但先有一份简单而全面的 API 地图会有利于我们的搜索。

先知

String 的作用

在 Java 中用 char 来表示单个字符信息。但是在现实世界中,信息一般不是以单个字符出现的,而是连续的、长短不一的,例如一句聊天记录,一段微博,一篇公众号文章等等。String(字符串) 可以让我们更方便地表示和处理连续的字符。

比较 String 需要注意什么?

比较 String 就是如何判断 String 是相等的。在 Java 中,比较方式有两种。

  1. 用 == 操作符比较。== 比较的是变量所指向的地址。
  2. 用 equals 函数比较。equals 比较的变量所指向的对象的内容。
String str1 = "a";
String str2 = "a";
String str3 = str1;
String str4 = "b";

System.out.println(str1 == str2); // false:地址不一样,内容一样
System.out.println(str1.equals(str2)); // true: 内容一样,地址不一样
System.out.println(str1 == str3); // true: 地址一样,地址一样内容肯定一样
System.out.println(str1 == str4); // false:地址不一样
System.out.println(str1.equals(str4)); // false:内容不一样

String 是不可变的 (Immutable)

  1. 什么是不可变?怎么看出 String 是不可变的?

  2. String 为什么设计成不可变的?

    主要是为了减少 String 对象占用的空间,便于 String 对象的重用。由于 String 对象是不可变的,所以可以将它缓存起来,具有相同内容的 String 变量可以指向同一个缓存,而不用额外复制一份。深入理解这个缓存,可以查看后面提到的常量池。

  3. String 的不可变带来什么附作用?

    如果 String 内容发生变更,会重新生成一个新的对象,而不是修改原对象的内容。如果 String 内容变更频繁,则需要考虑性能问题。频繁的 String 拼接可以使用 StringBuilder 或者 StringBuffer 这两个类。

String 拼接需要注意什么?

  1. 简单的 String 拼接可以直接用 + 操作符。为了性能考虑,可以用 StringBuilder。StringBuilder 就是个可变的 String,这样就不用频繁创建 String 对象。

  2. StringBuffer 又是什么?

    StringBuilder 是 JDK1.5 之后才加入的,在这之前,StringBuffer 负责表示可变的 String。StringBuilder 和 StringBuffer 的功能基本一样,StringBuffer 是线程安全的。线程安全导致性能有点损毁,所以加入了 StringBuilder,StringBuilder 去掉了线程安全相关的代码。绝大部分场景用 StringBuilder 就可以了。

源里

String 的不可变

  1. String 的不变性是怎么实现?
  2. 不可变性在代码设计上的好处

String 内部的数据结构

  1. 在 JDK1.6 中,String 内部有一个 char 数组,通过 offset 和 count 两个变量来定位字符串的内容。offset 表示字符串在 char 数组的开始位置,count 表示字符串的长度。设计这两个变量是为了快速地从当前 String 对象派生出其他子 String 对象。具体可以看 substring 函数,这个函数会将当前对象的 char 数组传递给新的 String 对象,新的 String 对象只是 offset 和 count 不同。

    public String substring(int beginIndex, int endIndex) {
        // throw IndexOutOfBoundsException if necessary
    
        return ((beginIndex == 0) && (endIndex == count)) ? this :
            new String(offset + beginIndex, endIndex - beginIndex, value);
    }
    

    但是由于子 String 和原 String 共用 char 数组,只要有一个子 String 还被引用,公用的 char 数组就不会被回收。如果 char 数组特别大,但是子 String 很短,这样内存就浪费了。

  2. 为了解决这个问题,在 JDK1.7 中,String 就去掉了 offset 和 count 两个变量,每次 subString 会拷贝一个新的 char 数组。

  3. 在 JDK1.9 中,String 用一个 byte 数组代替 char 数组。因为一个 char 占 2 个 byte ,对于一些只需要一个 byte 表示的字符,例如英文字母,用 char 去存储就会多用一倍的空间。改成用 byte 去存储就可以更好地利用空间。

    这样改动后,考虑一个问题,byte 数组如果存储单 byte 字符,那么 byte 数组的长度就是 String 的长度,如果存储双 byte 字符,那么 byte 数组的长度除以 2 才是 String 的长度。所以 String 需要根据这两种情况来做执行不同的逻辑。

    为了标识这两种字符,在 JDK1.9 中,String 新增一个 coder 变量,如果 String 判断字符串只包含单 byte 字符,则 coder 属性值为 0,反之则为 1。String 所有的 API 再根据这个变量做相应的行为变更。

String 常量池

常量池相关面试题

首先先看几个和常量池有关的面试题。

  1. 下面代码创建几个 String 对象?答案是3个。
String a = "11"
String b = new String("22")
  1. 说出为什么会出现下面比较的结果。注意这里有一个 intern 函数。
String s1 = new String("a");
s1.intern();
String s2 = "a";
System.out.println(s1 == s2);//JDK1.6 false, JDK1.7 false

String s3 = s2 + s2; 
s3.intern();
String s4 = "aa";
System.out.println(s3 == s4);//JDK1.6 false, JDK1.7 true

解决上面两个问题需要回答下面三个问题。

String 如何存放在常量池

  • 字符串字面量会自动存在常量池。
  • String 对象调用 intern 函数,如果常量池没有内容相同的对象,则存放一份。

常量池存在哪里?

常量池存放的是什么?

知道了常量池的存在后,我们在实践中使用常量池前需要知道常量池内部的数据结构和使用常量池需要注意什么。

常量池内部的数据结构

使用常量池注意事项

API 地图

这部分可以先大概浏览,用到的时候再细看。