Java面试基础篇(1)

0 阅读15分钟

Java架构老羊|十年大厂 Java 实战经验,专注技术干货分享!本文整理高频考点与思路,希望能帮到正在面试的你。


问答1:自我介绍

面试官:先简单做个自我介绍吧。
面试者:面试官你好!我是xxx,有一年Java后端开发经验。近期完成了公司内部审批模块的重构:基于Spring Boot搭建服务,用MyBatis-Plus操作MySQL数据库,通过慢查询日志定位到全表扫描等问题,同时对高频访问的审批模板数据加入Redis缓存,采用“查询时缓存+定时刷新”策略,重构后系统稳定运行,接口 P99 从 800ms+ 降到 90ms,整体体验反馈很好。最近有关注到贵司在XX方向的业务发展,我对这方面也比较感兴趣,希望可以加入团队,为公司创造价值,谢谢!
分析思路

  • 考点:表达逻辑、项目真实性、技能栈匹配度(初级是否接触主流框架)。
  • 注意:看面试者是否能把项目讲清楚(不是流水账),有没有回避技术细节;应届生重点看课程设计/实习项目的参与度。与工作无关的内容不要说,容易分散注意力,不要主动说来自哪里,避免无意识偏见。说到具体数据时,稍微放慢语速、加重语气,让面试官听清你的亮点。

问答2:JVM的内存结构

面试官:说一下你对jvm内存结构的理解?
面试者:主要分为两大块 —— 线程私有区域和线程共享区域

  • 线程私有区域
    1、程序计数器:这是 JVM 里最小的一块内存,用来记录当前线程执行到哪一行字节码指令,比如方法执行到第几条指令、分支 / 循环 / 异常处理的位置。当线程切换,计数器也会跟着切换,保证线程恢复后能继续执行。
    2、虚拟机栈:也叫 Java 栈,每个方法执行时都会创建一个 “栈帧”,栈帧里存放局部变量表、操作数栈、方法出口等信息。比如定义的 int、String 局部变量、方法调用的入参都存在这里。如果方法调用层级太深(比如递归没有终止条件),就会抛出 StackOverflowError;如果栈的动态扩展超出内存限制,就会出现 OOM。
    3、本地方法栈:和虚拟机栈功能类似,但它是给 Native 方法(比如调用 C/C++ 编写的方法)使用的,比如 System.currentTimeMillis() 底层就是 Native 方法。
  • 线程共享区域
    1、堆内存(Heap):这是 JVM 里最大的一块内存,几乎所有对象实例、数组都存放在堆中,也是垃圾回收(GC)的核心区域。堆又可以细分为新生代(Eden 区、SurvivorFrom/SurvivorTo 区)和老年代,新生代存放刚创建的对象,GC 频率高(使用复制算法),老年代存放存活时间较长的对象,GC 频率低(使用标记 - 整理算法)。平时遇到的 OOM,80% 以上都是堆内存溢出,比如创建大量对象没释放、内存泄漏导致对象无法被回收。
    2、元空间:存放类的结构、字段、方法、常量池、静态变量、编译后的字节码等内容。元空间默认使用本地内存,不属于 JVM 堆内存,但如果加载的类太多(比如频繁动态生成类),也会触发元空间 OOM。
  • 还有一块直接内存(堆外内存),它不属于 JVM 规范里的内存结构,比如 NIO 的 DirectByteBuffer 就是用直接内存,这块内存不受 JVM 堆大小限制,但受物理内存限制,也会导致 OOM,比如用 Netty 做网络编程时,如果直接内存分配过多,就容易出现这个问题。

分析思路

  • 考点:是否能准确区分 JVM 内存的核心分区,能否将内存结构与实际开发中的问题(如递归栈溢出、堆 OOM、元空间溢出)结合,体现对理论的落地认知。
  • 注意:程序计数器是唯一不会抛出 OOM 的 JVM 内存区域,且仅针对 Java 方法计数(Native 方法计数器值为 undefined);
  • 代码示例
/**
 * 递归调用无终止条件,导致虚拟机栈帧过多,抛出StackOverflowError
 * 原理:每个递归调用创建新栈帧,超出虚拟机栈深度限制
 */
public class StackOverflowDemo {
    private static int count = 0;
    public static void recursiveCall() {
        count++;
        recursiveCall(); // 无限递归,创建栈帧
    }
    public static void main(String[] args) {
        try {
            recursiveCall();
        } catch (StackOverflowError e) {
            System.out.println("递归调用次数:" + count);
            e.printStackTrace();
        }
    }
}

/**
 * 无限创建对象并放入List,对象无法被GC回收,导致堆内存溢出
 * JVM参数建议:-Xms20m -Xmx20m(限定堆大小为20M,加速OOM触发)
 */
