Java 基础相关

1 泛型是什么?为什么要用泛型,什么是类型擦除

1.1 泛型是什么?

泛型(Generics)是 JDK 1.5 引入的特性,允许在定义类、接口、方法时使用类型参数,在创建实例或调用时传入具体类型。

示例:List<String> list = new ArrayList(); 表示该列表只能存放 String 类型元素。

1.2 为什么要用泛型?
  • 类型安全: 编译期检查类型,避免运行时 ClassCastException;
  • 消除强制转换: 避免频繁的显式类型转换,代码更简洁;
  • 代码复用: 通过类型参数编写通用算法(如集合框架),一套代码适配多种类型;
1.3 什么是类型擦除?
  • 定义:Java 泛型在编译期将类型参数移除,替换为限定类型(若无限定则为 Object),并在必要时插入强制转换。这是 Java 泛型实现的一种折中,为了兼容 JDK 1.4 及之前的代码;
  • 过程:
    • 将泛型类型替换为原始类型(List<String> → List);
    • 在边界处插入强制类型转换(如从 List.get() 取出后自动强转为 String);
    • 生成桥接方法以保持多态;
  • 后果:
    • 运行时无法获取泛型的具体类型(如 list instanceof List<String> 非法)
    • 无法通过类型参数创建数组(T[] arr = new T[10]; 不允许);
    • 无法使用基本类型作为类型参数(需使用包装类);
1.4 总结
  • 泛型:编译期类型约束,保证类型安全、避免强转;
  • 类型擦除:JVM 没有泛型,编译后泛型被擦除为 Object/上限类型,是泛型底层实现原理;

2 谈谈你对 Java 泛型中类型擦除的理解,并说说其局限性

2.1 理解类型擦除
  • 定义:Java 泛型仅在编译期存在,编译后泛型类型信息被移除(擦除),替换为原始类型,导致运行时丢失类型信息。 这是 Java 为实现泛型且保持与旧版本(JDK 1.4 及之前)二进制兼容而采用的机制;
  • 擦除规则:
    • 无限定类型参数(如 <T>)替换为 Object;
    • 有限定类型参数(如 <T extends Number>)替换为第一个边界类型(此处为 Number);
    • 在访问泛型对象时,编译器自动插入必要的强制类型转换;
    • 为保持多态,编译器可能生成桥接方法(bridge method);
2.2 局限性(运行时限制)
  • 无法获取运行时具体类型: List<String>List<Integer> 在运行时都是 List,无法通过 instanceof、反射获取类型参数;
  • 不能创建泛型数组: T[] arr = new T[10]; 非法,因数组创建时必须知道确切类型(擦除后为 Object,赋值可能引发 ArrayStoreException);
  • 不能使用基本类型作为类型参数,需使用包装类:List<Integer> 而非 List<int>(自动装箱有性能开销);
  • 静态上下文无法引用类型参数: 静态变量、静态方法不能使用类的泛型参数(因为类加载时尚未确定具体类型);
  • 无法重栽擦除后相同签名的方法:
java 复制代码
void print(List<String> list) {}
void print(List<Integer> list) {} // 编译错误,擦除后都是 List

3 什么是反射机制?反射机制的应用场景有哪些?

3.1 什么是反射机制

反射(Reflection): 是 Java 语言的一种动态特性,允许程序在运行时获取任意类的信息(如类名、方法、字段、构造器),并能动态创建对象、调用方法、修改字段值,甚至访问私有成员。

核心类:Class、Method、Field、Constructor。

3.2 反射机制的应用场景
  • 框架开发(最广泛):Spring、MyBatis、Hibernate 等框架利用反射动态创建 Bean、解析注解、注入依赖;
  • 动态代理:JDK 动态代理通过反射在运行时生成代理类,调用目标方法(如 Spring AOP);
  • 注解处理器:运行时读取注解(如 @Override@Autowired),执行相应逻辑;
  • 序列化/反序列化:JSON、XML 等数据与 Java 对象互转时,通过反射读取/设置字段;
  • 动态加载类:如 JDBC 的 Class.forName(com.mysql.jdbc.Driver) 动态加载驱动;
3.3 总结

反射是 Java 运行时动态获取类信息并操作对象的机制,广泛应用于框架、动态代理、注解处理等场景。

4 说说你对 Java 注解的理解

4.1 注解是什么?

注解(Annotation)是 Java 提供的一种元数据机制,用于为代码(类、方法、字段)添加标记或配置信息。它不影响程序的直接运行,但可以被编译器、工具或框架在编译时或运行时读取和处理。

