博客记录-day012-Comparator、WeakHashMap、JavaIO+MyBatis

65 阅读17分钟

一、沉默王二-集合框架结尾+javaIO

1、java Comparable和Comparator

1)comparable

Comparable 接口的定义非常简单,源码如下所示。

public interface Comparable<T> {
    int compareTo(T t);
}

如果一个类实现了 Comparable 接口(只需要干一件事,重写 compareTo() 方法),就可以按照自己制定的规则将由它创建的对象进行比较。 compareTo() 方法,该方法的返回值可能为负数,零或者正数,代表的意思是该对象按照排序的规则小于、等于或者大于要比较的对象。

2)Comparator

Comparator 接口的定义相比较于 Comparable 就复杂的多了,不过,核心的方法只有两个,来看一下源码。

public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
}

第一个方法 compare(T o1, T o2) 的返回值可能为负数,零或者正数,代表的意思是第一个对象小于、等于或者大于第二个对象。

第二个方法 equals(Object obj) 需要传入一个 Object 作为参数,并判断该 Object 是否和 Comparator 保持一致。

有时候,我们想让类保持它的原貌,不想主动实现 Comparable 接口,但我们又需要它们之间进行比较,该怎么办呢?使用Comparator,重写compare方法。

Comparator 接口的实现类:

public class CmowerComparator implements Comparator<Cmower> {
    @Override
    public int compare(Cmower o1, Cmower o2) {
        return o1.getAge() - o2.getAge();
    }
}

再来看测试类:

Cmower wanger = new Cmower(19,"沉默王二");
Cmower wangsan = new Cmower(16,"沉默王三");
Cmower wangyi = new Cmower(28,"沉默王一");

List<Cmower> list = new ArrayList<>();
list.add(wanger);
list.add(wangsan);
list.add(wangyi);

list.sort(new CmowerComparator());

for (Cmower c : list) {
    System.out.println(c.getName());
}

再来看一下 sort 方法的源码:

public void sort(Comparator<? super E> c) {
    // 保存当前队列的 modCount 值,用于检测 sort 操作是否非法
    final int expectedModCount = modCount;
    // 调用 Arrays.sort 对 elementData 数组进行排序,使用传入的比较器 c
    Arrays.sort((E[]) elementData, 0, size, c);
    // 检查操作期间 modCount 是否被修改,如果被修改则抛出并发修改异常
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
    // 增加 modCount 值,表示队列已经被修改过
    modCount++;
}

可以看到,参数就是一个 Comparator 接口,并且使用了泛型 Comparator<? super E> c

3)用哪个?

通过上面的两个例子可以比较出 Comparable 和 Comparator 两者之间的区别:

  • 一个类实现了 Comparable 接口,意味着该类的对象可以直接进行比较(排序),但比较(排序)的方式只有一种,很单一。
  • 一个类如果想要保持原样,又需要进行不同方式的比较(排序),就可以定制比较器(实现 Comparator 接口)。
  • Comparable 接口在 java.lang 包下,而 Comparator 接口在 java.util 包下,算不上是亲兄弟,但可以称得上是表(堂)兄弟。

好了,关于 Comparable 和 Comparator 我们就先聊这么多。总而言之,如果对象的排序需要基于自然顺序,请选择 Comparable,如果需要按照对象的不同属性进行排序,请选择 Comparator

2、WeakHashMap

WeakHashMap其实和HashMap大多数行为是一样的,只是WeakHashMap不会阻止GC回收key对象(不是value),那么WeakHashMap是怎么做到的呢,这就是我们研究的主要问题。

在开始WeakHashMap之前,我们先要对弱引用有一定的了解。

在Java中,有四种引用类型

  • 强引用(Strong Reference),我们正常编码时默认的引用类型,强应用之所以为强,是因为如果一个对象到GC Roots强引用可到达,就可以阻止GC回收该对象
  • 软引用(Soft Reference)阻止GC回收的能力相对弱一些,如果是软引用可以到达,那么这个对象会停留在内存更时间上长一些。当内存不足时垃圾回收器才会回收这些软引用可到达的对象
  • 弱引用(WeakReference)无法阻止GC回收,如果一个对象时弱引用可到达,那么在下一个GC回收执行时,该对象就会被回收掉。
  • 虚引用(Phantom Reference)十分脆弱,它的唯一作用就是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁

这其中还有一个概念叫做引用队列(Reference Queue)

  • 一般情况下,一个对象标记为垃圾(并不代表回收了)后,会加入到引用队列
  • 对于虚引用来说,它指向的对象会只有被回收后才会加入引用队列,所以可以用作记录该引用指向的对象是否回收。

3、JavaIO

IO,即in和out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。

Java 中是通过流处理IO 的,那么什么是流?

流(Stream),是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。

当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。

一般来说关于流的特性有下面几点:

  • 先进先出:最先写入输出流的数据最先被输入流读取到。
  • 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)
  • 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。

1)传输方式划分

传输方式有两种,字节和字符,那首先得搞明白字节和字符有什么区别,对吧?

  • 字节(byte)是计算机中用来表示存储容量的一个计量单位,通常情况下,一个字节有 8 位(bit)。

  • 字符(char)可以是计算机中使用的字母、数字、和符号,比如说 A 1 $ 这些。

通常来说,一个字母或者一个字符占用一个字节,一个汉字占用两个字节。

明白了字节与字符的区别,再来看字节流和字符流就会轻松多了。

  • 字节流用来处理二进制文件,比如说图片啊、MP3 啊、视频啊。

  • 字符流用来处理文本文件,文本文件可以看作是一种特殊的二进制文件,只不过经过了编码,便于人们阅读。

换句话说就是,字节流可以处理一切文件,而字符流只能处理文本。

虽然 IO 类很多,但核心的就是 4 个抽象类:InputStreamOutputStreamReaderWriter

虽然 IO 类的方法也很多,但核心的也就 2 个:read 和 write。

InputStream 类

  • int read():读取数据
  • int read(byte b[], int off, int len):从第 off 位置开始读,读取 len 长度的字节,然后放入数组 b 中
  • long skip(long n):跳过指定个数的字节
  • int available():返回可读的字节数
  • void close():关闭流,释放资源

OutputStream 类

  • void write(int b): 写入一个字节,虽然参数是一个 int 类型,但只有低 8 位才会写入,高 24 位会舍弃(这块后面再讲)
  • void write(byte b[], int off, int len): 将数组 b 中的从 off 位置开始,长度为 len 的字节写入
  • void flush(): 强制刷新,将缓冲区的数据写入
  • void close():关闭流

Reader 类

  • int read():读取单个字符
  • int read(char cbuf[], int off, int len):从第 off 位置开始读,读取 len 长度的字符,然后放入数组 b 中
  • long skip(long n):跳过指定个数的字符
  • int ready():是否可以读了
  • void close():关闭流,释放资源

Writer 类

  • void write(int c): 写入一个字符
  • void write( char cbuf[], int off, int len): 将数组 cbuf 中的从 off 位置开始,长度为 len 的字符写入
  • void flush(): 强制刷新,将缓冲区的数据写入
  • void close():关闭流

字节流和字符流的区别:

  • 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。
  • 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。

2)操作对象划分

IO IO,不就是输入输出(Input/Output)嘛:

  • Input:将外部的数据读入内存,比如说把文件从硬盘读取到内存,从网络读取数据到内存等等
  • Output:将内存中的数据写入到外部,比如说把数据从内存写入到文件,把数据从内存输出到网络等等。

所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部,比如说文件。

文件操作算是 IO 中最典型的操作了,也是最频繁的操作。那其实你可以换个角度来思考,比如说按照 IO 的操作对象来思考,IO 就可以分类为:文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换等。

1、文件

文件流也就是直接操作文件的流,可以细分为字节流(FileInputStreamFileOuputStream)和字符流(FileReader 和 FileWriter)。

2、数组(内存)

通常来说,针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流,分为ByteArrayOutputStreamByteArrayInputStream

