Java基础八股文

接口和抽象类有什么区别?

接口 是用来定义行为规范的,适用于多个类需要实现相同的功能

抽象类 本质还是类,用来定义类的模板,适用于需要提供通用功能的同时有需要部分功能由子类实现

具体区别如下:

接口用interface定义,方法默认public abstract,字段是public static final,没有构造器,不能实例化。

抽象类用abstract定义,可以包含抽象方法(没有方法体)和普通方法,可以有构造器,但不能直接实例化

一个类可以实现多个接口,但只能继承一个抽象类,(多继承会造成菱形继承问题)


JDK 动态代理和 CGLIB 动态代理有什么区别?

特性 JDK 动态代理 CGLIB 动态代理
实现方式 基于 Java 的反射机制,使用 java.lang.reflect.Proxy 类。 基于字节码生成技术,依赖 net.sf.cglib 包。
代理目标 只能代理接口(必须有接口)。 可以代理类(不需要接口)。
原理 动态生成代理类,代理类实现目标对象的接口并重写方法。 动态生成目标类的子类,并覆盖其非 final 方法。
字节码生成工具 使用 JDK 自带的 Proxy 类。 使用 ASM 框架操作字节码。

你使用过 Java 的反射机制吗?如何应用反射?

Java 反射 是 Java 提供的一种机制,允许程序在运行时动态地检查和操作类、方法、字段和构造函数。通过反射,可以在运行时获取类的详细信息(比如类名、方法、字段等),并且可以动态调用方法或访问字段。

反射的作用:

在运行时动态获取类的完整信息(包括名称、构造器、字段和方法等)。

在运行时创建对象或调用方法,而无需明确指定类。

开发框架和工具(例如 Spring、MyBatis 等,都利用反射实现动态功能)。

操作不可直接访问的私有成员或方法(通过修改访问权限)。

反射的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//获取class对象
Class<?> clazz = Class.forName("com.test.hello");
//or
Class<?> clazz = MyClass.class;
//or
Class<?> clazz = obj.getClass();

//创建对象
Constructor<?> constructor = clazz.getConstructor();
Object obj = constructor.newInstance();

//访问字段
Field field = clazz.getField("name");
field.setAccessible(true); // 允许访问 private 字段
Object value = field.get(obj);
field.set(obj, newValue);

//调用方法
Method method = clazz.getMethod("method1", String.class);
Object result = method.invoke(obj, "param");


说说 Java 中 HashMap 的原理?

HashMap 是一种基于哈希表的集合,允许以键值对形式存储数据,并支持快速查找和操作。

在jdk1.7之前,HashMap采用数组+链表 结构存储数据,但如果哈希冲突过多,会导致链表变长,降低查询效率。

在jdk1.8之后,HashMap采用数组+链表+红黑树 结构存储数据,当链表长度大于8(默认),链表就会转化为红黑树,提高查询效率。

HashMap在存储数据时,使用hashCode()方法计算哈希值,并通过indexFor方法(jdk1.7及之后版本移出来这个方法,采用(n-1)&hash)确定元素在数组中的存储位置。如果位置一样,就采用链表法解决哈希冲突。

HashMap默认存储容量为16,负载因子0.75,2倍扩容。当元素数量超过16*0.75=12个时,进行扩容,但扩容比较耗时,会影响性能。

HashMap常用方法:put(k key,v value),get(Object key),remove(Object key)


Java 中有哪些集合类?请简单介绍

java中的集合类主要有两大类:Collection和Map

Collection(存储对象)中包含Set、List、Queue

Set: HashSet(哈希表,无序,不重复)、LinkedHashSet(链表和哈希表,维护插入顺序,不重复)、TreeSet(红黑树,元素有序,不重复)

List: ArrayList(动态数组,查询快,插入删除慢)、LinkedList(双向链表,插入删除快,查询慢),Vector(线程安全的动态数组,但开销大)

Queue: PriorityQueue(优先级堆)、LinkedList(可作为队列,支持FIFO操作)

Map(键值对)中包含HashMap(哈希表,无序,键不重复)、LinkedHashMap(链表和哈希表,维护插入顺序,键不重复)、TreeMap(红黑树,有序,键不重复)、Hashtable(线程安全,键或值不为null)、ConcurrentHashMap(线程安全,高并发,键或值不为null)


Java 中 HashMap 的扩容机制是怎样的?

HashMap默认存储容量为16,负载因子0.75,2倍扩容。

  1. HashMap底层维护了Node类型的数组table,默认为null
  2. 当创建对象时,将加载因子(loadfactor)初始化为0.75
  3. 当添加k-v时,通过key的哈希值得到下table 的索引,然后判断该索引处是否有元素,如果没有元素直接添加,如果该索引处有元素,则继续判断该元素的key是否和准备加入的key值一样,如果相等,则直接替换val,如果不相等需要判断是树结构还是链表结构,做出 相应的处理,如果添加时发现容量不够,则需要扩容
  4. 第一次添加时,扩容table容量为16,临界值(threshold)为12(16*0.75)
  5. 以后再次扩容,则需要扩容table容量为原来的两倍,临界值为原来的两倍,即24,以此类推
  6. 在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACIYU(默认64),就会树化(红黑树)

你了解 Java 线程池的原理吗?

Java线程池是一种池化技术,用于预先创建管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。

