# 第 11 章:随堂复习与企业真题(常用类与基础 API)


# 一、随堂复习

# 1. String 类

  • String 的声明:final 修饰(不能继承)、实现了 Comparable 接口(可以比较大小)

  • String 的不可变性

  • String 的两种定义方式:① 字面量的定义方式 String s = "hello"new 的方式: String s = new String("hello");

    • String 的内存解析:字符串常量池堆内存的使用
    • String s = new String("hello"); 在内存中创建的对象的个数:一个字符串常量对象,一个堆空间对象。
  • String 的连接操作: +

    结论

    • 常量 + 常量:结果存储在常量池中,返回此字面量的地址。且常量池中不会存在相同内容的常量。

      这里的常量有两种情况:

      • 字面量
      • final 修饰的常量
    • 常量 + 变量变量 + 变量:结果 new 在中,返回堆空间中此字符串对象的地址。

      如果变量声明为 final,那么就变成常量了!对应的情况就是 “常量 + 常量”

    • 拼接后调用 intern() :返回常量池中字面量的地址。

    • concat() 拼接:哪怕是两个常量对象拼接,结果也是 new 在空间中。

  • 熟悉 String 的构造器、与其他结构之间的转换常用方法

    • 编码和解码
      • 编码:字符、字符串 --> 字节、字节数组。对应着编码集
      • 解码:字节、字节数组 --> 字符、字符串。对应着解码集
      • 规则:解码集必须使用当初编码时使用的编码集。只要不一致,就可能出现乱码!
  • String 相关的算法问题

    • trim
    • 字符串反转
    • 子串出现次数
    • 最大相同子串
    • ...

# 2. StringBuffer 类StringBuilder 类

  • [面试题] String、StringBuffer、StringBuilder 的区别

    • String

      • 不可变的字符序列

        private final char value[];

      • 效率最低

      • 底层使用char[]数组存储 (JDK8.0 中),底层使用 byte [] 数组存储(JDK9 及之后)

    • StringBuffer

      • 可变的字符序列

        char[] value;

      • JDK1.0 引入

      • 线程安全(方法有 synchronized 修饰),因此效率比 StringBuilder 低

      • 底层使用char[]数组存储 (JDK8.0 中),底层使用 byte [] 数组存储(JDK9 及之后)

    • StringBuilder

      • 可变的字符序列

        char[] value;

      • jdk1.5 引入

      • 线程不安全的,效率高

      • 底层使用char[]数组存储 (JDK8.0 中),底层使用 byte [] 数组存储(JDK9 及之后)

  • 知道什么场景下使用 StringBuffer、StringBuilder

    • 如果开发中,需要对字符串进行频繁的增、删、改操作,建议使用 StringBuffer、StringBuilder
    • 如果开发中,不涉及多线程,建议使用 StringBuilder,因为效率更高
    • 如果开发中,可以大体确定要操作的字符个数,建议使用带有 int capacity 参数的构造器,可以避免多次扩容,性能更优

# 3. jdk8 之前的日期、时间 API

  • System.currentTimeMillis()
  • 两个 Date 的使用
  • SimpleDateFormat 用于格式化、解析
  • Calendar 日历类的使用

# 4. jdk8 中新的日期、时间 API——java.time 包

  • LocalDate、LocalTime、LocalDateTime --> 类似于 Calendar
  • Instant --> 类似于 Date
  • DateTimeFormatter ---> 类似于 SimpleDateFormat

# 5. 比较器 (重点)

  • 自然排序 Comparable 接口
    • compareTo(Object obj)
  • 定制排序 Comparator 接口
    • compare(Object obj1,Object obj2)

# 6. 其它 API

了解

# 二、企业真题

# 2.1 String

# 1. 以下两种方式创建的 String 对象有什么不同?(* 团)

String str = new String("test");
String str = "test";

new 方式创建的 String 对象存储在堆空间中,此外,如果 “test” 在字符串常量池中没有定义,还需要定义一个字符串常量对象。

字面量创建的对象存储在字符串常量池中。

# 2. String s = new String ("xyz"); 创建了几个 String Object? (新 * 陆)