元数据(Metadata):是描述数据的数据,用于提供关于其他数据的附加信息,如数据的结构、含义、来源、格式或约束。它本身不是业务数据,但帮助理解、管理或处理业务数据。

4.2 常见内置注解
4.3 元注解(注解的注解)

用于定义新注解,常见有:

4.4 注解的作用
  • 编译检查:如 @Override 防止拼写错误;
  • 代码生成:如 Lombok 的 @Data@Getter,编译时生成 getter/setter;
  • 运行时处理:通过反射读取 @Retention(RUNTIME) 注解,实现框架功能(Spring、JUnit、MyBatis);
4.5 使用场景
  • 框架配置:Spring 的 @Controller@Service@Autowired
  • 测试:JUnit 的 @Test@Before
  • ORM 映射:Hibernate/MyBatis 的 @Entity@Table@Column
  • 权限控制:自定义 @RequirePermission,结合 AOP 拦截;
4.6 总结
  • 注解是一种元数据形式,通过元注解定义其行为;
  • 内置注解提供编译检查,运行时注解配合反射被框架广泛用于配置、代码生成和测试;

5 重载 (Overload) 与 重写 (Override) 区别

总结:

  • 重载是编译期多态,同一个类中,方法名相同,参数列表不同(类型、个数、顺序);
  • 重写是运行时多态,子类中重新定义父类的方法,方法签名(名称 + 参数)完全相同;

6 接口 (interface) 和 抽象类 (abstract class) 区别

总结:

  • 抽象类描述对象的本质("is-a"),可以有状态和构造器,支持单继承;
  • 接口定义行为契约("can-do"),多实现,默认常量和抽象方法,Java 8 后可有默认/静态方法,但无实例字段;

7 equals 方法 与 ==hashCode() 的区别和使用场景

7.1 ==equals 方法的区别
7.2 equals 方法和 hashCode 方法的约定
  • 一致性:equals 相等,则 hashCode 必须相等;

  • 逆命题不成立: hashCode 相等,equals 不一定相等(哈希冲突);

  • 若不重写:

    • hashCode 默认返回对象内存地址(或随机值),两个逻辑相等的对象大概率哈希码不同;
    • 在哈希表中,put 根据哈希码存入不同桶,get 时无法找到对方,导致集合中出现"重复"元素(逻辑上相同却并存),破坏 Set 唯一性和 Map 键的查找功能;

    在哈希集合(HashMap、HashSet)中,先通过 hashCode 定位桶,再用 equals 比较;若违反约定,会导致集合中出现逻辑重复元素或无法找到元素;

7.3 使用场景
  • ==
    • 判断基本类型数值是否相等;
    • 判断两个引用是否指向同一个对象(如比较 this == obj 是否同一对象);
  • equals
    • 判断两个对象逻辑上是否相等(如 String 内容相等、自定义类的业务主键相同);
    • 集合(List、Set、Map)的 ContainsindexOf 等依赖 equals
  • hashCode
    • 在哈希集合(HashSet、HashMap 的 Key)中作为存储位置依据;
    • 需与 equals 保持一致,否则集合行为异常;
7.4 总结
  • == 比较基本类型值或引用地址;
  • equals 默认比较地址但通常重写为比较内容;
  • hashCode 必须与 equals 一致,以保证哈希集合正常工作;

8 谈谈如何重写 equals 方法?为什么还要重写 hashCode 方法?

**重写 equals 的步骤(规范): **重写 equals 方法需要满足自反性、对称性、传递性、一致性、非空性。

典型实现模版如下:

  • 判断是否同一对象: if(this == obj) return true;(性能优化);
  • 判断类型: 若参数为 null 或类型不匹配,返回 false。通常使用 getClass()instanceof
    • 若子类能改变父亲的相等语义,用 getClass();若子类不影响父类比较(如父类 equals 允许子类),用 instanceof
  • 强制转换: OtherClass other = (OtherClass)obj;
  • 比较关键字段: 使用 == 比较基本类型,Objects.equals 方法比较对象字段(处理 null);

代码示例:

kotlin 复制代码
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Person person = (Person) obj;
    return age == person.age &&
           Objects.equals(name, person.name);
}

9 基本数据类型有哪些?包装类区别

