2026小知识点-简(四)

11 阅读28分钟

1、Java中问题:==equals()的区别?String类的equals()为什么重写了Object的方法?

==是运算符:

• 对于基本数据类型(int、char、boolean等),比较的是“值是否相等”;

• 对于引用数据类型(对象、数组),比较的是“内存地址是否相同”(即是否指向同一个对象)。

• equals()是Object类的方法(所有类的父类),默认实现和==一致(比较地址);但String、Integer等类重写了equals(),改为比较“对象的逻辑内容是否相等”。

• String重写equals()的原因:String是“字符串内容”的载体,开发者更关注两个字符串的内容是否一样(比如"abc".equals("abc")应返回true),而非它们是否是同一个对象。重写后,equals()会比较字符序列的每个字符是否一致。

String s1 = new String("abc"); // 堆内存新建对象
String s2 = "abc";             // 常量池复用对象
System.out.println(s1 == s2);       // false(地址不同)
System.out.println(s1.equals(s2));  // true(内容相同)

2、Java中的final关键字有哪些用法?分别有什么注意点?

final可修饰类、方法、变量,核心作用是“限制修改”

修饰类:表示该类不能被继承(如java.lang.String就是final类,防止子类修改其核心逻辑)。

→ 注意:final类中的所有方法默认隐式为final(无需再写),但成员变量仍可修改。

修饰方法:表示该方法不能被子类重写(如Object的getClass()方法是final,确保类型判断的准确性)。

→ 注意:private方法默认是final的(因为子类无法访问,自然不能重写),但写了private final也不会报错。

修饰变量:表示该变量“只能赋值一次”,成为“常量”:

基本数据类型:值不可变(如final int a=10; a=20;编译报错);

引用数据类型:地址不可变(但对象内部属性可改,如final List list=new ArrayList(); list.add(1);合法,但list=new LinkedList<>();报错)。

→ 注意:final变量必须“显式初始化”(要么声明时赋值,要么在构造器中赋值,否则编译报错)。

3、String、StringBuilder、StringBuffer的区别?为什么String是不可变的?

区别

特性StringStringBuilderStringBuffer
可变性不可变(immutable)可变(mutable)可变(mutable)
线程安全安全(不可变天然线程安全)不安全(无同步锁)安全(有synchronized锁)
性能拼接时性能低(每次创建新对象)高(无锁,单线程首选)较低(锁开销)
  • 使用场景

    • String:适合存储“不经常修改的字符串”(如配置项、常量);
    • StringBuilder:单线程下频繁拼接字符串(如循环内拼接日志);
    • StringBuffer:多线程下频繁拼接字符串(如多线程写同一日志)。
  • String不可变的原因

    String类内部用final char[] value(JDK9后改为byte[])存储字符,且该数组被private final修饰,外部无法修改;同时String没有提供修改value的方法(如setCharAt),所有“修改”操作(如substringconcat)都会创建新的String对象。

    → 设计意义:① 安全性(如作为HashMap的key时,不会因内容修改导致哈希值变化);② 缓存哈希值(String的hashCode()会缓存结果,因为内容不变,哈希值也不变);③ 便于常量池复用(减少内存占用)。

4. 面向对象的三大特征(封装、继承、多态)分别是什么?

面向对象是“以对象为核心,将问题抽象为类和对象的交互”,三大特征是:

(1)封装(Encapsulation):隐藏内部实现,暴露必要接口

→ 定义:将类的属性私有化(private),通过公共方法(getter/setter)访问/修改,控制数据的访问权限,防止不合理修改。

public class Person {
    private String name; // 私有属性,外部无法直接访问
    private int age;

    // 公共getter:获取name(只读)
    public String getName() { return name; }
    // 公共setter:设置age(带校验,防止不合理值)
    public void setAge(int age) { 
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("年龄必须在0-150之间");
        }
        this.age = age; 
    }
}
// 外部使用:
Person p = new Person();
p.setName("张三"); // 只能通过getter/setter访问,不能直接p.name="李四"
p.setAge(-1);     // 会抛出异常,保证数据合法性
(2)继承(Inheritance):子类复用父类的属性和方法,实现代码复用

→ 定义:子类(extends父类)可以继承父类的非私有成员(属性、方法),并可扩展自己的特性或重写父类方法。

// 父类:动物
class Animal {
    protected String name; // protected:子类可访问
    public void eat() { System.out.println(name + "在吃饭"); }
}