两个

# 3. String a="abc" String b="a"+"bc" 问 a==b?(网 * 邮箱)

是!都在字符串常量池中,且常量池中不会存在相同内容的常量。

# 4. String 中 “+” 怎样实现?(阿 *)

常量 + 常量 :首先,在字符串常量池中查找有无拼接结果的字符串常量,如果有则不用创建;否则,在字符串常量池中新建一个字符串常量。

变量 + 常量 、变量 + 变量:创建一个 StringBuilder 的实例,通过 append () 添加字符串,最后调用 toString () 返回一个字符串。(toString () 内部 new 一个 String 的实例)

Java 中,String 的 “+” 操作是用来连接两个或多个字符串的,例如 "Hello" + "World" 就会得到 "HelloWorld"。

如果 "+" 操作符左右有变量参与,那么这个操作在编译时会被转换成 StringBuilder 的 append 方法,例如 "Hello" + "World" 会被转换成 new StringBuilder().append("Hello").append("World").toString() ,在 toString () 方法中会 new 一个 String 实例!这样做的目的是为了提高字符串连接的效率,因为 String 是不可变的,每次 “+” 操作都会创建一个新的 String 对象,而 StringBuilder 是可变的,可以在原有的基础上追加字符串。

# 5. Java 中 String 是不是 final 的?(凡 * 科技)

类似问题:
> String被哪些类继承?(网*邮箱)
> 是否可以继承String类?(湖南*利软件)
> String 是否可以继承?(阿*)

是,因此不能被继承。

# 6. String 为啥不可变,在内存中的具体形态?(阿 *)

因为其底层实现的 char 型数组 value 用了 final 修饰,这是一种享元设计模式。在开发中字符串的使用极其频繁,所以共用字符串信息,以便节省空间。

Java 中的 String 为什么是不可变的,有以下几个原因:

  • 安全性String 类是被 final 修饰的,不能被继承或修改。这样可以保证 String 在传递过程中不会被篡改,例如作为文件路径、网络地址、数据库连接等敏感信息。
  • 效率性:String 类的底层实现 char 型数组是被 private final 修饰的,只能在构造函数中赋值一次,此后无法修改。这样可以避免每次修改都要创建一个新的 String 对象,节省了内存空间和时间开销。同时,String 的不可变性也使得它可以被缓存和共享,例如字符串常量池、字符串字面量、字符串拼接等。
  • 一致性:String 是被 private final 修饰的,它的值在创建后就不会改变。这样可以保证 String 在多线程环境下不会出现数据不一致的问题,无需额外的同步机制

String:提供 **字符串常量池**。

在 jdk6 及之前,字符串常量池在方法区

之后,在堆空间

# 7. String 可以在 switch 中使用吗?(上海 * 睿)

JDK 7开始,switch 支持字符串 String 类型了 ¹³。但是要注意以下几点:

  • switch 表达式中的字符串必须是一个 String 对象,不能是 null³⁶。

  • case 标签必须是字符串常量或字面量,不能是变量或表达式 ¹³。

    字符串常量、字面量的区别,主要是在于它们的存储位置和创建方式不同:

    • 字符串常量(String Constant)是指在程序中直接用双引号括起来的字符串,例如 "Hello"。这些字符串常量会被编译器放在一个特殊的内存区域,叫做字符串常量池(String Constant Pool),它是 Java 堆(Heap)的一部分。字符串常量池可以实现字符串的共享,避免重复创建相同的字符串对象⁹⁷。
    • 字符串字面量(String Literal)是指在程序中用 new 关键字创建的字符串对象,例如 new String ("Hello")。这些字符串字面量会被分配在 Java中的普通区域,每次创建都会产生一个新的字符串对象⁹⁷。

    下面是一个示例代码,可以看出字符串常量和字面量的区别:

    String s1 = "Hello"; // 字符串常量
    String s2 = "Hello"; // 字符串常量
    String s3 = new String("Hello"); // 字符串字面量
    String s4 = new String("Hello"); // 字符串字面量
    System.out.println(s1 == s2); //true,s1 和 s2 指向同一个字符串常量池中的对象
    System.out.println(s3 == s4); //false,s3 和 s4 指向不同的字符串字面量对象
    System.out.println(s1 == s3); //false,s1 和 s3 指向不同的内存区域
  • switch 语句会根据字符串的 hash 值和 equals 方法来判断匹配的 case²⁶。

