侧边栏壁纸
博主头像
PPP的日记

行动起来,活在当下

  • 累计撰写 13 篇文章
  • 累计创建 14 个标签
  • 累计收到 23 条评论

目 录CONTENT

文章目录

JavaSE

主题:Java 基础语法

1. 直击要点

核心定义:Java 是一门强类型、跨平台、面向对象的编程语言。

面试高频点:面试官通常通过基础语法考察你对内存模型(栈 vs 堆)面向对象设计思想(多态)以及语言安全性(异常与类型系统)的理解。不仅仅是会写代码,而是理解代码在 JVM 中是如何组织的。

2. 详细拆解

第一层:基础概念(数据类型与内存)

  • 强类型机制:变量必须声明类型。

  • 8 大基本数据类型

    • 数值型:byte, short, int, long, float, double

    • 字符型:char

    • 布尔型:boolean

  • 内存分配规则(重要)

    • 基本数据类型:直接存储数值,通常存放在栈(Stack)帧的局部变量表中。

    • 引用数据类型(类、接口、数组):栈中存储的是内存地址(引用),真正的对象实例存储在堆(Heap)中。

第二层:核心原理(面向对象与控制流)

  • 流程控制

    • if-else / switch:逻辑分支。

    • for / while:循环结构。

    • 生动类比:代码执行就像流水线工厂if 是分拣员,决定包裹走哪条路;for 是循环传送带,重复加工同一个零件。

  • 面向对象三大特性(OOP)

    1. 封装:封装是指将对象的属性(数据)和行为(方法)结合在一起,对外隐藏对象的内部细节,仅通过对象提供的接口与外界交互。封装的目的是增强安全性和简化编程,使得对象更加独立。

    2. 继承:继承是一种可以使得子类自动共享父类数据结构和方法的机制。它是代码复用的重要手段,通过继承可以建立类与类之间的层次关系,使得结构更加清晰。

    3. 多态(重难点):多态是指允许不同类的对象对同一消息作出响应。即同一个接口,使用不同的实例而执行不同操作。多态性可以分为编译时多态(重载)和运行时多态(重写)。它使得程序具有良好的灵活性和扩展性。

    • 生动类比:多态就像USB接口。电脑(使用者)只认识 USB 标准接口。你插上鼠标它就移动光标,插上键盘它就输入文字,插上打印机它就打印。电脑不需要知道具体设备的内部构造,只要符合 USB 标准(父类/接口)即可。

第三层:亮点/加分项(底层机制)

  • 自动装箱/拆箱

    • Java 5 引入,int ↔​ Integer 自动转换。

    • 坑点Integer 缓存池(-128 到 127)。在此范围内的对象是同一个引用,超出范围则是新对象。

  • 异常体系

    • Checked Exception(编译时异常):必须处理(try-catch 或 throws),如 IOException。体现了 Java 对稳定性的极致追求。

    • Unchecked Exception(运行时异常):如 NullPointerException,通常是代码逻辑错误。

  • 泛型擦除

    • Java 的泛型只存在于编译期,编译成字节码后,泛型类型会被擦除为 Object。这是为了兼容旧版 JVM 的历史包袱。

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试关联点

Stack vs Heap

栈存引用/基本类型,堆存对象

内存泄漏、GC 回收区域

Polymorphism

多态(父类引用指向子类对象)

接口设计、设计模式的基础

Pass-by-Value

值传递(Java 只有值传递)

方法修改参数对外部的影响

Immutable

不可变性(如 String)

线程安全、HashMap Key 的设计

Auto-boxing

自动装箱/拆箱

性能损耗、== 和 equals 的区别

4. 压力面追问 (深挖)

  1. 关于值传递

    • 问题:Java 只有值传递吗?如果我传入一个 List 到方法里,在方法里 add 一个元素,外面的 List 会变吗?如果我在方法里把这个 List 赋值为 null,外面的 List 会变吗?

    • 考察点:是否真正理解“引用地址”也是一种“值”的拷贝。

    • 回答:Java 只有值传递(Pass-by-Value),没有引用传递。 无论传入的是基本类型还是引用类型,传递的都是“副本”

      • 基本类型传的是数值的副本

      • 引用类型传的是内存地址(引用)的副本

      场景一:方法内修改属性 (Mutating State)

      • 现象:传入一个 List,在方法里 list.add("data"),方法执行完后,外面的 List 变了

      • 原理:你拿着地址副本找到了堆内存中的对象,修缮了房子(修改了堆内存数据),所以外面看到的房子也变了。

      场景二:方法内重新赋值 (Reassignment)

      • 现象:传入一个 List,在方法里 list = nulllist = new ArrayList(),方法执行完后,外面的 List 没变

      • 原理:你只是把手里的“地址副本”扔掉了或换成了新地址,但原来的“地址正本”还在调用者手里,指向的还是原来的堆内存。

  2. 关于 String

    • 问题String s = new String("abc"); 这行代码到底创建了几个对象?

    • 考察点:字符串常量池与堆内存的区别。

    • 回答:通常是 2 个(假设 "abc" 之前从未出现过)。

      1. 字符串常量池中的 "abc"(类加载或首次使用时创建)。

      2. 堆(Heap)中的 new String 对象(它是对常量池内容的拷贝/包装)。

  3. 关于精度

    • 问题:为什么涉及钱的计算(金融场景)绝对不能用 floatdouble?应该用什么?

    • 考察点:浮点数的精度丢失问题及 BigDecimal 的使用。

    • 回答:因为计算机底层用二进制表示小数(IEEE 754 标准),大部分十进制小数(如 0.1)在二进制中是无限循环小数,只能截断存储,从而导致精度丢失。因此必须使用 BigDecimal 进行计算。

      构建 BigDecimal 必须使用 String 构造器new BigDecimal("0.1")不要用 new BigDecimal(0.1)(后者依然会丢失精度)。


主题:面向对象编程 (OOP)

1. 直击要点 (TL;DR)

核心定义:面向对象通过封装、继承、多态,将现实世界的事物抽象为代码中的对象,通过对象之间的交互来完成功能。

面试必杀技:不要只背定义,要强调 OOP 的终极目标是“高内聚、低耦合”

  • 高内聚:一个类只做它该做的事(单一职责)。

  • 低耦合:类与类之间尽量少依赖,依靠接口交互(便于扩展)。

2. 详细拆解

第一层:基础概念(类、对象、接口)

  • 类 (Class):是图纸/模板。

  • 对象 (Object):是根据图纸造出来的具体实物。

  • 抽象类 vs 接口 (面试高频)

    • 抽象类 (Abstract Class):是 is-a 关系(它是谁)。只能单继承。用于抽取子类的通用代码(模板设计模式)。

    • 接口 (Interface):是 can-do 关系(它能做什么)。可以多实现。用于定义行为规范(契约)。

    • 生动类比

      • 抽象类就像“亲爹”。你只能有一个亲爹,他给你遗传了基因(复用代码)。

      • 接口就像“驾照”“英语六级证”。你可以同时拥有驾照和六级证(多实现),这代表你拥有了“开车”和“说英语”的能力,不管你亲爹是谁。

第二层:核心原理(三大特性)

  1. 封装 (Encapsulation)

    • 原理:把数据藏起来(private),只提供专门的门窗(Getter/Setter)让你操作。

    • 目的:保护数据安全,隔离复杂度。

    • 类比自动售货机。你只能看到按钮和投币口(Public 接口),你看不到里面的齿轮、弹簧和制冷压缩机(Private 实现)。如果内部零件换了,只要投币口没变,你就不需要关心。

  2. 继承 (Inheritance)

    • 原理:子类继承父类的属性和方法。

    • 目的:代码复用。

    • 注意:Java 是单继承机制(避免“菱形继承”问题),但可以通过接口弥补。

  3. 多态 (Polymorphism) —— 重中之重

    • 定义:同一个接口/父类引用,指向不同的子类实例,执行不同的逻辑。

    • 实现三要素:继承、重写 (Override)、父类引用指向子类对象。

    • 生动类比CEO 下命令

      • CEO(调用者)对着所有员工大喊一声:“开始工作!”(调用通用接口 doWork())。

        • 程序员听到后开始写代码。

        • 销售听到后开始打电话。

        • CEO 不需要分别对每个人发不同的指令,只需要发一个统一的指令,每个人根据自己的身份(对象类型)做出不同的反应。这就是多态。

      • 应用

        • 方法重载:

          • 方法重载是指同一类中可以有多个同名方法,它们具有不同的参数列表(参数类型、数量或顺序不同)。虽然方法名相同,但根据传入的参数不同,编译器会在编译时确定调用哪个方法。

          • 示例:对于一个 add 方法,可以定义为 add(int a, int b)add(double a, double b)

        • 方法重写:

          • 方法重写是指子类能够提供对父类中同名方法的具体实现。在运行时,JVM会根据对象的实际类型确定调用哪个版本的方法。这是实现多态的主要方式。

          • 示例:在一个动物类中,定义一个 sound 方法,子类 Dog 可以重写该方法以实现 bark,而 Cat 可以实现 meow

        • 接口与实现:

          • 多态也体现在接口的使用上,多个类可以实现同一个接口,并且用接口类型的引用来调用这些类的方法。这使得程序在面对不同具体实现时保持一贯的调用方式。

          • 示例:多个类(如 Dog, Cat)都实现了一个 Animal 接口,当用 Animal 类型的引用来调用 makeSound 方法时,会触发对应的实现。

        • 向上转型和向下转型:

          • 在Java中,可以使用父类类型的引用指向子类对象,这是向上转型。通过这种方式,可以在运行时期采用不同的子类实现。

          • 向下转型是将父类引用转回其子类类型,但在执行前需要确认引用实际指向的对象类型以避免 ClassCastException

