Skip to content

Java 面试题

更新: 8/25/2025 字数: 0 字 时长: 0 分钟

接口和抽象类有什么区别?

在 Java 中,接口和抽象类都是实现抽象的机制,但它们在设计动机、功能和使用方式上存在显著差异。

设计动机和理念:

  1. 接口的设计是自顶向下的:

    • 理念:我们首先从高层次考虑“某种行为规范”或“契约”。我们预先知道或约定某一组行为(即“能力”),然后基于这些行为来定义接口。任何需要具备这些行为的类,就去实现对应的接口。
    • 思考过程:“我需要一个能跑(Runnable)的对象”、“我需要一个能比较(Comparable)的对象”、“我需要一个能连接数据库(Connection)的对象”。我们先定义这个“能做什么”的规范,再去考虑“谁来实现”以及“怎么实现”。
    • 目的:主要用于定义行为契约,实现多态,解决多重继承问题,强调能力
  2. 抽象类的设计是自底向上的:

    • 理念:我们通常是先写了许多具体的类,在这些类的开发过程中,我们发现它们之间存在共同的属性和行为,有很多代码是可以复用的。为了消除代码冗余、提高代码复用性并提供一个统一的父类模板,我们将这些公共的逻辑和成员抽象封装到一个抽象类中。
    • 思考过程:“我有很多形状类(Circle, Rectangle, Triangle),它们都有计算面积的方法,也有共同的颜色属性。我可以把这些共同的东西抽象成一个 AbstractShape 类。”
    • 目的:主要用于提供一个通用实现模板,共享代码,实现部分功能,并强制子类完成未实现的抽象部分,强调父子关系。在实际项目开发中,很多时候抽象类是重构的产物。

总结自顶向下与自底向上:

  • 自顶向下:先约定接口(规范),再由不同的类去实现它。关注点在“能做什么”。
  • 自底向上:先有一些具体的类,发现共性后,将共性抽象成一个父类(抽象类)。关注点在“是什么”和“共同之处”。

其他主要区别:

除了设计理念上的差异,还有以下几个关键的技术区别:

  1. 方法实现:

    • 接口:
      • 在 Java 8 之前,接口中的方法默认是 public abstract,不允许有方法实现。
      • 从 Java 8 开始,接口可以包含 default 方法(有默认实现)和 static 方法(静态实现)。
      • Java 9 以后还允许 private 方法和 private static 方法。
    • 抽象类
      • 抽象类可以包含 abstract 方法(没有实现,必须由子类实现)和具体方法(有实现,子类可以直接继承或覆盖)。
      • 允许子类继承并重用抽象类中的方法实现。
  2. 构造函数和成员变量:

    • 接口:
      • 接口不能包含构造函数。
      • 接口中的成员变量默认是 public static final,即常量。
    • 抽象类:
      • 抽象类可以包含构造函数。虽然抽象类不能直接实例化,但它的构造函数会在子类实例化时被调用,用于初始化抽象类中定义的成员变量。
      • 成员变量可以有不同的访问修饰符(如 private, protected, public),并且可以不是常量。
  3. 多重继承:

    • 接口:一个类可以实现多个接口。这是 Java 实现多重行为(多重实现)的关键机制,因为 Java 不支持类的多重继承。
    • 抽象类:一个类只能继承一个抽象类(遵循 Java 的单继承原则)。

总结对比表:

特性接口 (Interface)抽象类 (Abstract Class)
设计理念自顶向下:先定义行为规范/契约自底向上:先有具体类,再抽取共性作为模板
定义关键字interfaceabstract 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 的反射机制吗?如何应用反射?

反射机制允许程序在运行时检查或修改类的结构、方法和属性,而无需在编译时就知道这些信息。

具体应用举例:

  1. 框架开发:Spring 通过反射创建 Bean 实例,并调用其 setter 方法注入依赖。
  2. 动态代理:JDK 动态代理就是基于反射实现。
  3. 单元测试框架:JUnit 等框架通过反射来查找和执行测试方法。
  4. 序列化和反序列化:JSON 库(如 Gson、Jackson)通过反射来将 Java 对象转换为 JSON 字符串,或将 JSON 字符串解析为 Java 对象。
  5. 插件化开发:允许程序动态加载并执行外部定义的类。
  6. 配置文件解析:读取配置文件后,通过反射来调用相应的方法或设置属性。

如何应用反射:

要应用反射,通常涉及以下核心 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 的原理也差不多:

  1. 标签变数字(哈希函数):当你给 HashMap 一个 Key(比如你要存一件红色的 T 恤,Key 就是“红色 T 恤”),它会先对这个 Key 进行一番计算,把这个 Key 变成一个数字(这个计算过程就叫做哈希函数,结果叫哈希值)。

    • 例子:“红色 T 恤”计算后可能变成数字 5
  2. 找抽屉位置(索引):这个数字可能很大,但你的衣柜抽屉是有限的。所以 HashMap 会用这个数字再做一次运算(通常是取模运算),把它映射到衣柜里某个具体的抽屉位置上。

    • 例子:数字 5 在你的 HashMap 里可能对应第 5 号抽屉。
  3. 放衣服进去(存储):

    • 空抽屉:如果这个抽屉是空的,那太好了,直接把你的衣服放进去(Key-Value 对)。
    • 已有衣服(碰撞):如果这个抽屉里已经有衣服了(比如“蓝色衬衫”也计算到了第 5 号抽屉,这就叫哈希碰撞),HashMap 不会直接覆盖掉。它会把新的衣服挂到这个抽屉里已经有的衣服的后面,形成一个链子。这个链子可以是链表(早期版本)或者红黑树(当链子太长时,为了提高查找效率,JDK8 及以后会转换)。
      • 例子:第 5 号抽屉里本来有“蓝色衬衫”,现在“红色 T 恤”也来了,它们就会排队,比如“蓝色衬衫” → “红色 T 恤”。
  4. 找衣服(查找):当你想要找“红色 T 恤”的时候,HashMap 也会对“红色 T 恤”这个 Key 进行同样的计算,得到同样的抽屉位置(第 5 号)。然后它会到第 5 号抽屉里,沿着链子一条一条地比对,直到找到“红色 T 恤”为止。