下面是一个使用 String 类型的 switch 语句的代码示例:

String fruit = "apple"; //switch 表达式
switch (fruit) { //switch 语句
    case "apple": //case 标签
        System.out.println("It is an apple.");
        break;
    case "banana":
        System.out.println("It is a banana.");
        break;
    case "orange":
        System.out.println("It is an orange.");
        break;
    default:
        System.out.println("It is not a fruit.");
}

# 8. String 中有哪些方法?列举几个(闪 * 购)

角度一:String 与其他结构间的转换

  • 基本数据类型 / 包装类 --> String
    • public static String valueOf(基本数据类型 / 包装类 xxx)
  • String --> char[] / byte[]
    • public char[] toCharArray()
    • public byte[] getBytes()

角度二:常用方法...

  • isEmpty();length();concat(String str);
  • equals(Object obj);equalsIgnoreCase(String anotherString);
  • compareTo(String anotherString);compareToIgnoreCase(String other);
  • toLowerCase();toUpperCase();
  • trim();intern();

角度三:查找

  • contains(String str)
  • indexOf(String str); indexOf(String str, int fromIndex);
  • lastIndexOf(String str);lastIndexOf(String str, int fromIndex);

角度四:字符串截取

  • substring(int beginIndex);
  • substring(int beginIndex, int endIndex);

角度五:和 char/char [] 相关

  • charAt(int index)
  • toCharArray()

角度六:开头、结尾

  • startsWith(String prefix)
  • startsWith(String prefix, int offset)
  • endsWith(String postfix)

角度七:替换

  • replace(char oldChar, char newChar)
  • replaceAll(String regex, String replacement)
  • replaceFirst(String regex, String replacement)

# 9. substring () 到底做了什么?(银 * 数据)

substring () 方法的底层实现是 new String(value, beginIndex, subLen) ,即创建一个新的字符串对象, value 指向 s 的字符数组,起始索引为 beginIndex ,子串长度为 subLen

@Test
public void test5() {
    String s = "abcdefg"; // 创建一个字符串对象
    String s1 = s.substring(2, 5);
    System.out.println(s1); // cde
    // 底层实现相当于:
    // String s1 = new String (s.value, 2, 3); 创建一个新的字符串对象,value 指向 s 的字符数组,起始索引为 2,子串长度为 3
}

这样做的好处是可以实现字符串的快速共享,节省内存空间,提高效率⁴。但是也有一些缺点,比如:

  • 如果原字符串很大,而截取的子串很小,那么会造成内存浪费,因为子串对象仍然持有原字符串的字符数组的引用⁴。
  • 如果对原字符串或者子串对象进行修改(比如使用 replace () 方法),那么会导致新的字符数组的创建,增加了时间和空间的开销⁴。

# 2.2 String、StringBuffer、StringBuilder

# 1. Java 中操作字符串有哪些类?他们之间有什么区别。(南 * 电网)

类似问题:
> String 和 StringBuffer区别?(亿*国际、天*隆、*团)
> StringBuilder和StrignBuffer的区别?(平*金服)
> StringBuilder和StringBuffer的区别以及实现?(*为)
> String:不可变的字符序列;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
> StringBuffer:可变的字符序列;JDK1.0声明,线程安全的,效率低;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
> StringBuilder:可变的字符序列;JDK5.0声明,线程不安全的,效率高;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
  • String

    • 不可变的字符序列

      private final char value[];

    • 效率最低

    • 底层使用char[]数组存储 (JDK8.0 中),底层使用 byte [] 数组存储(JDK9 及之后)

  • StringBuffer

    • 可变的字符序列

      char[] value;

    • JDK1.0 引入

    • 线程安全(方法有 synchronized 修饰),因此效率比 StringBuilder 低

    • 底层使用char[]数组存储 (JDK8.0 中),底层使用 byte [] 数组存储(JDK9 及之后)

  • StringBuilder

    • 可变的字符序列

      char[] value;

    • jdk1.5 引入

    • 线程不安全的,效率高

    • 底层使用char[]数组存储 (JDK8.0 中),底层使用 byte [] 数组存储(JDK9 及之后)