数组流可以用于在内存中读写数据,比如将数据存储在字节数组中进行压缩、加密、序列化等操作。它的优点是不需要创建临时文件,可以提高程序的效率。但是,数组流也有缺点,它只能存储有限的数据量,如果存储的数据量过大,会导致内存溢出。

3、管道

Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的进程之间可以通过管道来通信,但 Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力

一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的PipedInputStream读取出来。

使用管道流可以实现不同线程之间的数据传输,可以用于线程间的通信、数据的传递等。但是,管道流也有一些局限性,比如只能在同一个 JVM 中的线程之间使用,不能跨越不同的 JVM 进程。

4、基本数据类型

基本数据类型输入输出流是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型。 DataInputStream 提供了一系列可以读基本数据类型的方法。DataOutputStream 提供了一系列可以写基本数据类型的方法。

除了 DataInputStreamDataOutputStream,Java IO 还提供了其他一些读写基本数据类型和字符串的流类,包括 ObjectInputStreamObjectOutputStream(用于读写对象)。

5、缓冲

CPU 很快,它比内存快 100 倍,比磁盘快百万倍。那也就意味着,程序和内存交互会很快,和硬盘交互相对就很慢,这样就会导致性能问题。

为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的那些,比如说 BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter

缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。简单来说,就是一次多读/写点,少读/写几次,这样程序的性能就会提高。

  • 使用 BufferedInputStream读取文件:

    • 首先创建了一个 BufferedInputStream 对象,用于从文件中读取数据。然后创建了一个字节数组作为缓存区,每次读取数据时将数据存储到缓存区中。读取数据的过程是通过 while 循环实现的,每次读取数据后对缓存区中的数据进行处理。最后关闭 BufferedInputStream,释放资源。
  • 使用 BufferedOutputStream 写入文件:

    • 首先创建了一个 BufferedOutputStream 对象,用于将数据写入到文件中。然后创建了一个字节数组作为缓存区,将数据写入到缓存区中。写入数据的过程是通过 write() 方法实现的,将字节数组作为参数传递给 write()方法即可。 最后,通过 flush() 方法将缓存区中的数据写入到文件中。在写入数据时,由于使用了 BufferedOutputStream数据会先被写入到缓存区中,只有在缓存区被填满或者调用了 flush() 方法时才会将缓存区中的数据写入到文件中。
  • 使用 BufferedReader 读取文件:

    • 上述代码中,首先创建了一个 BufferedReader 对象,用于从文件中读取数据。然后使用readLine() 方法读取文件中的数据,每次读取一行数据并将其存储到一个字符串中。读取数据的过程是通过 while 循环实现的。
  • 使用 BufferedWriter 写入文件:

    • 首先创建了一个 BufferedWriter 对象,用于将数据写入到文件中。然后使用 write() 方法将数据写入到缓存区中,写入数据的过程和使用 FileWriter 类似。需要注意的是,使用 BufferedWriter 写入数据时,数据会先被写入到缓存区中,只有在缓存区被填满或者调用了flush() 方法时才会将缓存区中的数据写入到文件中。最后,通过 flush() 方法将缓存区中的数据写入到文件中,并通过 close() 方法关闭 BufferedWriter,释放资源。
6、打印

Java 的打印流是一组用于打印输出数据的类,包括 PrintStreamPrintWriter 两个类。

恐怕 Java 程序员一生当中最常用的就是打印流了:System.out 其实返回的就是一个 PrintStream 对象,可以用来打印各式各样的对象。

System.out.println("沉默王二是真的二!");

PrintStream 最终输出的是字节数据,而 PrintWriter 则是扩展了 Writer 接口,所以它的 print()/println() 方法最终输出的是字符数据。使用上几乎和 PrintStream 一模一样。

StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
    pw.println("沉默王二");
}
System.out.println(buffer.toString());
7、对象序列化/反序列化

序列化本质上是将一个 Java 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程。 与其对应的,有序列化,就有反序列化,也就是再将字节数组转成 Java 对象的过程。

代码使用了 Java 的 ByteArrayOutputStreamObjectOutputStream 类,将字符串 "沉默王二" 写入到一个字节数组缓冲区中,并将缓冲区中的数据转换成字节数组输出到控制台。

