下面的面试题都是想起什么、遇到什么就记录下来,没有什么顺序,会比较跳跃,会持续更新(学又学不完,记又记不住),问题解答均由AI生成,仅供参数与记录
Java中的异常分为多种类型,以下是一些常见的异常:
运行时异常(RuntimeException
)
ArithmeticException
:算术条件异常,如整数除零等。ArrayIndexOutOfBoundsException
:数组索引越界异常,当对数组的索引值为负数或大于等于数组大小时抛出。ClassCastException
:类转换异常,试图将对象强制转换为不是其子类型的类时抛出。IllegalArgumentException
:非法参数异常,向方法传递了一个不合法或不正确的参数时抛出。NullPointerException
:空指针异常,在要求使用对象的地方使用了null时抛出。NumberFormatException
:数字格式异常,字符串转换为数字抛出的异常。StringIndexOutOfBoundsException
:字符串索引超出范围抛出的异常。检查型异常(Checked Exception
)
ClassNotFoundException
:找不到类异常,试图根据字符串形式的类名构造类,而在遍历calsspath
之后找不到对应名称的class文件时抛出。CloneNotSupportedException
:不支持克隆异常,当调用Object类中的clone
方法克隆对象,但该对象的类无法实现Cloneable
接口时抛出。FileNotFoundException
:文件未找到异常,试图访问一个不存在的文件时抛出。IOException
:操作输入流和输出流时可能出现的异常。NoSuchFieldException
:字段未找到异常。NoSuchMethodException
:方法未找到抛出的异常。SQLException
:操作数据库异常类。错误(Error
)
VirtualMachineError
:虚拟机运行错误。NoClassDefFoundError
:类定义错误。OutOfMemoryError
:内存不足错误。StackOverflowError
:栈溢出错误。在Java中,开启一个新的事务主要有以下几种方法:
使用Spring框架的声明式事务管理
applicationContext.xml
)中定义事务管理器bean。例如,使用DataSourceTransactionManager
来管理基于JDBC的数据源事务。@Transactional
注解:这是最常用的方式。将@Transactional
注解添加到需要事务管理的方法上,Spring会自动为该方法开启和管理事务。当方法执行成功时,事务会自动提交;如果方法执行过程中出现异常,事务则会自动回滚。
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
@Transactional
public void performTransactionalOperation() {
//业务逻辑
myRepository.save(new Entity());
//更多数据库操作
}
}
@Transactional
注解的propagation
属性可以指定事务的传播行为,常见的传播行为包括:
使用Java EE的JTA(Java Transaction API)
UserTransaction
对象:在Java EE应用程序中,通常通过@Resource
注解注入UserTransaction
对象来管理事务。userTransaction.begin()
开启一个新事务,执行完业务逻辑后调用userTransaction.commit()
提交事务。如果发生异常,则调用userTransaction.rollback()
回滚事务。
import javax.annotation.Resource;
import javax.transaction.UserTransaction;
public class MyService {
@Resource
private UserTransaction userTransaction;
public void performTransactionalOperation() {
try {
userTransaction.begin();
//业务逻辑
userTransaction.commit();
} catch (Exception e) {
try {
userTransaction.rollback();
} catch (Exception ex) {
ex.printStackTrace();
}
throw new RuntimeException(e);
}
}
}
使用JDBC手动管理事务
DriverManager.getConnection
或其他方式获取数据库连接。connection.setAutoCommit(false)
禁用连接的自动提交模式。connection.commit()
提交事务;如果出现异常则调用connection.rollback()
回滚事务。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class MyService {
public void performTransactionalOperation() {
Connection connection = null;
try {
connection = DriverManager.getConnection("jdbc:yourdatabaseurl", "username", "password");
connection.setAutoCommit(false); //禁用自动提交
//业务逻辑
//如:connection.prepareStatement("SQL语句").executeUpdate();
connection.commit(); //提交事务
} catch (SQLException e) {
if (connection != null) {
try {
connection.rollback(); //回滚事务
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close(); //关闭连接
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
在Java中,操作文件的流主要分为字节流和字符流两种,它们各自包含输入流和输出流。以下是对这些流的详细介绍:
字节流
FileInputStream
:从文件中读取字节数据。它是InputStream
的子类,用于读取文件内容。FileOutputStream
:向文件中写入字节数据。它是OutputStream
的子类,用于将数据写入文件。BufferedInputStream
:为FileInputStream
提供缓冲功能,提高读取效率。它通过内部缓冲区来减少实际的磁盘I/O操作次数。BufferedOutputStream
:为FileOutputStream
提供缓冲功能,提高写入效率。与BufferedInputStream
类似,它也是通过内部缓冲区来优化写入性能。DataInputStream
:允许从一个输入流中读取基本数据类型(如int
、float
等)和字符串。DataOutputStream
:允许向一个输出流中写入基本数据类型(如int
、float
等)和字符串。字符流
FileReader
:从文件中读取字符数据。它是Reader
的子类,用于读取文件内容。FileWriter
:向文件中写入字符数据。它是Writer
的子类,用于将数据写入文件。BufferedReader
:为FileReader
提供缓冲功能,提高读取效率。它通过内部缓冲区来减少实际的磁盘I/O操作次数。BufferedWriter
:为FileWriter
提供缓冲功能,提高写入效率。与BufferedReader
类似,它也是通过内部缓冲区来优化写入性能。InputStreamReader
:将字节流转换为字符流。它使用指定的字符集将字节流解码为字符。OutputStreamWriter
:将字符流转换为字节流。它使用指定的字符集将字符编码为字节。ThreadLocal
是 Java 中提供的一种机制,用于在多线程环境中为每个线程提供独立的变量副本。它的主要作用是解决多线程环境下的变量共享问题,避免线程间的数据干扰。
主要作用
synchronized
)来保护共享变量,从而简化了并发编程。示例
以下是一个简单的示例,展示了如何使用 ThreadLocal
来存储每个线程独有的数据。
public class ThreadLocalExample {
// 创建一个 ThreadLocal 对象,初始值为 null
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 创建两个线程
Thread thread1 = new Thread(new MyRunnable(), "Thread-1");
Thread thread2 = new Thread(new MyRunnable(), "Thread-2");
thread1.start();
thread2.start();
}
static class MyRunnable implements Runnable {
@Override
public void run() {
// 设置当前线程的 ThreadLocal 值
threadLocal.set((int) (Math.random() * 100));
// 获取当前线程的 ThreadLocal 值
System.out.println(Thread.currentThread().getName() + " initial value: " + threadLocal.get());
try {
// 模拟一些工作
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再次获取当前线程的 ThreadLocal 值
System.out.println(Thread.currentThread().getName() + " final value: " + threadLocal.get());
}
}
}
在这个示例中,我们创建了一个 ThreadLocal
对象,并在两个不同的线程中分别设置和获取其值。每个线程都会拥有自己独立的 ThreadLocal
变量副本,因此不会相互干扰。
ThreadLocal
变量是线程私有的,如果不及时清理,可能会导致内存泄漏。可以使用 ThreadLocal.remove()
方法手动清理。ThreadLocal
提供了一种简单而有效的方法来解决多线程环境下的变量共享问题,通过为每个线程提供独立的变量副本,避免了线程间的干扰,同时简化了并发编程的复杂性。Spring IoC(Inversion of Control,控制反转)和AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的两个核心概念,它们在软件开发中扮演着重要的角色。以下是对这两个概念的详细解释及示例:
Spring IoC(控制反转)
概念
IoC是一种设计原则,它将对象的创建权和管理权从应用程序本身反转到外部框架(如Spring容器)。这样,对象之间的依赖关系不再由对象自身来管理,而是由外部实体(如Spring容器)来负责。这种设计原则可以降低对象之间的耦合度,提高系统的可维护性和可扩展性。
实现方式
IoC主要通过依赖注入(Dependency Injection,简称DI)来实现。依赖注入有三种类型:构造器注入、Setter方法注入和注解注入。
示例
以下是一个使用注解实现IoC的简单示例:
定义接口与实现类:
// 定义一个服务接口
public interface UserService {
void saveUser();
}
// 实现该接口
@Component
public class UserServiceImpl implements UserService {
@Override
public void saveUser() {
System.out.println("用户数据保存成功!");
}
}
使用@Autowired自动注入:
@Controller
public class UserController {
@Autowired
private UserService userService;
public void execute() {
userService.saveUser();
}
}
在这个示例中,UserController
依赖于UserService
,但通过使用@Autowired
注解,Spring会自动将UserServiceImpl
的实例注入到UserController
中,从而实现了控制反转[1]。
Spring AOP(面向切面编程)
概念
AOP是一种编程范式,它允许开发者在不修改业务逻辑代码的情况下,为已有的方法增加额外的行为。这些额外的行为通常被称为“切面”(Aspect),它们可以在方法执行之前、之后或围绕方法执行时动态地织入到目标对象中。
核心概念
示例
以下是一个使用AspectJ注解实现日志记录功能的AOP示例:
定义切面类:
@Aspect
@Component
public class LogAspect {
// 定义一个切点,匹配com.example.service包下所有类的save方法
@Pointcut("execution(* com.example.service.*.save(..))")
public void logPointcut() {}
// 在切点处添加前置通知,记录方法执行前的日志
@Before("logPointcut()")
public void doBefore() {
System.out.println("执行前通知:方法开始执行");
}
// 在切点处添加后置通知,记录方法执行后的日志
@After("logPointcut()")
public void doAfter() {
System.out.println("执行后通知:方法执行结束");
}
}
应用切面:
将上述切面类添加到Spring容器的配置中(如果使用注解配置的话,确保组件扫描路径包含了切面类的包),Spring会自动将切面织入到匹配的目标对象中。当save
方法被调用时,日志记录功能就会自动触发。
Spring Boot中有许多常用注解,这些注解在简化配置、快速构建应用程序方面发挥着重要作用。以下是一些常用的Spring Boot注解及其简要说明:
核心注解
@SpringBootApplication
:这是一个组合注解,通常用于主应用程序类,标志着这是Spring Boot应用程序的入口点。它包含了其他注解,如@Configuration
、@ComponentScan
和@EnableAutoConfiguration
。@Configuration
:用于标记一个类作为配置类,它通常用于定义Bean
。@ComponentScan
:用于指定Spring容器扫描组件的基本包路径。Web注解
@Controller
:标志着一个类是Spring MVC控制器,处理HTTP请求。@RestController
:结合了@Controller
和@ResponseBody
,用于创建RESTful风格的控制器。@RequestMapping
:用于映射HTTP请求到控制器方法,并指定URL路径。Bean管理注解
@Component
:将一个类标识为Spring组件(Bean
),可以被Spring容器自动检测和注册。@Service
:用于标记一个类作为业务逻辑的服务组件。@Repository
:用于标记一个类作为数据访问组件,通常用于持久层。依赖注入注解
@Autowired
:用于自动装配Bean,通常与构造函数、Setter
方法或字段一起使用。@Resource
:按名称自动注入依赖对象(也可以按类型,但默认按名称)。@Qualifier
:与@Autowired
一起使用,用于指定要注入的Bean
的名称。数据访问注解
@Repository
:标志着一个类是Spring Data仓库,用于数据库访问。@Entity
:用于定义JPA实体类,映射到数据库表。其他常用注解
@Value
:注入配置文件中的值到对应的变量中。@Async
:标识异步方法,用于告诉Spring在调用该方法时使用异步线程执行。@Transactional
:提供声明式事务管理,用于标识需要使用事务的方法或类。@ExceptionHandler
:处理异常情况,用于定义全局的异常处理方法。@Scheduled
:定时任务注解,用于标识定时任务的方法。@Valid
:开启数据验证功能,用于对请求参数进行校验。Java中的锁机制是多线程编程中确保线程安全的重要工具,以下是常见的锁类型及其特点:
synchronized
关键字)
synchronized (object) {
// 临界区代码
}
显式锁(java.util.concurrent.locks.Lock
接口)
ReentrantLock
(可重入锁)Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
读写锁(ReentrantReadWriteLock
)
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock(); // 读锁
Lock writeLock = rwLock.writeLock(); // 写锁
StampedLock
(JDK8+)
StampedLock stampedLock = new StampedLock();
// 写锁
long stamp = stampedLock.writeLock();
try { /* 写操作 */ } finally { stampedLock.unlockWrite(stamp); }
// 乐观读
long stamp = stampedLock.tryOptimisticRead();
if (!stampedLock.validate(stamp)) {
stamp = stampedLock.readLock(); // 退化为悲观读
}
try { /* 读操作 */ } finally { stampedLock.unlockRead(stamp); }
共享锁与排他锁
synchronized
、ReentrantLock
,同一时刻仅一个线程持有。ReadWriteLock
)、信号量(Semaphore
),允许多线程并发访问。条件变量(Condition
)
ReentrantLock
)配合,实现线程间协调(等待/通知)。Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (条件不满足) {
condition.await(); // 释放锁并等待
}
// 条件满足后执行操作
condition.signalAll(); // 唤醒其他线程
} finally {
lock.unlock();
}
其他同步工具
Semaphore
(信号量):控制同时访问资源的线程数(如限流)。CountDownLatch
:等待多个线程完成操作后继续。CyclicBarrier
:线程到达屏障时等待,直到所有线程就绪。分布式锁(非Java内置)
RedLock
、ZooKeeper
)。锁的选择建议
synchronized
(自动管理,避免死锁风险)。ReentrantLock
(可中断、超时、公平性等高级功能)。ReentrantReadWriteLock
或 StampedLock
。StampedLock
的乐观读。在Java中,自定义线程池的拒绝策略通过实现 RejectedExecutionHandler
接口定义。以下是常见的 内置拒绝策略 及 自定义策略示例:
内置拒绝策略
线程池(ThreadPoolExecutor
)默认提供以下4种策略:
AbortPolicy
(默认策略)
RejectedExecutionException
异常。new ThreadPoolExecutor.AbortPolicy()
CallerRunsPolicy
new ThreadPoolExecutor.CallerRunsPolicy()
DiscardPolicy
new ThreadPoolExecutor.DiscardPolicy()
DiscardOldestPolicy
new ThreadPoolExecutor.DiscardOldestPolicy()
自定义拒绝策略
通过实现 RejectedExecutionHandler
接口,可灵活扩展拒绝逻辑:
示例:记录日志并持久化任务
public class CustomRejectionPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
// 1. 记录任务信息(日志、数据库等)
System.out.println("任务被拒绝: " + task);
// 2. 持久化任务到磁盘或消息队列(异步重试)
saveTaskToStorage(task);
// 3. 可选:触发告警通知
sendAlert("线程池过载,任务已持久化!");
}
private void saveTaskToStorage(Runnable task) {
// 实现具体存储逻辑(如写入文件、数据库或消息队列)
}
private void sendAlert(String message) {
// 发送告警通知(邮件、短信等)
}
}
使用自定义策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), // 任务队列容量
new CustomRejectionPolicy() // 自定义拒绝策略
);
拒绝策略的触发条件
当线程池同时满足以下条件时,触发拒绝策略:
shutdown()
后继续提交任务)。选择策略的建议
策略 | 适用场景 | 注意事项 |
---|---|---|
AbortPolicy | 需严格保证任务不丢失,允许通过异常处理 | 需捕获异常,避免程序崩溃 |
CallerRunsPolicy | 限制任务提交速度,防止系统过载 | 可能阻塞主线程或上游服务 |
DiscardPolicy | 允许静默丢弃非关键任务(如日志、监控) | 需确保数据丢失不影响业务 |
DiscardOldestPolicy | 新任务优先级高于旧任务(如实时数据处理) | 可能丢失重要历史任务 |
自定义策略 | 需要扩展拒绝逻辑(如重试、持久化、降级) | 实现复杂度较高,需处理异步逻辑和资源释放 |
RejectedExecutionHandler
满足业务定制需求(如任务重试、异步存储)。CAS(Compare And Swap
,比较并交换)是一种 无锁并发控制 的核心技术,通过 硬件指令(如CPU原子操作) 实现多线程环境下的原子性操作。其原理可以用以下步骤概括:
1. CAS的核心机制
CMPXCHG
)完成,避免多线程竞争问题。示例说明
以Java的AtomicInteger
实现为例:
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.compareAndSet(0, 1); // CAS操作
atomicInt
的值(初始为0)。CAS的底层实现
CMPXCHG
,ARM的LDREX/STREX
)。sun.misc.Unsafe
类的本地方法(如compareAndSwapInt
)调用底层指令。CAS的典型应用场景
原子类(java.util.concurrent.atomic
包)
AtomicInteger
、AtomicLong
等通过CAS实现无锁线程安全操作。incrementAndGet()
方法实现自增:public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
乐观锁
同步框架
AbstractQueuedSynchronizer
)通过CAS实现锁状态(state
)的原子更新。CAS的优缺点
优点
缺点
AtomicStampedReference
)。ABA问题详解
场景描述
解决方案
使用AtomicStampedReference
或AtomicMarkableReference
,通过 版本号/标记 跟踪变量变化:
AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(100, 0); // 初始值100,版本号0
int[] stampHolder = new int[1];
int currentValue = atomicRef.get(stampHolder); // 获取值和当前版本号
atomicRef.compareAndSet(currentValue, 200, stampHolder[0], stampHolder[0] + 1);
CAS与锁的对比
特性 | CAS | 锁(synchronized/Lock) |
---|---|---|
线程阻塞 | 无阻塞(自旋) | 可能阻塞(等待锁释放) |
适用场景 | 简单原子操作(如计数器) | 复杂临界区逻辑 |
性能开销 | 低(无上下文切换) | 高(上下文切换、锁竞争) |
编程复杂度 | 高(需处理ABA问题、自旋逻辑) | 低(自动管理临界区) |
ConcurrentHashMap
)。ConcurrentHashMap
和HashMap
是Java中两种常用的集合类,它们在多线程环境下的表现和性能有着显著的区别。以下是两者的详细对比:
线程安全性
ConcurrentModificationException
)。例如,在一个线程正在遍历HashMap
的同时,另一个线程对其进行了修改操作,就可能会抛出该异常。Compare-And-Swap
)操作和synchronized
关键字的组合来实现更高效的并发控制,只有在需要对某个节点进行修改时,才会对该节点进行加锁,减少了锁的竞争。迭代器是否是fail-fast的
ConcurrentModificationException
异常,但这并不是绝对的,取决于具体的修改操作和遍历方式。并发性能
ConcurrentHashMap
能够充分利用多核CPU的资源,实现高效的并发访问。初始化和扩容机制
HashMap
类似,当数据量超过容量时也会进行扩容,但ConcurrentHashMap
的扩容过程更加复杂,因为它需要考虑分段锁或并发控制的问题,以确保在扩容过程中数据的一致性和线程安全。应用场景
是否支持null键和null值
NullPointerException
异常。获取方式
new HashMap()
直接创建实例。new ConcurrentHashMap()
创建实例,也可以通过Collections.newConcurrentMap()
方法创建带有初始容量的实例。在Java集合框架中,HashMap
、HashTable
、TreeMap
、List
和Set
是常用的数据结构,它们的区别主要体现在接口归属、数据结构特性、线程安全性、性能等方面。以下是它们的详细对比:
1. 接口归属
Collection
接口的子接口,存储单列数据(单个元素)。Map
接口的实现类,存储键值对(双列数据)。核心特性
类型 | 有序性 | 元素重复性 | 底层数据结构 | 典型实现类 |
---|---|---|---|---|
List | 按插入顺序(索引访问) | 允许重复 | 动态数组/链表 | ArrayList , LinkedList |
Set | 无序(或按排序规则) | 不允许重复 | 哈希表/红黑树 | HashSet , TreeSet |
HashMap | 不保证顺序(哈希散列) | 键唯一,值可重复 | 数组+链表/红黑树(JDK8+) | HashMap , LinkedHashMap |
HashTable | 不保证顺序 | 键唯一,值可重复 | 数组+链表 | HashTable |
TreeMap | 按键的自然顺序或自定义排序 | 键唯一,值可重复 | 红黑树 | TreeMap |
HashTable
(所有方法用synchronized
修饰)。List
(如ArrayList
、LinkedList
)、Set
(如HashSet
、TreeSet
)、HashMap
、TreeMap
。
Collections.synchronizedXXX()
包装,或使用并发集合(如ConcurrentHashMap
)。在Java中,HashMap
和List
(以ArrayList
为例)的扩容机制是为了动态调整存储空间以适应元素数量的增长。以下是它们的扩容原理详解:
ArrayList的扩容原理
ArrayList
基于动态数组实现,当容量不足时自动扩容。
核心步骤:
触发条件:
add()
方法添加元素时,如果当前元素数量(size
)等于数组容量(elementData.length
),触发扩容。扩容规则:
int newCapacity = oldCapacity + (oldCapacity >> 1); // 位运算等价于 oldCapacity * 1.5
ensureCapacity()
),会取用户指定值和默认计算值的较大者。数据迁移:
Arrays.copyOf
)。O(n)
,频繁扩容会影响性能。示例:
// 初始容量为10
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // 添加第1~10个元素时,无需扩容
list.add(11); // 第11个元素触发扩容,新容量 = 10 * 1.5 = 15
优化建议:
new ArrayList<>(100)
),避免多次扩容。HashMap的扩容原理
HashMap
基于哈希表(数组+链表/红黑树)实现,扩容是为了减少哈希冲突,保证性能。
核心步骤:
触发条件:
size
)超过阈值(threshold = capacity * loadFactor
)时,触发扩容。loadFactor
)为0.75,默认初始容量为16。扩容规则:
bucket
),重新计算每个键值对的哈希值,分配到新数组的位置。哈希重分布:
newIndex = hash(key) & (newCapacity - 1)
。示例:
// 默认容量16,阈值 = 16 * 0.75 = 12
HashMap<Integer, String> map = new HashMap<>();
for (int i = 0; i < 12; i++) {
map.put(i, "value"); // 第13个元素触发扩容,新容量 = 16 * 2 = 32
}
优化建议:
new HashMap<>(64)
),避免多次扩容。new HashMap<>(16, 0.5f)
)可以更早扩容,减少哈希冲突。对比总结
特性 | ArrayList | HashMap |
---|---|---|
初始容量 | 10 | 16 |
扩容触发条件 | size == capacity | size > threshold(capacity * loadFactor) |
扩容倍数 | 1.5倍(旧容量 + 旧容量/2) | 2倍(旧容量 << 1) |
数据迁移成本 | O(n)(全量复制) | O(n)(重新哈希所有元素) |
设计目标 | 快速随机访问,减少内存浪费 | 减少哈希冲突,保证查询效率 |
ArrayList
和HashMap
均非线程安全,扩容时并发操作可能引发异常。CopyOnWriteArrayList
或ConcurrentHashMap
。在Java的HashMap
中,put(key, value)
操作通过以下步骤判断键是否重复:
核心判断逻辑
HashMap
通过**哈希值(hashCode
)和对象相等性(equals
)**共同判断键是否重复:
equals
返回true
→ 确认重复。put()
方法的具体流程
当调用map.put(key, value)
时,流程如下:
步骤1:计算键的哈希值
int hash = hash(key); // 实际是调用key.hashCode()并经过扰动处理
key.hashCode()
进行二次哈希(如异或高位),减少哈希冲突。static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
步骤2:定位桶(Bucket)
int index = (n - 1) & hash; // n是数组长度(2的幂)
步骤3:遍历桶内链表/红黑树
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
==
)或内容相等(equals
)→ 判定重复,替换旧值。步骤4:处理哈希冲突
关键点
** hashCode()
和equals()
的必要性**
hashCode()
:快速定位可能的桶,减少遍历次数。equals()
:精确判断键是否重复(解决哈希冲突)。(2) 自定义对象作为键
若使用自定义类作为键,必须重写hashCode()
和equals()
:
Object
的实现(基于内存地址),可能导致逻辑错误。class Student {
String name;
int id;
@Override
public int hashCode() {
return Objects.hash(name, id); // 基于name和id计算哈希值
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return id == student.id && Objects.equals(name, student.name);
}
}
(3) null
键的特殊处理
HashMap
允许一个null
键,存储在数组下标0的位置。null
键只需检查是否存在已有的null
键。4. 示例演示
HashMap<String, Integer> map = new HashMap<>();
map.put("a", 1); // 哈希值计算后定位到桶1,插入新节点
map.put("a", 2); // 哈希值相同 → 检查equals → 重复 → 替换值(1→2)
map.put(new String("a"), 3); // 哈希值相同,且equals为true → 替换值(2→3)
map.put(new String("a"), 4); // 同上
5. 总结
equals()
内容相等。hashCode()
设计能减少哈希冲突,提升效率。hashCode()
和equals()
。在Java中,final
、finally
和finalize
是三个不同的关键字,它们有不同的作用和用途。以下是关于这三个关键字的详细解释:
final
final
是一个关键字,用于修饰类、方法和变量,表示它们的定义不能被改变。final
修饰一个类时,这个类不能被继承。例如,public final class MyFinalClass {}
定义了一个不能被继承的类。final
修饰一个方法时,这个方法不能被子类重写。例如,public class MyClass { public final void myFinalMethod() {} }
定义了一个不能被子类重写的方法。final
修饰一个变量时,这个变量的值一旦被初始化后就不能被改变。例如,public static final String CONSTANT_STRING = "FINAL_STRING";
定义了一个常量字符串。finally
finally
是一个关键字,用于定义一个代码块,该代码块中的代码在不论是否发生异常的情况下都会被执行。finally
块通常与try-catch
块一起使用,以确保在处理异常时执行一些必要的清理工作,例如关闭文件、释放资源等。try
块中的代码是否抛出异常并被捕获,也无论catch
块中的代码是否成功执行,finally
块中的代码都会执行。finalize
finalize
是Object类中的一个方法,用于在垃圾回收器将对象从内存中清除之前通知对象进行清理操作。finalize()
方法。finalize()
方法中,可以进行一些清理操作,如关闭文件、释放内存等。finalize()
方法的执行时间不确定,且不保证一定会被执行。因此,它不应该被用于关键的清理操作。可变性
String
对象,它的值就不能被改变。每次对String
对象进行拼接、替换等操作时,实际上是创建了一个新的String
对象。StringBuffer
对象进行修改操作时,不会创建新的对象,而是直接在原对象的基础上进行修改。StringBuffer
类似,它内部同样有一个可动态扩展的字符数组,对StringBuilder
对象进行修改操作时,也是直接在原对象的基础上进行修改。线程安全性
String
是不可变的,所以它是线程安全的。多个线程可以同时访问同一个String
对象,而不会出现数据不一致的问题。synchronized
关键字进行同步,保证了在多线程环境下对StringBuffer
对象的操作是线程安全的。synchronized
关键字进行同步,因此在多线程环境下使用StringBuilder
可能会出现数据不一致的问题。但在单线程环境下,StringBuilder
的性能优于StringBuffer
。性能
String
是不可变的,每次对String
对象进行修改操作时都需要创建新的对象,这会导致频繁的内存分配和垃圾回收,因此在频繁进行字符串修改操作时,String
的性能较差。synchronized
关键字进行同步,会带来一定的性能开销。在单线程环境下,StringBuffer
的性能不如StringBuilder
。StringBuilder
的性能是最好的,尤其是在频繁进行字符串拼接等修改操作时。使用场景
String
类。例如,存储一些常量字符串、文件名等。StringBuffer
类,以保证线程安全。StringBuilder
类,以获得更好的性能。