跳至主要內容
Java 基础面试二

Java 基础面试二

钝悟...大约 18 分钟JavaJavaCore面试JavaJavaCore面试

Java 基础面试二

Java 面向对象

【简单】对象实体与对象引用有何不同?

(1)对象是用来描述客观事物的一个抽象。一个对象由一组属性和对这组属性进行操作的一组服务组成。

(2)类是具有相同属性和方法的一组对象的集合,它为属于该类的所有对象提供了统一的抽象描述,其内部包括属性和方法两个主要部分。

(3)对象实体与对象引用的不同之处在于:

  • new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)
  • 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);
  • 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。

【简单】接口和抽象类有什么区别?

(1)接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。

接口的主要特性有:

  • 接口不能实例化。
  • 接口不能包含任何非常量成员,任何字段都隐式的被 public static final 修饰。
  • 接口中没有非静态方法,也就是说要么是抽象方法,要么是静态方法。
  • 从 Java8 开始,接口增加了 default 方法特性,可以定义方法的默认实现;Java 9 以后,甚至可以定义私有的 default 方法。

(2)抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。

(3)接口和抽象类有什么相同点和不同点?

Java 中的类可以实现多个接口。

(4)与 C++ 等语言不一样,Java 类不支持多继承。这意味着,Java 不能通过继承多个抽象类来重用逻辑。那么,如何来实现重用呢?Java 的解决方案是:接口支持多继承,准确的说,接口支持扩展多个接口,而接口也支持实现多个接口。

【中等】什么是 Java 内部类?内部类有什么作用?

什么是内部类?

内部类 (Inner Class) 是定义在另一个类内部的类。Java 中有四种类型的内部类:

  • 成员内部类:作为外部类的成员存在
  • 局部内部类:定义在方法或作用域内的类
  • 匿名内部类:没有名字的局部内部类
  • 静态嵌套类:用 static 修饰的嵌套类

内部类有什么作用?

  • 逻辑分组:当某个类只对另一个类有用时,可以将其嵌入使用它的类中,保持代码在一起
  • 增强封装性:内部类可以访问外部类的私有成员,同时自身也可以对外部完全隐藏
  • 实现多重继承:通过内部类可以间接实现多重继承的效果
  • 回调机制:常用于事件处理和监听器实现
  • 代码简洁:特别是匿名内部类可以减少代码量

内部类有哪些特点?

  • 内部类可以访问外部类的所有成员(包括 private)
  • 外部类需要通过实例化内部类来访问其成员
  • 内部类编译后会生成独立的。class 文件(格式:OuterClass$InnerClass.class)
  • 非静态内部类不能有静态成员(静态内部类可以)
  • 内部类可以继承其他类或实现接口

【简单】为什么 Java 不支持多重继承?

Java 不支持多重继承的核心原因是为了避免【菱形继承问题(Diamond Problem)】

什么是菱形继承问题?

菱形继承存在歧义性:

  • 如果类 C 继承自类 A 和类 B,而 A 和 B 都有同名方法 method()
  • 调用 C.method() 时无法确定应该调用 A 还是 B 的版本

由于菱形继承歧义性而引发的复杂性增加问题:

  • 多重继承会显著增加编译器和 JVM 的实现复杂度
  • 方法调用、构造函数调用顺序变得难以确定

Java 如何解决多重继承?

在 Java 中,类可以实现多个接口。接口提供多重继承的行为规范,但不包含具体实现。

JDK8 之后,接口支持默认方法(default),是不是又出现了菱形继承问题?

为了规避这个问题,Java 强制规定,如果多个接口存在相同的默认方法,子类必须重写这个方法。否则,编译器会报错。

【中等】深拷贝和浅拷贝有什么区别?

深拷贝和浅拷贝有什么区别?

关键点浅拷贝深拷贝
复制对象只复制对象本身(基本类型值拷贝)递归复制对象及其引用的所有子对象
引用类型字段新旧对象共享同一引用(修改相互影响)创建全新引用对象(修改完全隔离)
内存开销小(仅复制一层)大(递归复制所有关联对象)
实现方式默认Object.clone()需手动实现递归克隆/序列化/工具类
适用场景对象无可变引用字段对象含可变引用字段且需完全独立

本质区别:浅拷贝是"复制钥匙",深拷贝是"复制钥匙+保险箱"。

注意事项

  • 深拷贝需处理循环引用问题
  • 推荐使用SerializationUtils.clone()或 JSON 序列化实现深拷贝
  • 不可变对象(如 String)的浅拷贝是安全的

深拷贝和浅拷贝实现方式有什么区别?

实现方式对比