第三层:亮点/加分项(设计原则)

  • SOLID 原则:如果你能随口提两个 SOLID 原则,面试官会对你刮目相看。

    • S (单一职责):一个类只负责一件事。

      • 实际场景 (Bad Case)

        • 你写了一个 UserService 类,里面既有 login() (业务逻辑),又有 saveUserToDB() (数据库操作),甚至还有 sendWelcomeEmail() (发送邮件)。

        • 后果:DB 结构变了,你要改这个类;邮件服务商变了,你也要改这个类。这个类会变得极其臃肿(God Class)。

        正确做法:拆分!UserBizService (业务), UserDao (存取), EmailUtil (发信)。

    • O (开闭原则):对扩展开放,对修改关闭。(想加新功能?去写个新类继承我,别改我原来的代码)。

      • 实际场景 (Bad Case)

        • 支付模块。你写了 if (type == 支付宝) { ... } else if (type == 微信) { ... }

        • 后果:老板明天说要接“抖音支付”,你就得去改原来的代码,万一改错了把支付宝搞挂了,你就背 P0 级事故。

        正确做法:定义一个 Payment 接口。支付宝写个类实现它,微信写个类实现它。新加抖音支付?新建一个类就行,老代码一行都不用动。

        1

        // 1. 每个策略自己说自己支持谁

        2

        class AlipayStrategy implements PaymentStrategy {

        3

            public boolean isSupport(String type) { return "ALIPAY".equals(type); }

        4

            public void pay(double amount) { /* ... */ }

        5

        }

        6

        // 2. 调用方全量注入,自动路由

        7

        @Service

        8

        public class PaymentService {

        9

            @Autowired

        10

            private List<PaymentStrategy> strategies; // Spring 会自动把所有实现类塞进来

        11

            public void process(String type, double amount) {

        12

                strategies.stream()

        13

                    .filter(s -> s.isSupport(type)) // 找到那个“自荐”的策略

        14

                    .findFirst()

        15

                    .orElseThrow(() -> new RuntimeException("无效类型"))

        16

                    .pay(amount);

        17

            }

        18

        }
    • L (里氏替换):子类别捣乱,要能完美顶替父类。

      • 实际场景 (Bad Case)

        • 经典的“正方形不是长方形”。如果你让 Square 继承 Rectangle,当你调用 setWidth(5) 时,长方形只改宽,正方形把高也改了。调用者以为改了宽不影响高,结果逻辑全错。

        正确做法:如果没有完美的继承关系(行为不一致),就不要强行继承。

    • I (接口隔离):接口别搞成“大杂烩”,要拆细点。

      • 实际场景 (Bad Case)

        • 你定义了一个 Animal 接口,里面有 fly(), run(), swim()

        • 后果:狗实现了这个接口,但狗不会飞。它被迫实现 fly() 方法,里面只能写 throw new CantFlyException()。这很尴尬。

        正确做法:拆分成 Flyable, Runnable, Swimmable 三个小接口。狗只实现 RunnableSwimmable

    • D (依赖倒置):别依赖具体的“人”,要依赖“工种”(抽象)。

      • 实际场景 (Bad Case)

        • OrderService 类里面直接 new MySqlDriver()

        • 后果:明天公司要换 Oracle 数据库,你的 OrderService 代码全得重写。

        正确做法OrderService 依赖 DataSource 接口。运行时通过 Spring 注入一个具体的实现(MySQL 或 Oracle)。这就是 Spring IOC 的核心思想

        1

        // 1. 定义抽象接口

        2

        interface Database {

        3

            void connect();

        4

        }

        5

        // 2. 实现类 (低层模块)

        6

        class MySqlDriver implements Database { ... }

        7

        class OracleDriver implements Database { ... }

        8

        // 3. 业务类 (高层模块)

        9

        public class UserService {

        10

            private Database db; // 🟢 只依赖接口,不关心具体是谁

        11

            // 通过构造函数注入 (IOC 的雏形)

        12

            public UserService(Database db) {

        13

                this.db = db;

        14

            }

        15

            public void add() {

        16

                db.connect();

        17

            }

        18

        }

        19

        // 4. 组装 (由 Spring 容器或 Main 方法完成)

        20

        // 想用 MySQL 就传 MySQL,想用 Oracle 就传 Oracle,Service 自身不用改。

        21

        UserService service = new UserService(new OracleDriver());
  • 组合优于继承 (Composition over Inheritance)

    • 继承虽然好,但耦合度太高(父类一变,子类全崩)。现代开发推崇用“组合”(在一个类里持有另一个类的对象)来代替复杂的继承关系。

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Override (重写)

运行时多态

方法名、参数必须相同,由实际对象类型决定调用哪个。

Overload (重载)

编译时多态

方法名相同,参数不同(个数/类型),与返回值无关。

Is-a vs Can-do

继承 vs 接口

抽象类解决身份问题,接口解决能力问题。

High Cohesion

高内聚

模块内部元素紧密相关,不仅易维护,Bug 也少。

Low Coupling

低耦合

模块间依赖少,修改一个模块不影响其他模块。

4. 压力面追问 (深挖)

  1. 面试官: “你刚才说了多态,那 Java 的 static 方法可以被重写(Override)吗?为什么?”

    • 答案不可以

    • 原理:重写是基于运行时的对象类型来动态绑定的(Dynamic Binding)。而 static 方法在编译时就绑定到了类上(Static Binding),跟具体的对象实例无关。如果你在子类写了一个同名静态方法,那叫“隐藏”,不叫“重写”。

  2. 面试官: “接口(Interface)和抽象类(Abstract Class)在 Java 8 之后越来越像了(接口有了 default 方法),那它们现在的本质区别到底是什么?”

    • 答案:本质区别在于状态(State)

    • 原理:抽象类可以有成员变量(字段),可以保存对象的状态。接口虽然能写方法逻辑了,但它仍然不能保存实例变量(只能有 static final 常量)。只要你需要存“状态”,就必须用抽象类。

  3. 面试官: “为什么说‘组合优于继承’?能举个反例说明继承的坏处吗?”

    • 答案:继承破坏了封装性,父类暴露了实现细节给子类。

    • 举例:如果父类 HashSetaddAll 方法内部调用了 add 方法。子类为了统计添加次数,重写了 addaddAll,结果调用子类 addAll 时,次数会被统计两遍(一次在 addAll,一次在内部调用的 add),这就是脆弱基类问题。用组合(装饰器模式)就能避免这个问题。

  4. 面试官: “在高并发下,如果你的策略类是单例的(Spring 默认),且里面有成员变量,会产生什么问题?”

    • 答案线程安全问题

    • 原理:Spring 管理的 Bean(如 Service、Strategy)默认是单例(Singleton)的,多线程会共享同一个策略实例的成员变量。

    • 规范建议:策略类应该是无状态的(Stateless),所有数据通过方法参数传入,或者使用 ThreadLocal 隔离。

  5. 面试官: “如果不同的支付策略依赖不同的配置(如支付宝需要 AppId,微信需要秘钥),你的路由逻辑怎么兼容?”

    • 答案上下文对象 (Context)

    • 原理:传入一个统一的 PaymentContext 对象,里面包含所有可能的参数。各策略根据需要自取。


主题:Java 常用工具类

1. 直击要点 (TL;DR)

核心价值:工具类是 Java 生态的“瑞士军刀”。

面试高频

  1. 原生神器java.util 下的 Collections, Arrays, Objects

  2. 并发神器JUC (java.util.concurrent) 下的 CountDownLatch, Semaphore(重点)。

  3. 三方神器:Apache Commons (StringUtils), Google Guava, Hutool(国内常用)。

    一句话原则优先用标准库,其次用成熟开源库,最后才自己写。

2. 详细拆解

第一层:JDK 原生基础工具

  • Objects (JDK 7+)

    • 用途:优雅地处理 null

    • 常用Objects.equals(a, b)

    • 避坑:不要再写 if (a != null && a.equals(b)) 这种啰嗦代码了,直接用 Objects.equals,它帮你处理了 null 判断。

  • Arrays & Collections

    • 用途:数组和集合的操作(排序、二分查找、转线程安全集合)。

    • 面试点Collections.synchronizedList() 确实能返回线程安全 List,但性能很差(全加了 synchronized 锁),高并发请转战 JUC。

  • Optional (JDK 8)

    • 用途:防止 NullPointerException (NPE)。

    • 生动类比防弹礼盒

      • 以前你直接拿礼物(对象),如果礼物是空的,你一摸就炸了(NPE)。

      • 现在礼物被装在一个防弹盒子(Optional)里。你先问盒子“里面有东西吗?”(isPresent),或者说“如果没东西给我个备用的”(orElse)。

第二层:高并发工具类 (JUC) —— 面试必问

这些工具类都在 java.util.concurrent 包下,用于协调多线程协作。

  1. CountDownLatch (倒计时门闩)

    • 作用:让一个线程等待其他 N 个线程执行完再执行。

    • 场景:主服务启动前,必须等待数据库检查、缓存预热、文件加载这 3 个任务都完成。

    • 生动类比监考老师收卷

      • 老师(主线程)在讲台上等着。

      • 学生(子线程)一个个交卷。

      • 只有当最后一个学生交完卷(计数器归零),老师才能打包走人。

  2. Semaphore (信号量)

    • 作用:控制同时访问资源的线程数量(限流)。

    • 场景:数据库连接池只有 10 个连接,同时只允许 10 个线程操作,多出来的要排队。

    • 生动类比停车场入口

      • 停车场只有 5 个车位(Permits = 5)。

      • 车进来一辆,显示屏数字减 1。

      • 数字变成 0 时,栏杆落下,外面的车必须等。

      • 有一辆车出去了(release),数字加 1,栏杆抬起,下一辆才能进。

  3. CyclicBarrier (循环栅栏)

    • 作用:让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达,屏障才会开门,所有线程同时继续。

    • 生动类比公司团建坐大巴

      • 导游举着旗子在车门口等。

      • 来一个人(await),上车坐好。

      • 人没齐绝对不发车。

      • 人齐了(达到阈值),司机一脚油门,大家一起出发。

第三层:常用三方库 (加分项)

面试官问你“平时怎么判断字符串为空”,也是在考察你的工程经验。

  • Apache Commons Lang3StringUtils.isBlank(str)。比 Java 原生的 isEmpty 强在它能识别 " " (纯空格) 也是空。

  • Guava (Google):提供了不可变集合 (ImmutableList),布隆过滤器等高级工具。

  • BeanUtils (Spring vs Apache)

    • 场景:对象属性拷贝 (DTO -> DO)。

    • 坑点:尽量用 Spring 的 BeanUtils,不要用 Apache Commons 的 BeanUtils。因为 Apache 的大量使用了反射,性能极差;Spring 的做了缓存优化。

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

NPE (空指针)

NullPointerException

"使用 Objects 或 Optional 优雅避开 NPE。"

Deep Copy (深拷贝)

复制对象及其引用的对象

"BeanUtils 通常是浅拷贝,深拷贝通常用序列化/反序列化实现。"

Thread Safe

线程安全

"Collections 工具类虽然能转换线程安全集合,但并发性能低。"

AQS

AbstractQueuedSynchronizer

"CountDownLatch 和 Semaphore 的底层核心都是基于 AQS 队列实现的。"

4. 压力面追问 (深挖)

  1. 面试官:Arrays.asList() 转换出来的 List 有什么坑?”

    • 答案

      1. 它是定长的(Fixed-size)。你不能 add 也不能 remove,否则报 UnsupportedOperationException

      2. 它是视图。修改原数组,List 里的元素也会变。

    • 底层:它返回的不是 java.util.ArrayList,而是 Arrays 内部的一个静态私有内部类。

  2. 面试官:Collection.sortArrays.sort 的底层排序算法一样吗?”

    • 答案

      • 基本数据类型(如 int[]):使用 Dual-Pivot Quicksort (双轴快排)。效率高,但不稳定(相同元素位置可能变),但数字本身无所谓稳定性。

      • 对象类型(如 User[]):使用 TimSort (归并+插入的混合排序)。保证稳定性(这一点对业务对象排序很重要)。

  3. 面试官: “你是怎么理解深拷贝(Deep Copy)和浅拷贝(Shallow Copy)的?BeanUtils 是哪种?”

    • 答案

      • 浅拷贝:只复制对象的第一层属性。如果属性是引用类型(如 List),两个对象共享同一个 List。

      • 深拷贝:完全独立的副本。

      • 结论BeanUtils.copyProperties浅拷贝。如果对象里嵌套了对象,修改副本会影响原件。要实现深拷贝,推荐用 JSON 序列化再反序列化。


主题:异常处理体系

1. 直击要点 (TL;DR)

  • 核心定义:异常是 Java 提供的用于处理程序运行错误的机制。它的核心在于“将错误处理代码与正常的业务代码分离”