8)转换

InputStreamReader从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符。

OutputStreamWriter 将一个字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁。

在使用转换流时需要注意字符编码的问题。如果不指定字符编码,则使用默认的字符编码,可能会出现乱码问题。因此,建议在使用转换流时,始终指定正确的字符编码,以避免出现乱码问题。

二、小博哥-开发基础

1、MyBatis

为了更好的把 MyBatis 常用的各项功能体现的清晰明了,小傅哥这里设定了公司雇员和对应薪酬关系的一个开发场景。

  • 首先,雇员员工和对应的薪资待遇,是一个1v1的关系。
  • 之后,薪资表与调薪表,是一个1vn的关系。每次晋升、普调,都会有一条对应的调薪记录。
  • 最后,有了这样3个表,我们就可以很好的完成,员工的插入、批量插入,和事务操作调薪。

DDD模型定义:

此场景的业务用于对指定的用户进行晋升加薪调幅,但因为加薪会需要操作3个表,包括:雇员表 - 修改个人Title、薪资表 - 修改薪酬、调薪记录表 - 每一次加薪都写一条记录。

1) 插入&批量插入

源码cn.bugstack.xfg.dev.tech.infrastructure.dao.IEmployeeDAO

@Mapper
public interface IEmployeeDAO {

    void insert(EmployeePO employee);

    void insertList(List<EmployeePO> list);

    void update(EmployeePO employeePO);

    EmployeePO queryEmployeeByEmployNumber(String employNumber);

}

xmlemployee_mapper.xml

<insert id="insert" parameterType="cn.bugstack.xfg.dev.tech.infrastructure.po.EmployeePO">
    INSERT INTO employee(employee_number, employee_name, employee_level, employee_title, create_time, update_time)
    VALUES(#{employeeNumber}, #{employeeName}, #{employeeLevel}, #{employeeTitle}, now(), now())
</insert>

<insert id="insertList" parameterType="java.util.List">
    INSERT INTO employee(employee_number, employee_name, employee_level, employee_title, create_time, update_time)
    VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.employeeNumber}, #{item.employeeName}, #{item.employeeLevel}, #{item.employeeTitle}, now(), now())
    </foreach>
</insert>   
  • 使用配置文件的方式比较好维护,当然也可以尝试使用 MyBatis 提供的注解方式,完成数据的操作。

2)事务注解

1、注解事务

源码cn.bugstack.xfg.dev.tech.infrastructure.repository.SalaryAdjustRepository

