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是不可变的?
区别:
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变(immutable) | 可变(mutable) | 可变(mutable) |
| 线程安全 | 安全(不可变天然线程安全) | 不安全(无同步锁) | 安全(有synchronized锁) |
| 性能 | 拼接时性能低(每次创建新对象) | 高(无锁,单线程首选) | 较低(锁开销) |
-
使用场景:
- String:适合存储“不经常修改的字符串”(如配置项、常量);
- StringBuilder:单线程下频繁拼接字符串(如循环内拼接日志);
- StringBuffer:多线程下频繁拼接字符串(如多线程写同一日志)。
-
String不可变的原因:
String类内部用
final char[] value(JDK9后改为byte[])存储字符,且该数组被private final修饰,外部无法修改;同时String没有提供修改value的方法(如setCharAt),所有“修改”操作(如substring、concat)都会创建新的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 class | interface |
| 构造方法 | 有(子类实例化时会调用父类构造) | 无(接口不能实例化) |
| 成员变量 | 可任意(public/protected/private,静态/非静态) | 只能是public static final(常量,必须初始化) |
| 方法类型 | 抽象方法(abstract)、具体方法、静态方法 | JDK8前:抽象方法;JDK8+:默认方法、静态方法;JDK9+:私有方法 |
| 继承/实现 | 子类extends一个抽象类(单继承) | 类implements多个接口(多实现) |
| 设计目的 | 代码复用(共享具体方法)+ 强制约束 | 定义行为规范(多实现灵活扩展) |
使用场景:
-
用抽象类:当多个子类有共同的属性/方法实现时(比如“动物”都有
name属性和eat()的具体实现,只是run()方式不同),用抽象类抽取共性,减少重复代码。 -
用接口:当需要定义跨类别的行为规范时(比如“飞行”“游泳”是不同动物的能力,飞机也能飞行,此时用
Flyable接口,让鸟类、飞机都实现它);或需要多实现时(如一个类既要“可序列化”又要“可比较”,可实现Serializable和Comparable接口)。
7.Java中的异常处理机制是什么?try-catch-finally中return的执行顺序是怎样的?
答案:
-
异常处理机制: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类)。 -
实现步骤:
- 获取Class对象:
Class.forName("全类名")、类名.class、对象.getClass(); - 操作类:
getDeclaredFields()(获取所有属性)、getDeclaredMethods()(获取所有方法)、newInstance()(创建对象,JDK9后推荐用getDeclaredConstructor().newInstance()); - 突破访问限制:
setAccessible(true)(可访问私有属性/方法)。
- 获取Class对象:
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:非核心线程的空闲存活时间(超过该时间,非核心线程会被销毁);④
unit:keepAliveTime的时间单位(如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的区别:
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证(互斥执行) |
| 可见性 | 保证 | 保证(释放锁时刷新主内存) |
| 有序性 | 禁止重排序 | 保证(临界区内的指令顺序) |
| 适用场景 | 单一变量的读写(如标志位) | 复合操作(如计数器递增) |
| 性能 | 轻量级(无锁) | 重量级(锁竞争) |
12.字节流与字符流的区别?如何选择?
核心区别:
| 维度 | 字节流(InputStream/OutputStream) | 字符流(Reader/Writer) |
|---|---|---|
| 处理单位 | 字节(8位) | 字符(16位,Unicode) |
| 适用场景 | 所有文件(二进制/文本) | 文本文件(如.txt、.java) |
| 编码处理 | 不涉及(直接读写字节) | 涉及(需指定字符编码,如UTF-8) |
| 缓冲机制 | 需要手动包装(如BufferedInputStream) | 自带缓冲(如BufferedReader) |
选择原则:
- 处理二进制文件(如.jpg、.zip、.class):必须用字节流(如
FileInputStream、FileOutputStream); - 处理文本文件(如配置文件、日志):优先用字符流(如
BufferedReader、BufferedWriter),因为字符流会自动处理字符编码(如将字节转为UTF-8字符),避免乱码。
13.NIO中的Channel和Buffer是什么?与传统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(如FileChannel、SocketChannel、ServerSocketChannel);
② Buffer(缓冲区):用于存储数据的容器(如ByteBuffer、CharBuffer),所有IO操作都通过Buffer进行(读:从Channel读到Buffer;写:从Buffer写到Channel);
③ Selector(选择器):用于监听多个Channel的事件(如连接、读、写),实现单线程处理多个Channel(多路复用),适合高并发场景(如网络服务器)。
与传统IO的区别:
| 维度 | 传统IO(流) | NIO(Channel+Buffer+Selector) |
|---|---|---|
| 方向 | 单向(输入/输出) | 双向(可读可写) |
| 阻塞方式 | 阻塞(如InputStream.read()会阻塞直到有数据) | 非阻塞(可选择阻塞或非阻塞模式) |
| 多路复用 | 不支持(每个流需要一个线程) | 支持(Selector监听多个Channel) |
| 适用场景 | 低并发、小数据量 | 高并发、大数据量(如网络编程) |
NIO 的核心价值:
- 高并发支持:单线程处理数千连接
- 零拷贝优化:
transferTo()和内存映射文件 - 非阻塞操作:避免线程因 I/O 而阻塞
- 事件驱动:基于事件的通知机制
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:非核心线程的空闲存活时间(超过该时间,非核心线程会被销毁);④
unit:keepAliveTime的时间单位(如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的理解,是否能区分volatile与synchronized的适用场景。
答案:
-
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):必须用字节流(如
FileInputStream、FileOutputStream); - 处理文本文件(如配置文件、日志):优先用字符流(如
BufferedReader、BufferedWriter),因为字符流会自动处理字符编码(如将字节转为UTF-8字符),避免乱码。
- 处理二进制文件(如.jpg、.zip、.class):必须用字节流(如
22. 问题:NIO中的Channel和Buffer是什么?与传统IO的区别?**
考察点:对NIO的理解,是否能区分传统IO与NIO的差异。
答案:
-
NIO核心组件:
① Channel(通道):类似于传统IO的“流”,但双向(可读可写),支持异步IO(如
FileChannel、SocketChannel、ServerSocketChannel);② Buffer(缓冲区):用于存储数据的容器(如
ByteBuffer、CharBuffer),所有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的区别:
| 维度 | DOM | SAX |
|---|---|---|
| 内存占用 | 高(加载整个文件) | 低(事件驱动,不存储整个文件) |
| 访问方式 | 随机访问(可修改文档树) | 顺序访问(只能读取,不能修改) |
| 适用场景 | 小XML文件(如配置文件) | 大XML文件(如数据交换) |
| 性能 | 慢(加载整个文件) | 快(事件驱动) |
28. 如何设计一个“多线程文件下载器”?
考察点:对多线程、文件IO的综合应用能力,是否能解决实际问题。
答案:
-
核心思路:
① 分块下载:将文件分成多个块(如每个块1MB),每个线程负责下载一个块;
② 线程池管理:使用线程池(如
FixedThreadPool)管理下载线程,控制线程数量;③ 文件合并:下载完成后,将各个块合并为一个完整文件;
④ 断点续传:记录每个块的下载进度(如保存到数据库或文件),下次下载时从断点开始。
-
实现步骤:
① 获取文件大小(通过HTTP的
Content-Length头);② 计算每个块的起始位置和结束位置(如块大小为1MB,第i块的起始位置为
i * 1MB,结束位置为(i+1) * 1MB - 1);③ 创建线程池,提交下载任务(每个任务下载一个块,使用
HttpURLConnection或OkHttp发送带Range头的请求,如Range: bytes=100-200);④ 每个线程将下载的块保存到临时文件(如
file.part1、file.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();
}
}