面试必考

  1. 继承体系Throwable 是所有异常的父类。

  2. 分类Error(系统崩溃) vs Exception(程序错误)。

  3. 性质Checked Exception(受检异常,编译不过) vs Unchecked/Runtime Exception(运行时异常,逻辑错误)。

  4. 语法糖try-with-resources (JDK 7+ 自动关闭资源)。

2. 详细拆解

第一层:异常家族谱系 (Hierarchy)

Java 的异常体系就像一个家族:

  • Throwable:老祖宗。

    • Error“绝症”。通常是 JVM 层面的严重错误,如 OutOfMemoryError (OOM)、StackOverflowError。程序无法处理,只能挂掉。

    • Exception“普通病”。程序可以捕获并处理。

      • Checked Exception (受检异常)“疫苗”。编译器强制要求你打疫苗(处理)。如果你不 try-catch 或者 throws,代码都编译不过。

        • 典型IOException, SQLException, ClassNotFoundException

        • 意义:提醒开发者,这里可能会出问题(如文件读不到、网断了),请做好预案。

      • Unchecked Exception (运行时异常)“意外”。通常是程序员的代码逻辑写错了。编译器不管。

        • 典型NullPointerException (NPE), IndexOutOfBoundsException, ArithmeticException (/0)。

第二层:核心原理 (捕获与处理)

  • try-catch-finally

    • try:尝试执行高风险代码。

    • catch:如果出事了,执行这里的补救措施。

    • finally无论出不出事,必须执行的代码(通常用于释放资源,如关流、关锁)。

  • try-with-resources (JDK 7+)

    • 原理:只要实现了 AutoCloseable

    • 口的类,放在 try(...) 的括号里,Java 会自动为你调用 close() 方法。

    • 价值:这是大厂代码规范中强制要求的写法,彻底杜绝“忘记关流”导致的内存/句柄泄漏。

强制举例 (生动类比)

开车过收费站

  • try:你开车上高速。

  • catch:如果爆胎了(异常),你靠边换备胎(处理逻辑)。

  • finally:无论你是否爆胎,最后出收费站时,你都必须交过路费(释放资源)。

  • Throws:如果你不会换备胎,你把车扔在路边,打电话叫拖车(把异常抛给上层调用者处理)。

第三层:亮点/加分项 (底层与性能)

  • 性能损耗

    • 抛出异常是昂贵的。

    • 原因:JVM 需要建立完整的堆栈追踪 (Stack Trace),这需要把当前线程栈里的所有帧(Frame)信息都快照下来。

    • 推论绝对不要用异常来做流程控制(比如用 try-catch 来判断循环结束),这会让性能慢一个数量级。

  • JVM 实现

    • JVM 编译后会生成一张 异常表 (Exception Table)。当异常发生时,JVM 去查表,看哪一行代码对应的异常应该跳转到哪一行指令去处理。这不是通过普通的 if-else 实现的,而是指令级的跳转。

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Checked vs Unchecked

编译时 vs 运行时

"IO/SQL 异常必须显式处理,NPE 等逻辑错误通常不处理。"

AutoCloseable

自动关闭接口

"JDK 7 引入的 try-with-resources 依赖此接口实现资源自动释放。"

Stack Trace

堆栈追踪

"异常性能开销的主要来源,记录了异常发生时的调用链路。"

Finally Block

最终执行块

"除非 JVM 宕机 (System.exit),否则 finally 一定执行。"

Exception Swallowing

吞掉异常

"捕获了异常却什么都不做(空 catch),是极其恶劣的代码习惯。"

4. 压力面追问 (深挖)

(这部分是“送命题”,请务必死记硬背)

  1. 面试官:try-catch-finally 中,如果在 catchreturn 了,finally 还会执行吗?如果是 finally 里也 return 了,谁说了算?”

    • 答案

      1. 会执行finally 块会在 catch 里的 return 执行前(准确说是准备返回结果之前)被插入执行。

      2. finally 说了算。如果 finally 里也有 return,它会覆盖trycatch 里的返回值。这是个著名的反模式绝对不要在 finally 里写 return,否则会丢失异常信息和原有的返回值。

  2. 面试官: “为什么阿里巴巴 Java 开发手册规定:‘禁止捕获 Exception 这种通用的异常,必须捕获具体的异常’?”

    • 答案:为了精准定位问题

    • 场景:你写了 catch (Exception e)。结果你的代码既可能报 IOException,也可能报 NullPointerException。如果你一锅端,在日志里就很难区分到底是网络坏了,还是你代码逻辑写错了。这就叫“掩耳盗铃”。

  3. 面试官: “我们都知道 Error 是严重的错误,那 OutOfMemoryError (OOM) 可以被 catch 吗?catch 了有用吗?”

    • 答案

      • 语法上:可以 catch (因为 Error 也是 Throwable 的子类)。

      • 实际上没有意义

      • 原因:当 OOM 发生时,JVM 的堆内存已经耗尽了。你 catch 住了,想做点补救措施(比如打印日志),但打印日志也需要分配内存对象,结果就是二次崩溃。遇到 Error,最好的办法是让程序赶紧死掉,保留现场(Dump 文件)供事后分析。


代码实战:Bad vs Good

Bad Case (经典的烂代码)

1

public void readFile() {

2

    FileInputStream fis = null;

3

    try {

4

        fis = new FileInputStream("test.txt");

5

        // ... 读文件

6

    } catch (Exception e) { 

7

        // 🔴 1. 捕获太宽泛,不知道具体错哪了

8

        // 🔴 2. 空 catch (吞异常),出了事就像没发生一样,排查火葬场

9

    } finally {

10

        try {

11

            if (fis != null) fis.close(); 

12

        } catch (IOException e) {

13

            // 🔴 3. 连关闭流都写得这么啰嗦,还可能覆盖主逻辑的异常

14

        }

15

    }

16

}

Good Case (优雅的 try-with-resources)

1

public void readFile() {

2

    // 🟢 自动关闭:fis 实现了 AutoCloseable

3

    try (FileInputStream fis = new FileInputStream("test.txt")) {

4

        // ... 读文件

5

    } catch (FileNotFoundException e) {

6

        // 🟢 精准捕获:文件找不到的处理逻辑

7

        log.error("文件不存在", e);

8

    } catch (IOException e) {

9

        // 🟢 精准捕获:读写失败的处理逻辑

10

        log.error("IO 异常", e);

11

    }

12

    // 🟢 不需要写 finally,JVM 自动帮你调 close()

13

}

主题:try-with-resources (自动资源管理)

1. 直击要点 (TL;DR)

核心定义:一种专门用于自动关闭资源的语法糖。

解决痛点:以前如果你打开了文件、数据库连接、网络流,必须手动在 finally 块里关闭。如果忘了关,或者关的时候报错了,代码会非常难看且容易导致资源泄漏。

一句话口诀“把资源创建写在 try 后面的圆括号里,出了括号自动关。”

2. 详细拆解

第一层:新旧对比 (以此突显你的专业度)

❌ The Old Way (JDK 6 及以前)

这是典型的“代码屎山”。为了安全关闭一个流,你需要写极其啰嗦的判空和嵌套 try-catch。

1

FileInputStream fis = null;

2

try {

3

    fis = new FileInputStream("test.txt");

4

    // 业务逻辑...

5

} catch (IOException e) {

6

    e.printStackTrace();

7

} finally {

8

    // 🔴 必须手动关闭

9

    if (fis != null) {

10

        try {

11

            fis.close();

12

        } catch (IOException e) {

13

            // 🔴 关闭时可能又报错,还得再 catch,无限套娃

14

            e.printStackTrace();

15

        }

16

    }

17

}

✅ The New Way (JDK 7+ try-with-resources)

这是“现代 Java 写法”。清爽、优雅、安全。

1

// 关键点:把 new 对象的代码放到 try(...) 的括号里

2

try (FileInputStream fis = new FileInputStream("test.txt")) {

3

    // 业务逻辑...

4

    // 🟢 你完全不需要写 finally,也不需要手动调 close()

5

    // 🟢 一旦代码执行出了这个大括号,JVM 自动帮你关流

6

} catch (IOException e) {

7

    e.printStackTrace();

8

}

第二层:核心原理 (AutoCloseable 接口)

你可能会问:“凭什么写在括号里它就能自动关?它是怎么知道怎么关的?”

  • 原理:Java 7 引入了一个新接口 java.lang.AutoCloseable

  • 机制:凡是实现了这个接口的类(比如所有的 IO 流、数据库连接 Connection、Socket),都拥有一个 close() 方法。

  • 编译期黑魔法:编译器在把 .java 编译成 .class 字节码时,会偷偷帮你把 try-with-resources 还原成 try-catch-finally 的样子

    • 也就是说,这是语法糖。底层还是用了 finally,只是编译器帮你写了,而且写得比你更严谨。

强制举例 (生动类比)

酒店的插卡取电模式

  • Old Way (手动关):你离开酒店房间,必须挨个把厕所灯、台灯、电视、空调都手动关掉。如果你忘关了空调,电费就一直在跑(资源泄漏)。

  • New Way (插卡取电):房间的总开关(try 的圆括号)就是那个插卡槽。

    • 当你进门(进入 try 块),把卡插进去,通电。

    • 当你拔卡出门(离开 try 块),所有电器瞬间自动断电。你根本不需要关心具体开了哪个灯,系统强制帮你全关了。

第三层:进阶技巧 (多资源与自定义)

  1. 处理多个资源

    如果你既要读文件 A,又要写文件 B,中间用分号 ; 隔开即可。

    Java

    1

    try (

    2

        FileInputStream fis = new FileInputStream("input.txt");

    3

        FileOutputStream fos = new FileOutputStream("output.txt")

    4

    ) {

    5

        // 读写逻辑

    6

    } // 🟢 出来时,fos 先关,fis 后关 (像栈一样,后进先出)
  2. 自定义资源

    你也可以自己写一个类,让它能自动关闭。只要实现 AutoCloseable 接口。

    Java

    1

    // 1. 定义资源

    2

    class MyResource implements AutoCloseable {

    3

        @Override

    4

        public void close() {

    5

            System.out.println("🔥 资源自动销毁中...");

    6

        }

    7

    }

    8

    // 2. 使用

    9

    public void test() {

    10

        try (MyResource r = new MyResource()) {

    11

            System.out.println("正在使用资源...");

    12

        } // 🟢 打印:"🔥 资源自动销毁中..."

    13

    }

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Syntactic Sugar

语法糖

"它不是 JVM 的新指令,而是编译器帮我们生成了 finally 代码。"

AutoCloseable

自动关闭接口

"所有能放入 try(...) 的类必须实现此接口。"

Suppressed Exception

被抑制的异常 (高阶)

"如果 try 块报错,close 也报错,TWR 会保留 try 的异常,把 close 的异常作为'抑制异常'挂在后面,不会弄丢。"


4. 压力面追问 (模拟面试场景)

面试官: “我在 try-with-resources 的括号里写了 FileInputStream,但是我在 try 块里面手动调了 close(),然后再抛出异常,请问最后会发生什么?会报错说‘流已关闭’吗?”

  • 你的回答

    • 不会报错

    • 原理AutoCloseable 的规范要求 close() 方法必须是幂等的(Idempotent)。也就是说,调用一次和调用 N 次效果一样。如果你手动关了,JVM 自动再关一次时,通常会判断状态,直接跳过,不会抛异常。

