45、Java日期时间处理与BigDecimal类的使用

Java日期时间处理与BigDecimal类使用

Java日期时间处理与BigDecimal类的使用

1. Java日期时间处理

1.1 计算两个日期之间的时间差

在Java中, LocalDate 类提供了便捷的方式来计算两个日期之间的时间差,通过 until() 方法可以实现这一功能。该方法会比较两个日期,并返回一个 Period 对象,表示两个日期之间的时间差。以下是示例代码:

import java.time.LocalDate;
import java.time.Period;

public class App {
    public static void main(String[] args) {
        LocalDate ld = LocalDate.now();
        LocalDate otherDate = LocalDate.parse("2022-01-01");
        Period diff = ld.until(otherDate);
        System.out.println("Starting date: " + ld);
        System.out.println("Other date:    " + otherDate);
        System.out.println("==========================");
        System.out.println("Difference: " + diff );
    }
}

运行上述代码时,输出的时间差信息可能比较晦涩,不过 Period 类提供了 getYears() getMonths() getDays() 方法,可以更清晰地获取时间差的年、月、日信息。更新后的代码如下:

import java.time.LocalDate;
import java.time.Period;

public class App {
    public static void main(String[] args) {
        LocalDate ld = LocalDate.now();
        LocalDate otherDate = LocalDate.parse("2022-01-01");
        Period diff = ld.until(otherDate);
        System.out.println("Starting date: " + ld);
        System.out.println("Other date:    " + otherDate);
        System.out.println("==========================");
        System.out.println("Difference: " + diff );
        System.out.println("Years: " + diff.getYears());
        System.out.println("Months: " + diff.getMonths());
        System.out.println("Days: " + diff.getDays());
    }
}

1.2 处理旧日期对象

在旧版本的Java中,使用 Date GregorianCalendar 对象来表示日期,现在可以将它们转换为 LocalDate 对象。

1.2.1 转换 Date 对象

Date 对象转换为 LocalDate 对象需要两个步骤:
1. 将 Date 转换为 Instant ,即将人类时间转换为机器时间。
2. 使用 ZonedDateTime 的静态方法 ofInstant() Instant 转换为 ZonedDateTime 对象,并传入系统默认的时区ID。
3. 调用 ZonedDateTime 对象的 toLocalDate() 方法得到 LocalDate 对象。

示例代码如下:

import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;

public class App {
    public static void main(String[] args) {
        LocalDate ld;
        Date legacyDate = new Date();
        // Step 1 
        ZonedDateTime zdt = ZonedDateTime.ofInstant(
                legacyDate.toInstant(), ZoneId.systemDefault());
        // Step 2
        ld = zdt.toLocalDate();
        System.out.println(legacyDate);
        System.out.println(ld);
    }
}
1.2.2 转换 GregorianCalendar 对象

GregorianCalendar 对象转换为 LocalDate 对象相对简单,只需调用 toZoneDateTime() 方法将其转换为 ZonedDateTime 对象,再调用 toLocalDate() 方法即可。示例代码如下:

import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.GregorianCalendar;

public class App {
    public static void main(String[] args) {
        LocalDate ld;
        GregorianCalendar legacyCalendar = new GregorianCalendar();
        // Step 1 
        ZonedDateTime zdt = legacyCalendar.toZonedDateTime();
        // Step 2
        ld = zdt.toLocalDate();
        System.out.println(legacyCalendar);
        System.out.println(ld);
    }
}

1.3 Java日期时间API总结

Java 8引入的日期时间API有很多优点,以下是一些重要的知识点:
- 使用ISO - 8601日期格式。
- 可以表示人类时间和机器时间。
- 机器时间是从纪元(1970年1月1日)开始的时间线,精确到纳秒。
- 时间周期使用 Duration (机器时间)和 Period (人类时间)两个类来测量。
- LocalDate LocalDateTime 对象使用工厂方法(如 now() parse() )进行实例化。
- 日期时间对象可以进行多种格式的格式化。
- LocalDate 提供了基于现有日期计算未来或过去日期的方法。
- 该API提供了与旧日期时间API相互转换的方法。

