面试题解六

101 阅读48分钟

image.png

1. Java中++操作符是否安全?阐述安全(不安全)原因?

在Java中,++操作符在多线程环境下是不安全的。以下是详细分析:


不安全的原因

  1. 非原子性操作
    ++操作符的实际执行分为三个步骤:

    • 读取变量的当前值
    • 递增该值
    • 写入新值到变量
      这三个步骤组合起来并非原子操作。在多线程环境下,多个线程可能同时读取到相同的值并进行递增,导致最终结果小于预期。
  2. 竞态条件(Race Condition)
    当多个线程同时操作共享变量时,线程A可能在读取变量后、写入新值前被其他线程打断,线程B读取到相同的旧值,导致两者递增操作后的结果不一致。


示例验证

以下代码演示多线程下++操作的问题:

public class UnsafeIncrement {
    private int count = 0;

    public void increment() {
        count++; // 非线程安全
    }

    public static void main(String[] args) throws InterruptedException {
        UnsafeIncrement example = new UnsafeIncrement();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) example.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) example.increment();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(example.count); // 通常输出小于20000
    }
}

如何保证线程安全?

  1. 使用同步机制
    通过synchronized关键字或Lock确保操作的原子性:

    public synchronized void increment() {
        count++;
    }
    
  2. 原子类(Atomic Classes)
    使用AtomicInteger等原子类替代基本类型:

    private AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count.incrementAndGet(); // 原子操作
    }
    
  3. volatile无法解决问题
    volatile仅保证可见性(直接读写主内存),但不保证复合操作的原子性。即使变量标记为volatile++操作仍不安全。


例外情况

  • 局部变量:若变量是线程私有的(如方法内的局部变量),不存在多线程共享问题,此时++是安全的。

总结

  • 单线程环境++操作是安全的。
  • 多线程环境:对共享变量使用++操作是不安全的,必须通过同步或原子类保证线程安全。

2. a=a+b与a+=b区别?

在Java中,a = a + ba += b 的主要区别在于类型转换的处理方式,尤其是当操作数类型不一致时。以下是两者的核心区别:

1. 类型转换的隐式与显式

  • a = a + b
    右侧表达式 a + b 的结果类型可能与左侧变量 a 的类型不兼容。如果右侧类型范围更大(如 int 类型变量与 long 类型相加),直接赋值会导致编译错误,需要显式强制转换
    示例

    int a = 5;
    long b = 10;
    a = a + b;          // 编译错误:可能损失精度(long→int)
    a = (int)(a + b);   // 正确,需显式转换
    
  • a += b
    复合赋值运算符 +=隐式将右侧结果转换为左侧变量的类型,无需手动转换。
    示例

    int a = 5;
    long b = 10;
    a += b;             // 正确,等效于 a = (int)(a + b)
    

2. 类型提升时的差异

  • a 是较小类型(如 byteshort),a + b 的结果可能被提升为 int,此时 a = a + b 需要强制转换,而 a += b 自动完成转换。
    示例
    byte a = 1;
    byte b = 2;
    a = a + b;          // 编译错误:int→byte 需强制转换
    a += b;             // 正确,等效于 a = (byte)(a + b)
    

3. 运算符的副作用

  • a 是复杂表达式(如涉及自增操作)时,a += b 仅计算一次 a 的值,而 a = a + b 可能多次计算,导致结果差异。
    示例
    int[] array = {0, 0};
    int i = 0;
    array[i++] += 1;    // array[0] = 1,i = 1
    i = 0;
    array[i++] = array[i++] + 1; // array[1] = 1,i = 2
    

总结

场景a = a + ba += b
类型兼容时无区别,结果相同。无区别,结果相同。
类型不兼容时需显式强制转换,否则编译错误。自动隐式转换,无需手动操作。
类型提升(如byte→int)需强制转换(如 (byte)(a + b))。自动转换,无需干预。

核心结论a += b 隐含类型转换,避免精度损失导致的编译错误,而 a = a + b 需开发者显式处理类型问题。在涉及类型提升或不一致时,这一差异尤为关键。

3. Java中synchronized关键字作用

在 Java 中,synchronized 关键字是 实现线程同步的核心机制,它的核心作用是 保证多线程环境下的代码块或方法的原子性、可见性和有序性。以下是详细分析:


synchronized 的三大核心作用

  1. 原子性(Atomicity)
    确保被修饰的代码块或方法在同一时刻只能被一个线程执行,避免多线程并发操作导致的数据不一致问题。

    • 示例:对共享变量的 ++ 操作(非原子操作)可以通过 synchronized 保护。
  2. 可见性(Visibility)
    线程在释放锁时,会将修改后的共享变量强制刷新到主内存;获取锁时,会清空本地内存并重新从主内存加载变量值。

    • 通过内存屏障(Memory Barrier)实现,确保多线程间数据的一致性。
  3. 有序性(Ordering)
    禁止编译器和处理器对同步代码块内的指令进行重排序优化,保证代码执行顺序与程序顺序一致。


synchronized 的四种用法

