最近Java结课大作业,有个附加题,需要我们编写一个Java程序,来从这个API里面获取每天每商品的售价和销量,算出每天总金额,最后算出上个月所有商品的总销量。一步一步,学习,复习了Java的很多知识点,所以特地写个文档记录下来。

Question:

需求:统计上个月所有商品销售的总金额。需要先遍历获取每一天的商品id及商品销售数量,以及获取每个商品id对应的单价,最终加和得到总销售金额。
提供的API:

  1. 获取一天商品的销售情况。http://localhost:11021/line/codetest2/sales/{dateStr}

* dateStr格式为yyyyMMdd
* 返回值为json数据,包括销售商品id,商品销售数量
* 请求示例 http://localhost:11021/line/codetest2/sales/20201204

  1. 获取一个商品的单价。http://localhost:11021/line/codetest2/item/{itemId}

* 返回值为json数据,包括商品id和商品价格
* 请求示例 http://localhost:11021/line/codetest2/item/1

期望的输出:
在控制台输出上个月所有商品销售的总金额,以及统计总耗时。

说明:
* 本地启动mock server: java -jar codetest-mockserver.jar
* 每个接口调用会有一定的耗时,可以想办法尽量减少统计的总耗时。

LocalDate类,DateTimeFormatter类

LocalDate类和LocalDateTime都是java.time包中的类,有无时间时区为区分。