1.4 日期时间处理练习

1.4.1 生日计算器

创建一个应用程序,帮助人们计算生日相关信息,具体功能如下:
1. 询问用户的生日(例如:01/01/2002)。
2. 告知用户生日是星期几。
3. 告知用户今年生日是星期几。
4. 告知用户今天的日期以及距离下一个生日的天数。
5. 告知用户的年龄。

示例输出如下:

Welcome to the Magical BirthDAY Calculator!
What's your birthday?
01-01-2002
That means you were born on a TUESDAY!
This year it falls on a MONDAY...
And since today is 12-30-2021, there are only 2 more days until the next one!
Bet you're excited to be turning 20!
1.4.2 世界末日时间计算

编写一个程序,计算从现在到几个预测的世界末日日期(如2026年1月1日、2028年1月1日、2129年12月31日、3239年1月1日)的剩余时间。

1.4.3 时间跟踪

编写一个程序,跟踪阅读下一课所需的时间,具体步骤如下:
1. 准备开始阅读课程时运行程序。
2. 程序记录开始的日期和时间。
3. 在循环中,提示用户“Are you done with the lesson (y/n)?”。
4. 如果用户回答“n”,显示当前时间,告知用户已经花费的小时和分钟数,并再次询问是否完成。
5. 如果用户回答“y”,显示结束的日期和时间,并告知用户总共花费的分钟数。
6. 如果用户回答不是“n”或“y”,提示用户需要回答“n”或“y”。

2. BigDecimal类的使用

2.1 探索BigDecimal类

BigDecimal 类用于表示任意精度的十进制数,它可以设置有效数字的位数,并配置数字的舍入方式,适用于所有涉及货币的计算。 BigDecimal 是不可变类型,一旦设置了值就不能更改,因此任何 BigDecimal 操作的结果都必须赋给另一个 BigDecimal 变量。

2.2 构造BigDecimals对象

BigDecimal 有多种构造函数,这里重点讨论 String 构造函数和 double 构造函数。
- 使用 double 构造函数 :使用 double 值创建 BigDecimal 实例可能会出现问题,因为 double 类型的精度有限。例如,使用 BigDecimal(0.1) 构造实例时,实际存储的值可能并非精确的0.1,示例代码如下:

import java.math.BigDecimal;

public class MyBigDecimal {
    public static void main(String[] args) {
        double x = 0.1;
        BigDecimal myNumber = new BigDecimal(x);
        System.out.println(myNumber);
    }
}

运行上述代码,输出结果可能是类似 0.1000000000000000055511151231257827021181583404541015625 的值,并非预期的0.1。

  • 使用 String 构造函数 :使用 String 构造函数可以避免精度问题,传入字符串 "0.1" 会创建一个值精确为0.1的 BigDecimal 对象,示例代码如下:
import java.math.BigDecimal;

public class MyBigDecimal {
    public static void main(String[] args) {
        BigDecimal myNumber = new BigDecimal("0.1");
        System.out.println(myNumber);
    }
}

运行上述代码,输出结果为0.1。

2.3 理解精度和舍入模式

2.3.1 精度

在处理 BigDecimal 时,理解精度很重要。精度值是小数点右侧的数字位数,例如在处理货币计算时,通常使用精度值2。

2.3.2 舍入模式

BigDecimal 类提供了多种舍入模式,具体使用哪种舍入模式取决于应用程序的业务规则。不同的行业和应用有不同的规则,通常开发团队不应在不了解业务规则的情况下做出决策。以下是 BigDecimal 的舍入模式列表:
| 舍入模式 | 描述 |
| ---- | ---- |
| CEILING | 向正无穷方向舍入 |
| DOWN | 向零方向舍入 |
| FLOOR | 向负无穷方向舍入 |
| HALF_DOWN | 向最近的邻居舍入,若两个邻居距离相等则向下舍入 |
| HALF_EVEN | 向最近的邻居舍入,若两个邻居距离相等则向偶数邻居舍入 |
| HALF_UP | 向最近的邻居舍入,若两个邻居距离相等则向上舍入 |
| UNNECESSARY | 断言请求的操作有精确结果,无需舍入 |
| UP | 远离零方向舍入 |

