Java面试复习 Day 1

Java面试复习 Day 1

一、大纲

分类必考题
JVM内存分区/垃圾回收算法/GC调优思路/类加载机制
并发线程池7参数及工作流程/synchronized与ReentrantLock区别/volatile可见性
集合HashMap 1.7/1.8区别及put流程/ConcurrentHashMap分段锁->CAS/ArrayList扩容机制
MySQL索引数据结构(B+树)/最左匹配/覆盖索引/事务隔离级别/MVCC
Redis5种数据结构/缓存穿透击穿雪崩/持久化RDB+AOF/过期删除
SpringIOC容器启动流程/AOP代理方式/事务传播机制
基础==和equals/重载重写/接口抽象类/String immutable

二、基础

1.==和equals

==是运算符,基本类型比值、引用类型比地址;equals()是方法,默认比地址但常用类已重写比内容。

基本类型:基本数据类型(int、char、boolean等)只能用==比较,直接比两个变量的实际值是否相等,equals()无法用于基本类型。

对象:引用类型用==比较的是两个引用是否指向堆内存中同一个对象(即内存地址是否相同)。

默认和重写:equals()默认继承自Object类,默认行为与==相同(比地址),但String、Integer等常用类已重写equals(),此时比较的是对象内容是否相等。

equals重写:标准步骤

必须遵循的五大契约

重要注意事项:同时重写hashCode()

规则‌:如果两个对象通过equals()判定为相等,那么它们的hashCode()方法必须返回相同的整数值。

  • 原因‌:Java 中的哈希集合(如HashMap,HashSet)首先通过hashCode()确定对象存储的位置(桶),然后再通过equals()确认对象是否真正相等。如果equals()相等但hashCode()不同,集合将无法找到已存在的对象,导致数据重复或查找失败。
  • 建议‌:在使用 IDE(如 IntelliJ IDEA 或 Eclipse)时,通常可以一键生成配套的equals()hashCode()方法,以确保逻辑的一致性。

常见误区

  • 不要只比较部分字段‌:如果两个对象被视为“相等”,它们的所有关键业务字段都应该参与比较。
  • 慎用instanceof‌:在存在继承关系时,如果使用instanceof进行类型检查,可能会破坏对称性。例如,父类对象equals子类对象可能为true,但子类对象equals父类对象可能为false(如果子类增加了新字段)。使用getClass()可以避免这个问题,但意味着子类对象永远不等于父类对象,即使父类字段完全相同。具体选择取决于业务需求。
  • 基本类型不能用equals‌:int,double等基本数据类型没有equals方法,必须使用==。如果需要比较包装类(如Integer),可以使用Objects.equals()来安全地处理null

2.重载重写

方法的重载(Overloading)和重写(Overriding)是面向对象编程中的两个核心概念,它们允许开发者根据不同的需求和场景使用相同的方法名,但两者间存在一些区别。

方法的重载:重载是指在同一个类中可以存在多个同名方法,只要它们的参数列表不同(参数的类型、数量或者顺序不同)。返回值类型不是区分重载方法的依据。

方法的重写:重写(也称为覆盖)指的是在子类中可以定义一个与父类中具有相同方法名和参数列表的方法。子类方法会覆盖父类中的方法。重写时,方法的返回类型、方法名和参数列表必须完全相同。访问修饰符的限制也更严格,子类方法不能有更严格的访问权限。

总结:

  • 重载允许你创建多个同名方法,只有它们的参数列表不同。
  • 重写允许你提供一个方法的新实现,该方法在子类中替换父类中的方法。
  • 重载是编译时多态性(静态多态性),重写是运行时多态性(动态多态性)。
  • 重载使用相同的类内部,重写在继承体系中从父类到子类之间进行。

3.接口抽象类

在Java中,接口(Interface)和抽象类(Abstract Class)都是用于定义方法和属性的模板,但是它们在设计和使用上有一些关键的区别。

接口(Interface)

1. 定义:接口是完全抽象的,它只包含抽象方法(即没有方法体的方法)和常量(使用`public static final`修饰的字段,默认即是如此)。

2. 实现:一个类可以实现多个接口。

