1. 简介
性能优化已成为开发者的必备技能。随着应用规模的扩大,微小的代码效率差异可能在高频调用下被显著放大,直接影响系统吞吐量与资源消耗。这些优化不仅提升运行效率,还增强代码可维护性与系统稳定性。掌握这些技巧,有助于开发者编写更高效、更健壮的Java应用,尤其适用于中间件、高并发服务与大规模数据处理系统。
2.实战案例
2.1 字符串格式化5种方式
1. 使用 String.format 进行字符串拼接
使用 + 操作符拼接包含多个参数的字符串(例如 URL 查询字符串)容易出错。
String url = "http://www.pack.com?userName=" + userName + "&age=" + age + "&address=" + address + "&sex=" + sex + "&roledId=" + roleId;2. 使用StringBuilder
StringBuilder sb = new StringBuilder("http://www.pack.com");sb.append("userName=").append(userName) .append("&age=").append(age) .append("&address=").append(address) .append("&sex=").append(sex) .append("&roledId=").append(roleId) ;3. 使用String.format
String urlTemplate = "http://www.pack.com?userName=%s&age=%s&address=%s&sex=%s&roledId=%s";String url = String.format(urlTemplate, userName, age, address, sex, roleId);String.format 显著提升了代码可读性。适用于 URL 参数和日志消息等场景。
4. 文本块 + formatted()方法(Java 15+)
Java 15 引入了 文本块("""),Java 17+ 支持在文本块中进行表达式替换,Java 15 起提供了 formatted(...) 方法,是 String.format 的更优雅替代。
String userName = "Pack" ;Integer age = 33 ;String address = "新疆乌鲁木齐" ;String sex = "男" ;Integer roleId = 666 ;String url = """ http://www.pack.com?userName=%s&age=%s&address=%s&sex=%s&roledId=%s """.formatted(userName, age, address, sex, roleId) ;System.err.println(url) ;5. 使用Spring中的UriComponentsBuilder
URI uri = UriComponentsBuilder .fromUriString("http://www.pack.com?userName={name}&age={age}&address={address}") .build("pack", 33, "新疆乌鲁木齐") ;build方法参数为可变参数,支持任意多的参数。
2.2 使用带缓冲的IO流
标准 IO 流在进行文件操作时可能效率低下,因为会频繁访问磁盘。
优化前:
FileInputStream fis = new FileInputStream(srcFile) ;FileOutputStream fos = new FileOutputStream(destFile) ;int len;while ((len = fis.read()) != -1) { fos.write(len) ;}fos.flush() ;优化后:
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));byte[] buffer = new byte[1024];int len;while ((len = bis.read(buffer)) != -1) { bos.write(buffer, 0, len);}bos.flush();更多优化
1. 使用Files#copy
Java 7 引入了 java.nio.file.Files 类,提供了高性能的文件操作方法。
Path srcPath = Path.of("d:/1.txt") ;Path destPath = Path.of("d:/2.txt") ;Files.copy(srcPath, destPath, StandardCopyOption.REPLACE_EXISTING) ;2. 使用 Files.newBufferedWriter() / Files.newBufferedReader()
如果是处理文本文件,推荐使用:
Path path = Path.of("d:/3.txt") ;// 写文本文件try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { writer.write("Hello, World!");}// 读文本文件try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); }}自动带缓冲,无需手动创建 BufferedReader
3. 使用 FileChannel 进行大文件高速复制
对于超大文件或追求极致性能的场景,可使用 FileChannel 的 transferTo() 或 transferFrom(),利用操作系统的零拷贝(zero-copy)优化。
try (FileInputStream fis = new FileInputStream(new File("d:/1.txt")) ; FileOutputStream fos = new FileOutputStream(new File("d:/2.txt")) ; FileChannel inChannel = fis.getChannel(); FileChannel outChannel = fos.getChannel()) { inChannel.transferTo(0, inChannel.size(), outChannel);}可实现 零拷贝(Zero-Copy),避免数据在用户空间和内核空间之间复制。性能远超传统流复制,尤其适合 GB 级大文件。
4. 使用 InputStream.transferTo()(Java 9+,简洁高效)
Java 9 为 InputStream 添加了 transferTo(OutputStream) 方法,自动完成复制,内部使用缓冲。
try (InputStream in = Files.newInputStream(srcPath); OutputStream out = Files.newOutputStream(destPath)) { in.transferTo(out);}2.3 减少嵌套循环
深度嵌套的循环会显著降低性能,尤其是在处理大数据集时。
优化前:
for (User user : userList) { for (Role role : roleList) { if (user.getRoleId().equals(role.getId())) { user.setRoleName(role.getName()); } }}优化后:
Map<Long, List<Role>> roleMap = roleList.stream() .collect(Collectors.groupingBy(Role::getId)) ;for (User user : userList) { List<Role> roles = roleMap.get(user.getRoleId()); if (CollectionUtils.isNotEmpty(roles)) { user.setRoleName(roles.get(0).getName()); }}2.4 资源关闭
未能关闭数据库连接等资源会导致资源泄漏。
优化前:
Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/db", "root", "xxxooo");PreparedStatement pstmt = conn.prepareStatement("select * from user");ResultSet rs = pstmt.executeQuery();优化后:
Connection conn = DriverManager.getConnection(url, user, password);PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM user");ResultSet rs = pstmt.executeQuery();try (conn; pstmt; rs) { // Java 9+ 支持 while (rs.next()) { // 处理结果 }} catch (SQLException e) { log.error("查询失败", e);}2.5 使用数据库连接池
每次请求都创建和关闭数据库连接是低效的。
HikariConfig config = new HikariConfig();config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");config.setUsername("root");config.setPassword("123456");config.setDriverClassName("com.mysql.cj.jdbc.Driver");// 核心配置config.setMaximumPoolSize(20); // 最大连接数config.setMinimumIdle(5); // 最小空闲连接config.setConnectionTimeout(30000); // 获取连接超时(30秒)config.setIdleTimeout(600000); // 空闲连接超时(10分钟)config.setMaxLifetime(1800000); // 连接最大存活时间(30分钟)HikariDataSource dataSource = new HikariDataSource(config);当然,在Spring Boot中你不需要这样配置,直接通过配置文件。
2.6 缓存反射结果
反射比直接实例化慢,但可以通过缓存来优化。
@Servicepublic class PaymentService implements ApplicationListener<ContextRefreshedEvent> { private static Map<String, IPayService> payMap = new ConcurrentHashMap<>(); @Override public void onApplicationEvent(ContextRefreshedEvent event) { Map<String, Object> beans = event.getApplicationContext().getBeansWithAnnotation(PayCode.class); beans.forEach((key, value) -> { String bizType = value.getClass().getAnnotation(PayCode.class).value(); payMap.put(bizType, (IPayService) value); }); } public void pay(String code) { payMap.get(code).pay(); }}缓存反射结果可以避免重复进行反射操作,显著提升性能和可扩展性。
2.7 多线程处理并行任务
串行调用远程服务很慢;可以使用 CompletableFuture 实现并行化。
private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10)) ;public UserInfo getUserInfo(Long id) throws Exception { UserInfo userInfo = new UserInfo(); CompletableFuture.allOf( CompletableFuture.runAsync(() -> getRemoteUserAndFill(id, userInfo), executor), CompletableFuture.runAsync(() -> getRemoteBonusAndFill(id, userInfo), executor), CompletableFuture.runAsync(() -> getRemoteGrowthAndFill(id, userInfo), executor) ).join(); return userInfo ;}并行执行将总耗时从多个请求的累加时间降低为最长单个请求的时间。例如,从 530ms 降低到 200ms,显著提升响应速度。
2.8 实现延迟加载
避免在不需要时就创建对象,只在真正需要时才进行实例化。
public class HolderLazySingleton { private HolderLazySingleton() {} // 静态内部类,只有在第一次访问时才加载 private static class InstanceHolder { private static final HolderLazySingleton INSTANCE = new HolderLazySingleton(); } public static HolderLazySingleton getInstance() { return InstanceHolder.INSTANCE; }}延迟加载(JVM 保证内部类在首次使用时才初始化)。线程安全(JVM 类加载机制保证)。无同步开销,性能极佳。代码简洁。
2.9 指定集合初始大小
未指定集合大小会导致频繁的内部数组扩容。
优化前:
List<Integer> list = new ArrayList<>();for (int i = 0; i < 100000; i++) { list.add(i);}优化后:
List<Integer> list = new ArrayList<>(100000);for (int i = 0; i < 100000; i++) { list.add(i);}预先定义集合大小可以避免昂贵的数组扩容操作(复制、分配),对于大型集合,性能可提升一倍。
2.10 避免过度使用try...catch
在每个方法中都使用 try-catch 会让代码变得臃肿。
全局异常处理:
@RestControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResposneEntity<R> handleException(Exception e) { // TODO }}使用 @RestControllerAdvice 集中处理异常,可以简化业务代码,确保错误响应的一致性。
2.11 优先使用位运算
在某些计算中,位运算速度更快。
int index1 = hashCode % 16; // 慢:除法运算int index2 = hashCode & (16 - 1); // 快:位运算注意:仅在是 2 的幂时才等价。如下是错误的:
int len2 = 15; // 不是 2 的幂 ❌int index2 = hash & (len2 - 1); // 错误!不等于 hash % 15当参与计算的数为 2 的幂时,使用按位与(&)比取模(%)更高效,ThreadLocal 和 HashMap 都采用了这种优化。
2.12 利用第三方库
借助 Guava 等工具库,可显著简化复杂操作。
List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);List<List<Integer>> partitionList = Lists.partition(list, 2);Lists.partition 等工具能减少样板代码(boilerplate code),提升代码可读性。
如果你使用的Java 9+,那么你还可以使用如下方法:
List<Integer> list = List.of(1, 2, 3, 4, 5);2.13 优先使用同步块而非同步方法
对整个方法加锁会降低并发性能。
优化前:
public synchronized void doSave(String fileUrl) { mkdir(); uploadFile(fileUrl); sendMessage(fileUrl);}优化后:
public void doSave(String path, String fileUrl) { synchronized(this) { if (!exists(path)) mkdir(path); } uploadFile(fileUrl); sendMessage(fileUrl);}缩小锁范围(仅锁定关键代码段)可提升并发性,因为非关键操作无需等待锁释放。
2.14 清理 ThreadLocal 数据
未清除的 ThreadLocal 数据可能导致内存泄漏。
public class CurrentUser { private static final ThreadLocal<UserInfo> THREAD_LOCAL = new ThreadLocal<>(); public static void set(UserInfo userInfo) { THREAD_LOCAL.set(userInfo); } public static UserInfo get() { return THREAD_LOCAL.get(); } public static void remove() { THREAD_LOCAL.remove(); }}使用方式:
try { CurrentUser.set(userInfo); // 业务逻辑} finally { CurrentUser.remove();}显式清除 ThreadLocal 数据可以防止在高并发场景下的内存泄漏问题(尤其是在使用线程池时,线程会复用,而 ThreadLocal 变量若未清理会持续存在,造成内存泄漏)。
2.15 使用 equals 进行比较
使用 == 比较 Integer 对象时,可能会在缓存范围(-128 到 127)之外失败。
Integer a = new Integer(1);Integer b = new Integer(1);System.out.println(a.equals(b)); // 输出:true或者如下方法:
Integer a = 200;Integer b = 200;System.out.println(Objects.equals(a, b)); // 推荐:安全、简洁、正确2.16 避免加载大型集合
将整张数据库表一次性加载到内存中可能导致内存溢出(OOM),引发应用崩溃。
解决方案
int PAGE_SIZE = 500;int currentPage = 1;RequestPage page = new RequestPage(currentPage, PAGE_SIZE);Page<User> pageUser = userMapper.search(page);while (pageUser.getPageCount() >= currentPage) { for (User user : pageUser.getData()) { // TODO } page.setPageNo(++currentPage); pageUser = userMapper.search(page);}通过分页,将大数据集拆分为小块处理,显著降低内存占用,避免因数据量过大导致的内存溢出。
当然你还可以通过流式查询,详细查看下面文章:
告别OOM!Spring Boot 流式导出百万数据:支持MyBatis/JPA/Jdbc
2.17 使用枚举(Enums)管理状态
枚举能显著提升状态字段的可读性和可维护性。
public enum OrderStatusEnum { CREATE(1, "取消订单"), PAY(2, "已支付"), DONE(3, "完成"), CANCEL(4, "已取消"); private final int code; private final String message; OrderStatusEnum(int code, String message) { this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; }}枚举集中管理状态,减少 if-else 判断,支持直接使用 == 进行高效比较(比 equals 更快且安全),并具备编译期检查优势。
2.18 杜绝使用魔法值
硬编码的值会降低代码维护及可读性。
优化前:
if (user.getId() < 1000L) { // TOOD}优化后:
private static final long DEFAULT_USER_ID = 1000L;if (user.getId() < DEFAULT_USER_ID) { // TODO}使用常量能提升代码的可读性、可维护性和可复用性。其他开发者能快速理解 1000L 的含义,且若需修改该值,只需更改一处。
2.19 避免大事务
大事务可能导致超时和性能问题。
优化建议:
减少 @Transactional 的使用范围将查询操作移出事务避免在事务中进行远程调用或大量数据处理尽可能使用异步处理小事务可减少锁竞争,提升系统性能。如下示例:
// 错误:查询包含在事务中@Transactionalpublic void processOrder(Order order) { Order data = orderRepository.findById(order.getId()); // 查询操作(无需事务) data.setStatus("PROCESSED"); orderRepository.save(data);}// 正确:查询移出事务public void processOrder(Order order) { Order data = orderRepository.findById(order.getId()); // 非事务 updateOrderStatus(data); // 仅更新操作在事务中}@Transactionalprivate void updateOrderStatus(Order data) { data.setStatus("PROCESSED"); orderRepository.save(data);}注意:一个查询是否应该在一个事务中是需要视情况而定,不能一概而论。@Transactional在私有方法,或者内部调用的时候,事务注解会失效,这里只是为了做个示例
2.20 消除冗长的 if-else 链
冗长的 if-else 链违反了软件设计原则。
优化前:
public void toPay(String code) { if ("alia".equals(code)) { aliaPay.pay(); } else if ("weixin".equals(code)) { weixinPay.pay(); } else if ("jingdong".equals(code)) { jingDongPay.pay(); } // 如果新增支付方式,需不断添加 else if ...}优化后:
public interface PaymentService { void pay(); String getPaymentCode(); // 返回 "alia", "weixin" 等}// 定义接口实现 @Componentpublic class AliaPaymentService implements PaymentService { @Override public void pay() { /* 支付逻辑 */ } @Override public String getPaymentCode() { return "alia"; }}@Componentpublic class WeixinPaymentService implements PaymentService { @Override public void pay() { /* 支付逻辑 */ } @Override public String getPaymentCode() { return "weixin"; }}// 工厂类注入所有实现@Servicepublic class PaymentFactory { private final Map<String, PaymentService> paymentMap = new HashMap<>(); // Spring 自动注入所有 PaymentService 实现 public PaymentFactory(List<PaymentService> paymentServices) { paymentServices.forEach(service -> paymentMap.put(service.getPaymentCode(), service) ); } public PaymentService get(String code) { return paymentMap.get(code); }}2.21 防止无限循环
无限循环,尤其是递归调用中的无限循环,可能导致应用崩溃(如栈溢出)。
带递归深度限制的示例:
public void printCategory(Category category, int depth) { if (category == null || category.getParentId() == null || depth > 4) { return; } System.out.println("Parent category: " + category.getName()); Category parent = categoryMapper.getCategoryById(category.getParentId()); printCategory(parent, depth + 1);}设置递归深度限制可以防止因数据中存在循环引用(例如 A → B → C → A)导致的栈溢出(StackOverflowError),提升系统健壮性。
2.22 正确使用 BigDecimal
BigDecimal 的构造函数可能导致精度丢失。
错误用法:
BigDecimal a = new BigDecimal(0.02);BigDecimal b = new BigDecimal(0.03);System.out.println(b.subtract(a); // 结果:0.0099999999999999982236431605997495353221893310546875(精度丢失)使用 BigDecimal.valueOf(double) 或 new BigDecimal(String) 可以确保小数运算的精度。而 new BigDecimal(double) 会直接使用 double 的二进制表示,由于 double 本身无法精确表示大多数十进制小数(如 0.1、0.02),导致构造出的 BigDecimal 本身就带有误差。
2.23 复用代码
避免代码重复,以降低维护成本。
修改前:
public class TestService1 { private void addLog(String info) { if (log.isInfoEnabled()) log.info("info:{}", info); }}// 在 TestService2、TestService3 中重复出现相同方法...public class TestService2 { private void addLog(String info) { if (log.isInfoEnabled()) log.info("info:{}", info); }}修改后:
public class LogUtil { public static void addLog(String info) { if (log.isDebugEnabled()) log.debug("debug:{}", info); }}将公共逻辑集中到工具类中,可以简化维护工作,减少因多处修改导致的遗漏和错误。
2.24 避免在 foreach 循环中删除元素
在 foreach 循环中删除元素会导致 ConcurrentModificationException(并发修改异常)。
正确做法:
Iterator<String> iterator = list.iterator();while (iterator.hasNext()) { String item = iterator.next(); if ("c".equals(item)) { iterator.remove(); // 正确方式:通过 Iterator 删除 }}2.25 控制日志输出
日志过多会迅速占满磁盘空间。
优化前:
log.info("request params:{}", ids);log.info("response:{}", userList);优化后:
if (log.isDebugEnabled()) { log.debug("request params:{}", ids); log.debug("response:{}", userList);}通过条件判断控制日志输出,可以在生产环境中避免大量日志写入导致磁盘溢出,同时在需要时(如调试模式)开启详细日志。
2.26 比较时将常量放在前面
避免在 equals 比较时出现 NullPointerException。
优化前:
if (user.getName().equals("Umesh")) { // ...}优化后:
private static final String FOUND_NAME = "Umesh";if (FOUND_NAME.equals(user.getName())) { // ...}// 或者 // ✅ 推荐:通用 null-safe 比较if (Objects.equals(user.getName(), "Umesh")) { // ...}当变量可能为 null 时,将常量(或确定不为 null 的对象)放在 equals 调用的前面,可以避免 NullPointerException。因为常量 .equals(null) 返回 false,而不会抛出异常。
2.27 使用清晰且一致的命名
命名不当会增加理解成本,导致混淆。
正确示例:
int supplierCount = 1;private static final int MAX_SUPPLIER_COUNT = 100;使用 驼峰命名法(camelCase) 命名变量(如 supplierCount),使用 全大写加下划线(UPPER_SNAKE_CASE) 命名常量(如 MAX_SUPPLIER_COUNT),有助于提升代码可读性和一致性。
2.28 确保SimpleDateFormat线程安全
当共享时SimpleDateFormat 不是线程安全的。
解决方案:
public class SimpleDateFormatService { private static final ThreadLocal<DateFormat> DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public Date time(String time) throws ParseException { return DATE_FORMAT.get().parse(time); }}使用 ThreadLocal 或在方法内创建局部变量可以确保 SimpleDateFormat 的线程安全。
在Java 8+中更推荐如下做饭:
import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;public class DateTimeFormatterService { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public LocalDateTime parse(String time) { return LocalDateTime.parse(time, FORMATTER); } public String format(LocalDateTime time) { return time.format(FORMATTER); }}2.29 避免使用 Executors 创建线程池
Executors 的静态工厂方法可能导致内存溢出(OOM)问题。
正确做法:
ExecutorService threadPool = new ThreadPoolExecutor( 8, // 核心线程数 10, // 最大线程数 60, // 空闲线程存活时间 TimeUnit.SECONDS, // 时间单位 new ArrayBlockingQueue<>(500), // 有界任务队列 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略);通过自定义 ThreadPoolExecutor,可以明确控制线程数量和队列大小,防止资源耗尽。而 Executors 工厂方法(如 newFixedThreadPool、newCachedThreadPool)可能使用无界队列或创建过多线程,导致内存溢出或系统不稳定。
2.30 谨慎使用 Arrays.asList
Arrays.asList 返回的是一个不可修改的列表(unmodifiable list)。
正确做法:
String[] array = {"a", "b", "c"};List<String> list = new ArrayList<>(Arrays.asList(array));list.add("d");或者
// 如果你只是从数组创建可变列表,可以直接用 List.of(更简洁)List<String> list = new ArrayList<>(List.of("a", "b", "c"));list.add("d"); // ✅ 可修改转载请注明来自海坡下载,本文标题:《程序设计方法与优化(30个优化Java代码的技巧)》
京公网安备11000000000001号
京ICP备11000001号
还没有评论,来说两句吧...