面试官: “如果 try 块里的业务代码抛了异常 A,然后自动执行 close() 的时候也抛了异常 B(比如硬盘突然坏了)。老式的 try-catch-finally 会抛出哪个?try-with-resources 会抛出哪个?”

  • 你的回答 (满分答案)

    • 老式写法:会抛出 B (close 的异常)。异常 A 被覆盖了(吞掉了)。这很糟糕,因为开发者丢失了业务错误的线索。

    • try-with-resources:会抛出 A (业务异常)。异常 B 会被标记为 Suppressed Exception(被抑制的异常),挂在 A 的异常堆栈里。你可以通过 e.getSuppressed() 查看到它。这才是完美的异常处理。


主题:集合框架与泛型 (容器与类型安全)

1. 直击要点 (TL;DR)

核心定义

  • 集合:用于存储对象的容器。主要分为 Collection (单列) 和 Map (双列/键值对)。

  • 泛型:JDK 5 引入的编译时安全检查机制。它强制要求集合只存特定类型,避免了取值时的强制类型转换,消灭了 ClassCastException

    面试必背

  • List (有序, 可重复)ArrayList (底层数组,查快增删慢), LinkedList (底层链表,增删快查慢)。

  • Set (无序, 不重复)HashSet (底层是 HashMap), TreeSet (底层是红黑树)。

  • Map (键值对)HashMap (数组+链表+红黑树), ConcurrentHashMap (CAS+synchronized)。

2. 详细拆解

第一层:泛型 (Generics) —— 伪泛型

  • 核心原理类型擦除 (Type Erasure)

    • Java 的泛型只存在于编译期

    • 编译成字节码 (.class) 后,所有的 List<String> 都会变成原生的 List (即 List<Object>)。

    • JVM 根本不知道泛型的存在。

  • 为什么要擦除?:为了兼容 JDK 5 之前的旧代码(那时候没有泛型)。

  • 生动类比便利贴标签

    • 泛型就是贴在箱子上的“书籍专用”便利贴。搬家工人(编译器)看到标签,会阻止你往里放砖头(编译报错)。

    • 一旦封箱打包(编译)发车了,便利贴就被撕掉了。到了新家(运行时),这就是一个普普通通的箱子,里面装的都是 Object。

第二层:集合顶层架构 (Collection vs Map)

  • Collection 接口

    • List

    • Set

    • Queue

  • Map 接口

    • Key-Value 映射。Key 是唯一的(像身份证号),Value 可以重复(像名字)。

第三层:ArrayList 深度剖析

  • 底层实现Object[] elementData (动态数组)。

  • 特点

    • 随机访问快 (Random Access):实现了 RandomAccess 接口。知道下标 i,直接根据内存地址偏移量就能取到数据,复杂度 O(1)

    • 增删慢:如果在中间插入或删除,需要把后面的元素全部挪窝(System.arraycopy),复杂度 O(n)

  • 扩容机制 (重中之重)

    1. 初始容量:JDK 7 以前是 10。JDK 8 以后是 0 (懒加载,第一次 add 时才变成 10)。

    2. 扩容时机:装不下了。

    3. 扩容倍数1.5 倍

      • 源码公式:int newCapacity = oldCapacity + (oldCapacity >> 1); (右移一位等于除以 2)。

    4. 搬家代价:每次扩容都要创建一个新数组,把老数据全拷过去。非常耗性能。

强制举例 (生动类比)

ArrayList 扩容 = 公司搬家

  • 你公司租了个10 人位的办公室(初始容量)。

  • 招到第 11 个人时,坐不下了。

  • 你不能在原办公室隔壁直接加个座位(数组在内存中是连续的,隔壁可能有别人)。

  • 你必须去租一个新的、更大的办公室(15 人位)。

  • 然后让所有员工停下工作,抱着电脑一个个走到新办公室(ArrayCopy)。

  • 教训:如果你知道公司今年要招 1000 人,一开始就直接租 1000 人位的办公室(new ArrayList(1000)),拒绝频繁搬家

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Type Erasure

类型擦除

"泛型在编译后会消失,JVM 看到的都是 Object。"

RandomAccess

随机访问

"ArrayList 支持 O(1) 访问,LinkedList 不支持。"

System.arraycopy

数组拷贝

"ArrayList 扩容和增删的核心开销,属于 Native 方法。"

Fail-Fast

快速失败机制

"迭代遍历时如果有人修改集合,立即抛 ConcurrentModificationException。"

Capacity vs Size

容量 vs 元素个数

"Capacity 是数组长度(房子大小),Size 是实际存的元素(住了几个人)。"


4. 压力面追问 (深挖)

  1. 面试官:ArrayListLinkedList 的区别?现在的项目中,你还会用 LinkedList 吗?”

    • 教科书回答:Array 查快增删慢,Linked 查慢增删快。

    • 大厂实战回答 (加分)“实际上,我们几乎不用 LinkedList。”

    • 原因:虽然 LinkedList 理论上增删快,但它对 CPU 缓存(Cache)不友好。ArrayList 的内存是连续的,CPU 预读取非常快;LinkedList 内存是分散的,CPU 命中率低。除非是极端的“头尾频繁操作”场景,否则无脑选 ArrayList。

  2. 面试官: “我们说 ArrayList 线程不安全,那如果我需要在多线程环境下使用 List,怎么做?”

    • 方案 A (错误)Vector。太老了,全方法加锁,性能极差。

    • 方案 B (不推荐)Collections.synchronizedList()。也是全局锁,并发度低。

    • 方案 C (推荐)CopyOnWriteArrayList (COW)

      • 原理:写时复制。读的时候不加锁(读旧数组),写的时候拷贝一份新数组进行修改,改完把指针指过去。适合读多写少的场景(如白名单、配置列表)。

  3. 面试官: “ArrayList 的扩容是 1.5 倍,HashMap 是 2 倍,为什么不一样?”

    • 答案

      • HashMap 用 2 倍(2 的幂次方)是为了配合位运算 (n - 1) & hash 来快速定位数组下标,代替取模运算 %,提升效率。

      • ArrayList 用 1.5 倍是为了平衡空间与时间。如果 2 倍可能浪费太多空间,如果 1.1 倍则扩容太频繁。1.5 倍是一个经验值。


主题:HashMap 深度解析

1. 直击要点 (TL;DR)

核心定义:HashMap 是基于哈希表实现的 Key-Value 容器。

数据结构 (JDK 1.8)数组 + 链表 + 红黑树

使用红黑树的原因:在频繁插入、删除的场景下,红黑树相比 AVL 树平衡调整更少、性能更优。

  • 详细分析:

    • 提升高哈希冲突下的性能:当大量元素哈希值相同,导致某一个桶(bucket)中的链表过长时,链表遍历的线性时间复杂度O(n)会导致 HashMap 性能极剧下降。红黑树作为一种自平衡二叉查找树,其查找、插入、删除的时间复杂度均稳定在 O(logn)

    • 红黑树比 AVL 树更适合工程应用

      • 插入/删除效率高:红黑树不追求完全的平衡,而是近似平衡(最大路径长度不超过最小路径的两倍),这使得在插入和删除操作时,所需的旋转和调整次数远少于要求严格平衡的 AVL 树。

      • 结构紧凑:红黑树节点相对较小,内存占用少。

核心流程

  1. 计算 Key 的 Hash 值。

  2. 通过 (n - 1) & hash 找到数组下标。

  3. 如果没有冲突,直接放入。

  4. 如果冲突(下标一样),挂在链表上。

  5. 进化点:当链表长度超过 8 且数组长度超过 64 时,链表转为红黑树,将查询复杂度从 O(n) 降为 O(log n)

2. 详细拆解

第一层:物理结构 (数组+链表)

  • 数组 (Node[] table):这是 HashMap 的躯干,是一排连续的桶(Bucket)。

  • 链表:这是为了解决哈希冲突 (Hash Collision)。当两个不同的 Key 算出了同一个下标时,它们只能挤在同一个房间里,形成链表。

  • 哈希算法hash(Object key)

    • 扰动函数:JDK 1.8 中,为了让 Hash 值更散列,使用了 (h = key.hashCode()) ^ (h >>> 16)(高 16 位与低 16 位异或)。

第二层:Put 方法的执行流程 (面试必考)

  1. Hash:拿到 Key,算 Hash 值。

  2. Index:定位下标。

  3. Check:看下标位置有没有人。

    • 没人:直接 new Node 占座。

    • 有人 (Hash 冲突)

      • 如果是红黑树:按树的规则插入。

      • 如果是链表:遍历链表。

        • 如果有 Key 相同的,覆盖 Value。

        • 如果没有,插在链表尾部 (JDK 1.8 尾插法)。

        • 插完后,检查是否需要转红黑树 (链表长度 > 8)。

  4. Resize:插入完成后,如果总元素个数 > Capacity * LoadFactor (默认 16 * 0.75 = 12),触发扩容

强制举例 (生动类比)

哈希魔法学校的宿舍分配

  • 数组:学校的 16 间宿舍 (编号 0-15)。

  • Key:学生 (比如 Harry)。

  • Hash 算法 (分院帽):分院帽算出 Harry 应该住 5 号宿舍。

  • 冲突:Ron 也被分到了 5 号宿舍。

  • JDK 1.7 (链表 - 拥挤的双层床)

    • 宿舍里只能放双层床。如果来了 10 个人都分到 5 号房,就要搭 10 层高的床。

    • 找人时(Get),宿管阿姨要爬梯子一个个看脸,累死 (O(n))。

  • JDK 1.8 (红黑树 - 魔法空间)

    • 当 5 号房挤了超过 8 个人,分院帽施法,把双层床变成了一颗巨大的魔法树

    • 学生们按名字规律挂在树枝上。

    • 找人时,阿姨只需要看“左边还是右边”,几下就能找到 (O(log n))。

第三层:JDK 1.7 vs 1.8 的关键区别 (高分项)

特性

JDK 1.7

JDK 1.8

原因

底层结构

数组 + 链表

数组 + 链表 + 红黑树

解决哈希冲突严重时的查询性能退化问题。

插入方式

头插法 (插在头部)

尾插法 (插在尾部)

头插法在多线程扩容时会导致死循环 (环形链表)。

扩容计算

重新计算所有 Hash

高位运算 (不用重算)

利用 oldCap 的二进制位,元素要么在原地,要么在 oldPos + oldCap,效率极高。

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Load Factor (0.75)

负载因子

"时间与空间的折中。太大省空间但冲突多,太小冲突少但频繁扩容。"

Treeify Threshold (8)

树化阈值

"泊松分布统计学结果。链表长度达到 8 的概率极低,此时转树性价比最高。"

Untreeify Threshold (6)

退化阈值

"为了避免频繁的树转链表、链表转树(抖动),特意留了 2 的缓冲差值。"

(n-1) & hash

下标定位

"当 n 是 2 的次幂时,这就等价于取模,但位运算快得多。"

Thread Unsafe

线程不安全

"多线程 put 可能导致数据覆盖;1.7 扩容可能死循环。"