3. 访问修饰符:接口中的方法默认是`public`的,而变量默认是`public static final`的。

4. 示例:

public interface Animal { void eat(); void sleep(); }

5. 实现接口:

public class Dog implements Animal { public void eat() { System.out.println("Dog is eating"); } public void sleep() { System.out.println("Dog is sleeping"); } }

抽象类(Abstract Class)

1. 定义:抽象类可以包含抽象方法(没有方法体的方法)和具体方法(有方法体的方法)。它还可以包含字段、构造器等。

2. 继承:一个类只能继承一个抽象类。

3. 访问修饰符:方法和字段可以有不同的访问修饰符。

4. 示例:

public abstract class Animal { public abstract void eat(); public void sleep() { System.out.println("Animal is sleeping"); } }

5. 继承抽象类:

public class Dog extends Animal { public void eat() { System.out.println("Dog is eating"); } }

区别总结

- 继承:一个类只能继承一个抽象类,但可以实现多个接口。
- 方法实现:接口只包含抽象方法,抽象类可以包含抽象方法和具体方法。
- 字段:接口中的字段默认是`public static final`,而抽象类可以包含任意类型的字段。
- 设计用途:接口主要用于定义一套方法规范,让不同的类去实现;抽象类主要用于代码复用,提供一个部分实现的基类。
- 访问修饰符:接口中的方法默认是`public`,而抽象类中的方法和字段可以有不同的访问级别。

选择使用接口还是抽象类?

- 当需要定义一套规范,让多个类去实现时,使用接口。
- 当需要定义一个基类,让其他类去继承并可能添加一些非抽象的方法实现时,使用抽象类。例如,当你有一个通用行为和一些通用属性,但又希望某些特定行为由子类实现时,可以使用抽象类。

4.String immutable

4.1含义

Java `String` 的不可变性(Immutable)指对象创建后,其内部字符序列无法被修改。任何看似“修改”的操作(如拼接、替换)实际都是返回新对象,原对象保持不变 。

核心实现机制
- `final` 类:`String` 被声明为 `final`,禁止继承,防止子类破坏不可变契约 。
- `final` 字段:底层存储数组(JDK 8 前为 `char[]`,JDK 9+ 为 `byte[]`)被 `private final` 修饰,引用地址不可变且未暴露修改方法 。
- 防御性拷贝:构造函数接收外部数组时会创建副本,切断外部引用对内部数据的修改路径 。
- 无 Setter 方法:类中不提供任何能改变内部状态的方法,所有操作均返回新实例 。

设计不可变的核心优势
- 安全性:作为网络参数、文件路径、数据库连接串等敏感信息的载体,不可变防止数据在传递中被恶意篡改,保障类加载安全 。
- 线程安全:多线程并发访问无需同步锁,天然避免竞态条件,提升并发性能 。
- 支持字符串常量池:不可变是常量池存在的前提,相同字面量共享同一实例,大幅节省内存并优化 `==` 比较逻辑 。
- 哈希值缓存:`hashCode()` 结果首次计算后缓存于 `private int hash` 字段,因内容不变,后续直接返回缓存值,显著提升 `HashMap`/`HashSet` 作为 Key 的性能 。

常见误区澄清
- 变量指向可变 ≠ 对象可变:`s = s + "x"` 是让引用 `s` 指向新对象,原 `"abc"` 对象并未改变 。
- 反射可突破限制:虽可通过反射修改 `value` 数组内容,但这违反封装原则,官方 API 不提供此类接口,开发中应视为不可变 。
- 频繁拼接需换工具:循环中用 `+` 拼接会生成大量临时对象,应改用 `StringBuilder`(非线程安全)或 `StringBuffer`(线程安全) 。

4.2.Java中String为什么不可变

Java 中String被设计为‌不可变(Immutable)‌,是 Java 语言在‌安全性、性能优化、线程安全‌以及‌系统设计一致性‌之间做出的核心权衡。这种设计并非偶然,而是为了支撑 JVM 底层机制和上层应用的高效运行。

4.3.String 设计为不可变的四大核心原因:

1. 支持字符串常量池(String Pool),节省内存