用法锁定对象作用范围示例
实例方法当前实例对象(this整个实例方法public synchronized void method() { ... }
静态方法当前类的 Class 对象整个静态方法public static synchronized void method() { ... }
代码块(实例对象锁)指定实例对象(如 obj代码块内部synchronized (this) { ... }
代码块(类锁)类的 Class 对象代码块内部synchronized (MyClass.class) { ... }

底层实现原理

  1. 基于 Monitor 机制

    • 每个 Java 对象都与一个 Monitor(监视器锁) 关联。
    • 当线程进入 synchronized 代码块时,会尝试获取对象的 Monitor 锁:
      • 成功获取锁:计数器 _count 加 1,线程成为锁的持有者。
      • 失败则阻塞,进入锁的等待队列(EntryList)。
  2. 锁的升级与优化(JDK 6+)

    • 偏向锁:减少无竞争时的开销(适用于单线程重复获取锁的场景)。
    • 轻量级锁:通过 CAS 自旋尝试获取锁(适用于短时间锁竞争)。
    • 重量级锁:真正的互斥锁,依赖操作系统 Mutex 实现(长时间竞争时使用)。

适用场景

  1. 保护共享资源的读写操作(如全局计数器、缓存等)。
  2. 实现线程安全的单例模式(双重检查锁)。
  3. 协调多线程任务(如生产者-消费者模型)。

代码示例

public class Counter {
    private int count = 0;

    // 实例方法同步(锁是当前实例对象)
    public synchronized void increment() {
        count++;
    }

    // 静态方法同步(锁是 Counter.class)
    public static synchronized void log() {
        System.out.println("Static method synchronized");
    }

    // 同步代码块(锁是自定义对象)
    private final Object lock = new Object();
    public void reset() {
        synchronized (lock) {
            count = 0;
        }
    }
}

synchronized 的优缺点

优点缺点
语法简单,自动释放锁功能扩展性较差(不可中断、不可超时)
JVM 内置优化(锁升级机制)锁粒度较粗时可能降低并发性能
无需手动管理锁的获取与释放不适用于需要复杂同步策略的场景

对比其他同步工具

  • ReentrantLock
    可中断、支持公平锁、多条件变量,但需要手动释放锁。
  • Atomic
    无锁 CAS 实现,适用于简单原子操作(如 count++)。

总结

synchronized 是 Java 中实现线程同步的基础工具,通过 锁对象 + Monitor 机制 保证多线程安全。在 JDK 6 后,通过 锁升级 机制优化了性能,适用于大部分中低并发场景。在高并发场景下,可结合 ReentrantLockStampedLock 等更灵活的锁机制使用。

4. Java中volatile关键字作用?

在Java中,volatile关键字主要用于解决多线程环境下的可见性有序性问题,但不保证原子性。以下是其核心作用及典型场景:


1. 保证可见性

  • 问题背景
    在多线程中,每个线程可能将共享变量缓存到本地内存(如CPU缓存),导致一个线程修改了变量,其他线程无法立即看到修改结果。

  • volatile的作用
    强制变量的读写直接操作主内存,而非线程的本地缓存。

    • 写操作:立即将修改后的值刷新到主内存。
    • 读操作:直接从主内存读取最新值。
  • 示例

    volatile boolean flag = false;
    
    // 线程A
    flag = true; // 修改后立即写回主内存
    
    // 线程B
    while (!flag); // 每次循环都从主内存读取最新值
    

    flag未用volatile修饰,线程B可能因缓存问题陷入死循环。


2. 禁止指令重排序

  • 问题背景
    编译器和处理器可能对代码的指令进行重排序优化,导致程序执行顺序与代码顺序不一致(在单线程中不影响结果,但多线程中可能引发问题)。

  • volatile的作用
    通过插入内存屏障(Memory Barrier),禁止对volatile变量的读写操作与其他指令重排序。
    具体规则:

    • 写屏障:确保volatile写之前的操作不会被重排到写之后。
    • 读屏障:确保volatile读之后的操作不会被重排到读之前。
  • 典型场景:双重检查锁定(Double-Checked Locking)

    class Singleton {
        private volatile static Singleton instance;
    
        public static Singleton getInstance() {
            if (instance == null) {                     // 第一次检查
                synchronized (Singleton.class) {
                    if (instance == null) {              // 第二次检查
                        instance = new Singleton();      // 对象的初始化可能被重排序
                    }
                }
            }
            return instance;
        }
    }
    

    instance未用volatile修饰,其他线程可能获取到未初始化完成的对象(因指令重排序导致对象引用先于构造函数执行)。volatile禁止这种重排序,保证对象的完整初始化。


3. 不保证原子性

  • 问题背景
    即使变量被volatile修饰,复合操作(如i++)仍可能因多线程竞争导致数据不一致。
    i++实际是读-改-写三步操作,volatile仅保证每次读取的是最新值,但无法保证这三步的原子性。

  • 解决方案

    • 使用synchronizedLock保证代码块原子性。
    • 使用AtomicInteger等原子类(基于CAS实现)。

总结

特性说明
可见性强制线程直接读写主内存,避免缓存不一致。
有序性禁止指令重排序,确保程序执行顺序符合预期。
原子性不保证,需结合锁或原子类。

适用场景

  1. 状态标志位:如线程启停控制的boolean标志。
  2. 单次写入的安全发布:对象初始化完成后对其他线程可见(如单例模式)。
  3. 独立观察变量:如定期更新的配置值,确保所有线程读取最新值。

注意事项

  • 过度使用volatile可能导致性能问题(频繁主内存访问)。
  • 无法替代锁机制解决复合操作的线程安全问题。

理解volatile的底层原理(如内存屏障、JMM规范)能更好地在并发编程中合理使用它。

5. JDK1.8中新增特性有哪些?

在JDK 1.8中,Java引入了多项重要特性,显著提升了开发效率、代码简洁性及并发性能。以下是主要新增特性及其核心要点:


1. Lambda表达式

  • 功能:允许将函数作为方法参数传递,简化匿名内部类的写法,支持函数式编程。
  • 语法(参数列表) -> {代码块},支持类型推断、单参数括号省略、单行代码大括号/return省略。
  • 示例
    list.sort((a, b) -> a - b);  // 排序简化
    list.forEach(x -> System.out.println(x));  // 遍历集合
    
  • 限制:只能引用final或隐式final的局部变量。

2. 函数式接口(Functional Interface)

  • 定义:仅含一个抽象方法的接口,可用@FunctionalInterface注解标记。
  • 核心接口
    • Function<T,R>:接收参数返回结果(如map操作)。
    • Predicate<T>:条件判断(如filter操作)。
    • Consumer<T>:消费参数无返回值(如forEach)。
    • Supplier<T>:无参数返回结果(如延迟初始化)。

3. Stream API

  • 作用:以声明式处理集合数据,支持链式操作(过滤、映射、归约等),可并行执行提升性能。
  • 操作步骤
    1. 创建流:通过集合、数组或Stream.of()生成。
    2. 中间操作filtermapsorted等,延迟执行。
    3. 终止操作collectforEachreduce等,触发计算。
  • 示例
    List<String> names = list.stream()
                            .filter(s -> s.startsWith("A"))
                            .map(String::toUpperCase)
                            .collect(Collectors.toList());  
    

4. 接口的默认方法与静态方法

  • 默认方法:使用default关键字在接口中定义实现方法,避免破坏现有实现类的兼容性。例如List.sort()默认方法。
  • 静态方法:接口可直接通过类名调用静态方法,如Comparator.comparing()

5. 新的日期时间API(JSR 310)

  • 核心类LocalDateLocalTimeLocalDateTimeZonedDateTime等,解决旧DateCalendar的线程安全问题。
  • 优势:不可变、线程安全、链式调用,支持复杂日期计算(如TemporalAdjusters调整日期)。
  • 示例
    LocalDate today = LocalDate.now();
    LocalDate nextWeek = today.plusWeeks(1);  
    

6. Optional类

  • 目的:优雅处理NullPointerException,封装可能为null的值。
  • 方法ofNullable()orElse()ifPresent()等,避免显式判空。
  • 示例
    Optional.ofNullable(user.getName())
            .orElse("default");  
    

7. 并发改进

  • LongAdder:替代AtomicLong,在高并发场景下通过分段CAS提升性能。
  • StampedLock:新增乐观读锁,减少读写锁竞争,避免线程饥饿问题。

8. HashMap优化

  • 红黑树:当链表长度超过8时转为红黑树,查询时间复杂度从O(n)降至O(log n)
  • 哈希碰撞处理:提升高负载下的性能稳定性。

其他特性

  • 方法引用:简化Lambda表达式,语法如ClassName::methodName
  • Nashorn引擎:基于JVM的JavaScript运行时,替代Rhino。
  • PermGen移除:元空间(Metaspace)替代永久代,降低内存溢出风险。

总结

JDK 1.8通过Lambda、Stream API、函数式接口等特性使代码更简洁高效,同时提升了并发性能和数据处理能力。其改进覆盖了语法糖、数据结构、并发工具及API设计,是现代Java开发的核心基础。深入理解这些特性,能显著提升代码质量和开发效率。

6. 常量与变量区别?

在 Java 中,常量(Constant)变量(Variable) 的核心区别在于 值的可变性,同时它们在声明方式、内存管理、使用场景等方面也存在显著差异。以下是详细对比:


1. 定义与核心区别

维度常量(Constant)变量(Variable)
值可变性不可变(一旦初始化后不能修改)可变(可随时重新赋值)
关键字必须使用 final 修饰无特殊关键字
命名规范全大写字母 + 下划线分隔(如 MAX_SIZE驼峰命名法(如 maxSize

2. 声明与初始化

常量

  • 声明方式:必须显式使用 final 修饰符。
  • 初始化时机
    • 静态常量:在声明时或静态代码块中初始化。
    • 实例常量:在声明时、构造方法或实例代码块中初始化。
  • 示例
    // 静态常量(类级别)
    public static final double PI = 3.1415;
    
    // 实例常量(对象级别)
    private final int id;
    public MyClass(int id) {
        this.id = id; // 必须在构造方法中初始化
    }
    

变量

  • 声明方式:无特殊修饰符。
  • 初始化时机:可延迟初始化,随时赋值。
  • 示例
    // 类变量(静态变量)
    private static int counter = 0;
    
    // 实例变量
    private String name;
    
    // 局部变量
    public void method() {
        int localVar = 10; // 必须显式初始化(否则编译报错)
    }
    

3. 内存管理与优化

维度常量变量
存储位置编译时常量(如 static final 基本类型/字符串)直接嵌入字节码,无运行时内存访问值存储在堆或栈内存中,每次访问需读取内存
优化机制编译器可能将常量替换为字面量(如 final int X=5X 直接替换为 5无特殊优化

4. 线程安全性

维度常量变量
线程安全天然线程安全(不可变特性)可能不安全(需通过锁、原子类等同步)

5. 使用场景

场景常量变量
典型用途配置参数(如数据库连接信息)、数学常量(如 PI)、枚举值计数器、临时计算结果、对象状态存储

6. 代码示例对比

public class Example {
    // 常量
    public static final int MAX_USERS = 100; // 静态常量
    private final String DEFAULT_NAME = "Guest"; // 实例常量

    // 变量
    private static int activeUsers = 0; // 类变量
    private String currentUser; // 实例变量

    public void updateUser(String user) {
        // 局部变量
        int attempts = 0;
        attempts = 5; // 变量可重新赋值

        // 常量不可修改(编译报错)
        // DEFAULT_NAME = "Admin"; 
    }
}

7. 注意事项

  • 编译时常量 vs 运行时常量
    • 编译时常量static final 修饰的基本类型或字符串,值在编译期确定(如 static final int X = 10;)。
    • 运行时常量:值在运行时确定(如 final int y = new Random().nextInt();)。
  • final 修饰的引用类型
    final 仅保证引用不可变,对象内部状态仍可能被修改(例如 final List<String> list = new ArrayList<>(); list.add("data"); 是允许的)。

总结

核心差异常量变量
核心特性不可变性可变性
设计目标保护数据不被意外修改,提升代码可读性与安全性存储程序运行中需要变化的数据
适用场景固定值、全局配置、枚举动态数据、临时存储、状态管理

掌握常量与变量的区别,能帮助开发者合理设计数据存储方式,提升代码的健壮性和可维护性。

7. Java中容易造成内存泄露的几种常见情况?

在Java中,内存泄漏(Memory Leak)指的是不再被程序使用的对象因被错误引用而无法被垃圾回收(GC),导致内存占用持续增长,最终可能引发OutOfMemoryError。以下是几种常见的容易导致内存泄漏的场景及其解决方案:


1. 静态集合类长期持有对象引用

  • 原因
    静态集合(如static HashMap, static List)的生命周期与JVM一致,若未及时清理无用对象,会导致对象无法释放。
  • 示例
    public class Cache {
        private static Map<String, Object> cache = new HashMap<>();
    
        public static void add(String key, Object value) {
            cache.put(key, value);
        }
        
        // 缺少remove方法清理无用对象
    }
    
  • 解决
    • 使用弱引用集合(如WeakHashMap),当键不再被强引用时自动回收。
    • 定期清理过期对象,或结合LRU(最近最少使用)策略限制缓存大小。

2. 未关闭的资源(如连接、流)

  • 原因
    数据库连接、文件流、网络连接等资源未显式关闭,可能导致底层资源未被释放(即使对象被回收,资源句柄可能仍被占用)。
  • 示例
    public void readFile() {
        try {
            FileInputStream fis = new FileInputStream("data.txt");
            // 读取文件但未关闭流
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
  • 解决
    • 使用try-with-resources(Java 7+)自动关闭资源:
      try (FileInputStream fis = new FileInputStream("data.txt")) {
          // 自动关闭
      }
      
    • finally块中手动关闭资源。

3. 监听器或回调未注销

  • 原因
    注册的事件监听器或回调未在对象销毁时移除,导致监听器持有对象引用,无法被回收。
  • 示例
    public class EventManager {
        private List<EventListener> listeners = new ArrayList<>();
    
        public void addListener(EventListener listener) {
            listeners.add(listener);
        }
    
        // 缺少removeListener方法
    }
    
  • 解决
    在对象不再使用时显式移除监听器:
    public void removeListener(EventListener listener) {
        listeners.remove(listener);
    }
    

4. 内部类持有外部类引用

  • 原因
    非静态内部类(包括匿名内部类)隐式持有外部类的引用。若内部类对象(如线程、Handler)生命周期较长,会导致外部类无法回收。
  • 示例
    public class Outer {
        private String data = "敏感数据";
    
        public void startAsyncTask() {
            new Thread(new Runnable() { // 匿名内部类隐式持有Outer的引用
                @Override
                public void run() {
                    System.out.println(data); // 引用外部类成员
                }
            }).start();
        }
    }
    
  • 解决
    • 将内部类改为静态内部类(static修饰)。
    • 若需访问外部类成员,通过弱引用(WeakReference)传递。

5. ThreadLocal使用不当

  • 原因
    ThreadLocal变量的值存储在Thread的私有内存中,若线程池中的线程复用且未调用remove()清理,会导致旧数据残留。
  • 示例
    private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
    
    // 线程池复用线程时,未清理ThreadLocal
    executor.execute(() -> {
        userThreadLocal.set(new User());
        // 业务逻辑...
        // 未调用userThreadLocal.remove()
    });
    
  • 解决
    使用后调用remove()清理数据:
    executor.execute(() -> {
        try {
            userThreadLocal.set(new User());
            // 业务逻辑...
        } finally {
            userThreadLocal.remove(); // 必须清理
        }
    });
    

6. 缓存未设置过期或淘汰策略

  • 原因
    使用强引用的缓存(如HashMap)存储大量数据,未设置过期时间或淘汰策略,导致无用数据堆积。
  • 解决
    • 使用WeakHashMap(弱引用键)或SoftReference(软引用值)。
    • 使用第三方缓存库(如Guava Cache、Caffeine)并配置过期策略:
      Cache<String, Object> cache = Caffeine.newBuilder()
              .expireAfterWrite(10, TimeUnit.MINUTES)
              .maximumSize(1000)
              .build();
      

7. 对象重写finalize()方法

  • 原因
    finalize()方法执行时间不确定,对象会被放入Finalizer队列,延迟GC回收,可能导致大量对象堆积。
  • 示例
    public class Resource {
        @Override
        protected void finalize() throws Throwable {
            // 复杂逻辑导致延迟回收
        }
    }
    
  • 解决
    • 避免重写finalize(),改用Cleaner(Java 9+)或手动释放资源。

内存泄漏检测工具

  • VisualVM:监控堆内存使用,生成堆转储(Heap Dump)。
  • MAT(Memory Analyzer Tool):分析堆转储,定位泄漏对象及引用链。
  • JProfiler:实时监控内存分配及GC行为。

总结

场景关键点解决方案
静态集合类长期持有无用对象使用弱引用集合或定期清理
未关闭资源资源句柄未释放try-with-resourcesfinally
监听器未注销回调长期持有对象引用显式移除监听器
内部类引用外部类隐式持有外部类改用静态内部类或弱引用
ThreadLocal未清理线程池复用导致数据残留使用后调用remove()
缓存无淘汰策略强引用堆积无用数据使用带过期策略的缓存库
finalize()方法滥用延迟GC回收避免使用finalize()

核心原则:及时释放无用对象的引用,避免长生命周期对象持有短生命周期对象的引用。

8. 静态方法与非静态方法的区别?

在 Java 中,静态方法(Static Method)非静态方法(Instance Method) 的核心区别在于 归属对象、内存分配、访问权限及使用场景。以下是详细对比:


1. 核心区别总结

维度静态方法非静态方法
归属属于类本身(类级别)属于类的实例(对象级别)
调用方式通过类名直接调用(无需实例化)必须通过对象实例调用
内存分配类加载时分配内存,生命周期与类相同实例化对象时分配内存,生命周期与对象相同
访问权限只能直接访问静态成员(静态变量/静态方法)可访问所有成员(静态 + 非静态)
this 关键字不可使用(无当前实例)可使用(指向当前对象实例)

2. 声明与调用方式

静态方法

  • 声明方式:使用 static 关键字修饰。
  • 调用方式:通过 类名.方法名() 直接调用。
  • 示例
    public class MathUtils {
        // 静态方法
        public static int add(int a, int b) {
            return a + b;
        }
    }
    
    // 直接通过类调用
    int sum = MathUtils.add(5, 3);
    

非静态方法

  • 声明方式:无 static 修饰。
  • 调用方式:必须通过 对象实例.方法名() 调用。
  • 示例
    public class User {
        private String name;
    
        // 非静态方法
        public void setName(String name) {
            this.name = name; // 使用 this 访问实例变量
        }
    }
    
    // 必须实例化后调用
    User user = new User();
    user.setName("Alice");
    

3. 访问权限对比

访问目标静态方法非静态方法
静态变量可直接访问可直接访问
静态方法可直接调用可直接调用
实例变量不可直接访问(需通过对象实例)可直接访问
实例方法不可直接调用(需通过对象实例)可直接调用

示例分析

public class Example {
    private static int staticVar = 10;
    private int instanceVar = 20;

    // 静态方法
    public static void staticMethod() {
        System.out.println(staticVar);  // 允许访问静态变量
        // System.out.println(instanceVar); // 编译报错!不能直接访问实例变量
        // instanceMethod();               // 编译报错!不能直接调用实例方法
    }

    // 非静态方法
    public void instanceMethod() {
        System.out.println(staticVar);     // 允许访问静态变量
        System.out.println(instanceVar);  // 允许访问实例变量
        staticMethod();                   // 允许调用静态方法
    }
}

4. 内存与生命周期

维度静态方法非静态方法
内存分配类加载时分配内存(方法区)对象实例化时分配内存(堆内存)
生命周期从类加载到程序结束从对象创建到被垃圾回收

5. 继承与多态

维度静态方法非静态方法
覆盖(Override)不支持(静态方法可被隐藏,非多态)支持(动态绑定,实现多态)
方法隐藏子类可定义同名静态方法(隐藏父类方法)子类定义同名实例方法会覆盖父类方法

示例:静态方法隐藏

class Parent {
    public static void print() {
        System.out.println("Parent static method");
    }
}

class Child extends Parent {
    public static void print() {
        System.out.println("Child static method"); // 隐藏父类静态方法
    }
}

public class Test {
    public static void main(String[] args) {
        Parent.print();   // 输出: Parent static method
        Child.print();    // 输出: Child static method

        Parent obj = new Child();
        obj.print();      // 输出: Parent static method(静态方法不依赖对象类型)
    }
}

6. 使用场景

场景静态方法非静态方法
典型用途工具类方法(如 Math.sqrt())、工厂方法操作对象状态(如 user.setName()
设计模式单例模式、工具类设计策略模式、模板方法模式

7. 线程安全性

  • 静态方法
    若操作静态变量(共享资源),需通过同步机制(如 synchronized)保证线程安全。
  • 非静态方法
    若操作实例变量(对象私有资源),默认线程安全(除非共享同一对象实例)。

8. 代码设计原则

  • 优先使用非静态方法
    符合面向对象设计(封装对象行为),更易扩展和维护。
  • 合理使用静态方法
    适用于无状态操作(如工具类)或全局唯一逻辑(如单例)。

总结

核心差异静态方法非静态方法
设计目标提供类级别功能,与对象无关封装对象行为,操作实例状态
适用性工具类、全局配置、单例模式对象状态管理、多态实现

理解两者的区别有助于合理设计代码结构,避免滥用静态方法导致代码耦合度高或内存泄漏问题。

9. ThreadPoolExecutor与Executor区别?

在Java并发编程中,ExecutorThreadPoolExecutor是线程池相关的两个核心概念,它们的区别主要体现在设计层次、功能范围及使用方式上。以下是两者的核心区别及关联:


1. 层次结构与定位

  • Executor
    是Java并发框架中的根接口,仅定义了一个方法execute(Runnable command),用于执行任务。其核心目的是解耦任务提交与执行,隐藏线程管理的细节。
    例如:

    Executor executor = Executors.newCachedThreadPool();
    executor.execute(() -> System.out.println("Task executed"));
    
  • ThreadPoolExecutor
    Executor接口的具体实现类,属于java.util.concurrent包中的类。它扩展了AbstractExecutorService(间接实现了ExecutorService接口),提供了完整的线程池管理能力,包括线程数控制、任务队列、拒绝策略等。


2. 功能差异

特性ExecutorThreadPoolExecutor
任务提交方法仅支持execute(Runnable)支持execute(Runnable)submit(Callable/Runnable)(返回Future对象)
生命周期管理无内置方法提供shutdown()shutdownNow()等终止方法
线程池配置无法直接配置参数支持核心线程数、最大线程数、存活时间、任务队列等参数
拒绝策略无默认策略支持AbortPolicyCallerRunsPolicy等策略
适用场景简单任务执行(不关注细节)复杂线程池管理(需精细控制资源)

3. 核心扩展功能(仅ThreadPoolExecutor具备)

  1. 线程池动态调整
    可通过setCorePoolSize()setMaximumPoolSize()等方法动态调整线程池参数。

  2. 任务队列与饱和处理
    支持多种阻塞队列(如ArrayBlockingQueueLinkedBlockingQueue),并通过RejectedExecutionHandler处理任务饱和时的行为。

  3. 线程工厂与监控
    允许自定义线程工厂(ThreadFactory),设置线程名称、优先级等,并提供getActiveCount()等方法监控线程池状态。


4. 实际应用中的选择

  • 使用Executor的场景
    当仅需简单执行异步任务且不关心线程池细节时,可通过Executors工具类快速创建线程池(如newCachedThreadPool()),但需注意其潜在风险(如无界队列导致OOM)。

  • 使用ThreadPoolExecutor的场景
    精细化控制线程池参数(如核心线程数、队列容量)或避免资源耗尽风险时,应直接通过其构造函数创建线程池。这也是《阿里巴巴Java开发手册》推荐的做法。


5. 示例对比

  • 通过Executor接口使用线程池

    Executor executor = Executors.newFixedThreadPool(4);
    executor.execute(() -> System.out.println("Task 1"));
    
  • 通过ThreadPoolExecutor直接配置

    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        4, 10, 60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(100),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );
    executor.submit(() -> "Result");
    executor.shutdown();
    

总结

  • Executor是接口,定义任务执行的抽象规范;ThreadPoolExecutor是具体实现,提供线程池的完整管理能力。
  • 选择依据:若需灵活配置或避免资源风险,优先使用ThreadPoolExecutor;若仅需简单异步执行,可使用Executor结合Executors工具类,但需注意潜在问题。

10. JSP四大作用域有哪些?

在 JSP 中,四大作用域定义了不同范围内数据的存储和共享方式,适用于不同业务场景。以下是详细分析:


1. 四大作用域对比

作用域对象类型生命周期共享范围典型应用场景
pagePageContext当前页面执行期间仅在当前 JSP 页面内有效页面内临时变量或逻辑处理
requestHttpServletRequest从请求开始到响应完成(含转发请求)同一请求链中的多个页面或 Servlet表单数据传递、请求转发共享数据
sessionHttpSession用户会话期间(默认 30 分钟或手动销毁)同一用户的所有请求用户登录状态、购物车信息
applicationServletContextWeb 应用启动到停止所有用户和请求全局共享全局配置、计数器、缓存数据

2. 作用域详解

(1) Page 作用域

  • 核心特点
    • 数据仅在同一 JSP 页面内有效,页面跳转后失效。
    • 通过 pageContext 对象操作(如 pageContext.setAttribute())。
  • 代码示例
    <%-- 设置 page 作用域属性 --%>
    <%
        pageContext.setAttribute("pageVar", "Page Scope Value");
    %>
    <%-- 读取属性 --%>
    <%= pageContext.getAttribute("pageVar") %>
    

(2) Request 作用域

  • 核心特点
    • 数据在一次 HTTP 请求中有效,可通过 forwardinclude 传递到其他页面。
    • 使用 request.setAttribute()request.getAttribute() 操作。
  • 代码示例
    // Servlet 中设置 request 属性
    request.setAttribute("requestVar", "Request Scope Value");
    RequestDispatcher dispatcher = request.getRequestDispatcher("target.jsp");
    dispatcher.forward(request, response);
    
    <%-- target.jsp 中获取属性 --%>
    <%= request.getAttribute("requestVar") %>
    

(3) Session 作用域

  • 核心特点
    • 数据在用户会话期间有效,跨多个请求(如用户登录后多次操作)。
    • 通过 session.setAttribute()session.getAttribute() 操作。
  • 代码示例
    // 用户登录后存储信息
    HttpSession session = request.getSession();
    session.setAttribute("user", userObject);
    
    <%-- 其他页面获取用户信息 --%>
    <%
        User user = (User) session.getAttribute("user");
        if (user != null) {
            out.print("Welcome, " + user.getName());
        }
    %>
    

(4) Application 作用域

  • 核心特点
    • 数据在整个 Web 应用生命周期内有效,所有用户共享。
    • 通过 application.setAttribute()application.getAttribute() 操作。
  • 代码示例
    // 初始化全局配置
    ServletContext application = getServletContext();
    application.setAttribute("appConfig", config);
    
    <%-- 任意页面读取全局配置 --%>
    <%
        Config config = (Config) application.getAttribute("appConfig");
    %>
    

3. 作用域选择原则

维度选择建议
数据独立性优先选择最小作用域(如 page > request > session > application)。
数据共享需求跨页面共享用 request,跨请求用 session,全局共享用 application
资源占用避免滥用 sessionapplication,防止内存泄漏或性能下降。

4. 注意事项

  • 线程安全
    application 作用域的数据需考虑并发访问(如使用 synchronized 或原子类)。
  • 内存管理
    及时清理不再使用的 session 数据(如调用 session.removeAttribute())。
  • 作用域优先级
    同名属性按 page > request > session > application 顺序覆盖。

总结

JSP 四大作用域为不同粒度的数据共享提供了灵活的支持:

  • page:临时数据,页面内自用。
  • request:请求链数据传递。
  • session:用户会话级状态管理。
  • application:全局资源共享。
    根据业务需求合理选择作用域,是优化性能和保证数据一致性的关键。

11. Spring动态代理与静态代理区别?

在Java中,代理模式是一种常见的设计模式,用于在不修改原始类代码的情况下增强其功能。Spring框架广泛使用动态代理来实现AOP(面向切面编程)。以下是静态代理动态代理的核心区别,以及Spring中动态代理的实现方式:


1. 定义与实现方式

特性静态代理动态代理
代理类生成时机编译时手动编写代理类(如UserServiceProxy)。运行时动态生成代理类(通过JDK动态代理或CGLIB)。
代码侵入性需为每个被代理类编写代理类,代码冗余。无需手动编写代理类,通过统一逻辑处理多个目标类。
灵活性仅能代理特定接口或类,扩展性差。可代理任意接口或类,通过反射或字节码增强实现通用逻辑。
维护成本接口变动时需修改所有相关代理类。接口变动时无需修改代理生成逻辑。

2. 实现机制对比

静态代理示例

  • 目标接口
    public interface UserService {
        void saveUser();
    }
    
  • 目标类
    public class UserServiceImpl implements UserService {
        @Override
        public void saveUser() {
            System.out.println("保存用户");
        }
    }
    
  • 静态代理类
    public class UserServiceProxy implements UserService {
        private UserService target;
    
        public UserServiceProxy(UserService target) {
            this.target = target;
        }
    
        @Override
        public void saveUser() {
            System.out.println("前置处理");
            target.saveUser();  // 调用目标方法
            System.out.println("后置处理");
        }
    }
    

动态代理示例(JDK动态代理)

  • InvocationHandler实现
    public class LogInvocationHandler implements InvocationHandler {
        private Object target;
    
        public LogInvocationHandler(Object target) {
            this.target = target;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("前置处理");
            Object result = method.invoke(target, args);  // 反射调用目标方法
            System.out.println("后置处理");
            return result;
        }
    }
    
  • 动态生成代理对象
    UserService target = new UserServiceImpl();
    UserService proxy = (UserService) Proxy.newProxyInstance(
        target.getClass().getClassLoader(),
        target.getClass().getInterfaces(),
        new LogInvocationHandler(target)
    );
    proxy.saveUser();  // 执行增强后的方法
    

3. Spring中的动态代理

Spring AOP默认根据目标类是否实现接口选择代理方式:

  • JDK动态代理:代理实现接口的类(基于java.lang.reflect.Proxy)。
  • CGLIB代理:代理未实现接口的类(通过生成目标类的子类)。

JDK动态代理 vs CGLIB

特性JDK动态代理CGLIB代理
依赖条件目标类必须实现至少一个接口。可代理无接口的类(通过继承)。
性能反射调用稍慢。字节码直接调用,通常更快。
限制无法代理未实现接口的类。无法代理final类或final方法。
生成方式运行时生成接口的代理类。运行时生成目标类的子类。

4. 核心区别总结

场景静态代理动态代理(Spring)
代理类生成手动编写,编译时确定。自动生成,运行时动态创建。
代码复用性每增加一个被代理类需新增代理类。一个InvocationHandler处理多个类。
适用场景代理类少且逻辑简单。代理大量类或需要通用增强逻辑(如AOP)。
维护难度高(需同步修改代理类)。低(逻辑集中,无需频繁修改)。

5. Spring AOP的代理选择

  • 强制使用CGLIB
    在Spring Boot中,可通过@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用CGLIB。
  • 性能权衡
    CGLIB在首次生成代理类时较慢,但执行效率高;JDK动态代理适合接口较多的场景。

实际应用建议

  • 优先使用动态代理:减少代码冗余,提升可维护性。
  • 明确代理需求
    • 若需代理接口且关注轻量级,选择JDK动态代理。
    • 若需代理类或无接口,选择CGLIB。

示例:Spring事务管理
Spring通过动态代理在方法调用前后添加事务的开启和提交逻辑,无需修改业务代码:

@Transactional
public void transferMoney() {
    // 业务代码
}

总结:Spring动态代理通过运行时动态生成代理对象,解决了静态代理的代码冗余问题,并提供了更高的灵活性,是AOP实现的核心技术。理解其原理有助于优化代码设计和性能调优。

12. Spring中IOC和AOP分别指的是什么?

在Spring框架中,IOC(控制反转)AOP(面向切面编程) 是两大核心设计思想,共同支撑了Spring的松耦合、高内聚特性。以下是两者的定义、作用及实际应用:


1. IOC(Inversion of Control,控制反转)

定义与核心思想

  • 传统对象管理:开发者直接通过new关键字创建对象并管理依赖(如手动设置属性)。
  • IOC思想:将对象的创建、依赖注入及生命周期管理交给容器(如Spring的ApplicationContext),开发者仅需定义依赖关系,由容器完成对象的组装。
  • 实现方式:通过依赖注入(DI,Dependency Injection) 实现,包括构造器注入、Setter注入和注解注入。

核心作用

  • 解耦:对象间的依赖由容器管理,降低代码耦合度。
  • 可维护性:修改依赖关系无需调整业务代码,仅需配置或注解。
  • 可测试性:便于通过Mock对象进行单元测试。

示例

// 传统方式:手动管理依赖
public class UserService {
    private UserRepository userRepo = new UserRepositoryImpl();
}

// IOC方式:依赖由容器注入
@Component
public class UserService {
    @Autowired  // 容器自动注入UserRepository实例
    private UserRepository userRepo;
}

关键组件

  • BeanFactory:基础容器,提供Bean的创建与访问。
  • ApplicationContext:扩展了BeanFactory,支持国际化、事件传播等。
  • Bean生命周期:通过@PostConstruct@PreDestroy等注解管理初始化和销毁逻辑。

2. AOP(Aspect-Oriented Programming,面向切面编程)

定义与核心思想

  • 横切关注点:多个模块共用的逻辑(如日志、事务、权限校验),传统OOP难以复用。
  • AOP思想:通过切面(Aspect) 将这些横切逻辑模块化,动态织入目标方法,实现与业务代码的分离。

核心概念

  • 切面(Aspect):封装横切逻辑的模块(如日志切面)。
  • 连接点(Join Point):程序执行中的可插入点(如方法调用)。
  • 通知(Advice):切面在连接点执行的动作,分@Before@After@Around等类型。
  • 切点(Pointcut):通过表达式定义哪些连接点需要被切入(如execution(* com.example.service.*.*(..)))。
  • 织入(Weaving):将切面应用到目标对象的过程(编译时、类加载时或运行时)。

核心作用

  • 代码复用:将通用逻辑(如事务管理)从业务代码中抽离。
  • 可维护性:修改横切逻辑时无需改动业务代码。
  • 非侵入性:通过动态代理实现,业务类无需感知AOP的存在。

示例

@Aspect
@Component
public class LogAspect {
    // 定义切点:拦截Service层的所有方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}

    // 前置通知:在目标方法执行前记录日志
    @Before("serviceLayer()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("方法调用: " + joinPoint.getSignature().getName());
    }
}

实现方式

  • JDK动态代理:基于接口,生成接口的代理类(要求目标类实现接口)。
  • CGLIB动态代理:基于继承,生成目标类的子类代理(可代理无接口的类)。

3. IOC与AOP的关系与协同

维度IOCAOP
核心目标管理对象及其依赖关系,实现解耦。模块化横切逻辑,增强代码复用性。
实现手段依赖注入(DI)与容器管理。动态代理与切面织入。
应用场景Bean的创建、装配、生命周期管理。日志、事务、安全等通用逻辑处理。
协作示例IOC容器管理AOP代理对象,确保切面逻辑正确注入。AOP依赖IOC管理的Bean作为切面目标。

实际应用场景

  1. IOC应用
    • 通过@Service@Repository注解声明Bean。
    • 使用@Autowired自动注入依赖,如Service层依赖DAO层。
  2. AOP应用
    • 事务管理:通过@Transactional注解自动管理数据库事务。
    • 性能监控:统计方法执行时间。
    • 权限校验:拦截请求并验证用户权限。

总结

  • IOC:通过容器管理对象生命周期和依赖注入,解决对象创建与耦合问题。
  • AOP:通过动态代理实现横切逻辑的模块化,解决代码重复和关注点分离问题。
  • 协同作用:IOC为AOP提供Bean管理基础,AOP通过动态代理增强Bean功能,两者共同支撑Spring的高效开发与灵活扩展。

13. 阐述SpringBoot run方法启动后执行流程。

在Spring Boot中,SpringApplication.run()方法的启动流程是一个多阶段的复杂过程,涉及环境准备、上下文初始化、自动配置、Bean加载及扩展点触发等核心步骤。以下是其详细执行流程:


1. SpringApplication初始化

在调用run()方法前,SpringApplication实例会完成初始化:

  • 推断应用类型:通过WebApplicationType.deduceFromClasspath()判断是Web应用(Servlet/Reactive)还是普通应用。
  • 加载ApplicationContextInitializer:从spring.factories读取并初始化上下文初始化器。
  • 加载ApplicationListener:从spring.factories读取并注册应用监听器(如日志监听器、配置监听器)。
  • 推断主配置类:通过堆栈分析找到包含main方法的类作为主配置类。

2. run()方法执行流程

阶段1:启动前准备

  1. 触发ApplicationStartingEvent:通知监听器应用启动开始。
  2. 启动时间统计:记录启动时间戳,用于后续性能分析。
  3. 准备环境(Environment)
    • 创建并配置ConfigurableEnvironment(根据应用类型选择StandardEnvironmentStandardServletEnvironment)。
    • 加载配置文件(application.properties/application.yml)和命令行参数。
    • 触发ApplicationEnvironmentPreparedEvent,通知监听器环境准备完成。

阶段2:创建应用上下文

  1. 创建ApplicationContext
    • 根据应用类型实例化对应的上下文:
      • AnnotationConfigServletWebServerApplicationContext(Servlet Web应用)
      • AnnotationConfigReactiveWebServerApplicationContext(Reactive Web应用)
      • AnnotationConfigApplicationContext(非Web应用)
    • 初始化BeanDefinitionLoader,准备加载Bean定义。

阶段3:上下文预处理

  1. 执行ApplicationContextInitializer
    • 调用所有注册的ApplicationContextInitializerinitialize()方法,定制上下文(如添加属性源)。
  2. 触发ApplicationContextInitializedEvent:通知监听器上下文初始化完成。

阶段4:刷新上下文(核心阶段)

  1. 调用refresh()方法(继承自AbstractApplicationContext):
    • 准备刷新:设置启动时间、活跃状态,初始化属性源。
    • 获取BeanFactoryobtainFreshBeanFactory()加载Bean定义(如@Component@Bean等)。
    • 预处理Bean工厂
      • 注册环境相关的Bean(environmentsystemProperties等)。
      • 添加BeanPostProcessor(如AutowiredAnnotationBeanPostProcessor)。
    • 执行BeanFactoryPostProcessor
      • 调用ConfigurationClassPostProcessor解析@Configuration类,处理@ComponentScan@Import等注解。
      • 处理自动配置:通过@EnableAutoConfiguration触发AutoConfigurationImportSelector,加载spring.factories中的自动配置类。
    • 注册BeanPostProcessor:将所有的后置处理器注册到Bean工厂。
    • 初始化消息源、事件广播器:用于国际化、事件发布。
    • 初始化单例Bean(非延迟加载)
      • 实例化所有非懒加载的单例Bean(如DataSourceServletWebServerFactory)。
      • 执行@PostConstruct方法和InitializingBeanafterPropertiesSet()
    • 触发ContextRefreshedEvent:通知监听器上下文刷新完成。

阶段5:Web服务器启动

  1. 启动嵌入式Web服务器(仅Web应用):
    • 通过ServletWebServerApplicationContext查找并创建ServletWebServerFactory(如Tomcat、Jetty)。
    • 创建DispatcherServlet并注册到Servlet容器。
    • 启动Web服务器,监听指定端口。

阶段6:扩展点执行

  1. 调用CommandLineRunnerApplicationRunner
    • 在所有Bean初始化完成后,按@Order顺序执行这些接口的实现类。
  2. 触发ApplicationStartedEventApplicationReadyEvent
    • 分别通知应用已启动和完全就绪。

3. 流程关键点总结

阶段核心操作
环境准备加载配置、触发环境事件
上下文创建根据应用类型实例化上下文
Bean加载与处理解析配置类、执行自动配置、注册Bean后置处理器
Web服务器启动创建并启动Tomcat/Jetty等服务器
扩展点触发执行CommandLineRunner、发布ApplicationReadyEvent

4. 核心机制解析

自动配置原理

  • 条件注解驱动:通过@ConditionalOnClass@ConditionalOnMissingBean等注解按需加载配置。
  • spring.factories:在META-INF/spring.factories中定义EnableAutoConfiguration的实现类列表。
  • 配置类优先级:用户自定义的@Bean优先于自动配置的Bean。

嵌入式服务器启动

  • ServletWebServerFactory:根据依赖(如spring-boot-starter-tomcat)动态选择服务器实现。
  • 端口绑定:通过server.port配置,支持随机端口(server.port=0)。

5. 示例:启动日志分析

@SpringBootApplication
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp.class, args);
    }
}

启动日志中可观察以下关键步骤:

  1. Starting MyApp on host with PID 12345
  2. No active profile set, falling back to default profiles: default
  3. Tomcat initialized with port(s): 8080 (http)
  4. Initializing Spring embedded WebApplicationContext
  5. Started MyApp in 2.5 seconds (JVM running for 3.0)

总结

Spring Boot的run()方法通过环境准备、上下文刷新、自动装配及扩展点触发等步骤,实现了从配置加载到应用就绪的全流程自动化。理解其启动流程有助于优化应用配置、排查启动问题及定制扩展逻辑(如自定义Starter)。

14. 拦截器和过滤器的区别?

在 Java Web 开发中,拦截器(Interceptor)过滤器(Filter) 都是用于对请求和响应进行预处理和后处理的组件,但它们在实现机制、作用范围、依赖框架等方面存在显著差异。以下是详细对比:


1. 核心区别总结

维度过滤器(Filter)拦截器(Interceptor)
规范/框架Servlet 规范(属于 Java EE 标准)Spring MVC 框架(属于 Spring 生态)
作用层级Servlet 容器层面(处理所有请求)Spring MVC 控制器层面(处理 Controller 请求)
依赖关系不依赖 Spring 框架依赖 Spring 容器
执行顺序先于拦截器执行(在请求到达 Servlet 前)在过滤器之后执行(在请求进入 Controller 前)
可访问对象ServletRequestServletResponseHttpServletRequestHttpServletResponseHandlerMethod

2. 功能与使用场景对比

维度过滤器拦截器
主要用途通用请求处理(编码转换、跨域、日志记录等)业务相关处理(权限校验、日志增强、参数预处理等)
典型场景- 全局字符编码设置
- 敏感词过滤
- 跨域处理
- 用户登录状态验证
- API 接口鉴权
- 接口耗时统计
能否中断请求是(通过 chain.doFilter() 决定是否放行)是(通过返回 false 终止执行)

3. 执行流程与生命周期

请求处理流程

  1. 过滤器
    客户端请求 → 过滤器1 → 过滤器2 → ... → Servlet
    (过滤器在请求到达 Servlet 前处理,响应时按反向顺序处理)

  2. 拦截器
    客户端请求 → 过滤器 → DispatcherServlet → 拦截器1 → 拦截器2 → ... → Controller
    (拦截器在 DispatcherServlet 处理请求后、进入 Controller 前执行)

生命周期

  • 过滤器
    由 Servlet 容器管理,随 Web 应用启动而初始化,应用关闭时销毁。
  • 拦截器
    由 Spring 容器管理,作为 Bean 存在,随 Spring 上下文初始化而加载。

4. 配置方式对比

过滤器配置

  • XML 配置(web.xml):
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>com.example.EncodingFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
  • 注解配置(Servlet 3.0+):
    @WebFilter(urlPatterns = "/*", filterName = "encodingFilter")
    public class EncodingFilter implements Filter { ... }
    

拦截器配置

  • Spring MVC 配置类
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new AuthInterceptor())
                    .addPathPatterns("/api/**")
                    .excludePathPatterns("/api/public/**");
        }
    }
    

5. 代码示例

过滤器示例(字符编码设置)

public class EncodingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        chain.doFilter(request, response); // 放行请求
    }
}