# StringBuffer、StringBuilder 的可变性分析

  • String

    String s1 = new String(); // final char[] value = new char[0];
    String s2 = new String("abc"); // final char[] value = new char[]{'a','b','c'};
  • StringBuilder

    • 内部属性
      • char [] value:存储字符序列
      • int count:实际存储的字符个数
    StringBuilder sBuilder1 = new StringBuilder(); // char[] value = new char[16];
    StringBuilder sBuilder2 = new StringBuilder("abc"); // char[] value = new char[16 + "abc".length()];
    sBuilder1.append("ac"); // value[0]='a'; value[1]='c';
    sBuilder1.append('b'); // value[2]='b';
    // 不断添加... 一旦 count 超过 value.length (),就需要扩容(扩容为原来的 2 倍 + 2),
    // 然后将原来的 value 数组复制到新的 value 数组中。

# 开发启示

  • 如果开发中,需要对字符串进行频繁的增、删、改操作,建议使用 StringBuffer、StringBuilder
  • 如果开发中,不涉及多线程,建议使用 StringBuilder,因为效率更高
  • 如果开发中,可以大体确定要操作的字符个数,建议使用带有 int capacity 参数的构造器,可以避免多次扩容,性能更优

# 2. String 的线程安全问题(闪 * 购)

String 类是 Java 中表示字符串的一个类,它有以下特点⁴:

  • String 类被 final 修饰,是一个不可变的类,也就是说一旦创建了一个 String 对象,它的内容就不能被修改 ²³⁵。
  • String 类重写了 equals () 和 hashCode () 方法,使得两个内容相同的 String 对象可以被认为是相等的,并且具有相同的哈希值⁵。
  • String 类实现了 Serializable 接口,表示它可以被序列化和反序列化⁵。
  • String 类实现了 Comparable 接口,表示它可以按照字典顺序进行比较⁵。

由于String是不可变的,所以它是线程安全的。也就是说多个线程可以同时访问同一个 String 对象而不会产生冲突³。但是这也意味着每次对 String 对象进行修改(比如拼接、替换、截取等操作)都会产生一个新的 String 对象,这会增加内存开销垃圾回收的压力³⁴。

为了解决这个问题,Java 提供了两个类:StringBufferStringBuilder,它们都继承自 AbstractStringBuilder 类,可以实现字符串的可变性动态扩容¹。它们的区别在于:

  • StringBuffer 是线程安全的,它的方法都使用了 synchronized 关键字进行同步,保证了多线程环境下的数据一致性 ¹。
  • StringBuilder 是非线程安全的,它的方法没有使用 synchronized 关键字进行同步,所以它的性能比 StringBuffer 更高,但是在多线程环境下可能会出现数据不一致的问题 ¹。

因此,在 Java 中使用字符串时,需要根据具体的场景和需求来选择合适的类。一般来说:

  • 如果字符串内容不需要改变,或者只有少量的改变操作,可以使用 String 类⁴。
  • 如果字符串内容需要频繁地改变,并且在多线程环境下运行,可以使用 StringBuffer 类 ¹。
  • 如果字符串内容需要频繁地改变,并且在单线程环境下运行,或者对线程安全没有要求,可以使用 StringBuilder 类 ¹。

# 3. StringBuilder 和 StringBuffer 的线程安全问题(润 * 软件)

见上一题。

# 2.3 Comparator 与 Comparable

# 1. 简单说说 Comparable 和 Comparator 的区别和场景?(软 ** 力)

自然排序:Comparable 接口定制排序:Comparator 接口
内部比较器外部比较器
单一的、唯一的灵活的、多样的
一劳永逸的临时的
重写 compareTo(Object obj) 抽象方法重写 compare(Object obj1,Object obj2) 抽象方法
对具体类的声明、内部进行修改将实现类的实例作为参数传递给 sort () 即可

