1. Java进阶核心技能解析
作为从业十年的Java开发者,我经常被问到"学完基础语法后该掌握哪些核心技能"。今天我们就来聊聊Java进阶路上必须啃下的三块硬骨头:时间日期处理、包装类机制和正则表达式。这些知识点看似基础,但实际项目中处处是坑,处理不好轻则功能异常,重则系统崩溃。
记得我刚工作时就踩过Date类的时区坑,导致海外用户看到的订单时间全部错乱。后来在金融项目里又因为Integer的缓存机制闹出过线程安全问题。至于正则表达式,更是一把双刃剑——用好了能优雅处理复杂文本,用不好直接让CPU跑满。下面我就结合这些血泪教训,带大家系统掌握这些进阶技能。
2. 时间与日期处理全攻略
2.1 新旧API对比与选型
Java 8之前的日期时间API主要有三大痛点:
- Date类既包含日期又包含时间,而且月份从0开始计算
- SimpleDateFormat非线程安全
- 时区处理需要手动计算
// 老API的典型问题 Date date = new Date(2023, 1, 1); // 实际表示2023年2月1日 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String str = sdf.format(date); // 线程不安全Java 8引入的java.time包完美解决了这些问题:
- LocalDate:纯日期
- LocalTime:纯时间
- LocalDateTime:日期+时间
- ZonedDateTime:带时区的完整时间
- DateTimeFormatter:线程安全的时间格式化
重要提示:新项目务必使用java.time包,旧项目改造时要注意Date和Instant的相互转换
2.2 时区处理的正确姿势
全球化的系统必须正确处理时区,常见坑点包括:
- 数据库存储时不带时区信息
- 前端传参时未明确时区
- 夏令时转换遗漏
// 推荐做法:统一使用UTC时间存储 ZonedDateTime utcTime = ZonedDateTime.now(ZoneOffset.UTC); // 展示时转换为本地时区 ZonedDateTime localTime = utcTime.withZoneSameInstant(ZoneId.of("Asia/Shanghai"));2.3 日期计算的实用技巧
- 计算两个日期之间的天数:
long days = ChronoUnit.DAYS.between(startDate, endDate);- 获取当月最后一天:
LocalDate lastDay = YearMonth.now().atEndOfMonth();- 工作日计算(排除周末):
public static long getWorkingDays(LocalDate start, LocalDate end) { return Stream.iterate(start, date -> date.plusDays(1)) .limit(ChronoUnit.DAYS.between(start, end)) .filter(d -> d.getDayOfWeek() != DayOfWeek.SATURDAY && d.getDayOfWeek() != DayOfWeek.SUNDAY) .count(); }3. 包装类深度解析
3.1 自动装箱拆箱原理
Java的自动装箱拆箱其实是编译器提供的语法糖:
Integer i = 10; // 实际编译为 Integer.valueOf(10) int n = i; // 实际编译为 i.intValue()但这里藏着一个性能陷阱:
Long sum = 0L; // 每次循环都会拆箱、计算、装箱 for(long i=0; i<Integer.MAX_VALUE; i++){ sum += i; } // 应该改为基本类型long3.2 缓存机制与对象复用
Integer缓存-128到127之间的值:
Integer a = 127; Integer b = 127; System.out.println(a == b); // true Integer c = 128; Integer d = 128; System.out.println(c == d); // false这种缓存机制会导致一些反直觉的结果:
Integer x = 100; Integer y = 100; Integer z = new Integer(100); System.out.println(x == y); // true System.out.println(x == z); // false最佳实践:包装类比较始终使用equals()方法
3.3 常见NPE防范
包装类最常见的坑就是NullPointerException:
public class Order { private Integer discount; // 可能为null public int getRealPrice(int price) { return price - discount; // 当discount为null时抛出NPE } }解决方法:
- 使用基本类型避免null
- 添加null检查
- 使用Optional包装
// 改进方案 public int getRealPrice(int price) { return price - (discount != null ? discount : 0); }4. 正则表达式实战精要
4.1 基础语法速记表
| 元字符 | 说明 | 示例 |
|---|---|---|
| . | 任意字符(除换行外) | a.c匹配abc |
| \d | 数字[0-9] | \d{3}匹配123 |
| \w | 单词字符[a-zA-Z0-9_] | \w+匹配hello_1 |
| \s | 空白字符 | a\sb匹配a b |
| ^ | 字符串开始 | ^Java匹配开头 |
| $ | 字符串结束 | end$匹配结尾 |
4.2 性能优化技巧
- 预编译正则表达式:
// 错误做法:每次调用都重新编译 boolean isValid = "123".matches("\\d+"); // 正确做法:预编译 private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); boolean isValid = NUMBER_PATTERN.matcher("123").matches();- 避免贪婪匹配:
// 贪婪匹配(默认) Pattern.compile("<.*>").matcher("<h1>title</h1>").find(); // 匹配整个字符串 // 懒惰匹配 Pattern.compile("<.*?>").matcher("<h1>title</h1>").find(); // 分别匹配<h1>和</h1>- 使用非捕获组提高性能:
// 捕获组(消耗资源) "(\\d{4})-(\\d{2})-(\\d{2})" // 非捕获组 "(?:\d{4})-(?:\d{2})-(?:\d{2})"4.3 实用案例集锦
- 手机号验证(中国大陆):
String regex = "^1[3-9]\\d{9}$";- 提取HTML中的链接:
Pattern.compile("<a\\s+href=\"([^\"]+)\"[^>]*>").matcher(html) .results() .map(m -> m.group(1)) .collect(Collectors.toList());- 复杂密码校验(至少8位,含大小写字母和数字):
String regex = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$";- 日志时间戳提取:
Pattern.compile("\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]") .matcher(log) .results() .forEach(m -> System.out.println(m.group(1)));5. 综合应用与避坑指南
5.1 日期验证的正则陷阱
很多开发者喜欢用正则验证日期,但这存在严重问题:
// 不推荐的正则验证 String dateRegex = "^\\d{4}-\\d{2}-\\d{2}$"; "2023-02-30".matches(dateRegex); // 会通过验证 // 推荐做法:使用专门的日期API public static boolean isValidDate(String dateStr) { try { LocalDate.parse(dateStr); return true; } catch (DateTimeParseException e) { return false; } }5.2 包装类在集合中的使用
集合不能存储基本类型,但要注意自动装箱的性能开销:
// 性能较差的写法 List<Integer> numbers = new ArrayList<>(); for (int i = 0; i < 1_000_000; i++) { numbers.add(i); // 发生自动装箱 } // 优化方案:使用原始类型数组 int[] numbers = new int[1_000_000]; for (int i = 0; i < numbers.length; i++) { numbers[i] = i; }5.3 时区转换的黄金法则
- 存储:统一使用UTC时间
- 传输:时间戳或带时区的字符串
- 展示:根据用户时区转换
// 存储时转换为UTC ZonedDateTime utcTime = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC); // 展示时转换回本地时间 ZonedDateTime localTime = utcTime.withZoneSameInstant(ZoneId.of("America/New_York"));5.4 正则表达式的安全风险
复杂正则可能导致ReDoS(正则表达式拒绝服务)攻击:
// 危险的正则:嵌套量词 String evilRegex = "(a+)+"; // 恶意输入会导致长时间匹配 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab".matches(evilRegex);防范措施:
- 避免嵌套量词
- 设置超时限制(Java 9+)
Pattern.compile(regex) .matcher(new TimeLimitedCharSequence(input, 1000)) // 自定义超时检查 .matches();6. 测试你的理解
检验一下你是否真正掌握了这些知识点:
- 下面的代码输出什么?为什么?
Integer a = 100; Integer b = 100; Integer c = 200; Integer d = 200; System.out.println(a == b); System.out.println(c == d);如何用Java 8的API获取下个月第一天的零点时间?
编写一个正则表达式,匹配符合以下要求的密码:
- 至少8个字符
- 包含至少一个大写字母
- 包含至少一个小写字母
- 包含至少一个数字
- 包含至少一个特殊字符(!@#$%^&*)
下面的日期处理代码有什么问题?
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executor.submit(() -> { try { System.out.println(sdf.parse("2023-01-01")); } catch (Exception e) { e.printStackTrace(); } }); }在多年Java开发中,我发现这些"基础"知识点反而是最容易出问题的。特别是时间处理,几乎每个项目都会遇到时区问题。建议大家在掌握这些知识后,一定要在实际项目中多加练习,遇到问题时多查阅官方文档。记住,好的开发者不是不犯错,而是能从错误中快速学习。