4. 压力面追问 (深挖)

  1. 面试官: “为什么 HashMap 的容量(Capacity)必须是 2 的次幂(16, 32, 64...)?如果我手动传个 17 进去会怎么样?”

    • 答案

      1. 为了位运算代替取模:只有当 n 是 2 的次幂时,hash % n 才等价于 (n - 1) & hash。位运算比除法快几十倍。

      2. 为了分布均匀(n - 1) 的二进制全是 1 (如 15 是 1111),这样 & 出来的结果能充分利用 Hash 值的每一位。如果是 17 (10001),& 完很多位永远是 0,导致严重的哈希冲突。

      3. 结果:如果你传 17,HashMap 构造函数里的 tableSizeFor() 方法会帮你把 17 向上取整变成 32。它根本不会让你用 17。

  2. 面试官: “JDK 1.7 的 HashMap 在多线程扩容时为什么会死循环?”

    • 答案

      • 根本原因并发扩容 + 头插法核心现象

        1. 头插法会改变链表的顺序(比如原链表 A -> B,扩容后变成 B -> A)。

        2. 并发竞争:线程 1 挂起时记住了旧的顺序,线程 2 执行完把顺序反过来了。

        3. 结果:线程 1 恢复执行时,拿着旧引用去操作新顺序,导致 A.next = BB.next = A,形成环形链表。

        4. 爆发点:当后续有人调用 get() 方法遍历该链表时,进入死循环,CPU 飙升 100%。

  3. 面试官: “如果 Key 是一个自定义的对象(比如 User),需要注意什么?”

    • 答案:必须重写 equals()hashCode() 方法。

    • 原则equals 相等的对象,hashCode 必须相等。

    • 后果:如果只重写 equals 不重写 hashCode,把 User 存进去后,下次用一个一模一样的 User 去取,会因为 Hash 值不同而算出不同的下标,导致取不到数据(内存泄漏)


主题:Java 线程安全集合 (并发容器 JUC)

1. 直击要点 (TL;DR)

核心分类:Java 中的线程安全集合主要分为三代:

  1. 第一代(骨灰级)Vector, Hashtable。全方法加锁 (synchronized),性能极差,面试时要说“坚决不用”

  2. 第二代(包装级)Collections.synchronizedList/Map。通过装饰器模式加了互斥锁,性能依然一般。

  3. 第三代(并发级 - JUC)ConcurrentHashMap, CopyOnWriteArrayList

    • 采用了分段锁CAS读写分离等高级机制。

    • 结论:高并发场景下,无脑选 JUC 包下的集合


2. 详细拆解

第一层:为什么不用 Vector 和 Hashtable?

  • 原理:它们简单粗暴地在所有 get, put, add 方法上加了 synchronized 关键字。

  • 后果:这把锁是对象级别的重量级锁。一个线程在写,其他线程连读都不能读。

  • 生动类比

    类比:不仅锁门,还锁大楼

    • 你想去图书馆借一本书(读操作)。

    • 但因为管理员怕有人偷书(写操作),规定只要有一个人在图书馆里,把整个图书馆大门锁死

    • 哪怕你在 1 楼看报纸,他在 10 楼搬桌子,你们完全不冲突,但你也得在门外等着。这就是 Vector 的效率。

第二层:ConcurrentHashMap (面试核心 - 1.7 vs 1.8) 🏆

这是面试中含金量最高的集合。它解决了 HashMap 线程不安全的问题,同时兼顾了超高的性能。

  • JDK 1.7 实现:分段锁 (Segment Locking)

    • 原理:把大数组切分成 16 个小段(Segment)。每个 Segment 配一把锁(ReentrantLock)。

    • 效果:线程 A 修改 Segment 1,线程 B 修改 Segment 2,互不干扰,支持 16 个线程并发。

    • 生动类比

      类比:图书馆分馆管理

      • 以前是锁大门(Hashtable)。

      • 现在把图书馆分成 A区、B区...P区(16 个区)。

      • 你想去 A 区借书,只锁 A 区的门。想去 B 区的人可以直接进。并发度提升了 16 倍。

  • JDK 1.8 实现:CAS + synchronized (节点锁)

    • 原理抛弃了分段锁。直接锁链表/红黑树的头节点

    • 机制

      1. 如果没有 Hash 冲突:使用 CAS (Compare And Swap) 无锁插入。

      2. 如果有冲突:使用 synchronized 锁住当前这个桶(Node)。

    • 效果:锁的粒度细到了极致(Hash Bucket 级别)。只要 Hash 不冲突,并发度理论上是无限的。

    • 生动类比

      类比:书架上的书

      • 1.7 是锁一个房间(Segment)。

      • 1.8 是只锁那一排书架(Node)。

      • 你要拿第一排的书,完全不影响我去拿第二排的书。粒度更细,排队的人更少。

第三层:CopyOnWriteArrayList (读写分离)

它是 ArrayList 的线程安全兄弟,专门用于读多写少的场景(如白名单、配置缓存)。

  • 原理

    • :不加锁,直接读原数组(极快)。

    • :不直接修改原数组。而是加锁,复制一份新数组,修改新数组,然后把指针指向新数组。

  • 生动类比

    类比:景区公示牌

    • 游客(读线程):成千上万个游客在看公示牌,大家随便看,不需要排队。

    • 管理员(写线程):要修改公告了。他不会把现在的牌子拆下来(那样游客就看不到了)。

    • 他会在后台做一个新的牌子,改好后,趁大家不注意,瞬间把旧牌子换成新牌子。


3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Segment (分段锁)

1.7 的核心

"将锁的粒度细化,降低锁竞争。1.8 弃用。"

CAS (无锁算法)

Compare And Swap

"1.8 CHM 插入新节点时优先用 CAS,失败了才用 synchronized。"

COW (写时复制)

Copy On Write

"读写分离思想,牺牲写性能换取读性能。存在内存占用高的问题。"

Fail-Safe

安全失败

"JUC 集合遍历时不会抛异常,因为它们操作的是副本或弱一致性视图。"

Volatile

可见性

"CHM 的 Node 中的 value 和 next 都是 volatile 的,保证读操作能立刻看到最新值。"


4. 压力面追问 (深挖)

  1. 面试官:ConcurrentHashMap 的 Key 和 Value 可以是 null 吗?HashMap 呢?为什么会有这种区别?”

    • 答案

      • HashMap:Key 和 Value 都可以是 null。

      • ConcurrentHashMap:Key 和 Value 都不能是 null。

    • 原因 (二义性问题)

      • 在多线程环境下,如果你 get(key) 返回了 null,你无法分辨是 "Key 不存在" 还是 "Key 对应的值就是 null"

      • HashMap 可以用 containsKey() 再查一次来确认。

      • 但多线程下,你刚查完 containsKey,可能另一个线程就把值改了,所以这种检查是不可靠的。为了避免歧义,Doug Lea 直接禁止了 null。

  2. 面试官: “为什么 JDK 1.8 的 ConcurrentHashMap 要放弃分段锁,改用 synchronized?”

    • 答案

      1. 减少内存开销:每个 Segment 都要继承 ReentrantLock,只要创建 CHM 就会预创建这些对象,内存占用大。

      2. 锁粒度更细:1.8 锁的是 Node(桶),并发度随着数组容量增加而增加;1.7 受限于 Segment 个数(默认 16)。

      3. JVM 优化:JDK 1.6 之后,synchronized 进行了大量优化(偏向锁、轻量级锁),性能已经不输给 ReentrantLock 了。

  3. 面试官:CopyOnWriteArrayList 有什么缺点?什么场景绝对不能用?”

    • 答案

      1. 内存占用高:每次写都要复制数组。如果数组很大(几百兆),频繁触发 GC,系统会卡死。

      2. 数据一致性:只能保证最终一致性,不能保证实时一致性。(读不到刚刚写入的数据,有一点延迟)。

      3. 结论写多读少的场景绝对不能用(如实时日志写入)。


主题:Java 泛型 (编译期的守门员)

1. 直击要点 (TL;DR)

核心定义:泛型是 JDK 5 引入的参数化类型机制。

本质语法糖

作用

  1. 编译期安全检查:把运行时的 ClassCastException 提前到编译期解决。

  2. 代码复用:一套代码可以适配多种数据类型(如 List<T>)。

    面试必背结论Java 的泛型是伪泛型,编译后会进行“类型擦除”,运行时根本不存在泛型信息。

2. 详细拆解

第一层:类型擦除 (Type Erasure) —— 核心原理

  • 现象:你在代码里写 List<String>List<Integer>,编译成 .class 文件后,它们都变成了原生类型 List (即 List<Object>)。

  • JVM 视角:JVM 看不懂 <String>。它只知道这是一个 List,里面装的是 Object。所有的类型转换(Checkcast)都是编译器帮你偷偷插入的。

  • 为什么这么设计?:为了 兼容性。Java 5 推出泛型时,为了让以前写的 JDK 1.4 的老代码(没有泛型)还能在新的 JVM 上跑,不得不做出的妥协。

强制举例 (生动类比)

便利贴与搬家箱子

  • 编写代码 (编译前):你往箱子上贴了一张便利贴,写着“书本专用”。

  • 编译器 (守门员):编译器看到便利贴,会检查你是不是往里放了书。如果你往里放砖头,它就拦截你(编译报错)。

  • 编译后 (运行时):一旦检查通过,箱子封口发货。为了省事,便利贴被撕掉了(擦除)

  • 运行中:到了 JVM 仓库里,这个箱子就是一个普通的、没有任何标记的箱子。里面装的东西在 JVM 看来全是 Object

第二层:通配符与 PECS 原则 (难点)

面试中最容易晕的地方:List<? extends T>List<? super T> 到底有什么区别?

  • PECS 原则Producer Extends, Consumer Super。

  • ? extends T (上界通配符)

    • 含义:装的必须是 T 或 T 的子类。

    • 能力只读不写(除了 null)。因为编译器只知道里面是“某种 T”,但不知道具体是哪种 T,所以不敢让你往里放东西(怕类型不匹配)。

    • 角色生产者 (Producer),适合往外拿数据。

  • ? super T (下界通配符)

    • 含义:装的必须是 T 或 T 的父类。

    • 能力只写不读(读出来的只能是 Object)。因为编译器知道里面装的至少是 T 的父类,所以把 T 放进去肯定是安全的。

    • 角色消费者 (Consumer),适合往里塞数据。

第一种情况:? extends T (只读不写)

口诀“它是生产者 (Producer),只能往外拿数据。”

假设你有一个箱子,标签上写着:List<? extends Cat>。 这代表:“这个箱子里装的是 Cat,或者是 Cat 的某个子类。”

  • 底层真相:这个箱子原本可能就是 List<Cat>,也可能甚至是 List<Garfield>

  • 编译器困境:因为编译器不知道箱子原本到底是装哪种猫的,所以为了安全,它禁止你往里放任何东西

第二种情况:? super T (只写不读)

口诀“它是消费者 (Consumer),只能往里塞数据。”

假设你有一个箱子,标签上写着:List<? super Cat>。 这代表:“这个箱子原本是用来装 Cat 的,或者装 Cat 的父类(Animal / Object)的。”

  • 底层真相:这个箱子原本可能是 List<Cat>,也可能是 List<Animal>,甚至是 List<Object>

  • 编译器逻辑:既然这个箱子至少能装 Cat,那么我往里扔 Cat 或者 Cat 的子孙,肯定装得下!