2.4 使用BigDecimal

2.4.1 设置精度

可以使用 setScale() 方法设置 BigDecimal 对象的精度。以下是示例代码:

import java.math.BigDecimal;

public class App {
    public static void main(String[] args) {
        BigDecimal aNum = new BigDecimal("23.45");
        BigDecimal bNum = aNum.setScale(4);
        System.out.println("aNum = " + aNum);
        System.out.println("bNum = " + bNum);
        System.out.println("aNum scale = " + aNum.scale() );
        System.out.println("bNum scale = " + bNum.scale() );
    }
}

运行上述代码,输出结果如下:

aNum = 23.45
bNum = 23.4500
aNum scale = 2
bNum scale = 4
2.4.2 不指定舍入模式设置精度

如果在设置精度时不指定舍入模式,可能会抛出异常。例如,将 bNum 的赋值改为 BigDecimal bNum = aNum.setScale(1); ,代码会产生错误,因为没有告诉方法在去除小数点右侧第二位数字时使用哪种舍入模式。

2.4.3 舍入BigDecimals

可以使用不同的舍入模式来设置精度。以下是使用 HALF_UP 舍入模式的示例代码:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class App {
    public static void main(String[] args) {
        BigDecimal aNum = new BigDecimal("23.45");
        BigDecimal bNum = aNum.setScale(1, RoundingMode.HALF_UP);
        System.out.println("aNum = " + aNum);
        System.out.println("bNum = " + bNum);
        System.out.println("aNum scale = " + aNum.scale() );
        System.out.println("bNum scale = " + bNum.scale() ); 
    }
}

运行上述代码,输出结果如下:

aNum = 23.45
bNum = 23.5
aNum scale = 2
bNum scale = 1

如果想向下舍入,可以将舍入模式改为 HALF_DOWN ,即 BigDecimal bNum = aNum.setScale(1, RoundingMode.HALF_DOWN);

2.5 BigDecimal操作流程总结

为了更清晰地理解和使用 BigDecimal ,我们可以将其操作流程总结如下:

2.5.1 创建BigDecimal对象
  • 使用 String 构造函数 :推荐使用,能避免精度问题,如 BigDecimal myNumber = new BigDecimal("0.1");
  • 使用其他构造函数( int , long 等) :无精度问题,可按需使用。
2.5.2 设置精度和舍入模式
  • 设置精度 :使用 setScale() 方法,如 BigDecimal bNum = aNum.setScale(4);
  • 指定舍入模式 :在设置精度时,指定合适的舍入模式,如 BigDecimal bNum = aNum.setScale(1, RoundingMode.HALF_UP);
2.5.3 进行计算

由于 BigDecimal 是不可变类型,任何操作结果都需赋给新的 BigDecimal 变量。

以下是一个简单的操作流程mermaid流程图:

graph LR
    A[创建BigDecimal对象] --> B{选择构造函数}
    B -- String --> C[避免精度问题]
    B -- 其他 --> D[无精度问题]
    C --> E[设置精度和舍入模式]
    D --> E
    E --> F[进行计算并赋值给新变量]

2.6 实际应用案例

2.6.1 货币计算

在涉及货币的计算中, BigDecimal 的优势明显。例如,计算商品总价:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class CurrencyCalculation {
    public static void main(String[] args) {
        // 商品单价
        BigDecimal price = new BigDecimal("19.99");
        // 商品数量
        BigDecimal quantity = new BigDecimal("3");
        // 计算总价
        BigDecimal total = price.multiply(quantity);
        // 设置精度为2,使用HALF_UP舍入模式
        total = total.setScale(2, RoundingMode.HALF_UP);
        System.out.println("商品总价: " + total);
    }
}
2.6.2 财务报表计算