拦截器示例(权限校验)

public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("Authorization");
        if (!validateToken(token)) {
            response.setStatus(401);
            return false; // 中断请求
        }
        return true; // 继续执行
    }
}

6. 关键差异点

特性过滤器拦截器
框架耦合度低(Servlet 规范,通用性强)高(仅适用于 Spring MVC)
处理粒度粗粒度(所有请求)细粒度(可针对特定 Controller 或路径)
获取上下文信息无法直接获取 Spring Bean 或 Controller 信息可访问 Spring 上下文及 Handler 元数据

7. 如何选择?

  • 优先使用过滤器
    处理与业务无关的通用逻辑(如编码、跨域、日志记录)。
  • 优先使用拦截器
    处理与业务紧密相关的逻辑(如鉴权、参数校验、接口监控)。
  • 组合使用
    例如先用过滤器处理全局编码,再用拦截器实现接口鉴权。

总结

核心设计目标过滤器:关注请求/响应的底层处理拦截器:关注业务逻辑的切面控制
适用场景通用、与框架无关的预处理Spring 生态下的业务逻辑增强

理解两者的差异,能帮助开发者更合理地设计请求处理链,提升代码的可维护性和扩展性。

15. 阐述Spring中Bean的生命周期

在Spring框架中,Bean的生命周期是指从Bean的创建到销毁的整个过程,Spring容器通过一系列步骤管理Bean的生命周期,并提供了多个扩展点供开发者自定义Bean的行为。以下是Spring Bean生命周期的详细流程:


1. Bean生命周期核心步骤

1.1 实例化(Instantiation)

  • 描述:容器根据Bean的定义(如XML配置、@Component@Bean等)创建Bean的实例。
  • 方式
    • 通过构造器(默认无参构造器或有参构造器)。
    • 通过静态工厂方法(factory-method)。
    • 通过实例工厂方法(factory-bean + factory-method)。
  • 关键类BeanFactoryApplicationContext

1.2 属性赋值(Populate Properties)

  • 描述:为Bean的属性注入值或依赖的其他Bean。
  • 方式
    • 通过Setter方法(XML的<property>@Autowired)。
    • 通过字段直接注入(@Autowired@Resource)。
    • 通过构造器参数(@Autowired或XML的<constructor-arg>)。

1.3 Aware接口回调

  • 描述:若Bean实现了某些Aware接口,容器会调用其方法注入容器相关信息。
  • 常见Aware接口
    • BeanNameAware:设置Bean的名称。
    • BeanFactoryAware:注入BeanFactory实例。
    • ApplicationContextAware:注入ApplicationContext实例(需注意循环依赖问题)。
    • EnvironmentAware:注入环境变量。
  • 示例
    public class MyBean implements BeanNameAware {
        private String beanName;
        
        @Override
        public void setBeanName(String name) {
            this.beanName = name;
        }
    }
    

