【八股战神篇】Spring高频面试题汇总

专栏简介

Bean 的生命周期了解么?

延伸

谈谈自己对于 Spring IoC 的了解

延伸

什么是动态代理?

延伸

动态代理和静态代理的区别

延伸

Spring AOP的执行流程

延伸

Spring的事务什么情况下会失效?

延伸


专栏简介

八股战神篇专栏是基于各平台共上千篇面经,上万道面试题,进行综合排序提炼出排序前百的高频面试题,并对这些高频八股进行关联分析,将每个高频面试题可能进行延伸的问题进行分析排序选出高频延伸八股题。面试官都是以点破面从一个面试题不断深入,目的是测试你的理解程度。本专栏将解决你的痛点,助你从容面对。本专栏已更新Java基础高频面试题、Java集合高频面试题、MySQL高频面试题、JUC 并发高频面试题、JVM高频面试题、Spring高频面试题,Redis高频面试题、操作系统高频面试题,后续会继续更新计算机网络、设计模式、场景题、RocketMQ等,计划在七月前更新完毕,点此链接订阅专栏“八股战神篇”。

Bean 的生命周期了解么?

Bean 的生命周期是 Spring 容器管理 Bean 从创建到销毁的整个过程,主要包括实例化、属性赋值、初始化和销毁这四个阶段,下面我将分阶段进行讲述。

首先,创建 Bean 实例。Spring 容器会根据配置文件或注解信息,使用 Java 反射机制实例化 Bean。

然后,属性赋值和依赖注入。Spring 会解析 Bean 的属性,例如 @Autowired、@Value 或 @Resource 等注解,并通过构造方法或 setter 方法将依赖注入到 Bean 中。

接着,初始化 Bean。在这个阶段,Spring 会进行一些额外的处理:

如果 Bean 实现了 BeanNameAware、BeanClassLoaderAware 或 BeanFactoryAware 接口,Spring 会调用相应的 set 方法,将 Bean 名称、类加载器和 BeanFactory 传递给 Bean。

如果有 BeanPostProcessor 处理器,Spring 会在初始化前调用 postProcessBeforeInitialization() 方法。

如果 Bean 实现了 InitializingBean 接口,则会执行 afterPropertiesSet() 方法。

如果在配置中指定了 init-method,Spring 会调用该方法执行自定义初始化逻辑。

初始化后,Spring 还会调用 BeanPostProcessor 的 postProcessAfterInitialization() 方法。

最后,销毁 Bean。当 Spring 容器关闭时,它会销毁 Bean:

如果 Bean 实现了 DisposableBean 接口,则会执行 destroy() 方法。

如果在配置中定义了 destroy-method,则会调用指定的销毁方法。

如果使用 @PreDestroy 注解标记了销毁前的方法,Spring 也会执行该方法释放资源。

延伸

1.Spring Bean 的生命周期图

2.Bean 是线程安全的吗?

Spring 框架中的 Bean 是否具备线程安全性,主要取决于它的作用域以及是否包含可变状态。

Bean 线程安全性的影响因素

Spring 默认的 Bean 作用域是 singleton,即在 IoC 容器中只会创建一个实例,并被多个线程共享。如果这个 Bean 维护了可变的成员变量,就可能在并发访问时引发数据不一致的问题,从而导致线程安全风险。

而 prototype 作用域 下,每次获取 Bean 都会创建新的实例,因此不会发生资源竞争,自然也就没有线程安全问题。

单例 Bean 是否一定不安全?

不一定!

无状态 Bean 是线程安全的:例如常见的 Service 或 Dao 层 Bean,它们通常不存储可变数据,仅执行业务逻辑,因此不会受到并发影响。

有状态 Bean 可能会引发线程安全问题:如果 Bean 存储了可变成员变量,比如用户会话信息、计数器等,可能会因多个线程同时访问导致数据不一致。

解决有状态 Bean 的线程安全问题

如果一个单例 Bean 需要维护状态,可通过以下方式确保线程安全:

设计为无状态 Bean:尽量避免定义可变成员变量,或在方法内部使用局部变量。

使用 ThreadLocal:让每个线程拥有独立的变量副本,防止数据共享导致冲突。

同步控制:在访问共享资源时,使用 synchronized 或 ReentrantLock 进行加锁,确保线程互斥访问。

3.将一个类声明为 Bean 的注解有哪些?

注解

用途