// 子类:狗(继承Animal)
class Dog extends Animal {
    public Dog(String name) { this.name = name; }
    // 扩展子类特有方法
    public void bark() { System.out.println(name + "在汪汪叫"); }
    // 重写父类方法(多态的基础)
    @Override
    public void eat() { System.out.println(name + "在吃骨头"); }
}

// 使用:
Animal dog = new Dog("旺财");
dog.eat(); // 输出“旺财在吃骨头”(调用子类重写的方法)
dog.bark();// 错误!Animal类没有bark方法(需用Dog类型接收)
(3)多态(Polymorphism):同一行为,不同对象有不同的表现形式

→ 定义:父类引用指向子类对象,调用方法时会根据“实际对象类型”执行子类的实现(需满足:继承、重写、向上转型)。

→ 核心: “编译看左边(父类),运行看右边(子类)”

→ 示例(接上面的Animal/Dog):

// 多态体现:父类引用指向子类对象
Animal animal1 = new Dog("旺财"); // 向上转型(自动完成)
Animal animal2 = new Cat("咪咪"); // 假设Cat也继承Animal并重写eat()

animal1.eat(); // 运行时执行Dog的eat() → “旺财在吃骨头”
animal2.eat(); // 运行时执行Cat的eat() → “咪咪在吃鱼”

→ 意义:降低代码耦合度(比如写一个feed(Animal animal)方法,可喂养所有Animal子类,无需为每个子类写重载方法)。

5.重载(Overload)和重写(Override)的区别?

答案

