java基础100道面试题
一、Java基础概念
1. Java的三大特性是什么?解释其含义。
Java的三大特性是封装、继承和多态:
-
封装(Encapsulation):将数据(属性)和操作数据的方法绑定在一起,形成一个类。通过访问修饰符(如
private
)限制对某些成员的直接访问,增强安全性。 -
继承(Inheritance):子类可以继承父类的属性和方法,从而实现代码复用和扩展性。
-
多态(Polymorphism):同一操作作用于不同的对象时,可以有不同的解释,产生不同的执行结果。主要通过方法重载和方法重写实现。
2. Java中 ==
和 equals()
的区别?
-
==
:用于比较两个变量或对象的引用是否相同。对于基本数据类型,比较的是值;对于引用数据类型,比较的是内存地址。 -
equals()
:默认情况下与==
相同(比较地址),但可以通过重写来比较对象的内容。例如,String
类重写了equals()
方法,用于比较字符串内容是否相等。
3. String、StringBuilder 和 StringBuffer 的区别?
特性/类名 | 是否可变 | 线程安全 | 性能 |
---|---|---|---|
String | 不可变 | 是 | 最低 |
StringBuilder | 可变 | 否 | 较高 |
StringBuffer | 可变 | 是 | 中等 |
-
String:不可变对象,每次修改都会生成新的对象,适合少量字符串操作。
-
StringBuilder:可变对象,线程不安全,适合单线程环境下的高效字符串操作。
-
StringBuffer:可变对象,线程安全,适合多线程环境下的字符串操作。
4. Java中的基本数据类型有哪些?对应的包装类是什么?
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
5. 自动装箱(Autoboxing)和拆箱(Unboxing)是什么?
-
自动装箱(Autoboxing):将基本数据类型自动转换为其对应的包装类。例如:
int i = 10; Integer obj = i;
-
拆箱(Unboxing):将包装类自动转换为对应的基本数据类型。例如:
Integer obj = new Integer(10); int i = obj;
6. final关键字的作用(修饰类、方法、变量)?
-
修饰类:被
final
修饰的类不能被继承。 -
修饰方法:被
final
修饰的方法不能被子类重写。 -
修饰变量:被
final
修饰的变量是一个常量,只能赋值一次,且不能修改。
7. static关键字的作用?
-
静态变量:属于类,所有对象共享同一个静态变量。
-
静态方法:可以直接通过类名调用,不需要创建对象。
-
静态代码块:在类加载时执行,用于初始化静态资源。
8. Java是否支持多继承?如何实现类似多继承的功能?
-
Java不支持类的多继承,即一个类只能继承一个父类。
-
实现类似多继承的功能:通过接口(Interface)实现。一个类可以实现多个接口,从而达到类似多继承的效果。
9. 接口(Interface)和抽象类(Abstract Class)的区别?
特性 | 接口(Interface) | 抽象类(Abstract Class) |
---|---|---|
成员变量 | 默认为public static final | 可以有各种修饰符的变量 |
方法 | 默认为public abstract | 可以有具体实现的方法 |
继承关系 | 类可以实现多个接口 | 类只能继承一个抽象类 |
构造方法 | 没有构造方法 | 可以有构造方法 |
使用场景 | 定义规范 | 提供部分实现和模板 |
10. 什么是不可变对象(Immutable Object)?举例说明。
-
不可变对象:一旦创建后,其状态(属性值)不能被修改的对象。
-
特点:
-
所有字段必须是
final
且不可变。 -
不提供任何修改对象状态的方法。
-
如果需要“修改”,则返回一个新的对象。
-
-
示例:
-
Java中的
String
、Integer
等包装类都是不可变对象。 -
示例代码:
String str = "hello"; str.concat(" world"); // 不会改变str,而是返回一个新的字符串对象 System.out.println(str); // 输出 "hello"
-
二、面向对象编程
1. 重载(Overload)和重写(Override)的区别?
特性 | 重载(Overload) | 重写(Override) |
---|---|---|
定义位置 | 同一个类中 | 子类中 |
方法签名 | 参数列表不同 | 参数列表必须相同 |
返回值类型 | 可以不同 | 必须相同或兼容(协变返回类型) |
访问修饰符 | 不影响 | 子类不能更严格 |
示例 | void show(int a) 和 void show(String a) | 父类方法被子类重新实现 |
2. 构造方法能否被重写?
-
不能。构造方法没有返回值,也不能使用
@Override
注解,因此无法被重写。 -
可以重载:同一个类中可以定义多个构造方法,只要参数列表不同即可。
3. 如何实现对象克隆?深拷贝和浅拷贝的区别?
-
浅拷贝:只复制对象本身,引用类型的成员变量仍然指向原来的内存地址。
-
深拷隆:不仅复制对象本身,还递归地复制引用类型的成员变量,确保新对象与原对象完全独立。
实现方式:
-
实现
Cloneable
接口,并重写clone()
方法。 -
示例代码:
class Person implements Cloneable { private String name; private int age; public Object clone() throws CloneNotSupportedException { return super.clone(); // 浅拷贝 } } // 深拷贝示例 class Student implements Cloneable { private String name; private Address address; // 引用类型 @Override protected Object clone() throws CloneNotSupportedException { Student cloned = (Student) super.clone(); // 浅拷贝 cloned.address = (Address) this.address.clone(); // 手动深拷贝 return cloned; } }
4. 类的初始化顺序(静态块、实例块、构造方法)?
类的初始化顺序如下:
-
加载类时:执行静态变量初始化和静态代码块(按代码出现顺序)。
-
创建对象时:
-
先执行实例变量初始化。
-
再执行实例代码块(按代码出现顺序)。
-
最后执行构造方法。
-
示例代码:
class Example { static { System.out.println("静态代码块"); } { System.out.println("实例代码块"); } Example() { System.out.println("构造方法"); } public static void main(String[] args) { new Example(); } }
输出:
静态代码块 实例代码块 构造方法
5. 什么是多态?如何通过代码体现?
-
多态:同一操作作用于不同的对象时,产生不同的执行结果。
-
实现方式:通过继承和方法重写实现。
-
代码示例:
class Animal { void sound() { System.out.println("动物发出声音"); } } class Dog extends Animal { @Override void sound() { System.out.println("汪汪"); } } class Cat extends Animal { @Override void sound() { System.out.println("喵喵"); } } public class Test { public static void main(String[] args) { Animal a1 = new Dog(); // 多态 a1.sound(); // 输出 "汪汪" Animal a2 = new Cat(); // 多态 a2.sound(); // 输出 "喵喵" } }
6. 解释Java中的访问修饰符(public、protected、default、private)。
修饰符 | 同一包内 | 子类(不同包) | 不同包且非子类 |
---|---|---|---|
public | √ | √ | √ |
protected | √ | √ | × |
default | √ | × | × |
private | × | × | × |
7. 如何防止类被继承?
-
使用
final
关键字修饰类,例如:
public final class MyClass { // 类不能被继承 }
8. 内部类有哪些类型?各自的特点是什么?
-
成员内部类:定义在类内部,作为外部类的成员,可以访问外部类的所有成员。
-
静态内部类:使用
static
修饰,属于外部类本身,不依赖于外部类实例。 -
局部内部类:定义在方法或代码块中,只能在定义它的方法或代码块中使用。
-
匿名内部类:没有名字的内部类,通常用于简化代码,常用于事件监听器等场景。
9. 匿名内部类能否访问外部类的非final
变量?
-
不能直接访问外部类的非
final
局部变量。 -
原因:匿名内部类的对象可能比外部方法的生命周期更长,为了保证安全性,JVM要求匿名内部类只能访问
final
或effectively final
(从Java 8开始支持)的局部变量。
10. 什么是单例模式?写出线程安全的实现方式。
-
单例模式:确保一个类只有一个实例,并提供全局访问点。
线程安全的实现方式:
-
饿汉式(线程安全,但可能会浪费资源):
public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
-
懒汉式 + 双重检查锁定(线程安全且延迟加载):
public class Singleton { private static volatile Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; } }
-
静态内部类(推荐,线程安全且延迟加载):
public class Singleton { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
三、集合框架
1. ArrayList 和 LinkedList 的区别及适用场景?
特性 | ArrayList | LinkedList |
---|---|---|
底层实现 | 基于动态数组 | 基于双向链表 |
随机访问效率 | 高(通过索引直接访问) | 低(需要遍历节点) |
插入/删除效率 | 低(可能涉及大量元素移动) | 高(只需修改指针) |
内存占用 | 较小 | 较大(每个节点包含额外的指针) |
适用场景 | 需要频繁随机访问的场景 | 需要频繁插入和删除操作的场景 |
2. HashMap 的工作原理?如何解决哈希冲突?
-
工作原理:
-
HashMap
使用哈希表存储键值对。 -
根据键的
hashCode()
计算哈希值,确定存储位置。 -
如果发生哈希冲突(多个键映射到同一个桶),则使用链表或红黑树存储这些键值对。
-
-
解决哈希冲突:
-
默认情况下,使用链表解决冲突。
-
当链表长度超过阈值(默认为8)且当前桶数量大于64时,链表会转换为红黑树以提高查找效率。
-
3. HashMap 和 Hashtable 的区别?
特性 | HashMap | Hashtable |
---|---|---|
线程安全 | 不是线程安全的 | 是线程安全的 |
允许 null 键值 | 允许一个 null 键和多个 null 值 | 不允许 null 键和 null 值 |
性能 | 更高(非同步) | 较低(同步) |
继承关系 | 实现 Map 接口 | 继承 Dictionary 类 |
4. ConcurrentHashMap 如何保证线程安全?
-
分段锁机制(Java 7):将整个哈希表分为多个段(Segment),每个段独立加锁,从而减少锁竞争。
-
CAS + synchronized(Java 8):使用 CAS 操作和 synchronized 锁来保证线程安全,避免了分段锁的设计,提升了性能。
5. HashSet 的底层实现是什么?
-
HashSet
底层基于HashMap
实现。 -
每个元素作为
HashMap
的键,而值是一个固定的对象(PRESENT
)。 -
因此,
HashSet
的特性与HashMap
的键一致:无序、不允许重复。
6. Comparable 和 Comparator 的区别?
特性 | Comparable | Comparator |
---|---|---|
定义方式 | 在类中实现 Comparable 接口 | 作为独立的比较器类实现 |
自然排序 | 提供自然排序规则 | 可以提供多种自定义排序规则 |
灵活性 | 单一排序规则 | 多种排序规则 |
使用场景 | 对象本身具有明确的排序规则时 | 需要多种排序规则时 |
7. Iterator 和 ListIterator 的区别?
特性 | Iterator | ListIterator |
---|---|---|
遍历方向 | 只能单向遍历(向前) | 可以双向遍历(向前和向后) |
功能 | 只能删除元素 | 支持添加、删除和替换元素 |
适用范围 | 所有集合 | 仅适用于 List 集合 |
8. fail-fast 和 fail-safe 机制的区别?
特性 | fail-fast | fail-safe |
---|---|---|
定义 | 快速失败机制,当检测到集合被修改时抛出异常 | 安全失败机制,允许在并发修改时正常运行 |
实现方式 | 使用 modCount 检测修改次数 | 使用副本机制(如 CopyOnWriteArrayList ) |
适用场景 | 单线程环境 | 多线程环境 |
9. 如何实现一个不可变的集合?
-
使用
Collections.unmodifiableXXX()
方法包装集合:
List<String> list = new ArrayList<>(); list.add("A"); List<String> unmodifiableList = Collections.unmodifiableList(list);
-
自定义实现:确保集合内部状态不可修改,并且不暴露任何可修改的方法。
10. ArrayList 的默认初始容量是多少?扩容机制是什么?
-
默认初始容量:10。
-
扩容机制:
-
当容量不足时,扩容为原容量的 1.5 倍(
newCapacity = oldCapacity + (oldCapacity >> 1)
)。 -
创建一个新的数组,并将原有元素复制到新数组中。
-
四、异常处理
1. Java 异常分类(Error 和 Exception 的区别)?
-
Error:
-
表示 JVM 系统级错误或资源耗尽等严重问题,程序无法处理。
-
示例:
OutOfMemoryError
、StackOverflowError
。
-
-
Exception:
-
表示程序运行时发生的异常情况,可以通过代码捕获和处理。
-
分为 受检异常(Checked Exception) 和 非受检异常(Unchecked Exception):
-
受检异常:必须在代码中显式处理(如
IOException
)。 -
非受检异常:无需强制处理(如
RuntimeException
及其子类)。
-
-
2. try-catch-finally 中,finally 是否一定会执行?
-
通常情况下会执行:
-
finally
块无论是否发生异常都会执行。
-
-
例外情况:
-
程序提前终止(如调用
System.exit()
)。 -
finally
块中发生死循环或抛出其他异常。
-
3. throw 和 throws 的区别?
特性 | throw | throws |
---|---|---|
用途 | 用于抛出具体的异常对象 | 用于声明方法可能抛出的异常类型 |
使用位置 | 方法体内 | 方法签名后 |
示例 | throw new NullPointerException(); | public void method() throws IOException {} |
4. 什么是自定义异常?如何实现?
-
自定义异常:根据业务需求定义的异常类,继承自
Exception
或RuntimeException
。 -
实现方式:
// 自定义受检异常 class MyException extends Exception { public MyException(String message) { super(message); } } // 自定义非受检异常 class MyRuntimeException extends RuntimeException { public MyRuntimeException(String message) { super(message); } }
5. try-with-resources 的作用及使用场景?
-
作用:自动关闭实现了
AutoCloseable
接口的对象(如文件流、数据库连接等),避免手动调用close()
方法。 -
使用场景:
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) { String line = br.readLine(); } catch (IOException e) { e.printStackTrace(); }
6. OutOfMemoryError 和 StackOverflowError 的区别?
特性 | OutOfMemoryError | StackOverflowError |
---|---|---|
原因 | 内存不足 | 栈溢出(递归过深或栈帧过大) |
触发场景 | 创建大对象、加载大量数据等 | 无限递归调用 |
7. 列举常见的 RuntimeException?
-
NullPointerException
-
ArrayIndexOutOfBoundsException
-
ClassCastException
-
IllegalArgumentException
-
ArithmeticException
-
NumberFormatException
-
IllegalStateException
8. 为什么推荐捕获具体异常而非 Exception?
-
捕获具体异常可以更精确地处理不同类型的异常,避免掩盖潜在问题。
-
示例:
try { // 可能抛出多种异常的代码 } catch (FileNotFoundException e) { // 处理文件未找到的情况 } catch (IOException e) { // 处理其他 I/O 错误 }
9. final、finally 和 finalize() 的区别?
特性 | final | finally | finalize() |
---|---|---|---|
作用 | 修饰变量、方法或类,表示不可修改 | 异常处理机制,确保代码块执行 | 对象销毁前的清理操作 |
使用场景 | 定义常量、防止覆盖等 | 确保资源释放 | 已被废弃,不推荐使用 |
10. 异常处理对性能的影响有哪些?
-
影响性能的原因:
-
异常处理机制涉及栈的展开和恢复,开销较大。
-
频繁抛出异常会显著降低程序性能。
-
-
优化建议:
-
避免在正常逻辑中频繁使用异常。
-
使用条件判断代替异常捕获来处理常见错误。
-
五、多线程与并发
1. 线程的创建方式有哪些?
-
继承
Thread
类:class MyThread extends Thread { public void run() { System.out.println("Thread running"); } } new MyThread().start();
-
实现
Runnable
接口:class MyRunnable implements Runnable { public void run() { System.out.println("Runnable running"); } } new Thread(new MyRunnable()).start();
-
实现
Callable
接口(支持返回值和抛出异常):Callable<Integer> task = () -> { return 42; }; FutureTask<Integer> futureTask = new FutureTask<>(task); new Thread(futureTask).start();
2. Runnable 和 Callable 的区别?
特性 | Runnable | Callable |
---|---|---|
返回值 | 无返回值 | 支持返回值 |
异常处理 | 不能抛出受检异常 | 可以抛出受检异常 |
使用场景 | 适合简单任务 | 适合需要返回结果的任务 |
3. 线程的生命周期有哪些状态?
-
新建(New):线程对象被创建,但尚未启动。
-
可运行(Runnable):线程已启动,等待 CPU 调度。
-
阻塞(Blocked):线程因竞争锁而被阻塞。
-
等待(Waiting/ Timed Waiting):线程调用
wait()
或sleep()
等方法进入等待状态。 -
终止(Terminated):线程执行完毕或异常退出。
4. sleep() 和 wait() 的区别?
特性 | sleep() | wait() |
---|---|---|
是否释放锁 | 不释放 | 释放 |
调用位置 | 可在任何地方调用 | 必须在同步代码块中调用 |
用途 | 暂停线程一段时间 | 让线程等待其他线程的通知 |
5. synchronized 关键字的用法(修饰方法、代码块)?
-
修饰方法:
-
对实例方法加锁:锁为当前对象(
this
)。 -
对静态方法加锁:锁为类的
Class
对象。
-
-
修饰代码块:
synchronized (lockObject) { // 同步代码块 }
6. volatile 关键字的作用?
-
作用:
-
保证变量的可见性(线程间共享变量的修改能及时更新到主内存)。
-
禁止指令重排序。
-
-
局限性:
-
不保证原子性,适用于单个变量的读写操作。
-
7. 什么是线程安全?如何保证线程安全?
-
线程安全:多个线程访问共享资源时,程序的行为符合预期。
-
实现方式:
-
使用同步机制(如
synchronized
、ReentrantLock
)。 -
使用不可变对象。
-
使用线程本地存储(
ThreadLocal
)。 -
使用并发集合(如
ConcurrentHashMap
)。
-
8. ReentrantLock 和 synchronized 的区别?
特性 | ReentrantLock | synchronized |
---|---|---|
功能扩展 | 支持公平锁、中断等待等 | 功能有限 |
性能 | 更灵活,可能更高 | 简单高效 |
使用复杂度 | 需要显式加锁和解锁 | 自动管理锁 |
9. 线程池的核心参数有哪些?工作原理是什么?
-
核心参数:
-
corePoolSize
:核心线程数。 -
maximumPoolSize
:最大线程数。 -
keepAliveTime
:空闲线程存活时间。 -
workQueue
:任务队列。 -
RejectedExecutionHandler
:拒绝策略。
-
-
工作原理:
-
当提交任务时,如果线程数小于核心线程数,则创建新线程执行任务。
-
如果线程数达到核心线程数且队列未满,则将任务放入队列。
-
如果队列已满且线程数小于最大线程数,则创建新线程。
-
如果无法处理任务,则触发拒绝策略。
-
10. ThreadLocal 的作用及可能的内存泄漏问题?
-
作用:为每个线程提供独立的变量副本,避免线程间数据共享。
-
内存泄漏问题:
-
ThreadLocal
使用弱引用存储键值对,但值是强引用。 -
如果不手动清理
ThreadLocal
,可能导致线程池中的线程持有无效的引用,造成内存泄漏。
-
11. 什么是死锁?如何避免死锁?
-
死锁:两个或多个线程互相等待对方释放资源,导致所有线程都无法继续执行。
-
避免死锁:
-
按固定顺序加锁。
-
使用超时机制。
-
减少锁的粒度。
-
12. ConcurrentHashMap 如何实现高并发?
-
分段锁机制(Java 7):将哈希表分为多个段,每个段独立加锁。
-
CAS + synchronized(Java 8):使用 CAS 操作和细粒度锁,减少锁竞争。
13. AtomicInteger 的实现原理?
-
基于 CAS(Compare-And-Swap)操作实现线程安全的原子操作。
-
内部维护一个
volatile
变量,确保可见性。
14. CountDownLatch 和 CyclicBarrier 的区别?
特性 | CountDownLatch | CyclicBarrier |
---|---|---|
重用性 | 不可重用 | 可重用 |
用途 | 等待多个线程完成 | 等待多个线程到达屏障点 |
15. 如何实现生产者-消费者模式?
-
基于阻塞队列:
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10); class Producer implements Runnable { public void run() { try { while (true) { queue.put(produce()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private Integer produce() { return 1; } } class Consumer implements Runnable { public void run() { try { while (true) { consume(queue.take()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private void consume(Integer item) { System.out.println("Consumed: " + item); } }
六、IO与NIO
1. Java IO流的分类(字节流、字符流)?
-
字节流:
-
基本单位是字节(
byte
),适用于处理二进制数据。 -
输入流:
InputStream
,输出流:OutputStream
。
-
-
字符流:
-
基本单位是字符(
char
),适用于处理文本数据。 -
输入流:
Reader
,输出流:Writer
。
-
2. BIO、NIO 和 AIO 的区别?
特性 | BIO | NIO | AIO |
---|---|---|---|
模型 | 阻塞式 I/O | 非阻塞式 I/O | 异步 I/O |
多路复用 | 每个连接一个线程 | 单线程管理多个连接 | 系统负责调度 |
适用场景 | 小规模连接 | 大规模连接 | 高并发场景 |
3. FileInputStream 和 FileReader 的区别?
特性 | FileInputStream | FileReader |
---|---|---|
数据类型 | 字节流 | 字符流 |
用途 | 读取二进制文件 | 读取文本文件 |
编码问题 | 不涉及编码 | 可能涉及编码转换 |
4. 什么是 NIO 中的 Channel、Buffer 和 Selector?
-
Channel:
-
类似于传统的流,但支持双向读写操作。
-
常见实现:
FileChannel
、SocketChannel
。
-
-
Buffer:
-
数据容器,用于存储和操作数据。
-
常见类型:
ByteBuffer
、CharBuffer
、IntBuffer
。
-
-
Selector:
-
用于监听多个通道的事件(如连接请求、数据到达等),实现多路复用。
-
5. 序列化和反序列化的作用?如何实现?
-
作用:
-
序列化:将对象转换为字节流,便于存储或传输。
-
反序列化:将字节流还原为对象。
-
-
实现方式:
-
实现
Serializable
接口:class Person implements Serializable { private static final long serialVersionUID = 1L; private String name; private int age; // Getters and setters } // 序列化 try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) { oos.writeObject(new Person("Alice", 25)); } // 反序列化 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) { Person person = (Person) ois.readObject(); }
-
6. transient 关键字的作用?
-
作用:标记某个字段不参与序列化过程。
-
示例:
class Person implements Serializable { private String name; private transient String password; // 不会被序列化 }
7. 什么是零拷贝(Zero Copy)?
-
零拷贝:减少数据在内核态和用户态之间的拷贝次数,提高 I/O 性能。
-
实现机制:
-
使用
sendfile()
或mmap()
等系统调用。 -
数据直接从磁盘缓冲区发送到网络接口,无需经过用户态。
-
8. 如何读取大文件(如 10GB)而不内存溢出?
-
方法:
-
使用缓冲流(
BufferedReader
或BufferedInputStream
)分块读取。 -
使用 NIO 的
MappedByteBuffer
映射文件到内存。
-
-
示例代码:
// 使用 BufferedReader 分块读取 try (BufferedReader br = new BufferedReader(new FileReader("large_file.txt"))) { String line; while ((line = br.readLine()) != null) { processLine(line); // 处理每一行 } } // 使用 MappedByteBuffer try (FileChannel channel = FileChannel.open(Paths.get("large_file.bin"), StandardOpenOption.READ)) { MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); byte[] data = new byte[buffer.limit()]; buffer.get(data); processData(data); // 处理数据 }
七、JVM与内存管理
1. JVM 内存区域划分(堆、栈、方法区等)?
-
程序计数器:记录当前线程执行的字节码指令地址。
-
虚拟机栈:存储局部变量表、操作数栈、动态链接等信息,每个线程独占。
-
本地方法栈:为 JNI(Native 方法)服务。
-
堆:存放对象实例和数组,所有线程共享。
-
方法区:存储类信息、常量池、静态变量等,也称元空间(Metaspace)。
-
直接内存:非 JVM 内部内存,用于 NIO 的
ByteBuffer
等。
2. 对象创建的过程是什么?
-
类加载检查:检查对象对应的
.class
文件是否已加载。 -
分配内存:在堆中分配内存(可能涉及垃圾回收或线程同步)。
-
初始化零值:将对象字段初始化为默认值。
-
执行构造方法:调用
<init>
方法完成对象初始化。
3. 垃圾回收算法有哪些?解释标记-清除、复制、标记-整理。
-
标记-清除:
-
标记所有存活对象,然后清除未被标记的对象。
-
缺点:会产生内存碎片。
-
-
复制:
-
将存活对象复制到另一个区域,清理原区域。
-
缺点:需要两倍内存空间。
-
-
标记-整理:
-
标记存活对象,并将它们移动到一端,清理尾部空间。
-
优点:避免内存碎片。
-
4. 什么是 GC Roots?哪些对象可以作为 GC Roots?
-
GC Roots:垃圾回收的起点,从这些对象开始遍历引用链。
-
常见的 GC Roots:
-
虚拟机栈中的局部变量。
-
方法区中的类静态属性。
-
方法区中的常量。
-
本地方法栈中的 JNI 引用。
-
5. 强引用、软引用、弱引用、虚引用的区别?
引用类型 | 特性 |
---|---|
强引用 | 普通引用,对象不会被回收。 |
软引用 | 内存不足时会被回收,适用于缓存场景。 |
弱引用 | GC 时一定会被回收,适用于临时对象。 |
虚引用 | 不会阻止对象被回收,仅用于跟踪对象的回收状态。 |
6. 如何判断对象是否可被回收?
-
如果对象无法通过任何引用链到达 GC Roots,则认为该对象可被回收。
-
判断过程分为两步:
-
引用可达性分析:检查是否有引用指向该对象。
-
finalize() 方法:如果对象实现了
finalize()
方法且未被调用,会在回收前尝试复活。
-
7. 常见的垃圾收集器有哪些(如 CMS、G1)?
收集器名称 | 特点 |
---|---|
Serial | 单线程,适合单核 CPU 和小内存环境。 |
ParNew | Serial 的多线程版本。 |
Parallel Scavenge | 注重吞吐量,适合科学计算等场景。 |
CMS | 低延迟,适合交互式应用,但会产生内存碎片。 |
G1 | 分代收集,注重平衡吞吐量和延迟,适合大内存环境。 |
ZGC | 高并发、低延迟,适合超大内存环境。 |
Shenandoah | 类似 ZGC,提供更低的暂停时间。 |
8. 什么是类加载机制?类加载过程有哪些阶段?
-
类加载机制:将
.class
文件加载到 JVM 中并生成对应的Class
对象。 -
类加载过程:
-
加载:从文件系统或网络中读取字节码。
-
验证:确保字节码格式正确且符合 JVM 规范。
-
准备:为类的静态变量分配内存并设置默认值。
-
解析:将符号引用替换为直接引用。
-
初始化:执行静态代码块和静态变量初始化。
-
9. 双亲委派模型是什么?如何打破它?
-
双亲委派模型:
-
加载类时优先由父类加载器加载,只有父类加载器无法加载时才由子类加载器加载。
-
目的是保证类的唯一性和安全性。
-
-
打破方式:
-
使用自定义类加载器覆盖
loadClass()
方法,改变加载顺序。
-
10. OutOfMemoryError 的可能原因及解决方案?
错误类型 | 原因 | 解决方案 |
---|---|---|
Java Heap Space | 堆内存不足 | 增加 -Xmx 参数 |
PermGen/Metaspace | 方法区或元空间不足 | 增加 -XX:MaxMetaspaceSize 参数 |
Direct Memory | 直接内存不足 | 增加 -XX:MaxDirectMemorySize 参数 |
StackOverflowError | 栈深度过大 | 增加 -Xss 参数 |
11. JVM 调优的常用参数有哪些?
-
堆内存设置:
-
-Xms
:初始堆大小。 -
-Xmx
:最大堆大小。
-
-
新生代设置:
-
-Xmn
:新生代大小。
-
-
垃圾收集器选择:
-
-XX:+UseG1GC
:启用 G1 收集器。 -
-XX:+UseConcMarkSweepGC
:启用 CMS 收集器。
-
-
其他参数:
-
-XX:MaxMetaspaceSize
:元空间最大大小。 -
-XX:+PrintGCDetails
:打印垃圾回收详细信息。
-
八、JDBC与数据库
1. JDBC 操作数据库的基本步骤?
-
加载驱动:使用
Class.forName()
加载数据库驱动。 -
建立连接:通过
DriverManager.getConnection()
获取数据库连接。 -
创建语句对象:使用
Connection
创建Statement
或PreparedStatement
。 -
执行 SQL:调用
executeQuery()
或executeUpdate()
执行查询或更新操作。 -
处理结果集:遍历
ResultSet
获取查询结果。 -
关闭资源:关闭
ResultSet
、Statement
和Connection
。
示例代码:
try { Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password"); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users"); while (rs.next()) { System.out.println(rs.getString("name")); } rs.close(); stmt.close(); conn.close(); } catch (Exception e) { e.printStackTrace(); }
2. Statement 和 PreparedStatement 的区别?
特性 | Statement | PreparedStatement |
---|---|---|
SQL 注入防护 | 容易受到 SQL 注入攻击 | 自动防止 SQL 注入 |
性能 | 每次执行都需要编译 SQL | 预编译 SQL,性能更高 |
参数化支持 | 不支持参数化 | 支持参数化查询 |
适用场景 | 简单的静态 SQL | 动态 SQL 或频繁执行的 SQL |
3. 什么是数据库连接池?为什么使用它?
-
数据库连接池:预先创建一组数据库连接并管理它们的生命周期,避免频繁创建和销毁连接。
-
优点:
-
提高性能:减少连接创建和关闭的开销。
-
控制资源:限制最大连接数,防止资源耗尽。
-
简化管理:统一管理和复用连接。
-
4. 事务的 ACID 特性是什么?
-
Atomicity(原子性):事务中的所有操作要么全部成功,要么全部失败。
-
Consistency(一致性):事务执行前后,数据库状态保持一致。
-
Isolation(隔离性):多个事务并发执行时,彼此隔离,互不干扰。
-
Durability(持久性):事务提交后,其结果永久保存。
5. 什么是脏读、不可重复读、幻读?
问题 | 描述 |
---|---|
脏读 | 一个事务读取了另一个未提交事务的数据。 |
不可重复读 | 同一事务中多次读取同一数据,结果不一致。 |
幻读 | 同一事务中多次查询同一条件,结果集发生变化。 |
6. 如何通过 JDBC 实现事务管理?
-
步骤:
-
禁用自动提交:
conn.setAutoCommit(false);
-
执行一系列 SQL 操作。
-
如果所有操作成功,调用
conn.commit();
提交事务。 -
如果发生异常,调用
conn.rollback();
回滚事务。 -
最后恢复自动提交:
conn.setAutoCommit(true);
-
示例代码:
try { Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password"); conn.setAutoCommit(false); String sql1 = "UPDATE accounts SET balance = balance - 100 WHERE id = 1"; String sql2 = "UPDATE accounts SET balance = balance + 100 WHERE id = 2"; Statement stmt = conn.createStatement(); stmt.executeUpdate(sql1); stmt.executeUpdate(sql2); conn.commit(); System.out.println("Transaction committed successfully."); } catch (SQLException e) { e.printStackTrace(); try { conn.rollback(); System.out.println("Transaction rolled back."); } catch (SQLException ex) { ex.printStackTrace(); } } finally { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } }
九、Java 8+ 新特性
1. Lambda 表达式的语法及使用场景?
-
语法:
(参数列表) -> { 函数体 }
-
示例:
// 无参数,无返回值 () -> System.out.println("Hello"); // 单个参数,无返回值 (x) -> System.out.println(x); // 多个参数,有返回值 (x, y) -> { return x + y; };
-
使用场景:
-
简化函数式接口的实现。
-
结合 Stream API 进行集合操作。
-
实现事件监听器等回调机制。
-
2. 函数式接口(Functional Interface)是什么?
-
定义:只包含一个抽象方法的接口。
-
注解:可以用
@FunctionalInterface
标记。 -
示例:
@FunctionalInterface interface MyFunctionalInterface { void doSomething(); } MyFunctionalInterface func = () -> System.out.println("Doing something"); func.doSomething();
3. Stream API 的核心操作有哪些?
-
创建 Stream:
-
集合类的
stream()
方法。 -
数组通过
Arrays.stream()
。
-
-
中间操作(惰性求值):
-
filter
:过滤元素。 -
map
:转换元素。 -
sorted
:排序。 -
distinct
:去重。
-
-
终端操作(触发执行):
-
forEach
:遍历。 -
collect
:收集结果。 -
reduce
:聚合操作。 -
count
:统计数量。
-
示例代码:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.stream() .filter(name -> name.startsWith("A")) .map(String::toUpperCase) .forEach(System.out::println);
4. Optional 类的作用及使用方法?
-
作用:避免空指针异常,提供更安全的空值处理方式。
-
常用方法:
-
of(T value)
:创建非空的Optional
。 -
ofNullable(T value)
:创建可能为空的Optional
。 -
isPresent()
:判断是否有值。 -
orElse(T other)
:如果没有值,返回默认值。 -
ifPresent(Consumer<? super T> consumer)
:如果有值,执行指定操作。
-
示例代码:
Optional<String> optional = Optional.ofNullable("Hello"); optional.ifPresent(System.out::println); String result = optional.orElse("Default Value"); System.out.println(result);
5. 接口的默认方法和静态方法是什么?
-
默认方法:
-
使用
default
关键字定义,默认方法可以有实现。 -
解决接口扩展问题,避免破坏现有实现类。
-
-
静态方法:
-
使用
static
关键字定义,可以直接通过接口调用。
-
示例代码:
interface MyInterface { default void defaultMethod() { System.out.println("Default method"); } static void staticMethod() { System.out.println("Static method"); } } MyInterface.staticMethod(); // 调用静态方法 MyInterface obj = new MyInterface() {}; obj.defaultMethod(); // 调用默认方法
6. 方法引用(Method Reference)的四种形式?
-
对象::实例方法:
user::getName
-
类::静态方法:
String::valueOf
-
类::实例方法:
String::toLowerCase
-
构造器引用:
MyClass::new
示例代码:
List<String> names = Arrays.asList("Alice", "Bob"); names.stream().map(String::toUpperCase).forEach(System.out::println);
7. 什么是 CompletableFuture?如何实现异步编程?
-
CompletableFuture:用于异步编程,支持链式调用和组合操作。
-
核心方法:
-
supplyAsync
:异步执行任务并返回结果。 -
thenApply
:对结果进行转换。 -
thenAccept
:消费结果。 -
exceptionally
:处理异常。 -
join
:阻塞等待结果。
-
示例代码:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { return 42; // 模拟耗时任务 }); future.thenAccept(result -> { System.out.println("Result: " + result); }).exceptionally(ex -> { System.err.println("Error: " + ex.getMessage()); return null; }); // 阻塞等待结果 int result = future.join(); System.out.println("Final Result: " + result);
十、设计模式
1. 列举常见的设计模式(至少5种)。
-
创建型模式:
-
单例模式(Singleton):确保一个类只有一个实例。
-
工厂模式(Factory):提供创建对象的接口,延迟实例化。
-
抽象工厂模式(Abstract Factory):提供一组相关或依赖对象的创建接口。
-
-
结构型模式:
-
适配器模式(Adapter):将一个类的接口转换成客户端期望的另一个接口。
-
代理模式(Proxy):为其他对象提供一种代理以控制对这个对象的访问。
-
-
行为型模式:
-
观察者模式(Observer):定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知。
-
2. 手写单例模式的双重检查锁(Double-Check Locking)实现。
public class Singleton { private static volatile Singleton instance; private Singleton() { // 私有构造函数,防止外部实例化 } public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // 创建实例 } } } return instance; } }
-
volatile 关键字:保证
instance
的可见性,防止指令重排序。
3. 工厂模式的作用及实现方式?
-
作用:
-
将对象的创建过程封装起来,降低耦合度。
-
提供统一的接口,隐藏具体的实现细节。
-
-
实现方式:
-
简单工厂模式:
public class CarFactory { public static Car createCar(String type) { if ("BMW".equals(type)) { return new BMW(); } else if ("Mercedes".equals(type)) { return new Mercedes(); } return null; } }
-
工厂方法模式:
public interface CarFactory { Car createCar(); } public class BMWFactory implements CarFactory { @Override public Car createCar() { return new BMW(); } }
-
4. 观察者模式如何实现?
-
定义:定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知。
-
实现方式:
-
示例代码:
import java.util.ArrayList; import java.util.List; // 观察者接口 interface Observer { void update(String message); } // 被观察者 class Subject { private List<Observer> observers = new ArrayList<>(); public void addObserver(Observer observer) { observers.add(observer); } public void removeObserver(Observer observer) { observers.remove(observer); } public void notifyObservers(String message) { for (Observer observer : observers) { observer.update(message); } } } // 具体观察者 class ConcreteObserver implements Observer { @Override public void update(String message) { System.out.println("Received: " + message); } } // 使用示例 public class Main { public static void main(String[] args) { Subject subject = new Subject(); Observer observer = new ConcreteObserver(); subject.addObserver(observer); subject.notifyObservers("Hello Observers!"); } }
-
5. 适配器模式和代理模式的区别?
特性 | 适配器模式 | 代理模式 |
---|---|---|
目的 | 将一个类的接口转换成客户端期望的接口 | 为其他对象提供代理,控制访问 |
适用场景 | 不兼容的接口之间进行适配 | 增强功能或延迟加载 |
实现方式 | 包装目标对象,暴露适配后的接口 | 实现与目标对象相同的接口 |
示例对比:
-
适配器模式:
class Adapter implements Target { private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee = adaptee; } @Override public void request() { adaptee.specificRequest(); } }
-
代理模式:
class Proxy implements Subject { private RealSubject realSubject; @Override public void request() { if (realSubject == null) { realSubject = new RealSubject(); } realSubject.request(); } }
十一、综合问题
1. String s = new String("abc");
创建了几个对象?
-
答案:创建了 两个对象。
-
解释:
-
字面量
"abc"
会先在字符串常量池中查找,如果不存在则创建一个对象。 -
new String("abc")
明确通过new
关键字创建了一个新的String
对象。
-
2. Integer a = 127; Integer b = 127;
,a == b
的结果?为什么?
-
结果:
true
。 -
原因:
-
Java 中
-128 ~ 127
范围内的Integer
对象会被缓存(IntegerCache
)。 -
因此,
a
和b
引用的是同一个缓存对象,==
比较的是引用地址,结果为true
。
-
-
注意:超出范围时(如
Integer a = 128; Integer b = 128;
),a == b
结果为false
,因为会创建不同的对象。
3. 如何实现 LRU 缓存?
-
思路:
-
使用
LinkedHashMap
实现,重写removeEldestEntry()
方法。 -
当缓存达到容量限制时,移除最久未使用的元素。
-
-
示例代码:
import java.util.LinkedHashMap; import java.util.Map; public class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int capacity; public LRUCache(int capacity) { super(capacity, 0.75f, true); // accessOrder=true 表示按访问顺序排序 this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; // 超过容量时移除最老的元素 } }
4. String
为什么是不可变的?
-
原因:
-
String
的值存储在final char[] value
中,无法修改。 -
不可变性保证了线程安全。
-
提高性能:字符串常量池可以复用相同的实例。
-
安全性:防止敏感信息(如密码)被篡改。
-
5. 什么是反射?如何通过反射创建对象?
-
反射:在运行时动态获取类的信息并操作类的行为。
-
创建对象:
Class<?> clazz = Class.forName("com.example.MyClass"); Object obj = clazz.getDeclaredConstructor().newInstance();
6. 泛型擦除是什么?如何绕过泛型擦除?
-
泛型擦除:编译时,泛型类型信息会被擦除,替换为原始类型(如
List<String>
变为List
)。 -
绕过方法:
-
使用
Class
对象保存类型信息。 -
示例:
public class GenericType<T> { private Class<T> type; public GenericType(Class<T> type) { this.type = type; } public boolean isTypeOf(Object obj) { return type.isInstance(obj); } } GenericType<String> stringType = new GenericType<>(String.class); System.out.println(stringType.isTypeOf("Hello")); // true
-
7. 如何实现深拷贝(Deep Copy)?
-
方式:
-
手动复制:递归复制对象及其所有子对象。
-
序列化:将对象序列化为字节流后再反序列化。
-
-
示例代码:
import java.io.*; public class DeepCopy implements Serializable { private static <T extends Serializable> T deepCopy(T obj) throws IOException, ClassNotFoundException { try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos)) { oos.writeObject(obj); try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis)) { return (T) ois.readObject(); } } } }
8. 如何用 Java 实现一个简单的阻塞队列?
-
思路:
-
使用
synchronized
或Lock
控制并发。 -
使用
wait()
和notify()
实现生产者和消费者之间的通信。
-
-
示例代码:
import java.util.LinkedList; import java.util.Queue; public class BlockingQueue<T> { private final Queue<T> queue = new LinkedList<>(); private final int capacity; public BlockingQueue(int capacity) { this.capacity = capacity; } public synchronized void put(T item) throws InterruptedException { while (queue.size() >= capacity) { wait(); // 队列满时等待 } queue.add(item); notifyAll(); // 唤醒消费者 } public synchronized T take() throws InterruptedException { while (queue.isEmpty()) { wait(); // 队列空时等待 } T item = queue.poll(); notifyAll(); // 唤醒生产者 return item; } }