方法浅拷贝深拷贝说明
Object.clone()默认浅拷贝
手动递归克隆需所有引用类型实现Cloneable
序列化反序列化通过ObjectOutputStream实现
工具类(Apache Commons)SerializationUtils.clone()
class Person implements Cloneable {
    String name;
    Address address; // 引用类型字段

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone(); // 默认浅拷贝
    }
}

// 测试
Person p1 = new Person("Alice", new Address("北京"));
Person p2 = (Person)p1.clone();
p2.address.city = "上海"; // p1.address.city 也会变成"上海"

【简单】面向对象和面向过程有什么区别?

面向对象和面向过程的主要区别:

维度面向对象(OOP)面向过程(POP)
核心思想对象为中心步骤为中心
代码组织现实实体抽象为类功能流程拆分为函数
数据管理数据与行为封装在对象中数据与函数独立
扩展方式通过继承/多态扩展(开闭原则)需修改函数逻辑
典型特性封装、继承、多态三大特性无三大特性
典型语言Java, Python, C++C, Pascal

【中等】面向对象三大特征和五大原则是什么?

面向对象三大特征是什么?

面向对象三大特征:

  • 封装(Encapsulation)隐藏内部细节,暴露安全接口

    • private 保护数据,通过 getter/setter 控制访问
    • 示例:BankAccount 类隐藏余额,提供 deposit()/withdraw() 方法
  • 继承(Inheritance)子类复用父类属性和方法

    • 通过 extends 实现(如 Dog extends Animal
    • 注意:Java 是单继承(一个子类只能有一个父类)
  • 多态(Polymorphism)同一行为的不同实现方式

    • 编译时多态:方法重载(Overload
    • 运行时多态:方法重写(Override)+ 父类引用指向子类对象(如 Animal a = new Dog(); a.sound();

一言以概之封装保证安全性,继承提高复用性,多态增强扩展性

面向对象的五大原则是什么?

面向对象的五大原则是 SOLID 原则:

  • 单一职责原则 (SRP)一个类只负责一个功能,避免职责过多导致代码臃肿。
  • 开闭原则 (OCP)对扩展开放,对修改关闭。通过抽象和继承扩展功能,而非直接修改原有代码。
  • 里氏替换原则 (LSP)子类必须能替换父类,确保继承关系不会破坏程序逻辑。
  • 接口隔离原则 (ISP)接口应当小而专,避免臃肿接口强制实现不必要的方法。
  • 依赖倒置原则 (DIP)依赖抽象而非具体,高层模块不直接依赖低层模块,而是通过接口或抽象类交互。

一言以概之:SOLID 原则让代码更灵活、可维护、易扩展。

设计模式

典型问题

(1)你知道哪些设计模式?

(2)你知道哪些设计模式在 Java 源码中的应用案例?

(3)你知道哪些设计模式在主流框架中的应用案例?

知识点

(1)23 种经典设计模式分类如下:

  • 创建型模式,是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)。
  • 结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见的结构型模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等。
  • 行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)。

(2)设计模式在 Java 源码中应用的经典案例:

InputStream 是一个抽象类,标准类库中提供了 FileInputStream、ByteArrayInputStream 等各种不同的子类,分别从不同角度对 InputStream 进行了功能扩展,这是典型的装饰器模式应用案例。

(3)设计模式在主流框架中应用的经典案例:

如 Spring 等如何在 API 设计中使用设计模式。你至少要有个大体的印象,如:

  • BeanFactoryopen in new windowApplicationContextopen in new window 应用了工厂模式。
  • 在 Bean 的创建中,Spring 也为不同 scope 定义的对象,提供了单例和原型等模式实现。
  • Spring Aop 使用了代理模式、装饰器模式、适配器模式等。
  • 各种事件监听器,是观察者模式的典型应用。
  • 类似 JdbcTemplate 等则是应用了模板模式。

Object

【简单】Object 类的常见方法有哪些?

Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

方法签名作用默认行为
String toString()返回对象的字符串表示类名@十六进制哈希码(如 Person@1b6d3586
boolean equals(Object obj)比较两个对象是否逻辑相等比较内存地址(==
int hashCode()返回对象的哈希码基于内存地址生成
Class<?> getClass()返回对象的运行时类(Class 对象)由 JVM 提供
protected Object clone()创建并返回对象的副本浅拷贝(需实现 Cloneable 接口)
protected void finalize()已废弃,对象被 GC 回收前调用空实现(不推荐使用)
void notify()唤醒一个等待该对象监视器的线程依赖 JVM 实现
void notifyAll()唤醒所有等待该对象监视器的线程依赖 JVM 实现
void wait()让当前线程等待,直到被唤醒必须在同步代码块中调用
void wait(long timeout)让线程等待,最多 timeout 毫秒超时后自动唤醒
void wait(long timeout, int nanos)更精确的等待(纳秒级)实际精度依赖系统

【简单】== 和 equals() 有什么区别?

对比项==equals()
基本类型比较比较不能比较
引用类型比较比较内存地址默认比较内存地址(同 ==),但可重写为逻辑比较(如内容是否相同)
是否可重写否(运算符,行为固定)是(可自定义比较逻辑)
用途快速判断基本类型值相等或引用是否指向同一对象判断对象逻辑是否相等(如内容、属性等)

【简单】为什么重写 equals() 时必须重写 hashCode() 方法?

因为 Java 规定:两个对象若equals()相等,它们的hashCode()必须相同

如果违背,则哈希集合(如 HashMapHashSet)无法正确去重或查找。

  • HashMap/HashSet 先通过 hashCode() 快速定位数据,再用 equals() 精确匹配。
  • hashCode() 不一致,即使 equals()true,集合会误判为不同对象。

如何正确重写 `hashCode()`?

  • equals():比较所有关键字段(如 nameage)。
  • hashCode():用 Objects.hash(字段1, 字段2) 生成(确保与 equals() 字段一致)。

【简单】finalize 有什么用?

一言以概之,finalize 可用于对象销毁前的清理,但不可靠且性能差,现代 Java 开发应避免使用,改用 AutoCloseableCleaner

Java 9+ 已弃用 finalize,推荐使用:

  • try-with-resources(实现 AutoCloseable 接口)
  • CleanerPhantomReference(更可控的清理机制)。

finalize 的作用(Java)

  • 对象被垃圾回收前的清理:在对象被 GC 回收前,finalize() 会被调用,可用于释放非内存资源(如文件句柄、数据库连接等)。
  • 最后的补救机会:如果对象未被正确关闭,finalize 提供最后一次资源释放的机会。

finalize 的问题

  • 不保证执行:JVM 不保证 finalize 一定会执行(如程序突然终止时)。即使对象可达性失效,GC 可能延迟回收,导致 finalize 延迟调用。
  • 性能开销:覆写 finalize 的对象会被 JVM 放入特殊队列,垃圾回收变慢。可能引发内存泄漏(如果 finalize 阻塞或执行过久)。
  • 安全问题:在 finalize 中抛出异常会导致清理中断,且异常被忽略。可能被恶意代码利用(如通过重写 finalize 复活对象,干扰 GC)。

String

【简单】String、StringBuffer、StringBuilder 的区别?

特性StringStringBufferStringBuilder
可变性❌ 不可变✅ 可变✅ 可变
线程安全✅(因不可变)✅(同步方法)❌(非线程安全)
性能⚠️ 最差(频繁创建新对象)⚠️ 中等(同步开销)✅ 最高(无同步开销)
适用场景常量、少量拼接多线程字符串操作单线程字符串操作(推荐)

概括

  • String 存储常量StringBuilder 高效拼接(单线程)StringBuffer 保证线程安全(多线程)
  • 优先选 StringBuilder(90%场景适用)。

【简单】String 为什么是不可变的?

String 的不可变性是 Java 为安全、性能、线程安全做的核心设计。

String 不可变的核心原因

  • final 修饰的 char[] 数组:Java 中 String 内部用 private final char[](JDK 9+ 改为 byte[])存储数据,数组引用和内容均不可修改。
  • 无修改内部状态的方法:所有看似“修改”的方法(如 concat()substring())都返回String 对象,原对象不变。

设计安全优化

  • 线程安全:不可变天然线程安全,无需同步。
  • 缓存哈希值StringhashCode() 计算结果可缓存(因内容不变),提升性能(如 HashMap 的键)。
  • 字符串常量池复用:如 String s = "abc" 会复用常量池中的相同字符串,减少内存开销。

为什么这样设计

  • 安全:防止恶意修改(如网络请求参数、数据库连接字符串被篡改)。
  • 性能:哈希缓存、常量池复用提升效率。
  • 简单:避免多线程同步问题。

示例验证不可变性

String s1 = "Hello";
String s2 = s1.concat(" World");
System.out.println(s1); // 输出 "Hello"(原字符串未变)
System.out.println(s2); // 输出 "Hello World"(新对象)

【简单】字符串拼接用“+” 还是 StringBuilder?

循环/动态拼接 → StringBuilder;简单常量拼接 → "+";多线程 → StringBuffer(极少用)。
StringBuilder 是默认推荐选择!

优先用 StringBuilder(大多数场景)

  • 适用情况:循环、动态拼接、大量字符串操作。
  • 原因
    • 高性能:直接修改缓冲区,避免 + 频繁创建新对象。
    • 低内存开销:减少临时对象和 GC 压力。

简单拼接可用 "+"(编译期优化)

  • 适用情况:少量固定字符串拼接(如 "a" + "b")。
  • 原因
    • 代码简洁:可读性更好。
    • 编译器优化:JVM 自动合并为常量(如 "ab"),无性能损失。
    • 通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的。
    • 在循环内使用“+”,会导致创建过多的 StringBuilder 对象。JDK9 中,优化了这个问题,字符串相加 “+” 改为了用动态方法 makeConcatWithConstants() 来实现,而不是大量的 StringBuilder 了。

多线程拼接用 StringBuffer(极少需要)

  • 适用情况:多线程环境且需线程安全(通常局部变量仍可用 StringBuilder)。

【简单】String#equals() 和 Object#equals() 有何区别?

对比项Object#equals()String#equals()
默认行为比较内存地址==比较字符串内容(逐字符对比)
重写目的需子类自行重写以实现逻辑相等已优化为内容比较,满足字符串业务需求
性能影响无额外开销需遍历字符数组,但优先检查地址和长度
使用场景通用对象比较(默认不满足内容相等)字符串内容对比(如 "abc".equals("abc")

【简单】字符串常量池有什么用?

字符串常量池是JVM 的特殊内存区域,用于存储字符串字面量(如 "abc"),确保相同内容的字符串只存一份。

字符串常量池通过复用相同字符串,节省内存并提升性能,直接赋值("abc")优先使用池,new String() 强制创建新对象。

字符串常量池的作用有:

节省内存:相同字符串复用,避免重复创建(如 String s1 = "hello"String s2 = "hello" 指向同一对象)。

提升性能

  • 快速比较:直接通过 == 判断地址是否相同(比 equals() 更快)。
  • 哈希优化:如 HashMap 的键可复用缓存的 hashCode

实现规则

  • 直接赋值String s = "abc")→ 优先从常量池引用
  • new String("abc")强制在堆中创建新对象(不推荐,除非需隔离实例)。
  • intern() 方法 → 将堆中的字符串对象添加到常量池(若池中不存在)。

注意事项

  • 避免滥用 new String():无特殊需求时,直接用字面量赋值。
  • intern() 慎用:可能增加常量池内存压力,需权衡性能。

【简单】String s = new String("abc") 创建了几个字符串对象?

new String("abc") 可能创建1~2个对象(取决于常量池是否已存在"abc"),但堆中的新对象必定创建。

  • 常量池已存在"abc"1个对象(仅堆中的 new String
  • 常量池不存在"abc"2个对象(常量池的"abc" + 堆中的 new String

【简单】String#intern 方法有什么用?

String#intern 方法的作用有:

  • 强制字符串入池:将堆中的 String 对象添加到字符串常量池(若池中不存在)
  • 返回池中引用:保证相同内容的字符串始终返回同一内存地址

注意

  • JDK7+ 优化:常量池从方法区移至堆内存,减少内存溢出风险。
  • 慎用场景
    • 避免对动态生成的短生命周期字符串使用(可能导致池膨胀)
    • 优先用于高频使用的静态字符串(如配置键值)

【简单】String 类型的变量和常量做“+”运算时会发生什么?

常量相加编译期优化,变量相加隐式转 StringBuilder,循环拼接必须显式使用 StringBuilder 避免性能损耗。

常量折叠(编译期优化)

  • 纯常量运算(如 "a"+"b")→ 直接合并为 "ab",仅存于常量池
  • final 变量 视为常量,同样触发优化

变量拼接(运行时行为)

  • 含变量的运算(如 str + "b")→ 隐式转换为 StringBuilder 操作
    // 实际执行逻辑
    new StringBuilder().append(str).append("b").toString()
    
  • 每次运算 生成临时 StringBuilder 和最终 String 对象

性能关键差异

场景内存/性能表现优化建议
常量+常量零运行时开销无需处理
单次变量+常量1次 StringBuilder 创建可接受
循环内拼接多次创建 StringBuilder(性能陷阱)必须显式用 StringBuilder

最佳实践

  • 简单拼接:直接使用 +(可读性优先)

  • 循环/批量拼接

    // ✅ 正确写法
    StringBuilder sb = new StringBuilder();
    for (String str : list) sb.append(str);
    String result = sb.toString();
    
    // ❌ 错误写法(低效)
    String s = "";
    for (String str : list) s += str; // 每次循环隐式新建 StringBuilder
    
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.7