示例场景

@Component

通用注解,标记任意类为 Bean

通用组件

@Service

标记服务层组件

业务逻辑层

@Repository

标记数据访问层组件

数据库操作

@Controller

标记表现层组件

Web 控制器

@Configuration

配置类,配合 @Bean 显式声明 Bean

自定义 Bean 配置

@Conditional

根据条件动态注册 Bean

环境或逻辑条件

@Profile

根据激活的环境注册 Bean

不同环境下的配置

@Scope

定义 Bean 的作用域

单例、原型等

@RestController

RESTful Web 服务控制器

API 接口开发

@EnableScheduling

配合 @Scheduled 声明定时任务 Bean

定时任务

谈谈自己对于 Spring IoC 的了解

Spring IoC(Inversion of Control,控制反转)是 Spring 框架的核心机制之一,负责管理对象的创建、依赖关系和生命周期,从而实现组件解耦,提升代码的可维护性和扩展性。接下来我会详细讲述 Spring IoC 的核心概念、实现方式、工作流程以及它解决的问题。

首先,IoC 的核心思想 是将对象的管理权从应用程序代码中转移到 Spring 容器。传统方式下,类 A 依赖于类 B,A 需要自己创建 B 的实例,而在 IoC 模式下,Spring 负责实例化和注入 B,A 只需要声明依赖即可。

其次,Spring IoC 主要通过依赖注入(DI)来实现。Spring 通过 XML 配置、Java 注解(@Autowired、@Resource)或 Java 代码(@Bean)定义 Bean 及其依赖关系,容器会在运行时自动解析并注入相应的对象。

接着,Spring IoC 的工作流程 可以分为三个阶段:

第一个阶段是IOC 容器初始化,

Spring 解析 XML 配置或注解,获取所有 Bean 的定义信息,生成 BeanDefinition。

BeanDefinition 存储了 Bean 的基本信息(类名、作用域、依赖等),并注册到 IOC 容器的 BeanDefinitionMap 中。

这个阶段完成了 IoC 容器的初始化,但还未实例化 Bean。

第二个阶段是Bean 实例化及依赖注入

Spring 通过反射实例化那些 未设置 lazy-init 且是单例模式 的 Bean。

依赖注入(DI)发生在这个阶段,Spring 根据 BeanDefinition 解析 Bean 之间的依赖关系,并通过构造方法、setter 方法或字段注入(@Autowired)完成对象的注入。

第三个阶段是Bean 的使用

业务代码可以通过 @Autowired 或 BeanFactory.getBean() 获取 Bean。

对于 设置了 lazy-init 的 Bean 或非单例 Bean,它们的实例化不会在 IoC 容器初始化时完成,而是在 第一次调用 getBean() 时 进行创建和初始化,且 Spring 不会长期管理它们。

最后,Spring IoC 主要解决三个问题

第一个是降低耦合,组件之间通过接口和依赖注入解耦,增强了代码的灵活性。

第二个是简化对象管理,开发者无需手动创建对象,Spring 统一管理 Bean 生命周期。

第三个是提升维护性,当需要修改依赖关系时,只需调整配置,而无需修改业务代码。

延伸

1.传统应用程序图

2.IoC控制反转图

3.IoC 和 DI 的区别?

IoC(Inversion of Control,控制反转)和 DI(Dependency Injection,依赖注入)是 Spring 框架中非常重要的两个概念。虽然它们密切相关,但它们的含义和作用有所不同。以下是它们的区别及联系:

(1)定义与核心思想

IoC(控制反转)

定义:控制反转是一种设计原则,指的是将对象的创建、依赖管理和生命周期的控制权从应用程序代码转移到框架或容器中。

核心思想:传统开发中,对象需要自己负责创建依赖的对象(即“正向控制”)。而在 IoC 中,对象不再负责创建依赖,而是由容器来管理这些依赖关系。

DI(依赖注入)

定义:依赖注入是 IoC 的一种实现方式,指的是容器通过构造方法、setter 方法或字段注入的方式,将对象的依赖自动传递给它。

核心思想:对象只需要声明它需要的依赖,而不需要关心如何获取这些依赖。容器会负责将依赖注入到对象中。

(2)区别对比

维度

IoC(控制反转)

DI(依赖注入)

定义

一种设计原则,强调控制权的转移

一种具体实现方式,用于实现 IoC

关注点

对象的创建、依赖管理和生命周期的控制权

