• [问题求助] Java判断时间间隔是否超限的方法?
    Java判断时间间隔是否超限的方法?
  • [技术干货] Java Spring Boot集成Redis实战——缓存与分布式锁实现
    Redis是一款高性能的内存数据库,支持字符串、哈希、列表、集合等多种数据结构,广泛应用于缓存、分布式锁、消息队列等场景。本文基于Spring Boot框架,集成Redis,实现缓存管理(查询缓存、缓存更新、缓存失效)和分布式锁两大核心功能,结合实际业务场景编写完整代码,详解Redis在Java项目中的落地应用。一、环境准备与依赖配置1. 开发环境JDK 1.8、Spring Boot 2.7.x、Redis 6.2.x、Spring Data Redis 2.7.x、Maven 3.6.x、IDEA2. Maven依赖配置在pom.xml中引入Spring Boot Redis依赖、Spring Boot Web依赖等: &lt;dependencies&gt; <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; <!-- Spring Data Redis(Spring Boot集成Redis的核心依赖) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis&lt;/artifactId&gt; &lt;/dependency&gt; <!-- Redis客户端(默认使用Lettuce,可替换为Jedis) --> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId&gt; &lt;/dependency&gt; <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency><!-- Spring Boot Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> 3. 配置文件设置在application.yml中配置Redis连接信息(主机、端口、密码、数据库索引)、缓存配置、Lettuce客户端配置等: spring: # Redis配置 redis: host: localhost # Redis服务器地址 port: 6379 # Redis端口 password: 123456 # Redis密码(若未设置则省略) database: 0 # 连接的数据库索引(默认0) timeout: 3000ms # 连接超时时间 # Lettuce客户端配置(连接池) lettuce: pool: max-active: 8 # 连接池最大活跃连接数 max-idle: 8 # 连接池最大空闲连接数 min-idle: 2 # 连接池最小空闲连接数 max-wait: -1ms # 连接池最大阻塞等待时间(-1表示无限制) # 缓存配置(使用Redis作为缓存管理器) cache: type: redis # 缓存类型为Redis redis: time-to-live: 3600000ms # 缓存默认过期时间(1小时) cache-null-values: false # 是否缓存null值(避免缓存穿透,根据业务需求设置) key-prefix: "cache:" # 缓存key前缀 4. Redis配置类创建RedisConfig类,配置RedisTemplate(自定义序列化方式,避免key和value序列化乱码)、缓存管理器等: import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; @Configuration @EnableCaching // 开启缓存注解支持 public class RedisConfig { // 配置RedisTemplate,自定义序列化方式 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置key序列化器(String序列化,避免key乱码) StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); // 设置value序列化器(JSON序列化,支持对象序列化) GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); redisTemplate.setValueSerializer(jsonRedisSerializer); redisTemplate.setHashValueSerializer(jsonRedisSerializer); // 初始化RedisTemplate redisTemplate.afterPropertiesSet(); return redisTemplate; } // 配置缓存管理器,自定义缓存配置 @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { // 基础缓存配置(默认过期时间1小时) RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(1)) // 缓存过期时间 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); // 不缓存null值 // 创建缓存管理器 RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(defaultCacheConfig) // 可为不同缓存设置不同过期时间(如user缓存过期时间30分钟) .withCacheConfiguration("user", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.of 
  • [技术干货] Java IO流实战——文件上传下载与批量处理
    IO流是Java核心基础知识点,用于处理文件、网络数据等输入输出操作,在实际开发中应用广泛(如文件上传下载、日志读写、数据导入导出等)。本文将围绕文件上传、文件下载、批量读取文件内容三个核心场景,结合Spring Boot框架,实现完整的IO流实战案例,包含详细代码与场景说明,帮助开发者快速掌握IO流的实际应用。一、IO流核心概念梳理Java IO流按操作方向分为输入流(InputStream/Reader)和输出流(OutputStream/Writer),按操作数据类型分为字节流(InputStream/OutputStream)和字符流(Reader/Writer):1. 字节流:以字节为单位处理数据,适用于所有文件类型(如图片、视频、文档等),核心类包括FileInputStream、FileOutputStream、BufferedInputStream、BufferedOutputStream;2. 字符流:以字符为单位处理数据,适用于文本文件(如txt、java文件等),核心类包括FileReader、FileWriter、BufferedReader、BufferedWriter;3. 缓冲流:基于基础流封装,增加缓冲区功能,减少IO操作次数,提升读写效率(如BufferedInputStream比FileInputStream读写速度更快)。实际开发中,推荐使用缓冲流结合try-with-resources语法(自动关闭流资源,避免资源泄漏),简化代码并提升安全性。二、实战场景一:文件上传(单文件+多文件)文件上传是Web开发常见场景,本文基于Spring Boot+IO流实现单文件和多文件上传功能,限制文件大小和类型,将上传文件保存到本地指定目录。1. 配置文件设置在application.yml中配置文件上传相关参数(上传文件保存路径、最大文件大小、请求大小): spring: servlet: multipart: max-file-size: 10MB # 单个文件最大大小 max-request-size: 50MB # 单次请求最大文件大小 # 自定义文件上传路径(Windows系统可改为D:/upload/files) file: upload: path: /Users/xxx/upload/files/ 2. 工具类:文件上传工具创建FileUploadUtils类,封装文件上传核心逻辑(校验文件类型、生成唯一文件名、写入文件到本地): import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.util.UUID; @Component public class FileUploadUtils { // 注入文件上传保存路径 @Value("${file.upload.path}") private String uploadPath; // 允许上传的文件类型(后缀名) private static final String[] ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "txt", "doc", "docx", "pdf"}; // 校验文件类型是否合法 private boolean isAllowedExtension(String originalFilename) { if (originalFilename == null || originalFilename.isEmpty()) { return false; } // 获取文件后缀名 String extension = getFileExtension(originalFilename); for (String allowedExtension : ALLOWED_EXTENSIONS) { if (allowedExtension.equalsIgnoreCase(extension)) { return true; } } return false; } // 获取文件后缀名 private String getFileExtension(String originalFilename) { int lastIndexOfDot = originalFilename.lastIndexOf("."); if (lastIndexOfDot == -1) { return ""; } return originalFilename.substring(lastIndexOfDot + 1); } // 生成唯一文件名(避免文件名重复覆盖) private String generateUniqueFilename(String originalFilename) { String extension = getFileExtension(originalFilename); return UUID.randomUUID().toString() + "." + extension; } // 单文件上传 public String uploadFile(MultipartFile file) throws IOException { // 校验文件是否为空 if (file.isEmpty()) { throw new IOException("上传文件不能为空"); } // 校验文件类型 String originalFilename = file.getOriginalFilename(); if (!isAllowedExtension(originalFilename)) { throw new IOException("不允许上传该类型文件,允许的类型:" + String.join(",", ALLOWED_EXTENSIONS)); } // 生成唯一文件名 String uniqueFilename = generateUniqueFilename(originalFilename); // 创建上传目录(若目录不存在则创建) File uploadDir = new File(uploadPath); if (!uploadDir.exists()) { boolean mkdirs = uploadDir.mkdirs(); if (!mkdirs) { throw new IOException("创建上传目录失败"); } } // 拼接文件完整路径 String filePath = uploadPath + File.separator + uniqueFilename; // 使用IO流将文件写入本地(try-with-resources自动关闭流) try (InputStream inputStream = file.getInputStream(); OutputStream outputStream = new FileOutputStream(filePath); BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) { byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区 int len; while ((len = bufferedInputStream.read(buffer)) != -1) { bufferedOutputStream.write(buffer, 0, len); } // 刷新缓冲区,确保数据全部写入 bufferedOutputStream.flush(); } // 返回文件保存路径(可存储到数据库,用于后续下载) return filePath; } // 多文件上传(批量上传) public String[] uploadFiles(MultipartFile[] files) throws IOException { if (files == null || files.length == 0) { throw new IOException("上传文件不能为空"); } String[] filePaths = new String[files.length]; for (int i = 0; i < files.length; i++) { MultipartFile file = files[i]; // 调用单文件上传方法 String filePath = uploadFile(file); filePaths[i] = filePath; } return filePaths; } } 3. 控制器:文件上传接口创建FileUploadController,提供单文件和多文件上传接口: import com.example.iodemo.utils.FileUploadUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @RestController @RequestMapping("/api/file") public class FileUploadController { @Autowired private FileUploadUtils fileUploadUtils; // 单文件上传 @PostMapping("/upload/single") public String uploadSingleFile(@RequestParam("file") MultipartFile file) { try { String filePath = fileUploadUtils.uploadFile(file); return "单文件上传成功,文件保存路径:" + filePath; } catch (IOException e) { return "单文件上传失败:" + e.getMessage(); } } // 多文件上传 @PostMapping("/upload/multiple") public String uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) { try { String[] filePaths = fileUploadUtils.uploadFiles(files); return "多文件上传成功,文件保存路径:" + String.join(";", filePaths); } catch (IOException e) { return "多文件上传失败:" + e.getMessage(); } } } 三、实战场景二:文件下载文件下载功能通过IO流读取本地文件,将文件数据写入响应流,返回给前端。本文实现根据文件路径下载文件,并支持设置下载文件名。1. 工具类:文件下载工具创建FileDownloadUtils类,封装文件下载核心逻辑(校验文件是否存在、读取文件数据、写入响应流): import javax.servlet.http.HttpServletResponse; import java.io.*; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; public class FileDownloadUtils { // 文件下载核心方法 public static void downloadFile(String filePath, String downloadFilename, HttpServletResponse response) throws IOException { // 校验文件路径 File file = new File(filePath); if (!file.exists()) { throw new FileNotFoundException("文件不存在,路径:" + filePath); } if (!file.isFile()) { throw new IOException("路径对应的不是文件:" + filePath); } // 设置响应头,告知浏览器以附件形式下载 response.setContentType("application/octet-stream"); response.setContentLength((int) file.length()); // 编码文件名,避免中文乱码 String encodedFilename = URLEncoder.encode(downloadFilename, StandardCharsets.UTF_8.name()); response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFilename + "\""); // 使用IO流读取文件并写入响应流(try-with-resources自动关闭流) try (InputStream inputStream = new FileInputStream(file); BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); OutputStream outputStream = response.getOutputStream(); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) { byte[] buffer = new byte[1024 * 8]; int len; while ((len = bufferedInputStream.read(buffer)) != -1) { bufferedOutputStream.write(buffer, 0, len); } bufferedOutputStream.flush(); } } } 2. 控制器:文件下载接口创建FileDownloadController,提供文件下载接口,接收文件路径和下载文件名参数: import com.example.iodemo.utils.FileDownloadUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @RestController @RequestMapping("/api/file") public class FileDownloadController { @GetMapping("/download") public void downloadFile( @RequestParam("filePath") String filePath, @RequestParam("filename") String filename, HttpServletResponse response) { try { // 调用文件下载工具类 FileDownloadUtils.downloadFile(filePath, filename, response); } catch (IOException e) { // 处理异常,设置响应状态 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); try { response.getWriter().write("文件下载失败:" + e.getMessage()); } catch (IOException ex) { ex.printStackTrace(); } } } } 四、实战场景三:批量读取文本文件内容批量读取文本文件内容是数据处理常见场景(如读取日志文件、导入CSV数据等),本文实现批量读取指定目录下所有txt文件的内容,并统计文件总数量和总字符数。1. 工具类:文件读取工具创建FileReadUtils类,封装批量读取文本文件核心逻辑(遍历目录下文件、读取文件内容、统计数据): import java.io.*; import java.util.ArrayList; import java.util.List; public class FileReadUtils { // 批量读取指定目录下所有txt文件内容 public static FileReadResult batchReadTxtFiles(String dirPath) throws IOException { File dir = new File(dirPath); if (!dir.exists()) { throw new FileNotFoundException("目录不存在:" + dirPath); } if (!dir.isDirectory()) { throw new IOException("路径对应的不是目录:" + dirPath); } // 存储所有文件内容 List<String> allFileContents = new ArrayList<>(); // 统计文件总数量和总字符数 int fileCount = 0; long totalCharCount = 0; // 遍历目录下所有文件 File[] files = dir.listFiles(); if (files == null) { throw new IOException("读取目录文件失败:" + dirPath); } for (File file : files) { // 只处理txt文件 if (file.isFile() && file.getName().endsWith(".txt")) { fileCount++; // 读取单个txt文件内容 String fileContent = readSingleTxtFile(file); allFileContents.add(fileContent); // 统计字符数(去除空格和换行符) totalCharCount += fileContent.replaceAll("\\s+", "").length(); } } // 返回读取结果 return new FileReadResult(fileCount, totalCharCount, allFileContents); } // 读取单个txt文件内容(字符流,避免中文乱码) private static String readSingleTxtFile(File file) throws IOException { StringBuilder contentBuilder = new StringBuilder(); // 使用BufferedReader读取文本文件,指定编码为UTF-8 try (Reader reader = new FileReader(file, java.nio.charset.StandardCharsets.UTF_8); BufferedReader bufferedReader = new BufferedReader(reader)) { String line; while ((line = bufferedReader.readLine()) != null) { contentBuilder.append(line).append("\n"); } } return contentBuilder.toString(); } // 定义文件读取结果实体类 public static class FileReadResult { private int fileCount; // 文件总数量 private long totalCharCount; // 总字符数(去除空格和换行) private List<String> allFileContents; // 所有文件内容 public FileReadResult(int fileCount, long totalCharCount, List<String> allFileContents) { this.fileCount = fileCount; this.totalCharCount = totalCharCount; this.allFileContents = allFileContents; } // getter和setter方法 public int getFileCount() { return fileCount; } public void setFileCount(int fileCount) { this.fileCount = fileCount; } public long getTotalCharCount() { return totalCharCount; } public void setTotalCharCount(long totalCharCount) { this.totalCharCount = totalCharCount; } public List<String> getAllFileContents() { return allFileContents; } public void setAllFileContents(List<String> allFileContents) { this.allFileContents = allFileContents; } } } 2. 控制器:文件读取接口创建FileReadController,提供批量读取文本文件接口: import com.example.iodemo.utils.FileReadUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; @RestController @RequestMapping("/api/file") public class FileReadController { @GetMapping("/read/batch") public FileReadUtils.FileReadResult batchReadTxtFiles(@RequestParam("dirPath") String dirPath) { try { return FileReadUtils.batchReadTxtFiles(dirPath); } catch (IOException e) { throw new RuntimeException("批量读取文件失败:" + e.getMessage()); } } } 五、核心注意事项1. 资源关闭:IO流属于稀缺资源,必须确保关闭,推荐使用try-with-resources语法(JDK7+),自动关闭实现AutoCloseable接口的资源;2. 编码问题:读取文本文件时需指定编码(如UTF-8),避免中文乱码;文件下载时需对文件名进行URLEncoder编码,适配不同浏览器;3. 缓冲区优化:使用缓冲流(BufferedInputStream/BufferedOutputStream等),设置合适的缓冲区大小(一般8KB或16KB),减少IO操作次数,提升性能;4. 异常处理:IO操作易出现异常(如文件不存在、权限不足等),需捕获异常并友好提示,避免程序崩溃;5. 文件大小限制:文件上传时需限制文件大小,避免超大文件占用过多服务器资源,可通过Spring Boot配置或手动校验实现。本文通过三个核心场景实现了IO流的实际应用,代码可直接落地到文件管理、数据导入导出等业务场景,帮助开发者快速掌握IO流的使用技巧与优化方法。
  • [技术干货] Java Spring Boot集成MyBatis-Plus实战——快速实现CRUD与分页查询
    MyBatis-Plus是MyBatis的增强工具,在MyBatis基础上简化了CRUD操作,提供了分页插件、条件构造器、代码生成器等强大功能,能够大幅减少开发工作量。本文将基于Spring Boot框架,集成MyBatis-Plus,实现一个用户管理系统,覆盖实体类映射、CRUD操作、分页查询、条件查询等核心功能,包含完整代码与详细说明。一、环境准备与依赖配置1. 开发环境JDK 1.8、Spring Boot 2.7.x、MyBatis-Plus 3.5.x、MySQL 8.0、Maven 3.6.x、IDEA2. Maven依赖配置在pom.xml中引入Spring Boot Web、MyBatis-Plus、MySQL驱动等依赖: &lt;dependencies&gt; <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency><!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime&lt;/scope&gt; &lt;/dependency&gt; <!-- Lombok(简化实体类编写) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional&gt; &lt;/dependency&gt; <!-- Spring Boot Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> 3. 配置文件设置在application.yml中配置数据库连接信息、MyBatis-Plus相关配置(如Mapper扫描路径、日志打印): spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatis_plus_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false username: root password: root # MyBatis-Plus配置 mybatis-plus: # Mapper接口扫描路径 mapper-locations: classpath:mapper/**/*.xml # 实体类扫描路径(可选,若使用@TableName注解可省略) type-aliases-package: com.example.mybatisplusdemo.entity # 日志打印(开发环境开启,方便调试) configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 数据库表下划线转驼峰(默认开启,可省略) map-underscore-to-camel-case: true 二、数据库表设计创建用户表(sys_user),包含id、username、password、nickname、age、email、create_time、update_time等字段,SQL语句如下: CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(加密存储)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `age` int DEFAULT NULL COMMENT '年龄', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; 三、核心代码实现1. 实体类(Entity)创建SysUser实体类,使用Lombok的@Data注解简化getter/setter方法,使用MyBatis-Plus的@TableName、@TableId、@TableField等注解实现实体类与数据库表的映射: import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("sys_user") // 关联数据库表名 public class SysUser { // 主键ID,使用雪花算法自动生成(MyBatis-Plus默认策略) @TableId(type = IdType.AUTO) private Long id; // 用户名,对应表中username字段(若字段名与属性名一致,可省略@TableField) @TableField("username") private String username; // 密码 private String password; // 昵称 private String nickname; // 年龄 private Integer age; // 邮箱 private String email; // 创建时间,自动填充(新增时触发) @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; // 更新时间,自动填充(新增和修改时触发) @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; } 2. 自动填充处理器针对createTime和updateTime字段,实现MyBatis-Plus的元对象处理器,实现字段自动填充(无需手动设置时间): import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Component public class MyMetaObjectHandler implements MetaObjectHandler { // 新增时自动填充 @Override public void insertFill(MetaObject metaObject) { // 填充createTime字段 this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 填充updateTime字段 this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } // 修改时自动填充 @Override public void updateFill(MetaObject metaObject) { // 填充updateTime字段 this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } } 3. Mapper接口创建SysUserMapper接口,继承MyBatis-Plus的BaseMapper接口,BaseMapper已封装了CRUD核心方法,无需编写XML即可实现基础操作: import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mybatisplusdemo.entity.SysUser; import org.apache.ibatis.annotations.Mapper; // @Mapper注解:标识该接口为MyBatis Mapper接口,Spring Boot会自动扫描 @Mapper public interface SysUserMapper extends BaseMapper<SysUser> { // 基础CRUD方法已由BaseMapper提供,如需自定义SQL可在此添加方法 } 4. 服务层(Service)创建SysUserService接口及其实现类,继承MyBatis-Plus的IService和ServiceImpl,ServiceImpl已实现IService接口的核心方法,可直接使用或重写。 // 服务接口 import com.baomidou.mybatisplus.extension.service.IService; import com.example.mybatisplusdemo.entity.SysUser; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; public interface SysUserService extends IService<SysUser> { // 分页查询用户 IPage<SysUser> pageQuery(IPage<SysUser> page, QueryWrapper<SysUser> queryWrapper); // 根据用户名查询用户(自定义方法) SysUser getByUsername(String username); } // 服务实现类 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.mybatisplusdemo.entity.SysUser; import com.example.mybatisplusdemo.mapper.SysUserMapper; import org.springframework.stereotype.Service; @Service public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService { @Override public IPage<SysUser> pageQuery(IPage<SysUser> page, QueryWrapper<SysUser> queryWrapper) { // 调用BaseMapper的selectPage方法实现分页查询 return baseMapper.selectPage(page, queryWrapper); } @Override public SysUser getByUsername(String username) { // 使用QueryWrapper构建查询条件 QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); // 调用BaseMapper的selectOne方法查询单个结果 return baseMapper.selectOne(queryWrapper); } } 5. 控制器(Controller)创建SysUserController,提供RESTful接口,实现用户的新增、修改、删除、查询、分页查询等功能: import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.mybatisplusdemo.entity.SysUser; import com.example.mybatisplusdemo.service.SysUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/user") public class SysUserController { @Autowired private SysUserService sysUserService; // 新增用户 @PostMapping public String addUser(@RequestBody SysUser sysUser) { boolean save = sysUserService.save(sysUser); return save ? "新增用户成功" : "新增用户失败"; } // 修改用户 @PutMapping("/{id}") public String updateUser(@PathVariable Long id, @RequestBody SysUser sysUser) { sysUser.setId(id); boolean update = sysUserService.updateById(sysUser); return update ? "修改用户成功" : "修改用户失败"; } // 删除用户(逻辑删除可配置,本文为物理删除) @DeleteMapping("/{id}") public String deleteUser(@PathVariable Long id) { boolean remove = sysUserService.removeById(id); return remove ? "删除用户成功" : "删除用户失败"; } // 根据ID查询用户 @GetMapping("/{id}") public SysUser getUserById(@PathVariable Long id) { return sysUserService.getById(id); } // 查询所有用户 @GetMapping("/list") public List<SysUser> getAllUser() { return sysUserService.list(); } // 分页查询用户(支持条件查询,如按昵称、年龄筛选) @GetMapping("/page") public IPage<SysUser> pageUser( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String nickname, @RequestParam(required = false) Integer age) { // 构建分页对象 IPage<SysUser> page = new Page<>(pageNum, pageSize); // 构建查询条件 QueryWrapper<SysUser> queryWrapper = Wrappers.query(); if (nickname != null && !nickname.isEmpty()) { queryWrapper.like("nickname", nickname); // 模糊查询 } if (age != null) { queryWrapper.eq("age", age); // 精确查询 } // 调用分页查询方法 return sysUserService.pageQuery(page, queryWrapper); } // 根据用户名查询用户 @GetMapping("/username/{username}") public SysUser getUserByUsername(@PathVariable String username) { return sysUserService.getByUsername(username); } } 6. 分页插件配置MyBatis-Plus的分页功能需要配置分页插件,创建MyBatisPlusConfig类: import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } } 四、接口测试与效果验证启动Spring Boot应用,使用Postman或浏览器调用接口进行测试:1. 新增用户:POST /api/user,请求体为JSON格式用户数据,返回“新增用户成功”即表示成功;2. 分页查询:GET /api/user/page?pageNum=1&pageSize=5&nickname=张,返回包含总条数、总页数、当前页数据的分页结果;3. 修改用户:PUT /api/user/1,请求体为修改后的用户数据,返回“修改用户成功”;4. 删除用户:DELETE /api/user/1,返回“删除用户成功”;5. 根据用户名查询:GET /api/user/username/admin,返回对应用户名的用户信息。五、核心功能总结本文基于Spring Boot+MyBatis-Plus实现了用户管理系统,核心优势的在于:1. 简化CRUD操作:通过继承BaseMapper和IService,无需编写大量XML和SQL语句,即可实现基础增删改查;2. 强大的条件构造器:QueryWrapper支持多条件组合查询(精确、模糊、范围等),无需手动拼接SQL;3. 便捷的分页功能:配置分页插件后,只需传入分页参数即可实现分页查询,无需手动处理分页逻辑;4. 自动填充功能:通过元对象处理器,实现创建时间、更新时间等字段的自动填充,减少重复代码。MyBatis-Plus还提供了代码生成器、逻辑删除、乐观锁等功能,可根据业务需求进一步拓展,大幅提升Java项目的开发效率。
  • [技术干货] Java并发编程之线程池实战——从原理到落地
    在Java开发中,并发场景无处不在,线程池作为管理线程的核心组件,能够有效避免频繁创建和销毁线程带来的性能损耗,提升系统并发处理能力。本文将从线程池核心原理出发,结合实际业务场景,实现一个基于线程池的异步任务处理系统,并详解核心代码与优化要点。一、线程池核心原理梳理Java线程池核心类为ThreadPoolExecutor,其构造方法包含7个核心参数,决定了线程池的运行机制: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { // 构造逻辑省略 } 各参数核心作用:corePoolSize:核心线程数,线程池长期保留的线程数量,即使处于空闲状态也不会销毁(除非设置allowCoreThreadTimeOut为true);maximumPoolSize:最大线程数,线程池允许创建的最大线程数量,当任务队列满且核心线程都在工作时,会创建临时线程直至达到该数量;keepAliveTime:临时线程空闲存活时间,超过该时间未处理任务则销毁临时线程;workQueue:任务阻塞队列,用于存储等待执行的任务,常见实现有ArrayBlockingQueue、LinkedBlockingQueue等;threadFactory:线程创建工厂,用于自定义线程名称、优先级等属性;handler:拒绝策略,当线程池与任务队列都满时,对新提交任务的处理方式,常见策略有AbortPolicy(直接抛异常)、CallerRunsPolicy(调用者线程执行)等。线程池核心运行流程:提交任务时,先判断核心线程是否空闲,若空闲则直接执行;若核心线程已满,将任务加入阻塞队列;若队列已满,创建临时线程执行任务;若临时线程达到最大线程数,触发拒绝策略。二、实际业务场景:异步任务处理系统假设业务需求:电商平台订单支付成功后,需要异步执行一系列后续任务(发送支付成功短信、更新订单状态、同步库存、生成交易日志),要求这些任务并行执行,且不影响支付接口的响应速度。此时使用线程池实现异步处理,既能提升接口性能,又能保证任务有序可控。三、完整代码实现1. 线程池配置类通过@Configuration注解创建线程池实例,自定义线程工厂和拒绝策略,适配业务场景需求(核心线程数8,最大线程数16,队列容量100,临时线程空闲时间30秒)。 import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; @Configuration public class ThreadPoolConfig { // 核心线程数 private static final int CORE_POOL_SIZE = 8; // 最大线程数 private static final int MAX_POOL_SIZE = 16; // 队列容量 private static final int QUEUE_CAPACITY = 100; // 临时线程空闲时间(秒) private static final long KEEP_ALIVE_TIME = 30; @Bean("orderThreadPool") public ThreadPoolExecutor orderThreadPool() { // 自定义线程工厂,设置线程名称前缀 ThreadFactory threadFactory = new ThreadFactory() { private final AtomicInteger threadNum = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("order-handle-thread-" + threadNum.getAndIncrement()); thread.setPriority(Thread.NORM_PRIORITY); return thread; } }; // 拒绝策略:当线程池和队列都满时,由调用者线程执行任务,避免任务丢失 RejectedExecutionHandler rejectedHandler = new ThreadPoolExecutor.CallerRunsPolicy(); // 创建线程池实例 return new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new LinkedBlockingQueue<>(QUEUE_CAPACITY), threadFactory, rejectedHandler ); } } 2. 任务接口与实现类定义任务接口OrderTask,包含各异步任务的执行方法,分别实现短信发送、订单状态更新、库存同步、日志生成功能。 import org.springframework.stereotype.Component; // 任务接口 public interface OrderTask { // 发送支付成功短信 void sendPaySms(Long orderId, String phone); // 更新订单状态为已支付 void updateOrderStatus(Long orderId); // 同步库存(减少对应商品库存) void syncStock(Long orderId); // 生成交易日志 void generateTradeLog(Long orderId, BigDecimal amount); } // 任务实现类 @Component public class OrderTaskImpl implements OrderTask { @Override public void sendPaySms(Long orderId, String phone) { // 模拟短信发送耗时操作 try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("订单" + orderId + ":向手机号" + phone + "发送支付成功短信"); } @Override public void updateOrderStatus(Long orderId) { // 模拟数据库操作耗时 try { Thread.sleep(150); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("订单" + orderId + ":状态更新为已支付"); } @Override public void syncStock(Long orderId) { // 模拟库存同步耗时 try { Thread.sleep(300); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("订单" + orderId + ":库存同步完成"); } @Override public void generateTradeLog(Long orderId, BigDecimal amount) { // 模拟日志生成耗时 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("订单" + orderId + ":交易日志生成完成,金额:" + amount); } } 3. 业务服务类创建OrderService类,在支付成功方法中,通过线程池提交异步任务,实现多任务并行执行。 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.concurrent.ThreadPoolExecutor; @Service public class OrderService { @Autowired private OrderTask orderTask; @Autowired @Qualifier("orderThreadPool") private ThreadPoolExecutor threadPoolExecutor; // 支付成功处理方法 public void paySuccess(Long orderId, String phone, BigDecimal amount) { // 提交异步任务:发送短信 threadPoolExecutor.submit(() -> orderTask.sendPaySms(orderId, phone)); // 提交异步任务:更新订单状态 threadPoolExecutor.submit(() -> orderTask.updateOrderStatus(orderId)); // 提交异步任务:同步库存 threadPoolExecutor.submit(() -> orderTask.syncStock(orderId)); // 提交异步任务:生成交易日志 threadPoolExecutor.submit(() -> orderTask.generateTradeLog(orderId, amount)); // 支付接口直接返回成功,无需等待异步任务完成 System.out.println("订单" + orderId + ":支付成功,异步任务已提交"); } } 4. 测试类编写测试类,模拟订单支付成功场景,验证线程池异步任务执行效果。 import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.math.BigDecimal; @SpringBootTest @RunWith(SpringRunner.class) public class OrderServiceTest { @Autowired private OrderService orderService; @Test public void testPaySuccess() { // 模拟订单数据 Long orderId = 10001L; String phone = "13800138000"; BigDecimal amount = new BigDecimal("999.00"); // 调用支付成功方法 orderService.paySuccess(orderId, phone, amount); // 等待异步任务执行完成(测试用,实际业务无需此操作) try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } 四、核心优化与注意事项1. 线程池参数优化:核心线程数和最大线程数需根据CPU核心数和业务并发量调整,一般核心线程数=CPU核心数+1,最大线程数=2*CPU核心数+1;队列容量需避免过大(导致任务堆积)或过小(频繁触发拒绝策略)。2. 任务异常处理:异步任务中若发生异常,线程池会直接销毁该线程且不抛出异常,需在任务中捕获异常并处理(如记录日志、重试机制),避免线程泄漏。3. 线程池监控:可通过ThreadPoolExecutor的getActiveCount()、getQueue().size()等方法监控线程池运行状态,及时发现任务堆积、线程泄漏等问题。4. 避免使用Executors默认线程池:Executors创建的线程池(如FixedThreadPool、CachedThreadPool)存在队列无界、线程数无上限等问题,易导致OOM,推荐自定义ThreadPoolExecutor。本文通过线程池实现了异步任务处理系统,核心代码可直接落地到电商、支付等并发场景,有效提升系统性能和稳定性。
  • [技术干货] 删除链表中重复的节点
    题⽬描述在⼀个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5示例1输⼊:{1,2,3,3,4,4,5}返回值:{1,2,5}思路及解答hash统计第一次遍历统计频率,第二次遍历删除重复节点javaimport java.util.HashMap;public class Solution { public ListNode deleteDuplication(ListNode head) { if (head == null || head.next == null) { return head; } // 第一次遍历:统计每个节点值出现的次数 HashMap<Integer, Integer> countMap = new HashMap<>(); ListNode current = head; while (current != null) { countMap.put(current.val, countMap.getOrDefault(current.val, 0) + 1); current = current.next; } // 第二次遍历:删除重复节点 ListNode dummy = new ListNode(-1); // 哑节点简化边界处理 dummy.next = head; ListNode prev = dummy; current = head; while (current != null) { if (countMap.get(current.val) > 1) { // 当前节点重复,跳过 prev.next = current.next; } else { // 当前节点不重复,移动prev指针 prev = prev.next; } current = current.next; } return dummy.next; }}时间复杂度:O(n)空间复杂度:O(n)直接遍历(推荐)注意,题目已经提到是排序的节点,那么就可以直接原地删除对⽐前后两个元素,如果相同的情况下,接着遍历后⾯的元素,直到元素不相等的时候,将前⾯的指针指向最后⼀个相同的元素的后⾯,相当于跳过了相同的元素。javapublic class Solution { public ListNode deleteDuplication(ListNode pHead) { //遍历链表,直接删除 if(pHead == null || pHead.next == null) return pHead; ListNode head = new ListNode(0); head.next = pHead; ListNode cur = head.next; ListNode pre = head; while(cur != null){ //将重复的结点都遍历过,然后将后面节点复制给pre结点后面 if(cur.next != null && cur.val == cur.next.val){ while(cur.next != null && cur.val == cur.next.val){ cur = cur.next; } pre.next = cur.next; cur = cur.next; }else{ pre = pre.next; cur = cur.next; } } return head.next; }}空间复杂度为 O(1) ,没有借助额外的空间时间复杂度为 O(n) ,只遍历了⼀次链表递归将大问题分解为当前节点+剩余链表的子问题java/** * 递归法:分治思想解决子问题 * 思路:将大问题分解为当前节点+剩余链表的子问题 * */public class Solution { public ListNode deleteDuplication(ListNode head) { // 递归终止条件:空链表或单节点链表 if (head == null || head.next == null) { return head; } // 情况1:当前节点与下一节点重复 if (head.val == head.next.val) { // 跳过所有重复节点,找到第一个不重复的节点 ListNode node = head.next; while (node != null && head.val == node.val) { node = node.next; } // 递归处理剩余部分 return deleteDuplication(node); } // 情况2:当前节点不重复 else { head.next = deleteDuplication(head.next); return head; } }}时间复杂度:O(n)空间复杂度:O(n) ,递归栈空间三指针法使用pre、cur、next三个指针精确控制删除范围javapublic class Solution { public ListNode deleteDuplication(ListNode head) { if (head == null || head.next == null) { return head; } ListNode dummy = new ListNode(-1); dummy.next = head; ListNode pre = dummy; // 前驱指针 ListNode cur = head; // 当前指针 ListNode next = null; // 后继指针 while (cur != null && cur.next != null) { next = cur.next; // 发现重复节点 if (cur.val == next.val) { // 移动next直到找到不重复的节点 while (next != null && cur.val == next.val) { next = next.next; } // 跳过所有重复节点 pre.next = next; cur = next; } // 没有重复,正常移动指针 else { pre = cur; cur = cur.next; } } return dummy.next; }}时间复杂度:O(n)空间复杂度:O(1)转载自https://www.cnblogs.com/sevencoding/p/19411014
  • [技术干货] ⼆叉搜索树的第k个结点
    题⽬描述给定⼀棵⼆叉搜索树,请找出其中的第 k ⼩的 TreeNode 结点。示例1输⼊:{5,3,7,2,4,6,8},3返回值:{4}思路及解答二叉搜索树的关键性质二叉搜索树具有一个重要特性:中序遍历(左-根-右)BST会得到一个升序排列的节点值序列。因此,寻找第k小的节点本质上就是获取中序遍历序列中的第k个元素。理解这一点是掌握所有解法的基石。递归中序遍历(直观版)算法思路:进行递归中序遍历将遍历到的节点值依次加入一个列表。遍历完成后,列表中的元素就是升序排列的。从列表中取出第k-1个元素(索引从0开始)即为答案。javaclass TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; }}public class Solution { public int kthSmallest(TreeNode root, int k) { // 用于存储中序遍历结果的列表 List<Integer> inorderList = new ArrayList<>(); // 执行中序遍历 inorderTraversal(root, inorderList); // 返回第k小的元素(列表索引从0开始,所以是k-1) return inorderList.get(k - 1); } /** * 递归中序遍历二叉树 * @param node 当前遍历的节点 * @param list 存储遍历结果的列表 */ private void inorderTraversal(TreeNode node, List<Integer> list) { if (node == null) { return; // 递归终止条件:遇到空节点则返回 } inorderTraversal(node.left, list); // 递归遍历左子树 list.add(node.val); // 访问当前节点,将值加入列表 inorderTraversal(node.right, list); // 递归遍历右子树 }}时间复杂度:O(n)。需要遍历树中的所有n个节点。空间复杂度:O(n)。主要取决于递归调用栈的深度(最坏情况为O(n),树退化成链表)和存储遍历结果的列表(O(n))。迭代中序遍历(提前终止)方法一需要遍历完整棵树,即使答案在很早就已确定。我们可以利用迭代中序遍历实现提前终止,找到第k小的节点后立即返回,提升效率。算法思路:使用一个栈来模拟递归过程。从根节点开始,将所有左子节点压入栈,直到最左边的节点。弹出栈顶元素,这将是当前最小的节点。每弹出一个节点,计数器k减1。当k减到0时,当前节点就是第k小的节点,直接返回。如果k不为0,则转向当前节点的右子树,重复步骤2-4。javapublic class Solution { public int kthSmallest(TreeNode root, int k) { Deque<TreeNode> stack = new LinkedList<>(); TreeNode current = root; while (current != null || !stack.isEmpty()) { // 将当前节点及其所有左子节点压入栈 while (current != null) { stack.push(current); current = current.left; } // 弹出栈顶节点,即当前最小的节点 current = stack.pop(); k--; // 计数器减1 // 如果k减到0,说明找到了第k小的节点 if (k == 0) { return current.val; } // 转向右子树 current = current.right; } // 如果k超出节点总数,返回-1(根据题目保证k有效,此情况可不处理) return -1; }}时间复杂度:最坏情况O(n)(当k=n时仍需遍历大部分节点),平均情况优于O(n),因为可能提前返回。空间复杂度:O(h),其中h是树的高度。栈的深度最大为树高,在平衡BST中为O(log n)。记录子节点数的递归(进阶优化)如果BST结构频繁变动(插入、删除),但需要频繁查询第k小的值,前两种方法每次查询都可能需要O(n)时间。我们可以通过扩展树节点结构,记录以每个节点为根的子树中的节点个数,来优化查询效率。算法思路:修改树节点结构,增加一个字段(如size)表示以该节点为根的子树的总节点数。在插入、删除节点时,维护每个节点的size信息。查询第k小的节点时:从根节点开始。计算左子树的节点数leftSize。如果k <= leftSize,说明目标节点在左子树,递归地在左子树中寻找第k小的节点。如果k == leftSize + 1,说明当前根节点就是目标节点。如果k > leftSize + 1,说明目标节点在右子树,递归地在右子树中寻找第k - (leftSize + 1)小的节点。javaclass TreeNodeWithSize { int val; TreeNodeWithSize left; TreeNodeWithSize right; int size; // 以该节点为根的子树包含的节点总数 TreeNodeWithSize(int x) { val = x; size = 1; // 初始时只有自身 } // 假设插入操作会更新size,这里省略具体的树结构维护代码}public class Solution { public int kthSmallest(TreeNodeWithSize root, int k) { if (root == null) { return -1; } // 计算左子树的节点数(如果左子树为空,则节点数为0) int leftSize = (root.left != null) ? root.left.size : 0; if (k <= leftSize) { // 第k小的节点在左子树 return kthSmallest(root.left, k); } else if (k == leftSize + 1) { // 当前节点就是第k小的节点 return root.val; } else { // 第k小的节点在右子树,在右子树中寻找第 (k - (leftSize + 1)) 小的节点 return kthSmallest(root.right, k - (leftSize + 1)); } }}转载自https://www.cnblogs.com/sevencoding/p/19468628
  • [技术干货] InheritableThreadLocal,从入门到放弃
    InheritableThreadLocal相比ThreadLocal多一个能力:在创建子线程Thread时,子线程Thread会自动继承父线程的InheritableThreadLocal信息到子线程中,进而实现在在子线程获取父线程的InheritableThreadLocal值的目的。关于ThreadLocal详细内容,可以看这篇文章:史上最全ThreadLocal 详解和 ThreadLocal 的区别举个简单的栗子对比下InheritableThreadLocal和ThreadLocal:javapublic class InheritableThreadLocalTest {     private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();     private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();     public static void main(String[] args) {        testThreadLocal();        testInheritableThreadLocal();    }     /**     * threadLocal测试     */     public static void testThreadLocal() {         // 在主线程中设置值到threadLocal         threadLocal.set("我是父线程threadLocal的值");          // 创建一个新线程并启动          new Thread(() -> {              // 在子线程里面无法获取到父线程设置的threadLocal,结果为null             System.out.println("从子线程获取到threadLocal的值: " + threadLocal.get());        }  ).start();     }       /**     * inheritableThreadLocal测试     */  public static void testInheritableThreadLocal() {         // 在主线程中设置一个值到inheritableThreadLocal        inheritableThreadLocal.set("我是父线程inheritableThreadLocal的值");         // 创建一个新线程并启动         new Thread(() -> {             // 在子线程里面可以自动获取到父线程设置的inheritableThreadLocal    System.out.println("从子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get());        }).start();    } }执行结果:text从子线程获取到threadLocal的值:null从子线程获取到inheritableThreadLocal的值:我是父线程inheritableThreadLocal的值可以看到子线程中可以获取到父线程设置的inheritableThreadLocal值,但不能获取到父线程设置的threadLocal值实现原理InheritableThreadLocal 的实现原理相当精妙,它通过在创建子线程的瞬间,“复制”父线程的线程局部变量,从而实现了数据从父线程到子线程的一次性、创建时的传递 。其核心工作原理可以清晰地通过以下序列图展示,它描绘了当父线程创建一个子线程时,数据是如何被传递的:子线程ThreadLocalMapInheritableThreadLocalThread构造方法父线程子线程ThreadLocalMapInheritableThreadLocalThread构造方法父线程关键步骤:初始化检查父线程的 inheritableThreadLocalsloop[遍历父线程Map中的每个Entry]子线程拥有父线程变量的副本创建 new Thread()调用 init() 方法createInheritedMap(parent.inheritableThreadLocals)新建一个ThreadLocalMap调用 key.childValue(parentValue)返回子线程初始值(默认返回父值,可重写)将 (key, value) 放入新Map返回新的ThreadLocalMap对象将新Map赋给子线程的inheritableThreadLocals属性下面我们来详细拆解图中的关键环节。核心实现机制**数据结构基础:Thread类内部维护了两个 ThreadLocalMap类型的变量 :threadLocals:用于存储普通 ThreadLocal设置的变量副本。inheritableThreadLocals:专门用于存储 InheritableThreadLocal设置的变量副本 。InheritableThreadLocal通过重写 getMap和 createMap方法,使其所有操作都针对 inheritableThreadLocals字段,从而与普通 ThreadLocal分离开 。继承触发时刻:子线程的创建。继承行为发生在子线程被创建(即执行 new Thread())时。在 Thread类的 init方法中,如果判断需要继承(inheritThreadLocals参数为 true)且父线程(当前线程)的 inheritableThreadLocals不为 null,则会执行复制逻辑 。复制过程的核心:createInheritedMap。这是实现复制的核心方法 。它会创建一个新的 ThreadLocalMap,并将父线程 inheritableThreadLocals中的所有条目遍历拷贝到新 Map 中。Key的复制:Key(即 InheritableThreadLocal对象本身)是直接复制的引用。Value的生成:Value 并非直接复制引用,而是通过调用 InheritableThreadLocal的 childValue(T parentValue)方法来生成子线程中的初始值。默认实现是直接返回父值(return parentValue;),这意味着对于对象类型,父子线程将共享同一个对象引用 。关键特性与注意事项创建时复制,后续独立:继承只发生一次,即在子线程对象创建的瞬间。此后,父线程和子线程对各自 InheritableThreadLocal变量的修改互不影响 。在线程池中的局限性:这是 InheritableThreadLocal最需要警惕的问题。线程池中的线程是复用的,这些线程在首次创建时可能已经从某个父线程继承了值。但当它们被用于执行新的任务时,新的任务提交线程(逻辑上的“父线程”)与工作线程已无直接的创建关系,因此之前继承的值不会更新,这会导致数据错乱(如用户A的任务拿到了用户B的信息)或内存泄漏​ 。对于线程池场景,应考虑使用阿里开源的 TransmittableThreadLocal (TTL)​ 。浅拷贝与对象共享:由于 childValue方法默认是浅拷贝,如果存入的是可变对象(如 Map、List),父子线程实际持有的是同一个对象的引用。在一个线程中修改该对象的内部状态,会直接影响另一个线程 。若需隔离,可以重写 childValue方法实现深拷贝 。内存泄漏风险:与 ThreadLocal类似,如果线程长时间运行(如线程池中的核心线程),并且未及时调用 remove方法清理,那么该线程的 inheritableThreadLocals会一直持有值的强引用,导致无法被GC回收。良好的实践是在任务执行完毕后主动调用 remove()线程池中局限性一般来说,在真实的业务场景下,没人会直接 new Thread,而都是使用线程池的,因此InheritableThreadLocal在线程池中的使用局限性要额外注意首先,我们先理解 InheritableThreadLocal的继承前提InheritableThreadLocal的继承只发生在 新线程被创建时(即 new Thread()并启动时)。在创建过程中,子线程会复制父线程的 InheritableThreadLocal值。在线程池中,线程是预先创建或按需创建的,并且会被复用。因此,继承只会在线程池创建新线程时发生,而不会在复用现有线程时发生。再看线程池创建新线程的条件,对于标准的 ThreadPoolExecutor,新线程的创建遵循以下规则:当前线程数 < 核心线程数:当提交新任务时,如果当前运行的线程数小于核心线程数,即使有空闲线程,线程池也会创建新线程来处理任务。此时,新线程会继承父线程(提交任务的线程)的 InheritableThreadLocal。当前线程数 >= 核心线程数 && 队列已满 && 线程数 < 最大线程数:当任务队列已满,且当前线程数小于最大线程数时,线程池会创建新线程来处理任务。同样,新线程会继承父线程的 InheritableThreadLocal。不会继承的场景线程复用:当线程池中有空闲线程时(例如,当前线程数 >= 核心线程数,但队列未满),任务会被分配给现有线程执行。此时,没有新线程创建,因此不会发生继承。现有线程的 InheritableThreadLocal值保持不变(可能是之前任务设置的值),这可能导致数据错乱(如用户A的任务看到用户B的数据)。线程数已达最大值:如果线程数已达最大线程数,且队列已满,新任务会被拒绝(根据拒绝策略),也不会创建新线程,因此不会继承。不只是线程池污染,线程池使用 InheritableThreadLocal 还可能存在获取不到值的情况。例如,在执行异步任务的时候,复用了某个已有的线程A,并且当时创建该线程A的时候,没有继承InheritableThreadLocal,进而导致后面复用该线程的时候,从InheritableThreadLocal获取到的值为null:javapublic class InheritableThreadLocalWithThreadPoolTest {     private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();     // 这里线程池core/max数量都只有2     private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(             2,             2,             0L,            TimeUnit.MILLISECONDS,             new LinkedBlockingQueue<Runnable>(3000),             new ThreadPoolExecutor.CallerRunsPolicy()    );     public static void main(String[] args) {         // 先执行了不涉及InheritableThreadLocal的子任务初始化线程池线程        testAnotherFunction();              testAnotherFunction();               // 后执行了涉及InheritableThreadLocal       testInheritableThreadLocalWithThreadPool("张三");              testInheritableThreadLocalWithThreadPool("李四");              threadPoolExecutor.shutdown();    }     /**     * inheritableThreadLocal+线程池测试     */         public static void testInheritableThreadLocalWithThreadPool(String param) {             // 1. 在主线程中设置一个值到inheritableThreadLocal                inheritableThreadLocal.set(param);                // 2. 提交异步任务到线程池               threadPoolExecutor.execute(() -> {                    // 3. 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗?                   System.out.println("线程名: " + Thread.currentThread().getName() + ", 父线程设置的inheritableThreadLocal值: " + param + ", 子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get());               });                // 4. 清除inheritableThreadLocal               inheritableThreadLocal.remove();      }                     /**     * 模拟另一个独立的功能     */      public static void testAnotherFunction() {           // 提交异步任务到线程池              threadPoolExecutor.execute(() -> {                   // 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗?                  System.out.println("线程名: " + Thread.currentThread().getName() + ", 线程池-子线程摸个鱼");              });      }}执行结果:text线程名:pool-1-thread-2,线程池-子线程摸个鱼线程名:pool-1-thread-1,线程池-子线程摸个鱼线程名:pool-1-thread-1,父线程设置的inheritableThreadLocal值:李四,子线程获取到inheritableThreadLocal的值:null线程名:pool-1-thread-2,父线程设置的inheritableThreadLocal值:张三,子线程获取到inheritableThreadLocal的值:null当然了,解决这个问题可以考虑使用阿里开源的 TransmittableThreadLocal (TTL),​或者在提交异步任务前,先获取线程数据,再传入。例如:java// 1. 在主线程中先获取inheritableThreadLocal的值String name = inheritableThreadLocal.get();        // 2. 提交异步任务到线程池        threadPoolExecutor.execute(() -> {            // 3. 在线程池-子线程里面直接传入数据  System.out.println("线程名: " + Thread.currentThread().getName() + ", 父线程设置的inheritableThreadLocal值: " + param + ", 子线程获取到inheritableThreadLocal的值: " + name);               });        与 ThreadLocal 的对比特性ThreadLocalInheritableThreadLocal数据隔离​线程绝对隔离线程绝对隔离子线程继承​不支持​支持(创建时)底层存储字段​Thread.threadLocalsThread.inheritableThreadLocals适用场景​线程内全局变量,避免传参父子线程间需要传递上下文数据转载自
  • [技术干货] CopyOnWriteArrayList:写时复制机制与高效并发访问
    前言Vector无论是add方法还是get方法都加上了synchronized修饰,当多线程读写List必须排队执行,很显然这样效率比较是低下的,CopyOnWriteArrayList是读写分离的,好处是提高线程访问效率。CopyOnWrite容器即写时复制的容器。通俗的理解是当往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器里的值Copy到新的容器,然后再往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读 要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。底层原理CopyOnWriteArrayList的动态数组机制 -- 它内部有个volatile数组(array)来保持数据。在“添加/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给volatile数组。这就是它叫做CopyOnWriteArrayList的原因!每一个CopyOnWriteArrayList都和一个监视器锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥添加/删除。类的继承关系CopyOnWriteArrayList实现了List接口,List接口定义了对列表的基本操作;同时实现了RandomAccess接口,表示可以随机访问(数组具有随机访问的特性);同时实现了Cloneable接口,表示可克隆;同时也实现了Serializable接口,表示可被序列化。javapublic class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}类的内部类COWIterator类COWIterator表示迭代器,其也有一个Object类型的数组作为CopyOnWriteArrayList数组的快照,这种快照风格的迭代器方法在创建迭代器时使用了对当时数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。javastatic final class COWIterator<E> implements ListIterator<E> { /** Snapshot of the array */ // 快照 private final Object[] snapshot; /** Index of element to be returned by subsequent call to next. */ // 游标 private int cursor; // 构造函数 private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } // 是否还有下一项 public boolean hasNext() { return cursor < snapshot.length; } // 是否有上一项 public boolean hasPrevious() { return cursor > 0; } // next项 @SuppressWarnings("unchecked") public E next() { if (! hasNext()) // 不存在下一项,抛出异常 throw new NoSuchElementException(); // 返回下一项 return (E) snapshot[cursor++]; } @SuppressWarnings("unchecked") public E previous() { if (! hasPrevious()) throw new NoSuchElementException(); return (E) snapshot[--cursor]; } // 下一项索引 public int nextIndex() { return cursor; } // 上一项索引 public int previousIndex() { return cursor-1; } /** * Not supported. Always throws UnsupportedOperationException. * @throws UnsupportedOperationException always; {@code remove} * is not supported by this iterator. */ // 不支持remove操作 public void remove() { throw new UnsupportedOperationException(); } /** * Not supported. Always throws UnsupportedOperationException. * @throws UnsupportedOperationException always; {@code set} * is not supported by this iterator. */ // 不支持set操作 public void set(E e) { throw new UnsupportedOperationException(); } /** * Not supported. Always throws UnsupportedOperationException. * @throws UnsupportedOperationException always; {@code add} * is not supported by this iterator. */ // 不支持add操作 public void add(E e) { throw new UnsupportedOperationException(); } @Override public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); Object[] elements = snapshot; final int size = elements.length; for (int i = cursor; i < size; i++) { @SuppressWarnings("unchecked") E e = (E) elements[i]; action.accept(e); } cursor = size; }}类的属性属性中有一个可重入锁,用来保证线程安全访问,还有一个Object类型的数组,用来存放具体的元素。当然,也使用到了反射机制和CAS来保证原子性的修改lock域。javapublic class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { // 版本序列号 private static final long serialVersionUID = 8673264195747942595L; // 可重入锁 final transient ReentrantLock lock = new ReentrantLock(); // 对象数组,用于存放元素 private transient volatile Object[] array; // 反射机制 private static final sun.misc.Unsafe UNSAFE; // lock域的内存偏移量 private static final long lockOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> k = CopyOnWriteArrayList.class; lockOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("lock")); } catch (Exception e) { throw new Error(e); } }}类的构造函数默认构造函数javapublic CopyOnWriteArrayList() { // 设置数组 setArray(new Object[0]);}CopyOnWriteArrayList(Collection<? extends E>)javapublic CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements; if (c.getClass() == CopyOnWriteArrayList.class) // 类型相同 // 获取c集合的数组 elements = ((CopyOnWriteArrayList<?>)c).getArray(); else { // 类型不相同 // 将c集合转化为数组并赋值给elements elements = c.toArray(); // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elements.getClass() != Object[].class) // elements类型不为Object[]类型 // 将elements数组转化为Object[]类型的数组 elements = Arrays.copyOf(elements, elements.length, Object[].class); } // 设置数组 setArray(elements);}该构造函数的处理流程如下判断传入的集合c的类型是否为CopyOnWriteArrayList类型,若是,则获取该集合类型的底层数组(Object[]),并且设置当前CopyOnWriteArrayList的数组(Object[]数组),进入步骤③;否则,进入步骤②将传入的集合转化为数组elements,判断elements的类型是否为Object[]类型(toArray方法可能不会返回Object类型的数组),若不是,则将elements转化为Object类型的数组。进入步骤③设置当前CopyOnWriteArrayList的Object[]为elements。CopyOnWriteArrayList(E[]):该构造函数用于创建一个保存给定数组的副本的列表。javapublic CopyOnWriteArrayList(E[] toCopyIn) { // 将toCopyIn转化为Object[]类型数组,然后设置当前数组 setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));}核心函数分析对于CopyOnWriteArrayList的函数分析,主要明白Arrays.copyOf方法即可理解CopyOnWriteArrayList其他函数的意义。copyOf函数该函数用于复制指定的数组,截取或用 null 填充(如有必要),以使副本具有指定的长度。javapublic static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") // 确定copy的类型(将newType转化为Object类型,将Object[].class转化为Object类型; // 判断两者是否相等,若相等,则生成指定长度的Object数组 // 否则,生成指定长度的新类型的数组) T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); // 将original数组从下标0开始,复制长度为(original.length和newLength的较小者),复制到copy数组中(也从下标0开始) System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy;}add函数javapublic boolean add(E e) { // 可重入锁 final ReentrantLock lock = this.lock; // 获取锁 lock.lock(); try { // 元素数组 Object[] elements = getArray(); // 数组长度 int len = elements.length; // 复制数组 Object[] newElements = Arrays.copyOf(elements, len + 1); // 存放元素e newElements[len] = e; // 设置数组 setArray(newElements); return true; } finally { // 释放锁 lock.unlock(); }}此函数用于将指定元素添加到此列表的尾部,处理流程如下获取锁(保证多线程的安全访问),获取当前的Object数组,获取Object数组的长度为length,进入步骤②。根据Object数组复制一个长度为length+1的Object数组为newElements(此时,newElements[length]为null),进入下一步骤。将下标为length的数组元素newElements[length]设置为元素e,再设置当前Object[]为newElements,释放锁,返回。这样就完成了元素的添加。addIfAbsent方法该函数用于添加元素(如果数组中不存在,则添加;否则,不添加,直接返回),可以保证多线程环境下不会重复添加元素。javaprivate boolean addIfAbsent(E e, Object[] snapshot) { // 重入锁 final ReentrantLock lock = this.lock; // 获取锁 lock.lock(); try { // 获取数组 Object[] current = getArray(); // 数组长度 int len = current.length; if (snapshot != current) { // 快照不等于当前数组,对数组进行了修改 // Optimize for lost race to another addXXX operation // 取较小者 int common = Math.min(snapshot.length, len); for (int i = 0; i < common; i++) // 遍历 if (current[i] != snapshot[i] && eq(e, current[i])) // 当前数组的元素与快照的元素不相等并且e与当前元素相等 // 表示在snapshot与current之间修改了数组,并且设置了数组某一元素为e,已经存在 // 返回 return false; if (indexOf(e, current, common, len) >= 0) // 在当前数组中找到e元素 // 返回 return false; } // 复制数组 Object[] newElements = Arrays.copyOf(current, len + 1); // 对数组len索引的元素赋值为e newElements[len] = e; // 设置数组 setArray(newElements); return true; } finally { // 释放锁 lock.unlock(); }}该函数的流程如下:获取锁,获取当前数组为current,current长度为len,判断数组之前的快照snapshot是否等于当前数组current,若不相等,则进入步骤2;否则,进入步骤4不相等,表示在snapshot与current之间,对数组进行了修改(如进行了add、set、remove等操作),获取长度(snapshot与current之间的较小者),对current进行遍历操作,若遍历过程发现snapshot与current的元素不相等并且current的元素与指定元素相等(可能进行了set操作),进入步骤5,否则,进入步骤3在当前数组中索引指定元素,若能够找到,进入步骤5,否则,进入步骤4复制当前数组current为newElements,长度为len+1,此时newElements[len]为null。再设置newElements[len]为指定元素e,再设置数组,进入步骤5释放锁,返回。set函数此函数用于用指定的元素替代此列表指定位置上的元素,也是基于数组的复制来实现的。javapublic E set(int index, E element) { // 可重入锁 final ReentrantLock lock = this.lock; // 获取锁 lock.lock(); try { // 获取数组 Object[] elements = getArray(); // 获取index索引的元素 E oldValue = get(elements, index); if (oldValue != element) { // 旧值等于element // 数组长度 int len = elements.length; // 复制数组 Object[] newElements = Arrays.copyOf(elements, len); // 重新赋值index索引的值 newElements[index] = element; // 设置数组 setArray(newElements); } else { // Not quite a no-op; ensures volatile write semantics // 设置数组 setArray(elements); } // 返回旧值 return oldValue; } finally { // 释放锁 lock.unlock(); }}remove函数此函数用于移除此列表指定位置上的元素。javapublic E remove(int index) { // 可重入锁 final ReentrantLock lock = this.lock; // 获取锁 lock.lock(); try { // 获取数组 Object[] elements = getArray(); // 数组长度 int len = elements.length; // 获取旧值 E oldValue = get(elements, index); // 需要移动的元素个数 int numMoved = len - index - 1; if (numMoved == 0) // 移动个数为0 // 复制后设置数组 setArray(Arrays.copyOf(elements, len - 1)); else { // 移动个数不为0 // 新生数组 Object[] newElements = new Object[len - 1]; // 复制index索引之前的元素 System.arraycopy(elements, 0, newElements, 0, index); // 复制index索引之后的元素 System.arraycopy(elements, index + 1, newElements, index, numMoved); // 设置索引 setArray(newElements); } // 返回旧值 return oldValue; } finally { // 释放锁 lock.unlock(); }}处理流程如下获取锁,获取数组elements,数组长度为length,获取索引的值elements[index],计算需要移动的元素个数(length - index - 1),若个数为0,则表示移除的是数组的最后一个元素,复制elements数组,复制长度为length-1,然后设置数组,进入步骤③;否则,进入步骤②先复制index索引前的元素,再复制index索引后的元素,然后设置数组。释放锁,返回旧值 CopyOnWriteArrayList是Fail Safe的采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。Vector无论是add方法还是get方法都加上了synchronized修饰,当多线程读写List必须排队执行,很显然这样效率比较是低下的,CopyOnWriteArrayList是读写分离的,好处是提高线程访问效率。缺陷和使用场景CopyOnWriteArrayList的写效率比Vector慢。当CopyOnWriteArrayList写元素时是通过备份数组的方式实现的,当多线程同步激烈,数据量较大时会不停的复制数组,内存浪费严重。如果原数组的内容比较多的情况下,可能导致young gc或者full gc弱一致性:不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;小结: CopyOnWriteArrayList合适读多写少的场景,例如黑名单白名单等转载自https://www.cnblogs.com/sevencoding/p/19525347
  • [技术干货] 数据流中的中位数
    如何得到⼀个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使⽤ Insert() ⽅法读取数据流,使⽤ GetMedian() ⽅法获取当前读取数据的中位数。思路及解答排序列表法维护一个列表,每次获取中位数前进行排序javaimport java.util.ArrayList;import java.util.Collections;import java.util.List;public class MedianFinder1 { private List<Integer> data; public MedianFinder1() { data = new ArrayList<>(); } // 插入数字到数据流 public void Insert(Integer num) { data.add(num); // 每次插入后排序,保持列表有序 Collections.sort(data); } // 获取当前数据流的中位数 public Double GetMedian() { int size = data.size(); if (size == 0) return 0.0; if (size % 2 == 1) { // 奇数个元素,返回中间值 return (double) data.get(size / 2); } else { // 偶数个元素,返回中间两个数的平均值 int mid = size / 2; return (data.get(mid - 1) + data.get(mid)) / 2.0; } }}插入操作:每次插入需要排序,时间复杂度O(n log n)获取中位数:直接通过索引访问,时间复杂度O(1)空间复杂度:O(n),需要存储所有数据插入排序法在方法一基础上优化,在插入时就找到正确位置,避免每次都完整排序。同时利用二分查找找到插入位置,减少排序开销javaimport java.util.ArrayList;import java.util.List;public class MedianFinder2 { private List<Integer> data; public MedianFinder2() { data = new ArrayList<>(); } public void Insert(Integer num) { // 使用二分查找找到合适的插入位置 int left = 0, right = data.size() - 1; while (left <= right) { int mid = left + (right - left) / 2; if (data.get(mid) < num) { left = mid + 1; } else { right = mid - 1; } } // 在找到的位置插入元素 data.add(left, num); } public Double GetMedian() { int size = data.size(); if (size == 0) return 0.0; if (size % 2 == 1) { return (double) data.get(size / 2); } else { int mid = size / 2; return (data.get(mid - 1) + data.get(mid)) / 2.0; } }}插入操作:二分查找O(log n) + 插入操作O(n) = O(n)获取中位数:O(1),通过索引直接访问优化效果:比方法一有明显提升,特别适合部分有序的数据双堆法是最高效的解法,利用大顶堆和小顶堆的特性来动态维护中位数,使用大顶堆存较小一半,小顶堆存较大一半⽤⼀个数字来不断统计数据流中的个数,并且创建⼀个最⼤堆,⼀个最⼩堆如果插⼊的数字的个数是奇数的时候,让最⼩堆⾥⾯的元素个数⽐最⼤堆的个数多 1 ,这样⼀来中位数就是⼩顶堆的堆顶如果插⼊的数字的个数是偶数的时候,两个堆的元素保持⼀样多,中位数就是两个堆的堆顶的元素相加除以2 。javapublic class Solution { private int count = 0; private PriorityQueue<Integer> min = new PriorityQueue<Integer>(); private PriorityQueue<Integer> max = new PriorityQueue<Integer>(new Comparator<Integer>() { public int compare(Integer o1, Integer o2) { return o2 - o1; } }); public void Insert(Integer num) { count++; if (count % 2 == 1) { // 奇数的时候,需要最⼩堆的元素⽐最⼤堆的元素多⼀个。 // 先放到最⼤堆⾥⾯,然后弹出最⼤的 max.offer(num); // 把最⼤的放进最⼩堆 min.offer(max.poll()); } else { // 放进最⼩堆 min.offer(num); // 把最⼩的放进最⼤堆 max.offer(min.poll()); } } public Double GetMedian() { if (count % 2 == 0) { return (min.peek() + max.peek()) / 2.0; } else { return (double) min.peek(); } }}插入操作:堆的插入操作O(log n),平衡操作O(log n),总体O(log n)获取中位数:直接访问堆顶元素,O(1)时间复杂度空间复杂度:O(n),需要存储所有数据为什么这种方法有效?大顶堆(maxHeap):存储数据流中较小的一半数字,堆顶是这一半中的最大值小顶堆(minHeap):存储数据流中较大的一半数字,堆顶是这一半中的最小值平衡维护:确保两个堆的大小相差不超过1,这样中位数就只与两个堆顶有关转载自https://www.cnblogs.com/sevencoding/p/19468596
  • [技术干货] 一月技术干货合集来啦
    1、 使用Git实现revert的完整操作步骤【转载】cid:link_02、C++中new关键字用法示例详解【转载】cid:link_13、在C# WinForm项目中跨.cs文件传值的六种常用方案【转载】cid:link_24、 一文带你搞懂Java中Error和Exception的区别【转载】cid:link_35、 Java中实现Word和TXT之间互相转换的实用教程【转载】cid:link_46、MyBatis-Plus 默认不更新null的4种方法【转载】cid:link_57、SpringBoot接口防抖的5种高效方案【转载】cid:link_68、 Java中锁分类及在什么场景下使用【转载】cid:link_79、 Java中锁的全面解析之类型、使用场景、优缺点及实现方式(示例代码【转载】cid:link_810、 Caffeine结合Redis空值缓存实现多级缓存【转载】cid:link_911、在PostgreSQL中优雅高效地进行全文检索的完整过程【转载】cid:link_1012、MySQL CDC原理解析及实现方案【转载】cid:link_1113、 PostgreSQL优雅的进行递归查询的实战指南【转载】cid:link_1214、Redis 常用命令之基础、进阶与场景化实战案例【转载】https://bbs.huaweicloud.com/forum/thread-0212720487861500817-1-1.html15、Git中忽略文件机制的.gitignore与.git/info/exclude两种方式详解【转载】https://bbs.huaweicloud.com/forum/thread-0212720487688092711-1-1.html
  • [技术干货] Java中锁的全面解析之类型、使用场景、优缺点及实现方式(示例代码【转载】
    Java中锁的全面解析:类型、使用场景、优缺点及实现方式在多线程编程中,锁是保证数据一致性和线程安全的核心机制。Java 提供了丰富的锁机制来应对不同的并发场景。本文将从锁的基本概念出发,详细讲解 Java 中常见的锁类型、它们的使用场景、优缺点以及底层实现原理,并通过代码示例帮助读者深入理解。1. 锁的基本概念锁是一种同步机制,用于控制多个线程对共享资源的访问。当一个线程获取锁后,其他线程必须等待该锁被释放才能继续执行,从而避免竞态条件(Race Condition)。2. Java 中常见的锁类型2.1 互斥锁(Mutex Lock)特点:一次只允许一个线程持有锁。保证临界区的独占访问。常见实现:synchronized 关键字(内置锁)ReentrantLock(可重入锁)代码示例:使用 synchronized12345678910111213141516public class Counter {    private int count = 0;    // 同步方法,使用对象锁(this)    public synchronized void increment() {        count++;    }    // 同步代码块,使用指定对象锁    public void decrement() {        synchronized (this) {            count--;        }    }    public int getCount() {        return count;    }}优点:简单易用,无需手动释放锁。JVM 自动管理锁的获取与释放。缺点:无法中断等待中的线程。不能设置超时时间。只能是非公平锁(默认)。2.2 可重入锁(Reentrant Lock)特点:支持同一个线程多次获取同一把锁(即“可重入”)。提供更灵活的控制能力。代码示例:使用 ReentrantLock12345678910111213141516171819202122232425import java.util.concurrent.locks.ReentrantLock;public class ReentrantExample {    private final ReentrantLock lock = new ReentrantLock();    private int count = 0;    public void increment() {        lock.lock();        try {            count++;            // 可以再次获取锁(可重入)            if (count == 1) {                System.out.println("Thread " + Thread.currentThread().getName() + " is re-entering the lock.");            }        } finally {            lock.unlock();        }    }    public int getCount() {        lock.lock();        try {            return count;        } finally {            lock.unlock();        }    }}优点:支持可中断的锁获取(lockInterruptibly())。支持超时锁获取(tryLock(timeout))。支持公平锁和非公平锁(通过构造函数选择)。缺点:必须手动释放锁,容易忘记 unlock() 导致死锁。语法比 synchronized 复杂。2.3 读写锁(ReadWriteLock)特点:分离读操作和写操作的锁。多个读线程可以同时访问共享资源,但写操作必须独占。常见实现:ReentrantReadWriteLock代码示例:读写锁的应用1234567891011121314151617181920212223242526import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteExample {    private final ReadWriteLock lock = new ReentrantReadWriteLock();    private String data = "Default";    // 读操作:多个线程可同时读    public String readData() {        lock.readLock().lock();        try {            System.out.println(Thread.currentThread().getName() + " is reading data: " + data);            return data;        } finally {            lock.readLock().unlock();        }    }    // 写操作:独占访问,其他读写均被阻塞    public void writeData(String newData) {        lock.writeLock().lock();        try {            System.out.println(Thread.currentThread().getName() + " is writing data: " + newData);            data = newData;        } finally {            lock.writeLock().unlock();        }    }}优点:读多写少的场景下性能显著提升(减少锁竞争)。提高并发效率。缺点:写操作会阻塞所有读操作,可能导致“写饥饿”(Writer Starvation)。逻辑复杂度增加。2.4 原子锁(Atomic Lock)特点:使用原子操作(CAS)实现无锁编程。避免传统锁带来的上下文切换开销。常见类:AtomicInteger,AtomicReference,StampedLock(分段锁)代码示例:使用 AtomicInteger12345678910import java.util.concurrent.atomic.AtomicInteger;public class AtomicCounter {    private final AtomicInteger count = new AtomicInteger(0);    public void increment() {        count.incrementAndGet(); // CAS 操作,原子递增    }    public int getCount() {        return count.get();    }}优点:无锁机制,避免线程阻塞。性能高,适合高并发场景。缺点:无法处理复杂的复合操作(如“读-修改-写”)。存在 ABA 问题(可通过 AtomicStampedReference 解决)。2.5 StampedLock(戳记锁)特点:JDK 8 引入,支持乐观读锁(Optimistic Read)。读写分离,支持读写锁和乐观读锁三种模式。代码示例:StampedLock 用法12345678910111213141516171819202122232425262728293031import java.util.concurrent.locks.StampedLock;public class StampedLockExample {    private final StampedLock stampedLock = new StampedLock();    private double x, y;    // 乐观读:不加锁,先尝试读取,失败则升级为悲观读    public double distanceFromOrigin() {        long stamp = stampedLock.tryOptimisticRead();        double currentX = x, currentY = y;        // 检查是否在读期间发生了写操作(版本变化)        if (!stampedLock.validate(stamp)) {            stamp = stampedLock.readLock();            try {                currentX = x;                currentY = y;            } finally {                stampedLock.unlockRead(stamp);            }        }        return Math.sqrt(currentX * currentX + currentY * currentY);    }    // 写操作:独占锁    public void move(double deltaX, double deltaY) {        long stamp = stampedLock.writeLock();        try {            x += deltaX;            y += deltaY;        } finally {            stampedLock.unlockWrite(stamp);        }    }}优点:读操作性能极高(乐观读无需阻塞)。适用于读多写少且对读性能要求极高的场景。缺点:API 复杂,需要显式验证版本。容易出错(如忘记验证或释放锁)。3. 锁的使用场景总结锁类型适用场景推荐程度synchronized简单同步,小范围临界区⭐⭐⭐⭐⭐ReentrantLock需要超时、中断、公平性控制⭐⭐⭐⭐☆ReentrantReadWriteLock读多写少的共享数据⭐⭐⭐⭐☆AtomicXXX简单计数器、状态标志⭐⭐⭐⭐⭐StampedLock极高读性能需求,读多写少⭐⭐⭐☆☆4. 锁的底层实现原理(简述)synchronized:基于 JVM 的对象头(Mark Word)实现,通过 Monitor 机制管理锁状态。ReentrantLock:基于 AQS(AbstractQueuedSynchronizer)实现,使用 CAS+FIFO 队列管理线程排队。StampedLock:基于版本戳(Stamp)和状态位管理,支持乐观读。5. 最佳实践建议优先使用 synchronized,除非有特殊需求。避免在 finally 块外调用 unlock(),防止死锁。读写锁适用于读多写少的场景,避免“写饥饿”。原子类适合简单操作,复杂逻辑仍需锁保护。StampedLock 适合高性能读场景,但需谨慎使用。
  • [技术干货] Java中锁分类及在什么场景下使用【转载】
    一、基础分类(按实现方式)这是最核心的分类维度,直接决定锁的使用方式和核心能力。1. 内置锁(synchronized)- 隐式锁核心定义Java 关键字,JVM 层面实现的隐式锁(无需手动释放),是最基础、使用最广泛的锁。JDK1.6 后引入「锁升级」机制,性能大幅提升。核心特点可重入、默认非公平锁;自动加锁 / 解锁(方法 / 代码块执行完自动释放,无需手动处理);底层依赖对象头的Mark Word + 监视器锁(ObjectMonitor);支持锁升级(偏向锁→轻量级锁→重量级锁),适配不同并发场景。适用场景简单互斥场景(如方法 / 代码块的线程安全);并发度不高、代码简洁性优先的场景;不需要灵活特性(如可中断、超时获取锁)的场景;绝大多数普通业务场景(JVM 优化后性能接近显式锁)。代码示例1234567891011121314151617181920212223242526public class SynchronizedDemo {    // 1. 实例方法锁(对象锁):锁当前实例对象    public synchronized void objectLock() {        System.out.println(Thread.currentThread().getName() + "获取对象锁");        try { Thread.sleep(100); } catch (InterruptedException e) {}    }    // 2. 静态方法锁(类锁):锁当前类的Class对象    public static synchronized void classLock() {        System.out.println(Thread.currentThread().getName() + "获取类锁");        try { Thread.sleep(100); } catch (InterruptedException e) {}    }    // 3. 代码块锁:自定义锁对象(灵活度最高)    private final Object lockObj = new Object();    public void blockLock() {        synchronized (lockObj) {            System.out.println(Thread.currentThread().getName() + "获取代码块锁");            try { Thread.sleep(100); } catch (InterruptedException e) {}        }    }    public static void main(String[] args) {        SynchronizedDemo demo = new SynchronizedDemo();        // 竞争同一对象锁,串行执行        new Thread(demo::objectLock, "线程1").start();        new Thread(demo::objectLock, "线程2").start();    }}2. 显式锁(Lock 接口)- 手动锁核心定义JUC 包下java.util.concurrent.locks.Lock接口的实现类,手动加锁 / 释放锁(需在finally中释放,避免死锁),是 synchronized 的补充和增强。核心实现类 & 特点实现类核心特性ReentrantLock可重入、支持公平 / 非公平、可中断、超时获取锁ReentrantReadWriteLock读写分离(读共享、写独占)、可重入StampedLock支持乐观读、读写锁、写锁,性能优于读写锁适用场景需要灵活锁控制(如可中断、超时获取锁、公平锁)的场景;读多写少的高并发场景(选 ReentrantReadWriteLock/StampedLock);高并发、需要精细控制锁生命周期的场景。代码示例(ReentrantLock)1234567891011121314151617181920212223242526import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockDemo {    // 公平锁(按请求顺序获取),默认非公平锁(性能更高)    private static final ReentrantLock lock = new ReentrantLock(true);    public static void doTask() {        // 1. 加锁(可替换为lockInterruptibly():可中断锁)        lock.lock();        try {            System.out.println(Thread.currentThread().getName() + "获取锁");            // 模拟业务操作            Thread.sleep(100);        } catch (InterruptedException e) {            Thread.currentThread().interrupt(); // 恢复中断状态        } finally {            // 2. 释放锁(必须放finally,否则死锁)            if (lock.isHeldByCurrentThread()) {                lock.unlock();                System.out.println(Thread.currentThread().getName() + "释放锁");            }        }    }    public static void main(String[] args) {        new Thread(ReentrantLockDemo::doTask, "线程A").start();        new Thread(ReentrantLockDemo::doTask, "线程B").start();    }}二、进阶分类(按锁的核心特性)基于锁的行为和并发特性分类,帮你理解锁的底层逻辑和适用场景。1. 可重入锁 vs 不可重入锁类型定义示例适用场景可重入锁同一线程可多次获取同一把锁,不会死锁synchronized、ReentrantLock所有业务场景(递归调用、同一线程多次操作共享资源)不可重入锁同一线程多次获取同一锁会死锁自定义简单自旋锁(未处理重入)极少使用(仅严格限制锁获取次数的特殊场景)可重入锁示例(synchronized 递归调用)123456789101112public class ReentrantDemo {    public synchronized void outer() {        System.out.println("外层方法获取锁");        inner(); // 同一线程再次获取同一锁,无死锁    }    public synchronized void inner() {        System.out.println("内层方法获取锁");    }    public static void main(String[] args) {        new ReentrantDemo().outer(); // 正常执行,无死锁    }}2. 乐观锁 vs 悲观锁这是并发设计思想的分类,而非具体锁实现。类型核心思想实现方式适用场景悲观锁假设必有竞争,先锁后执行synchronized、ReentrantLock高冲突、写多读少(如库存扣减、转账)乐观锁假设无竞争,先执行后检测CAS(Atomic 类)、版本号低冲突、读多写少(如计数器、缓存更新)乐观锁示例(CAS 实现)12345678910111213141516171819import java.util.concurrent.atomic.AtomicInteger;public class OptimisticLockDemo {    // CAS是乐观锁的核心实现    private static final AtomicInteger count = new AtomicInteger(0);    // 原子自增(无锁,冲突时重试)    public static void increment() {        count.incrementAndGet();    }    public static void main(String[] args) throws InterruptedException {        Runnable task = () -> {            for (int i = 0; i < 1000; i++) increment();        };        Thread t1 = new Thread(task);        Thread t2 = new Thread(task);        t1.start(); t2.start();        t1.join(); t2.join();        System.out.println("最终计数:" + count.get()); // 2000(线程安全)    }}3. 公平锁 vs 非公平锁类型定义示例适用场景公平锁按请求顺序获取锁,先到先得ReentrantLock(true)对公平性要求高(如任务排队、避免线程饥饿)非公平锁不按顺序,线程可插队获取锁synchronized、ReentrantLock()大部分场景(优先性能,容忍轻微饥饿)4. 读写锁(ReentrantReadWriteLock)- 共享 + 独占锁核心定义将锁拆分为「读锁(共享锁)」和「写锁(独占锁)」,核心规则:读 - 读共享:多个线程可同时获取读锁;读 - 写互斥:读锁和写锁不能同时持有;写 - 写互斥:多个线程不能同时获取写锁。适用场景读多写少的场景(如缓存、配置读取、商品详情页、数据查询),相比普通独占锁,能大幅提升读并发效率。代码示例12345678910111213141516171819202122232425262728293031323334353637383940import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockDemo {    private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();    private static final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();    private static final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();    private static int cacheData = 0; // 模拟缓存数据    // 读操作(共享锁,多线程同时执行)    public static void readCache() {        readLock.lock();        try {            System.out.println(Thread.currentThread().getName() + "读取缓存:" + cacheData);            Thread.sleep(200); // 模拟读耗时        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        } finally {            readLock.unlock();        }    }    // 写操作(独占锁,串行执行)    public static void updateCache(int newData) {        writeLock.lock();        try {            System.out.println(Thread.currentThread().getName() + "更新缓存:" + newData);            cacheData = newData;            Thread.sleep(200); // 模拟写耗时        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        } finally {            writeLock.unlock();        }    }    public static void main(String[] args) {        // 5个读线程(可同时执行,并发效率高)        for (int i = 0; i < 5; i++) {            new Thread(ReadWriteLockDemo::readCache, "读线程" + i).start();        }        // 1个写线程(独占,所有读线程等待)        new Thread(() -> updateCache(100), "写线程").start();    }}5. synchronized 的锁升级(偏向锁→轻量级锁→重量级锁)JDK1.6 为优化 synchronized 引入的自适应锁机制,锁级别从低到高升级(不可逆),适配不同并发场景:锁类型核心特点适用场景偏向锁锁偏向第一个线程,无竞争时零开销单线程执行同步代码(如初始化资源)轻量级锁多线程交替竞争,CAS 自旋获取锁少量线程(2-3 个)交替执行重量级锁多线程激烈竞争,线程阻塞(OS 层面)大量线程同时竞争锁6. 自旋锁 vs 阻塞锁类型定义示例适用场景自旋锁获取锁失败时循环重试(自旋),不阻塞CAS、synchronized 轻量级锁锁持有时间短、低冲突(如简单变量更新)阻塞锁获取锁失败时线程阻塞,释放 CPUsynchronized 重量级锁、ReentrantLock锁持有时间长、高冲突(如复杂业务逻辑)7. 分段锁(ConcurrentHashMap JDK1.7)核心定义将数据拆分为多个「段(Segment)」,每个段独立加锁,不同段的操作互不阻塞,并发度 = 段数(默认 16)。适用场景JDK1.7 的 ConcurrentHashMap(JDK1.8 后用 CAS+Synchronized 替代),适用于高并发读写 Map 的场景。三、实战选型指南(核心)业务场景推荐锁类型选型原因简单互斥、代码简洁synchronized隐式锁,无需手动释放,JVM 优化优异可中断 / 超时 / 公平锁ReentrantLock支持灵活的锁控制特性读多写少(缓存 / 查询)ReentrantReadWriteLock/StampedLock读共享,大幅提升读并发效率低冲突、简单变量更新CAS(AtomicInteger/AtomicLong)无锁,性能最高,避免自旋消耗高冲突、写多读少(转账)synchronized/ReentrantLock悲观锁,避免高冲突下的 CAS 自旋 CPU 开销公平性要求高(任务排队)ReentrantLock(true)按请求顺序获取锁,避免线程饥饿高并发 Map 操作ConcurrentHashMapJDK1.8 用 CAS+Synchronized,兼顾性能和安全性总结关键点回顾基础核心:synchronized(简单、通用)和 Lock 接口(灵活、高级)是 Java 锁的两大基石,前者适用于普通场景,后者适用于需要灵活控制的场景;特性选型:读多写少选读写锁,低冲突选乐观锁 / CAS,高冲突选悲观锁,公平性要求高选公平锁;性能原则:优先选择「开销低」的锁(如偏向锁、CAS),高冲突场景才用「高开销」的阻塞锁(如重量级锁);实战建议:90% 的普通业务场景用 synchronized 即可,仅在需要可中断、超时、读写分离时才用 Lock 接口实现类。
  • [技术干货] Java中实现Word和TXT之间互相转换的实用教程【转载】
    Spire.Doc for Java:Word 与 TXT 转换的利器在 Java 生态中,处理 Word 文档的库并不少见,但 Spire.Doc for Java 凭借其强大的功能和易用性脱颖而出。它是一个专业的 Word 文档处理组件,支持创建、读写、编辑、转换和打印 Word 文档,并且兼容多种 Word 版本。其中,对 Word 和 TXT 格式的互相转换提供了非常便捷的 API。引入 Spire.Doc for Java要开始使用 Spire.Doc,您需要将其作为依赖添加到您的 Maven 项目中。Maven 配置示例:1234567891011121314  <repositories>    <repository>        <id>com.e-iceblue</id>        <name>e-iceblue</name>        <url>https://repo.e-iceblue.cn/repository/maven-public/</url>    </repository></repositories><dependencies>    <dependency>        <groupId>e-iceblue</groupId>        <artifactId>spire.doc</artifactId>        <version>14.1.3</version>    </dependency></dependencies>请确保您使用的版本是最新的稳定版本,以获取最佳的兼容性和功能。从 Word 到 TXT:逐步实现文档内容提取将 Word 文档转换为纯文本(TXT)是一个常见的需求,例如用于内容提取、文本分析或跨平台传输。Spire.Doc for Java 提供了一行代码即可完成此操作。实现步骤加载 Word 文档: 使用 Document 类的 loadFromFile() 方法加载目标 Word 文档。保存为 TXT 格式: 调用 saveToFile() 方法,并指定输出路径和 FileFormat.Txt 格式。释放资源: 调用 dispose() 方法释放文档对象占用的资源。Java 代码示例1234567891011121314151617181920import com.spire.doc.Document;import com.spire.doc.FileFormat; public class ConvertWordtoText {     public static void main(String[] args) {         // 创建 Document 对象        Document doc = new Document();         // 加载 Word 文件        doc.loadFromFile("示例.docx");         // 将文档保存为 TXT 格        doc.saveToFile("Word转文本.txt", FileFormat.Txt);         // 释放资源        doc.dispose();    }}代码解析:document.loadFromFile(inputWordPath): 负责读取指定路径的 Word 文档内容。document.saveToFile(outputTxtPath, FileFormat.Txt): 这是转换的核心。它将加载的 Word 文档内容以纯文本格式写入到 outputTxtPath 指定的文件中。FileFormat.Txt 枚举值明确指示了目标格式。document.dispose(): 释放资源,用于关闭文件流并释放内存,特别是在处理大量文档时。从 TXT 到 Word:构建富文本格式文档将纯文本(TXT)文件转换为 Word 文档,通常是为了对其进行格式化、添加图片、表格或其他富文本元素。Spire.Doc 同样能轻松实现这一目标。实现步骤创建或加载 Word 文档: 对于从 TXT 创建新的 Word 文档,直接创建 Document 对象即可。加载 TXT 内容: 使用 Document 类的 loadFromFile() 方法加载 TXT 文件。保存为 Word 格式: 调用 saveToFile() 方法,并指定输出路径和 FileFormat.Docx(或 FileFormat.Doc)格式。释放资源: 调用 dispose() 方法释放文档对象占用的资源。Java 代码示例1234567891011121314151617181920import com.spire.doc.Document;import com.spire.doc.FileFormat; public class ConvertTextToWord {     public static void main(String[] args) {         // 创建 Document 对象        Document txt = new Document();         // 加载 .txt 文本文件        txt.loadFromFile("介绍.txt");         // 将文件保存为 Word 格式        txt.saveToFile("TXT转Word.docx", FileFormat.Docx);         // 释放资源        txt.dispose();    }}代码解析:document.loadFromFile(inputTxtPath): 这里巧妙地利用了 spire.doc for java 的 loadFromFile 方法不仅可以加载 Word 文档,还能加载 TXT 文件并将其内容导入到 Document 对象中。document.saveToFile(outputWordPath, FileFormat.Docx): 将包含 TXT 内容的 Document 对象保存为 Word 格式。FileFormat.Docx 是现代 Word 文档的默认格式,您也可以选择 FileFormat.Doc。格式调整建议:将 TXT 转换为 Word 后,默认情况下可能只是简单的文本导入。如果需要更复杂的格式,例如设置字体、段落样式、页眉页脚等,Spire.Doc 也提供了丰富的 API 来实现这些功能,您可以在 loadFromFile 之后、saveToFile 之前,对 document 对象进行进一步的编辑操作。
  • [技术干货] 一文带你搞懂Java中Error和Exception的区别【转载】
    Error 和 Exception 的基本概念在开始之前,我们先来理解一下这两个概念的基本含义。Error(错误):通常指的是系统级的错误,这些错误往往是程序无法恢复的,或者恢复起来非常困难。比如内存溢出、栈溢出、系统资源耗尽等。这些错误一般不是由程序逻辑问题引起的,而是由系统环境、硬件资源等外部因素导致的。Exception(异常):则是指程序运行过程中出现的异常情况,这些异常通常是可以被程序捕获和处理的。比如数组越界、空指针引用、文件不存在、网络连接失败等。这些异常往往是由程序的设计问题、逻辑错误或者外部输入导致的。简单来说,Error 是"系统说不行",Exception 是"程序说有问题"。两者的核心区别虽然 Error 和 Exception 都是程序运行时的异常情况,但它们有几个关键的区别:严重程度不同Error 通常比 Exception 更严重。Error 往往意味着系统级别的故障,比如内存溢出(OutOfMemoryError)、栈溢出(StackOverflowError)等。这些错误一旦发生,程序通常无法继续正常运行,甚至可能导致整个应用崩溃。Exception 相对来说就没那么严重了。虽然有些 Exception 也会导致程序崩溃(比如未捕获的运行时异常),但大多数 Exception 都是可以被程序捕获和处理的。比如文件读取失败,我们可以提示用户重新选择文件;网络请求失败,我们可以重试或者显示错误信息。处理方式不同对于 Error,我们通常不应该尝试捕获和处理。因为 Error 往往意味着系统资源已经耗尽或者系统环境出现了严重问题,这时候程序已经无法正常工作了,强行处理可能会让问题变得更糟。对于 Exception,我们应该主动捕获和处理。这是程序健壮性的重要体现。比如在读取文件时,我们应该捕获 IOException,然后给用户一个友好的提示,而不是让程序直接崩溃。来源不同Error 通常来自系统层面,比如 JVM 运行时错误、操作系统错误等。这些错误不是由我们的业务代码直接引起的,而是由底层系统或环境问题导致的。Exception 通常来自应用层面,比如我们的业务逻辑、API 调用、数据处理等。这些异常往往可以通过改进代码逻辑、添加校验、重试机制等方式来处理。实际应用场景让我们通过几个实际的场景来理解 Error 和 Exception 的区别:场景一:内存溢出假设你正在开发一个图片处理应用,用户上传了一张非常大的图片。如果你的程序试图将整个图片加载到内存中,而系统内存不足,就会抛出 OutOfMemoryError。这是一个典型的 Error。因为内存不足是系统资源问题,不是你的程序逻辑问题。虽然你可以通过优化代码(比如分块处理图片)来避免这个问题,但一旦内存真的耗尽了,程序就很难恢复了。正确的做法是:在程序设计阶段就考虑内存限制,避免一次性加载过大的数据。如果真的遇到了 OutOfMemoryError,最好的处理方式可能是记录错误日志,然后优雅地退出程序,而不是试图捕获和处理这个错误。场景二:文件读取失败假设你的应用需要读取一个配置文件。如果文件不存在,或者文件被其他程序占用,就会抛出 FileNotFoundException 或 IOException。这是一个典型的 Exception。因为文件读取失败是可以通过程序逻辑来处理的。你可以捕获这个异常,然后给用户一个友好的提示,比如"配置文件不存在,请检查文件路径",或者使用默认配置。123456789101112try {    File configFile = new File("config.properties");    // 读取配置文件} catch (FileNotFoundException e) {    // 文件不存在,使用默认配置    logger.warn("配置文件不存在,使用默认配置");    loadDefaultConfig();} catch (IOException e) {    // 文件读取失败,提示用户    logger.error("读取配置文件失败", e);    showErrorDialog("无法读取配置文件,请检查文件权限");}场景三:网络请求超时假设你的应用需要调用一个远程 API。如果网络连接不稳定,或者服务器响应慢,可能会抛出 SocketTimeoutException 或 ConnectException。这也是一个典型的 Exception。你可以捕获这个异常,然后实现重试机制,或者给用户一个友好的提示。1234567891011121314int maxRetries = 3;for (int i = 0; i < maxRetries; i++) {    try {        // 发送网络请求        return httpClient.execute(request);    } catch (SocketTimeoutException e) {        if (i == maxRetries - 1) {            // 最后一次重试也失败了            throw new ApiException("网络请求超时,请检查网络连接");        }        // 等待一段时间后重试        Thread.sleep(1000 * (i + 1));    }}场景四:空指针引用假设你的代码中有一个对象可能为 null,但你没有做空值检查就直接使用了它,就会抛出 NullPointerException。这也是一个典型的 Exception。虽然 NullPointerException 是运行时异常,不需要强制捕获,但我们应该在代码中主动避免这种情况。1234567891011// 不好的做法String name = user.getName(); // 如果 user 为 null,会抛出 NullPointerExceptionSystem.out.println(name.length()); // 好的做法if (user != null) {    String name = user.getName();    if (name != null) {        System.out.println(name.length());    }}不同语言中的实现虽然 Error 和 Exception 的概念是相通的,但不同语言的实现方式可能不太一样:Java 中,Error 和 Exception 都是 Throwable 的子类。Error 包括 OutOfMemoryError、StackOverflowError 等系统级错误;Exception 包括 RuntimeException(运行时异常)和 CheckedException(检查异常)。Python 中,所有的异常都继承自 BaseException。系统退出异常(SystemExit、KeyboardInterrupt)类似于 Error,其他异常类似于 Exception。Swift 中,Error 是一个协议,任何遵循 Error 协议的类型都可以被抛出。Swift 没有严格区分 Error 和 Exception,但我们可以通过命名和文档来区分系统级错误和应用级异常。最佳实践在实际开发中,我们应该遵循以下原则:对于 Error(系统级错误):不要尝试捕获和处理系统级错误在程序设计阶段就考虑资源限制,避免触发系统错误如果真的遇到了系统错误,记录日志并优雅退出对于 Exception(应用级异常):主动捕获和处理可能出现的异常给用户提供友好的错误提示实现重试机制、降级方案等容错处理记录详细的异常日志,方便问题排查代码设计建议:使用防御性编程,提前检查可能的问题合理使用异常处理,不要过度捕获异常区分可恢复的异常和不可恢复的异常对于关键操作,实现重试和降级机制总结Error 和 Exception 虽然都是程序运行时的异常情况,但它们有本质的区别:Error 是系统级的错误,通常无法恢复,不应该被捕获处理Exception 是应用级的异常,可以被捕获和处理,是程序健壮性的重要体现在实际开发中,我们应该:通过合理的设计避免系统级错误主动捕获和处理应用级异常给用户提供友好的错误提示实现完善的容错和降级机制
总条数:696 到第
上滑加载中