对比维度重载(Overload)重写(Override)
发生范围同一个类中(或父子类,但少见)父子类之间(子类重写父类方法)
方法名必须相同必须相同
参数列表必须不同(个数/类型/顺序)必须相同(否则是重载)
返回值类型无要求(但不能仅靠返回值区分重载)子类返回值必须是父类返回值的子类(协变返回类型,JDK5+)
访问修饰符无要求子类修饰符不能比父类更严格(如父类是public,子类不能是private
异常抛出无要求子类抛出的异常不能比父类更宽泛(如父类抛IOException,子类不能抛Exception
核心目的方便调用(同一行为,不同参数)实现多态(子类修改父类方法的实现)

示例

// 重载:同一个类中的add方法,参数不同
class Calculator {
    public int add(int a, int b) { return a + b; }
    public double add(double a, double b) { return a + b; } // 参数类型不同
    public int add(int a, int b, int c) { return a + b + c; } // 参数个数不同
}

// 重写:子类重写父类的run方法
class Animal {
    public void run() { System.out.println("动物在跑"); }
}
class Horse extends Animal {
    @Override // 注解:强制检查是否符合重写规则(不加也能重写,但建议加)
    public void run() { System.out.println("马在奔跑"); }
}

6.抽象类(Abstract Class)和接口(Interface)的区别?什么时候用抽象类,什么时候用接口?

答案

先明确两者的核心定位:

  • 抽象类“is-a”关系(子类是父类的一种),是“模板类”,可包含抽象方法(无实现)和具体方法(有实现),用于抽取子类的共同属性和方法,强制子类实现抽象方法。
  • 接口“has-a”关系(类具备某种能力),是“契约/规范”,早期只能包含抽象方法和常量(JDK8前),JDK8后可包含默认方法(default)和静态方法(static),JDK9后可包含私有方法(private),用于定义类的行为标准,实现“多实现”(一个类可实现多个接口)。

具体区别

对比维度抽象类(Abstract Class)接口(Interface)
关键字abstract classinterface
构造方法有(子类实例化时会调用父类构造)无(接口不能实例化)
成员变量可任意(public/protected/private,静态/非静态)只能是public static final(常量,必须初始化)
方法类型抽象方法(abstract)、具体方法、静态方法JDK8前:抽象方法;JDK8+:默认方法、静态方法;JDK9+:私有方法
继承/实现子类extends一个抽象类(单继承)implements多个接口(多实现)
设计目的代码复用(共享具体方法)+ 强制约束定义行为规范(多实现灵活扩展)

使用场景

  • 抽象类:当多个子类有共同的属性/方法实现时(比如“动物”都有name属性和eat()的具体实现,只是run()方式不同),用抽象类抽取共性,减少重复代码。

  • 接口:当需要定义跨类别的行为规范时(比如“飞行”“游泳”是不同动物的能力,飞机也能飞行,此时用Flyable接口,让鸟类、飞机都实现它);或需要多实现时(如一个类既要“可序列化”又要“可比较”,可实现SerializableComparable接口)。

7.Java中的异常处理机制是什么?try-catch-finallyreturn的执行顺序是怎样的?

答案

  • 异常处理机制:Java通过“异常类”(Throwable的子类:Error和Exception)处理程序运行中的错误:

    • Error:严重错误(如OutOfMemoryError),程序无法处理,需修改代码;
    • Exception:可处理的异常(分为受检异常(如IOException,必须捕获或抛出)和非受检异常(如NullPointerException,RuntimeException子类,可不捕获))。
    • 处理方式:try-catch(捕获并处理)、throws(向上抛出)、finally(必执行的清理代码)。
  • try-catch-finally中return的顺序

    核心原则:finally块会在try/catch的return执行前执行,但如果finally也有return,会覆盖前面的return

    示例1(finally无return):

public static int test() {
    try {
        return 1; // 步骤1:先保存return的值(1)到临时变量
    } finally {
        System.out.println("finally执行"); // 步骤2:执行finally
    } // 步骤3:返回临时变量的值(1)
}
// 输出:finally执行 → 返回1

示例2(finally有return):

public static int test() {
    try {
        return 1;
    } finally {
        return 2; // 覆盖try的return
    }
}
// 返回2(finally的return直接生效)

→ 注意:不要在finally中写return,会导致逻辑混乱,难以调试。

8.什么是Java的反射机制?它有什么优缺点?

答案

  • 反射机制:Java在运行时动态获取类的信息(属性、方法、构造器),并能动态创建对象、调用方法、修改属性的能力(核心是java.lang.Class类)。

  • 实现步骤

    1. 获取Class对象:Class.forName("全类名")类名.class对象.getClass()
    2. 操作类:getDeclaredFields()(获取所有属性)、getDeclaredMethods()(获取所有方法)、newInstance()(创建对象,JDK9后推荐用getDeclaredConstructor().newInstance());
    3. 突破访问限制:setAccessible(true)(可访问私有属性/方法)。
Class<?> clazz = Class.forName("com.example.Person");
Object obj = clazz.newInstance(); // 创建Person对象
Method setNameMethod = clazz.getMethod("setName", String.class);
setNameMethod.invoke(obj, "张三"); // 调用setName("张三")
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true); // 允许访问私有属性
System.out.println(nameField.get(obj)); // 输出“张三”
  • 优点:灵活性高,可动态扩展功能(如Spring的IOC容器、MyBatis的ORM映射、JSON框架的序列化/反序列化);

  • 缺点:① 性能开销大(反射需动态解析,比直接调用慢);② 破坏封装性(可访问私有成员,增加安全风险);③ 代码可读性差(逻辑隐藏在反射中,难以维护)。


9.什么是线程安全?如何保证线程安全?

答案

  • 线程安全:多线程环境下,多个线程访问共享数据时,不会出现数据不一致或错误的情况(如计数器递增时,结果符合预期)。

  • 保证线程安全的方式

    同步代码块/方法:用synchronized关键字修饰代码块或方法,确保同一时间只有一个线程执行该区域(锁对象可以是this、类对象或自定义锁)。

// 同步方法:锁是当前对象(this)
public synchronized void increment() {
    count++;
}
// 同步代码块:锁是自定义对象(lock)
private final Object lock = new Object();
public void increment() {
    synchronized (lock) {
        count++;
    }
}

使用Lock接口ReentrantLock是常用的可重入锁,支持公平锁(按申请顺序获取锁)、超时获取锁(tryLock(long timeout, TimeUnit unit))等功能,比synchronized更灵活。

private final Lock lock = new ReentrantLock();
public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock(); // 必须在finally中释放锁,避免死锁
    }
}

③ 使用线程安全的数据结构:ConcurrentHashMap(并发哈希表,分段锁实现)、AtomicInteger(原子整数,CAS操作)、CopyOnWriteArrayList(写时复制列表,适合读多写少场景)等,这些类内部已实现同步,无需手动加锁。

// AtomicInteger:原子递增,无需同步
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
    count.incrementAndGet(); // 原子操作,线程安全
}

10.线程池的核心参数有哪些?如何选择合适的线程池?