如何将依赖传递给对象

实现方式

通过容器(如 Spring 容器)管理对象

通过构造方法、setter 方法或字段注入依赖

范围

更广泛,包含 DI 和其他实现方式

是 IoC 的一个子集

示例

Spring 容器接管了对象的创建和管理

Spring 容器通过 @Autowired 注入依赖

(3)联系

DI 是 IoC 的实现方式:依赖注入是控制反转的一种具体实现形式。IoC 是一种更广泛的设计原则,而 DI 是 IoC 的一种技术手段。

共同目标:两者都旨在降低代码的耦合性,提升代码的可维护性和扩展性。

(4)示例说明

传统方式(无 IoC 和 DI)

public class UserService {
    private UserRepository userRepository;

    public UserService() {
        this.userRepository = new UserRepository(); // 自己创建依赖
    }

    public void addUser(String name) {
        userRepository.save(name);
    }
}

问题:UserService 直接依赖 UserRepository,耦合度高。如果需要更换 UserRepository 的实现,必须修改 UserService 的代码。

使用 IoC 和 DI

@Component
public class UserService {
    @Autowired
    private UserRepository userRepository; // 声明依赖

    public void addUser(String name) {
        userRepository.save(name);
    }
}

改进:UserService 不再负责创建 UserRepository,而是通过 Spring 容器注入依赖。如果需要更换 UserRepository 的实现,只需在配置中调整,无需修改 UserService 的代码。

什么是动态代理?

动态代理是一种在运行时动态生成代理对象,并在代理对象中增强目标对象方法的技术。它被广泛用于 AOP(面向切面编程)、权限控制、日志记录等场景,使得程序更加灵活、可维护。动态代理可以通过 JDK 原生的 Proxy 机制或 CGLIB 方式实现。接下来我会讲述动态代理的实现方式和执行流程。

首先,JDK 动态代理基于接口,适用于代理实现了接口的对象,当使用 JDK 动态代理时,主要分为四步,

第一步是定义接口,由于动态代理是基于接口进行代理的,因此目标对象必须实现接口。

第二步是创建并实现 InvocationHandler 接口,并在 invoke 方法中定义增强逻辑。

第三步是生成代理对象,使用 Proxy.newProxyInstance 创建代理对象,代理对象内部会调用 invoke 方法。

第四步是调用代理方法,当调用代理对象的方法时,invoke 方法会被触发,执行增强逻辑,并最终调用目标方法。

其次,CGLIB 通过子类继承目标类,适用于没有实现接口的类,当使用 CGLIB 动态代理时,主要分为四步,

第一步是通过 Enhancer 创建代理对象。

第二步是设置父类,CGLIB 代理基于子类继承,因此代理对象是目标类的子类。

第三步是定义并实现 MethodInterceptor 接口,在 intercept 方法中增强目标方法。

第四步是调用代理方法,当调用代理对象的方法时,intercept 方法会被触发,执行增强逻辑,并最终调用目标方法。

延伸

1.动态代理有哪些应用场景?

Spring AOP(面向切面编程):如事务管理、日志记录、权限控制等。

MyBatis Mapper 代理:使用 JDK 动态代理创建 Mapper 接口的代理对象。

RPC 远程调用:动态代理封装远程服务调用(如 Dubbo)。

拦截器机制:如 Servlet 过滤器、拦截器等。

Mock 测试:动态代理可用于创建测试桩对象。

动态代理和静态代理的区别

动态代理和静态代理都属于代理模式,它们都用于在不修改目标对象代码的情况下增强其功能。接下来我会详细讲述动态代理和静态代理的五点区别。

第一点是实现方式的不同,静态代理需要手动编写代理类,而动态代理在运行时动态生成代理类。

第二点是灵活性的不同,静态代理不够灵活,代理类与目标类一一对应;而动态代理更加灵活,适用于多种目标类。

第三点是维护成本的不同,静态代理的维护成本较高,因为每个目标类都需要一个代理类;而动态代理的维护成本较低,因为代理逻辑是通用的。

第四点是技术依赖的不同,静态代理基于普通 Java 类实现,而动态代理依赖于反射机制(JDK 动态代理)或字节码技术(CGLIB)。

第五点是适用场景的不同,静态代理则适用于简单的、目标类较少的场景;而动态代理适合需要为多个目标类添加相同逻辑的场景。

延伸