本项目用了LocalDate类来获取上个月的开始日期和截止日期(e.g. 20250501~20250531,并以List<LocalDate>列表的形式储存上个月的每一天来构造API请求。

同时用了DateTimeFormatter来对日期进行格式化,这个类可以将日期转换为字符串,也可以将字符串解析为日期,还可以格式化日期对象。

LocalDate currentDate = LocalDate().now
LocalDate lastMonthStart = currentDate.minusMonths(1).withDayOfMonths(1)//跳转到上个月第一天
LocalDate lasstMonthEnd = lastMonthStart.with(lastDayOfMonth()); //获取上个月最后一天
List<Localdate> dates = new ArrayList<LocalDate>();
while(!lastMonthStart.isAfter(lasstMonthEnd)){
    dates.add(lastMonthStart);
    lastMonthStart = lastMonthStart.plusDays(1);
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");

不可变性

上面提到的LocalDate,LocalDateTimeDateTimeFormatter,以及一些包装类Interger,String都具有不可变性,这些类新建出来的对象,一旦完成创建就不可被修改。如果当这些不可变的类引用了一个可变对象,那就会触发深拷贝

深拷贝?浅拷贝?

浅拷贝,可以类比成C语言中的指针,在进行浅拷贝时,使用浅拷贝创建一个新的对象时,如果是基本类型->直接复制其值;如果是引用类型(List、Object)->复制其引用的内存地址。它实际引用的类型字段和原始的对象相同,也就是说,无论修改原始对象,还是浅拷贝出来的对象,两者中存储的数据都会同时发生改变。

深拷贝,如果是基本类型->直接复制其值;如果是引用类型,会创建一个新的实例,并将原引用类型对象的内容复制到这个新的实例当中,与原始对象完全独立。

List?ArrayList?

这是一个面向接口编程的典型设计。

List是一个接口,其定义了所有列表应该具有的功能,规定了我们可用队列表进行怎么样的操作,比如add(),remove(),get()等等。ArrayList则是符合List接口的一个具体实现类,它以动态数组实现列表。

我们来看看List的三种常见实现:

ArrayList LinkedList Vector
数据结构 动态数组 双向链表 动态数组
优点 快速随机访问 插入和修改元素效率高 线程安全
缺点 插入和删除中间元素效率低,线程不安全 不支持随机访问,线程不安全 插入和删除效率低,性能开销大

关于ArrayList和Vector区别如下:

  1. ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍。
  2. Vector提供indexOf(obj, start)接口,ArrayList没有。
  3. Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销。

HashMap、ConcurrentHashMap

HashMap哈希表是通过一定的哈希算法,将较大的一个状态空间映射到较小的空间,用空间换时间,通过建立key<->value键值对,以实现O(1)的增删改查效率,而ConcurrentHashMap是一种线程安全的哈希表,可用于多线程高并发,具体哈希表原理可见这里

多线程

创建线程有三种方式:

  • 继承Thread类 并重写run()方法,然后使用父类的.start()方法启动进程。

  • 实现Runnable接口,并重写run()方法,然后使用父类的.start()方法启动进程。

  • 实现Callable接口,重写call()方法,然后使用父类的.start()方法启动进程。这种方法可以通过FutureTask来获取返回值,本次实验使用的就是Callable接口实现多线程。

为什么不直接用.run()方法?直接调用.run()方法实际上跟直接执行代码没有区别,只有.start()才会启动线程,并由JVM调用.run()方法。

调用多线程可以大大提升程序并发执行的速度,但是,要注意一个问题——线程安全。本实验在线程中使用ConcurrentHashMap来确保使用哈希表的线程安全。

更详细的多线程参见

线程池

池化技术的思想其实就是减少资源反复创建和销毁产生的性能开销,提高资源复用率,线程池就是用来管理一堆线程,程序需要线程时从线程中取,任务完成后将线程归还池中。

Java通过ThreadPoolExecutor 来创建线程池。线程池中有BlockingQueue(阻塞队列)corePoolSizemaximumPoolSize。一开始线程池是没有任何线程的,除非调用prestartAllCoreThreads方法提前准备好所有核心线程。

如果有个线程通过.execute方法提交了一个任务,线程池会判断当前线程数是否少于核心线程,如果是,则通过ThreadFactory 创建一个线程执行这个任务,执行完成后将其置于corePool中,去阻塞队列中获取任务,如果否,会尝试将任务入队到BlockingQueue中,如果队已满,且池中线程数小于maximumPoolSize,则会创建非核心线程来执行这个任务,如果已达最大线程,则会触发线程池创建时设定的RejectedExecutionHandler拒绝策略,来处理这个任务。

如果提交任务时依旧满足当前线程数是否少于核心线程数,还是会创建新线程来执行新的任务,直到达到预期的核心线程数。

线程池是怎么实现线程复用的呢?

实际上,ThreadPoolExecutor线程池把线程封装成了一个Worker对象,其继承了AQS(?),具有一定锁的特性。在runWorker中写了一个死循环,会不断getTask拿取任务,拿取到任务后会给Worker加锁并执行任务,完成后再释放锁,这样,可以通过判断加锁状态来查看当前进程是否正在运行任务。

objectMapper

objectMapper是一个很著名的JSON解析库Jackson中的对象,可以将JSON转换为Java对象,也可以将Java对象转换为JSON。

序列化(对象->JSON 需要Public getter):

  • writeValueAsString(Object value) 方法,将对象存储成字符串
  • writeValueAsBytes(Object value) 方法,将对象存储成字节数组
  • writeValue(File resultFile, Object value) 方法,将对象存储成文件

反序列化(JSON-> 对象 需要Public setter):

  • readValue(String content, Class<T> valueType) 方法,将字符串反序列化为 Java 对象
  • readValue(byte[] src, Class<T> valueType) 方法,将字节数组反序列化为 Java 对象
  • readValue(File src, Class<T> valueType) 方法,将文件反序列化为 Java 对象

Lambda表达式

Integer price = priceCache.computeIfAbsent(itemId, id -> {
    try {
        String visit = this.visit("http://localhost:11021/line/codetest2/item/" + id);
        Item o = objectMapper.readValue(visit, new TypeReference<>() {});
        return o.getPrice();
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }

Lambda表达式描述了一个代码块(匿名方法),可以将代码块中的参数传给方法执行。有@FunctionalInterface注解的接口可用通过Lambda表达式创建实例。

->是Lambda表达式的标识符。左边为传入参数,如果有多个,用(parm1 , parm2)的形式传入。右边为{ code }代码块,代表着要执行的代码。

异常处理

  • 异常Exception:程序出现了一些可控范围内的问题,可以采取措施挽救。
  • 错误Error:程序出现了严重的问题,Java本身无法处理,例如OOM。

异常有两种类型

  • checked(检查型异常)

    • 编译时就要处理这类异常
    • 一般表示程序可从这个错误中恢复,这个错误由程序外部因素引起,例如找不到文件,数据库连接失败
    • 可以提供一个恢复处理机制
  • unchecked(非检查型异常)

    • 运行时才能被发现
    • 一旦发生,且异常没有被捕获,程序通常就会被终止执行
    • 一般是空指针,数组越界,非法参数等

异常可以用try-catch语句声明,捕获。

try {
    Class clz = Class.forName("com.dango.demo1");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

也可以在方法签名上使用throws关键字声明,使用该方法可能会抛出这个异常,让调用这个方法的人使用try-catch来决定处理异常的方式。

public static void main(String args[]){
    try {
        myMethod1();
    } catch (ArithmeticException e) {
        // 算术异常
    } catch (NullPointerException e) {
        // 空指针异常
    }
}
public static void myMethod1() throws ArithmeticException, NullPointerException{
    // 方法签名上声明异常
}

throw关键字用于显式抛出异常

throw new ArithmeticException("年纪未满 18 岁,禁止观影");

try-catch-finallyfinally块可以用来写一些始终要执行的代码,即使try中执行了return、break、continue等,finally也会被执行。除非,遇到了死循环和System.exit()

try {
    // 可能发生异常的代码
}catch {
   // 异常处理
}finally {
   // 必须执行的代码
}

try-with-resources可以用来自动释放资源,不需要手动在finally块中进行释放,且确保不会丢失任何异常,前提是,需要释放的资源实现了AutoCloseable接口,然后把要释放的资源写在try后的括号中。

try (IOStream io = new IOStream(fileLocation);){
    String str = null;
    while ((str = io.read()) != null) {
        System.out.println(str);
    }
} catch (IoException e){
    e.printStackTrace();
}