Java 面试题
更新: 8/25/2025 字数: 0 字 时长: 0 分钟
接口和抽象类有什么区别?
在 Java 中,接口和抽象类都是实现抽象的机制,但它们在设计动机、功能和使用方式上存在显著差异。
设计动机和理念:
接口的设计是自顶向下的:
- 理念:我们首先从高层次考虑“某种行为规范”或“契约”。我们预先知道或约定某一组行为(即“能力”),然后基于这些行为来定义接口。任何需要具备这些行为的类,就去实现对应的接口。
- 思考过程:“我需要一个能跑(
Runnable
)的对象”、“我需要一个能比较(Comparable
)的对象”、“我需要一个能连接数据库(Connection
)的对象”。我们先定义这个“能做什么”的规范,再去考虑“谁来实现”以及“怎么实现”。 - 目的:主要用于定义行为契约,实现多态,解决多重继承问题,强调能力。
抽象类的设计是自底向上的:
- 理念:我们通常是先写了许多具体的类,在这些类的开发过程中,我们发现它们之间存在共同的属性和行为,有很多代码是可以复用的。为了消除代码冗余、提高代码复用性并提供一个统一的父类模板,我们将这些公共的逻辑和成员抽象封装到一个抽象类中。
- 思考过程:“我有很多形状类(
Circle
,Rectangle
,Triangle
),它们都有计算面积的方法,也有共同的颜色属性。我可以把这些共同的东西抽象成一个AbstractShape
类。” - 目的:主要用于提供一个通用实现模板,共享代码,实现部分功能,并强制子类完成未实现的抽象部分,强调父子关系。在实际项目开发中,很多时候抽象类是重构的产物。
总结自顶向下与自底向上:
- 自顶向下:先约定接口(规范),再由不同的类去实现它。关注点在“能做什么”。
- 自底向上:先有一些具体的类,发现共性后,将共性抽象成一个父类(抽象类)。关注点在“是什么”和“共同之处”。
其他主要区别:
除了设计理念上的差异,还有以下几个关键的技术区别:
方法实现:
- 接口:
- 在 Java 8 之前,接口中的方法默认是
public abstract
,不允许有方法实现。 - 从 Java 8 开始,接口可以包含
default
方法(有默认实现)和static
方法(静态实现)。 - Java 9 以后还允许
private
方法和private static
方法。
- 在 Java 8 之前,接口中的方法默认是
- 抽象类:
- 抽象类可以包含
abstract
方法(没有实现,必须由子类实现)和具体方法(有实现,子类可以直接继承或覆盖)。 - 允许子类继承并重用抽象类中的方法实现。
- 抽象类可以包含
- 接口:
构造函数和成员变量:
- 接口:
- 接口不能包含构造函数。
- 接口中的成员变量默认是
public static final
,即常量。
- 抽象类:
- 抽象类可以包含构造函数。虽然抽象类不能直接实例化,但它的构造函数会在子类实例化时被调用,用于初始化抽象类中定义的成员变量。
- 成员变量可以有不同的访问修饰符(如
private
,protected
,public
),并且可以不是常量。
- 接口:
多重继承:
- 接口:一个类可以实现多个接口。这是 Java 实现多重行为(多重实现)的关键机制,因为 Java 不支持类的多重继承。
- 抽象类:一个类只能继承一个抽象类(遵循 Java 的单继承原则)。
总结对比表:
特性 | 接口 (Interface) | 抽象类 (Abstract Class) |
---|---|---|
设计理念 | 自顶向下:先定义行为规范/契约 | 自底向上:先有具体类,再抽取共性作为模板 |
定义关键字 | interface | abstract class |
方法实现 | Java 8 前无实现;Java 8+ 可有 default /static 方法 | 可有抽象方法 (无实现) 和具体方法 (有实现) |
构造器 | 不能有 | 可以有 |
成员变量 | 默认 public static final (常量) | 可有各种修饰符 (private , protected , public ),可变变量 |
多重继承 | 一个类可实现多个接口 | 一个类只能继承一个抽象类 |
强调 | 行为、能力、契约、多态 | 模板、父子关系、代码复用 |
实例化 | 不能直接实例化 | 不能直接实例化 (但有构造器供子类调用) |
JDK 动态代理和 CGLIB 动态代理有什么区别?
- JDK 动态代理:
- 实现方式:基于 Java 的反射机制,在运行时为接口生成代理类。
- 要求:目标对象必须实现一个或多个接口。代理类会实现这些相同的接口。
- 优点:Java 原生支持,无需引入第三方库。
- 缺点:只能代理接口,无法代理没有实现接口的普通类或最终类(final class)。
- CGLIB 动态代理:
- 实现方式:基于 ASM 字节码生成库,在运行时动态生成目标类的子类。
- 要求:目标对象不能是 final 类或 final 方法(因为无法被继承和覆盖)。
- 优点:可以代理没有实现接口的普通类,以及最终类中非 final 方法。
- 缺点:需要引入第三方库(CGLIB 或 Spring AOP 内部集成的 ASM)。对于 final 类和 final 方法无能为力。
总结:JDK 代理是面向接口的代理,CGLIB 代理是面向类的代理(通过继承)。
你使用过 Java 的反射机制吗?如何应用反射?
反射机制允许程序在运行时检查或修改类的结构、方法和属性,而无需在编译时就知道这些信息。
具体应用举例:
- 框架开发:Spring 通过反射创建 Bean 实例,并调用其 setter 方法注入依赖。
- 动态代理:JDK 动态代理就是基于反射实现。
- 单元测试框架:JUnit 等框架通过反射来查找和执行测试方法。
- 序列化和反序列化:JSON 库(如 Gson、Jackson)通过反射来将 Java 对象转换为 JSON 字符串,或将 JSON 字符串解析为 Java 对象。
- 插件化开发:允许程序动态加载并执行外部定义的类。
- 配置文件解析:读取配置文件后,通过反射来调用相应的方法或设置属性。
如何应用反射:
要应用反射,通常涉及以下核心 API:
- 获取 Class 对象:
Class.forName("com.example.MyClass")
或myObject.getClass()
或MyClass.class
。 - 获取构造器并创建实例:
Class.getConstructor()
/getConstructors()
,然后Constructor.newInstance()
。 - 获取方法并调用:
Class.getMethod("methodName", paramTypes...)
/getMethods()
,然后Method.invoke(object, args...)
。 - 获取字段并操作:
Class.getField("fieldName")
/getFields()
,然后Field.get(object)
/Field.set(object, value)
。
说说 Java 中 HashMap 的原理?
想象一下你有一个巨大的衣柜,里面有很多抽屉,每个抽屉上都贴着一个标签(这个标签就是键,Key),你把衣服(这个衣服就是值,Value)放进对应的抽屉里。
HashMap
的原理也差不多:
标签变数字(哈希函数):当你给
HashMap
一个Key
(比如你要存一件红色的 T 恤,Key
就是“红色 T 恤”),它会先对这个Key
进行一番计算,把这个Key
变成一个数字(这个计算过程就叫做哈希函数,结果叫哈希值)。- 例子:“红色 T 恤”计算后可能变成数字
5
。
- 例子:“红色 T 恤”计算后可能变成数字
找抽屉位置(索引):这个数字可能很大,但你的衣柜抽屉是有限的。所以
HashMap
会用这个数字再做一次运算(通常是取模运算),把它映射到衣柜里某个具体的抽屉位置上。- 例子:数字
5
在你的HashMap
里可能对应第5
号抽屉。
- 例子:数字
放衣服进去(存储):
- 空抽屉:如果这个抽屉是空的,那太好了,直接把你的衣服放进去(
Key-Value
对)。 - 已有衣服(碰撞):如果这个抽屉里已经有衣服了(比如“蓝色衬衫”也计算到了第
5
号抽屉,这就叫哈希碰撞),HashMap
不会直接覆盖掉。它会把新的衣服挂到这个抽屉里已经有的衣服的后面,形成一个链子。这个链子可以是链表(早期版本)或者红黑树(当链子太长时,为了提高查找效率,JDK8 及以后会转换)。- 例子:第
5
号抽屉里本来有“蓝色衬衫”,现在“红色 T 恤”也来了,它们就会排队,比如“蓝色衬衫” → “红色 T 恤”。
- 例子:第
- 空抽屉:如果这个抽屉是空的,那太好了,直接把你的衣服放进去(
找衣服(查找):当你想要找“红色 T 恤”的时候,
HashMap
也会对“红色 T 恤”这个Key
进行同样的计算,得到同样的抽屉位置(第5
号)。然后它会到第5
号抽屉里,沿着链子一条一条地比对,直到找到“红色 T 恤”为止。
核心思想就是:
- 快速定位:通过
Key
算出哈希值,再映射到数组索引,大大提高了查找效率,不用一个一个地遍历所有元素。 - 解决冲突:用链表(或红黑树)来处理不同
Key
算出相同索引的情况。
Java 中有哪些集合类?请简单介绍
想象一下你有很多东西要管理,比如一堆照片、一份购物清单、一套学生花名册。Java 为了帮你管理这些东西,提供了各种“容器”,这些容器就是集合类。它们主要分成几大家族:
List (列表) 家族:
- 特点:有序(你放进去的顺序就是它存储的顺序),可重复(可以放两个一模一样的元素)。
- 类比:你的购物清单。买了重复的东西没关系,物品的顺序也很重要。
- 常见成员:
ArrayList
:基于数组实现。查改快,增删慢(特别是中间位置)。就像一排整齐的格子,找东西方便,但中间加东西要挪动后面的所有格子。LinkedList
:基于链表实现。增删快,查改慢。就像一串手拉手的人,加个人很容易插队,但找人要一个一个问过去。
Set (集合) 家族:
- 特点:无序(你放进去的顺序不一定是你取出来的顺序),不可重复(只能放一个,重复的会被忽略)。
- 类比:你的收藏品清单。收藏品不会重复,你也不关心它们摆放的先后顺序。
- 常见成员:
HashSet
:基于HashMap
实现。查找、添加、删除都很快。它利用了哈希表的快速查找特性。LinkedHashSet
:继承HashSet
,内部用链表维护插入顺序。既保证不重复,又能记住你添加的顺序。TreeSet
:基于红黑树实现。能自动排序(自然顺序或自定义顺序)。
Map (映射) 家族:
- 特点:存储 键值对 (Key-Value Pair)。
Key
唯一(就像抽屉标签不能重复),Value
可以重复。 - 类比:学校的花名册。每个学生学号(Key)唯一对应一个学生姓名(Value),通过学号快速找到学生,但不同的学号可能对应相同的姓名(重名)。
- 常见成员:
HashMap
:基于哈希表实现。查找、添加、删除都很快(我们上面原理讲的就是它)。无序。LinkedHashMap
:继承HashMap
,内部用链表维护插入顺序。既能快速查找,又能记住你添加的顺序。TreeMap
:基于红黑树实现。能自动按Key
排序。
- 特点:存储 键值对 (Key-Value Pair)。
总结:
- List:存一堆东西,讲究顺序,允许重复。
- Set:存一堆不重复的东西,不讲究顺序。
- Map:存一对一对的东西(Key-Value),Key 不能重复。
Java 中 HashMap 的扩容机制是怎样的?
想象你的衣柜一开始只有 16
个抽屉。你不断往里放衣服,直到抽屉越来越满,或者因为哈希碰撞导致链子越来越长,找衣服越来越慢。
HashMap
也一样,当它达到一定程度时,为了保持效率,它会进行扩容:
触发条件:
- 装载因子(Load Factor):
HashMap
有一个“装载因子”,默认是0.75
。意思是当抽屉里衣服的数量(也就是HashMap
里的元素个数)达到抽屉总数的75%
时,就触发扩容。- 例子:如果有
16
个抽屉,当放了16 * 0.75 = 12
件衣服时,就会开始考虑扩容。
- 例子:如果有
- 链表过长(JDK8+):在 JDK8 以后,如果某个抽屉里的链子长度超过一个阈值(默认是
8
),并且总抽屉数量还不够大(默认是64
),也会触发扩容(或者将链表转为红黑树)。
- 装载因子(Load Factor):
扩容操作:
- 翻倍:
HashMap
扩容时,通常会将抽屉的总数翻倍。- 例子:从
16
个抽屉变成32
个抽屉。
- 例子:从
- 重新分配:这不是简单地把旧抽屉里的衣服搬到新衣柜里,而是需要重新计算所有衣服的哈希值和它们在新衣柜里的抽屉位置。因为抽屉总数变了,
Key
映射到索引的计算方式(通常是取模)也会变。- 例子:“红色 T 恤”原来在
16
个抽屉的衣柜里对应第5
号抽屉,现在变成32
个抽屉的衣柜,它可能就要去第21
号抽屉了。所有的衣服都要重新计算一遍,搬到新位置。
- 例子:“红色 T 恤”原来在
- 为什么是翻倍? 主要是为了利用位运算(
&
运算)来提高计算新索引的效率,因为HashMap
的容量总是2
的幂次方。
- 翻倍:
优点和缺点:
- 优点:保证了
HashMap
在元素增多的情况下依然能保持较高的查找效率。 - 缺点:扩容是一个比较耗时的操作,因为它需要重新计算所有元素的哈希值和索引,并重新放置。所以如果能预估
HashMap
的大小,最好在创建时就指定一个合适的初始容量,减少扩容次数。
- 优点:保证了
简单来说:HashMap
会观察自己有多满。当它觉得太挤了,就会造一个更大的衣柜(通常是原来的两倍大),然后把所有衣服都拿出来,重新计算它们在新衣柜里的位置,再全部放进去。这样虽然麻烦一点,但确保了以后还能继续快速找衣服。