女:你好,我们今天来聊聊Java面试里的一些高频基础题吧。我最近在学习,发现有些概念还挺绕的。比如,我们天天都在说异常,那在Java里,Exception 和 Error 到底有什么区别呢?
男:问得好,这确实是面试的开场题之一。简单来说,它们都继承自一个叫 Throwable
的“老祖宗”,但性格完全不同。你可以把 Exception
想象成程序里可预见的“小意外”,比如你想读一个文件但它不存在。这种意外,程序本身是可以处理的,我们可以用 try-catch
把它“接住”,然后决定是提醒用户还是换个文件读。而 Error
呢,就是“天灾”级别的,比如JVM虚拟机自己没内存了(OutOfMemoryError
),或者某个类的定义找不到了(NoClassDefFoundError
)。这种问题通常是程序自身无法解决的,就像大楼地基塌了,你在楼里做什么修补都意义不大了。所以我们一般不建议,也没法去捕获Error
,JVM遇到这种事,通常会直接让线程终止。
女:原来如此,Exception
是能处理的意外,Error
是处理不了的灾难。那在Exception
里,我总听到两个词:Checked Exception和Unchecked Exception,它们又有什么区别呢?
男:这个区分是Java设计里一个很有意思的点。Checked Exception
,我们叫它“受检查异常”,它非常有“契约精神”。Java编译器会强制你必须处理它,要么用try-catch
捕获,要么在方法上用throws
声明抛出去,告诉调用者“我这里可能会出这个错,你得注意”。比如我们做IO操作,像FileNotFoundException
,编译器一眼就能看到,你不处理,代码根本通不过编译。这就像签合同,风险条款必须白纸黑字写清楚。
女:那Unchecked Exception
呢?是不是就不用管了?
男:对,Unchecked Exception
,也就是“不受检查异常”,编译器对它就“睁一只眼闭一只眼”了,你可以处理,也可以不处理,编译都能通过。它主要是指 RuntimeException
和它的所有子类。比如我们最常见的NullPointerException
(空指针),ArrayIndexOutOfBoundsException
(数组越界),这些通常都是代码逻辑写得不严谨导致的,属于程序员应该在编码时就避免的“bug”。所以,Java的设计者认为,这类问题不应该强制你处处catch
,否则代码就太臃肿了。
女:我明白了,一个是“必须处理的外部风险”,一个是“应该避免的内部逻辑错误”。那我们处理异常时,Throwable
这个基类本身有没有提供一些好用的方法呢?
男:当然有。Throwable
提供了几个核心方法来帮我们追踪问题。最常用的是printStackTrace()
,它会把完整的异常堆栈信息打印到控制台,从哪儿错的,怎么调过来的,一目了然,是我们调试的“GPS”。如果你想获取更简洁的信息,可以用getMessage()
,它只返回异常的描述信息,比如“文件未找到”。还有个toString()
,它会给出异常类型和描述信息。这几个方法是我们诊断问题的基本工具。
女:说到处理异常,try-catch-finally
这个组合我倒是天天用。不过它具体是怎么个用法和执行顺序呢?
男:try-catch-finally
是异常处理的“三剑客”。try
块里放的是可能出问题的代码,像一个“雷区”。catch
块就是“排雷兵”,专门捕获并处理try
块里抛出的特定类型的异常。而finally
块是“清扫战场”的,无论try
块里的代码是否发生异常,无论异常是否被catch
捕获,finally
里的代码最后都会被执行。它最经典的应用场景就是释放资源,比如关闭文件流、数据库连接,确保这些重要操作总能被执行,避免资源泄漏。
女:你刚刚说finally
里的代码“最后都会被执行”,那有没有什么情况是,finally
里的代码其实不会执行的?
男:问到点子上了,面试官就喜欢问这种“绝对”说法的反例。finally
虽然很强大,但它也不是万能的。有一种情况它就无能为力:如果在try
或catch
块里,你调用了System.exit(1)
,这个是直接终止当前Java虚拟机的命令。JVM都“关机”了,finally
自然也就没机会执行了。另外还有两种更极端的情况,比如程序所在的线程直接“死亡”了,或者物理上CPU被关闭了,那也执行不到了。所以,说finally
“几乎”总会执行,是比较严谨的。
女:哇,还有这种细节。另外,我注意到Java 7之后好像有个叫try-with-resources
的语法,说是可以替代try-catch-finally
,这个该怎么用呢?
男:try-with-resources
是Java 7带来的一个巨大福音,专门用来优雅地处理那些需要关闭的资源。任何实现了AutoCloseable
或Closeable
接口的类,比如各种IO流,都可以用它。用法非常简洁,你直接在try
后面的括号里声明并初始化资源,比如try (Scanner scanner = new Scanner(new File("test.txt")))
。这样,当try
块执行完毕,不管正常结束还是发生异常,Java都会自动帮你调用这个资源的close()
方法。代码不仅更短、更清晰,而且还能避免忘记关闭资源导致的潜在问题。《Effective Java》这本书里就强烈推荐我们优先使用它。
女:那在使用异常的时候,有没有什么最佳实践或者需要特别注意的地方?
男:有几点非常重要。第一,不要把异常定义为静态变量,因为异常对象包含了堆栈信息,如果是静态的,多线程下信息就会错乱。每次都应该new
一个新的异常对象抛出。第二,抛出的异常信息要有意义,别抛一个Exception
然后里面什么信息都没有。第三,尽量抛出具体的异常,比如能抛NumberFormatException
就别抛它爹IllegalArgumentException
,这样问题定位更准。最后一点,避免重复打印日志。如果底层的代码已经catch
并详细记录了异常,上层业务代码只是把它继续往上抛,那就没必要再打一遍日志了,否则日志文件会变得很臃-肿,干扰问题排查。
女:好,异常这部分清晰多了。我们换个话题,聊聊泛型吧。到底什么是泛型?它有什么作用?
男:泛型(Generics)是JDK 5引入的一个里程碑式的特性。你可以把它想象成一个“类型占位符”。在没有泛型之前,像ArrayList
这样的集合,你什么都能往里放,Integer
、String
、Cat
、Dog
,它都当成Object
类型存着。取出来用的时候,你就得手动做类型转换,比如 (String)list.get(0)
。这有两个大问题:一是容易出错,万一把一个Integer
错当成String
来转,运行时就会抛出ClassCastException
;二是代码可读性差,不看上下文,你根本不知道这个List里到底应该放什么。泛型就是来解决这个的。通过ArrayList
,我们明确告诉编译器,这个List只能放String
,放别的类型,编译阶段就直接报错。取出来的时候,也不用你手动转了,编译器帮你搞定。所以,泛型的核心作用就是:提高代码的可读性和稳定性。
女:原来是为了在编译期就锁定类型,保证安全。那泛型的使用方式有哪几种呢?
男:主要有三种。第一种是泛型类,就像我们刚刚说的ArrayList
,在定义类的时候,用一个T
或E
这样的标识符作为类型参数。实例化这个类的时候,比如new ArrayList()
,再指定具体的类型。第二种是泛-型接口,和泛型类很像,比如public interface Generator
,实现这个接口的类可以选择指定具体类型,也可以继续把泛型带下去。第三种是泛型方法,它更灵活,可以在一个普通类里定义一个方法,这个方法自己有泛型参数,不受类本身是不是泛型类的限制。比如写一个工具方法,打印任何类型的数组,就可以定义成public static void printArray(E[] inputArray)
,这样它就能同时接受Integer
数组和String
数组了。
女:听起来很灵活。那在实际项目中,我们一般都在哪些地方用到泛型呢?
男:用得非常多。一个最典型的场景就是封装通用的API返回结果。我们常常会定义一个CommonResult
这样的类,T
就是泛型,代表具体返回的数据。如果是返回用户信息,T
就是User
对象;如果是返回文章列表,T
就是List
。这样,整个项目的返回结构就统一了。另外,像写一些通用的工具类,比如你提到的Excel导出工具ExcelUtil
,用泛型T
来动态指定要导出的数据类型,也非常普遍。基本上,只要你想写一段能处理多种数据类型的通用、安全的代码,泛型就是首选。
女:好,下一个话题,反射。这个词听起来就很高深,到底什么是反射?
男:反射(Reflection)确实听起来有点玄学,但理解了就很简单。你可以把它想象成程序拥有了“照镜子”的能力。通常,我们的代码在编译的时候,要调用哪个类的哪个方法都是写死的。但反射允许我们的程序在运行时,才去动态地了解一个类的信息,比如这个类有哪些方法、哪些属性,它的构造函数是什么样的。不仅能看,还能动!它可以在运行时动态地创建对象、调用方法、修改属性,哪怕这些方法或属性是private
的。正是这种在运行时“反观自身”并进行操作的能力,让Java变得异常灵活。
女:听起来非常强大,但凡事都有两面性,反射有什么优缺点呢?
男:说得没错。它的优点显而易见:灵活性和动态性。这是很多高级框架存在的基石,像Spring、MyBatis,它们的核心功能,比如依赖注入、AOP,都离不开反射。它能让代码高度解耦,变得非常通用。但缺点也很突出。首先是性能开销,反射调用比直接调用要慢,因为它涉及动态解析和查找,绕过了JIT编译器的很多优化。其次是安全性问题,反射能破坏类的封装性,连private
成员都能访问,这可能会带来安全隐-患。最后,过度使用反射会让代码变得复杂,可读性和维护性下降,因为很多错误要到运行时才会暴露,不像编译期错误那么容易发现。
女:既然有性能和安全问题,为什么还有那么多框架在用呢?它具体的应用场景有哪些?
男:好问题!这恰恰说明了它的价值。虽然有缺点,但在某些场景下,它带来的灵活性是不可替代的。我们平时写的业务代码可能很少直接用,但我们无时无刻不在享受它带来的便利。举几个例子:第一个是Spring的依赖注入(DI)。Spring启动时会扫描你的代码,看到@Autowired
注解,就用反射找到对应的Bean,再用反射把它注入到你的字段里。第二个是动态代理和AOP。你想在某个方法执行前后自动打印日志或开启事务,AOP就能做到。JDK动态代理就是利用反射,在调用真实方法前后“加戏”,那个核心的method.invoke(target, args)
就是反射调用。第三个是MyBatis这样的ORM框架。它能把数据库查出的一行数据,自动变成一个Java对象。怎么做到的?就是通过反射获取你Java类的所有属性,然后把查询结果按名字一个一个地用反射给你set进去。所以说,没有反射,就没有现在这些“自动化”和“智能化”的框架。
女:原来我们天天都在和反射打交道!那和反射经常一起出现的“注解”又是什么呢?
男:注解(Annotation)是JDK 5引入的另一个重要特性。你可以把它看作是一种特殊的“标签”,贴在类、方法或变量上,用来给它们附加一些元数据信息。这些信息可以被编译器或者程序在运行时读取和使用。比如我们最熟悉的@Override
,就是贴在方法上的一个标签,告诉编译器“这个方法是重写父类的”,编译器就会帮你检查是不是真的写对了。从本质上看,注解其实是一个继承了Annotation
的特殊接口。