强制举例 (生动类比)

水果盘 (extends) vs 垃圾桶 (super)

  • List<? extends Fruit> (水果盘)

    • 这就是一盘某种水果。可能是“一盘苹果”,也可能是“一盘香蕉”。

    • 能不能吃 (Get)? 能!拿出来的肯定是水果。

    • 能不能放 (Add)? 不能! 如果这盘其实是香蕉,你硬塞一个苹果进去,就乱套了。

  • List<? super Apple> (苹果回收桶)

    • 这是一个能装苹果的桶。它可能是“苹果桶”,也可能是“水果桶”,或者是“万物桶(Object)”。

    • 能不能放 (Add)? 能!只要是苹果,尽管往里扔,肯定装得下。

    • 能不能拿 (Get)? 不能! 你伸手进去掏,不知道掏出来的是苹果,还是香蕉,还是烂泥(Object)。

第三层:泛型的局限性 (坑点)

  • 不能用基本类型List<int> 是非法的,必须用 List<Integer>。因为泛型擦除后是 Object,int 不是 Object。

  • 不能创建泛型数组new List<String>[10] 是非法的。

    • 原因:数组是协变的(Covariant)且保留类型信息,泛型是不可变的且擦除类型信息。两者机制冲突,为了安全直接禁止。

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Type Erasure

类型擦除

"编译后泛型消失,List<String> 和 List<Integer> 是同一个类。"

Pseudo-Generics

伪泛型

"C# 是真泛型(运行时保留),Java 是伪泛型(为了兼容历史)。"

PECS

存取原则

"生产数据用 extends(读),消费数据用 super(写)。"

Bridge Method

桥接方法

"编译器自动生成的 helper 方法,用于在字节码层面保持多态性。"

Reifiable

具体化类型

"数组是具体化的(运行时知道类型),泛型是非具体化的(运行时不知道)。"


4. 压力面追问 (深挖)

(这些问题专门用来识别你是否真正理解了“擦除”)

  1. 面试官:ArrayList<String> list1ArrayList<Integer> list2,请问 list1.getClass() == list2.getClass() 的结果是什么?”

    • 答案true

    • 解释:因为类型擦除。在运行时,它们全是 java.util.ArrayList,没有区别。

  2. 面试官: “既然泛型在编译期就检查了,那我有办法在运行时往 List<String> 里塞一个 Integer 吗?”

    • 答案可以。使用 反射 (Reflection)

    • 代码

      Java

      1

      List<String> list = new ArrayList<>();

      2

      // list.add(1); // 编译报错

      3

      Method add = list.getClass().getMethod("add", Object.class);

      4

      add.invoke(list, 1); // 运行成功!因为运行时它就是个 List<Object>
    • 后果:当你尝试 list.get(0) 时,会抛出 ClassCastException

  3. 面试官:List<?>List<Object> 有什么区别?”

    • 答案:区别很大。

    • List<Object>:表明它可以存储任何 Object。你可以随意 add 任何对象。

    • List<?>:表明它是“未知类型的 List”。它是只读的(除了 null)。编译器不知道它具体是什么类型,为了安全,禁止你往里 add 任何非 null 元素。


主题:IO 模型演进 (BIO →​ NIO →​ AIO)

1. 直击要点 (TL;DR)

核心演进:从“一个连接一个线程”的笨重模式,进化到“一个线程管理成千上万个连接”的高效模式。

  • BIO (Blocking IO)同步阻塞。传统的 java.io 包。一根筋,死等。

  • NIO (Non-blocking IO)同步非阻塞(多路复用)。java.nio 包。核心是 Selector(轮询)。

  • AIO (Asynchronous IO)异步非阻塞java.nio.channels.AsynchronousSocketChannel。核心是 回调

2. 详细拆解

第一层:BIO (Blocking IO) —— 传统派

  • 别名:OIO (Old IO)。

  • 模型Thread-Per-Connection(一个连接配一个线程)。

  • 工作流程:服务端 ServerSocket.accept() 等待连接。一旦有客户端连上来,就这就分配一个线程专门伺候它。

  • 缺点:如果客户端连上来不说话(不发数据),这个线程就干等着(阻塞),浪费 CPU 和内存资源。并发量一高(比如 10 万连接),服务器直接 OOM 挂掉。

强制举例 (生动类比)

食堂排队打饭 (BIO)

  • 场景:你是食堂阿姨(线程)。

  • 工作:一个学生(连接)过来,你问他吃什么。

  • 阻塞:学生说“我还没想好”,然后在窗口前思考了 5 分钟。你就必须拿着勺子傻等他 5 分钟,后面的人全堵死了,你也干不了别的事。

  • 扩容:想服务更多人?只能多雇阿姨(多开线程),成本极高。

第二层:NIO (Non-blocking IO / New IO) —— 变革派 (面试重点)

  • 核心组件

    1. Buffer (缓冲区):数据都在 Buffer 里读写(面向块),而不是面向流。

    2. Channel (通道):双向的(类似铁路),既能读也能写。

    3. Selector (选择器)灵魂所在。一个线程(Selector)可以监控成千上万个 Channel。

  • 工作流程:所有连接都注册到 Selector 上。Selector 不断轮询(或通过 epoll 事件通知),谁有数据过来了,就处理谁。没数据的连接完全不占线程资源。

强制举例 (生动类比)

餐厅的点餐器 (NIO)

  • 场景:你是唯一的服务员(Selector 线程)。

  • 工作:进来了 100 个客人(连接)。你给每人发一个震动取餐器(注册 Channel)。

  • 非阻塞:客人坐下思考吃什么(不发数据),你不等他,你就在柜台玩手机(处理其他事)。

  • 多路复用:谁选好了,按一下取餐器,你的柜台响了(事件触发),你立马过去只给这一个客人点餐。

  • 效果:你一个人就能轻松应付 100 个客人,只要他们不是同时按铃。

第三层:AIO (Asynchronous IO) —— 理想派

  • 模型:真正的异步。

  • 工作流程:应用程序向操作系统发起一个 IO 请求,然后直接返回去做别的事。等操作系统把数据读完、写到内存里了,再回调通知应用程序:“货到了,你来拿吧”。

  • 现状:在 Linux 上,AIO 的底层实现(epoll 模拟)并不比 NIO 强太多,且编程极其复杂。所以主流框架(如 Netty)后来放弃了 AIO,回归了 NIO

强制举例 (生动类比)

外卖送货上门 (AIO)

  • BIO/NIO:虽然 NIO 不用傻等,但饭做好了,还得你自己去窗口端过来(线程还要负责数据拷贝)。

  • AIO:你点完外卖就去打游戏了(完全不管 IO)。等饭送到了,外卖员敲你家门(回调),你直接开吃。数据拷贝的过程是外卖员(操作系统)帮你搞定的。

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Blocking vs Non-blocking

阻塞 vs 非阻塞

"发起 IO 请求后,线程是否被挂起等待数据就绪。"

Sync vs Async

同步 vs 异步

"数据从内核拷贝到用户空间,是应用线程自己拷(同步),还是OS拷完通知我(异步)。"

Multiplexing (多路复用)

IO 复用

"NIO 的核心。通过一个线程监听多个文件描述符(FD)的状态。"

Zero Copy (零拷贝)

性能优化

"减少 CPU 在内核态和用户态之间的数据拷贝次数。"


4. 压力面追问 (深挖)

  1. 面试官: “你刚才提到了 NIO 的零拷贝 (Zero Copy),能详细讲讲是什么吗?在 Java 里怎么实现?”

    • 答案

      • 传统 IO:数据从磁盘读到网卡,需要经过 4 次拷贝(磁盘 -> 内核 Buffer -> 用户 Buffer -> Socket Buffer -> 网卡)。CPU 很累。

      • 零拷贝:减少拷贝次数。

      • 实现方式

        1. mmap (内存映射):通过 FileChannel.map(),将文件直接映射到内存,减少一次内核到用户的拷贝。

        2. sendfile:通过 FileChannel.transferTo(),数据直接从文件通道传到 Socket 通道,完全不经过用户态,速度极快。

      • 场景:Kafka 高吞吐量的秘诀就是用了 sendfile

  2. 面试官: “Netty 是基于 NIO 的,为什么它不直接用 JDK 原生的 NIO,而要自己封装一套?”

    • 答案

      1. API 繁琐:JDK 原生 NIO 的 API 极其难用,容易写出 Bug。

      2. Epoll Bug:JDK 早期版本的 NIO 有著名的 空轮询 Bug(CPU 100%),Netty 巧妙地规避了这个问题。

      3. 功能增强:Netty 提供了更强大的 ByteBuf(池化、零拷贝支持)和完善的线程模型(Reactor 模型)。

  3. 面试官: “BIO、NIO、AIO 到底分别适用于什么场景?”

    • 答案

      • BIO:连接数少且固定(如公司内部的小系统)。

      • NIO:连接数多且连接比较短(轻操作),如聊天服务器、弹幕系统、Netty、Tomcat

      • AIO:连接数多且连接比较长(重操作),如相册服务器(传大文件)。但在 Linux 下优势不明显。


主题:线程基础、线程池与锁机制

1. 直击要点 (TL;DR)

核心定义

  • 线程创建:不要只知道 new Thread。要掌握 Runnable (无返回值) vs Callable (有返回值)。

  • 线程池生产环境禁止显式创建线程,必须用线程池。核心是 7 大参数。

  • 锁机制synchronized (JVM 层面的锁,会自动升级) vs ReentrantLock (API 层面的锁,基于 AQS)。

  • JMMvolatile 保证可见性有序性,但不保证原子性

2. 详细拆解

第一层:线程的创建与生命周期

  1. 创建方式

    • 继承 Thread (不推荐,Java 单继承局限)。

    • 实现 Runnable (推荐,解耦)。

    • 实现 Callable + FutureTask (面试重点):能拿到返回值,能抛出异常

  2. 6 大生命周期 (State)

    • NEW (新建)。

    • RUNNABLE (运行中):包含操作系统层面的 Running 和 Ready。

    • BLOCKED (阻塞):专指等待 synchronized 锁

    • WAITING (等待):死等,需要被 notify (如 wait(), join())。

    • TIMED_WAITING (超时等待):过时不候 (如 sleep(1000)).

    • TERMINATED (终止)。

第二层:线程池 (ThreadPoolExecutor) —— 必考中的必考

面试官会问:“线程池有哪几个参数?如果队列满了会发生什么?”

核心:7 大参数

  1. corePoolSize:核心线程数(正式工)。就算没事干,这些线程也养着。

    1. maximumPoolSize:最大线程数(正式工 + 临时工)。

    2. keepAliveTime:临时工的空闲存活时间。没人来办事,临时工就解雇。

    3. unit:时间单位。

    4. workQueue:任务队列(候客区)。

    5. threadFactory:线程工厂(负责给线程起名字)。

    6. handler拒绝策略(忙不过来了怎么办)。

强制举例 (生动类比)