public class HeapOOMDemo {
    static class OOMObject {}
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        try {
            while (true) {
                list.add(new OOMObject()); // 持续创建对象,占用堆内存
            }
        } catch (OutOfMemoryError e) {
            System.out.println("堆内存溢出:" + e.getMessage());
            e.printStackTrace();
            throw e;
        }
    }
}

/**
 * 动态生成大量类,导致元空间(存储类结构)溢出
 * JVM参数建议:-XX:MaxMetaspaceSize=10m(限定元空间大小为10M)
 * 依赖:需引入cglib库(<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version></dependency>)
 */
public class MetaspaceOOMDemo {
    static class TargetClass {}
    public static void main(String[] args) {
        int count = 0;
        try {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(TargetClass.class);
            enhancer.setUseCache(false); // 禁用缓存,每次生成新类
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            while (true) {
                enhancer.create(); // 动态生成子类,类结构存入元空间
                count++;
            }
        } catch (OutOfMemoryError e) {
            System.out.println("生成类的数量:" + count);
            System.out.println("元空间溢出:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

问答3:Java的面向对象特性

面试官:说说你对Java面向对象特性的理解?
面试者

  • 封装:核心是“隐藏内部实现,对外提供统一接口”。比如电商订单模块,我把订单状态修改逻辑(创建、支付、发货、完成)封装在OrderService里,外部只能调用updateOrderStatus()方法,不直接修改status字段,既防止状态被随意篡改,也方便后续维护(比如新增状态校验)。
  • 继承:核心是“复用共性,扩展特性”,仅用于真正的“is-a”关系。比如项目中WechatPayAlipay都继承抽象Payment类,将签名、验签等通用逻辑放在父类,不同支付方式的回调处理、金额计算在子类重写,实现代码复用和灵活扩展。
  • 多态:核心是“一个接口,多种实现”,也是Spring IoC的核心思想之一。比如营销系统的优惠券、满减、折扣三种优惠方式,都实现DiscountStrategy接口的calculate()方法,下单时根据优惠类型动态注入对应实现类,避免大量if-else,符合开闭原则,新增优惠方式只需添加新实现类。
  • 抽象类与接口的区别及实践
    • 核心区别:抽象类是“半抽象”,可包含抽象方法和具体实现,仅支持单继承;接口是“全抽象”(JDK8后可含default方法),支持多实现。抽象类体现“继承/共性复用”,接口体现“行为约定/能力扩展”。
    • 选型原则:复用代码逻辑用抽象类(如BaseEntity封装所有实体的id、创建时间、更新时间);定义行为规范用接口(如ExportAble接口定义exportToExcel()方法,订单、用户等需导出的实体都实现该接口,各自实现导出逻辑)。

分析思路

  • 考点:1、能否清晰阐述封装、继承、多态三大核心特性的定义与设计思想;2、能否结合实际项目场景说明特性的应用价值,体现理论落地能力;3、能否准确区分抽象类与接口的本质差异,并给出合理的选型原则;4、考察对面向对象设计原则(如开闭原则、复用原则)的理解。
  • 注意:1、避免空泛描述,必须结合项目实例说明特性的使用场景;2、强调继承的慎用原则,避免过度继承导致代码耦合;3、区分抽象类与接口的设计意图,而非仅停留在语法层面;4、多态的实现要关联到实际技术(如Spring IoC、策略模式),体现工程化理解。
  • 代码示例
// 1. 封装示例:订单状态封装
public class OrderService {
    public void updateOrderStatus(Order order, OrderStatus newStatus) {
        if (order.getStatus() == OrderStatus.COMPLETED) {
            throw new IllegalStateException("已完成订单无法修改状态");
        }
        order.setStatus(newStatus);
    }
}
// 2. 继承示例:支付抽象类与子类
abstract class Payment {
    public boolean verifySignature(String params, String sign) {
        return true;
    }
    public abstract void callback(String notifyData);
}
class WechatPay extends Payment {
    @Override
    public void callback(String notifyData) {}
}
class Alipay extends Payment {
    @Override
    public void callback(String notifyData) {}
}
// 3. 多态示例:优惠策略接口与实现
interface DiscountStrategy {
    BigDecimal calculate(Order order);
}
class CouponDiscount implements DiscountStrategy {
    @Override
    public BigDecimal calculate(Order order) {
        return order.getAmount().subtract(new BigDecimal("10"));
    }
}
// 4. 抽象类与接口结合示例
abstract class BaseEntity {
    private Long id;
    private Date createTime;
    private Date updateTime;
}
interface ExportAble {
    void exportToExcel(OutputStream os) throws IOException;
}
class Order extends BaseEntity implements ExportAble {
    @Override
    public void exportToExcel(OutputStream os) {}
}

问答4:重载和重写的区别

面试官:方法重载和重写有什么区别?
面试者

  • 重载(Overload):同一个类中,方法名相同,参数列表不同(个数/类型/顺序不同),与返回值无关。
  • 重写(Override):子类继承父类后,重新实现父类方法,方法名、参数列表、返回值类型必须一致(协变返回类型除外),子类访问权限不能更严格。

分析思路

  • 考点:多态的两种表现形式、方法签名规则
  • 注意:踩坑点:返回值不同不算重载;子类重写父类方法时,不能抛出比父类更宽泛的异常。
  • 代码示例
// 重载
class Demo {
    public void test(int a) {}
    public void test(String a) {} // 参数类型不同,构成重载
}
// 重写
class Parent {
    public void say() {}
}
class Child extends Parent {
    @Override
    public void say() {} // 重写父类方法
}

问答5:构造方法可以重写吗

面试官:什么是构造方法?构造方法可以被重写吗?
面试者

  • 构造方法:与类名相同,无返回值,用于创建对象时初始化成员变量。
  • 构造方法不能被重写,因为子类无法继承父类构造方法,只能通过super()调用。

分析思路

  • 考点:构造方法特性、继承机制
  • 注意:踩坑点:若父类无无参构造,子类必须显式调用父类有参构造,否则编译报错。
  • 代码示例
class Parent {
    public Parent(String name) {}
}
class Child extends Parent {
    // 必须显式调用父类构造
    public Child(String name) {
        super(name);
    }
}

问答6:静态代码块、构造代码块、局部代码块

面试官:静态代码块、构造代码块、局部代码块有什么区别?
面试者

  • 静态代码块:static{},类加载时执行,只执行一次,用于初始化静态资源。
  • 构造代码块:{},创建对象时执行,早于构造方法,用于初始化实例资源。
  • 局部代码块:方法内{},限定变量作用域,执行完即释放。

分析思路

  • 考点:代码块执行顺序、初始化时机
  • 注意:执行顺序:静态代码块 → 构造代码块 → 构造方法 → 局部代码块。
  • 代码示例
class Demo {
    static { System.out.println("静态代码块"); }
    { System.out.println("构造代码块"); }
    public Demo() { System.out.println("构造方法"); }
    public void test() {
        { System.out.println("局部代码块"); }
    }
}

问答7:String Builder和String Buffer

面试官: 你了解StringBuffer和StringBuilder的区别吗?在什么场景下应该使用哪一个?
面试者: String Buffer和StringBuilder的核心区别只有线程安全性,其余功能完全一致:

  • 线程安全性:StringBuffer:所有方法(如appendinsertdelete)都用synchronized修饰,多线程环境下安全,但有同步开销。StringBuilder:无同步机制,单线程下性能高10%-20%,多线程下需外部同步(如加锁)。
  • 性能差异:单线程场景:StringBuilder比StringBuffer快10%-20%(如循环拼接10万次,StringBuilder快几十毫秒)。多线程场景:StringBuffer安全但性能低;StringBuilder需外部同步,否则可能引发数据错乱(如字符覆盖、数组越界)。
  • 适用场景:StringBuffer:多线程环境下的字符串操作(如日志收集、多线程数据组装)。StringBuilder:单线程环境下的字符串操作(如方法内临时拼接、循环中构建SQL/JSON)。

分析思路

  • 考点:线程安全与性能的权衡,避免死记硬背"StringBuffer线程安全,StringBuilder快",需结合场景理解。
  • 注意:StringBuffer的synchronized仅保证单个方法原子性,不保证复合操作(如if (sb.length() == 0) sb.append("default"))安全,仍需外层加锁。
  • 代码示例
// 正确用法:单线程场景(StringBuilder)
public String buildOrderLog(Order order) {
    StringBuilder sb = new StringBuilder();
    sb.append("OrderID:").append(order.getId());
    sb.append(",Status:").append(order.getStatus());
    return sb.toString();
}
// 正确用法:多线程场景(StringBuffer)
public class LogCollector {
    private final StringBuffer buffer = new StringBuffer();

    public void addLog(String log) {
        synchronized (buffer) { // 外部同步
            buffer.append(log).append("\n");
        }
    }
}

问答8:final关键字

面试官:final 关键字可以用在哪些地方,分别有什么作用?
面试者

  • 修饰类:类不可被继承(如StringMath)。
  • 修饰方法:方法不可被重写。
  • 修饰变量:基本类型值不可变;引用类型地址不可变(但对象内容可变)。

分析思路

  • 考点:不可变性设计、类/方法/变量的约束
  • 注意:踩坑点:final List<String> list = new ArrayList<>(); 可以add/remove元素,只是不能把list指向另一个对象。
  • 代码示例
final class Demo {} // 不可继承
class Parent {
    final void test() {} // 不可重写
}
final int a = 10; // 基本类型值不可变
final List<String> list = new ArrayList<>();
list.add("a"); // 允许
// list = new ArrayList<>(); // 编译报错

问答9:Java异常的理解

面试官:说说你对Java异常的理解?
面试者

  • 异常的核心定义:异常是程序运行时发生的非正常情况(比如空指针、数组越界、IO失败),Java通过异常机制将错误处理和业务逻辑解耦,避免程序直接崩溃,同时让错误处理更规范。
  • 异常的分类
    • 按继承体系:所有异常都继承自Throwable,核心分为ErrorExceptionError是JVM层面的严重错误(如StackOverflowErrorOutOfMemoryError),程序无法捕获和处理;Exception是程序可处理的异常,又分为受检异常(编译时必须处理,如IOExceptionSQLException)和非受检异常(运行时异常,编译不校验,如NullPointerExceptionArrayIndexOutOfBoundsException)。
  • 异常的处理方式
    • 捕获(try-catch-finally):适合处理已知的、可恢复的异常,比如读取文件时捕获IOException,打印错误日志并提示用户重试;finally块用于释放资源(如关闭流、数据库连接),即使发生异常也会执行。
    • 抛出(throws/throw):适合处理无法在当前方法解决的异常,比如DAO层抛出SQLException,由Service层统一处理;throw用于手动抛出异常(如参数校验失败时抛IllegalArgumentException)。
  • 异常的最佳实践
    • 避免捕获Exception这类大而全的异常,要精准捕获具体异常,便于定位问题;
    • 不要空catch块(吞掉异常),至少打印日志;
    • 自定义异常(如BusinessException)处理业务错误,区分系统异常和业务异常,方便前端提示用户;
    • 尽量使用try-with-resources(JDK7+)替代finally释放资源,更简洁且能避免资源泄漏。

分析思路

  • 考点:1、能否清晰梳理Java异常的继承体系和核心分类(Error/Exception、受检/非受检);2、能否准确说明异常的两种处理方式(捕获/抛出)的适用场景;3、能否结合实际开发给出异常处理的最佳实践,体现工程化思维;4、考察对异常设计思想(解耦错误处理与业务逻辑)的理解。
  • 注意:1、区分ErrorException的本质差异,避免将StackOverflowError归为Exception;2、明确受检/非受检异常的区别,不要混淆编译时和运行时的校验规则;3、最佳实践需结合项目场景(如自定义业务异常),避免纯理论描述;4、说明try-with-resources的优势,体现对JDK新特性的掌握。
  • 代码示例
import java.io.FileReader;
import java.io.IOException;
// 自定义业务异常
class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}
public class ExceptionDemo {
    // 读取文件:使用try-with-resources自动释放资源
    public static String readFile(String path) {
        // 参数校验:手动抛出非受检异常
        if (path == null || path.isEmpty()) {
            throw new IllegalArgumentException("文件路径不能为空");
        }
        // try-with-resources自动关闭FileReader
        try (FileReader reader = new FileReader(path)) {
            char[] buf = new char[1024];
            int len = reader.read(buf);
            return new String(buf, 0, len);
        } catch (IOException e) {
            // 捕获具体异常,打印日志并封装为业务异常抛出
            System.err.println("读取文件失败:" + e.getMessage());
            throw new BusinessException("文件读取失败,请检查文件是否存在");
        }
    }
    public static void main(String[] args) {
        try {
            String content = readFile("test.txt");
            System.out.println(content);
        } catch (BusinessException e) {
            // 处理业务异常,提示用户
            System.out.println("业务错误:" + e.getMessage());
        } catch (IllegalArgumentException e) {
            // 处理参数异常
            System.out.println("参数错误:" + e.getMessage());
        }
    }
}

问答10:反问环节

面试官:好了,我的问题问完了,你有什么问题想问我吗?
面试者:刚才听您介绍,咱们团队正在推进 [XX业务/项目]。在这个项目中,目前后端面临的最大技术挑战是什么?是数据一致性、高并发流量,还是复杂业务逻辑的解耦?如果我加入,可能会优先参与到哪一部分?
分析思路

  • 考点:面试者的关注点(是关注技术成长,还是只关注薪资)、对岗位的兴趣(是否真的想加入)。
  • 注意:初级开发可以问“团队技术栈”、“有没有导师带”、“团队的开发流程”,不要问“薪资多少?有没有年终奖”(一般HR会谈),也不要问“加班多吗”(可以委婉问“团队的工作节奏怎么样”)。

持续更新 ✨ Java 后端、面试真题、技术架构、性能调优干货。
点赞收藏不迷路 📌,欢迎点赞、收藏、在看三连支持一下!


👉 文末公众号备注(直接复制)

公众号:Java 架构老羊
专注Java面试、后端架构、实战干货分享,持续更新~


未经授权,禁止任何形式的转载、抄袭、洗稿;如需引用,请注明原文出处。