核心思想就是:

  • 快速定位:通过 Key 算出哈希值,再映射到数组索引,大大提高了查找效率,不用一个一个地遍历所有元素。
  • 解决冲突:用链表(或红黑树)来处理不同 Key 算出相同索引的情况。
Java 中有哪些集合类?请简单介绍

想象一下你有很多东西要管理,比如一堆照片、一份购物清单、一套学生花名册。Java 为了帮你管理这些东西,提供了各种“容器”,这些容器就是集合类。它们主要分成几大家族:

  1. List (列表) 家族:

    • 特点:有序(你放进去的顺序就是它存储的顺序),可重复(可以放两个一模一样的元素)。
    • 类比:你的购物清单。买了重复的东西没关系,物品的顺序也很重要。
    • 常见成员:
      • ArrayList:基于数组实现。查改快,增删慢(特别是中间位置)。就像一排整齐的格子,找东西方便,但中间加东西要挪动后面的所有格子。
      • LinkedList:基于链表实现。增删快,查改慢。就像一串手拉手的人,加个人很容易插队,但找人要一个一个问过去。
  2. Set (集合) 家族:

    • 特点:无序(你放进去的顺序不一定是你取出来的顺序),不可重复(只能放一个,重复的会被忽略)。
    • 类比:你的收藏品清单。收藏品不会重复,你也不关心它们摆放的先后顺序。
    • 常见成员:
      • HashSet:基于 HashMap 实现。查找、添加、删除都很快。它利用了哈希表的快速查找特性。
      • LinkedHashSet:继承 HashSet,内部用链表维护插入顺序。既保证不重复,又能记住你添加的顺序
      • TreeSet:基于红黑树实现。能自动排序(自然顺序或自定义顺序)。
  3. Map (映射) 家族:

    • 特点:存储 键值对 (Key-Value Pair)Key 唯一(就像抽屉标签不能重复),Value 可以重复。
    • 类比:学校的花名册。每个学生学号(Key)唯一对应一个学生姓名(Value),通过学号快速找到学生,但不同的学号可能对应相同的姓名(重名)。
    • 常见成员:
      • HashMap:基于哈希表实现。查找、添加、删除都很快(我们上面原理讲的就是它)。无序
      • LinkedHashMap:继承 HashMap,内部用链表维护插入顺序。既能快速查找,又能记住你添加的顺序
      • TreeMap:基于红黑树实现。能自动按 Key 排序

总结:

  • List:存一堆东西,讲究顺序,允许重复。
  • Set:存一堆不重复的东西,不讲究顺序。
  • Map:存一对一对的东西(Key-Value),Key 不能重复。
Java 中 HashMap 的扩容机制是怎样的?

想象你的衣柜一开始只有 16 个抽屉。你不断往里放衣服,直到抽屉越来越满,或者因为哈希碰撞导致链子越来越长,找衣服越来越慢。

HashMap 也一样,当它达到一定程度时,为了保持效率,它会进行扩容

  1. 触发条件:

    • 装载因子(Load Factor)HashMap 有一个“装载因子”,默认是 0.75。意思是当抽屉里衣服的数量(也就是 HashMap 里的元素个数)达到抽屉总数的 75% 时,就触发扩容。
      • 例子:如果有 16 个抽屉,当放了 16 * 0.75 = 12 件衣服时,就会开始考虑扩容。
    • 链表过长(JDK8+):在 JDK8 以后,如果某个抽屉里的链子长度超过一个阈值(默认是 8),并且总抽屉数量还不够大(默认是 64),也会触发扩容(或者将链表转为红黑树)。
  2. 扩容操作:

    • 翻倍HashMap 扩容时,通常会将抽屉的总数翻倍。
      • 例子:从 16 个抽屉变成 32 个抽屉。
    • 重新分配:这不是简单地把旧抽屉里的衣服搬到新衣柜里,而是需要重新计算所有衣服的哈希值和它们在新衣柜里的抽屉位置。因为抽屉总数变了,Key 映射到索引的计算方式(通常是取模)也会变。
      • 例子:“红色 T 恤”原来在 16 个抽屉的衣柜里对应第 5 号抽屉,现在变成 32 个抽屉的衣柜,它可能就要去第 21 号抽屉了。所有的衣服都要重新计算一遍,搬到新位置。
    • 为什么是翻倍? 主要是为了利用位运算(& 运算)来提高计算新索引的效率,因为 HashMap 的容量总是 2 的幂次方。
  3. 优点和缺点:

    • 优点:保证了 HashMap 在元素增多的情况下依然能保持较高的查找效率。
    • 缺点:扩容是一个比较耗时的操作,因为它需要重新计算所有元素的哈希值和索引,并重新放置。所以如果能预估 HashMap 的大小,最好在创建时就指定一个合适的初始容量,减少扩容次数。

简单来说HashMap 会观察自己有多满。当它觉得太挤了,就会造一个更大的衣柜(通常是原来的两倍大),然后把所有衣服都拿出来,重新计算它们在新衣柜里的位置,再全部放进去。这样虽然麻烦一点,但确保了以后还能继续快速找衣服。

贡献者

The avatar of contributor named as LI SIR LI SIR

页面历史