答案

  • 线程池的核心参数ThreadPoolExecutor的构造函数):

    corePoolSize:核心线程数(线程池保持的最小线程数,即使空闲也不销毁);

    maximumPoolSize:最大线程数(线程池允许创建的最大线程数);

    keepAliveTime:非核心线程的空闲存活时间(超过该时间,非核心线程会被销毁);

    unitkeepAliveTime的时间单位(如TimeUnit.SECONDS);

    workQueue:任务队列(用于存放等待执行的任务,常见实现有LinkedBlockingQueue(无界队列)、ArrayBlockingQueue(有界队列)、SynchronousQueue(直接提交队列));

    threadFactory:线程工厂(用于创建线程,可自定义线程名称、优先级等);

    handler:拒绝策略(当任务队列满且线程数达到最大值时,如何处理新任务,常见策略有AbortPolicy(抛出异常)、CallerRunsPolicy(由调用者线程执行)、DiscardOldestPolicy(丢弃队列中最旧的任务)、DiscardPolicy(丢弃新任务))。

 // 核心参数配置
        int corePoolSize = 2;           // 核心线程数
        int maxPoolSize = 4;            // 最大线程数
        long keepAliveTime = 10;        // 空闲线程存活时间
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2); // 任务队列
        ThreadFactory threadFactory = new NamedThreadFactory("MyPool"); // 自定义线程工厂
        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 拒绝策略

        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );

常见线程池及适用场景

FixedThreadPool(固定大小线程池):newFixedThreadPool(n),核心线程数=最大线程数=n,使用LinkedBlockingQueue。适用于负载较重的服务器(如Web服务器),控制线程数量,避免资源耗尽。

CachedThreadPool(缓存线程池):newCachedThreadPool(),核心线程数=0,最大线程数=Integer.MAX_VALUE,使用SynchronousQueue。适用于短期异步任务(如临时数据处理),线程空闲60秒后销毁,重用空闲线程。

SingleThreadExecutor(单线程线程池):newSingleThreadExecutor(),核心线程数=最大线程数=1,使用LinkedBlockingQueue。适用于需要顺序执行任务的场景(如日志写入),保证任务按提交顺序执行。

ScheduledThreadPool(定时任务线程池):newScheduledThreadPool(n),支持定时或周期性执行任务(如schedule(Runnable command, long delay, TimeUnit unit)scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit))。适用于定时任务(如心跳检测、数据备份)。

11.volatile关键字的作用?与synchronized的区别?

答案

  • volatile的作用

    可见性:当一个线程修改了volatile变量的值,其他线程能立即看到最新值(强制线程从主内存读取变量,而非工作内存);

    禁止指令重排序volatile变量的读写操作前后会插入内存屏障(Memory Barrier),防止编译器或CPU对指令进行重排序(如双重检查锁定中的volatile修饰)。

    注意volatile不保证原子性(如volatile int count = 0; count++不是原子操作,仍需同步)。

    synchronized的区别

特性volatilesynchronized
原子性不保证保证(互斥执行)
可见性保证保证(释放锁时刷新主内存)
有序性禁止重排序保证(临界区内的指令顺序)
适用场景单一变量的读写(如标志位)复合操作(如计数器递增)
性能轻量级(无锁)重量级(锁竞争)

12.字节流与字符流的区别?如何选择?

核心区别

维度字节流(InputStream/OutputStream)字符流(Reader/Writer)
处理单位字节(8位)字符(16位,Unicode)
适用场景所有文件(二进制/文本)文本文件(如.txt、.java)
编码处理不涉及(直接读写字节)涉及(需指定字符编码,如UTF-8)
缓冲机制需要手动包装(如BufferedInputStream)自带缓冲(如BufferedReader)

选择原则

  • 处理二进制文件(如.jpg、.zip、.class):必须用字节流(如FileInputStreamFileOutputStream);
  • 处理文本文件(如配置文件、日志):优先用字符流(如BufferedReaderBufferedWriter),因为字符流会自动处理字符编码(如将字节转为UTF-8字符),避免乱码。

13.NIO中的ChannelBuffer是什么?与传统IO的区别?

Java NIO(New I/O 或 Non-blocking I/O)是 Java 1.4 引入的新的 I/O API,用于补充和替代传统的 Java I/O(java.io 包)。它提供了非阻塞式、事件驱动的 I/O 操作能力。

NIO核心组件

Channel(通道):类似于传统IO的“流”,但双向(可读可写),支持异步IO(如FileChannelSocketChannelServerSocketChannel);

Buffer(缓冲区):用于存储数据的容器(如ByteBufferCharBuffer),所有IO操作都通过Buffer进行(读:从Channel读到Buffer;写:从Buffer写到Channel);

Selector(选择器):用于监听多个Channel的事件(如连接、读、写),实现单线程处理多个Channel(多路复用),适合高并发场景(如网络服务器)。

与传统IO的区别

