一、异常(Exception)
异常是 Java 处理程序运行时错误的核心机制,企业开发中要求精准捕获、合理抛出、规范处理,避免程序崩溃或隐藏问题。
1. 核心代码示例(分类处理 + 自定义异常)
java
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* 异常核心示例:
* 1. 编译时异常(检查型)处理(try-catch/throws)
* 2. 运行时异常(非检查型)处理
* 3. 自定义业务异常
*/
// 自定义业务异常(企业中用于标识特定业务错误)
class BusinessException extends RuntimeException {
// 带错误信息的构造方法
public BusinessException(String message) {
super(message);
}
// 带错误信息+根因的构造方法(便于排查问题)
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
public class ExceptionDemo {
// 读取文件(抛出编译时异常,由调用方处理)
public static void readFile(String path) throws IOException {
if (path == null || !path.endsWith(".txt")) {
// 抛出运行时异常(参数非法)
throw new IllegalArgumentException("文件路径非法,必须是txt文件");
}
FileInputStream fis = new FileInputStream(path);
fis.close();
}
// 处理业务逻辑(封装底层异常为业务异常)
public static void doBusiness(String filePath) {
try {
readFile(filePath);
System.out.println("文件读取成功,业务处理完成");
} catch (IllegalArgumentException e) {
// 捕获参数异常,包装为业务异常抛出(统一异常类型)
throw new BusinessException("业务处理失败:参数错误", e);
} catch (FileNotFoundException e) {
throw new BusinessException("业务处理失败:文件不存在", e);
} catch (IOException e) {
throw new BusinessException("业务处理失败:文件读取异常", e);
}
}
public static void main(String[] args) {
// 测试异常处理
try {
// 测试1:参数错误(运行时异常)
// doBusiness("test.doc");
// 测试2:文件不存在(编译时异常)
doBusiness("none.txt");
} catch (BusinessException e) {
// 统一捕获业务异常,打印错误信息和根因
System.err.println("错误信息:" + e.getMessage());
// 打印异常栈(排查问题用)
e.printStackTrace();
}
}
}
代码说明:
- 异常分类 :编译时异常(
IOException/FileNotFoundException)必须显式处理(try-catch/throws);运行时异常(IllegalArgumentException/NullPointerException)可不用显式处理,但企业中建议主动捕获。 - 自定义异常 :企业中会为不同业务场景定义专属异常(如
UserNotFoundException、OrderException),便于统一异常处理和前端错误提示。 - 异常包装:底层异常(如 IO 异常)包装为业务异常抛出,避免上层感知底层实现细节,符合 "面向业务编程" 思想。
2. 企业面试题
-
问:编译时异常和运行时异常的区别?企业中如何处理?答:
- 编译时异常(Checked Exception):继承
Exception(非 RuntimeException),编译期强制处理(try-catch/throws),如IOException、SQLException; - 运行时异常(Unchecked Exception):继承
RuntimeException,编译期不强制处理,如NullPointerException、IndexOutOfBoundsException; - 企业处理原则:编译时异常要么捕获处理,要么抛给上层并说明文档;运行时异常优先预防(如判空),关键节点主动捕获,避免程序崩溃。
- 编译时异常(Checked Exception):继承
-
问:try-catch-finally 中,finally 一定会执行吗?return 在 finally 前执行会怎样?答:finally几乎 一定会执行,除非 JVM 退出(如
System.exit(0));若 try/catch 中有 return,会先执行 return 的表达式计算,再执行 finally,最后返回(若 finally 中有 return,会覆盖原 return 值,企业中禁止这么做)。 -
问:throw 和 throws 的区别?答:
throw是语句,手动抛出具体异常对象(如throw new BusinessException("错误"));throws是方法声明,声明该方法可能抛出的异常类型,告知调用方需要处理。 -
问:企业中为什么要自定义异常?答:统一异常类型,便于全局异常处理(如 Spring 的
@ControllerAdvice);区分不同业务错误场景,方便定位问题;可携带业务错误码 / 信息,适配前端错误提示。
3. 企业实际应用场景
- 全局异常处理 :Spring Boot 项目中用
@ControllerAdvice+@ExceptionHandler统一捕获所有异常,返回标准化错误响应(错误码 + 错误信息)。 - 业务流程校验 :下单时若库存不足,抛出
StockInsufficientException;登录时若用户名密码错误,抛出AuthFailedException。 - 资源操作容错 :数据库操作(
SQLException)、文件操作(IOException)捕获后,记录日志并转换为业务异常,避免暴露底层敏感信息。 - 参数校验 :接口入参校验失败时,抛出
IllegalArgumentException或自定义ParamValidException,配合校验框架(如 Hibernate Validator)使用。
二、泛型(Generic)
泛型是 JDK 5 + 特性,核心作用是 "类型参数化",避免类型强转,提升代码复用性和类型安全性,是集合、框架的基础。
1. 核心代码示例(泛型类 / 方法 / 接口 + 通配符)
typescript
import java.util.ArrayList;
import java.util.List;
/**
* 泛型核心示例:
* 1. 泛型类
* 2. 泛型方法
* 3. 泛型接口
* 4. 泛型通配符(? extends/super)
*/
// 1. 泛型类:通用数据包装类
class DataWrapper<T> {
private T data;
public DataWrapper(T data) {
this.data = data;
}
public T getData() {
return data;
}
// 泛型方法:独立于类的泛型
public <E> void printData(E extraData) {
System.out.println("核心数据:" + data + ",额外数据:" + extraData);
}
}
// 2. 泛型接口
interface Converter<F, T> {
T convert(F from);
}
// 泛型接口实现:String转Integer
class StringToIntConverter implements Converter<String, Integer> {
@Override
public Integer convert(String from) {
return Integer.parseInt(from);
}
}
// 3. 泛型通配符示例
class GenericUtil {
// 上界通配符:只能读取,不能添加(除了null)
public static void printList(List<? extends Number> list) {
for (Number num : list) {
System.out.print(num + " ");
}
System.out.println();
// list.add(10); // 编译错误:无法确定具体类型
}
// 下界通配符:只能添加,读取时只能转Object
public static void addIntToList(List<? super Integer> list) {
list.add(10);
list.add(20);
// Integer num = list.get(0); // 编译错误:只能转Object
}
}
public class GenericDemo {
public static void main(String[] args) {
// 泛型类使用
DataWrapper<String> strWrapper = new DataWrapper<>("黑马程序员");
System.out.println(strWrapper.getData()); // 无需强转
strWrapper.printData(123); // 泛型方法:E自动推断为Integer
// 泛型接口使用
Converter<String, Integer> converter = new StringToIntConverter();
Integer num = converter.convert("100");
System.out.println("字符串转数字:" + num);
// 泛型通配符使用
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
GenericUtil.printList(intList); // 上界通配符:Number的子类都可传入
List<Object> objList = new ArrayList<>();
GenericUtil.addIntToList(objList); // 下界通配符:Integer的父类都可传入
System.out.println("添加后列表:" + objList);
}
}
代码说明:
-
泛型类 :
DataWrapper<T>可包装任意类型数据,避免为 String/Integer 等分别写包装类,提升复用性。 -
泛型方法 :
<E> void printData(E extraData)独立于类的泛型,调用时自动推断类型,灵活度更高。 -
通配符:
? extends Number(上界):只能读取,适合 "生产者" 场景(如遍历输出);? super Integer(下界):只能添加,适合 "消费者" 场景(如添加元素)。
-
类型安全 :泛型在编译期检查类型,避免运行时
ClassCastException(如集合中存 String 却取 Integer)。
2. 企业面试题
-
问:泛型的作用?为什么要使用泛型?答:
- 类型安全:编译期检查类型,避免运行时类型转换异常;
- 代码复用:一套代码适配多种类型(如 List/List);
- 消除强制类型转换:提升代码可读性和简洁性。
-
问:泛型擦除是什么?运行时能获取泛型类型吗?答:泛型擦除是 JVM 的机制:编译期保留泛型检查,运行时泛型信息被擦除(如
List<String>变为List);默认无法获取,但可通过反射(如ParameterizedType)获取泛型类型(企业中框架常用,如 MyBatis)。 -
问:泛型通配符
?、? extends T、? super T的区别?答:?:无界通配符,可接收任意类型,只能读取(转 Object),不能添加;? extends T:上界通配符,只能接收 T 或 T 的子类,适合读取;? super T:下界通配符,只能接收 T 或 T 的父类,适合添加。
-
问:泛型类和泛型方法的区别?答:泛型类的泛型参数在类级别,所有方法共享;泛型方法的泛型参数在方法级别,独立于类,可在非泛型类中定义,调用时更灵活。
3. 企业实际应用场景
- 集合框架 :Java 集合(List/Map/Set)全部基于泛型,如
Map<String, User>、List<Order>,避免类型混乱。 - 通用工具类 :企业封装通用工具(如缓存工具、转换工具),用泛型适配多种类型,如
CacheUtil<K, V>、ConvertUtil<F, T>。 - 框架开发 :Spring/MyBatis 等框架大量使用泛型,如
Mapper<User>、RestTemplate.getForObject(String url, Class<T> responseType)。 - 自定义容器 :业务中封装分页结果
PageResult<T>,适配用户列表、订单列表等不同分页场景。
三、集合(Collection)
集合是 Java 存储和操作一组数据的容器,企业中 90% 以上的业务都会用到,核心分为Collection(单列)和Map(双列)两大体系。
1. 核心代码示例(常用集合 + 核心操作)
csharp
import java.util.*;
/**
* 集合核心示例:
* 1. List(ArrayList/LinkedList)
* 2. Set(HashSet/TreeSet)
* 3. Map(HashMap/TreeMap)
* 4. 集合遍历/排序/筛选
*/
public class CollectionDemo {
public static void main(String[] args) {
// 1. List:有序、可重复、索引访问
List<String> arrayList = new ArrayList<>();
arrayList.add("Java");
arrayList.add("Python");
arrayList.add("Java"); // 允许重复
// 遍历
for (String s : arrayList) {
System.out.print(s + " ");
}
System.out.println();
// 排序
Collections.sort(arrayList);
System.out.println("排序后:" + arrayList);
// LinkedList:适合频繁增删(链表结构)
List<Integer> linkedList = new LinkedList<>();
linkedList.addFirst(1);
linkedList.addLast(3);
linkedList.add(1, 2); // 索引插入
System.out.println("LinkedList:" + linkedList);
// 2. Set:无序、不可重复
Set<String> hashSet = new HashSet<>();
hashSet.add("A");
hashSet.add("B");
hashSet.add("A"); // 重复元素不会添加
System.out.println("HashSet:" + hashSet); // 无序
// TreeSet:有序(自然排序/自定义排序)
Set<Integer> treeSet = new TreeSet<>();
treeSet.add(3);
treeSet.add(1);
treeSet.add(2);
System.out.println("TreeSet(自然排序):" + treeSet); // [1,2,3]
// 3. Map:键值对、键唯一
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("语文", 90);
hashMap.put("数学", 95);
hashMap.put("语文", 92); // 覆盖已有键
// 遍历Map(企业推荐方式)
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
// 获取值
Integer mathScore = hashMap.getOrDefault("数学", 0);
System.out.println("数学成绩:" + mathScore);
// TreeMap:按键排序
Map<String, Integer> treeMap = new TreeMap<>(hashMap);
System.out.println("TreeMap(按键排序):" + treeMap);
// 4. 集合筛选(JDK 8+ Stream)
List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenList = numList.stream()
.filter(num -> num % 2 == 0) // 筛选偶数
.collect(Collectors.toList());
System.out.println("偶数列表:" + evenList);
}
}
代码说明:
- List 选型 :
ArrayList(数组结构)适合随机访问、读多写少;LinkedList(链表结构)适合频繁增删(如队列 / 栈)。 - Set 选型 :
HashSet(哈希表)查询快(O (1)),无序;TreeSet(红黑树)有序,查询 O (logn)。 - Map 选型 :
HashMap(哈希表)企业首选,查询 / 插入快;TreeMap按键排序,适合需要有序的场景。 - Stream 流:JDK 8 + 特性,企业中替代传统循环,实现集合的筛选、映射、聚合,代码更简洁。
2. 企业面试题
-
问:ArrayList 和 LinkedList 的区别?企业中如何选择?答:
- 底层结构:ArrayList 是动态数组,LinkedList 是双向链表;
- 访问效率:ArrayList 随机访问快(O (1)),LinkedList 慢(O (n));
- 增删效率:ArrayList 尾部增删快,中间增删慢(需移动元素);LinkedList 任意位置增删快(O (1));
- 内存:ArrayList 有扩容冗余,LinkedList 每个节点存前后指针,内存占用高;
- 选型:读多写少用 ArrayList,频繁增删(如队列)用 LinkedList。
-
问:HashMap 的底层原理?JDK 1.7 和 1.8 的区别?答:
- JDK 1.7:数组 + 链表,哈希冲突用链表解决,头插法(多线程下易成环);
- JDK 1.8:数组 + 链表 + 红黑树,链表长度≥8 且数组长度≥64 时转红黑树,尾插法,解决成环问题;
- 核心:通过 hashCode 计算索引,equals 判断键是否相同,负载因子 0.75(平衡空间和时间)。
-
问:HashSet 和 HashMap 的关系?HashSet 如何保证元素不重复?答:HashSet 底层是 HashMap(用 HashMap 的 key 存元素,value 存固定对象);添加元素时,先计算 hashCode,再用 equals 判断,若已存在则不添加,保证唯一性。
-
问:集合的遍历方式有哪些?哪种效率最高?答:
- List:普通 for(索引)、增强 for、迭代器(Iterator)、Stream;
- Map:keySet+get、entrySet、forEach(JDK 8+);
- 效率:ArrayList 用普通 for 最快,LinkedList 用迭代器 / 增强 for 最快;Map 用 entrySet 遍历最快(避免多次 get)。
3. 企业实际应用场景
- 数据存储 :接口返回列表(如用户列表、订单列表)用
List;去重数据(如商品标签、用户 ID)用HashSet;键值对数据(如用户 ID→用户信息)用HashMap。 - 缓存实现 :本地缓存(如加载字典数据)用
HashMap,有序缓存用TreeMap。 - 数据处理:批量数据筛选(如筛选未支付订单)、聚合(如统计各部门人数)用 Stream 流。
- 并发场景 :高并发下用
ConcurrentHashMap(线程安全)替代HashMap,用CopyOnWriteArrayList替代ArrayList。
总结
- 异常:核心是区分编译时 / 运行时异常,企业中自定义业务异常 + 全局统一处理,避免程序崩溃和信息泄露。
- 泛型:解决类型安全和代码复用问题,是集合、框架的基础,重点掌握泛型类 / 方法和通配符的使用。
- 集合:企业开发高频使用,核心是根据业务场景选型(ArrayList/HashMap 为首选),掌握底层原理和高效遍历方式。