线程池的核心参数

  1. 核心线程数:线程池中保持活动状态的最小线程数。即使线程池空闲,这些线程也不会被销毁,除非设置了 allowCoreThreadTimeOut
  2. 最大线程数:线程池中允许的最大线程数,包括核心线程和非核心线程。
  3. 空闲存活时间:非核心线程的最大空闲时间,超过这个时间非核心线程会被销毁。如果设置了 allowCoreThreadTimeOut,核心线程也会遵守这个时间。
  4. 时间单位:用于指定 keepAliveTime 的时间单位。
  5. 工作队列: 存放等待执行的任务的阻塞队列。
  6. 线程工厂:用于创建新线程。
  7. 拒绝策略:当任务无法被执行时的处理策略,如直接丢弃任务、抛出异常等。

工作原理

  1. 默认情况下,线程池不会提前创建线程,任务提交后才会创建线程(除非调用 prestartAllCoreThreads 来提前创建核心线程)。
  2. 如果线程池中线程数小于核心线程数,线程池会优先创建核心线程来处理任务。
  3. 如果核心线程数已满,新任务会进入工作队列等待。
  4. 如果工作队列已满且线程池中的线程数小于最大线程数,线程池会创建非核心线程处理任务。
  5. 如果线程池已达到最大线程数且工作队列已满,新的任务会根据指定的拒绝策略进行处理。
  6. 当线程空闲时间超过 keepAliveTime 后,非核心线程会被回收。如果设置了 allowCoreThreadTimeOut,核心线程也会被回收。

Java创建线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

public class CustomThreadPoolExample {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(10), // 工作队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

// 提交任务
for (int i = 0; i < 15; i++) {
final int taskId = i;
threadPool.execute(() -> {
System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

// 关闭线程池
threadPool.shutdown();
}
}


你使用过哪些 Java 并发工具类?

java并发工具类:ConcurrentHashMap、Atomiclnteger、Semaphore、CyclicBarrier、CountDownLatch、BlockingQueue

ConcurrentHashMap:线程安全,适用于高并发环境

Atomiclnteger :提供了一个支持原子操作的 int 类型变量,基于CAS实现,适合高并发

Semaphore :用于限制同时访问资源的线程数量

CyclicBarrier: 让一组线程在到达某个屏障点后继续执行,适合多线程分阶段执行任务

CountDownLatch :一个线程等待其他线程完成任务后再继续执行。倒计数器,线程通过 countDown() 减少计数,计数为 0 时释放等待线程。

BlockingQueue: 线程安全的队列,支持阻塞操作,适合生产者-消费者模式。


什么是 Java 的 CAS(Compare-And-Swap)操作?

Java中的CAS是一种实现多线程并发操作的无锁机制,由硬件原子指令实现。

它是通过比较内存中的某个变量是否为预期值,如果是,则更新为新值,否则不修改。

先比较(Compare),如果相等,则交换(Swap)更新为新值,不相等就失败重试

CAS操作会引起ABA问题,什么是ABA问题?

就是一个变量值从A变为B,再从B变为A,CAS会认为没有变化,导致错误

解决办法:引入版本号或时间戳,每次更新的同时更新版本号


说说 AQS 吧?

AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 Java 并发工具包中的一个核心框架,用于构建锁、信号量以及其他同步器工具的基础。

AQS就是起到一个抽象、封装的作用,将一些排队、入队、加锁、中断等方法提供出来,便于其他JUC锁的使用。

AQS 通过一个先进先出(FIFO)等待队列一个 state 状态变量来管理线程的同步状态和排队机制,提供了一个通用的框架来实现同步器。

AQS 的两种工作模式

独占模式:只有一个线程可以访问共享资源。如 ReentrantLock 的实现。

核心方法:tryAcquire(int arg):尝试获取资源;tryRelease(int arg):尝试释放资源。

共享模式:多个线程可以同时访问共享资源。如 SemaphoreCountDownLatch 的实现。

核心方法:tryAcquireShared(int arg):尝试以共享模式获取资源;tryReleaseShared(int arg):尝试以共享模式释放资源。

AQS 的典型实现

ReentrantLock:基于 AQS 的独占模式实现。公平锁和非公平锁的实现不同:

  • 公平锁:线程按照进入队列的顺序获取锁;
  • 非公平锁:直接抢占锁。

Semaphore:基于 AQS 的共享模式实现,用于限制同时访问资源的线程数量。

CountDownLatch:使用 AQS 的共享模式实现倒计时功能。

CyclicBarrier:基于 AQS 实现线程的同步点。


Synchronized 和 ReentrantLock 有什么区别?

Synchronized 是Java内置的关键字,是一种隐式锁,可以实现基本的同步机制,不支持超时,非公平,不可中断,不支持多条件。

ReentrantLock 是JUC类库提供的,由JDK1.5引入,支持设置超时时间,可以避免死锁,比较灵活,支持公平锁,可以中断,支持多条件判断。

ReentrantLock 需要手动解锁,而Synchronized 不需要,都是可重入锁

一般情况用Synchronized 即可,比较简单,而ReentrantLock比较灵活,支持的功能比较多,复杂情况下用ReentrantLock

可重入锁 是指一种允许同一个线程在持有锁的情况下再次获取该锁的锁机制。它解决了线程因重复获取自身已持有的锁而导致的死锁问题。


Java 中 volatile 关键字的作用是什么?

Java中Volatile 的作用是保证变量的可见性禁止指令重排优化

可见性:是指当一个线程修改了变量的值,新值会立马刷新到主内存中,其他线程在读取该变量时立马就可以获取到最新的值,避免了线程由于缓存一致性的问题导致的读取旧值的现象。

禁止指令重排优化:由于编译器和CPU为了优化性能,会对指令顺序进行重新排序,导致实际执行顺序和代码顺序不同,使用了Volatile就可以禁止指令重排,保证代码执行的顺序性。