1.如何使用静态代理?

首先,静态代理是指在编译时就已经确定了代理类和目标类的关系。开发者需要手动编写代理类,并在代理类中调用目标类的方法,同时添加额外的逻辑。

当使用静态代理时,

首先,开发者需要为目标类创建一个代理类,代理类通常会实现与目标类相同的接口。

其次,在代理类中,开发者需要手动编写方法调用逻辑,例如在调用目标方法前后添加自定义逻辑(如日志记录或权限校验)。

然后,通过代理类的对象调用方法,实际执行的是代理类中的逻辑,而不是直接调用目标类的方法。

最后,由于代理类是手动编写的,因此每个目标类都需要一个对应的代理类,这会导致代码冗余,尤其是在目标类较多时维护成本较高。

Spring AOP的执行流程

Spring AOP(Aspect-Oriented Programming,面向切面编程)是一种通过代理机制实现方法增强的技术,它允许在不修改原始代码的情况下,对方法的执行过程进行扩展,如日志记录、事务管理、权限控制等。Spring AOP 主要依赖动态代理来实现,接下来我会讲述 Spring AOP 的执行流程,主要分为六步。

当 Spring AOP 拦截一个方法调用时:

第一步是要定义切面(Aspect),可以使用 @Aspect 标注类,并在其中定义切点(Pointcut)和通知(Advice),如 @Before、@After、@Around 等。

第二步是要解析切点,Spring 会解析 @Pointcut 表达式,确定需要增强的方法。

第三步是要创建代理对象,如果目标类实现了接口,Spring 使用 JDK 动态代理,通过 Proxy.newProxyInstance 生成代理对象;如果目标类没有实现接口,Spring 使用 CGLIB 动态代理,通过创建目标类的子类来生成代理对象。

第四步是要方法调用拦截,如果是JDK 动态代理,代理对象会拦截方法调用,并调用 InvocationHandler#invoke,执行增强逻辑后,再调用目标方法;如果是CGLIB 动态代理,代理对象则通过 MethodInterceptor#intercept 代理方法调用,执行增强逻辑后,再调用目标方法。

第五步是要执行增强逻辑,根据通知类型,在方法执行前后或异常时,执行对应的 AOP 逻辑,如日志记录、事务提交等。

第六步是要执行目标方法,最终调用目标对象的方法,完成实际业务逻辑。

延伸

1. AOP 关键术语

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来。以下是 AOP 的关键术语:

(1) Aspect(切面)

定义:切面是横切关注点的模块化表示,通常包含一组相关的通知(Advice)和切点(Pointcut)。

作用:将通用的功能(如日志、事务管理)封装到一个独立的模块中。

示例:

@Aspect
public class LoggingAspect {
    // 切面内容
}

(2) Join Point(连接点)

定义:程序执行过程中的某个特定点,例如方法调用、方法执行、异常抛出等。

作用:Spring AOP 中的连接点通常是方法执行。

示例:UserService.addUser() 方法的执行是一个连接点。

(3) Pointcut(切点)

定义:切点是一组连接点的集合,用于定义哪些连接点需要被增强。

作用:通过表达式匹配特定的方法或类。

示例:

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}

(4) Advice(通知)

定义:通知是切面在特定连接点上执行的动作,定义了“何时”以及“如何”增强。

作用:在方法执行前、后或异常时执行额外的逻辑。

类型:见下文详细说明。

(5) Target Object(目标对象)

定义:被代理的对象,也就是需要增强的业务逻辑对象。

作用:目标对象的方法会被切面拦截并增强。

(6) Proxy(代理对象)

定义:由 AOP 框架生成的代理对象,用于拦截目标对象的方法调用并执行增强逻辑。

作用:实现方法拦截和增强。

(7) Weaving(织入)

定义:将切面应用到目标对象并创建代理对象的过程。

作用:织入可以在编译时、类加载时或运行时完成。

2. AOP 常见的通知类型

通知(Advice)定义了切面在连接点上的行为,Spring AOP 提供了以下五种常见的通知类型:

(1) Before Advice(前置通知)

定义:在目标方法执行之前执行的通知。

注解:@Before

示例:

@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
    System.out.println("Before method: " + joinPoint.getSignature().getName());
}

(2) After Returning Advice(返回后通知)

定义:在目标方法成功执行并返回结果后执行的通知。

注解:@AfterReturning

示例:

@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterReturning(Object result) {
    System.out.println("Method returned: " + result);
}

(3) After Throwing Advice(异常后通知)

定义:在目标方法抛出异常后执行的通知。

注解:@AfterThrowing

示例:

@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void logAfterThrowing(Exception ex) {
    System.out.println("Exception thrown: " + ex.getMessage());
}

(4) After (Finally) Advice(最终通知)

定义:无论目标方法是否成功执行,都会在方法结束后执行的通知。

注解:@After

示例:

@After("execution(* com.example.service.*.*(..))")
public void logAfter() {
    System.out.println("After method execution");
}

(5) Around Advice(环绕通知)

定义:在目标方法执行前后都执行的通知,可以控制目标方法的执行。

注解:@Around

示例:

@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("Before method: " + joinPoint.getSignature().getName());
    Object result = joinPoint.proceed(); // 执行目标方法
    System.out.println("After method: " + joinPoint.getSignature().getName());
    return result;
}

总结表

通知类型

注解

执行时机

用途

Before Advice

@Before

在目标方法执行之前

日志记录、权限检查

After Returning

@AfterReturning

在目标方法成功返回后

记录返回值、清理资源

After Throwing

@AfterThrowing

在目标方法抛出异常后

异常处理、日志记录

After (Finally)

@After

在目标方法结束时(无论成功还是失败)

资源释放、日志记录

Around Advice

@Around

在目标方法执行前后,可控制目标方法的执行

性能监控、事务管理、日志记录

3. AOP 的应用场景有哪些?

日志记录:在方法执行前后自动记录日志。

事务管理:自动开启、提交或回滚事务(如 @Transactional)。

权限控制:在方法执行前进行权限校验,决定是否继续执行。

性能监控:统计方法执行时间,进行性能分析。

异常处理:统一捕获和处理异常,避免在业务代码中大量 try-catch。

缓存机制:拦截方法调用,判断是否需要从缓存中获取数据,而不是重复查询数据库。

分布式追踪:在微服务架构下,拦截请求并添加分布式跟踪信息,如 Sleuth + Zipkin。

4.AOP 为什么叫做切面编程?

AOP 之所以被称为 切面编程,是因为它的核心思想是 "切面"(Aspect),即 把系统中的横切关注点抽取出来,通过切面进行统一管理。

为什么叫“切面”:

横切关注点(Cross-cutting Concerns):日志、事务、权限等功能本身不属于某个具体业务,而是多个模块都需要的功能,称为“横切关注点”。

“切片”思想:AOP 允许在不修改原有业务逻辑的情况下,给多个业务方法“切入”额外的功能。

解耦业务代码:通过“切面”技术,AOP 让这些横切关注点从业务代码中分离出来,集中管理,避免代码重复。

形象比喻:

如果把业务代码比作“面包”,AOP 就像“切刀”,可以在方法执行的不同阶段(前、后、异常)“切入”额外功能,而不影响面包的主要结构。

Spring的事务什么情况下会失效?

Spring 的事务管理是 Spring 框架中非常重要的功能,它通过声明式事务(如 @Transactional 注解)或编程式事务简化了事务的管理。然而,在某些情况下,Spring 的事务可能会失效,导致事务无法正常回滚或提交。接下来我会详细讲述 Spring 事务失效的常见场景及其原因,失效的常见场景主要有六个。

第一个场景是方法未被代理对象调用,当使用 @Transactional 注解时,Spring 会为目标类生成一个代理对象,并通过代理对象拦截方法调用以管理事务。如果目标方法是通过类内部调用(即 this.method())而不是通过代理对象调用,则事务会失效。这是因为代理对象无法拦截类内部的方法调用,导致事务逻辑未被执行。

第二个场景是异常未被捕获或未触发回滚规则,当事务方法抛出异常时,Spring 默认只会在遇到 RuntimeException 或 Error 时回滚事务,而不会对受检异常(Checked Exception)进行回滚。如果开发者捕获了异常但未重新抛出,或者未正确配置回滚规则(如通过 @Transactional(rollbackFor = Exception.class)),事务也会失效。

第三个场景是事务传播行为配置不当,当多个事务方法相互调用时,Spring 提供了多种事务传播行为(如 REQUIRED、REQUIRES_NEW 等)。如果传播行为配置不当,可能导致事务未按预期工作。例如,如果外部方法的事务传播行为为 NOT_SUPPORTED 或 NEVER,则内部方法的事务可能被挂起或完全不生效。