9.1 8 种基本数据类型及其包装类
9.2 基本类型 vs 包装类
9.3 自动装箱与拆箱
  • 装箱:基本类型 → 包装类(Integer i = 10;
  • 拆箱:包装类 → 基本类型(int n = i
  • 编译器自动插入 Integer.valueOf()intValue() 等代码;
9.4 总结
  • 基本类型存值在栈,性能好,无方法;
  • 包装类是对象在堆,可为 null,支持泛型和工具方法;
  • 包装类有缓存(Integer[-128 ~ 127]);

10 自动装箱与拆箱原理

10.1 定义
  • 装箱: 将基本类型自定转换为对应的包装类对象;
    • 例:Integer i = 10 → 编译器调用 Integer.valueOf(10)
  • 拆箱: 将包装类对象自动转换为对应的基本类型;
    • 例:int n = i → 编译器调用 i.intValue()
10.2 底层原理

编译期处理: Java 编译器在生成字节码时,自动插入相应的 valueOf()(装箱)和 xxxValue()(拆箱)方法调用。

java 复制代码
Integer i = 100;       // 装箱 → Integer.valueOf(100)
int n = i;             // 拆箱 → i.intValue()
10.3 缓存机制
  • 某些包装类(Integer、Byte、Short、Long、Character、Boolean)内部维护了缓存池;
  • Integer 缓存范围:-128 ~127;
  • 原理:Integer.valueOf(127) 返回缓存中的对象,多次调用得到同一对象;Integer.valueOf(128),每次都 new 新对象;
  • 注意:缓存只对 valueOf() 有效,new Integer() 始终创建新对象(已废弃);
10.4 常见陷阱
  • ==equals:包装类比较时,== 比较引用(可能受缓存影响),equals 比较值;
  • 空指针异常:包装类为 null 是自动拆箱会抛出 NullPointerException;
  • 循环中装箱:频繁装箱会创建大量对象,影响性能,应优先使用基本类型;
10.5 总结背诵
  • 装箱:基本类型 → 包装类,底层调用 Integer.valueOf()
  • 拆箱:包装类 → 基本类型,底层调用 intValue()
  • 部分包装类使用缓存池(如 Integer -128 ~ 127)优化性能,缓存范围外 new 对象;

11 Java 中深拷贝和浅拷贝的区别

11.1 定义
  • 浅拷贝: 复制对象时,只复制基本类型字段和引用类型的地址(不复制引用对象本身)。新旧对象共享内部引用对象;
  • 深拷贝: 复制对象时,不仅复制基本类型字段,还复制引用类型所指向的对象(递归复制)。新旧对象完全独立;
11.2 区别对比
11.3 如何实现深拷贝
  • 重写 clone() 方法:手动为每个引用类型字段调用其 clone() 方法,且所有相关类需实现 Cloneable 接口;
  • 序列化:将对象写入字节流再读回(需实现 Serivalizable),简单但性能较低;
  • 拷贝工具库:如 Apache Commons Lang 的 SerializationUtils.cone()、Gson 等 JSON 转换后重建;
11.4 示例(Java 浅拷贝 vs 深拷贝)
java 复制代码
// 浅拷贝:clone() 默认行为
Person p1 = new Person("张三");
Person p2 = p1.clone();  // p2 和 p1 共享引用字段

// 深拷贝(序列化方式)
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(p1);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Person p3 = (Person) ois.readObject();
11.5 总结
  • 浅拷贝只复制引用地址,新旧对象共享内部对象;
  • 深拷贝递归复制所有引用对象,新旧对象完全独立;深拷贝可通过重写 clone 或序列化实现;

12 异常体系:Error 和 Exception 区别

Java 异常体系顶层为 Throwable,其下分为 Error 和 Exception。

Exception 细分:

  • 受检异常(Checked Exception): 编译时需处理(throwstry-catch),如 IOException、SQLException;
  • 非受检异常(Unchecked Exception): 运行时异常,继承 RuntimeException,编译不强制处理,如 NullPointerException、IllegalArgumentException;

总结:Error 表示 JVM 内部严重错误,程序无法恢复;Exception 表示程序可处理的异常,分为必须显示处理的受检异常(如 IOExcption)和可不处理的非受检异常(如 RuntimeException)。

13 谈一谈 Java 成员变量、局部变量和静态变量的创建和回收机制

13.1 三类变量的存储位置
13.2 创建机制(时机与初始化)
  • 成员变量: 随对象创建(new)在堆中分配内存,并自动赋默认零值(整型 0,布尔 false,引用 null),然后执行构造器中的显式赋值或代码块赋值;
  • 局部变量: 在方法被调用时,在栈帧的局部变量表中分配空间,必须显式初始化后才能使用(编译器强制),否则报错;
  • 静态变量: 在类加载的准备阶段分配内存并赋予默认零值,在初始化阶段(clinit) 执行显式赋值或哦静态代码块;
13.3 回收机制(销毁)
  • 成员变量: 当对象不再被任何根(GC Roots)引用,对象成为垃圾,GC 会回收堆内存,成员变量随之被回收;
  • 局部变量: 方法调用结束,对应的栈帧被弹出,局部变量表直接销毁,无需 GC 参与(栈内存自动管理);
  • 静态变量: 生命周期与类相同。类未被卸载时,静态变量一直存在(可能造成内存泄漏)。类卸载条件苛刻(自定义类加载器且无引用),普通应用静态变量会随 JVM 退出而释放;
13.4 补充要点
  • 局部变量可声明为 final,但不会有独立于方法外的生命周期;
  • 静态变量属于类,所有实例共享;成员变量属于对象,每个实例独立;
  • 大量静态变量或大对象被静态引用会导致内存泄漏(无法回收);
13.5 总结
  • 成员变量随对象创建于堆,GC 回收;
  • 局部变量随方法栈帧创建与销毁;
  • 静态变量随类加载与方法区,类卸载时才回收;

14 分别讲讲 final、static、synchronized

14.1 final 关键字

作用:

  • 修饰类: 该类不能被继承(如 String、System);
  • 修饰方法: 方法不能被重写(但可被重载);
  • 修饰变量: 变量成常量,基本类型值不可变;引用类型指向的对象地址不可变,但对象内容可变;

细节:

  • final 成员变量必须在声明、构造器或代码中显式赋值;
  • final 局部变量只需在使用前赋值;
  • final 参数不能修改;
  • 编译时常量(static final 基本类型或 String)会被 javac 内联优化;

底层:

  • JVM 通过常量池和字节码指令(如 putfield 检查)保证不可修改;
  • final 方法可能被内联优化;
14.2 static 关键字

作用:

  • 修饰变量: 类变量,所有实例共享,内存中仅一份,类加载时初始化;
  • 修饰方法: 静态方法,可直接通过类调用,不能访问实例成员(无 this);
  • 修饰代码块: 静态代码块,类加载时执行,常用语初始化静态变量;
  • 修饰内部类: 静态内部类,不持有外部类引用;
  • 静态导入: import static 导入静态成员;

细节:

  • 静态方法不能被重写,但可以被子类定义同名静态方法(隐藏);
  • 静态变量有默认零值,可通过类名访问;
  • 静态内部类实例化:new Outer.Inner()

底层:

  • 静态变量存储在方法区(JDK 8 元空间),在类加载的准备阶段分配内存并设默认值,初始化阶段赋真实值;
  • 静态方法的调用通过 invokestatic 指令;
14.3 synchronize 关键字

作用:

  • 实现线程同步,保证同一时刻只有一个线程执行临界区代码;
  • 修饰实例方法:锁当前实例(this);
  • 修饰静态方法:锁当前类的 Class 对象;
  • 修饰代码块:锁指定对象;

原理(JVM 层面):

  • 对象头(Mark Word):存储锁状态、哈希码、分代年龄等;
  • 锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(JDK 1.6+);
  • Monitor:重量级锁依赖 ObjectMonitor(_owner_EntryList_WaitSet);
  • 字节码:monitorenter/monitorexit

特性:

  • 可重入:同一个线程可多次获取锁,计数器累加;
  • 非公平:不保证线程按等待顺序获取锁;

与 Lock 对比

  • synchronized 自动释放锁,Lock 需手动 unlock()
  • synchronized 不可中断、无超时、单一条件队列;Lock 更灵活;
14.5 总结
  • final 定义不可变性、static 定义类级别共享、synchronized 定义线程同步锁;
  • 三者各司其职,常结合使用(如 static final 常量、synchronized static 类锁);

15 final、finally、finalize 区别

15.1 final --- 关键字
  • 修饰类:不可被继承(如 String);
  • 修饰方法:不可被子类重写;
  • 修饰变量:基本类型值不可变;引用类型地址不可变(对象内容可变);
  • final = "最终",不可改变;
15.2 finally --- 异常处理块
  • 作用:与 try-catch 配合使用,保证无论是否发生异常,其中的代码都会执行(除非 JVM 退出或当前线程被中断);
  • 典型场景:释放资源(关闭文件、数据库连接、锁等);
  • 注意:finally 块中若抛出异常或执行 return,会覆盖 try/catch 中的返回结果;
15.3 finalize --- Object 类的方法
  • 作用:垃圾回收器在回收对象前会调用该方法(JDK 9 起已标记为废弃);
  • 特点:对象可以在此方法中自救(重新与 GC Roots 建立引用),但执行时机不确定,性能差,容易引发问题;
  • 替代方案:Cleaner 或 try-with-resources 替代;
15.4 区别对比表
15.5 总结

final 是修饰符(不可变)、finally 是异常处理块(保证执行),finalize 是对象回收方法(已废弃)。

16 String 为什么设计成不可变的?

  • 字符串常量池(String Pool)
    • 不可变性保证了同一个字符串字面量可以安全地被多个引用共享,而不担心修改影响其他作用。如果 String 可变,常量池机制就无法实现;
  • 线程安全
    • 不可变对象天然线程安全,无需额外同步,简化并发编程;
  • 安全性
    • String 被广泛用于类加载、文件路径、网络连接、数据库 URL 等敏感参数。若可变,攻击者可能修改引用指向字符串内容,造成安全隐患(如篡改路径);
  • 哈希码缓存(HashCode Caching)
    • String 常被用作 HashMap/HashSet 的键。不可变使得哈希码只需计算一次并缓存,提高性能;
  • 设计清晰
    • String 的不可变配合 StringBuilder/StringBuffer 的可变,职责分明:不变用于共享和常量,可变用于频繁拼接;

17 请简述一下 String、StringBuffer 和 StringBuilder 的区别

总结:

  • String 不可变线程安全,适合少量拼接;
  • StringBuffer 可变且线程安全(同步),适合多线程;
  • StringBuilder 可变非线程安全,适合但线程且性能最高;

18 请说说 Java 中 String.length() 的运作原理

18.1 底层存储的演变

String.length() 的返回值取决于内部如何存储字符序列。不同 Java 版本的实现有差异。

Java 8 及之前:

  • String 内部使用 char[] value 数组存储字符;
  • 每个 char 占 2 字节(UTF - 16 编码单元);
  • length() 方法直接返回 value.length,即字符数组的长度,也就是字符串中 UTF - 16 code unit 的个数;
  • 时间复杂度 O(1);

Java 9 及之后(Compact Strings):

  • 为了节省内存,引入 byte[] value 存储,并增加 byte coder 字段标识编码方式;
  • 编码方式:
    • coder == LATIN1:字符串只包含 Latin - 1(ISO-8859-1)字符,每个字符占 1 字节;
    • coder == UTF 16:字符串包含需要 2 字节表示的字符,每个字符占 2 字节;
  • length() 逻辑:
    • coder == LATIN1,直接返回 value.length(字节数等于字符数);
    • coder == UTF16,返回 value.length >> 1(字节数除以 2,即字符数);
  • 仍为 O(1) 操作;
18.2 关键点补充
  • length() 返回的是字符数(UTF - 16 code unit 的数量),不是字节数,也不是 Unicode 码点数量。对于辅助平面字符(如 emoji,占用两个 code unit),length() 会返回 2,而 codePointCount() 返回 1;
  • 因为 String 不可变,长度在构造时已确定,所以 length() 无需动态计算,直接读取字段或简单计算即可;
  • 该方法为 final,不能被重写;
18.3 总结

String.length() 返回字符串中 UTF - 16 code unit 的数量,底层利用内部数组长度直接或经移位得到,时间复杂度 O(1)。

Java 9+ 通过 Compact Strings 优化内存,但逻辑依然高效。

相关推荐
勿忘,瞬间2 小时前
SpringBoot配置文件
java·spring boot·后端
fTiN CAPA2 小时前
Tomcat 都有哪些核心组件
java·tomcat·firefox
2601_949814492 小时前
Spring Boot中使用Server-Sent Events (SSE) 实现实时数据推送教程
java·spring boot·后端
splage2 小时前
HeidiSQL导入与导出数据
java
热爱Java,热爱生活2 小时前
浅谈Spring三级缓存
java·spring·缓存
星越华夏2 小时前
python——三角函数用法
开发语言·python
代码中介商2 小时前
C语言数据存储深度解析:从原码反码补码到浮点数存储
c语言·开发语言·内存
@ chen2 小时前
IDEA初始化配置
java·ide·intellij-idea
wellc3 小时前
SpringBoot集成Flowable
java·spring boot·后端