这是 String 不可变最直接的收益。JVM 在堆内存中开辟了一块特殊区域称为‌字符串常量池‌。

  • 机制‌:当使用字面量创建字符串(如String s = "abc";)时,JVM 会先检查常量池中是否已存在该字符串。如果存在,直接返回引用;如果不存在,则创建新对象并放入池中。
  • 为什么必须不可变‌:如果 String 是可变的,多个变量指向同一个常量池对象时,其中一个变量修改了内容,会导致其他所有指向该对象的变量值发生“意外”改变,造成数据污染和逻辑错误。
  • 收益‌:大量重复的字符串(如配置项、状态码)只需存储一份,极大节省了堆内存空间。

2. 安全性保障(Security)

String 在 Java 中被广泛用于存储敏感信息和关键参数,不可变性是防止恶意篡改的基础。

  • 敏感数据保护‌:用户名、密码、数据库连接 URL、网络请求参数等通常以 String 形式传递。如果 String 可变,黑客或恶意代码可能在参数传递过程中修改其内容(例如将指向合法服务器的 URL 改为恶意服务器),导致严重的安全漏洞。
  • 类加载安全‌:JVM 的类加载器通过字符串全限定名(如"java.lang.String")来定位和加载类文件。如果类名字符串可被修改,可能导致加载错误的类,破坏双亲委派模型和沙箱安全机制。
  • 反射与网络操作‌:许多底层 API(如打开文件、网络连接)接收 String 参数。不可变性确保了方法在执行前和执行期间,参数值保持一致,防止竞态条件下的参数篡改。

3. 天然的线程安全(Thread Safety)

  • 无锁并发‌:不可变对象的状态在创建后无法改变。因此,多个线程可以同时读取同一个 String 实例,而无需担心数据不一致或竞态条件。
  • 简化开发‌: 在多线程环境下共享 String 对象时,不需要额外的同步措施(如synchronized或锁),降低了并发编程的复杂度和性能开销。

4. 哈希值缓存,提升集合性能

String 是HashMapHashSetHashTable等哈希集合中最常用的 Key。

  • 哈希缓存机制‌:String 类内部有一个private int hash字段。由于内容不可变,hashCode()在首次计算后会将结果缓存起来。后续调用直接返回缓存值,无需重新遍历字符数组计算。
  • 保证集合稳定性‌:如果 String 可变,作为 Key 存入 HashMap 后,若内容被修改,其hashCode也会变化,导致无法在原桶位置找到该对象,造成“数据丢失”(实际上是被映射到了错误的桶中)。不可变性彻底杜绝了这一问题。

4.4.String不可变的实现原理

Java 通过以下三重机制在代码层面强制保证了不可性:

1‌.final类声明‌:

public final class String ...

String类被声明为final,禁止被继承。这防止了子类通过重写方法来破坏不可变契约。

2.private final底层存储数组‌:

  • JDK 8 及之前‌:使用private final char value[]存储字符。
  • JDK 9 及之后‌:优化为private final byte[] value配合编码标识符,进一步节省内存。
  • final修饰保证了数组的‌引用地址‌不可变;private修饰保证了外部无法直接访问该数组。

3.防御性拷贝与无 Setter 方法‌:

  • String 类不提供任何修改内部value数组的方法(如setCharAt)。
  • 所有看似“修改”的方法(如substring,concat,replace)实际上都创建了‌新的 String 对象‌并返回,原对象保持不变。
  • 在构造函数中,如果传入的是外部数组,String 会进行‌防御性拷贝‌(Copy),切断外部引用对内部数据的潜在修改路径。

注意:虽然通过 Java 反射机制可以强行修改String内部的value数组,但这违反了封装原则和安全规范,在实际开发中应严格避免。从 API 设计和语言规范角度看,String 就是不可变的。

4.5.总结

String 的不可变性是以‌牺牲少量创建新对象的开销‌为代价,换取了‌内存效率(常量池)、执行速度(Hash缓存)、并发安全(无锁)和系统稳健性(防篡改)‌的巨大收益。在Effective Java 等经典著作建议:“‌只要可能,就使用不可变对象‌”。