维度传统IO(流)NIO(Channel+Buffer+Selector)
方向单向(输入/输出)双向(可读可写)
阻塞方式阻塞(如InputStream.read()会阻塞直到有数据)非阻塞(可选择阻塞或非阻塞模式)
多路复用不支持(每个流需要一个线程)支持(Selector监听多个Channel)
适用场景低并发、小数据量高并发、大数据量(如网络编程)

NIO 的核心价值:

  1. 高并发支持:单线程处理数千连接
  2. 零拷贝优化transferTo()和内存映射文件
  3. 非阻塞操作:避免线程因 I/O 而阻塞
  4. 事件驱动:基于事件的通知机制

14.如何处理大文件(如10GB)的读取?

答案

  • 核心思路分块读取(避免将整个文件加载到内存),使用缓冲流(减少IO次数)。

  • 实现步骤

    ① 使用FileInputStream(字节流)或BufferedReader(字符流)包装文件;

    ② 定义一个固定大小的缓冲区(如byte[] buffer = new byte[1024 * 1024](1MB));

    ③ 循环读取文件内容到缓冲区,处理缓冲区中的数据(如解析、存储),直到文件读完。

代码示例(读取大文本文件)

try (BufferedReader br = new BufferedReader(new FileReader("large_file.txt"), 1024 * 1024)) { // 1MB缓冲
    char[] buffer = new char[1024 * 1024]; // 1MB缓冲区
    int len;
    while ((len = br.read(buffer)) != -1) { // 读取到缓冲区
        // 处理缓冲区中的数据(如解析每一行)
        String content = new String(buffer, 0, len);
        process(content); // 自定义处理方法
    }
} catch (IOException e) {
    e.printStackTrace();
}

注意事项

  • 使用try-with-resources(自动关闭流,避免资源泄漏);
  • 缓冲区大小适中(如1MB-8MB,太大浪费内存,太小增加IO次数);
  • 对于二进制大文件(如视频),使用FileInputStream+byte[]分块读取;对于文本大文件,使用BufferedReader+char[]分块读取。

15.Java中解析JSON的常用库有哪些?如何选择?

答案

  • 常用JSON库

    Jackson(推荐):功能强大,支持流式解析(Streaming)、树模型(Tree Model)、数据绑定(Data Binding),性能优秀(比Gson快),是Spring Boot默认的JSON库;

    Gson(Google):简单易用,支持对象与JSON的相互转换(toJson()fromJson()),适合快速开发;

    Fastjson(阿里巴巴):性能极高(号称“最快的JSON库”),但安全性较差(曾爆出多个远程代码执行漏洞),不推荐生产环境使用。

  • 选择原则

    • 如果需要高性能和灵活性(如处理复杂JSON结构):选Jackson;
    • 如果需要简单易用(如快速实现对象与JSON的转换):选Gson;
    • 避免使用Fastjson(安全问题)。

16.如何用Jackson解析JSON数组?

针对“熟悉Java多线程、文件IO操作、数据解析等”这一技能点,面试官会从核心概念理解代码实践能力问题解决思路三个维度提问,重点考察你是否“真掌握”而非“背概念”。以下是高频问题及针对性答案(附代码示例与底层逻辑):

17. 问题:什么是线程安全?如何保证线程安全?**

答案

  • 线程安全:多线程环境下,多个线程访问共享数据时,不会出现数据不一致或错误的情况(如计数器递增时,结果符合预期)。

  • 保证线程安全的方式

    同步代码块/方法:用synchronized关键字修饰代码块或方法,确保同一时间只有一个线程执行该区域(锁对象可以是this、类对象或自定义锁)。

    java
    java
    下载
    复制
    // 同步方法:锁是当前对象(this)
    public synchronized void increment() {
        count++;
    }
    // 同步代码块:锁是自定义对象(lock)
    private final Object lock = new Object();
    public void increment() {
        synchronized (lock) {
            count++;
        }
    }
    

    使用Lock接口ReentrantLock是常用的可重入锁,支持公平锁(按申请顺序获取锁)、超时获取锁(tryLock(long timeout, TimeUnit unit))等功能,比synchronized更灵活。

    java
    java
    下载
    复制
    private final Lock lock = new ReentrantLock();
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock(); // 必须在finally中释放锁,避免死锁
        }
    }
    

    使用线程安全的数据结构ConcurrentHashMap(并发哈希表,分段锁实现)、AtomicInteger(原子整数,CAS操作)、CopyOnWriteArrayList(写时复制列表,适合读多写少场景)等,这些类内部已实现同步,无需手动加锁。

    java
    java
    下载
    复制
    // AtomicInteger:原子递增,无需同步
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count.incrementAndGet(); // 原子操作,线程安全
    }
    