1.4 BeanPostProcessor前置处理

  • 描述BeanPostProcessorpostProcessBeforeInitialization()方法在Bean初始化前执行。
  • 用途:修改Bean属性、生成代理对象等。
  • 示例:Spring的AutowiredAnnotationBeanPostProcessor处理@Autowired注解。

1.5 初始化(Initialization)

  • 步骤
    1. @PostConstruct注解方法:JSR-250标准,在依赖注入完成后执行。
    2. InitializingBean接口的afterPropertiesSet():Spring提供的初始化方法。
    3. 自定义init-method:通过XML的init-method属性或@Bean(initMethod = "...")指定。
  • 执行顺序@PostConstructInitializingBeaninit-method
  • 示例
    @Component
    public class MyBean implements InitializingBean {
        @PostConstruct
        public void initAnnotation() {
            // 初始化逻辑
        }
    
        @Override
        public void afterPropertiesSet() {
            // 初始化逻辑
        }
    }
    

1.6 BeanPostProcessor后置处理

  • 描述BeanPostProcessorpostProcessAfterInitialization()方法在Bean初始化后执行。
  • 用途:生成代理对象(如AOP)、包装Bean等。
  • 示例:Spring AOP通过AnnotationAwareAspectJAutoProxyCreator生成代理对象。

