一、沉默王二-集合框架结尾+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 个抽象类:InputStream、OutputStream、Reader、Writer。
虽然 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、文件
文件流也就是直接操作文件的流,可以细分为字节流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。
2、数组(内存)
通常来说,针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流,分为ByteArrayOutputStream和ByteArrayInputStream。
数组流可以用于在内存中读写数据,比如将数据存储在字节数组中进行压缩、加密、序列化等操作。它的优点是不需要创建临时文件,可以提高程序的效率。但是,数组流也有缺点,它只能存储有限的数据量,如果存储的数据量过大,会导致内存溢出。
3、管道
Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的进程之间可以通过管道来通信,但 Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力。
一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的PipedInputStream读取出来。
使用管道流可以实现不同线程之间的数据传输,可以用于线程间的通信、数据的传递等。但是,管道流也有一些局限性,比如只能在同一个 JVM 中的线程之间使用,不能跨越不同的 JVM 进程。
4、基本数据类型
基本数据类型输入输出流是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型。
DataInputStream 提供了一系列可以读基本数据类型的方法。DataOutputStream 提供了一系列可以写基本数据类型的方法。
除了 DataInputStream 和 DataOutputStream,Java IO 还提供了其他一些读写基本数据类型和字符串的流类,包括 ObjectInputStream 和 ObjectOutputStream(用于读写对象)。
5、缓冲
CPU 很快,它比内存快 100 倍,比磁盘快百万倍。那也就意味着,程序和内存交互会很快,和硬盘交互相对就很慢,这样就会导致性能问题。
为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的那些,比如说 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。
缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。简单来说,就是一次多读/写点,少读/写几次,这样程序的性能就会提高。
-
使用
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 的打印流是一组用于打印输出数据的类,包括 PrintStream 和 PrintWriter 两个类。
恐怕 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 的 ByteArrayOutputStream 和 ObjectOutputStream 类,将字符串 "沉默王二" 写入到一个字节数组缓冲区中,并将缓冲区中的数据转换成字节数组输出到控制台。
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);
}
xml:employee_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();
}
}
});
}
- 之后就可以手动处理事务了,因为手动的处理可以更细节的控制,也可以根据返回的结果,手动回滚。而不非得异常回滚。