// 声明一个事务,指定在发生异常时回滚,设置超时时间为350秒,传播行为为REQUIRED,隔离级别为DEFAULT  
@Transactional(rollbackFor = Exception.class, timeout = 350, propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)  
public String adjustSalary(AdjustSalaryApplyOrderAggregate adjustSalaryApplyOrderAggregate) {  
    // 获取员工编号  
    String employeeNumber = adjustSalaryApplyOrderAggregate.getEmployeeNumber();  
    // 获取订单ID  
    String orderId = adjustSalaryApplyOrderAggregate.getOrderId();  
    // 获取员工实体  
    EmployeeEntity employeeEntity = adjustSalaryApplyOrderAggregate.getEmployeeEntity();  
    // 获取薪资调整实体  
    EmployeeSalaryAdjustEntity employeeSalaryAdjustEntity = adjustSalaryApplyOrderAggregate.getEmployeeSalaryAdjustEntity();  
    // 构建员工数据传输对象  
    EmployeePO employeePO = EmployeePO.builder()  
            // 设置员工编号  
            .employeeNumber(employeeNumber)  
            // 设置员工级别  
            .employeeLevel(employeeEntity.getEmployeeLevel().getCode())  
            // 设置员工职位  
            .employeeTitle(employeeEntity.getEmployeeTitle().getDesc()).build();  
    // 更新员工信息  
    employeeDAO.update(employeePO);  
    // 构建薪资数据传输对象  
    EmployeeSalaryPO employeeSalaryPO = EmployeeSalaryPO.builder()  
            // 设置员工编号  
            .employeeNumber(employeeNumber)  
            // 设置总薪资金额  
            .salaryTotalAmount(employeeSalaryAdjustEntity.getAdjustTotalAmount())  
            // 设置绩效薪资金额  
            .salaryMeritAmount(employeeSalaryAdjustEntity.getAdjustMeritAmount())  
            // 设置基本薪资金额  
            .salaryBaseAmount(employeeSalaryAdjustEntity.getAdjustBaseAmount())  
            .build();  
    // 更新薪资信息  
    employeeSalaryDAO.update(employeeSalaryPO);  
    // 构建薪资调整记录的数据传输对象  
    EmployeeSalaryAdjustPO employeeSalaryAdjustPO = EmployeeSalaryAdjustPO.builder()  
            // 设置员工编号  
            .employeeNumber(employeeNumber)  
            // 设置调整订单ID  
            .adjustOrderId(orderId)  
            // 设置调整总金额  
            .adjustTotalAmount(employeeSalaryAdjustEntity.getAdjustTotalAmount())  
            // 设置调整基本薪资金额  
            .adjustBaseAmount(employeeSalaryAdjustEntity.getAdjustMeritAmount())  
            // 设置调整绩效薪资金额  
            .adjustMeritAmount(employeeSalaryAdjustEntity.getAdjustBaseAmount())  
            .build();  
    // 插入薪资调整记录  
    employeeSalaryAdjustDAO.insert(employeeSalaryAdjustPO);  
    // 返回订单ID  
    return orderId;  
}
    
  • 这个事务所做的内容,就是前面小傅哥提到的调整薪资的处理。它的具体操作就是放到仓储层实现。
  • 注意事务注解的配置。
2、编程事务
事务模板

  • 使用编程事务,需要在这里创建出一个事务模板,当然你不创建也可以使用。只不过这样统一的配置会更加方便。
事务使用
// 声明事务模板  
private TransactionTemplate transactionTemplate;  

// 重写insertEmployeeInfo方法,传入员工信息实体  
@Override  
public void insertEmployeeInfo(EmployeeInfoEntity employeeInfoEntity) {  
    // 执行事务,使用回调来处理事务中的操作  
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {  
        // 定义在事务中执行的操作  
        @Override  
        protected void doInTransactionWithoutResult(TransactionStatus status) {  
            try {  
                // 构建员工数据传输对象  
                EmployeePO employeePO = EmployeePO.builder()  
                        // 设置员工编号  
                        .employeeNumber(employeeInfoEntity.getEmployeeNumber())  
                        // 设置员工姓名  
                        .employeeName(employeeInfoEntity.getEmployeeName())  
                        // 设置员工级别  
                        .employeeLevel(employeeInfoEntity.getEmployeeLevel())  
                        // 设置员工职位  
                        .employeeTitle(employeeInfoEntity.getEmployeeTitle())  
                        .build();  
                // 插入员工信息到数据库  
                employeeDAO.insert(employeePO);  
                
                // 构建薪资数据传输对象  
                EmployeeSalaryPO employeeSalaryPO = EmployeeSalaryPO.builder()  
                        // 设置员工编号  
                        .employeeNumber(employeeInfoEntity.getEmployeeNumber())  
                        // 设置总薪资金额  
                        .salaryTotalAmount(employeeInfoEntity.getSalaryTotalAmount())  
                        // 设置绩效薪资金额  
                        .salaryMeritAmount(employeeInfoEntity.getSalaryMeritAmount())  
                        // 设置基本薪资金额  
                        .salaryBaseAmount(employeeInfoEntity.getSalaryBaseAmount())  
                        .build();  
                // 插入薪资信息到数据库  
                employeeSalaryDAO.insert(employeeSalaryPO);  
            } catch (Exception e) {  
                // 若发生异常,标记事务为回滚状态  
                status.setRollbackOnly();  
                // 打印异常堆栈信息  
                e.printStackTrace();  
            }  
        }  
    });  
}
  • 之后就可以手动处理事务了,因为手动的处理可以更细节的控制,也可以根据返回的结果,手动回滚。而不非得异常回滚。