18. 问题:线程池的核心参数有哪些?如何选择合适的线程池?**

答案

  • 线程池的核心参数ThreadPoolExecutor的构造函数):

    corePoolSize:核心线程数(线程池保持的最小线程数,即使空闲也不销毁);

    maximumPoolSize:最大线程数(线程池允许创建的最大线程数);

    keepAliveTime:非核心线程的空闲存活时间(超过该时间,非核心线程会被销毁);

    unitkeepAliveTime的时间单位(如TimeUnit.SECONDS);

    workQueue:任务队列(用于存放等待执行的任务,常见实现有LinkedBlockingQueue(无界队列)、ArrayBlockingQueue(有界队列)、SynchronousQueue(直接提交队列));

    threadFactory:线程工厂(用于创建线程,可自定义线程名称、优先级等);

    handler:拒绝策略(当任务队列满且线程数达到最大值时,如何处理新任务,常见策略有AbortPolicy(抛出异常)、CallerRunsPolicy(由调用者线程执行)、DiscardOldestPolicy(丢弃队列中最旧的任务)、DiscardPolicy(丢弃新任务))。

  • 常见线程池及适用场景

    FixedThreadPool(固定大小线程池):newFixedThreadPool(n),核心线程数=最大线程数=n,使用LinkedBlockingQueue。适用于负载较重的服务器(如Web服务器),控制线程数量,避免资源耗尽。

    CachedThreadPool(缓存线程池):newCachedThreadPool(),核心线程数=0,最大线程数=Integer.MAX_VALUE,使用SynchronousQueue。适用于短期异步任务(如临时数据处理),线程空闲60秒后销毁,重用空闲线程。

    SingleThreadExecutor(单线程线程池):newSingleThreadExecutor(),核心线程数=最大线程数=1,使用LinkedBlockingQueue。适用于需要顺序执行任务的场景(如日志写入),保证任务按提交顺序执行。

    ScheduledThreadPool(定时任务线程池):newScheduledThreadPool(n),支持定时或周期性执行任务(如schedule(Runnable command, long delay, TimeUnit unit)scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit))。适用于定时任务(如心跳检测、数据备份)。

19. 问题:volatile关键字的作用?与synchronized的区别?**

考察点:对volatile的理解,是否能区分volatilesynchronized的适用场景。

答案

  • volatile的作用

    可见性:当一个线程修改了volatile变量的值,其他线程能立即看到最新值(强制线程从主内存读取变量,而非工作内存);

    禁止指令重排序volatile变量的读写操作前后会插入内存屏障(Memory Barrier),防止编译器或CPU对指令进行重排序(如双重检查锁定中的volatile修饰)。

    注意volatile不保证原子性(如volatile int count = 0; count++不是原子操作,仍需同步)。

  • synchronized的区别

    特性volatilesynchronized
    原子性不保证保证(互斥执行)
    可见性保证保证(释放锁时刷新主内存)
    有序性禁止重排序保证(临界区内的指令顺序)
    适用场景单一变量的读写(如标志位)复合操作(如计数器递增)
    性能轻量级(无锁)重量级(锁竞争)

20. 文件IO操作相关问题**

文件IO考察流的分类NIO特性大文件处理等知识点。

21. 问题:字节流与字符流的区别?如何选择?**

考察点:对IO流的本质理解,是否能根据场景选择合适的流。

答案

  • 核心区别

    维度字节流(InputStream/OutputStream)字符流(Reader/Writer)
    处理单位字节(8位)字符(16位,Unicode)
    适用场景所有文件(二进制/文本)文本文件(如.txt、.java)
    编码处理不涉及(直接读写字节)涉及(需指定字符编码,如UTF-8)
    缓冲机制需要手动包装(如BufferedInputStream)自带缓冲(如BufferedReader)
  • 选择原则

    • 处理二进制文件(如.jpg、.zip、.class):必须用字节流(如FileInputStreamFileOutputStream);
    • 处理文本文件(如配置文件、日志):优先用字符流(如BufferedReaderBufferedWriter),因为字符流会自动处理字符编码(如将字节转为UTF-8字符),避免乱码。

22. 问题:NIO中的ChannelBuffer是什么?与传统IO的区别?**

考察点:对NIO的理解,是否能区分传统IO与NIO的差异。

