Java 基础与面向对象
Q1:哪个不属于面向对象特性?
题目: 以下哪个不属于面向对象的特性?
- 复用 ❌(正确答案)
- 封装 ✔
- 多态 ✔
- 继承 ✔
答案: 复用
知识点解析:
面向对象的四大特性:
- 封装(Encapsulation)
- 将数据和方法封装在类中,隐藏内部实现细节
- 通过访问修饰符(private、protected、public)控制访问权限
- 提供公共接口供外部使用
public class BankAccount {
private double balance; // 封装:私有字段
public void deposit(double amount) { // 公共接口
if (amount > 0) {
balance += amount;
}
}
public double getBalance() {
return balance;
}
}
- 继承(Inheritance)
- 子类继承父类的属性和方法
- 实现代码复用
- 支持方法重写(Override)
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal { // 继承
@Override
public void eat() { // 方法重写
System.out.println("Dog is eating");
}
}
- 多态(Polymorphism)
- 同一接口可以有不同的实现
- 运行时多态(方法重写)
- 编译时多态(方法重载)
// 运行时多态
Animal animal = new Dog(); // 父类引用指向子类对象
animal.eat(); // 调用子类重写的方法
// 编译时多态(方法重载)
public void print(int i) { }
public void print(String s) { }
- 抽象(Abstraction)
- 抽象类和接口
- 定义规范,隐藏实现细节
- 通过抽象方法定义契约
// 抽象类
abstract class Shape {
abstract double area(); // 抽象方法
}
// 接口
interface Drawable {
void draw(); // 接口方法
}
扩展知识点:
- 复用(Reusability):是面向对象的优势,但不是特性。通过继承、组合等方式实现代码复用
- 组合 vs 继承:优先使用组合而非继承,提高灵活性
- SOLID 原则:单一职责、开闭原则、里氏替换、接口隔离、依赖倒置
Java 并发与多线程
Q2:Thread 和 Runnable 正确说法?
题目: 关于 Thread 和 Runnable,以下哪个说法正确?
Thread.start()才能开启新线程 ✔(正确答案)Thread.run()可以开启新线程 ❌- 实现 Runnable 接口必须重写 run() 方法 ✔
- Thread 类实现了 Runnable 接口 ✔
答案: Thread.start() 才能开启新线程
知识点解析:
Thread 和 Runnable 的关系:
// Thread 类实现了 Runnable 接口
public class Thread implements Runnable {
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
创建线程的方式:
-
继承 Thread 类
class MyThread extends Thread { @Override public void run() { System.out.println("Thread running"); } } MyThread thread = new MyThread(); thread.start(); // 开启新线程 -
实现 Runnable 接口(推荐)
class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable running"); } } Thread thread = new Thread(new MyRunnable()); thread.start(); // 开启新线程 -
实现 Callable 接口(有返回值)
class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "Result"; } } FutureTask<String> futureTask = new FutureTask<>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); String result = futureTask.get(); // 获取返回值
start() vs run():
start():启动新线程,JVM 调用run()方法run():在当前线程中执行,不会创建新线程
Thread thread = new Thread(() -> System.out.println("Running"));
thread.start(); // 新线程执行,输出 "Running"
thread.run(); // 当前线程执行,输出 "Running"(但不是新线程)
Q3:哪个能让线程立刻停止?
题目: 以下哪个方法能让线程立刻停止?
stop()(已废弃,但确实立即终止)✔interrupt()❌suspend()(已废弃)✔yield()❌
答案: stop() 和 suspend()(都已废弃)
知识点解析:
已废弃的方法:
-
**Thread.stop()**(已废弃)
- 立即终止线程,可能导致数据不一致
- 释放所有锁,可能导致对象状态损坏
-
**Thread.suspend()**(已废弃)
- 挂起线程,可能导致死锁
- 如果线程持有锁,其他线程无法获取
正确的线程终止方式:
-
使用中断标志(推荐)
class MyThread extends Thread { private volatile boolean running = true; @Override public void run() { while (running) { // 检查中断标志 if (Thread.currentThread().isInterrupted()) { break; } // 执行业务逻辑 } } public void stopThread() { running = false; } } -
使用 interrupt()
Thread thread = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 重新设置中断标志 break; } } }); thread.start(); thread.interrupt(); // 中断线程
Q4:以下语句哪个会导致空指针?
题目: 以下哪个语句可能导致空指针异常?
(s != null) & (s.length() > 0)✔(单 & 不短路)(s != null) && (s.length() > 0)❌(短路与)(s == null) || (s.length() > 0)❌(短路或)
答案: (s != null) & (s.length() > 0)
知识点解析:
逻辑运算符:
- 短路运算符(&&、||)
&&:左边为 false 时,右边不执行||:左边为 true 时,右边不执行
String s = null;
// 安全:短路与,s 为 null 时不执行 s.length()
if (s != null && s.length() > 0) {
// 不会抛出异常
}
// 危险:非短路与,s 为 null 时仍执行 s.length()
if (s != null & s.length() > 0) { // 抛出 NullPointerException
// 会抛出异常
}
- 位运算符(&、|)
&:按位与,两边都执行|:按位或,两边都执行
// 位运算符示例
int a = 5; // 101
int b = 3; // 011
int result = a & b; // 001 = 1
Q5:ReentrantLock 默认是?
题目: ReentrantLock 默认是什么锁?
- 公平锁 ❌
- 非公平锁(non-fair)✔(正确答案)
- 可重入锁 ✔
- 不可重入锁 ❌
答案: 非公平锁
知识点解析:
ReentrantLock 的特点:
- 可重入锁(Reentrant)
- 同一线程可以多次获取同一把锁
- 避免死锁
ReentrantLock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
method2(); // 可以再次获取锁
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock(); // 同一线程可以再次获取
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
- 公平锁 vs 非公平锁
// 非公平锁(默认)
ReentrantLock lock = new ReentrantLock(); // 非公平锁
// 公平锁
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
公平锁 vs 非公平锁对比:
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 按照等待时间顺序 | 抢占式 |
| 性能 | 吞吐量低 | 吞吐量高 |
| 适用场景 | 需要公平性 | 追求性能 |
公平锁实现原理:
- 使用队列维护等待线程
- 按照 FIFO 顺序获取锁
非公平锁实现原理:
- 新线程可以直接尝试获取锁
- 如果获取失败才进入队列
Q6:ReentrantLock.tryLock() 行为?
题目: ReentrantLock.tryLock() 的行为是什么?
tryLock()不阻塞,立即返回 ✔(正确答案)tryLock()会阻塞等待 ❌tryLock()必须配合超时使用 ❌
答案: tryLock() 不阻塞,立即返回
知识点解析:
ReentrantLock 的获取锁方法:
-
**lock()**:阻塞等待获取锁
ReentrantLock lock = new ReentrantLock(); lock.lock(); // 阻塞等待,直到获取锁 try { // 临界区 } finally { lock.unlock(); } -
**tryLock()**:尝试获取锁,不阻塞
ReentrantLock lock = new ReentrantLock(); if (lock.tryLock()) { // 立即返回 true/false try { // 临界区 } finally { lock.unlock(); } } else { // 获取锁失败,执行其他逻辑 } -
**tryLock(timeout, unit)**:带超时的尝试获取锁
ReentrantLock lock = new ReentrantLock(); try { if (lock.tryLock(5, TimeUnit.SECONDS)) { // 最多等待 5 秒 try { // 临界区 } finally { lock.unlock(); } } else { // 超时未获取到锁 } } catch (InterruptedException e) { // 处理中断 }
扩展知识点:
-
synchronized vs ReentrantLock
- synchronized:JVM 层面,自动释放锁
- ReentrantLock:API 层面,需要手动释放,功能更强大
-
Condition 条件变量
ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); // 等待条件 lock.lock(); try { while (!conditionMet) { condition.await(); // 释放锁并等待 } } finally { lock.unlock(); } // 通知条件 lock.lock(); try { conditionMet = true; condition.signal(); // 唤醒等待的线程 } finally { lock.unlock(); }
集合与容器类
Q7:哪个线程安全、且不允许 null?
题目: 以下哪个集合类线程安全且不允许 null 值?
HashMap❌(线程不安全,允许 null)ConcurrentHashMap✔(线程安全,不允许 null)Hashtable✔(线程安全,不允许 null)TreeMap❌(线程不安全,不允许 null)
答案: ConcurrentHashMap 和 Hashtable
知识点解析:
HashMap vs ConcurrentHashMap vs Hashtable:
| 特性 | HashMap | ConcurrentHashMap | Hashtable |
|---|---|---|---|
| 线程安全 | ❌ | ✔ | ✔ |
| 允许 null key | ✔ | ❌ | ❌ |
| 允许 null value | ✔ | ❌ | ❌ |
| 锁粒度 | - | 分段锁/CAS | 表级锁 |
| 性能 | 高 | 较高 | 低 |
| 迭代器 | Fail-Fast | Fail-Safe | Fail-Fast |
ConcurrentHashMap 实现原理:
-
JDK 1.7:分段锁(Segment)
// 将数据分成多个段,每个段独立加锁 static final class Segment<K,V> extends ReentrantLock implements Serializable { // 每个段是一个小的 HashMap } -
JDK 1.8+:CAS + synchronized
// 使用 CAS 和 synchronized 实现 // 锁粒度更细,性能更好 final V putVal(K key, V value, boolean onlyIfAbsent) { // 使用 CAS 尝试插入 // 如果失败,使用 synchronized 锁住链表头节点 }
代码示例:
// HashMap(线程不安全)
Map<String, String> map = new HashMap<>();
map.put(null, "value"); // 允许 null key
map.put("key", null); // 允许 null value
// ConcurrentHashMap(线程安全)
ConcurrentHashMap<String, String> concurrentMap = new ConcurrentHashMap<>();
// concurrentMap.put(null, "value"); // 抛出 NullPointerException
// concurrentMap.put("key", null); // 抛出 NullPointerException
// 多线程安全使用
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
concurrentMap.put("key" + j, "value" + j);
}
}).start();
}
扩展知识点:
- CopyOnWriteArrayList:写时复制,适合读多写少
- **Collections.synchronizedMap()**:将普通 Map 包装成线程安全的
- ConcurrentHashMap 的 size() 方法:是近似值,不是精确值
JVM 内存模型
Q9:方法局部变量存放在哪里?
题目: 方法中的局部变量存放在哪里?
- 堆(Heap)❌
- 方法区(Method Area)❌
- 线程独享的栈(Stack)✔(正确答案)
- 程序计数器(PC Register)❌
答案: 线程独享的栈
知识点解析:
JVM 内存区域:
-
线程共享区域:
- 堆(Heap):对象实例、数组
- 方法区(Method Area):类信息、常量、静态变量
- 元空间(Metaspace):JDK 1.8+,替代永久代
-
线程私有区域:
- JVM 栈(Stack):局部变量、方法参数、返回值
- 本地方法栈(Native Method Stack):Native 方法
- 程序计数器(PC Register):当前执行的字节码指令地址
栈内存结构:
栈帧(Stack Frame):
+-------------------+
| 局部变量表(Local Variables)|
+-------------------+
| 操作数栈(Operand Stack) |
+-------------------+
| 动态链接(Dynamic Linking) |
+-------------------+
| 方法返回地址(Return Address)|
+-------------------+
代码示例:
public class MemoryExample {
private static int staticVar = 1; // 方法区(静态变量)
private int instanceVar = 2; // 堆(实例变量)
public void method() {
int localVar = 3; // 栈(局部变量)
String str = "hello"; // 栈(引用),堆(对象)
}
}
内存分配示例:
public void example() {
int a = 10; // 栈:局部变量 a = 10
String s = "hello"; // 栈:引用 s,堆:字符串对象 "hello"
Object obj = new Object(); // 栈:引用 obj,堆:Object 对象
}
扩展知识点:
- 栈溢出(StackOverflowError):递归调用过深
- 堆溢出(OutOfMemoryError):对象过多,堆内存不足
- 方法区溢出:类加载过多,常量池过大
- 内存泄漏:对象引用未释放,导致无法 GC
Spring & Spring Boot
Q10:@Transactional 不能配置的是什么?
题目: @Transactional 注解不能配置以下哪个选项?
- 事务传播行为(propagation)❌
- 事务隔离级别(isolation)❌
- 异常类注解数组回滚(rollbackFor)❌
- 使用”异常类注解数组”回滚(错误方式)✔
答案: 使用”异常类注解数组”回滚(这是错误的配置方式)
知识点解析:
@Transactional 常用配置:
@Transactional(
propagation = Propagation.REQUIRED, // 传播行为
isolation = Isolation.READ_COMMITTED, // 隔离级别
timeout = 30, // 超时时间(秒)
readOnly = false, // 是否只读
rollbackFor = Exception.class, // 回滚的异常类型
noRollbackFor = RuntimeException.class // 不回滚的异常类型
)
public void transferMoney() {
// 业务逻辑
}
事务传播行为(Propagation):
| 传播行为 | 说明 |
|---|---|
| REQUIRED(默认) | 如果存在事务则加入,否则创建新事务 |
| REQUIRES_NEW | 总是创建新事务,挂起当前事务 |
| SUPPORTS | 如果存在事务则加入,否则非事务执行 |
| NOT_SUPPORTED | 非事务执行,挂起当前事务 |
| MANDATORY | 必须在事务中,否则抛出异常 |
| NEVER | 不能在事务中,否则抛出异常 |
| NESTED | 嵌套事务 |
代码示例:
@Service
public class UserService {
@Autowired
private UserDao userDao;
// 正确配置
@Transactional(
rollbackFor = Exception.class, // 所有异常都回滚
propagation = Propagation.REQUIRED
)
public void createUser(User user) {
userDao.insert(user);
if (user.getAge() < 0) {
throw new IllegalArgumentException("年龄不能为负");
}
}
// 错误配置示例(不存在这种写法)
// @Transactional(rollbackFor = @Exception.class) // 错误:不能使用注解数组
}
Q11:自动配置错误的描述?
题目: 关于 Spring Boot 自动配置,以下哪个描述是错误的?
- 自动配置不会通过
@EnableAutoConfiguration关闭 ❌(错误)✔ - 自动配置基于类路径和条件注解
- 可以通过
spring.autoconfigure.exclude排除自动配置 - 自动配置类通常以
AutoConfiguration结尾
答案: 自动配置不会通过 @EnableAutoConfiguration 关闭(这是错误的描述)
知识点解析:
Spring Boot 自动配置原理:
-
@EnableAutoConfiguration
@SpringBootApplication // 包含 @EnableAutoConfiguration public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } -
自动配置类示例
@Configuration @ConditionalOnClass(DataSource.class) @EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceAutoConfiguration { // 自动配置逻辑 } -
关闭自动配置的方式
// 方式1:使用 @SpringBootApplication 的 exclude @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class Application { } // 方式2:使用配置文件 // application.yml spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration // 方式3:使用 @EnableAutoConfiguration 的 exclude @EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) public class Application { }
自动配置原理:
- 基于类路径:检测类路径中是否存在特定类
- 基于条件注解:@ConditionalOnClass、@ConditionalOnBean 等
- 基于属性配置:@EnableConfigurationProperties
Q12:Bean 依赖顺序,A 在 B 后初始化?
题目: 如何让 BeanA 在 BeanB 之后初始化?
- 在 BeanA 上写
@DependsOn("beanB")✔(正确答案) - 在 BeanB 上写
@DependsOn("beanA")❌ - 使用 @Order 注解 ❌
- 使用 @Primary 注解 ❌
答案: 在 BeanA 上写 @DependsOn("beanB")
知识点解析:
@DependsOn 注解:
@Component
@DependsOn("beanB") // BeanA 依赖于 BeanB,BeanB 先初始化
public class BeanA {
@Autowired
private BeanB beanB;
}
@Component
public class BeanB {
public BeanB() {
System.out.println("BeanB initialized");
}
}
Bean 初始化顺序控制:
-
@DependsOn:明确声明依赖关系
@Component @DependsOn({"beanB", "beanC"}) // 依赖多个 Bean public class BeanA { } -
@Order:控制 Bean 的加载顺序(主要用于 List 注入)
@Component @Order(1) // 数字越小,优先级越高 public class BeanA implements Ordered { @Override public int getOrder() { return 1; } } -
@Primary:指定主要 Bean(当有多个同类型 Bean 时)
@Component @Primary // 优先注入 public class PrimaryBean implements Service { } @Component public class SecondaryBean implements Service { }
扩展知识点:
- Bean 生命周期:实例化 → 属性注入 → 初始化 → 使用 → 销毁
- @PostConstruct:初始化方法
- @PreDestroy:销毁方法
- InitializingBean 接口:实现 afterPropertiesSet() 方法
设计模式
Q13:用于动态给对象添加功能的是?
题目: 以下哪个设计模式用于动态给对象添加功能?
- 装饰器模式(Decorator)✔(正确答案)
- 代理模式(Proxy)❌
- 适配器模式(Adapter)❌
- 策略模式(Strategy)❌
答案: 装饰器模式
知识点解析:
装饰器模式(Decorator Pattern):
装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构。它是作为现有类的一个包装。
// 组件接口
interface Coffee {
String getDescription();
double getCost();
}
// 具体组件
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double getCost() {
return 5.0;
}
}
// 装饰器抽象类
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double getCost() {
return coffee.getCost();
}
}
// 具体装饰器
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
@Override
public double getCost() {
return coffee.getCost() + 2.0;
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Sugar";
}
@Override
public double getCost() {
return coffee.getCost() + 1.0;
}
}
// 使用示例
Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription()); // Simple Coffee, Milk, Sugar
System.out.println(coffee.getCost()); // 8.0
Java 中的装饰器模式应用:
java.io包中的流类(InputStream、OutputStream 等)java.util.Collections中的装饰器方法
Q14:代理 vs 装饰
题目: 代理模式和装饰器模式的区别是什么?
答案对比:
| 区别 | 代理模式 | 装饰器模式 |
|---|---|---|
| 目的 | 控制访问 | 增强功能 |
| 关注点 | 控制对象的访问 | 动态添加功能 |
| 是否改变对象行为 | 否(保持原行为) | 是(增强行为) |
| 关系 | 代理类和被代理类实现同一接口 | 装饰器和被装饰类实现同一接口 |
| 使用场景 | 远程代理、虚拟代理、保护代理 | 动态添加功能,如 Java IO 流 |
代理模式示例:
// 接口
interface Image {
void display();
}
// 真实对象
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading " + filename);
}
@Override
public void display() {
System.out.println("Displaying " + filename);
}
}
// 代理对象
class ProxyImage implements Image {
private RealImage realImage;
private String filename;
public ProxyImage(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filename); // 延迟加载
}
realImage.display();
}
}
装饰器模式示例:(见 Q13)
扩展知识点:
- 适配器模式:改变接口,使不兼容的类可以一起工作
- 桥接模式:将抽象与实现分离,使它们可以独立变化
- 外观模式:为子系统提供统一的接口
Q15:状态模式关系?
题目: 状态模式中,上下文(Context)和状态(State)的关系是什么?
- 上下文持有状态对象 ✔(正确答案)
- 状态持有上下文对象 ❌
- 上下文和状态相互独立 ❌
- 状态继承上下文 ❌
答案: 上下文持有状态对象
知识点解析:
状态模式(State Pattern):
允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
// 状态接口
interface State {
void handle(Context context);
}
// 具体状态
class ConcreteStateA implements State {
@Override
public void handle(Context context) {
System.out.println("State A handling");
context.setState(new ConcreteStateB()); // 切换到下一个状态
}
}
class ConcreteStateB implements State {
@Override
public void handle(Context context) {
System.out.println("State B handling");
context.setState(new ConcreteStateA()); // 切换回上一个状态
}
}
// 上下文类(持有状态对象)
class Context {
private State state; // 上下文持有状态对象
public Context(State state) {
this.state = state;
}
public void setState(State state) {
this.state = state;
}
public void request() {
state.handle(this); // 委托给状态对象处理
}
}
// 使用示例
Context context = new Context(new ConcreteStateA());
context.request(); // State A handling
context.request(); // State B handling
状态模式结构:
- Context(上下文):维护一个 State 实例,定义当前状态
- State(状态接口):定义状态的行为
- ConcreteState(具体状态):实现特定状态的行为
扩展知识点:
- 策略模式:策略可以互换,状态转换有规律
- 状态机:有限状态机(FSM)的实现
- 应用场景:订单状态、游戏角色状态、工作流等
数据库 & SQL
Q16:drop / truncate / delete 对比
题目: 关于 DROP、TRUNCATE、DELETE 的区别,以下哪个说法正确?
delete可加 WHERE ✔(正确答案)truncate可加 WHERE ❌drop可加 WHERE ❌- 三者都可以回滚 ❌
答案: delete 可加 WHERE
知识点解析:
DROP、TRUNCATE、DELETE 对比:
| 特性 | DELETE | TRUNCATE | DROP |
|---|---|---|---|
| 类型 | DML(数据操作语言) | DDL(数据定义语言) | DDL |
| WHERE 子句 | 支持 | 不支持 | 不支持 |
| 回滚 | 可以(事务内) | 不可以(隐式提交) | 不可以 |
| 删除内容 | 删除数据 | 删除所有数据 | 删除表结构 |
| 速度 | 慢(逐行删除) | 快(直接删除数据页) | 最快 |
| 自增 ID | 不重置 | 重置 | 删除表 |
| 触发器 | 触发 | 不触发 | 不触发 |
| 锁 | 行锁 | 表锁 | 表锁 |
代码示例:
-- DELETE:删除指定数据
DELETE FROM users WHERE id = 1; -- 可以加 WHERE
DELETE FROM users; -- 删除所有数据(慢)
-- TRUNCATE:清空表数据
TRUNCATE TABLE users; -- 不能加 WHERE,清空所有数据(快)
-- DROP:删除表结构
DROP TABLE users; -- 删除整个表
使用场景:
- DELETE:需要删除部分数据,需要回滚
- TRUNCATE:快速清空表,不需要回滚
- DROP:删除整个表结构
Q17:聚簇索引正确的是?
题目: 关于聚簇索引,以下哪个说法正确?
- 可用排序树实现(即 B+Tree)✔(正确答案)
- 只能有一个聚簇索引 ❌(错误:一个表只能有一个聚簇索引,但这是对的)
- 聚簇索引就是主键索引 ❌(不一定)
- 聚簇索引存储的是指针 ❌(存储的是数据本身)
答案: 可用排序树实现(即 B+Tree)
知识点解析:
聚簇索引(Clustered Index):
聚簇索引的索引和数据存储在一起,索引的叶子节点就是数据行。
特点:
- 一个表只能有一个聚簇索引
- 数据按照索引顺序物理存储
- 通常使用 B+Tree 实现
- 主键默认创建聚簇索引
B+Tree 结构:
[根节点]
/ | \
[内部节点] [内部节点] [内部节点]
/ | \ / | \ / | \
[叶子节点] [叶子节点] [叶子节点]
(存储数据) (存储数据) (存储数据)
聚簇索引 vs 非聚簇索引:
| 特性 | 聚簇索引 | 非聚簇索引 |
|---|---|---|
| 数量 | 一个表只有一个 | 可以有多个 |
| 存储 | 索引和数据在一起 | 索引和数据分离 |
| 叶子节点 | 存储数据行 | 存储指向数据行的指针 |
| 查找速度 | 快(一次查找) | 较慢(两次查找) |
| 实现 | B+Tree | B+Tree |
MySQL InnoDB 中的聚簇索引:
-- 主键自动创建聚簇索引
CREATE TABLE users (
id INT PRIMARY KEY, -- 自动创建聚簇索引
name VARCHAR(50),
email VARCHAR(100)
);
-- 如果没有主键,InnoDB 会:
-- 1. 选择第一个非空唯一索引作为聚簇索引
-- 2. 如果没有,创建一个隐藏的 row_id 作为聚簇索引
扩展知识点:
- B+Tree vs B-Tree:B+Tree 非叶子节点只存储键值,叶子节点存储数据
- 覆盖索引:查询的列都在索引中,不需要回表
- 索引下推:在索引层面过滤数据,减少回表次数
Q18:SQL Join 哪些正确?
题目: 以下哪些 SQL 连接方式是正确的?
- INNER JOIN 只返回两个表中匹配的行,如果在任何一个表中没有匹配的记录,那么这些行不会出现在结果集中。✔
- LEFT JOIN 返回左表中所有行,即使在右表中没有匹配的行。对于右表中没有匹配的左表行,结果是NULL填充对应的行。✔
- RIGHT JOIN 返回右表中所有行,即使在左表中没有匹配的行。对于左表中没有匹配的右表行,结果是NULL填充对应的行。✔
- UNION ALL 操作符用于合并两个或更多SELECT语句的结果集,它会保留所有行,包括重复的行。如果两个查询结果集中有在所有列上都完全相同的行,UNION ALL 会全部保留,不会去重。
(UNION:会自动去除重复的行(去重)
UNION ALL:保留所有行,包括重复的行(不去重,性能更好))✔ - 所以答案是 4 个都对 ✔
答案: 4 个都对
知识点解析:
SQL JOIN 类型:
-
INNER JOIN(内连接)
-- 只返回两个表中匹配的行 SELECT u.name, o.order_id FROM users u INNER JOIN orders o ON u.id = o.user_id; -
LEFT JOIN(左连接)
-- 返回左表所有行,右表匹配的行 SELECT u.name, o.order_id FROM users u LEFT JOIN orders o ON u.id = o.user_id; -- 如果右表没有匹配,返回 NULL -
RIGHT JOIN(右连接)
-- 返回右表所有行,左表匹配的行 SELECT u.name, o.order_id FROM users u RIGHT JOIN orders o ON u.id = o.user_id; -
FULL OUTER JOIN(全外连接)
-- MySQL 不支持,可以用 UNION 实现 SELECT u.name, o.order_id FROM users u LEFT JOIN orders o ON u.id = o.user_id UNION SELECT u.name, o.order_id FROM users u RIGHT JOIN orders o ON u.id = o.user_id; -
UNION / UNION ALL
-- UNION:去重 SELECT name FROM users UNION SELECT name FROM customers; -- UNION ALL:不去重(性能更好) SELECT name FROM users UNION ALL SELECT name FROM customers;
JOIN 性能优化:
- 确保 JOIN 条件有索引
- 小表驱动大表
- 避免在 JOIN 条件中使用函数
扩展知识点:
- CROSS JOIN(笛卡尔积):返回两个表的所有组合
- SELF JOIN(自连接):表与自身连接
- 子查询 vs JOIN:JOIN 通常性能更好
HTTP & 网络
Q19:HTTP 状态码正确描述
题目: 关于 HTTP 状态码,以下哪个描述正确?
- 3xx → 重定向,需要进一步操作 ✔(正确答案)
- 2xx → 客户端错误 ❌
- 4xx → 服务器错误 ❌
- 5xx → 成功 ❌
答案: 3xx → 重定向,需要进一步操作
知识点解析:
HTTP 状态码分类:
1xx(信息性状态码):
100 Continue:客户端应继续请求101 Switching Protocols:协议切换
2xx(成功状态码):
200 OK:请求成功201 Created:资源创建成功204 No Content:请求成功,但无内容返回206 Partial Content:部分内容(用于断点续传)
3xx(重定向状态码):
301 Moved Permanently:永久重定向302 Found:临时重定向304 Not Modified:资源未修改(使用缓存)
4xx(客户端错误状态码):
400 Bad Request:请求错误401 Unauthorized:未授权403 Forbidden:禁止访问404 Not Found:资源不存在405 Method Not Allowed:方法不允许
5xx(服务器错误状态码):
500 Internal Server Error:服务器内部错误502 Bad Gateway:网关错误503 Service Unavailable:服务不可用504 Gateway Timeout:网关超时
状态码速记:
- 1xx:提示信息
- 2xx:成功
- 3xx:重定向,需要进一步操作
- 4xx:客户端错误
- 5xx:服务器错误
常见状态码应用场景:
// RESTful API 状态码使用
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User created = userService.create(user);
return ResponseEntity.status(HttpStatus.CREATED).body(created); // 201
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
if (user == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); // 404
}
return ResponseEntity.ok(user); // 200
}
Q20:TCP/UDP 正确描述
题目: 关于 TCP 和 UDP,以下哪个描述正确?
- TCP 与 UDP 都不是面向连接(错误:TCP 是面向连接的)❌
- TCP 丢包后无法”继续传”(要靠重传)❌
- 同端口可同时监听 TCP 和 UDP ✔(正确答案)
- UDP 保证数据有序 ❌
答案: 同端口可同时监听 TCP 和 UDP
知识点解析:
TCP vs UDP 对比:
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(确认、重传) | 不可靠 |
| 有序性 | 保证有序 | 不保证有序 |
| 速度 | 较慢 | 较快 |
| 头部开销 | 20 字节 | 8 字节 |
| 流量控制 | 有(滑动窗口) | 无 |
| 拥塞控制 | 有 | 无 |
| 应用场景 | HTTP、FTP、SMTP | DNS、视频流、游戏 |
TCP 三次握手:
客户端 服务器
| |
|----SYN (seq=x)-------->|
| |
|<--SYN-ACK(seq=y,ack=x+1)|
| |
|----ACK(ack=y+1)------->|
| |
TCP 四次挥手:
客户端 服务器
| |
|----FIN---------------->|
|<----ACK---------------|
| |
|<----FIN---------------|
|----ACK---------------->|
| |
端口复用:
// TCP 和 UDP 可以使用相同的端口号
// 因为端口是在协议栈中区分的
// TCP 服务器
ServerSocket tcpSocket = new ServerSocket(8080);
// UDP 服务器(可以使用相同端口)
DatagramSocket udpSocket = new DatagramSocket(8080);
TCP 丢包处理:
- TCP 通过确认机制和重传机制保证可靠性
- 发送方发送数据后等待确认(ACK)
- 如果超时未收到确认,会重传数据
- 接收方通过序列号保证数据有序
UDP 特点:
- 无连接:不需要建立连接
- 不可靠:不保证数据到达
- 无状态:不维护连接状态
- 适合实时应用:低延迟
扩展知识点:
- HTTP/1.1 vs HTTP/2:HTTP/2 支持多路复用
- HTTPS:HTTP + TLS/SSL 加密
- WebSocket:全双工通信协议
- QUIC:基于 UDP 的可靠传输协议
线程池 ThreadPoolExecutor
Q21:workQueue 是什么?
题目: ThreadPoolExecutor 中的 workQueue 是什么?
- 阻塞队列 ✔(正确答案)
- 普通队列 ❌
- 同步队列 ❌
- 优先级队列 ❌
答案: 阻塞队列
知识点解析:
ThreadPoolExecutor 核心参数:
ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列(阻塞队列)
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
线程池任务执行流程:
提交任务
↓
核心线程是否已满?
├─ 否 → 创建核心线程执行
└─ 是 → 工作队列是否已满?
├─ 否 → 加入工作队列
└─ 是 → 线程数是否达到最大值?
├─ 否 → 创建新线程执行
└─ 是 → 执行拒绝策略
工作队列类型:
-
ArrayBlockingQueue:有界数组队列
new ThreadPoolExecutor( 5, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100) // 容量为 100 ); -
LinkedBlockingQueue:无界链表队列
new ThreadPoolExecutor( 5, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>() // 无界队列 ); -
SynchronousQueue:同步队列(不存储元素)
new ThreadPoolExecutor( 5, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue<>() // 直接提交,不存储 ); -
PriorityBlockingQueue:优先级队列
new ThreadPoolExecutor( 5, 10, 60L, TimeUnit.SECONDS, new PriorityBlockingQueue<>() // 按优先级执行 );
拒绝策略:
- AbortPolicy(默认):抛出异常
- CallerRunsPolicy:调用者执行
- DiscardPolicy:直接丢弃
- DiscardOldestPolicy:丢弃最老的任务
线程池使用示例:
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, // 线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(100), // 工作队列
new ThreadFactory() { // 线程工厂
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("MyThread-" + t.getId());
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 提交任务
executor.execute(() -> {
System.out.println("Task executed");
});
// 关闭线程池
executor.shutdown();
扩展知识点:
- Executors 工具类:提供常用线程池(但不推荐使用)
- 线程池监控:监控线程池状态、任务数量等
- 线程池调优:根据业务场景调整参数
综合算法题解
题:计算日程需要的最少行数(区间重叠)
题目描述: 给定一组时间区间,计算同时进行的最多会议数(即需要的最少会议室数量)。
方法: 小顶堆 + 排序
算法思路:
- 按开始时间排序所有区间
- 使用小顶堆存储当前正在进行的会议结束时间
- 遍历每个区间:
- 如果堆顶的结束时间 <= 当前区间的开始时间,说明会议室已空,移除堆顶
- 将当前区间的结束时间加入堆
- 更新最大堆大小
代码实现:
import java.util.*;
class Interval {
int start;
int end;
Interval(int start, int end) {
this.start = start;
this.end = end;
}
}
public class MeetingRooms {
public int minMeetingRooms(Interval[] intervals) {
if (intervals == null || intervals.length == 0) {
return 0;
}
// 按开始时间排序
Arrays.sort(intervals, (a, b) -> a.start - b.start);
// 小顶堆,存储结束时间
PriorityQueue<Integer> pq = new PriorityQueue<>();
int ans = 0;
for (Interval interval : intervals) {
// 移除已结束的会议
while (!pq.isEmpty() && pq.peek() <= interval.start) {
pq.poll();
}
// 添加当前会议
pq.offer(interval.end);
// 更新最大会议室数量
ans = Math.max(ans, pq.size());
}
return ans;
}
}
时间复杂度: O(n log n)
- 排序:O(n log n)
- 遍历 + 堆操作:O(n log n)
空间复杂度: O(n)
- 堆最多存储 n 个元素
扩展题目:
- 合并区间:合并重叠的区间
- 插入区间:在已排序的区间列表中插入新区间
- 无重叠区间:移除最少的区间使剩余区间不重叠
总结
核心知识点回顾
1. Java 基础与面向对象
- 面向对象四大特性:封装、继承、多态、抽象
- 复用是优势,不是特性
- SOLID 原则
2. Java 并发与多线程
- Thread 和 Runnable 的区别
- 正确的线程终止方式(中断标志)
- 逻辑运算符的短路特性
- ReentrantLock:可重入、公平/非公平锁
- tryLock() 不阻塞
3. 集合与容器类
- ConcurrentHashMap:线程安全,不允许 null
- HashMap vs ConcurrentHashMap vs Hashtable
4. JVM 内存模型
- 局部变量存放在栈中
- 线程共享:堆、方法区
- 线程私有:栈、程序计数器、本地方法栈
5. Spring & Spring Boot
- @Transactional 配置
- 自动配置原理和关闭方式
- @DependsOn 控制 Bean 初始化顺序
6. 设计模式
- 装饰器模式:动态添加功能
- 代理模式 vs 装饰器模式
- 状态模式:上下文持有状态对象
7. 数据库 & SQL
- DELETE vs TRUNCATE vs DROP
- 聚簇索引:B+Tree 实现
- SQL JOIN 类型
8. HTTP & 网络
- HTTP 状态码分类
- TCP vs UDP
- 端口复用
9. 线程池
- workQueue 是阻塞队列
- 线程池执行流程
- 拒绝策略
10. 算法
- 区间重叠问题:小顶堆 + 排序
面试重点
- 并发编程:线程创建、同步机制、线程池
- 集合框架:HashMap、ConcurrentHashMap 原理
- JVM:内存模型、垃圾回收
- Spring:IoC、AOP、事务管理
- 设计模式:常用设计模式的应用
- 数据库:索引、事务、SQL 优化
- 网络:HTTP、TCP/IP 协议
学习建议
- 理论结合实践:不仅要理解概念,还要会写代码
- 深入源码:阅读 JDK、Spring 等框架源码
- 多做练习:算法题、项目实战
- 总结归纳:建立知识体系,形成思维导图
- 持续学习:关注新技术,保持学习热情
参考资料:
- 《Java 并发编程实战》
- 《深入理解 Java 虚拟机》
- 《Spring 实战》
- 《设计模式:可复用面向对象软件的基础》
- 《高性能 MySQL》
- 《HTTP 权威指南》
Originally published on mlangTse's Blog. View source