1.7 Bean就绪(Ready)

  • 描述:Bean初始化完成,可被应用程序使用。

1.8 销毁(Destruction)

  • 步骤
    1. @PreDestroy注解方法:JSR-250标准,在Bean销毁前执行。
    2. DisposableBean接口的destroy():Spring提供的销毁方法。
    3. 自定义destroy-method:通过XML的destroy-method属性或@Bean(destroyMethod = "...")指定。
  • 执行顺序@PreDestroyDisposableBeandestroy-method
  • 触发条件:容器关闭时(如调用context.close()或注册JVM钩子)。
  • 示例
    @Component
    public class MyBean implements DisposableBean {
        @PreDestroy
        public void preDestroy() {
            // 销毁前逻辑
        }
    
        @Override
        public void destroy() {
            // 销毁逻辑
        }
    }
    

2. 完整流程图

sequenceDiagram
    participant Container as Spring容器
    participant Bean as Bean实例
    participant BPP as BeanPostProcessor

    Container->>Bean: 1. 实例化Bean
    Container->>Bean: 2. 属性赋值(依赖注入)
    Container->>Bean: 3. Aware接口回调(BeanNameAware等)
    Container->>BPP: 4. BeanPostProcessor.postProcessBeforeInitialization()
    Container->>Bean: 5. 初始化(@PostConstruct → InitializingBean → init-method)
    Container->>BPP: 6. BeanPostProcessor.postProcessAfterInitialization()
    Container->>Bean: 7. Bean就绪,可被使用
    Container->>Bean: 8. 销毁(@PreDestroy → DisposableBean → destroy-method)

