Java 基础面试题
一、数据类型
Java 有哪些基本数据类型?
Java 的基本数据类型有 8 种,分别是:byte(1 字节,有符号整数,范围 -128 到 127)、short(2 字节,有符号整数,范围 -32768 到 32767)、int(4 字节,有符号整数,范围约 -21 亿到 21 亿)、long(8 字节,有符号整数)、float(4 字节,单精度浮点数)、double(8 字节,双精度浮点数)、char(2 字节,用于存储单个字符)、boolean(1 位,存储 true 或 false)。
基本数据类型和包装类型的区别?
基本数据类型在栈内存中直接存储值,占用空间小、性能高,如 int a = 5; 而包装类型是对象,存储在堆内存中,像 Integer b = new Integer (5); 包装类型提供了很多实用方法,如 Integer.parseInt (),并且在集合类(如 ArrayList)中只能存储对象,所以要用包装类型。自动装箱和拆箱机制使得基本类型和包装类型转换更方便,但要注意装箱后的对象可能存在内存占用问题。
二、面向对象
解释一下 Java 中的多态?
多态是指同一个行为具有多个不同表现形式或形态的能力。在 Java 中有两种实现方式:方法重载(Overloading)和方法重写(Overriding)。方法重载是在一个类中,多个方法同名但参数列表不同(参数个数、类型、顺序不同),编译器根据传入参数来决定调用哪个方法;方法重写发生在子类与父类之间,子类重写父类的同名同参数方法,运行时根据对象实际类型来决定调用父类还是子类的方法,多态使得代码更灵活、可扩展性强,如 Shape 类作为父类,有 draw 方法,Circle、Rectangle 等子类重写 draw 方法,通过 Shape 类型引用指向不同子类对象,调用 draw 方法能展现不同图形绘制效果。
什么是抽象类和接口,区别是什么?
抽象类使用 abstract 关键字修饰,包含抽象方法(只有方法声明没有方法体)和普通方法,不能实例化,用于作为一种模板被子类继承,子类继承抽象类必须实现抽象方法,它可以有成员变量、构造方法等,体现一种 “is-a” 的关系,如 Animal 抽象类,有抽象的 eat 方法,Dog 子类继承它并实现 eat 方法。接口使用 interface 关键字定义,所有方法默认是 public abstract 的,变量默认是 public static final 的,接口主要定义一组规范或契约,一个类可以实现多个接口,实现接口必须实现接口中所有方法,接口常用于实现不相关类的相同行为规范,如 Serializable 接口让类具备序列化能力,区别在于抽象类侧重于共性,接口侧重于规范,一个类只能继承一个抽象类但能实现多个接口,接口没有构造方法、不能有实例变量等。
三、异常处理
Java 中的异常体系结构是怎样的?
Java 异常体系的顶层是 Throwable 类,它有两个子类:Error 和 Exception。Error 表示严重的错误,一般是 JVM 内部错误、资源耗尽等,程序通常无法处理,如 OutOfMemoryError;Exception 又分为 checked Exception(受检异常)和 unchecked Exception(非受检异常或运行时异常)。checked Exception 是在编译期必须处理的异常,如 IOException,要么 try-catch 捕获处理,要么在方法声明上 throws 抛出;unchecked Exception 继承自 RuntimeException,如 NullPointerException、ArrayIndexOutOfBoundsException,编译器不强制要求处理,通常是代码逻辑问题导致,在运行时抛出,合理的代码应该尽量避免这类异常发生。
请举例说明 try-catch-finally 的用法?
例如读取文件操作:
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class ExceptionDemo {
  public static void main(String\[] args) {
  FileReader reader = null;
  try {
  File file = new File("test.txt");
  reader = new FileReader(file);
  int data = reader.read();
  while (data!= -1) {
  System.out.print((char) data);
  data = reader.read();
  }
  } catch (IOException e) {
  e.printStackTrace();
  } finally {
  if (reader!= null) {
  try {
  reader.close();
  } catch (IOException e) {
  e.printStackTrace();
  }
  }
  }
  }
}
在上述代码中,try 块中尝试打开并读取文件,如果出现 IOException 异常,会被 catch 块捕获并打印堆栈信息,finally 块无论是否发生异常,都会执行关闭文件流操作,确保资源释放,避免资源泄露。
Java 进阶面试题
一、集合框架
List、Set、Map 的区别?
List 是有序、可重复的集合,常见实现类有 ArrayList 和 LinkedList。ArrayList 基于数组实现,随机访问快,插入删除慢(涉及数组元素移动);LinkedList 基于链表实现,插入删除快(只需修改指针),随机访问慢。Set 是无序、不可重复的集合,HashSet 根据对象的 hashCode 和 equals 方法来保证元素唯一性,TreeSet 则是基于红黑树实现的有序 Set,可按照自然顺序或自定义比较器排序。Map 是键值对集合,用于存储和查找键值映射关系,HashMap 是最常用实现,基于哈希表实现,查询快,允许 null 键和值,但线程不安全;TreeMap 基于红黑树实现,键有序,常用于需要排序的场景,Hashtable 类似 HashMap 但线程安全,不过不建议使用,因为性能较差且方法基本都加了 synchronized 关键字。
说一下 ArrayList 的扩容机制?
ArrayList 初始容量默认为 10,当添加元素导致容量不足时,会进行扩容。它的扩容逻辑是新容量 = 旧容量 + 旧容量 >> 1(即旧容量的 1.5 倍),例如初始容量 10,当添加第 11 个元素时,会创建一个新数组,容量变为 15,然后将原数组元素复制到新数组中,这个过程涉及数组复制,频繁扩容会影响性能,所以如果能预估元素个数,可在初始化 ArrayList 时指定合适容量,如 new ArrayList<>(20)。
二、多线程
创建线程有哪几种方式?
第一种是继承 Thread 类,重写 run 方法,然后创建子类对象并调用 start 方法启动线程,如:
class MyThread extends Thread {
  @Override
  public void run() {
  System.out.println("线程执行");
  }
}
public class ThreadDemo {
  public static void main(String\[] args) {
  MyThread thread = new MyThread();
  thread.start();
  }
}
第二种是实现 Runnable 接口,实现 run 方法,再将实现类对象作为参数传入 Thread 构造函数创建线程并启动,这种方式避免了单继承局限,如:
class MyRunnable implements Runnable {
  @Override
  public void run() {
  System.out.println("通过实现 Runnable 接口的线程执行");
  }
}
public class RunnableDemo {
  public static void main(String\[] args) {
  MyRunnable runnable = new MyRunnable();
  Thread thread = new Thread(runnable);
  thread.start();
  }
}
还有一种是通过实现 Callable 接口结合 FutureTask 来创建线程,Callable 接口有返回值且可抛出异常,FutureTask 用于获取线程执行结果,如:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable\<Integer> {
  @Override
  public Integer call() throws Exception {
  return 1 + 1;
  }
}
public class CallableDemo {
  public static void main(String\[] args) throws Exception {
  MyCallable callable = new MyCallable();
  FutureTask\<Integer> futureTask = new FutureTask<>(callable);
  Thread thread = new Thread(futureTask);
  thread.start();
  Integer result = futureTask.get();
  System.out.println("结果:" + result);
  }
}
说一下 synchronized 关键字的用法和原理?
synchronized 可以修饰方法(实例方法、静态方法)和代码块。修饰实例方法时,锁定的是当前实例对象,同一时间只有一个线程能访问该实例的这个同步方法,如:
public class SynchronizedDemo {
  public synchronized void method() {
  // 同步代码块
  }
}
修饰静态方法时,锁定的是类对象,因为静态方法属于类,如:
public class SynchronizedDemo {
  public static synchronized void staticMethod() {
  // 同步代码块
  }
}
修饰代码块时,需要指定锁对象,如:
public class SynchronizedDemo {
  private Object lock = new Object();
  public void blockMethod() {
  synchronized (lock) {
  // 同步代码块
  }
  }
}
其原理基于对象头的 Mark Word,在进入同步代码块或方法时,线程会获取锁,将 Mark Word 中的锁标志位修改,若其他线程尝试获取已被锁定的对象锁,会进入阻塞状态,等待锁释放,释放锁时再修改 Mark Word 状态,确保同一时间只有一个线程能进入临界区操作共享资源,保证数据一致性。
三、JVM 相关
简述 Java 内存模型(JVM)?
Java 内存模型主要分为堆、栈、方法区、程序计数器、本地方法栈几个区域。堆是最大的一块内存区域,用于存放对象实例,垃圾回收主要在堆中进行;栈用于存储局部变量、方法调用信息等,每个线程有自己独立的栈空间,栈帧随着方法调用入栈、返回出栈;方法区用于存储已被加载的类信息、常量、静态变量等,在 JDK 8 之后,方法区的实现从永久代变为元数据区;程序计数器记录当前线程执行的字节码指令行号,保证线程切换后能恢复到正确执行位置;本地方法栈与栈类似,不过它用于存储本地方法调用信息,即调用 Native 方法时使用。不同区域有不同的生命周期和用途,相互协作保证 Java 程序运行。
什么是垃圾回收(GC),常用的垃圾回收算法有哪些?
垃圾回收是 JVM 自动管理内存,识别并回收不再使用的对象所占用空间的过程。常用垃圾回收算法有:标记 - 清除算法,先标记出所有需要回收的对象,然后统一清除,缺点是会产生内存碎片;复制算法,将内存分为大小相等的两块,每次只使用一块,回收时把存活对象复制到另一块,再清空使用过的那块,优点是简单高效、不会产生内存碎片,但内存利用率低;标记 - 整理算法,标记存活对象后,将存活对象向一端移动,然后清理掉边界以外的内存,解决了标记 - 清除的碎片问题,综合性能较好;分代收集算法,根据对象存活周期将堆分为新生代、老年代,新生代对象朝生夕灭,常用复制算法,老年代对象存活时间长,常用标记 - 清除或标记 - 整理算法,这种算法结合了不同算法优势,是目前主流的垃圾回收策略。
Java 框架相关面试题(以 Spring 为例)
一、Spring 基础
什么是 Spring 框架,它的核心模块有哪些?
Spring 是一个开源的轻量级 Java 企业级应用开发框架,它旨在简化企业级应用开发,提供了依赖注入(Dependency Injection,DI)、面向切面编程(Aspect Oriented Programming,AOP)等核心功能。Spring 的核心模块包括:Core 模块,提供了框架的基本功能,如 IoC 容器基础实现;Beans 模块,负责对象创建、配置和管理;Context 模块,构建在 Core 和 Beans 模块之上,提供了一种访问应用程序组件的方式,定义了应用上下文环境;AOP 模块,实现面向切面编程,用于分离系统中的横切关注点,如日志记录、事务管理;Web 模块,支持 Web 应用开发,包括 Spring MVC 等,方便构建 Web 应用的控制器、视图等组件;ORM 模块,对主流的对象关系映射(Object Relational Mapping,ORM)框架提供支持,便于在 Spring 中集成数据库操作。
请解释一下 Spring 的 IoC(控制反转)和 DI(依赖注入)?
IoC 是一种设计思想,它将对象的创建、管理、生命周期控制的权力从程序代码本身反转到了外部容器(如 Spring 容器)。传统代码中,对象是在需要的地方直接 new 出来,而在 IoC 模式下,对象由容器统一创建并管理,程序代码只需从容器获取所需对象即可。DI 是 IoC 的一种具体实现方式,它是指容器在创建对象时,将对象所依赖的其他对象通过构造函数、 setter 方法等方式注入进去。例如,有一个 UserService 类依赖于 UserDao 类,在 Spring 中,不用在 UserService 内部 new UserDao,而是通过配置(如 XML 配置或注解)让 Spring 容器知道在创建 UserService 时注入一个合适的 UserDao 实例,这样降低了代码耦合度,方便测试、维护和扩展。
二、Spring MVC
简述 Spring MVC 的工作流程?
首先,客户端发起请求,请求到达前端控制器 DispatcherServlet,它是 Spring MVC 的核心,负责接收所有请求。DispatcherServlet 根据请求的 URL 信息,查找对应的 HandlerMapping,HandlerMapping 用于确定处理该请求的具体 Handler(通常是一个 Controller 方法),找到 Handler 后,DispatcherServlet 将请求交给对应的 Handler 进行处理,Handler 处理完业务逻辑后,返回一个 ModelAndView 对象,该对象包含视图名称和模型数据,接着 DispatcherServlet 拿着 ModelAndView 对象找到合适的 ViewResolver,ViewResolver 根据视图名称解析出具体的视图(如 JSP、Thymeleaf 等页面模板),最后将模型数据填充到视图中并返回给客户端,完成一次请求响应过程。
Spring MVC 中 @RequestMapping 注解的作用是什么?
@RequestMapping 注解用于将一个 URL 路径映射到一个具体的 Controller 方法上,它可以标注在类级别和方法级别。标注在类级别时,相当于给类下所有方法设置了一个公共的 URL 前缀,如:
@Controller
@RequestMapping("/user")
public class UserController {
  // 类下方法
}
这里类下所有方法的 URL 都以 “/user” 开头,在方法上再使用 @RequestMapping 可以进一步细化 URL,如:
@Controller
@RequestMapping("/user")
public class UserController {
  @RequestMapping("/add")
  public String addUser() {
  // 处理添加用户逻辑
  return "success";
  }
}
这个 “addUser” 方法对应的 URL 就是 “/user/add”,通过这种方式方便灵活地配置请求与方法的映射关系,实现 RESTful 风格编程等多种需求。
输出为markdown源码