第四个场景是数据库引擎不支持事务,当使用不支持事务的数据库引擎时,例如 MySQL 的 MyISAM 引擎不支持事务,即使代码中配置了事务管理,也无法生效。因此,确保使用的数据库引擎(如 InnoDB)支持事务是非常重要的。

第五个场景是代理模式配置错误,当使用 @Transactional 注解时,Spring 默认使用 JDK 动态代理或 CGLIB 动态代理来管理事务。如果目标类没有实现接口且未启用 CGLIB 代理(如未设置 proxyTargetClass=true),事务可能会失效。此外,如果目标类被标记为 final 或方法被标记为 private,CGLIB 代理也无法生成,导致事务失效。

第六个场景是事务管理器配置错误,当 Spring 容器中存在多个事务管理器时,如果未明确指定事务管理器(如通过 @Transactional("transactionManagerName")),可能导致事务管理器选择错误。这会导致事务无法正常工作,尤其是在多数据源场景下。

延伸

1.事务的特性(ACID)

(1)A(Atomicity,原子性)定义: 事务是一个 不可分割的最小执行单位,要么全部执行,要么全部回滚,不会有部分成功、部分失败的情况。示例: 转账操作 A → B,如果 A 账户扣款成功,但 B 账户未到账,系统必须回滚,保证 A 账户的钱不会凭空减少。

(2)C(Consistency,一致性)定义: 事务执行前后,数据库要始终保持 一致性状态,不会破坏数据的完整性和约束条件。示例: 银行总账恒定:A 给 B 转账 100 元,无论事务成功还是失败,A + B 的总金额不变。

(3)I(Isolation,隔离性)定义: 并发事务之间 互不影响,一个事务的操作对其他事务不可见,直到它提交。隔离级别(由低到高):

Read Uncommitted(读未提交):可能出现脏读(读取未提交数据)。

Read Committed(读已提交):防止脏读,但可能出现不可重复读(同一查询两次结果不同)。

Repeatable Read(可重复读)(MySQL 默认):防止脏读、不可重复读,但可能有幻读。

Serializable(可串行化):最高级别,事务串行执行,性能最低。

(4)D(Durability,持久性)定义: 事务提交后,对数据库的更改是 永久性的,即使系统崩溃也不会丢失数据。示例: 订单支付成功后,系统崩溃,重启后订单状态依然保持“已支付”。

