🤔 面试官问:“你能通俗地讲一下接口和抽象类有什么区别吗?在实际写项目的时候,你是怎么决定什么时候用抽象类,什么时候用接口的?”
面试完美话术总结:
从本质和设计意图上看:抽象类表示的是
is-a的关系,是对一类事物的抽象(比如大类);而接口表示的是can-do的关系,是对行为规范的定义。从应用场景上看(这里直接用你的原话加亮点):在实际项目中,如果要进行模块间的分层解耦,或者定义一套统一的 API 规范,我会优先使用接口(比如
Service接口和它的Impl实现类);而当多个子类有重复代码,需要复用核心逻辑时,我会使用抽象类,特别是结合模板方法模式,在抽象类里写好核心骨架,让子类去实现具体的细节。从 Java 语法限制上看:Java 类只能单继承,所以抽象类的使用成本比较高(用一次就没了);而接口可以多实现,扩展性更强。
🤔面试官问:“你能说说
String、StringBuilder 和
StringBuffer
这三个类的区别吗?在实际开发中需要大量拼接字符串时,你会怎么选择?”
第一层:可变性(能不能被修改?)
String是不可变的(Immutable):在它的底层源码里,保存字符的数组被final关键字修饰了。这意味着一旦一个String被创建,它的内容就死死固定了。如果你在代码里写str = str + "a",Java 底层其实是重新new了一个新的对象,极其浪费内存和性能。StringBuilder和StringBuffer是可变的:它们的底层数组没有被final修饰。当你调用append()追加内容时,它们是在原来的那块内存空间上直接修改,这就大大节省了资源。
第二层:线程安全性(多线程下安不安全?)
这就和我们刚才聊过的并发知识无缝衔接起来了!
StringBuffer(线程安全,但慢):它是 Java 早期版本的设计。为了保证多线程下的安全,它给几乎所有的方法(比如append)都加上了笨重的synchronized同步锁。你可以把它理解为字符串界的HashTable。StringBuilder(非线程安全,极快):这是后来推出的优化版。它去掉了所有的同步锁,轻装上阵。所以在单线程环境下,它的运行速度是最快的!
🏆 面试完美总结话术:
“这三者的主要区别在于可变性和线程安全。
String是不可变的,每次拼接都会产生新对象;而StringBuilder和StringBuffer是可变的。在可变类中,StringBuffer加了同步锁,是线程安全的,但性能较差;StringBuilder没有加锁,非线程安全,但性能最高。 在实际开发中,如果只是少量的字符串定义,我会直接用String;如果是单线程下需要大量拼接或修改字符串,我会毫不犹豫地选择StringBuilder;只有在极少见的需要多线程并发操作同一个字符串的场景下,我才会用StringBuffer。”
🤔
面试官问:“既然你提到了 final,那你能说说 Java 里的
final、finally 和 finalize
这三个长得很像的词,分别是什么意思,用在什么地方吗?”
1. final:霸道的”最终决断者”
🛑
这是一个修饰符。它的核心含义就是”不可改变”。它可以用来修饰三种东西:
修饰变量:这个变量就变成了”常量”,一旦赋值就永远不能再修改了(比如圆周率 PI)。
修饰方法:这个方法不能被子类重写(覆盖)。这就好比老祖宗定下的规矩,后代只能照做,不能篡改。
修饰类:这个类不能被继承(也就是不能有子类)。我们前面刚刚聊过的
String类,底层就是被final修饰的,所以你绝对无法写一个类去extends String。
2.
finally:风雨无阻的”保洁阿姨” 🧹
这是一个和异常处理(try...catch)绑定的关键字。
在写代码时,我们经常会打开一些昂贵的资源,比如数据库连接、文件读取流。不管程序是正常运行结束,还是中途崩溃报错(抛出异常),这些资源都必须被关闭,否则会导致内存泄漏。
finally
代码块就是干这个的。无论前面的代码有没有报错,finally
里面的代码是一定会被执行的!
所以我们通常把关闭资源、打扫战场的代码写在这里。
💡 面试加分项:你可以补充一句:“不过在 Java 7 之后,更推荐使用
try-with-resources语法来自动关闭资源,使得代码更简洁,从而减少对finally的直接依赖。” 面试官会觉得你紧跟时代!
3.
finalize:濒死对象的”临终遗言” 🪦
这是一个方法名,定义在所有类的老祖宗
Object 类里面。 当 Java
的垃圾回收器(GC)发现一个对象再也没人用了,准备把它销毁、回收内存之前,会最后调用一次这个对象的
finalize() 方法。这就像是给它交代”临终遗言”的机会。
但是!在实际开发中,千万不要去用它!
因为垃圾回收器的运行时间是完全不可控的,你不知道它什么时候会执行,过度依赖它会导致严重的性能问题甚至内存溢出。事实上,从
JDK 9 开始,这个方法已经被官方正式标记为废弃(Deprecated)了。
🏆 总结一下你的面试话术:
“这三个关键字其实毫无关系。
final是修饰符,用于声明属性、方法和类,表示不可变、不可重写、不可继承;finally是异常处理的一部分,不管是否发生异常,它里面的代码一定会执行,通常用来释放资源;finalize是Object类的一个方法,在对象被垃圾回收前调用,但由于执行时机不可控且影响性能,现在已经不推荐使用了。”
🤔
面试官可能会问:“你能说说 Java 里的 Error 和
Exception
有什么区别吗?什么是运行时异常(RuntimeException)?”
我们可以用一个”上班日常”的通俗例子来区分它们:
1. Error(错误):天塌了,没救了 💥
定义:指的是 Java 虚拟机(JVM)无法解决的严重问题。
特点:程序根本无法处理,也不应该去尝试用
catch捕获它。一旦发生,程序会直接崩溃。生活例子:你正在公司敲代码,突然发生了大地震,整栋楼都要塌了。这时候你无能为力,只能立刻跑路(程序崩溃)。
代码例子:最著名的就是内存溢出
OutOfMemoryError(OOM),或者无限递归导致的栈溢出StackOverflowError。
2. Exception(异常):出问题了,但还能抢救一下 🔧
定义:程序本身可以处理的异常。我们平时写的
try...catch就是专门用来对付它们的。生活例子:你正在打印文件,打印机突然提示”缺纸”。这不是世界末日,你只需要加点纸(异常处理),工作就能继续。
分类:
Exception家族内部又分为了两大派系,这就是你刚才提到的RuntimeException发挥作用的地方:第一派:RuntimeException(运行时异常 / 不受检查异常 Unchecked)
怎么理解:就像你说的,运行的时候才发现的异常。编译器在编译代码时不会强制要求你写
try...catch。为什么会发生:通常是因为程序员自己的代码逻辑写得太烂造成的!比如最经典的”空指针异常”
NullPointerException(NPE),或者数组越界IndexOutOfBoundsException。面试官如果听到你说”这通常是代码逻辑错误导致的,应该在写代码时尽量避免”,绝对会给你加分。第二派:编译时异常(受检查异常 Checked Exception)
怎么理解:还没等运行,在你写代码、点编译的那一刻,Java 编译器就会拿着刀架在你的脖子上,强制要求你必须写
try...catch或者抛出(throws),否则代码标红,根本不让你运行!代码例子:比如你要读取一个硬盘上的文件(
IOException),或者连接数据库(SQLException)。Java 觉得这种涉及外部资源的操作太容易出错了,所以逼着你提前做好应对方案。
🏆 面试完美总结话术:
“
Error和Exception都是Throwable的子类。Error指的是系统级的严重错误,比如内存溢出(OOM),这种错误程序无法恢复,也不应该去捕获。Exception是程序可以处理的异常。它又分为两种:一种是运行时异常(RuntimeException),比如空指针异常,这通常是代码逻辑问题,编译器不会强制检查;另一种是编译时异常(受检查异常),比如IOException,编译器会强制要求我们进行try...catch处理或抛出,否则无法通过编译。 在日常开发中,我们要尽量通过严谨的逻辑来避免运行时异常,而对编译时异常要做好合理的资源释放和错误提示。”