答案

  • NIO核心组件

    Channel(通道):类似于传统IO的“流”,但双向(可读可写),支持异步IO(如FileChannelSocketChannelServerSocketChannel);

    Buffer(缓冲区):用于存储数据的容器(如ByteBufferCharBuffer),所有IO操作都通过Buffer进行(读:从Channel读到Buffer;写:从Buffer写到Channel);

    Selector(选择器):用于监听多个Channel的事件(如连接、读、写),实现单线程处理多个Channel(多路复用),适合高并发场景(如网络服务器)。

  • 与传统IO的区别

    维度传统IO(流)NIO(Channel+Buffer+Selector)
    方向单向(输入/输出)双向(可读可写)
    阻塞方式阻塞(如InputStream.read()会阻塞直到有数据)非阻塞(可选择阻塞或非阻塞模式)
    多路复用不支持(每个流需要一个线程)支持(Selector监听多个Channel)
    适用场景低并发、小数据量高并发、大数据量(如网络编程)

23. 问题:如何处理大文件(如10GB)的读取?**

考察点:对大文件处理的实践经验,是否能避免内存溢出。

答案

  • 核心思路分块读取(避免将整个文件加载到内存),使用缓冲流(减少IO次数)。

  • 实现步骤

    ① 使用FileInputStream(字节流)或BufferedReader(字符流)包装文件;

    ② 定义一个固定大小的缓冲区(如byte[] buffer = new byte[1024 * 1024](1MB));

    ③ 循环读取文件内容到缓冲区,处理缓冲区中的数据(如解析、存储),直到文件读完。

  • 代码示例(读取大文本文件)

    java
    java
    下载
    复制
    try (BufferedReader br = new BufferedReader(new FileReader("large_file.txt"), 1024 * 1024)) { // 1MB缓冲
        char[] buffer = new char[1024 * 1024]; // 1MB缓冲区
        int len;
        while ((len = br.read(buffer)) != -1) { // 读取到缓冲区
            // 处理缓冲区中的数据(如解析每一行)
            String content = new String(buffer, 0, len);
            process(content); // 自定义处理方法
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    
  • 注意事项

    • 使用try-with-resources(自动关闭流,避免资源泄漏);
    • 缓冲区大小适中(如1MB-8MB,太大浪费内存,太小增加IO次数);
    • 对于二进制大文件(如视频),使用FileInputStream+byte[]分块读取;对于文本大文件,使用BufferedReader+char[]分块读取。

24. 数据解析相关问题**

数据解析考察JSON/XML解析正则表达式数据清洗等知识点。

25. 问题:Java中解析JSON的常用库有哪些?如何选择?**

考察点:对JSON解析库的了解,是否能根据场景选择合适的库。

答案

  • 常用JSON库

    Jackson(推荐):功能强大,支持流式解析(Streaming)、树模型(Tree Model)、数据绑定(Data Binding),性能优秀(比Gson快),是Spring Boot默认的JSON库;

    Gson(Google):简单易用,支持对象与JSON的相互转换(toJson()fromJson()),适合快速开发;

    Fastjson(阿里巴巴):性能极高(号称“最快的JSON库”),但安全性较差(曾爆出多个远程代码执行漏洞),不推荐生产环境使用。

  • 选择原则

    • 如果需要高性能和灵活性(如处理复杂JSON结构):选Jackson;
    • 如果需要简单易用(如快速实现对象与JSON的转换):选Gson;
    • 避免使用Fastjson(安全问题)。

26. 问题:如何用Jackson解析JSON数组?**

考察点:对Jackson的实际应用能力,是否能处理复杂JSON结构。

答案

  • 步骤

    ① 定义Java类(与JSON结构匹配);

    ② 使用ObjectMapper(Jackson的核心类)的readValue()方法,将JSON字符串解析为Java对象(数组或集合)。

  • 代码示例

    假设JSON数组为:[{"name":"Alice","age":30},{"name":"Bob","age":25}]

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;

// 定义Java类(与JSON对象匹配)
static class User {
    private String name;
    private int age;
    // 必须有默认构造函数(Jackson需要)
    public User() {}
    // getter和setter(可选,Jackson会通过反射访问字段)
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

public static void main(String[] args) throws Exception {
    String json = "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]";
    ObjectMapper mapper = new ObjectMapper();
    // 解析JSON数组为List<User>
    List<User> users = mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, User.class));
    // 遍历结果
    for (User user : users) {
        System.out.println("Name: " + user.getName() + ", Age: " + user.getAge());
    }
}

27. 问题:如何解析XML文件?DOM与SAX的区别?**

考察点:对XML解析的理解,是否能区分DOM与SAX的适用场景。

答案

  • XML解析方式

    DOM(文档对象模型) :将整个XML文件加载到内存,构建一棵文档树(如Document对象),支持随机访问(如获取任意节点的内容);

    SAX(简单API for XML) :基于事件驱动的解析方式(如遇到开始标签、结束标签、文本节点时触发事件),不加载整个文件到内存,适合处理大XML文件;

    JAXB(Java Architecture for XML Binding) :将XML与Java对象绑定(如@XmlRootElement@XmlElement注解),实现XML与Java对象的相互转换(类似于JSON的Jackson)。 DOM与SAX的区别

维度DOMSAX
内存占用高(加载整个文件)低(事件驱动,不存储整个文件)
访问方式随机访问(可修改文档树)顺序访问(只能读取,不能修改)
适用场景小XML文件(如配置文件)大XML文件(如数据交换)
性能慢(加载整个文件)快(事件驱动)

28. 如何设计一个“多线程文件下载器”?

考察点:对多线程、文件IO的综合应用能力,是否能解决实际问题。

答案

  • 核心思路

    分块下载:将文件分成多个块(如每个块1MB),每个线程负责下载一个块;

    线程池管理:使用线程池(如FixedThreadPool)管理下载线程,控制线程数量;

    文件合并:下载完成后,将各个块合并为一个完整文件;

    断点续传:记录每个块的下载进度(如保存到数据库或文件),下次下载时从断点开始。

  • 实现步骤

    ① 获取文件大小(通过HTTP的Content-Length头);

    ② 计算每个块的起始位置和结束位置(如块大小为1MB,第i块的起始位置为i * 1MB,结束位置为(i+1) * 1MB - 1);

    ③ 创建线程池,提交下载任务(每个任务下载一个块,使用HttpURLConnectionOkHttp发送带Range头的请求,如Range: bytes=100-200);

    ④ 每个线程将下载的块保存到临时文件(如file.part1file.part2);

    ⑤ 所有线程完成后,合并临时文件为完整文件(如按顺序读取临时文件,写入目标文件);

    ⑥ 删除临时文件。

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MultiThreadDownloader {
    private static final int THREAD_COUNT = 4; // 线程数
    private static final int BLOCK_SIZE = 1024 * 1024; // 块大小(1MB)
    private final URL url;
    private final File outputFile;

    public MultiThreadDownloader(URL url, File outputFile) {
        this.url = url;
        this.outputFile = outputFile;
    }

    public void download() throws Exception {
        // 1. 获取文件大小
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("HEAD");
        int fileSize = conn.getContentLength();
        conn.disconnect();

        // 2. 计算每个块的起始和结束位置
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
        for (int i = 0; i < THREAD_COUNT; i++) {
            long start = i * BLOCK_SIZE;
            long end = Math.min(start + BLOCK_SIZE - 1, fileSize - 1);
            executor.submit(new DownloadTask(url, outputFile, start, end, i));
        }
        executor.shutdown();
        while (!executor.isTerminated()) {
            Thread.sleep(100);
        }

        // 3. 合并临时文件
        mergeFiles();
    }

    private void mergeFiles() throws IOException {
        try (FileOutputStream fos = new FileOutputStream(outputFile)) {
            for (int i = 0; i < THREAD_COUNT; i++) {
                File partFile = new File(outputFile.getParent(), outputFile.getName() + ".part" + i);
                try (FileInputStream fis = new FileInputStream(partFile)) {
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = fis.read(buffer)) != -1) {
                        fos.write(buffer, 0, len);
                    }
                }
                partFile.delete(); // 删除临时文件
            }
        }
    }

    // 下载任务(内部类)
    private static class DownloadTask implements Runnable {
        private final URL url;
        private final File outputFile;
        private final long start;
        private final long end;
        private final int partNum;

        public DownloadTask(URL url, File outputFile, long start, long end, int partNum) {
            this.url = url;
            this.outputFile = outputFile;
            this.start = start;
            this.end = end;
            this.partNum = partNum;
        }

        @Override
        public void run() {
            try {
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
                try (InputStream is = conn.getInputStream();
                     FileOutputStream fos = new FileOutputStream(new File(outputFile.getParent(), outputFile.getName() + ".part" + partNum))) {
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = is.read(buffer)) != -1) {
                        fos.write(buffer, 0, len);
                    }
                }
                conn.disconnect();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        URL url = new URL("https://example.com/large_file.zip");
        File outputFile = new File("large_file.zip");
        new MultiThreadDownloader(url, outputFile).download();
    }
}