银行网点办理业务

  • 核心线程 (2个):银行常开的 2 个柜台。哪怕没人排队,柜员也在那坐着。

  • 任务队列 (3个):里面的 3 把等待椅子。核心柜台忙时,新来的人坐椅子上等。

  • 最大线程 (5个):椅子也坐满了!大堂经理赶紧把另外 3 个备用窗口打开(招聘临时工)。现在 5 个窗口火力全开。

  • 拒绝策略:窗口全开,椅子全满。又来了一个人。经理直接说:“今天不办号了,你回去吧”(AbortPolicy),或者“你去那个角落自己填单子,别烦我”(CallerRunsPolicy)。

第三层:锁机制与 JMM (底层原理)

  1. synchronized 的锁升级 (Highlights)

    • JDK 1.6 之前是重量级锁。之后引入了偏向锁 →​ 轻量级锁 →​ 重量级锁的升级机制。

    • 偏向锁:只有一个线程访问时,贴个标签,不加锁。

    • 轻量级锁 (CAS):出现竞争,但竞争不激烈(且持有锁时间短),通过自旋(循环)等待,不把线程挂起。

    • 重量级锁:竞争激烈,直接让操作系统挂起线程(切换到内核态,开销大)。

  2. volatile 关键字

    • 作用

      1. 可见性:一个线程改了,其他线程立刻看见(强制刷回主内存,使缓存失效)。

      2. 有序性:禁止指令重排序(通过内存屏障)。

    • 短板不保证原子性i++ 用 volatile 也不安全。

  3. ReentrantLock (基于 AQS)

    • 基于代码实现的锁。

    • 功能比 synchronized 多:可中断 (lockInterruptibly)、支持公平锁 (new ReentrantLock(true))、支持尝试拿锁 (tryLock)。

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Callable

有返回值的线程

"Runnable 没返回值,Callable 有 Future 拿返回值。"

OOM

内存溢出

"Executors.newFixedThreadPool 允许无界队列,容易导致 OOM。"

AbortPolicy

拒绝策略

"默认策略,直接抛异常。还有 CallerRuns (谁调用谁执行)。"

CAS

比较并交换

"乐观锁机制。包含三个值:内存值、预期值、新值。可能有 ABA 问题。"

Monitor

监视器锁

"synchronized 底层依赖 ObjectMonitor 实现。"

AQS

抽象队列同步器

"JUC 的基石。用 int state 标识锁状态,用双向队列存等待线程。"


4. 压力面追问 (深挖)

  1. 面试官: “为什么阿里巴巴手册禁止使用 Executors 去创建线程池,而要让你手动 new ThreadPoolExecutor?”

    • 答案

      • FixedThreadPoolSingleThreadPool 的队列是 LinkedBlockingQueue,长度是 Integer.MAX_VALUE (无界)。如果任务处理不过来,队列会无限膨胀,导致 OOM (Out Of Memory)

      • CachedThreadPool 的最大线程数是 Integer.MAX_VALUE。如果并发极高,会创建几万个线程,导致 CPU 卡死

      • 结论:必须手动传参,明确限制队列大小和线程数,对系统负责。

  2. 面试官:synchronizedReentrantLock 有什么区别?你是怎么选择的?”

    • 答案

      • 用法:Sync 是关键字,自动释放;Lock 是类,必须在 finally 中手动 unlock()

      • 功能:Lock 更强大(公平锁、可中断、Condition 分组唤醒)。

      • 选择

        • 一般情况用 Sync(代码简洁,且 JDK一直在优化它)。

        • 如果需要公平锁,或者需要尝试加锁tryLock),或者需要中断等待,必须用 Lock。

  3. 面试官: “怎么合理设置线程池的大小?有什么公式吗?”

    • 答案:看任务类型。

      • CPU 密集型 (计算多):N + 1 (N 是 CPU 核数)。因为 CPU 一直在忙,线程多了也是切上下文浪费时间。

      • IO 密集型 (读写库、网络多):2N 甚至更多 (如 N / (1 - 阻塞系数))。因为线程大部分时间在等 IO,CPU 空闲,可以多开点线程让 CPU 忙起来。


主题:注解 (Annotation) —— 代码的“便利贴”

1. 直击要点 (TL;DR)

核心定义:注解本身没有任何逻辑,它只是元数据 (Metadata),也就是贴在代码上的“标签”或“便利贴”。

如何生效

  1. 编译器扫描:在编译时检查(如 @Override)或生成代码(如 Lombok)。

  2. 运行期反射:框架(Spring/MyBatis)在运行时通过反射读取标签,并执行相应的业务逻辑(如注入依赖、开启事务)。

2. 详细拆解

第一层:基础概念 (元注解)

要自定义注解,首先要了解“元注解” (Meta-Annotation),也就是“注解的注解”。

面试必考这两个:

  1. @Target:贴在哪?

    • ElementType.TYPE (类/接口), METHOD (方法), FIELD (字段)。

  2. @Retention:活多久?(生命周期,最重要)

    • SOURCE: 源码级。编译后丢弃。

    • CLASS: 字节码级。编译在 .class 里,但 JVM 加载时丢弃(默认值)。

    • RUNTIME: 运行时级。JVM 加载后依然存在,反射可见。(Spring 的注解几乎都是这个)。

第二层:生命周期与底层原理

  • Source (源码期)

    • 例子@Override,Lombok 的 @Data

    • 作用:给编译器看的。Lombok 利用 APT (Annotation Processing Tool) 在编译时修改抽象语法树 (AST),自动“种”入 Getter/Setter 代码。

  • Runtime (运行期)

    • 例子@Autowired, @RequestMapping

    • 作用:给 JVM (框架) 看的。

    • 原理反射 (Reflection)。JVM 会解析这些注解,创建一个实现了该注解接口的动态代理对象

强制举例 (生动类比)

商品标签的生命周期

  • SOURCE (便利贴):程序员写代码时贴的草稿纸(如 @Override)。检查完代码(编译)后,这张纸就被撕了扔进垃圾桶,成品里没有。

  • CLASS (吊牌):衣服出厂时挂的吊牌。它在衣服(.class文件)上,但你穿衣服(JVM 运行)的时候,通常会把它剪掉。

  • RUNTIME (水洗标):缝在衣服里的水洗标。无论你什么时候穿(运行),它都在。洗衣店老板(Spring 容器) 需要翻开衣服,看一眼水洗标(反射读取),才知道这件衣服要“干洗”还是要“水洗”(注入 Bean 还是开启事务)。

第三层:自定义注解实战 (亮点)

面试官可能会让你口述一个自定义注解的场景。

  • 场景:操作日志记录 (@Log) 或 权限校验 (@Auth)。

  • 实现步骤

    1. 定义 @Log 注解 (@Retention(RUNTIME), @Target(METHOD)).

    2. 写一个 AOP 切面 (Aspect)

    3. 在切面里用 @Pointcut 拦截所有贴了 @Log 的方法。

    4. @Around 环绕通知里,通过反射获取注解上的参数(如模块名),记录日志。

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Metadata

元数据

"注解是解释代码的代码,是被动的数据。"

Reflection

反射

"Runtime 注解必须配合反射机制才能生效。"

APT

注解处理工具

"Lombok 的原理,在编译期修改语法树,生成字节码。"

@Retention

保留策略

"绝大多数业务注解必须是 RUNTIME,否则反射读不到。"

Dynamic Proxy

动态代理

"JVM 在运行时为注解生成了代理对象。"


4. 压力面追问 (深挖)

  1. 面试官: “Lombok 的 @Data 注解,和 Spring 的 @Autowired 注解,原理一样吗?”

    • 答案完全不同

    • Lombok (SOURCE):利用 JSR 269 插件化注解处理 API (APT)。在 javac 编译期,它偷偷修改了代码的 AST(抽象语法树),把 get/set 方法直接“写”进了 .class 文件里。运行时不需要 Lombok 的包。

    • Spring (RUNTIME):利用 反射机制。在程序启动时,Spring 扫描类,发现有注解,就通过反射处理依赖注入。

  2. 面试官: “如果我定义一个注解,没写 @Retention,默认是什么?”

    • 答案:默认是 CLASS

    • 陷阱:这意味着你写的注解在源码里有,编译后 .class 里也有,但是在代码运行起来后,反射是读不到的! 这是一个常见的初学者 Bug。所以做业务注解一定要手动写 @Retention(RetentionPolicy.RUNTIME)

  3. 面试官: “注解可以是继承的吗?比如父类贴了注解,子类能读到吗?”

    • 答案

      • 默认不能

      • 如果在定义注解时加上了 @Inherited 元注解,那么子类可以继承父类类级别的注解。

      • 注意:接口上的注解、方法上的注解,永远无法被继承。Spring 里的 @Transactional 建议写在实现类上而不是接口上,就是为了避免某些动态代理场景下注解失效。


主题:反射 (Reflection) —— 框架的灵魂

1. 直击要点 (TL;DR)

核心定义:反射机制允许程序在运行时 (Runtime) 自行解析类的结构(方法、属性、构造器),并能动态地创建对象、调用方法、修改属性。

一句话总结“正射”是把类 new 出来使用;“反射”是把类解剖开来看。

核心价值:它是所有 Java 框架的基石(Spring、MyBatis、Tomcat)。框架本身不知道你将来会写什么类,只有通过反射,才能在运行时加载你的类并管理它们。

2. 详细拆解

第一层:什么是“正”?什么是“反”?

  • 正向 (Static)

    • 代码:User user = new User(); user.sayHello();

    • 特点:编译时必须知道 User 类存在,否则编译报错。

    • 类比照着菜单点菜。菜单上有什么(编译期确定),你就只能点什么。

  • 反向 (Dynamic)

    • 代码:Class clz = Class.forName("com.demo.User"); Method m = clz.getMethod("sayHello");

    • 特点:编译时可以不知道这个类叫什么,字符串变量传进来啥就是啥。

    • 类比进厨房自己做。你不看菜单,直接闯进后厨(运行时),翻看食材库(Class 对象),想怎么搭配就怎么搭配。

第二层:核心三要素 (Class, Field, Method)

反射的操作起点永远是 Class 对象

JVM 加载完一个 .class 文件后,会在堆内存产生一个 Class 类型的对象,它包含了这个类的所有元信息。

  1. 获取 Class 对象的三种方式

    • User.class (最快,编译时确定)。

    • user.getClass() (运行时,得先有对象)。

    • Class.forName("com.xxx.User") (最常用,Spring 配置全靠它,解耦最强)。

  2. 暴力反射 (setAccessible)

    • 反射最“流氓”的地方在于,它可以无视 private 修饰符。

    • 只要调用 field.setAccessible(true),私有属性也能读写,私有方法也能调用。

    • 生动类比配钥匙 vs 砸窗户

      • 正常访问 (public) 像是拿着钥匙开门。

      • 暴力反射 (setAccessible) 像是直接把窗户砸了跳进去。虽然能进屋,但破坏了封装性(窗户破了),而且动作慢(性能差)。

第三层:亮点/加分项 (性能与应用)

  • 为什么反射慢?

    1. 安全检查:每次反射调用,JVM 都要检查权限(能不能访问这个类?)。

    2. 无法内联优化:JIT 编译器没法对反射代码做深度优化(如方法内联)。

    3. 包装开销:基本类型(int)需要自动装箱成对象(Integer)。

  • 如何优化?

    • 缓存:把获取到的 MethodField 对象缓存起来,不要每次都 Class.forName

    • ReflectASM:使用字节码操作库(如 ASM)直接生成调用代码,绕过反射。

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Class Object

