# 第 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 提供了两个类:StringBuffer和StringBuilder,它们都继承自 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]] | |
} | |
} |