在财务报表计算中,需要精确的小数计算。例如,计算利润率:

import java.math.BigDecimal;
import java.math.RoundingMode;

public class FinancialCalculation {
    public static void main(String[] args) {
        // 收入
        BigDecimal revenue = new BigDecimal("10000.00");
        // 成本
        BigDecimal cost = new BigDecimal("8000.00");
        // 利润
        BigDecimal profit = revenue.subtract(cost);
        // 利润率
        BigDecimal profitRate = profit.divide(revenue, 4, RoundingMode.HALF_UP);
        System.out.println("利润率: " + profitRate.multiply(new BigDecimal("100")).toPlainString() + "%");
    }
}

2.7 总结与建议

2.7.1 总结
  • BigDecimal 类是处理任意精度十进制数的理想选择,尤其适用于货币计算和财务报表计算。
  • 使用 String 构造函数创建 BigDecimal 对象可避免精度问题。
  • 理解并根据业务规则选择合适的精度和舍入模式。
2.7.2 建议
  • 在开发涉及小数计算的应用时,优先考虑使用 BigDecimal 类。
  • 在设置精度和舍入模式时,与业务人员沟通,确保符合业务规则。
  • 多进行实际应用练习,加深对 BigDecimal 类的理解和掌握。

3. 综合练习与拓展

3.1 综合练习

结合日期时间处理和 BigDecimal 类的知识,完成以下练习:

编写一个程序,模拟一个简单的财务系统,记录每天的收入和支出,并计算每月的结余。具体要求如下:
1. 程序启动时,提示用户输入当前日期(格式:YYYY - MM - DD)。
2. 允许用户输入每天的收入和支出,使用 BigDecimal 类进行计算。
3. 每月结束时,计算该月的结余,并输出结果。
4. 程序持续运行,直到用户选择退出。

以下是示例代码:

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class FinancialSystem {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入当前日期(YYYY - MM - DD): ");
        String dateStr = scanner.nextLine();
        LocalDate currentDate = LocalDate.parse(dateStr);
        YearMonth currentMonth = YearMonth.from(currentDate);

        Map<LocalDate, BigDecimal[]> dailyRecords = new HashMap<>();

        while (true) {
            System.out.print("请输入收入(输入0退出): ");
            BigDecimal income = new BigDecimal(scanner.nextLine());
            if (income.compareTo(BigDecimal.ZERO) == 0) {
                break;
            }
            System.out.print("请输入支出: ");
            BigDecimal expense = new BigDecimal(scanner.nextLine());

            dailyRecords.put(currentDate, new BigDecimal[]{income, expense});

            if (YearMonth.from(currentDate).isAfter(currentMonth)) {
                // 计算上月结余
                BigDecimal totalIncome = BigDecimal.ZERO;
                BigDecimal totalExpense = BigDecimal.ZERO;
                for (Map.Entry<LocalDate, BigDecimal[]> entry : dailyRecords.entrySet()) {
                    if (YearMonth.from(entry.getKey()).equals(currentMonth)) {
                        totalIncome = totalIncome.add(entry.getValue()[0]);
                        totalExpense = totalExpense.add(entry.getValue()[1]);
                    }
                }
                BigDecimal balance = totalIncome.subtract(totalExpense);
                System.out.println(currentMonth + " 月结余: " + balance.setScale(2, RoundingMode.HALF_UP));
                currentMonth = YearMonth.from(currentDate);
            }

            currentDate = currentDate.plusDays(1);
        }
        scanner.close();
    }
}
3.2 拓展思考
  • 如何优化上述财务系统,使其支持更多的功能,如按年统计、生成报表等?
  • 在多线程环境下,如何确保 BigDecimal 的计算安全?
  • 除了日期时间处理和 BigDecimal 类,还有哪些Java类库可以用于数据处理和计算?

通过不断的练习和拓展思考,我们可以更好地掌握Java中的日期时间处理和 BigDecimal 类的使用,提高编程能力和解决实际问题的能力。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值