类对象

"反射的入口,包含了类的所有元数据信息。"

Runtime

运行时

"反射让 Java 具有了动态性,可以在运行期加载编译期未知的类。"

setAccessible

暴力访问

"可以访问 private 成员,但会破坏封装性并降低性能。"

Frameworks

框架基石

"Spring 的 Bean 实例化、AOP 动态代理全是基于反射实现的。"

Inspection

自省/内省

"反射的主要用途是查看类内部结构 (Inspect)。"


4. 压力面追问 (深挖)

  1. 面试官:Class.forName("com.mysql.jdbc.Driver") 这行代码经常看到,它除了获取 Class 对象,还干了什么?”

    • 答案:它会触发类的初始化 (Initialization),也就是执行 static 静态代码块。

    • 原理:MySQL 驱动在静态代码块里把自己注册到了 DriverManager 里。如果只用 .class,是不会触发静态块的。

  2. 面试官: “反射能修改 final 修饰的字段吗?”

    • 答案大部分情况下可以,但有坑

    • 原理:通过 field.setAccessible(true) 可以去除 final 的限制并修改值。

    • :如果那个 final 字段在编译期就被优化成常量了(比如 final String s = "hello"),那你反射改了也没用,其他代码读取时依然会直接读取常量池里的 "hello"(编译器内联了)。

  3. 面试官: “既然反射这么慢,为什么 Spring 这种高性能框架还大规模使用?”

    • 答案

      1. 启动慢,运行快:Spring 主要在容器启动时狂用反射(扫描 Bean、创建对象),一旦启动完成,Bean 都放在单例池里了,业务运行时直接拿,不需要再反射。

      2. 字节码增强:对于高频调用的地方(如 AOP),Spring (CGLIB) 实际上是通过生成新的 .class 字节码来运行的,而不是纯反射调用,性能接近原生。


代码实战:反射的标准套路

Java

1

public void testReflection() throws Exception {

2

    // 1. 获取 Class 对象 (模拟 Spring 读取配置文件)

3

    String className = "com.example.User"; 

4

    Class<?> clz = Class.forName(className);

5

6

    // 2. 创建实例 (模拟 Spring 创建 Bean)

7

    // JDK 9 之后推荐用 getConstructor().newInstance()

8

    Object user = clz.getDeclaredConstructor().newInstance();

9

10

    // 3. 获取方法 (模拟调用 controller 方法)

11

    Method method = clz.getMethod("sayHello", String.class);

12

13

    // 4. 调用方法

14

    // invoke(对象, 参数)

15

    method.invoke(user, "Interview");

16

    

17

    // 5. 暴力破解私有属性

18

    Field ageField = clz.getDeclaredField("age");

19

    ageField.setAccessible(true); // 🔓 砸窗户

20

    ageField.set(user, 18);

21

}

主题:Java 8 新特性

1. 直击要点 (TL;DR)

核心变革:引入了 函数式编程 (Functional Programming)

四大支柱

  1. Lambda 表达式:让代码更简洁,把“行为”当作参数传递。

  2. Stream API:像处理 SQL 一样处理集合数据(链式调用)。

  3. Optional:优雅地解决 NullPointerException (NPE)。

  4. 新日期时间 API (java.time):解决了旧版 Date 线程不安全和设计混乱的问题。


2. 详细拆解

第一点:Lambda 表达式 (函数式编程)

  • 痛点:以前为了写一个回调(比如线程启动),必须 new Runnable() 写一大坨匿名内部类。

  • 解决() -> {}。只关注参数执行体,省略画蛇添足的样板代码。

  • 底层:依赖 函数式接口 (Functional Interface),即只有一个抽象方法的接口(如 Runnable, Callable, Comparator)。

  • 场景:启动一个线程,或者给列表排序。

    核心变化:省去了笨重的匿名内部类语法,只保留参数核心逻辑

    • ❌ Old Way (Java 7 匿名内部类)

      Java

      1

      new Thread(new Runnable() {

      2

          @Override

      3

          public void run() {

      4

              System.out.println("线程启动了");

      5

          }

      6

      }).start();
    • ✅ New Way (Java 8 Lambda)

      Java

      1

      // 只有核心逻辑:() 代表无参,-> 代表去执行,{} 代表逻辑

      2

      new Thread(() -> System.out.println("线程启动了")).start();

      3

      4

      // 排序更是经典:(a, b) 代表两个元素,-> 后面是比较逻辑

      5

      list.sort((a, b) -> a - b); 

第二点:Stream API (流式处理) —— 面试最爱

  • 痛点:以前要筛选 List 里大于 10 的数并排序,要写好几个 for 循环和 if,代码又臭又长。

  • 解决:一套流水线操作。filter (过滤) →​ map (转换) →​ sorted (排序) →​ collect (装箱)。

  • 特性惰性求值 (Lazy Evaluation)。只有调用了终端操作(如 collect, count),前面的中间操作(filter)才会真正执行。

  • 场景:从一堆用户里,找出“年龄大于 20 岁”的用户的“名字”,并“排序”。

    核心变化:从“命令式编程”(怎么做)变成了“声明式编程”(做什么)。

    • ❌ Old Way (For 循环地狱)

      Java

      1

      List<String> names = new ArrayList<>();

      2

      for (User u : users) {

      3

          if (u.getAge() > 20) { // 1. 筛选

      4

              names.add(u.getName()); // 2. 提取名字

      5

          }

      6

      }

      7

      Collections.sort(names); // 3. 排序
    • ✅ New Way (Stream 流水线)

      Java

      1

      List<String> names = users.stream()

      2

          .filter(u -> u.getAge() > 20)  // 1. 筛选:只要 > 20 的

      3

          .map(User::getName)            // 2. 映射:把 User 对象变成 String 名字

      4

          .sorted()                      // 3. 排序

      5

          .collect(Collectors.toList()); // 4. 装箱:结果收集回 List

第三点:Optional (空指针杀手)

  • 痛点if (user != null && user.getAddress() != null) 这种判空代码写得让人想吐。

  • 解决:给对象穿上一层“防弹衣”

  • 常用方法

    • ofNullable(): 创建,允许为空。

    • orElse(): 如果为空,给个备胎。

    • map(): 如果不为空,转换一下。

  • 场景:获取用户的家庭住址的城市。用户可能为空,地址可能为空,城市也可能为空。

    核心变化:消灭了嵌套的 if (null) 判断。

    • ❌ Old Way (嵌套判空)

      Java

      String city = "Unknown";
      if (user != null) {
          Address address = user.getAddress();
          if (address != null) {
              String c = address.getCity();
              if (c != null) {
                  city = c;
              }
          }
      }
    • ✅ New Way (Optional 链式调用)

      Java

      String city = Optional.ofNullable(user) // 1. 包装:不管 user 是不是 null,先包起来
          .map(User::getAddress)              // 2. 拆包取地址,如果 user 是 null,这里直接跳过
          .map(Address::getCity)              // 3. 拆包取城市
          .orElse("Unknown");                 // 4. 兜底:如果中间任何一步断了(是null),返回默认值

第四点:新日期时间 API (LocalDate/LocalDateTime)

  • 痛点

    1. java.util.Date可变的(Mutable),多线程不安全。

    2. 月份是从 0 开始的(0 是 1 月),设计反人类。

  • 解决java.time 包下的 LocalDate, LocalTime, LocalDateTime

    • 不可变:类似 String,修改时间会返回一个新对象,线程绝对安全

    • 设计清晰:月份从 1 开始。

  • 场景:获取“明天”的日期,并格式化输出。

    核心变化:不可变对象,逻辑清晰,线程安全。

    • ❌ Old Way (Date/Calendar)

      Java

      Calendar cal = Calendar.getInstance();
      cal.add(Calendar.DATE, 1); // 这里的 1 是啥?很容易传错参数
      Date tomorrow = cal.getTime();
      // SimpleDateFormat 线程不安全,必须每次 new 或者加锁
      System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(tomorrow));
    • ✅ New Way (LocalDate)

      Java

      // 语义非常清晰:当前日期 -> 加一天
      LocalDate tomorrow = LocalDate.now().plusDays(1);
      // DateTimeFormatter 是线程安全的,定义成 static 常量也没事
      System.out.println(tomorrow.format(DateTimeFormatter.ISO_DATE));

3. 核心关键词总结 (复习速记)

关键词

核心含义

面试复述要点

Functional Interface

函数式接口

"只有一个抽象方法的接口,Lambda 的基石。如 Predicate, Consumer, Function。"

Lazy Evaluation

惰性求值

"Stream 的中间操作不会立即执行,只有终端操作才会触发。"

Parallel Stream

并行流

"利用 ForkJoinPool 并行处理数据,但要注意线程安全问题。"

Immutable

不可变性

"新日期类是不可变的,解决了旧 Date 的并发安全问题。"

Default Method

默认方法

"接口里可以写实现体了。为了让 List 能直接调用 stream() 而不破坏旧代码。"


4. 压力面追问 (深挖)

  1. 面试官:Stream 并行流 (parallelStream) 是怎么实现的?它一定比普通流快吗?”

    • 答案

      • 原理:基于 ForkJoinPool 框架,把大任务拆成小任务多线程执行。

      • 不一定快

        1. 如果数据量小,线程切换开销反而比单线程慢。

        2. 如果涉及装箱/拆箱操作,性能损耗大。

        3. 大坑:它是全进程共享同一个 ForkJoinPool 的,如果你的任务里有 IO 阻塞,会拖慢整个系统的并行流。

  2. 面试官: “Lambda 表达式里使用的外部变量有什么限制?”

    • 答案:必须是 finaleffectively final (事实上的 final)。

    • 原因变量捕获。Lambda 运行可能在另一个线程或另一个时间点,它拷贝了变量的而不是引用。如果变量后来变了,数据就不一致了,所以 Java 强制禁止修改。

  3. 面试官: “接口有了 default 方法,那接口和抽象类还有区别吗?”

    • 答案有本质区别

    • 状态:抽象类可以有成员变量(保存状态),接口不能(只能有 static final 常量)。

    • 继承:类只能单继承抽象类,但可以多实现接口。default 方法主要是为了向后兼容(API 演进),而不是为了替代抽象类。

  4. 面试官:mapflatMap 有什么区别?给我举个例子。”

    • 答案

      • map (1对1):输入一个对象,输出一个对象。

        • 例子Stream<String> (一堆单词) -> map(s -> s.length()) -> Stream<Integer> (一堆长度)。

      • flatMap (1对多/扁平化):输入一个对象,输出一个流(Stream),最后把所有流合并成一个大流。

        • 例子Stream<List<String>> (几个班级的名单) -> flatMap(List::stream) -> Stream<String> (全校所有学生的名单放在一起)。

  5. 面试官: “在使用 Stream 进行 list 转换 map 时 (Collectors.toMap),如果 Key 重复了会怎么样?”

    • 答案会直接报错 Duplicate key ...

    • 解决:必须指定第三个参数(合并函数)

    • 代码Collectors.toMap(User::getId, User::getName, (oldValue, newValue) -> newValue) (如果 key 重复,用新值覆盖旧值)。

1

评论区