3. 扩展点与注意事项

3.1 BeanPostProcessor

  • 作用范围:全局,影响容器中所有Bean。
  • 典型应用
    • AOP代理生成。
    • 属性验证(如@Valid)。
    • 自定义注解处理。

3.2 作用域(Scope)的影响

  • Singleton(默认):容器启动时初始化,容器关闭时销毁。
  • Prototype:每次获取时初始化,销毁由调用者管理(容器不跟踪其生命周期)。
  • Request/Session:Web作用域,生命周期与HTTP请求或会话绑定。

3.3 循环依赖问题

  • Setter注入:Spring通过三级缓存解决单例Bean的循环依赖(构造器注入无法解决)。
  • 原型Bean:无法解决循环依赖,会抛出BeanCurrentlyInCreationException

4. 示例:自定义BeanPostProcessor

public class CustomBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        System.out.println("Before初始化: " + beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("After初始化: " + beanName);
        return bean;
    }
}

总结

Spring Bean的生命周期通过容器严格管理,开发者可通过以下方式干预:

  1. Aware接口:获取容器信息。
  2. BeanPostProcessor:在初始化前后处理Bean。
  3. 初始化/销毁方法@PostConstructInitializingBeaninit-method等。
  4. 作用域控制:调整Bean的创建和销毁策略。

理解Bean的生命周期有助于优化Bean的初始化性能、解决依赖问题及实现高级定制逻辑(如动态代理)。

16. 手写代码:实现拷贝文件夹中所有文件以及子孙文件夹中所有文件