Comparable 和 Comparator 都是 Java 中用来实现对象比较和排序的接口,它们的区别和场景如下 ²³⁴⁵:

  • Comparable 是一个内部比较器,它定义在要比较的类中,让类实现 Comparable 接口并重写 compareTo 方法,该方法返回一个整数,表示当前对象和另一个对象的大小关系。如果要让一个类的对象可以按照自然顺序进行排序,就需要实现 Comparable 接口。
  • Comparator 是一个外部比较器,它定义在要比较的类之外,让一个单独的类实现 Comparator 接口并重写 compare 方法,该方法也返回一个整数,表示两个对象的大小关系。如果要让一个类的对象可以按照不同的规则进行排序,就需要使用 Comparator 接口。
  • Comparable 和 Comparator 都可以用来对数组或集合中的元素进行排序,但是Comparable 只能提供一种排序规则,而 Comparator 可以提供多种排序规则。如果要对数组或集合中的元素进行排序,可以使用 Arrays.sort 或 Collections.sort 方法,并传入相应的比较器。
  • Comparable 和 Comparator 都可以与 lambda 表达式结合使用,简化代码的编写。例如,可以使用 (a,b)->a.getName ().compareTo (b.getName ()) 来创建一个按照名称排序的 Comparator 对象。

# 2. Comparable 接口和 Comparator 接口实现比较(阿 *)

Java 中 Comparable 接口和 Comparator 接口都可以用来实现对象的比较和排序,它们的用法如下⁴⁶⁷:

  • Comparable 接口是一个内部比较器,它定义在要比较的类中,让类实现 Comparable 接口并重写 compareTo 方法,该方法返回一个整数,表示当前对象和另一个对象的大小关系。例如,String 类就实现了 Comparable 接口,可以按照字典顺序进行比较。要使用 Comparable 接口对数组或集合中的元素进行排序,可以使用 Arrays.sort 或 Collections.sort 方法,并不需要传入比较器。
  • Comparator 接口是一个外部比较器,它定义在要比较的类之外,让一个单独的类实现 Comparator 接口并重写 compare 方法,该方法也返回一个整数,表示两个对象的大小关系。例如,可以创建一个按照年龄排序的 Comparator 对象,用来比较 Student 类的对象。要使用 Comparator 接口对数组或集合中的元素进行排序,可以使用 Arrays.sort 或 Collections.sort 方法,并传入相应的比较器
  • 代码示例:
// 定义一个 Student 类
class Student implements Comparable<Student>{
    private String name;
    private int age;
    public Student(String name,int age){
        this.name=name;
        this.age=age;
    }
    // 实现 Comparable 接口的 compareTo 方法,按照姓名排序
    public int compareTo(Student o){
        return this.name.compareTo(o.name);
    }
    // 重写 toString 方法
    public String toString(){
        return "Student[name="+name+",age="+age+"]";
    }
}
// 定义一个按照年龄排序的 Comparator 对象
class AgeComparator implements Comparator<Student>{
    public int compare(Student o1,Student o2){
        return o1.age-o2.age;
    }
}
// 测试代码
public class Test{
    public static void main(String[] args){
        // 创建一个 Student 数组
        Student[] students=new Student[]{
            new Student("Alice",20),
            new Student("Bob",18),
            new Student("Charlie",22),
            new Student("David",19)
        };
        //------ 使用 Arrays.sort 方法对数组进行排序,不传入比较器,按照姓名排序 ------
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
        // 输出:[Student [name=Alice,age=20], Student [name=Bob,age=18], Student [name=Charlie,age=22], Student [name=David,age=19]]
        //------ 创建一个 AgeComparator 对象 ------
        AgeComparator ac=new AgeComparator();
        // 使用 Arrays.sort 方法对数组进行排序,传入比较器,按照年龄排序
        Arrays.sort(students,ac);
        System.out.println(Arrays.toString(students));
        // 输出:[Student [name=Bob,age=18], Student [name=David,age=19], Student [name=Alice,age=20], Student [name=Charlie,age=22]]
    }
}