16. ArrayList 和 Vector 的区别是什么?
ArrayList 和 Vector 均为 List 接口的实现类,底层均基于动态数组实现,但二者在线程安全、性能和扩容机制上存在核心差异,具体区别如下:
- 线程安全:Vector 使用
synchronized关键字实现线程同步,是线程安全的;而 ArrayList 未做同步处理,是非线程安全的,多线程并发修改可能导致数据错乱(如元素覆盖、数组索引越界)。 - 性能:由于 Vector 存在同步锁开销,在单线程环境下,ArrayList 的读写性能远高于 Vector;即使在多线程环境,也更推荐使用
ConcurrentHashMap等并发集合替代 Vector(性能更优)。 - 扩容机制:二者都会根据实际需求动态调整容量,但扩容策略不同:Vector 扩容时,默认将容量翻倍(若未指定扩容增量,
newCapacity = oldCapacity * 2);ArrayList 扩容时,默认将容量增加 50%(newCapacity = oldCapacity + (oldCapacity >> 1))。
17. Array 和 ArrayList 有何区别?
Array(数组)和 ArrayList 是 Java 中两种常用的存储结构,核心区别体现在存储数据类型、容量灵活性和功能丰富度上:
- 存储数据类型:Array 既可以存储基本数据类型(如
int[]、char[]),也可以存储引用类型(如String[]、Object[]);而 ArrayList 只能存储引用类型(若需存储基本类型,需使用其包装类,如ArrayList<Integer>)。 - 容量灵活性:Array 是固定大小的,在初始化时必须指定容量,后续无法动态调整(若需扩容,需手动创建新数组并复制元素);ArrayList 是动态扩容的,初始化时可指定容量,也可使用默认容量(16),当元素数量超过容量时会自动扩容,无需手动处理。
- 功能丰富度:Array 仅提供 length 属性获取长度,无其他内置操作方法;ArrayList 继承了 Collection 接口,提供了丰富的操作方法,如
addAll()(批量添加)、removeAll()(批量删除)、contains()(判断元素是否存在)、iterator()(迭代器遍历)等,开发效率更高。
18. 在 Queue 中 poll () 和 remove () 有什么区别?
poll () 和 remove () 均为 Queue 接口的方法,作用都是 “返回队列的第一个元素,并将该元素从队列中删除”,但二者在无元素时的处理方式上存在关键差异:
- 相同点:当队列中有元素时,调用 poll () 或 remove () 都会返回队首元素,并移除该元素,执行逻辑一致。
- 不同点:若队列中无元素(为空),poll () 会返回
null,不会抛出异常;而 remove () 会直接抛出NoSuchElementException异常,需通过异常处理机制捕获。
代码示例:
java
运行
import java.util.LinkedList;
import java.util.Queue;
import java.util.NoSuchElementException;
public class QueuePollRemoveDemo {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
queue.offer("first"); // 向队列添加元素
// 队列有元素时,二者行为一致
System.out.println(queue.poll()); // 输出:first,队列变为空
// 队列无元素时,poll()返回null
System.out.println(queue.poll()); // 输出:null
// 队列无元素时,remove()抛出异常
try {
queue.remove();
} catch (NoSuchElementException e) {
System.out.println("异常:队列无元素,remove()无法执行");
}
}
}
19. 创建线程有哪几种方式?
在 Java 中,创建线程的核心方式有四种,分别基于不同的接口或类实现,具体如下:
- 继承 Thread 类,重写 run () 方法:Thread 类实现了 Runnable 接口,继承后需重写 run () 方法定义线程执行逻辑,通过调用
start()方法启动线程(而非直接调用 run (),否则会以普通方法形式同步执行)。代码示例:java
运行
class MyThread extends Thread { @Override public void run() { System.out.println("线程1执行:继承Thread类"); } } // 启动线程 new MyThread().start(); - 实现 Runnable 接口,重写 run () 方法:Runnable 接口仅定义 run () 方法,实现类需重写该方法,再将实现类对象作为参数传入 Thread 构造器,通过 Thread 对象的
start()方法启动线程。该方式避免了单继承的限制,更推荐使用。代码示例:java
运行
class MyRunnable implements Runnable { @Override public void run() { System.out.println("线程2执行:实现Runnable接口"); } } // 启动线程 new Thread(new MyRunnable()).start(); - 实现 Callable 接口,重写 call () 方法:Callable 接口与 Runnable 类似,但 call () 方法有返回值且可抛出异常,需结合
FutureTask类(实现 Future 接口)使用,通过 FutureTask 的get()方法获取线程执行结果。代码示例:java
运行
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "线程3执行:实现Callable接口,返回结果"; } } // 启动线程并获取结果 FutureTask<String> futureTask = new FutureTask<>(new MyCallable()); new Thread(futureTask).start(); System.out.println(futureTask.get()); // 输出线程返回结果 - 使用线程池创建线程:通过线程池(如
ThreadPoolExecutor、Executors工具类创建的线程池)管理线程生命周期,避免频繁创建销毁线程的开销。只需将任务(Runnable/Callable 实现类)提交给线程池,线程池会自动分配线程执行任务。代码示例:java
运行
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolDemo { public static void main(String[] args) { // 创建固定大小的线程池 ExecutorService executorService = Executors.newFixedThreadPool(2); // 提交任务 executorService.submit(() -> System.out.println("线程4执行:线程池创建")); // 关闭线程池 executorService.shutdown(); } }
20. 在 Java 程序中怎么保证多线程的运行安全(多线程并发写共享数据)?
多线程并发写共享数据时,需避免数据竞争(多个线程同时修改同一数据导致结果异常),核心保证线程安全的方式有三种:
- 使用线程安全类:Java 提供了
java.util.concurrent包,包含多种线程安全的工具类,可直接用于共享数据操作,无需手动处理同步。例如:AtomicInteger:原子整数类,支持原子化的incrementAndGet()(自增)、decrementAndGet()(自减)等操作,避免并发修改问题;ArrayBlockingQueue:阻塞队列,支持多线程环境下的安全入队、出队操作,实现线程间数据传递。
- 使用自动锁 synchronized:synchronized 是 Java 内置的同步锁,可修饰方法或代码块,确保同一时间只有一个线程进入同步区域执行代码,从而保证共享数据的原子性、可见性和有序性。代码示例(同步代码块):
java
运行
class SafeCounter { private int count = 0; // 共享数据修改方法 public void increment() { // 锁定当前对象,确保同一时间仅一个线程执行 synchronized (this) { count++; } } public int getCount() { return count; } } - 使用手动锁 Lock:Lock 是
java.util.concurrent.locks包下的接口,提供比 synchronized 更灵活的锁控制(如可中断锁、超时锁、读写分离锁),需手动调用lock()加锁和unlock()解锁(通常在 finally 块中解锁,避免锁泄漏)。常用实现类为ReentrantLock(可重入锁)。代码示例:java
运行
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class SafeCounterWithLock { private int count = 0; private Lock lock = new ReentrantLock(); public void increment() { lock.lock(); // 加锁 try { count++; // 共享数据修改 } finally { lock.unlock(); // 解锁(确保无论是否异常都执行) } } public int getCount() { return count; } }
21. 什么是死锁?
死锁是多线程并发场景中的一种阻塞状态,指两个或多个线程互相持有对方所需的独占资源(锁),且均不主动释放已持有的资源,导致所有线程都无法继续执行,陷入永久阻塞。核心产生条件:
- 线程 A 持有独占锁 a,同时尝试获取线程 B 持有的独占锁 b;
- 线程 B 持有独占锁 b,同时尝试获取线程 A 持有的独占锁 a;
- 二者均不释放已持有的锁,形成循环等待,最终导致死锁。
代码示例(死锁场景):
java
运行
class DeadLockDemo {
// 定义两个独占锁对象
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
// 线程1:持有lockA,尝试获取lockB
new Thread(() -> {
synchronized (lockA) {
System.out.println("线程1持有lockA,尝试获取lockB");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("线程1获取lockB,执行完成");
}
}
}).start();
// 线程2:持有lockB,尝试获取lockA
new Thread(() -> {
synchronized (lockB) {
System.out.println("线程2持有lockB,尝试获取lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("线程2获取lockA,执行完成");
}
}
}).start();
}
}
执行结果:两个线程均会卡在尝试获取对方锁的步骤,无法继续执行,形成死锁。
22. 怎么防止死锁?
防止死锁的核心思路是破坏死锁产生的必要条件(如循环等待、永久持有锁),常用方法如下:
- 使用带超时的锁尝试机制:避免线程永久等待锁,通过
tryLock(long timeout, TimeUnit unit)方法(如ReentrantLock、ReentrantReadWriteLock支持)尝试获取锁,若超时未获取到则主动释放已持有的锁,退出等待。代码示例:java
运行
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; class AvoidDeadLockWithTryLock { private static final ReentrantLock lockA = new ReentrantLock(); private static final ReentrantLock lockB = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { try { // 尝试获取lockA,超时时间1秒 if (lockA.tryLock(1, TimeUnit.SECONDS)) { try { System.out.println("线程1持有lockA,尝试获取lockB"); // 尝试获取lockB,超时时间1秒 if (lockB.tryLock(1, TimeUnit.SECONDS)) { try { System.out.println("线程1获取lockB,执行完成"); } finally { lockB.unlock(); // 释放lockB } } else { System.out.println("线程1获取lockB超时,释放lockA"); } } finally { lockA.unlock(); // 释放lockA } } else { System.out.println("线程1获取lockA超时"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); } } - 使用并发工具类替代手动锁:优先使用 Java 提供的
java.util.concurrent包下的并发类(如ConcurrentHashMap、CountDownLatch),这些类内部已优化锁机制,避免死锁风险,无需手动设计锁逻辑。 - 降低锁的使用粒度与减少同步代码块:
- 降低锁粒度:将一个大锁拆分为多个小锁(如
ConcurrentHashMap的分段锁),减少线程对同一锁的竞争; - 减少同步代码块:仅在修改共享数据的核心逻辑处加锁,避免锁覆盖无关代码,缩短线程持有锁的时间。
- 降低锁粒度:将一个大锁拆分为多个小锁(如
23. 什么是 Java 序列化?什么情况下需要序列化?
23.1 定义
Java 序列化是一种将对象在内存中的状态(属性值、类型信息)转换为字节序列的过程;反序列化则是将字节序列恢复为内存中对象的过程。序列化的核心目的是实现对象的 “持久化存储” 和 “跨进程传输”。
23.2 需要序列化的场景
- 对象状态持久化到存储介质:需将内存中的对象状态保存到文件、数据库等存储介质时(如保存用户会话信息到文件),需先序列化对象为字节序列,再写入存储介质;读取时通过反序列化恢复对象。
- 对象通过网络传输:需通过套接字(Socket)在网络中传输对象时(如分布式系统中跨节点传递对象、RPC 远程方法调用),需先序列化对象为字节流,通过网络发送;接收方再反序列化字节流为对象。
- 对象通过 RMI 传输:Java 远程方法调用(RMI)中,客户端与服务器端之间传递的对象需实现序列化,否则无法跨进程传输。
23.3 注意事项
- 被序列化的类需实现
java.io.Serializable接口(标记接口,无任何抽象方法,仅用于标识该类可序列化); - 若类中存在不需要序列化的字段,需用
transient关键字修饰(序列化时会忽略该字段,反序列化时该字段为默认值,如基本类型为 0,引用类型为 null); - 静态成员变量不会被序列化(静态变量属于类,不属于对象状态,序列化仅处理对象的实例属性)。
24. Error 和 Exception 区别是什么?
Error 和 Exception 均为 java.lang.Throwable 类的子类,是 Java 异常体系的两大核心类型,但二者在错误性质、处理方式和适用场景上存在本质区别:
| 对比维度 | Error | Exception |
|---|---|---|
| 错误性质 | 虚拟机级别的严重错误,如系统崩溃、内存不足、堆栈溢出等,属于 “不可恢复的错误” | 应用程序级别的异常,如空指针、数组越界、文件未找到等,属于 “可恢复的异常” |
| 编译期检测 | 编译器不检测 Error,无法在编译阶段预知 | 分为受检异常(如 IOException)和非受检异常(如 RuntimeException),受检异常需编译器强制处理 |
| 处理方式 | 应用程序不应捕获和处理 Error(捕获后也无法恢复),通常会导致程序终止 | 应用程序需捕获并处理 Exception(如通过 try-catch 块),处理后程序可继续执行 |
| 示例 | OutOfMemoryError(内存不足)、StackOverflowError(堆栈溢出) | NullPointerException(空指针)、FileNotFoundException(文件未找到) |
25. 运行时异常和一般异常(受检异常)区别是什么?
运行时异常和一般异常(受检异常)均为 Exception 的子类,核心区别在于编译器强制处理要求和使用场景:
25.1 运行时异常(非受检异常)
- 定义:继承自
RuntimeException类及其子类的异常,代表 JVM 运行期间可能出现的异常(如空指针、数组越界)。 - 编译器处理:编译器不强制要求捕获或声明该类异常,即使未处理,程序也可通过编译;若运行时发生该异常且未捕获,会导致线程终止并抛出异常栈信息。
- 示例:
NullPointerException(空指针访问)、ArrayIndexOutOfBoundsException(数组索引越界)、ClassCastException(类型转换异常)。 - 使用建议:通常用于表示程序逻辑错误(如参数非法),无需强制处理,推荐通过优化代码逻辑避免(如判断对象非 null 后再访问)。
25.2 一般异常(受检异常)
- 定义:Exception 类中除 RuntimeException 及其子类之外的异常,代表程序运行中可预知的异常(如文件操作、网络连接异常)。
- 编译器处理:编译器强制要求处理该类异常,若未通过 try-catch 捕获或在方法签名中用
throws声明,程序无法通过编译。 - 示例:
IOException(IO 操作异常)、SQLException(数据库操作异常)、ClassNotFoundException(类未找到异常)。 - 使用建议:通常用于表示外部环境异常(如文件不存在、网络断开),需强制处理,确保程序在异常发生后可优雅恢复(如提示用户 “文件不存在” 并重新选择文件)。
25.3 核心区别总结
| 对比维度 | 运行时异常(非受检异常) | 一般异常(受检异常) |
|---|---|---|
| 父类 | RuntimeException | Exception(非 RuntimeException 分支) |
| 编译器强制处理 | 否(无需捕获或声明) | 是(必须捕获或声明) |
| 异常来源 | 程序逻辑错误(如空指针) | 外部环境异常(如文件未找到) |
| 处理原则 | 优化逻辑避免异常发生 | 强制捕获并处理,确保程序恢复 |
26. throw 和 throws 的区别是什么?
throw 和 throws 均用于 Java 异常处理,但其作用位置、功能和使用方式完全不同,具体区别如下:
| 对比维度 | throw | throws |
|---|---|---|
| 作用位置 | 方法内部(代码块中) | 方法声明处(方法名后,参数列表与方法体之间) |
| 功能 | 主动抛出一个具体的异常对象(触发异常) | 声明该方法可能抛出的异常列表(告知调用者异常风险) |
| 抛出对象数量 | 一次只能抛出一个异常对象(如 throw new NullPointerException()) | 可声明多个异常(用逗号分隔,如 throws IOException, SQLException) |
| 异常类型 | 可抛出受检异常和非受检异常 | 声明的异常可为受检异常或非受检异常(受检异常需调用者处理) |
| 使用示例 | if (obj == null) throw new NullPointerException(); | public void readFile() throws FileNotFoundException {} |
代码示例(结合使用):
java
运行
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class ThrowThrowsDemo {
// 方法声明可能抛出FileNotFoundException(受检异常)
public static String readFile(String path) throws FileNotFoundException {
File file = new File(path);
if (!file.exists()) {
// 方法内部主动抛出具体异常对象
throw new FileNotFoundException("文件不存在:" + path);
}
// 读取文件逻辑(省略)
return new Scanner(file).nextLine();
}
public static void main(String[] args) {
// 调用readFile(),需处理其声明的FileNotFoundException
try {
readFile("test.txt");
} catch (FileNotFoundException e) {
System.out.println("处理异常:" + e.getMessage());
}
}
}
27. try-catch-finally 中哪个部分可以省略?
在 try-catch-finally 结构中,catch 块可以省略,但需满足特定条件;try 块是核心,不可省略;finally 块也可省略,但通常用于资源清理,建议保留。具体规则如下:
27.1 可省略的情况:catch 块
- 适用场景:仅处理运行时异常(非受检异常),且无需显式捕获异常(如仅需通过 finally 块做资源清理,不关心异常具体信息)。此时可省略 catch 块,直接用 try-finally 结构。代码示例:
java
运行
public class TryFinallyDemo { public static void main(String[] args) { Scanner scanner = null; try { scanner = new Scanner(new File("test.txt")); System.out.println(scanner.nextLine()); // 可能抛出运行时异常(如NoSuchElementException) } finally { // 无论是否发生异常,均关闭资源 if (scanner != null) { scanner.close(); } } } } - 注意:若 try 块中可能抛出受检异常(如
FileNotFoundException),则不能省略 catch 块,也需在方法签名中用 throws 声明该异常,否则编译器报错(受检异常必须强制处理)。
27.2 不可省略的情况:try 块
try 块是异常处理的核心,用于包裹可能抛出异常的代码,必须存在,否则 catch 和 finally 块无意义(无代码可监控异常,也无资源需清理)。
27.3 可省略的情况:finally 块
若无需在异常发生前后执行固定清理逻辑(如关闭文件、释放锁),则可省略 finally 块,直接用 try-catch 结构。但通常建议保留 finally 块处理资源清理,避免资源泄漏。
28. == 和 equals 的区别是什么?
== 和 equals 均用于判断 “相等性”,但二者判断维度和适用类型完全不同,具体区别如下:
28.1 == 运算符
- 判断维度:判断两个对象的内存地址是否相同(即是否为同一对象);对于基本数据类型,判断值是否相同(因基本数据类型直接存储值,无内存地址概念)。
- 适用类型:
- 基本数据类型(如
int、char):比较值是否相等(如1 == 1为 true); - 引用数据类型(如
String、Object):比较内存地址是否相同(如new Object() == new Object()为 false,因两个对象在堆中地址不同)。
- 基本数据类型(如
28.2 equals () 方法
- 定义:
equals()是Object类的方法,默认实现与 == 一致(比较引用地址),但多数类会重写该方法,改为比较对象的 “内容是否相等”。 - 适用类型:仅适用于引用数据类型(基本数据类型无方法,需通过包装类调用 equals ())。
- 两种使用情况:
- 类未重写
equals():等价于 ==,比较引用地址(如自定义类未重写时,new MyClass() == new MyClass()为 false); - 类重写
equals():比较对象内容(如String类重写后,"abc".equals("abc")为 true,即使是不同对象,只要内容相同则返回 true)。
- 类未重写
代码示例:
java
运行
public class EqualsDemo {
public static void main(String[] args) {
// 1. 基本数据类型:==比较值
int a = 10;
int b = 10;
System.out.println(a == b); // 输出:true
// 2. 引用数据类型:==比较地址
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2); // 输出:false(不同对象,地址不同)
System.out.println(s1.equals(s2)); // 输出:true(String重写equals,比较内容)
// 3. 未重写equals的自定义类
class MyClass {}
MyClass obj1 = new MyClass();
MyClass obj2 = new MyClass();
System.out.println(obj1.equals(obj2)); // 输出:false(默认比较地址)
}
}
29. Java 中 IO 流分为几种?
Java IO 流可根据不同维度划分为三类,具体分类如下:
29.1 按流的流向划分
- 输入流(InputStream/Reader):数据从外部存储介质(如文件、网络)流向内存的流,用于读取数据(如
FileInputStream从文件读取字节到内存)。 - 输出流(OutputStream/Writer):数据从内存流向外部存储介质的流,用于写入数据(如
FileOutputStream将内存中的字节写入文件)。
29.2 按操作单元划分
- 字节流(InputStream/OutputStream):以字节(8 位)为操作单元的流,可处理任意类型的数据(如文本、图片、视频),核心类包括
FileInputStream、FileOutputStream、BufferedInputStream。 - 字符流(Reader/Writer):以字符(16 位,适配 Unicode 编码)为操作单元的流,仅用于处理文本数据(如
.txt文件),可自动处理字符编码转换,核心类包括FileReader、FileWriter、BufferedReader。
29.3 按流的角色划分
- 节点流(直接流):直接与数据源(如文件、网络端口)连接的流,直接操作数据源,无中间缓冲层(如
FileInputStream直接读取文件,SocketInputStream直接读取网络数据)。 - 处理流(包装流):基于节点流或其他处理流包装而成的流,用于增强节点流的功能(如缓冲、编码转换),不直接连接数据源(如
BufferedInputStream为节点流添加缓冲,减少 IO 次数;InputStreamReader将字节流转换为字符流)。
30. 什么是反射机制?
30.1 定义
Java 反射机制是指程序在运行状态中,对于任意一个类,能够动态获取该类的所有属性(成员变量)、方法(构造方法、普通方法)、接口等信息;对于任意一个对象,能够动态调用其任意方法和修改其任意属性的能力。反射打破了 Java 编译期的类型绑定限制,实现 “动态编译”(运行时确定类型和绑定对象)。
30.2 静态编译与动态编译的对比
- 静态编译:编译期确定类的类型和对象的绑定关系,代码执行逻辑固定(如
Student stu = new Student();,编译期已确定 stu 为 Student 类型)。 - 动态编译:运行期才确定类的类型和对象的绑定关系,代码执行逻辑可动态调整(如通过反射加载
Student类,运行时才创建对象,无需编译期知晓Student类)。
30.3 反射的核心价值
- 框架开发:多数 Java 框架(如 Spring、MyBatis)基于反射实现对象创建、依赖注入、配置解析(如 Spring 通过反射创建配置文件中指定的 Bean 对象);
- 动态代理:实现 AOP(面向切面编程)时,通过反射动态生成代理类,增强目标方法的功能(如事务管理、日志记录);
- 工具开发:开发 IDE、调试工具时,通过反射获取类的结构信息(如属性、方法),实现代码提示、断点调试等功能。
27万+

被折叠的 条评论
为什么被折叠?