### Java 高频面试题整理 以下是针对 Java 技术栈的一些高频面试题目及其解析: --- #### **1. JVM 的组成结构是什么?** JVM 是 Java 虚拟机的缩写,其核心功能是加载并执行字节码文件。它由类加载器、运行时数据区(方法区、堆、虚拟机栈、本地方法栈、程序计数器)、垃圾回收机制等部分构成[^1]。 ```java // 示例:通过反射查看当前 JVM 的版本号 public class JVMVersion { public static void main(String[] args) { System.out.println(System.getProperty("java.version")); // 获取 JVM 版本 } } ``` --- #### **2. JRE 和 JDK 的区别是什么?** JRE(Java Runtime Environment)是指 Java 运行环境,主要用于运行已编写的 Java 应用程序;而 JDK(Java Development Kit)则是一个完整的开发工具包,除了包含 JRE 外,还包括编译器 `javac` 及其他调试和分析工具[^2]。 --- #### **3. Java 中的面向对象特性有哪些?** Java 支持的主要 OOP 特性包括封装、继承、多态和抽象。这些特性使得代码更加模块化、可维护性和扩展性强。 - **封装**:隐藏内部实现细节,仅暴露必要的接口给外部调用。 - **继承**:子类可以继承父类的方法和属性,减少重复代码。 - **多态**:同一操作作用于不同对象时可能表现出不同的行为。 - **抽象**:允许定义只有方法签名而不提供具体实现的类或接口。 ```java // 示例:展示多态的应用 class Animal { public void sound() {} } class Dog extends Animal { @Override public void sound() { System.out.println("Bark"); } } class Cat extends Animal { @Override public void sound() { System.out.println("Meow"); } } public class PolymorphismExample { public static void makeSound(Animal animal) { animal.sound(); } public static void main(String[] args) { makeSound(new Dog()); // 输出 Bark makeSound(new Cat()); // 输出 Meow } } ``` --- #### **4. try-catch-finally 块中的返回值如何处理?** 当 `try` 或 `catch` 块中有 `return` 语句时,如果 `finally` 块也存在 `return` 语句,则最终会优先返回 `finally` 块的结果。这可能会覆盖掉原本的返回值[^3]。 ```java public class FinallyReturnTest { public static int testFinally() { try { return 1; } catch (Exception e) { return 2; } finally { return 3; // 返回此值 } } public static void main(String[] args) { System.out.println(testFinally()); // 输出 3 } } ``` --- #### **5. short s = 0; s += 1 是否合法?为什么?** 该表达式是合法的,因为在复合赋值运算符中,右侧计算结果会被隐式转换为目标变量的数据类型。例如,`s += 1` 等价于 `s = (short)(s + 1)`,因此不会引发编译错误[^4]。 ```java public class CompoundAssignmentTest { public static void main(String[] args) { short s = 0; s += 1; // 合法 System.out.println(s); // 输出 1 } } ``` --- #### **6. String 是基本数据类型吗?** String 不属于基本数据类型,而是引用类型。它是不可变的对象,一旦创建就不能修改其内容。每次对字符串的操作都会生成新的对象实例。 ```java public class StringImmutabilityTest { public static void main(String[] args) { String str = "hello"; str.concat(" world"); // 创建新对象,原对象未改变 System.out.println(str); // 输出 hello } } ``` --- #### **7. ArrayList 和 LinkedList 的区别是什么?** 两者都实现了 List 接口,但在底层存储方式上有显著差异: - **ArrayList** 使用动态数组作为基础容器,随机访问效率高(时间复杂度为 O(1)),但增删元素较慢(需移动后续元素)。 - **LinkedList** 利用双向链表存储节点,适合频繁插入删除场景,但随机访问性能较差(时间复杂度为 O(n))。 ```java import java.util.ArrayList; import java.util.LinkedList; public class ListComparison { public static void main(String[] args) { ArrayList<Integer> arrayList = new ArrayList<>(); LinkedList<Integer> linkedList = new LinkedList<>(); long startTime, endTime; // 测试 ArrayList 插入性能 startTime = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { arrayList.add(i); } endTime = System.currentTimeMillis(); System.out.println("ArrayList insert time: " + (endTime - startTime)); // 测试 LinkedList 插入性能 startTime = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { linkedList.add(i); } endTime = System.currentTimeMillis(); System.out.println("LinkedList insert time: " + (endTime - startTime)); } } ``` --- #### **8. HashMap 的工作原理是什么?** HashMap 是基于哈希表实现的一种集合类,用于存储键值对映射关系。它的核心逻辑如下: - 计算 key 对应的 hash 值来定位桶位置; - 如果发生碰撞(多个 key 映射到同一个 bucket 上),采用链地址法解决冲突; - 当负载因子超过阈值时触发扩容操作,重新分配所有 entry 至更大的 buckets 数组。 ```java import java.util.HashMap; public class HashMapExample { public static void main(String[] args) { HashMap<String, Integer> map = new HashMap<>(); map.put("Alice", 25); map.put("Bob", 30); System.out.println(map.get("Alice")); // 输出 25 } } ``` --- #### **9. synchronized 关键字的作用是什么?** `synchronized` 主要用来控制线程同步问题,防止多个线程同时访问共享资源而导致数据不一致的情况。它可以修饰方法或者代码块,确保任意时刻只有一个线程能够进入临界区域。 ```java public class SynchronizationExample { private int count = 0; public synchronized void increment() { count++; } public static void main(String[] args) throws InterruptedException { SynchronizationExample example = new SynchronizationExample(); Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { example.increment(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { example.increment(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(example.count); // 输出 2000 } } ``` --- #### **10. volatile 关键字的意义是什么?** volatile 保证了被标记字段的可见性,即一个线程对该变量的修改立即反映到主内存中,并通知其他线程刷新缓存副本。但它无法替代锁机制,不能保障原子性。 ```java public class VolatileExample { private volatile boolean flag = true; public static void main(String[] args) throws InterruptedException { VolatileExample example = new VolatileExample(); Thread t = new Thread(() -> { while (example.flag) {} // 自旋等待 System.out.println("Thread stopped."); }); t.start(); Thread.sleep(1000); example.flag = false; // 修改标志位 } } ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小粥的编程笔记

你的鼓励将是我创造的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值