以下是一个实现拷贝文件夹及其所有子文件夹和文件的Java代码示例:

import java.io.IOException;
import java.nio.file.*;

public class FolderCopy {

    public static void main(String[] args) {
        String sourcePath = "/path/to/source";
        String targetPath = "/path/to/destination";
        
        try {
            copyFolder(sourcePath, targetPath);
            System.out.println("文件夹拷贝完成!");
        } catch (IOException e) {
            System.err.println("拷贝过程中发生错误: " + e.getMessage());
        }
    }

    /**
     * 递归拷贝文件夹及其所有内容
     */
    public static void copyFolder(String source, String target) throws IOException {
        Path src = Paths.get(source);
        Path dest = Paths.get(target);

        // 校验源路径合法性
        if (!Files.exists(src)) {
            throw new IOException("源路径不存在: " + src);
        }
        if (!Files.isDirectory(src)) {
            throw new IOException("源路径不是文件夹: " + src);
        }

        // 创建目标文件夹(包含所有父级目录)
        Files.createDirectories(dest);

        // 遍历文件夹内容
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(src)) {
            for (Path entry : stream) {
                Path targetPath = dest.resolve(entry.getFileName());
                
                if (Files.isDirectory(entry)) {
                    // 递归处理子文件夹
                    copyFolder(entry.toString(), targetPath.toString());
                } else {
                    // 拷贝文件(覆盖已存在的文件)
                    Files.copy(entry, targetPath, StandardCopyOption.REPLACE_EXISTING);
                }
            }
        }
    }
}

代码解析

  1. 路径验证

    • 检查源路径是否存在且为文件夹
    • 若路径非法则抛出 IOException
  2. 目标目录创建

    Files.createDirectories(dest);
    
    • 使用 createDirectories 创建完整目录结构,确保父目录存在
  3. 递归遍历

    try (DirectoryStream<Path> stream = Files.newDirectoryStream(src)) {
        // 遍历逻辑
    }
    
    • 使用 DirectoryStream 遍历文件夹内容
    • 自动资源管理(try-with-resources)确保流关闭
  4. 文件/目录处理

    • 文件:直接复制并覆盖已存在文件
      Files.copy(entry, targetPath, StandardCopyOption.REPLACE_EXISTING);
      
    • 子目录:递归调用 copyFolder 方法
  5. 异常处理

    • 主方法捕获并打印异常信息
    • 方法声明 throws IOException 传递检查型异常

关键特性

  1. 支持嵌套目录
    通过递归调用处理任意深度的文件夹结构

  2. 保留目录结构
    dest.resolve(entry.getFileName()) 保持源目录的相对路径

  3. 文件覆盖策略
    StandardCopyOption.REPLACE_EXISTING 覆盖已存在的目标文件

  4. 高效IO操作
    使用 NIO 的 Files.copy() 方法实现高效文件复制


优化方向

  1. 进度反馈
    添加计数器统计已拷贝文件数/总大小

  2. 异常恢复
    记录失败文件路径并继续执行

  3. 性能优化
    使用多线程并行处理独立子目录

  4. 属性保留
    保留文件属性(修改时间、权限等)

    Files.copy(entry, targetPath, 
              StandardCopyOption.COPY_ATTRIBUTES, 
              StandardCopyOption.REPLACE_EXISTING);
    

使用示例

假设目录结构如下:

src/
├── file1.txt
└── subdir/
    └── file2.txt

执行 copyFolder("src", "backup") 后生成:

backup/
├── file1.txt
└── subdir/
    └── file2.txt

该实现完整覆盖了文件夹拷贝的核心需求,通过NIO API保证了代码简洁性和执行效率。

17. 手写代码:实现列表数据转树结构

以下是一个将列表数据转换为树形结构的 Java 实现代码,包含详细注释和示例:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TreeBuilder {

    // 定义树节点结构
    public static class TreeNode {
        private Integer id;
        private Integer parentId;
        private String name;
        private List<TreeNode> children = new ArrayList<>();

        public TreeNode(Integer id, Integer parentId, String name) {
            this.id = id;
            this.parentId = parentId;
            this.name = name;
        }

        // Getters and Setters
        public Integer getId() { return id; }
        public Integer getParentId() { return parentId; }
        public List<TreeNode> getChildren() { return children; }
    }

    /**
     * 将扁平列表转换为树形结构
     * @param nodes 原始数据列表
     * @return 树形结构的根节点列表
     */
    public static List<TreeNode> buildTree(List<TreeNode> nodes) {
        Map<Integer, TreeNode> nodeMap = new HashMap<>();
        List<TreeNode> rootNodes = new ArrayList<>();

        // 1. 使用哈希表快速索引所有节点
        for (TreeNode node : nodes) {
            nodeMap.put(node.getId(), node);
        }

        // 2. 构建父子关系
        for (TreeNode node : nodes) {
            Integer parentId = node.getParentId();
            
            if (parentId == null || parentId == 0) { // 根节点条件
                rootNodes.add(node);
            } else {
                TreeNode parent = nodeMap.get(parentId);
                if (parent != null) {
                    parent.getChildren().add(node);
                }
                // 如果父节点不存在,该节点将被丢弃(可根据需求调整)
            }
        }

        return rootNodes;
    }

    // 测试示例
    public static void main(String[] args) {
        List<TreeNode> nodes = new ArrayList<>();
        nodes.add(new TreeNode(1, 0, "Root"));
        nodes.add(new TreeNode(2, 1, "Child 1"));
        nodes.add(new TreeNode(3, 1, "Child 2"));
        nodes.add(new TreeNode(4, 2, "Grandchild 1"));
        nodes.add(new TreeNode(5, 3, "Grandchild 2"));

        List<TreeNode> tree = buildTree(nodes);
        printTree(tree, 0);
    }

    // 辅助方法:打印树结构
    private static void printTree(List<TreeNode> nodes, int level) {
        for (TreeNode node : nodes) {
            System.out.println("  ".repeat(level) + node.name);
            if (!node.getChildren().isEmpty()) {
                printTree(node.getChildren(), level + 1);
            }
        }
    }
}

代码解析

1. 核心逻辑

  • 数据结构定义TreeNode 包含 idparentIdchildren 集合
  • 两次遍历策略
    • 第一次遍历:将节点存入 Map<Integer, TreeNode>,实现 O(1) 时间复杂度的节点查找
    • 第二次遍历:通过 parentId 建立父子关系,根节点直接加入结果集

2. 执行流程

graph TD
    A[原始列表数据] --> B{构建节点映射表}
    B --> C(Map存储所有节点)
    C --> D{遍历建立父子关系}
    D --> E[找到父节点]
    E --> F[将当前节点加入父节点的children]
    D --> G[未找到父节点则作为根节点]
    G --> H[加入根节点列表]

3. 时间复杂度

  • 时间复杂度:O(n),两次线性遍历
  • 空间复杂度:O(n),哈希表存储所有节点

关键特性

  1. 灵活根节点判断
    通过 parentId == null || parentId == 0 识别根节点,可根据业务需求调整

  2. 自动处理层级关系
    自动将子节点挂载到对应的父节点下,支持无限层级嵌套

  3. 容错处理
    父节点不存在时自动丢弃子节点(可根据需求改为抛出异常或创建虚拟父节点)


测试输出

运行示例代码将输出:

Root
  Child 1
    Grandchild 1
  Child 2
    Grandchild 2

扩展优化方向

  1. 循环引用检测
    增加逻辑防止 A→B→A 的循环依赖

    if (isCyclic(parent, node)) {
        throw new IllegalArgumentException("检测到循环引用");
    }
    
  2. 排序支持
    添加节点排序功能(如按ID/名称排序)

    parent.getChildren().sort(Comparator.comparing(TreeNode::getName));
    
  3. 缺失父节点处理
    定义策略处理孤儿节点(如自动升级为根节点)

    if (parent == null) {
        rootNodes.add(node); // 将孤儿节点作为根节点
    }
    
  4. 高性能优化
    改用并发容器处理海量数据(如 ConcurrentHashMap


该实现覆盖了树形结构转换的核心需求,通过清晰的逻辑分层和高效的数据结构选择,能够处理大多数业务场景的树结构转换需求。