第一部分:异常处理 (Exception)
1. 异常体系结构
Q: Java异常体系的顶层父类是什么?它下面分为哪两大类?
A:
- 顶层父类:
java.lang.Throwable - 两大类:
- Error: JVM级别错误,不需要开发人员解决(如内存溢出)
- Exception: 程序可能出现的问题,需要程序员处理
// Throwable体系
// java.lang.Throwable
// ├── java.lang.Error (JVM错误,如StackOverflowError)
// └── java.lang.Exception (程序异常)
// ├── RuntimeException及其子类 (运行时异常)
// └── 其他非RuntimeException (编译时异常)
2. 运行时异常 vs 编译时异常
Q: 运行时异常和编译时异常的区别是什么?各举一个例子。
A:
| 特性 | 运行时异常 | 编译时异常 |
|---|---|---|
| 继承关系 | 继承 RuntimeException |
不继承 RuntimeException |
| 编译阶段 | 不报错 | 报错(红色波浪线) |
| 处理要求 | 不强制处理 | 必须处理 |
| 举例 | NullPointerException, ArrayIndexOutOfBoundsException |
FileNotFoundException, ParseException |
// 运行时异常示例 - 编译时不报错,运行时报错
String str = null;
str.length(); // NullPointerException
// 编译时异常示例 - 写代码时就报错,必须处理
// FileInputStream fis = new FileInputStream("d:/demo.txt");
// 报错:Unhandled exception: java.io.FileNotFoundException
3. 异常处理的两种方案
Q: Java中处理异常的两种方案是什么?企业开发推荐如何使用?
A:
方案1:try-catch捕获异常(最外层使用)
try {
// 可能出现异常的代码
} catch (FileNotFoundException e) {
// 处理特定异常
e.printStackTrace(); // 给程序员看
System.out.println("文件不存在"); // 给用户看
} catch (Exception e) {
// 兜底异常
}
方案2:throws抛出异常(非最外层使用)
public void method() throws FileNotFoundException, IOException {
// 抛出异常给调用者处理
}
// 简化写法:直接抛父类
public void method() throws Exception {
}
企业推荐:底层方法 throws抛出,最外层 try-catch捕获,给用户友好提示。
4. 异常处理的注意事项
Q: catch多个异常时,顺序有什么要求?为什么?
A:
子类异常在前,父类异常在后。因为异常匹配是从上到下,如果父类在前,子类异常永远匹配不到。
try {
method();
} catch (FileNotFoundException e) { // ✓ 子类在前
// 处理文件未找到
} catch (IOException e) { // 父类在后
// 处理IO异常
} catch (Exception e) { // 最终兜底
// 处理其他异常
}
// 错误写法:
// } catch (Exception e) { // ✗ 父类在前
// } catch (FileNotFoundException e) { // 编译报错:已被捕获
// }
5. 异常的作用
Q: 异常在程序中有哪些作用?
A:
- 定位Bug:异常信息包含类名、行号,快速定位问题
- 作为特殊返回值:通知上层调用者方法执行问题
- 恢复程序运行:捕获异常后程序可以继续执行
// 作用2:作为特殊返回值
public static void checkGender(String gender) {
if ("男".equals(gender)) {
throw new RuntimeException("男生禁止进入"); // 作为返回值
}
System.out.println("欢迎进入");
}
// 作用3:恢复程序运行
public static int getValidAge() {
while (true) {
try {
Scanner sc = new Scanner(System.in);
return sc.nextInt(); // 输入非数字会抛异常
} catch (Exception e) {
System.out.println("输入不合法,请重新输入"); // 恢复,继续循环
}
}
}
6. 自定义异常 - 运行时异常(推荐)
Q: 如何自定义运行时异常?什么场景下使用?
A: 推荐方式,代码简洁,编译时不强制处理。
步骤:
- 定义类继承
RuntimeException - 重写构造器
throw new创建并抛出
// 1. 定义异常类
public class GenderRuntimeException extends RuntimeException {
public GenderRuntimeException() {}
public GenderRuntimeException(String message) {
super(message); // 调用父类构造器
}
}
// 2. 使用
public static void checkGender(String gender) {
if ("男".equals(gender)) {
throw new GenderRuntimeException("男生禁止进入"); // 抛出自定义异常
}
}
// 3. 捕获处理
try {
checkGender("男");
} catch (GenderRuntimeException e) {
System.out.println(e.getMessage()); // "男生禁止进入"
}
7. 自定义异常 - 编译时异常
Q: 如何自定义编译时异常?与运行时异常的区别是什么?
A: 继承 Exception而非 RuntimeException。
区别:
- 编译时异常:方法必须
throws声明,调用者必须处理 - 运行时异常:不需要
throws声明,不强制处理
// 1. 定义编译时异常
public class GenderException extends Exception {
public GenderException() {}
public GenderException(String message) {
super(message);
}
}
// 2. 使用 - 必须声明throws
public static void checkGender(String gender) throws GenderException {
if ("男".equals(gender)) {
throw new GenderException("男生禁止进入");
}
}
// 3. 调用 - 必须处理(try-catch或继续throws)
public static void main(String[] args) {
try {
checkGender("男"); // 不try-catch会编译报错
} catch (GenderException e) {
e.printStackTrace();
}
}
第二部分:泛型 (Generics)
8. 泛型的本质与作用
Q: 什么是泛型?泛型的本质是什么?
A: 泛型是JDK5引入的特性,可以在编译阶段约束数据类型,避免强制类型转换。
本质:把具体的数据类型作为参数传给类型变量。
// 不使用泛型 - 需要强制转换,可能类型转换异常
ArrayList list = new ArrayList();
list.add("hello");
list.add(100); // 也能存进去
String str = (String) list.get(0); // 需要强转
// String str2 = (String) list.get(1); // 运行时报ClassCastException
// 使用泛型 - 编译时检查类型,无需强转
ArrayList<String> list2 = new ArrayList<>();
list2.add("hello");
// list2.add(100); // 编译报错:类型不匹配
String str = list2.get(0); // 直接获取,无需强转
9. 自定义泛型类
Q: 如何定义泛型类?类型变量命名有什么规范?
A:
语法:修饰符 class 类名<类型变量, ...> { }
常用类型变量名:
E- Element(元素)T- Type(类型)K- Key(键)V- Value(值)
// 定义泛型类
public class MyArrayList<E> {
private ArrayList<E> list = new ArrayList<>();
public void add(E e) { // E作为参数类型
list.add(e);
}
public E get(int index) { // E作为返回值类型
return list.get(index);
}
}
// 使用泛型类
MyArrayList<String> strList = new MyArrayList<>();
strList.add("黑马"); // 只能存String
String s = strList.get(0); // 返回就是String,无需强转
MyArrayList<Integer> intList = new MyArrayList<>();
intList.add(100); // 只能存Integer
10. 自定义泛型接口
Q: 如何实现泛型接口?不同实现类可以指定不同类型吗?
A: 实现类可以指定具体类型或继续保持泛型。
// 1. 定义泛型接口
public interface DataOperator<E> {
void add(E e);
E get(int index);
}
// 2. 实现类指定具体类型 - StudentDataOperator只能操作Student
public class StudentDataOperator implements DataOperator<Student> {
private ArrayList<Student> list = new ArrayList<>();
@Override
public void add(Student student) {
list.add(student);
}
@Override
public Student get(int index) {
return list.get(index);
}
}
// 3. 实现类指定具体类型 - TeacherDataOperator只能操作Teacher
public class TeacherDataOperator implements DataOperator<Teacher> {
// ... 类似实现
}
// 4. 使用
DataOperator<Student> operator = new StudentDataOperator();
operator.add(new Student("张三", 20)); // 只能传Student
11. 泛型方法
Q: 泛型方法在泛型类和非泛型类中的写法有什么区别?
A:
| 场景 | 语法 |
|---|---|
| 泛型类中 | 修饰符 E 方法名(E e) - 直接使用类定义的E |
| 非泛型类中 | 修饰符 <T> T 方法名(T t) - 必须声明 <T> |
// 非泛型类中的泛型方法
public class Demo {
// 必须声明<T>,表示这是泛型方法
public static <T> void printArray(T[] array) {
for (T t : array) {
System.out.println(t);
}
}
}
// 使用
String[] strs = {"a", "b"};
Integer[] ints = {1, 2};
Demo.printArray(strs); // T自动推断为String
Demo.printArray(ints); // T自动推断为Integer
12. 泛型通配符与上下限
Q: ?、? extends T、? super T分别表示什么?
A:
| 通配符 | 含义 | 可接收类型 |
|---|---|---|
? |
任意类型 | 任意类型 |
? extends Car |
上限 | Car及其子类 |
? super Car |
下限 | Car及其父类 |
class Che {} // 祖父类
class Car extends Che {} // 父类
class BMW extends Car {} // 子类
class Cat {} // 无关类
// ? - 任意类型
public static void print1(List<?> list) {}
print1(new ArrayList<Che>()); // ✓
print1(new ArrayList<Cat>()); // ✓
// ? extends Car - Car及其子类
public static void print2(List<? extends Car> list) {}
print2(new ArrayList<Car>()); // ✓
print2(new ArrayList<BMW>()); // ✓ (BMW是Car子类)
// print2(new ArrayList<Che>()); // ✗ (Che是父类)
// ? super Car - Car及其父类
public static void print3(List<? super Car> list) {}
print3(new ArrayList<Che>()); // ✓ (Che是父类)
print3(new ArrayList<Car>()); // ✓
// print3(new ArrayList<BMW>()); // ✗ (BMW是子类)
13. 包装类与自动装箱拆箱(面试重点)
Q: 为什么泛型不支持基本数据类型?Integer i = 100和 Integer j = 100,i == j的结果是什么?200呢?
A: 泛型只支持对象类型,基本类型需用包装类替代。
Integer缓存池:-128~127的整数会缓存,超出范围每次新建对象。
// 泛型不支持基本类型
// List<int> list; // 编译报错
List<Integer> list = new ArrayList<>(); // 必须使用包装类
// 自动装箱:int -> Integer
Integer i1 = 100; // 等价于 Integer.valueOf(100)
// 自动拆箱:Integer -> int
int i2 = i1; // 等价于 i1.intValue()
// 面试题核心
Integer i3 = 100;
Integer i4 = 100;
System.out.println(i3 == i4); // true(-128~127从缓存池取,同一对象)
Integer i5 = 200;
Integer i6 = 200;
System.out.println(i5 == i6); // false(超出缓存范围,新建不同对象)
// 包装类常用功能
String str = Integer.toString(100); // "100"
int num = Integer.parseInt("100"); // 100
double d = Double.parseDouble("3.14"); // 3.14
第三部分:集合 (Collection)
14. Collection集合体系
Q: List和Set系列集合的特点分别是什么?
A:
| 特性 | List系列 | Set系列 |
|---|---|---|
| 有序性 | 有序(添加顺序) | HashSet无序,LinkedHashSet有序,TreeSet排序 |
| 重复性 | 可重复 | 不重复 |
| 索引 | 有索引 | 无索引 |
| 实现类 | ArrayList, LinkedList | HashSet, LinkedHashSet, TreeSet |
// List:有序、可重复、有索引
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("a"); // 可重复
System.out.println(list); // [a, b, a]
System.out.println(list.get(0)); // a,有索引
// Set:无序、不重复、无索引
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("a"); // 添加失败,不重复
System.out.println(set); // 无序输出,如 [a, b]
// set.get(0); // 编译报错,无索引
15. Collection常用方法
Q: Collection接口提供了哪些通用方法?
A:
| 方法 | 作用 |
|---|---|
add(E e) |
添加元素 |
size() |
返回元素个数 |
remove(Object o) |
删除指定元素 |
isEmpty() |
判断是否为空 |
clear() |
清空集合 |
contains(Object o) |
判断是否包含 |
toArray(T[] a) |
转换为数组 |
Collection<String> c = new ArrayList<>();
c.add("黑马"); // 添加
c.add("java");
System.out.println(c.size()); // 2
System.out.println(c.contains("黑马")); // true
c.remove("java"); // 删除
System.out.println(c.isEmpty()); // false
// 转数组(指定类型)
String[] arr = c.toArray(new String[c.size()]);
System.out.println(Arrays.toString(arr)); // [黑马]
c.clear(); // 清空
System.out.println(c.isEmpty()); // true
16. 集合遍历的6种方式
Q: List集合有哪些遍历方式?写出迭代器和Lambda方式的代码。
A:
List<String> list = Arrays.asList("a", "b", "c");
// 方式1:迭代器(集合专有)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
// 方式2:fori(有索引才能用)
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 方式3:增强for
for (String s : list) {
System.out.println(s);
}
// 方式4:forEach + 匿名内部类
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
// 方式5:forEach + Lambda(推荐)
list.forEach(s -> System.out.println(s));
// 方式6:方法引用
list.forEach(System.out::println);
17. 并发修改异常及解决方案
Q: 使用fori遍历ArrayList并删除元素会有什么问题?如何解决?
A: 问题:删除元素后,后续元素前移,导致漏删。
解决方案:
- fori删除后
i-- - fori倒序遍历删除
- 使用迭代器的remove方法
ArrayList<String> list = new ArrayList<>();
list.add("Java入门");
list.add("宁夏枸杞");
list.add("黑枸杞");
list.add("人字拖");
// 问题代码:漏删
for (int i = 0; i < list.size(); i++) {
if (list.get(i).contains("枸杞")) {
list.remove(i); // 删除后i++,会跳过下一个元素
}
}
// 结果:[Java入门, 黑枸杞, 人字拖] - 黑枸杞没删掉!
// 方案1:i--
for (int i = 0; i < list.size(); i++) {
if (list.get(i).contains("枸杞")) {
list.remove(i);
i--; // 关键:不让i递增
}
}
// 方案2:倒序(推荐)
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).contains("枸杞")) {
list.remove(i); // 删除不影响前面元素的索引
}
}
// 方案3:迭代器(最标准)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.contains("枸杞")) {
it.remove(); // 用迭代器的remove,不是list的remove
}
}