集合框架
Java集合框架是面试中的重中之重,尤其是对List
、Set
、Map
的实现类及其底层原理的考察。
1. List
-
ArrayList:
- 底层是动态数组,支持随机访问(通过索引),时间复杂度为O(1)。
- 插入和删除元素时,如果是在中间位置,需要移动后续元素,时间复杂度为O(n)。
- 扩容机制:默认初始容量为10,扩容时增加为原来的1.5倍。
- 线程不安全。
面试问题:
- ArrayList的扩容机制是什么?
- ArrayList和LinkedList的区别?
案例:
java">List<Integer> list = new ArrayList<>(); list.add(1); // 添加元素 list.get(0); // 获取元素 list.remove(0); // 删除元素
-
LinkedList:
- 底层是双向链表,适合频繁的插入和删除操作,时间复杂度为O(1)。
- 随机访问效率低,时间复杂度为O(n)。
- 线程不安全。
面试问题:
- LinkedList的实现原理是什么?
- 如何实现一个LRU缓存?(可以用LinkedList实现)
案例:
java">List<Integer> linkedList = new LinkedList<>(); linkedList.add(1); linkedList.add(2); linkedList.remove(1); // 删除元素
2. Set
-
HashSet:
- 基于
HashMap
实现,元素无序且不允许重复。 - 添加元素时,先计算哈希值,再通过
equals
方法判断是否重复。 - 线程不安全。
面试问题:
- HashSet如何保证元素唯一性?
- HashSet和TreeSet的区别?
案例:
java">Set<String> set = new HashSet<>(); set.add("A"); set.add("B"); set.add("A"); // 不会重复添加
- 基于
-
TreeSet:
- 基于
TreeMap
实现,元素按自然顺序或自定义顺序排序。 - 内部使用红黑树实现,插入、删除、查找的时间复杂度为O(log n)。
- 线程不安全。
案例:
java">Set<Integer> treeSet = new TreeSet<>(); treeSet.add(3); treeSet.add(1); treeSet.add(2); System.out.println(treeSet); // 输出 [1, 2, 3]
- 基于
3. Map
-
HashMap:
- 基于哈希表实现,允许
null
键和null
值。 - 扩容机制:默认初始容量为16,负载因子为0.75,扩容时容量变为原来的2倍。
- 线程不安全。
面试问题:
- HashMap的底层实现原理?
- HashMap如何解决哈希冲突?(链地址法)
- JDK 1.8中HashMap的优化?(引入红黑树)
案例:
java">Map<String, Integer> map = new HashMap<>(); map.put("A", 1); map.put("B", 2); map.get("A"); // 获取值
- 基于哈希表实现,允许
-
ConcurrentHashMap:
- 线程安全的HashMap,JDK 1.8之前使用分段锁,JDK 1.8之后使用CAS + synchronized。
- 面试问题:
- ConcurrentHashMap如何保证线程安全?
- ConcurrentHashMap的锁粒度是什么?
案例:
java">Map<String, Integer> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.put("A", 1); concurrentMap.put("B", 2);
File API
File API用于操作文件和目录,常见操作包括创建、删除、重命名、遍历等。
常见面试问题:
-
如何递归遍历目录下的所有文件?
- 使用递归或
Files.walk()
方法。
案例:
java">public void listFiles(File dir) {if (dir.isDirectory()) {File[] files = dir.listFiles();for (File file : files) {if (file.isDirectory()) {listFiles(file); // 递归} else {System.out.println(file.getName());}}} }
- 使用递归或
-
如何读取大文件?
- 使用
BufferedReader
逐行读取。
案例:
java">try (BufferedReader br = new BufferedReader(new FileReader("largeFile.txt"))) {String line;while ((line = br.readLine()) != null) {System.out.println(line);} }
- 使用
IO
Java IO分为字节流和字符流,常见类包括InputStream
、OutputStream
、Reader
、Writer
。
常见面试问题:
-
字节流 vs 字符流的区别?
- 字节流以字节为单位操作数据,适合处理二进制文件(如图片、视频)。
- 字符流以字符为单位操作数据,适合处理文本文件。
案例:
java">// 字节流 try (FileInputStream fis = new FileInputStream("input.txt");FileOutputStream fos = new FileOutputStream("output.txt")) {int data;while ((data = fis.read()) != -1) {fos.write(data);} }// 字符流 try (FileReader fr = new FileReader("input.txt");FileWriter fw = new FileWriter("output.txt")) {int data;while ((data = fr.read()) != -1) {fw.write(data);} }
-
NIO是什么?
- NIO(Non-blocking IO)是Java提供的高性能IO API,基于通道(Channel)和缓冲区(Buffer)操作。
- 核心组件:
Channel
、Buffer
、Selector
。
案例:
java">try (FileChannel channel = FileChannel.open(Paths.get("input.txt"), StandardOpenOption.READ)) {ByteBuffer buffer = ByteBuffer.allocate(1024);while (channel.read(buffer) > 0) {buffer.flip();while (buffer.hasRemaining()) {System.out.print((char) buffer.get());}buffer.clear();} }
线程
线程是Java并发编程的核心,面试中常考线程的创建、生命周期、同步机制等。
常见面试问题:
-
如何实现线程同步?
- 使用
synchronized
关键字或ReentrantLock
。
案例:
java">class Counter {private int count = 0;public synchronized void increment() {count++;}public int getCount() {return count;} }
- 使用
-
wait()、notify()、notifyAll()的作用?
- 用于线程间的通信,
wait()
使线程等待,notify()
唤醒一个等待线程,notifyAll()
唤醒所有等待线程。
案例:
java">class SharedResource {private boolean flag = false;public synchronized void produce() {while (flag) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}flag = true;notify();}public synchronized void consume() {while (!flag) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}flag = false;notify();} }
- 用于线程间的通信,
线程池
线程池是面试中的高频考点,尤其是对ThreadPoolExecutor
的参数和原理的考察。
常见面试问题:
-
线程池的核心参数有哪些?
corePoolSize
:核心线程数。maximumPoolSize
:最大线程数。keepAliveTime
:空闲线程存活时间。workQueue
:任务队列。threadFactory
:线程工厂。handler
:拒绝策略。
-
线程池的拒绝策略有哪些?
AbortPolicy
:直接抛出异常。CallerRunsPolicy
:由调用线程处理任务。DiscardOldestPolicy
:丢弃队列中最老的任务。DiscardPolicy
:直接丢弃任务。
案例:
java">ThreadPoolExecutor executor = new ThreadPoolExecutor(2, // corePoolSize5, // maximumPoolSize60, // keepAliveTimeTimeUnit.SECONDS,new LinkedBlockingQueue<>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy() );
问题答案
ArrayList的扩容机制是什么?
-
答案:
ArrayList
的底层是一个动态数组,初始容量为10。- 当元素数量超过当前容量时,
ArrayList
会自动扩容。 - 扩容时,新容量通常是旧容量的1.5倍(即
oldCapacity + (oldCapacity >> 1)
)。 - 扩容过程会创建一个新的数组,并将旧数组中的元素复制到新数组中。
示例:
java">ArrayList<Integer> list = new ArrayList<>(); for (int i = 0; i < 20; i++) {list.add(i); // 当元素数量超过10时,ArrayList会自动扩容 }
ArrayList和LinkedList的区别?
-
答案:
- 底层实现:
ArrayList
基于动态数组,支持随机访问,时间复杂度为O(1)。LinkedList
基于双向链表,插入和删除操作效率高,时间复杂度为O(1),但随机访问效率低,时间复杂度为O(n)。
- 内存占用:
ArrayList
占用连续内存空间。LinkedList
占用非连续内存空间,每个节点需要额外存储前后节点的引用。
- 适用场景:
ArrayList
适合频繁查询的场景。LinkedList
适合频繁插入和删除的场景。
示例:
java">List<Integer> arrayList = new ArrayList<>(); arrayList.add(1); // 适合查询 arrayList.get(0); // O(1)List<Integer> linkedList = new LinkedList<>(); linkedList.add(1); // 适合插入和删除 linkedList.remove(0); // O(1)
- 底层实现:
HashSet如何保证元素唯一性?
-
答案:
HashSet
基于HashMap
实现,元素存储在HashMap
的键中。- 添加元素时,先计算元素的哈希值(通过
hashCode()
方法),然后通过equals()
方法判断是否重复。 - 如果哈希值相同且
equals()
返回true
,则认为元素重复,不会添加。
示例:
java">Set<String> set = new HashSet<>(); set.add("A"); set.add("B"); set.add("A"); // 不会重复添加
HashMap的底层实现原理?
-
答案:
HashMap
基于哈希表实现,底层是数组 + 链表(JDK 1.8之前)或数组 + 链表 + 红黑树(JDK 1.8之后)。- 添加元素时,先计算键的哈希值,然后通过哈希值找到数组中的位置(桶)。
- 如果发生哈希冲突(即多个键映射到同一个桶),则使用链表或红黑树存储冲突的元素。
- 当链表长度超过8时,链表会转换为红黑树,以提高查找效率。
示例:
java">Map<String, Integer> map = new HashMap<>(); map.put("A", 1); map.put("B", 2); map.get("A"); // 获取值
LinkedList的实现原理是什么?
-
答案:
LinkedList
是基于双向链表实现的。- 每个节点(
Node
)包含三个部分:- 数据域(存储元素值)。
- 前驱指针(指向前一个节点)。
- 后继指针(指向后一个节点)。
- 链表的头节点(
first
)和尾节点(last
)分别指向链表的第一个和最后一个节点。 - 插入和删除操作只需要修改节点的指针,时间复杂度为O(1)。
- 随机访问需要从头节点或尾节点遍历,时间复杂度为O(n)。
示例:
java">LinkedList<Integer> list = new LinkedList<>(); list.add(1); // 添加到链表尾部 list.add(2); list.remove(1); // 删除元素
如何实现一个LRU缓存?(可以用LinkedList实现)
-
答案:
- LRU(Least Recently Used)缓存是一种淘汰最近最少使用数据的缓存策略。
- 可以使用
LinkedList
+HashMap
实现:LinkedList
用于维护数据的访问顺序,最近访问的数据放在链表头部。HashMap
用于快速查找数据。
实现步骤:
- 当访问一个数据时,如果数据在缓存中,则将其移动到链表头部。
- 如果数据不在缓存中,则将其添加到链表头部。
- 如果缓存已满,则移除链表尾部的数据。
示例:
java">class LRUCache<K, V> {private final int capacity;private final Map<K, V> map;private final LinkedList<K> list;public LRUCache(int capacity) {this.capacity = capacity;this.map = new HashMap<>();this.list = new LinkedList<>();}public V get(K key) {if (map.containsKey(key)) {list.remove(key); // 移除旧位置list.addFirst(key); // 放到链表头部return map.get(key);}return null;}public void put(K key, V value) {if (map.containsKey(key)) {list.remove(key); // 移除旧位置} else if (map.size() >= capacity) {K lastKey = list.removeLast(); // 移除链表尾部数据map.remove(lastKey);}list.addFirst(key); // 放到链表头部map.put(key, value);} }
HashSet和TreeSet的区别?
-
答案:
- 底层实现:
HashSet
基于HashMap
实现,元素无序。TreeSet
基于TreeMap
实现,元素按自然顺序或自定义顺序排序。
- 性能:
HashSet
的插入、删除、查找操作的时间复杂度为O(1)。TreeSet
的插入、删除、查找操作的时间复杂度为O(log n)。
- 适用场景:
HashSet
适合需要快速查找且不关心顺序的场景。TreeSet
适合需要有序数据的场景。
示例:
java">Set<Integer> hashSet = new HashSet<>(); hashSet.add(3); hashSet.add(1); hashSet.add(2); System.out.println(hashSet); // 输出顺序不确定Set<Integer> treeSet = new TreeSet<>(); treeSet.add(3); treeSet.add(1); treeSet.add(2); System.out.println(treeSet); // 输出 [1, 2, 3]
- 底层实现:
HashMap如何解决哈希冲突?(链地址法)
-
答案:
HashMap
使用链地址法解决哈希冲突。- 当多个键映射到同一个桶(数组位置)时,
HashMap
会将这些键值对存储在同一个链表中。 - 在JDK 1.8中,当链表长度超过8时,链表会转换为红黑树,以提高查找效率。
示例:
java">Map<String, Integer> map = new HashMap<>(); map.put("A", 1); map.put("B", 2); map.put("C", 3); // 假设 "A" 和 "B" 的哈希值相同,它们会被存储在同一个链表中
JDK 1.8中HashMap的优化?(引入红黑树)
-
答案:
- 在JDK 1.8中,
HashMap
引入了红黑树来优化性能。 - 当链表长度超过8时,链表会转换为红黑树,查找时间复杂度从O(n)降低到O(log n)。
- 当红黑树的节点数少于6时,红黑树会退化为链表。
优化点:
- 提高了哈希冲突严重时的查找效率。
- 减少了链表过长时的性能问题。
示例:
java">Map<String, Integer> map = new HashMap<>(); for (int i = 0; i < 100; i++) {map.put("Key" + i, i); } // 当链表长度超过8时,会自动转换为红黑树
- 在JDK 1.8中,
ConcurrentHashMap的锁粒度是什么?
-
答案:
- 在JDK 1.7中,
ConcurrentHashMap
使用分段锁(Segment),每个段独立加锁,锁粒度是段级别。 - 在JDK 1.8中,
ConcurrentHashMap
使用CAS +synchronized
,锁粒度是桶级别(数组中的每个位置)。- 对每个桶(数组中的每个位置)使用
synchronized
锁。 - 使用CAS操作保证原子性。
- 对每个桶(数组中的每个位置)使用
示例:
java">Map<String, Integer> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.put("A", 1); concurrentMap.put("B", 2);
- 在JDK 1.7中,
ConcurrentHashMap如何保证线程安全?
-
答案:
- 在JDK 1.7中,
ConcurrentHashMap
使用分段锁(Segment),每个段独立加锁。 - 在JDK 1.8中,
ConcurrentHashMap
使用CAS(Compare-And-Swap) +synchronized
实现线程安全。- 对每个桶(数组中的每个位置)使用
synchronized
锁。 - 使用CAS操作保证原子性。
- 对每个桶(数组中的每个位置)使用
示例:
java">Map<String, Integer> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.put("A", 1); concurrentMap.put("B", 2);
- 在JDK 1.7中,
如何递归遍历目录下的所有文件?
-
答案:
- 使用递归方法遍历目录及其子目录。
- 对于每个文件,如果是目录,则递归调用;如果是文件,则输出文件名。
示例:
java">public void listFiles(File dir) {if (dir.isDirectory()) {File[] files = dir.listFiles();for (File file : files) {if (file.isDirectory()) {listFiles(file); // 递归} else {System.out.println(file.getName());}}} }
字节流 vs 字符流的区别?
-
答案:
- 字节流:以字节为单位操作数据,适合处理二进制文件(如图片、视频)。
- 核心类:
InputStream
、OutputStream
。
- 核心类:
- 字符流:以字符为单位操作数据,适合处理文本文件。
- 核心类:
Reader
、Writer
。
- 核心类:
示例:
java">// 字节流 try (FileInputStream fis = new FileInputStream("input.txt");FileOutputStream fos = new FileOutputStream("output.txt")) {int data;while ((data = fis.read()) != -1) {fos.write(data);} }// 字符流 try (FileReader fr = new FileReader("input.txt");FileWriter fw = new FileWriter("output.txt")) {int data;while ((data = fr.read()) != -1) {fw.write(data);} }
- 字节流:以字节为单位操作数据,适合处理二进制文件(如图片、视频)。
如何实现线程同步?
-
答案:
- 使用
synchronized
关键字或ReentrantLock
实现线程同步。 synchronized
可以修饰方法或代码块,确保同一时刻只有一个线程访问共享资源。
示例:
java">class Counter {private int count = 0;public synchronized void increment() {count++;}public int getCount() {return count;} }
- 使用
线程池的核心参数有哪些?
-
答案:
corePoolSize
:核心线程数。maximumPoolSize
:最大线程数。keepAliveTime
:空闲线程存活时间。workQueue
:任务队列。threadFactory
:线程工厂。handler
:拒绝策略。
示例:
java">ThreadPoolExecutor executor = new ThreadPoolExecutor(2, // corePoolSize5, // maximumPoolSize60, // keepAliveTimeTimeUnit.SECONDS,new LinkedBlockingQueue<>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy() );
线程池的拒绝策略有哪些?
-
答案:
AbortPolicy
:直接抛出异常。CallerRunsPolicy
:由调用线程处理任务。DiscardOldestPolicy
:丢弃队列中最老的任务。DiscardPolicy
:直接丢弃任务。
示例:
java">ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS,new LinkedBlockingQueue<>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy() );
哈希表核心原理