1. 项目概述
作为一名Java开发者,我经常遇到需要处理日期、数字格式和基本数据类型转换的场景。这些看似基础的功能在实际开发中却经常成为"坑点"。今天我想系统梳理Java中这三个核心工具类的使用技巧,分享一些我在项目中积累的实战经验。
日期处理是任何业务系统都绕不开的话题,从简单的日志记录到复杂的定时任务调度,都需要精确的日期时间操作。数字格式化则直接影响着金融、电商等领域的金额展示和计算精度。而包装类作为基本数据类型的对象化实现,在集合操作、泛型使用等场景中扮演着关键角色。
这三个主题虽然基础,但掌握它们的正确使用方式可以避免很多隐蔽的bug。比如SimpleDateFormat的线程安全问题、BigDecimal的精度丢失问题、自动装箱拆箱的性能问题等,都是新手容易踩的坑。接下来我将结合具体案例,详细解析这些类的核心用法和注意事项。
2. 日期时间处理类详解
2.1 Date与Calendar的传统用法
Java最早的日期处理类是java.util.Date,它的设计存在不少缺陷。比如年份从1900年开始计算,月份从0开始计数等反人类设计。虽然现在推荐使用新的时间API,但很多遗留系统仍然在使用Date,所以我们还是需要了解它的基本用法:
java复制// 创建当前时间的Date对象
Date now = new Date();
System.out.println(now); // 输出类似:Thu May 25 14:30:22 CST 2023
// 获取时间戳(毫秒数)
long timestamp = now.getTime();
Calendar类作为Date的补充,提供了更丰富的日期操作方法:
java复制Calendar calendar = Calendar.getInstance();
calendar.set(2023, Calendar.MAY, 25); // 注意月份从0开始
// 获取各字段值
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1; // 需要+1
int day = calendar.get(Calendar.DAY_OF_MONTH);
重要提示:SimpleDateFormat是线程不安全的!在多线程环境下必须为每个线程创建独立实例,或者使用ThreadLocal进行封装。这是很多线上事故的根源。
2.2 Java 8全新时间API
Java 8引入了全新的java.time包,解决了旧API的各种问题。核心类包括:
- LocalDate:只包含日期,如2023-05-25
- LocalTime:只包含时间,如14:30:22
- LocalDateTime:包含日期和时间
- ZonedDateTime:带时区的日期时间
- Instant:时间戳(精确到纳秒)
java复制// 获取当前日期时间
LocalDateTime now = LocalDateTime.now();
// 创建指定日期
LocalDate date = LocalDate.of(2023, 5, 25);
// 日期运算
LocalDate nextWeek = date.plusWeeks(1);
// 格式化输出
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(formatter);
新API的优势:
- 不可变对象,线程安全
- 更直观的方法命名(plusDays()、minusMonths()等)
- 更好的时区处理
- 更精确的时间单位(纳秒级)
2.3 日期格式化与解析
日期字符串的相互转换是常见需求。Java 8推荐使用DateTimeFormatter:
java复制// 预定义格式
DateTimeFormatter isoFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
// 自定义格式
DateTimeFormatter customFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
// 格式化
String str = LocalDateTime.now().format(customFormatter);
// 解析
LocalDateTime dt = LocalDateTime.parse("2023/05/25 14:30", customFormatter);
对于旧版SimpleDateFormat,使用时要注意:
- 不要定义为static变量
- 每次使用前最好重置格式(调用set2DigitYearStart等方法)
- 考虑使用FastDateFormat等线程安全替代品
3. 数字格式化类解析
3.1 NumberFormat与子类
NumberFormat是数字格式化的抽象基类,常用的子类有:
- DecimalFormat:十进制数格式化
- ChoiceFormat:区间映射格式化
- CompactNumberFormat:紧凑格式化(如1K代替1000)
基本使用示例:
java复制// 获取默认数值格式
NumberFormat nf = NumberFormat.getInstance();
// 设置小数位数
nf.setMaximumFractionDigits(2);
nf.setMinimumFractionDigits(2);
String result = nf.format(1234.5678); // "1,234.57"
3.2 DecimalFormat深度使用
DecimalFormat提供了更灵活的数字格式化能力:
java复制DecimalFormat df = new DecimalFormat("#,##0.00");
df.setPositivePrefix("+"); // 正数前缀
df.setNegativePrefix("-"); // 负数前缀
System.out.println(df.format(1234.5)); // +1,234.50
System.out.println(df.format(-1234.5)); // -1,234.50
模式字符说明:
- 0:数字,不足补零
- #:数字,不补零
- ,:分组分隔符
- %:百分比格式
- ¤:货币符号
3.3 货币与百分比格式化
java复制// 货币格式化
NumberFormat cf = NumberFormat.getCurrencyInstance(Locale.US);
System.out.println(cf.format(1234.56)); // $1,234.56
// 百分比格式化
NumberFormat pf = NumberFormat.getPercentInstance();
pf.setMinimumFractionDigits(1);
System.out.println(pf.format(0.1234)); // 12.3%
注意:格式化时默认使用JVM默认Locale,如果需要特定地区的格式,务必显式指定Locale参数。
4. 包装类与自动装箱拆箱
4.1 八大基本类型的包装类
Java为每个基本类型提供了对应的包装类:
| 基本类型 | 包装类 | 大小 | 默认值 |
|---|---|---|---|
| byte | Byte | 8位 | 0 |
| short | Short | 16位 | 0 |
| int | Integer | 32位 | 0 |
| long | Long | 64位 | 0L |
| float | Float | 32位 | 0.0f |
| double | Double | 64位 | 0.0d |
| char | Character | 16位 | '\u0000' |
| boolean | Boolean | - | false |
4.2 自动装箱与拆箱机制
Java 5引入了自动装箱(Autoboxing)和拆箱(Unboxing):
java复制// 自动装箱
Integer i = 10; // 实际调用Integer.valueOf(10)
// 自动拆箱
int n = i; // 实际调用i.intValue()
这个特性虽然方便,但也带来了性能问题和一些陷阱:
java复制Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true (缓存范围-128~127)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false (超出缓存范围)
4.3 包装类的常用方法
各包装类提供了一些实用的静态方法:
java复制// 字符串转数字
int num = Integer.parseInt("123");
double d = Double.parseDouble("3.14");
// 进制转换
String binary = Integer.toBinaryString(10); // "1010"
String hex = Integer.toHexString(255); // "ff"
// 比较方法
int compare = Integer.compare(10, 20); // -1 (10 < 20)
// 无符号运算
long unsigned = Integer.toUnsignedLong(-1); // 4294967295
5. 实战中的常见问题与解决方案
5.1 日期时间处理陷阱
-
时区问题:服务器时区与应用时区不一致
- 解决方案:明确指定时区,使用ZonedDateTime
- 示例:
ZonedDateTime.now(ZoneId.of("Asia/Shanghai"))
-
夏令时问题:某些时区会实行夏令时
- 解决方案:使用时区数据库,避免手动计算
-
性能问题:频繁创建SimpleDateFormat实例
- 解决方案:使用ThreadLocal或DateTimeFormatter
5.2 数字精度问题
-
浮点数精度丢失:
java复制System.out.println(0.1 + 0.2); // 0.30000000000000004- 解决方案:使用BigDecimal进行精确计算
- 正确做法:
new BigDecimal("0.1").add(new BigDecimal("0.2"))
-
舍入模式选择:
- 银行家舍入(ROUND_HALF_EVEN)是最常用的舍入方式
- 示例:
df.setRoundingMode(RoundingMode.HALF_EVEN)
5.3 包装类使用注意事项
-
NPE风险:
java复制Integer num = null; int n = num; // 抛出NullPointerException- 防御措施:自动拆箱前做null检查
-
缓存机制:
- Integer缓存-128~127的对象,超出范围会创建新对象
- 比较应使用equals()而非==
-
性能优化:
- 循环内避免频繁装箱拆箱
- 集合优先使用基本类型特化版本(如IntStream)
6. 最佳实践与性能优化
6.1 日期处理最佳实践
-
新旧API选择:
- 新项目一律使用java.time
- 旧系统逐步迁移,可使用转换方法:
java复制Date date = Date.from(instant); Instant instant = date.toInstant();
-
常用操作封装:
java复制public class DateUtils { private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public static String formatNow() { return LocalDateTime.now().format(DEFAULT_FORMATTER); } }
6.2 数字处理优化技巧
-
BigDecimal使用规范:
- 避免使用double构造器:
new BigDecimal(0.1)→ 精度已丢失 - 推荐使用String构造器:
new BigDecimal("0.1")
- 避免使用double构造器:
-
数值格式化复用:
java复制private static final NumberFormat CURRENCY_FORMAT = NumberFormat.getCurrencyInstance(Locale.US); static { CURRENCY_FORMAT.setMaximumFractionDigits(2); }
6.3 包装类性能优化
-
基本类型优先:
- 方法参数和局部变量尽量使用基本类型
- 特别是循环体内避免包装类
-
集合优化:
- 考虑使用Trove、FastUtil等第三方库
- 或者使用Java 8的原始类型特化流:
java复制IntStream.range(0, 100).sum();
-
对象复用:
- 对于常用值(如Boolean.TRUE/FALSE)直接使用静态实例
- 在缓存范围内的Integer使用valueOf()方法
7. 综合应用案例
7.1 电商订单金额计算
java复制public class OrderCalculator {
private static final DecimalFormat AMOUNT_FORMAT = new DecimalFormat("#,##0.00");
private static final NumberFormat CURRENCY_FORMAT =
NumberFormat.getCurrencyInstance(Locale.CHINA);
public String calculateTotal(List<BigDecimal> prices) {
BigDecimal total = prices.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 折扣计算(保留2位小数,四舍五入)
BigDecimal discounted = total.multiply(new BigDecimal("0.95"))
.setScale(2, RoundingMode.HALF_UP);
return "原价:" + AMOUNT_FORMAT.format(total) +
",折后价:" + CURRENCY_FORMAT.format(discounted);
}
}
7.2 日志时间戳处理
java复制public class LogProcessor {
private static final DateTimeFormatter LOG_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
public void processLog(String logLine) {
// 解析日志时间
String timeStr = logLine.substring(0, 23);
LocalDateTime logTime = LocalDateTime.parse(timeStr, LOG_FORMATTER);
// 计算时间差
Duration duration = Duration.between(logTime, LocalDateTime.now());
long minutes = duration.toMinutes();
System.out.println("日志产生于" + minutes + "分钟前");
}
}
7.3 统计数据分析
java复制public class DataAnalyzer {
public void analyze(int[] data) {
IntSummaryStatistics stats = IntStream.of(data).summaryStatistics();
System.out.println("记录数: " + stats.getCount());
System.out.println("最小值: " + stats.getMin());
System.out.println("最大值: " + stats.getMax());
System.out.println("平均值: " + stats.getAverage());
}
}
在实际项目中,日期、数字和包装类的使用无处不在。掌握它们的正确用法不仅能避免很多隐蔽的bug,还能写出更高效、更健壮的代码。特别是在金融、电商、物联网等领域,对时间和精度的处理要求极高,这些基础类库的深入理解就显得尤为重要。
最后分享一个实用技巧:在处理复杂日期逻辑时,可以借助Joda-Time库(Java 8之前)或java.time.temporal.TemporalAdjusters类来实现更灵活的日期调整,比如获取当月最后一天、下一个工作日等常见需求。这些工具类可以大大简化业务代码的编写。