Java终极冒险:从基础到实战,解锁Java的神秘密码

本文最后更新于:1 个月前

破冰

🥇 推荐阅读:

🍖 反射:

java 中的反射原理,为什么要使用反射以及反射使用场景(面试常问) - 掘金 (juejin.cn)

Java 反射机制详解 | JavaGuide(Java 面试 + 学习指南)

☕ 代理:

Java 代理模式详解 | JavaGuide(Java 面试 + 学习指南)

🥣 注解:

详解 JAVA 注解机制 - 掘金 (juejin.cn)

🌮 语法糖:

Java 语法糖详解 | JavaGuide(Java 面试 + 学习指南)

🍛 集合:

【Java 从 0 到 1 学习】11 Java 集合框架-CSDN 博客

巩固基础:

混凝土巩基 - 竹子爱熊猫的专栏 - 掘金

如何干掉你代码里的if,让请求参数校验变的更加优雅?你是否还在写if来完成参数校验?如何有效减少代码中的if数量,并使得 - 掘金

大家都说Java有三种创建线程的方式!并发编程中的惊天骗局!Java中有几种创建线程的方式?这是一道Java Plus版 - 掘金

四十五图,一万五千字!一文让你走出迷雾玩转Maven!Maven是大家的老熟客,几乎每天都会跟他打交道,不过许多人对它似 - 掘金

思维碰撞

序列化

2025 年 7 月 31 日

(十二)探索高性能通信与RPC框架基石:Json、ProtoBuf、Hessian序列化详解如今这个分布式风靡的时代,网 - 掘金

上面扯了一堆理论,下面简单实战一下,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
java 体验AI代码助手 代码解读复制代码@Data
@AllArgsConstructor
public class ZhuZi implements Serializable {
private static final long serialVersionUID = 1L;
// 序号
private Integer id;
// 名称
private String name;
// 等级
private String grade;
}

public class SerializableDemo {
public static void main(String[] args) throws Exception {
// 1. 序列化
ZhuZi zhuZi = new ZhuZi(1, "黄金竹子", "A级");
byte[] serializeBytes = serialize(zhuZi);
System.out.println("JDK序列化后的字节数组长度:" + serializeBytes.length);

// 2. 反序列化
ZhuZi deserializeZhuZi = deserialize(serializeBytes);
System.out.println(deserializeZhuZi.toString());
}

/**
* 序列化方法
* @param zhuZi 需要序列化的对象
* @return 序列化后生成的字节流
*/
private static byte[] serialize(ZhuZi zhuZi) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(zhuZi);
return bos.toByteArray();
}

/**
* 反序列化方法
* @param bytes 字节序列(字节流)
* @return 实体类对象
*/
private static ZhuZi deserialize(byte[] bytes) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
return (ZhuZi) ois.readObject();
}
}

为了减少代码量,这里用了Lombok的注解,上面这个案例没啥好说的,相信大家曾经都学习过,这里说明几点:

  • Serializable具备向下传递性,父类实现了该接口,子类默认实现序列化接口;
  • Serializable具备引用传递性,两个实现Serializable接口的类产生引用时,序列化时会一起处理;
  • ③序列化前的对象,和反序列化得到的对象,如案例中的zhuZi、deserializeZhuZi,是两个状态完全相同的不同对象,相当于一次深拷贝;
  • JDK并不会序列化静态变量,因为序列化只会保存对象的状态,而静态成员属于类的“状态”;
  • ⑤序列化机制会打破单例模式,如果一个单例对象要序列化,一定要手写一次readResolve()方法;
  • Serializable默认会把所有字段序列化,网络传输时想要对字段脱敏,可以结合transient关键字使用。

重点来看看最后一点,这里提到一个少见的Java原生关键字,我们可以将ZhuZi类的一个属性,加上transient关键字做个实验,如下:

1
2
3
4
5
java

体验AI代码助手
代码解读
复制代码private transient String grade;

这时来看看前后两次的执行结果对比:

1
2
3
4
5
java 体验AI代码助手 代码解读复制代码JDK序列化后的字节数组长度:224
ZhuZi(id=1, name=黄金竹子, grade=A级)
=============================================
JDK序列化后的字节数组长度:204
ZhuZi(id=1, name=黄金竹子, grade=null)

从结果可明显观察出,被transient修饰的属性,并不会参与序列化,grade=null,并且序列化后的字节长度也有明显变化。


正因Json格式特别流行,随之演变出诸多Json库,通过使用这些库,可以让每位开发者更关注业务,屏蔽掉底层的序列化处理工作,Java中常用的库有:Gson、Jackson、FastJsonSpringMVC框架中默认使用Jackson来解析请求参数,不过我们这里以FastJson举例,首先引入相关依赖:

1
2
3
4
5
xml 体验AI代码助手 代码解读复制代码<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.27</version>
</dependency>

依旧使用之前ZhuZi这个类,来演示Json与对象的互相转换(序列化与反序列化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java 体验AI代码助手 代码解读复制代码public static void main(String[] args) {
ZhuZi zhuZi = new ZhuZi(1,"黄金竹子", "A级");

// 1. Java对象转Json字符串
String json = JSONObject.toJSONString(zhuZi);
System.out.println(json);
System.out.println("Json序列化后的体积:" + json.getBytes().length);

// 2. Json字符串转Java对象
ZhuZi zhuZiJson = JSONObject.parseObject(json, ZhuZi.class);
System.out.println(zhuZiJson);
}
/* 输出结果:
* {"grade":"A级","id":1,"name":"黄金竹子"}
* Json序列化后的体积:45
* ZhuZi(id=1, name=黄金竹子, grade=A级)
*/

使用起来特别简单,重点来看看输出结果里的体积,比JDK序列化后的体积,大概小了五倍左右~

接着来讲两个常用的场景,如何处理集合类型以及多泛型对象?

先来看看集合类型的Json序列化,如下:

1
2
3
4
5
6
7
8
9
java 体验AI代码助手 代码解读复制代码private static void testList(){
List<ZhuZi> zhuZis = Arrays.asList(
new ZhuZi(1,"黄金竹子","A级"),
new ZhuZi(2, "白玉竹子", "S级"));
String json = JSONArray.toJSONString(zhuZis);
System.out.println(json);
List<ZhuZi> zhuZisJson = JSONArray.parseArray(json, ZhuZi.class);
System.out.println(zhuZisJson);
}

这里可以直接用JSONArray类来转换,也可以用JSONObject类,反序列化时调用parseArray()方法即可。

接着来看看多泛型对象的处理,以Map为例,Map集合需要传入两个泛型,这时该如何反序列化呢?如下:

1
2
3
4
5
6
7
8
9
10
java 体验AI代码助手 代码解读复制代码private static void testMap(){
Map<String, ZhuZi> zhuZiMap = new HashMap<>();
zhuZiMap.put("1", new ZhuZi(1,"黄金竹子","A级"));
zhuZiMap.put("2", new ZhuZi(2, "白玉竹子", "S级"));
String json = JSONObject.toJSONString(zhuZiMap);
System.out.println(json);
HashMap<String, ZhuZi> zhuZiMapJson = JSONObject
.parseObject(json, new TypeReference<HashMap<String, ZhuZi>>() {});
System.out.println(zhuZiMapJson);
}

序列化操作与之前没区别,重点是反序列化时,由于泛型有两个,就无法通过前面那种方式指定,如果直接传入HashMap.class,会被转换为HashMap<Object,Object>类型,想要正确的完成转换,则需要传入一个TypeReference对象,以此精准的告知反序列化类型。


(十二)探索高性能通信与RPC框架基石:Json、ProtoBuf、Hessian序列化详解如今这个分布式风靡的时代,网 - 掘金


除开前面提到的几种序列化方案外,相信看过Dubbo框架源码的小伙伴,一定还知道一种方案,即基于二进制实现Hessian,这是Dubbo中默认的序列化机制,用于服务提供者与消费者之间进行数据传输,这里咱们也简单过一下。

HessianJDK原生的序列化技术,兼容度很高,相较于使用ProtoBuf而言,成本要低许多,首先导入一下依赖包:

1
2
3
4
5
xml 体验AI代码助手 代码解读复制代码<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.65</version>
</dependency>

接着依旧基于最开始的ZhuZi实体类,来写一下测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java 体验AI代码助手 代码解读复制代码public class HessianDemo {
public static void main(String[] args) throws Exception {
// 1. 序列化
ZhuZi zhuZi = new ZhuZi(1,"黄金竹子", "A级");
byte[] serializeBytes = serialize(zhuZi);
System.out.println("Hessian序列化后字节数组长度:" + serializeBytes.length);

// 2. 反序列化
ZhuZi deserializeZhuZi = deserialize(serializeBytes);
System.out.println(deserializeZhuZi.toString());
}

/**
* 序列化方法
* @param zhuZi 需要序列化的对象
* @return 序列化后生成的字节流
*/
private static byte[] serialize(ZhuZi zhuZi) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output h2o = new Hessian2Output(bos);
h2o.writeObject(zhuZi);
h2o.close();
return bos.toByteArray();
}

/**
* 反序列化方法
* @param bytes 字节序列(字节流)
* @return 实体类对象
*/
private static ZhuZi deserialize(byte[] bytes) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
Hessian2Input h2i = new Hessian2Input(bis);
ZhuZi zhuZi = (ZhuZi) h2i.readObject();
h2i.close();
return zhuZi;
}
}

上述代码对比最开始的JDK序列化方案,几乎一模一样,只是将输出/输入流对象,从ObjectOutputStream、ObjectInputStream换成了Hessian2Output、Hessian2Input,此时来看结果对比,如下:

1
2
3
4
5
java 体验AI代码助手 代码解读复制代码JDK序列化后的字节数组长度:224
ZhuZi(id=1, name=黄金竹子, grade=A级)
=============================================
Hessian序列化后字节数组长度:70
ZhuZi(id=1, name=黄金竹子, grade=A级)

是不是特别惊讶?其余任何地方没有改变,仅用Hessian2替换掉JDK原生的IO流对象,结果码流体积竟然缩小了3.2倍!并且还完全保留了JDK序列化技术的特性,还支持多语言异构……,所以,这也是Dubbo使用Hessian2作为默认序列化技术的原因,不过Dubbo使用的是定制版,依赖如下:

1
2
3
4
5
xml 体验AI代码助手 代码解读复制代码<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-hessian2</artifactId>
<version>3.2.0-beta.6</version>
</dependency>

感兴趣的可以去看看DecodeableRpcInvocation#decode()、encode()这个两个方法,其中涉及到数据的编解码工作,默认采用Hessian2序列化技术~

同步

2025 年 7 月 31 日

(七)Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!如若你对于面试要求中的“ - 掘金

同步是指线程串行的依次执行,异步则是可以将自己要做的事情交给其他线程执行,然后主线程就能立马返回干其他事情。

BIO就是Java的传统IO模型,与其相关的实现都位于java.io包下,其通信原理是客户端、服务端之间通过Socket套接字建立管道连接,然后从管道中获取对应的输入/输出流,最后利用输入/输出流对象实现发送/接收信息,案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
java 体验AI代码助手 代码解读复制代码// BIO服务端
public class BioServer {
public static void main(String[] args) throws IOException {
System.out.println(">>>>>>>...BIO服务端启动...>>>>>>>>");
// 1.定义一个ServerSocket服务端对象,并为其绑定端口号
ServerSocket server = new ServerSocket(8888);
// 2.监听客户端Socket连接
Socket socket = server.accept();
// 3.从套接字中得到字节输入流并封装成输入流对象
InputStream inputStream = socket.getInputStream();
BufferedReader readBuffer =
new BufferedReader(new InputStreamReader(inputStream));
// 4.从Buffer中读取信息,如果读到信息则输出
String msg;
while ((msg = readBuffer.readLine()) != null) {
System.out.println("收到信息:" + msg);
}
// 5.从套接字中获取字节输出流并封装成输出对象
OutputStream outputStream = socket.getOutputStream();
PrintStream printStream = new PrintStream(outputStream);
// 6.通过输出对象往服务端传递信息
printStream.println("Hi!我是竹子~");
// 7.发送后清空输出流中的信息
printStream.flush();
// 8.使用完成后关闭流对象与套接字
outputStream.close();
inputStream.close();
socket.close();
inputStream.close();
outputStream.close();
socket.close();
server.close();
}
}

// BIO客户端
public class BioClient {
public static void main(String[] args) throws IOException {
System.out.println(">>>>>>>...BIO客户端启动...>>>>>>>>");
// 1.创建Socket并根据IP地址与端口连接服务端
Socket socket = new Socket("127.0.0.1", 8888);
// 2.从Socket对象中获取一个字节输出流并封装成输出对象
OutputStream outputStream = socket.getOutputStream();
PrintStream printStream = new PrintStream(outputStream);
// 3.通过输出对象往服务端传递信息
printStream.println("Hello!我是熊猫~");
// 4.通过下述方法告诉服务端已经完成发送,接下来只接收消息
socket.shutdownOutput();
// 5.从套接字中获取字节输入流并封装成输入对象
InputStream inputStream = socket.getInputStream();
BufferedReader readBuffer =
new BufferedReader(new InputStreamReader(inputStream));
// 6.通过输入对象从Buffer读取信息
String msg;
while ((msg = readBuffer.readLine()) != null) {
System.out.println("收到信息:" + msg);
}
// 7.发送后清空输出流中的信息
printStream.flush();
// 8.使用完成后关闭流对象与套接字
outputStream.close();
inputStream.close();
socket.close();
}
}

分别启动BioServer、BioClient类,运行结果如下:

1
2
3
4
5
6
7
java 体验AI代码助手 代码解读复制代码// ------服务端---------
>>>>>>>...BIO服务端启动...>>>>>>>>
收到信息:Hello!我是熊猫~

// ------客户端---------
>>>>>>>...BIO客户端启动...>>>>>>>>
收到信息:Hi!我是竹子~

观察如上结果,其实执行过程原理很简单:

  • ①服务端启动后会执行accept()方法等待客户端连接到来。
  • ②客户端启动后会通过IP及端口,与服务端通过Socket套接字建立连接。
  • ③然后双方各自从套接字中获取输入/输出流,并通过流对象发送/接收消息。

大体过程如下:
Java-BIO
在上述Java-BIO的通信过程中,如若客户端一直没有发送消息过来,服务端则会一直等待下去,从而服务端陷入阻塞状态。同理,由于客户端也一直在等待服务端的消息,如若服务端一直未响应消息回来,客户端也会陷入阻塞状态。


简单了解了选择器的基础概念后,那如何使用它实现非阻塞模型呢?如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
java 体验AI代码助手 代码解读复制代码// ----NIO服务端实现--------
public class NioServer {
public static void main(String[] args) throws Exception {
System.out.println(">>>>>>>...NIO服务端启动...>>>>>>>>");
// 1.创建服务端通道、选择器与字节缓冲区
ServerSocketChannel ssc = ServerSocketChannel.open();
Selector selector = Selector.open();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 2.为服务端绑定IP地址+端口
ssc.bind(new InetSocketAddress("127.0.0.1",8888));
// 3.将服务端设置为非阻塞模式,同时绑定接收事件注册到选择器
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
// 4.通过选择器轮询所有已就绪的通道
while (selector.select() > 0){
// 5.获取当前选择器上注册的通道中所有已经就绪的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 6.遍历得到的所有事件,并根据事件类型进行处理
while (iterator.hasNext()){
SelectionKey next = iterator.next();
// 7.如果是接收事件就绪,那则获取对应的客户端连接
if (next.isAcceptable()){
SocketChannel channel = ssc.accept();
// 8.将获取到的客户端连接置为非阻塞模式,绑定事件并注册到选择器上
channel.configureBlocking(false);
int event = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
channel.register(selector,event);
System.out.println("客户端连接:" + channel.getRemoteAddress());
}
// 9.如果是读取事件就绪,则先获取对应的通道连接
else if(next.isReadable()){
SocketChannel channel = (SocketChannel)next.channel();
// 10.然后从对应的通道中,将数据读取到缓冲区并输出
int len = -1;
while ((len = channel.read(buffer)) > 0){
buffer.flip();
System.out.println("收到信息:" +
new String(buffer.array(),0,buffer.remaining()));
}
buffer.clear();
}
}
// 11.将已经处理后的事件从选择器上移除(选择器不会自动移除)
iterator.remove();
}
}
}

// ----NIO客户端实现--------
public class NioClient {
public static void main(String[] args) throws Exception {
System.out.println(">>>>>>>...NIO客户端启动...>>>>>>>>");
// 1.创建一个TCP类型的通道并指定地址建立连接
SocketChannel channel = SocketChannel.open(
new InetSocketAddress("127.0.0.1",8888));
// 2.将通道置为非阻塞模式
channel.configureBlocking(false);
// 3.创建字节缓冲区,并写入要传输的消息数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
String msg = "我是熊猫!";
buffer.put(msg.getBytes());
// 4.将缓冲区切换为读取模式
buffer.flip();
// 5.将带有数据的缓冲区写入通道,利用通道传输数据
channel.write(buffer);
// 6.传输完成后情况缓冲区、关闭通道
buffer.clear();
channel.close();
}
}

在如上案例中,即实现了一个最简单的NIO服务端与客户端通信的案例,重点要注意:注册到选择器上的通道都必须要为非阻塞模型,同时通过缓冲区传输数据时,必须要调用flip()方法切换为读取模式。

OK,最后简单叙述一下缓冲区、通道、选择器三者关系:
![多路复用模型](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/faba15014ec648dab401d7cfdba85e9f
tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=685&h=508&s=142945&e=png&b=fcfcfc)
如上图所示,每个客户端连接本质上对应着一个Channel通道,而一个通道也有一个与之对应的Buffer缓冲区,在客户端尝试连接服务端时,会利用通道将其注册到选择器上,这个选择器则会有一条对应的线程。在开始工作后,选择器会根据不同的事件在各个通道上切换,对于已就绪的数据会基于通道与Buffer缓冲区进行读写操作。

作者:竹子爱熊猫
链接:https://juejin.cn/post/7130952602350534693
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

跨域

2025 年 7 月 29 日

(十二)漫谈分布式之接口设计上篇:写出一个优秀的接口我们需要考虑什么?作为一名后端开发,你会写接口吗?有人说你这不废话吗 - 掘金

跨域问题的产生背景是浏览器的同源策略(Same-Origin Policy),同源策略是浏览器的一种安全机制,它限制了从同一个源加载的文档或脚本,如何与来自不同源的资源进行交互。这里的源(origin)是指协议、域名、端口号组成的唯一标识,如果出现两个地址,这三者组合起来对比不一致,就代表着不同源。

如何解决不同源的跨域问题呢?也很简单,在SpringBoot里通过一个@CrossOrigin注解就能搞定,如果嫌挨个Controller加注解麻烦,也可以通过实现WebMvcConfigurer接口,并重写addCorsMappings()方法来全局配置跨域,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java 体验AI代码助手 代码解读复制代码@Configuration  
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry
// 代表允许所有路径跨域
.addMapping("/**")
// 允许来自http://xxx.com的请求跨域
.allowedOrigins("http://xxx.com")
// 允许这些HTTP方法跨域
.allowedMethods("GET", "POST", "PUT", "DELETE")
// 允许任何头跨域
.allowedHeaders("*")
// 允许发送Cookie
.allowCredentials(true)
// 预检请求的缓存时间(秒)
.maxAge(3600);
}
}

当然,解决跨域问题的本质,是往响应头里塞几个字段,即:

1
2
3
4
java 体验AI代码助手 代码解读复制代码response.setHeader("Access-Control-Allow-Origin", "*");  
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization");

因此,只要能够改写响应头的地方都可以解决跨域问题,所以市面上才会出现那么多解决跨域的方式:

  • 使用@CrossOrigin注解解决跨域问题;
  • 实现WebMvcConfigurer接口解决跨域问题;
  • 通过自定义Filter过滤器解决跨域问题;
  • 通过Response对象实现跨域问题;
  • 通过实现ResponseBodyAdvice接口解决跨域问题;
  • 通过设置Nginx配置解决跨域问题;
  • ……

这么多种跨域解决方案听起来很唬人,但只要是服务端解决的跨域问题,殊途同归都是改写响应头罢了。

并发安全

(十三)漫谈分布式之接口设计下篇:设计一个优秀写接口的13条黄金法则!引言 在前面《写好一个接口需要考虑什么?》这篇文章 - 掘金

2025 年 7 月 29 日

什么情况下需要考虑线程安全问题?我听到过最多的回答就是下单扣库存,每当我让候选人换一个例子的时候,多数人就很难继续回答了,那么到底什么情况下会线程不安全呢?来看个例子:

1
2
3
4
5
6
7
8
9
java 体验AI代码助手 代码解读复制代码public void joinGroup(Long groupId) {
// 查询拼团人数是否已满
boolean flag = groupService.groupBuyingFull(groupId);
if(flag) {
throw new BusinessException("拼团人数已满员!");
}
// 如果拼团人数未满,则加入拼团
groupService.insertJoinGroupRecord(UserHolder.getUserId(), groupId);
}

这是一段极其简单的伪代码,首先会根据团ID去查询是否已满员,如果满员则返回拼团失败,反之则插入拼团记录返回拼团成功。请问各位小伙伴,这段代码是否存在线程安全问题呢?大家可以认真思考片刻……

答案是存在,Why?很简单,因为后面的写操作,依赖于前面的查操作,而之前并发编程专栏曾提到过一个定律:同一时刻多条线程对共享资源进行非原子性操作,则有可能产生线程安全问题,而这个例子恰恰满足条件,拆开分析下:

  • 多线程(条件1):两个用户同时请求这个拼团接口,就会转变为两条线程执行;
  • 共享资源(条件2):对于两个请求而言,数据库里的拼团记录是共享可见的;
  • 非原子性操作(条件3):这里先查询、再插入,分为两步执行,并非一起执行的。

综上,这个场景完全符合线程安全问题的产生背景,比如目前”小竹搬砖团“还剩最后一个名额,两个用户同时申请加入该团,代表两个请求几乎是同时过来的,那么在执行”拼团人数是否已满查询“时,这时看到的结果都是false,因为此时最后一个名额还在,然后两个请求都会执行insertJoinGroupRecord()方法,最终导致最后一个名额被两人同时拿到。

那该如何解决这个问题呢?打破构成线程不安全的三个条件即可:

  • ①破坏多线程条件:同一时刻,只允许一条线程对共享资源进行非原子性操作;
  • ②破坏共享资源条件:同一时刻多条线程对局部资源进行非原子性操作;
  • ③破坏非原子性条件:同一时刻多条线程对共享资源进行原子性操作。

说简单一点就是方案一就是加锁,方案二在这个场景里实现不了,方案三可以理解成CAS无锁自旋,即乐观锁方案。不过最常用的还是加锁,如果你是单体应用,则可使用synchronized关键字ReetrantLock可重入锁这种单机锁,具体怎么用可以参考之前《单体项目并发漏洞》这篇文章,这里就不做重复赘述。如果是分布式集群部署的环境,则可以使用基于Redis、Zookeeper实现的分布式锁,用起来都不难~

但不管是单机锁也好,分布式锁也罢,其实核心思想都是一样的,底层的本质就是一个对所有线程可见的锁标识,谁先将其改为1就代表先拿到锁,拿到锁的线程可以先执行,执行结束后再把锁放掉,其余线程也可以继续抢占锁资源了。

OK,再来看个问题,还是前面的代码,假设这里是单体应用,现在我对其加一把锁:

1
2
3
4
5
6
7
8
9
10
11
java 体验AI代码助手 代码解读复制代码public void joinGroup(Long groupId) {
// 查询拼团人数是否已满
boolean flag = groupService.groupBuyingFull(groupId);
if(flag) {
throw new BusinessException("拼团人数已满员!");
}
// 如果拼团人数未满,则加入拼团
synchronized(this) {
groupService.insertJoinGroupRecord(UserHolder.getUserId(), groupId);
}
}

因为写数据存在线程安全问题,所以我用synchronized将其包裹,这段代码有没有问题?答案是仍然有问题,因为这里锁的范围不够,还要将前面的查询一起放进synchronized才对。最后,还有个细节就是锁的维度,这里是基于this加锁,这时就算不同的团购拼团,也会竞争同一把锁,最终导致性能低效,怎么办才好呢?直接基于团购ID加锁就好啦,不过里面有些细节坑,不了解的可参考前面给出的文章链接。

多线程优化

2025 年 7 月 29 日

(十三)漫谈分布式之接口设计下篇:设计一个优秀写接口的13条黄金法则!引言 在前面《写好一个接口需要考虑什么?》这篇文章 - 掘金

前面的批处理,是优化写接口性能的一种方式,而当接口出现性能问题时,多线程技术永远是解决问题的一大利器,不过许多人对多线程的适用并不熟练,这里先来说明异步和并发(并行)的区别。

  • 异步:将对应的任务递交给其他线程后,不需要等待结果返回,可以直接对外响应;
  • 并发:通过多条线程来提升效率,提交任务的主线程需要等待结果返回,只是优化性能。

异步和并发两个概念彼此兼容,所以许多人有点犯迷糊,结合生活来理解,比如现在我要搬一百块砖头,可是我一个人搬的太慢了,所以想着多喊几个人来帮忙,于是我找到X、Y、Z,并叫它们一起来搬砖提升效率,这时我会等它们搬完,这就是并发的概念。

如果我找到X、Y、Z把搬砖任务丢给了它们,不管它们有没有搬完,然后我就自己走了,这就是异步的概念。

综上,当接口写入性能较差时,咱们确实可以通过多线程来优化性能,可到底要用多线程来并发处理,还是用它来异步处理呢?这就取决于你实际的业务场景,来看例子:

1
2
3
4
java 体验AI代码助手 代码解读复制代码public String writeBigData(List<Panda> pandas) {
pandaService.writePandas(pandas);
return "写入成功";
}

这是一个写熊猫数据的接口,假设外部传入了10W条熊猫数据需要落库,单线程处理的效率过低,这时用多线程优化,可以这么写:

1
2
3
4
5
6
java 体验AI代码助手 代码解读复制代码public String writeBigData(List<Panda> pandas) {
threadPool.submit(() -> {
pandaService.writePandas(pandas);
});
return "写入成功";
}

这段代码中,主线程将写熊猫数据的任务,丢给线程池后立马返回了,这是典型的异步写法,再来看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java 体验AI代码助手 代码解读复制代码public String writeBigData(List<Panda> pandas) {
// 先对数据进行切分,分割为1000一批的数据
List<List<Panda>> partitions = ListUtils.partition(pandas, 1000);
// 定义计数器
AtomicInteger count = new AtomicInteger(0);
// 循环所有批次,将任务提交给线程池
for (List<Panda> partition : partitions) {
threadPool.submit(() -> {
pandaService.writePandas(partition);
count.incrementAndGet();
});
}

// 模拟阻塞(实际要通过Future来阻塞等待执行结果)

// 如果写入成功的批数,等于划分出的批数,返回写入成功
if (count.get() == partitions.size()) {
return "写入成功";
}
return "写入失败";
}

再来看这种写法,首先对传入的熊猫集合进行了分批,将数据分为多个1000条的小批次,而后遍历拆分后的批次列表,将拆分的每批数据都丢给了线程池去执行。再来看外部的主线程,任务投递给线程池后并未立马返回,而是在等待所有批次的执行结果,只有当所有批次都完成写入后,才真正向调用方返回了写入成功。

通过上面的案例,我们演示了多线程异步和并发的用法,具体的场景中诸位要用哪种方式,可以结合业务场景来做抉择。

函数式接口

2025 年 7 月 29 日

詹姆斯·高斯林:整整十年过去了!你小子还不会用我的Java8?距离Java8的发布,至今已过去了整整十年,而JDK22在 - 掘金

归功于类型推导机制,我们可以在Java8中使用lambda来使得代码简洁化,不过经过上阶段的学习会发现一个致命问题:每写一个Lambda表达式,就需要单独定义一个接口,如果真是这样,Lambda省下来的代码,又全都在接口定义上补回去了,这有点拆东墙补西墙的味道

JDK官方显然也想到了这一点,所以提供了一个java.util.function包,这里面定义了一系列可复用的、使用频率较高的函数式接口,以此避免日常开发过程中重复定义类似的接口,可到底啥叫做函数式接口?函数式接口是Java8新增的一种接口定义

但说到底,函数式接口跟普通的接口写法都一样,唯一的区别在于:函数式接口就是一个只具有一个抽象方法的特殊接口(可以定义多个方法,但其他的方法只能是default或static)。同时,也可以用@FunctionalInterface注解来将一个接口声明函数式接口,不过这个注解加不加,都不影响表达式的执行,仅仅只是起到编译校验的作用,如:

1
2
3
4
5
java 体验AI代码助手 代码解读复制代码@FunctionalInterface 
public interface A {
void a();
default void b() {}
}

这个接口只有一个抽象方法,所以编译能正常通过,再看个反例:

1
2
3
4
5
java 体验AI代码助手 代码解读复制代码@FunctionalInterface 
public interface B {
void a();
void b();
}

这个接口有多个抽象方法,所以编译会提示错误。OK,接着来看看java.util.function包下提供的函数式接口,这里列几个常用:

接口 描述 示例
Supplier 无入参,返回一个结果 () -> {return 0;};
Function 单个入参,返回一个结果 i -> {return i * 100;};
Consumer 单个入参,无返回结果 str -> System.out.println(str);
Predicate 单个入参,返回一个布尔值结果 str -> {return str.isEmpty();}
…… …… ……

当然,还有一系列和命名上述类似,但是以Bi……开头的函数式接口,例如BiFunction,其实这就是前面的增强版,只是支持两个入参罢了。好了,那么我们该如何使用JDK自带的这些函数式接口呢?来个例子感受一下。

需求:实现两个数字的加减乘除计算。

如果用之前的思维来实现,要么就分别定义加、减、乘、除四个方法,要么就传一个运算符,在用ifswitch判断,以此实现不同的计算逻辑,但现在可以用lambda表达式来换一种实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java 体验AI代码助手 代码解读复制代码/*
* 计算两个数字的方法
* */
public static int calculate(int x, int y, BiFunction<Integer, Integer, Integer> calculateModel) {
return calculateModel.apply(x, y);
}

public static void main(String[] args) {
int a = 4;
int b = 2;

// 加法计算
int result1 = calculate(a, b, (x, y) -> x + y);
System.out.println("两数之和:" + result1);

// 减法计算
int result2 = calculate(a, b, (x, y) -> x - y);
System.out.println("两数之差:" + result2);

// 乘法计算
int result3 = calculate(a, b, (x, y) -> x * y);
System.out.println("两数之积:" + result3);

// 除法计算
int result4 = calculate(a, b, (x, y) -> x / y);
System.out.println("两数之商:" + result4);
}

上述代码的运行结果如下:

1
2
3
4
java 体验AI代码助手 代码解读复制代码两数之和:6
两数之差:2
两数之积:8
两数之商:8

这个例子中,我们基于JDK提供的函数式接口,完成了一个小需求的开发。函数式接口和lambda表达式结合,能使得程序更加灵活,允许将一个函数作为参数传递。

Lambda 表达式

2025 年 7 月 29 日

詹姆斯·高斯林:整整十年过去了!你小子还不会用我的Java8?距离Java8的发布,至今已过去了整整十年,而JDK22在 - 掘金

在JDK1.8之前,一个方法能接收的入参类型,都只能是“值类型”,要么是基本数据类型,要么就是一个引用对象,如果想要将另一个方法作为入参怎么办?在之前的版本中只能通过匿名内部类来拐着弯实现,不过匿名内部类依赖于接口,所以先定义一个接口:

1
2
3
4
5
6
java 体验AI代码助手 代码解读复制代码public interface ZhuZiCallback {
/*
* 回调方法
* */
void callback(ZhuZi zhuZi);
}

下面来看如何将这个回调方法作为入参传递给一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java 体验AI代码助手 代码解读复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
public class ZhuZi {
private Long id;
private String name;
}

public class Test {
/*
* 创建完对象后,触发指定的回调逻辑
* */
public static void create(long id, String name, ZhuZiCallback zhuZiCallback) {
ZhuZi zhuZi = new ZhuZi(id, name);
zhuZiCallback.callback(zhuZi);
}

public static void main(String[] args) {
Test.create(88888888, "竹子爱熊猫", new ZhuZiCallback() {
@Override
public void callback(ZhuZi zhuZi) {
System.out.println("我是创建完竹子对象后的回调,创建的对象为:" + zhuZi);
}
});
}
}

/*
* 执行结果:
* 我是创建完竹子对象后的回调,创建的对象为:ZhuZi(id=88888888, name=竹子爱熊猫)
* */

来看上面这个回调事件的例子,其中的ZhuZiCallback是一种动作,我们真正关心的只有callback()方法里的逻辑而已,可是Java中不支持直接传递函数,所以为了将这个回调方法传递给要执行的create()方法,必须得new一个匿名内部类,写起来费劲不说,还不美观!

到了JDK1.8,就可以直接用Lambda表达式来代替,上述代码可以优化成:

1
2
3
4
5
java 体验AI代码助手 代码解读复制代码public static void main(String[] args) {
Test.create(88888888, "竹子爱熊猫", zhuZi -> {
System.out.println("我是创建完竹子对象后的回调,创建的对象为:" + zhuZi);
});
}

这样写起来更简单,看起来更优雅!不过值得注意的是,Test.create()方法的第三个入参,仍然是ZhuZiCallback这个接口类型,至于为什么可以用Lambda表达式代替,这一点放在后面再聊,下面重点说说Lambda表达式。

泛型封装

2025 年 7 月 29 日

Java泛型大揭秘:从基础操作到实现原理,及如何规避常见陷阱与问题!作为每天跟代码打交道的我们,相信对泛型这个技术并不陌 - 掘金

基于泛型封装一个常用、通用的方法,即Bean拷贝场景,在日常编码设计中,都会将对象分为BO、VO、DTO、DO、PO……各种模型,为了满足不同业务,数据会在这些对象之间流转。

可是挨个属性Get/Set属实麻烦,在平时大家使用较多的就是Spring提供的BeanUtils这个工具类,但这个工具类用起来还是有点繁琐,比如:

1
2
3
java 体验AI代码助手 代码解读复制代码User user = userMapper.selectById(userId);
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);

正如上述所示,每次都得手动new出目标对象才行,而且BeanUtils也没提供集合拷贝的方法,因此,我们就可以基于泛型封装两个通用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java 体验AI代码助手 代码解读复制代码/**
* Bean拷贝工具类
*/
public class BeanCopyUtil {
/*
* 拷贝单个Bean对象
* */
public static <T> T copy(Object source, Class<T> clazz) {
if (null == source) {
return null;
}
T target;
try {
target = clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("bean copy exception: " + e.getMessage());
}
BeanUtils.copyProperties(source, target);
return target;
}

/*
* 拷贝Bean对象集合
* */
public static <T> List<T> copyList(List<T> sourceList, Class<T> clazz) {
if (null == sourceList || 0 == sourceList.size()) {
return null;
}

List<T> targetList = new ArrayList<>();
for (T source : sourceList) {
T target = copy(source, clazz);
targetList.add(target);
}
return targetList;
}
}

基于这两个封装的方法,能特别方便的应对平时Bean拷贝场景,用起来也格外简单:

1
2
3
4
5
6
7
java 体验AI代码助手 代码解读复制代码// 拷贝单个Bean对象
User user = userMapper.selectById(userId);
UserVO result = BeanCopyUtil.copy(user, UserVO.class);

// 拷贝Bean对象集合
List<User> users = userMapper.selectList();
List<UserVO> results = BeanCopyUtil.copyList(users, UserVO.class);

谈谈 Lambda 表达式的语法:

Lambda表达式,是JDK1.8从函数式编程语言中“借鉴”而来的特性,Lambda允许将一个函数作为方法的入参。而Lambda表达式的基础语法由三部分组成:

()包裹的参数列表、–>符号、{}包裹的函数体。

通过前面的例子来套入分析下:

1
2
3
java 体验AI代码助手 代码解读复制代码Test.create(88888888, "竹子爱熊猫", (ZhuZi zhuZi) -> {
System.out.println("我是创建完竹子对象后的回调,创建的对象为:" + zhuZi);
});

ZhuZi代表是入参的类型,zhuZi代表是方法的参数名,这个名字你想叫啥就叫啥。->Lambda表达式的固定语法,这个是固定的语法糖,不能改变成→、_>、=>或其他箭头。最后就是{}这对花括号包裹的代码块,实际上就是具体要执行的函数体,就跟方法体一样。

掌握上述基本语法后,下面再来看几类变种写法,先来看无参数的lambda写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java 体验AI代码助手 代码解读复制代码/**
* 无参数回调
*/
public interface NoArgsCallback {
void callback();
}

public class Test {
public static void noArgs(NoArgsCallback noArgsCallback) {
noArgsCallback.callback();
}

public static void main(String[] args) {
Test.noArgs(() -> {
System.out.println("我是无参数的lambda语法……");
});
}
}

/*
* 执行结果:
* 我是无参数的lambda语法……
* */

注意看上面无参数的lambda写法,和之前的唯一区别在于:如果对应的函数没有入参,那么参数列表部分就用()小括号代替即可。再来看看多参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java 体验AI代码助手 代码解读复制代码/**
* 无参数回调
*/
public interface MultipleArgsCallback {
void callback(int arg1, String arg2);
}

public class Test {
public static void multipleArgs(int arg1, String arg2,
MultipleArgsCallback multipleArgsCallback) {
multipleArgsCallback.callback(arg1, arg2);
}

public static void main(String[] args) {
Test.multipleArgs(1, "竹子爱熊猫", (int a, String b) -> {
System.out.println("我是" + b + ",想要" + a + "个点赞!");
});
}
}

/*
* 执行结果:
* 我是竹子爱熊猫,想要1个点赞!
* */

与无参数的写法对比,如果函数存在多个入参,只需要用()将参数列表包起来、多个参数用,逗号隔开就行,函数存在多少个入参,这里就需要定义多少个参数,顺序与函数定义的入参列表一一对应。好了,再回去看到只有一个入参的lambda案例:

1
2
3
4
5
6
7
8
java 体验AI代码助手 代码解读复制代码Test.create(88888888, "竹子爱熊猫", (ZhuZi zhuZi) -> {
System.out.println("我是创建完竹子对象后的回调,创建的对象为:" + zhuZi);
});

// 可以优化为:
Test.create(88888888, "竹子爱熊猫", zhuZi ->
System.out.println("我是创建完竹子对象后的回调,创建的对象为:" + zhuZi)
);

区别在哪儿呢?优化之后的写法,参数列表没有()包裹了,函数体也没用{}包裹了,sout这行代码最后的;分号也去掉了,为啥可以这样写?因为这个案例中,参数只有一个,所以可以省略();函数体也只有一行代码,所以{}也可以省略不写~

并发

2025 年 4 月 1 日

[01 使用了并发工具类库,线程安全就高枕无忧了吗? (lianglianglee.com)](https://learn.lianglianglee.com/专栏/Java 业务开发常见错误 100 例/01 使用了并发工具类库,线程安全就高枕无忧了吗?.md)

没有意识到线程重用导致用户信息错乱的 Bug。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);

public static void main(String[] args) {
System.out.println("Hello world!");
Map wrong = wrong(1);
System.out.println(wrong);
}

public static Map wrong(Integer integer) {
// 设置前先查询
String before = Thread.currentThread().getName() + ":" + currentUser.get();
// 设置信息到 ThreadLocal
currentUser.set(integer);
// 设置后再查询
String after = Thread.currentThread().getName() + ":" + currentUser.get();
System.out.println(before + "->" + after);
// 汇总后返回
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("before", before);
hashMap.put("after", after);
return hashMap;
}

使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static Map right(Integer integer) {
// 设置前先查询
String before = Thread.currentThread().getName() + ":" + currentUser.get();
// 设置信息到 ThreadLocal
currentUser.set(integer);
try {
// 设置后再查询
String after = Thread.currentThread().getName() + ":" + currentUser.get();
System.out.println(before + "->" + after);
// 汇总后返回
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("before", before);
hashMap.put("after", after);
return hashMap;
} finally {
// 最后移除 ThreadLocal 中的信息
currentUser.remove();
}
}

使用了线程安全的并发工具,并不代表解决了所有线程安全问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Main2 {
// 线程个数
private static final int THREAD_COUNT = 10;
// 总元素数量
private static final int TOTAL_ELEMENTS = 1000;

// 获取一个指定元素数量模拟数据的 ConcurrentHashMap
private static ConcurrentHashMap<Integer, Integer> getMockConcurrentHashMap(int elements) {
ConcurrentHashMap<Integer, Integer> concurrentHashMap = new ConcurrentHashMap<>();
for (int i = 0; i < elements; i++) {
concurrentHashMap.put(i, i);
}
return concurrentHashMap;
}

// 模拟数据
private static ConcurrentHashMap<String, Long> getData(int count) {
return LongStream.rangeClosed(1, count)
.boxed()
.collect(Collectors.toConcurrentMap(i -> UUID.randomUUID().toString(), Function.identity(),
(o1, o2) -> o1, ConcurrentHashMap::new));
}

public static void main(String[] args) throws InterruptedException {
// 初始900个元素
ConcurrentHashMap<String, Long> concurrentHashMap = getData(TOTAL_ELEMENTS - 100);
System.out.println("init size:" + concurrentHashMap.size());
ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
// 使用线程池并发处理逻辑
forkJoinPool.execute(() -> IntStream.rangeClosed(1, 10).parallel().forEach(i -> {
// 查询还要补充多少元素
int gap = TOTAL_ELEMENTS - concurrentHashMap.size();
System.out.println("gap size:" + gap);
// 补充元素
concurrentHashMap.putAll(getData(gap));
}));

// 等待所有任务完成
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
// 验证最后元素
System.out.println("last size:" + concurrentHashMap.size());
}
}

image-20250401222235818

回到 ConcurrentHashMap,我们需要注意 ConcurrentHashMap 对外提供的方法或能力的限制:

  • 使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。
  • 诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映 ConcurrentHashMap 的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用 size 方法计算差异值,是一个流程控制。
  • 诸如 putAll 这样的聚合方法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到部分数据。

代码的修改方案很简单,整段逻辑加锁即可:

1
2
3
4
5
6
7
8
9
10
// 使用线程池并发处理逻辑
forkJoinPool.execute(() -> IntStream.rangeClosed(1, 10).parallel().forEach(i -> {
synchronized (concurrentHashMap) {
// 查询还要补充多少元素
int gap = TOTAL_ELEMENTS - concurrentHashMap.size();
System.out.println("gap size:" + gap);
// 补充元素
concurrentHashMap.putAll(getData(gap));
}
}));

没有充分了解并发工具的特性,从而无法发挥其威力。

我们来看一个使用 Map 来统计 Key 出现次数的场景吧,这个逻辑在业务代码中非常常见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Main3 {
// 循环次数
private static final int LOOP_COUNT = 1000000;
// 线程数量
private static final int THREAD_COUNT = 10;
// 元素数量
private static final int TOTAL_ELEMENTS = 10;

public static void main(String[] args) {
Map<String, Long> stringLongMap = normalUse();
System.out.println(stringLongMap);
}

private static Map<String, Long> normalUse() {
ConcurrentHashMap<String, Long> concurrentHashMap = new ConcurrentHashMap<>(TOTAL_ELEMENTS);
System.out.println("init size:" + concurrentHashMap.size());
ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {
// 获得一个随机 key
// String key = UUID.randomUUID().toString().substring(0, 8);
String key = "item" + ThreadLocalRandom.current().nextInt(TOTAL_ELEMENTS);
synchronized (concurrentHashMap) {
// 如果 Map 中已经有了这个 key,那么次数 +1
if (concurrentHashMap.containsKey(key)) {
concurrentHashMap.put(key, (Long) concurrentHashMap.get(key) + 1);
}
// 如果 Map 中没有这个 key,那么放进去,次数初始化为 1
else {
concurrentHashMap.put(key, 1L);
}
}
}));

forkJoinPool.shutdown();
forkJoinPool.awaitQuiescence(1, TimeUnit.HOURS);
return concurrentHashMap;
}
}

image-20250401223807691

这段代码在功能上没有问题,但无法充分发挥 ConcurrentHashMap 的威力,改进后的代码如下:

在这段改进后的代码中,我们巧妙利用了下面两点:

  • 使用 ConcurrentHashMap 的原子性方法 computeIfAbsent 来做复合逻辑操作,判断 Key 是否存在 Value,如果不存在则把 Lambda 表达式运行后的结果放入 Map 作为 Value,也就是新创建一个 LongAdder 对象,最后返回 Value。
  • 由于 computeIfAbsent 方法返回的 Value 是 LongAdder,是一个线程安全的累加器,因此可以直接调用其 increment 方法进行累加。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static Map<String, Long> goodUse() throws InterruptedException {
ConcurrentHashMap<String, Long> concurrentHashMap = new ConcurrentHashMap<>(TOTAL_ELEMENTS);
System.out.println("init size:" + concurrentHashMap.size());
ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {
String key = "item" + ThreadLocalRandom.current().nextInt(TOTAL_ELEMENTS);
// 利用computeIfAbsent()方法来实例化LongAdder,然后利用LongAdder来进行线程安全计数
concurrentHashMap.computeIfAbsent(key, k -> new LongAdder()).increment();
}
));
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
//因为我们的Value是LongAdder而不是Long,所以需要做一次转换才能返回
return concurrentHashMap.entrySet().stream()
.collect(Collectors.toMap(
e -> e.getKey(),
e -> e.getValue().longValue())
);
}

没有认清并发工具的使用场景,因而导致性能问题。

在 Java 中,CopyOnWriteArrayList 虽然是一个线程安全的 ArrayList,但因为其实现方式是,每次修改数据时都会复制一份数据出来,所以有明显的适用场景,即读多写少或者说希望无锁读的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 测试并发写
public Map testWrite() {
List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
StopWatch stopWatch = new StopWatch();
int loopCount = 100000;
stopWatch.start("Write:copyOnWriteArrayList");
//循环100000次并发往CopyOnWriteArrayList写入随机元素
IntStream.rangeClosed(1, loopCount).parallel().forEach(__ -> copyOnWriteArrayList.add(ThreadLocalRandom.current().nextInt(loopCount)));
stopWatch.stop();
stopWatch.start("Write:synchronizedList");
//循环100000次并发往加锁的ArrayList写入随机元素
IntStream.rangeClosed(1, loopCount).parallel().forEach(__ -> synchronizedList.add(ThreadLocalRandom.current().nextInt(loopCount)));
stopWatch.stop();
log.info(stopWatch.prettyPrint());
Map result = new HashMap();
result.put("copyOnWriteArrayList", copyOnWriteArrayList.size());
result.put("synchronizedList", synchronizedList.size());
return result;
}
//帮助方法用来填充List
private void addAll(List<Integer> list) {
list.addAll(IntStream.rangeClosed(1, 1000000).boxed().collect(Collectors.toList()));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 测试并发读
public Map testRead() {
//创建两个测试对象
List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
//填充数据
addAll(copyOnWriteArrayList);
addAll(synchronizedList);
StopWatch stopWatch = new StopWatch();
int loopCount = 1000000;
int count = copyOnWriteArrayList.size();
stopWatch.start("Read:copyOnWriteArrayList");
//循环1000000次并发从CopyOnWriteArrayList随机查询元素
IntStream.rangeClosed(1, loopCount).parallel().forEach(__ -> copyOnWriteArrayList.get(ThreadLocalRandom.current().nextInt(count)));
stopWatch.stop();
stopWatch.start("Read:synchronizedList");
//循环1000000次并发从加锁的ArrayList随机查询元素
IntStream.range(0, loopCount).parallel().forEach(__ -> synchronizedList.get(ThreadLocalRandom.current().nextInt(count)));
stopWatch.stop();
log.info(stopWatch.prettyPrint());
Map result = new HashMap();
result.put("copyOnWriteArrayList", copyOnWriteArrayList.size());
result.put("synchronizedList", synchronizedList.size());
return result;
}
1
2
3
4
//帮助方法用来填充List
private void addAll(List<Integer> list) {
list.addAll(IntStream.rangeClosed(1, 1000000).boxed().collect(Collectors.toList()));
}

新特性

2024 年 6 月 20 日

JShell:快速验证简单的小问题

挺有意思的小功能,可以用来做简单的输出和运算:

image-20240620234548463

反射机制

什么是反射

Java 中的反射机制是指:在运行状态中,对于任何一个类,我们都能获取到这个类的所有属性和方法,还可以调用这些属性和方法

🔥 推荐阅读:Java 反射机制详解 | JavaGuide(Java 面试 + 学习指南)

反射的应用场景

Spring 中的依赖注入、RPC 调用、Java 中的动态代理实现

简单演示

👏 有关反射内容的介绍,网上多了去了,详尽且细致,可以参考本文开头的推荐阅读篇目挑选阅读

  • 我们的特点就是不讲任何多余的废话,直接通过简单的代码演示,体会 Java 中的反射机制** (2023/10/24 午)

构造 Person 对象

  • 成员属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 姓名
*/
private String name;
/**
* 分数
*/
protected Double grade;
/**
* 学生id
*/
long id;
/**
* 年龄
*/
public int age;
  • 构造器
1
2
3
4
5
/**
* 无参构造器
*/
public Person() {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 带参构造器
*
* @param name 姓名
* @param grade 分数
* @param id 学生id
* @param age 年龄
*/
public Person(String name, Double grade, long id, int age) {
this.name = name;
this.grade = grade;
this.id = id;
this.age = age;
}
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 带参构造器 私有
*
* @param name 姓名
* @param grade 分数
* @param id 学生id
*/
private Person(String name, Double grade, long id) {
this.name = name;
this.grade = grade;
this.id = id;
}
  • 成员方法
1
2
3
4
5
6
7
8
9
10
11
public void sayHello(){
System.out.println("Hello!");
}

private void sayHi(){
System.out.println("Hi!");
}

private void sayName(String name){
System.out.println(name);
}
  • 当然了,每个成员属性都提供有 getter、setter 方法,这里省略不写

获取 Class 对象

  • 代码详情:
1
2
3
4
5
6
7
8
9
10
11
12
// 1.1.通过具体的类的情况
Class<Person> personClass1 = Person.class;
// 1.2.通过现有的对象实例
Person person = new Person();
Class<? extends Person> personClass2 = person.getClass();
// 1.3.通过读取类路径
Class<?> personClass3 = Class.forName("反射.Person");

System.out.println("-----------获取Class对象-----------");
System.out.println(personClass1);
System.out.println(personClass2);
System.out.println(personClass3);
  • 执行结果如下:

image-20231024154344266

获取属性

  • 代码详情:
1
2
3
4
5
6
// 2.1.获取属性(非私有)
Field[] fields = personClass1.getFields();
System.out.println("-----------获取属性(Public)-----------");
for (Field field : fields) {
System.out.println("属性名: " + field.getName() + " | 类型: " + field.getType());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 2.2.获取属性(所有)
Person person1 = new Person("邓哈哈", 99.0, 18889898988L, 18);

Field[] fields2 = personClass1.getDeclaredFields();
System.out.println("-----------获取属性(所有)-----------");
for (Field field : fields2) {
field.setAccessible(true); // 启用访问私有字段

System.out.println("属性名: " + field.getName()
+ " | 类型: " + field.getType()
+ " | 访问修饰符: " + field.getModifiers()
+ " | 属性值: " + field.get(person1));
}
  • 执行结果如下: (2023/10/24 午)

image-20231024154829028

获取方法

  • 代码详情:
1
2
3
4
5
6
// 3.1.获取方法(所有)
System.out.println("-----------获取方法(所有)-----------");
Method[] methods = personClass1.getMethods();
for (Method method : methods) {
System.out.println("方法名: " + method.getName());
}
1
2
3
4
5
// 3.2.调用指定方法(指定方法名)
System.out.println("-----------调用指定方法(指定方法名)-----------");
Method sayHello = personClass1.getMethod("sayHello");
System.out.print("方法名: " + sayHello.getName() + " | 调用结果: ");
sayHello.invoke(person);
1
2
3
4
5
6
// 3.3.调用私有方法
System.out.println("-----------调用私有方法-----------");
Method sayHi = personClass1.getDeclaredMethod("sayHi");
sayHi.setAccessible(true);
System.out.print("方法名: " + sayHi.getName() + " | 调用结果: ");
sayHi.invoke(person);
1
2
3
4
5
6
// 3.4.调用带参方法
System.out.println("-----------调用带参方法-----------");
Method sayName = personClass1.getDeclaredMethod("sayName", String.class);
sayName.setAccessible(true);
System.out.print("方法名: " + sayName.getName() + " | 调用结果: ");
sayName.invoke(person, "邓哈哈");
  • 执行结果如下:

image-20231024154741899

获取构造器

  • 代码详情:
1
2
3
4
5
6
7
// 4.获取构造器
// 4.1.获取构造器(Public)
System.out.println("-----------获取构造器(Public)-----------");
Constructor<?>[] constructors = personClass1.getConstructors();
for (Constructor constructor1 : constructors) {
System.out.println(constructor1);
}
1
2
3
4
5
6
// 4.2.获取构造器(所有)
System.out.println("-----------获取构造器(所有)-----------");
Constructor[] constructors01 = personClass1.getDeclaredConstructors();
for (Constructor constructor1 : constructors01) {
System.out.println(constructor1);
}
  • 执行结果如下: (2023/10/24 午)

image-20231024155438415

踩坑记录

image-20231023223351284

代理模式

什么是代理

Java 中的代理模式是指:使用代理对象来代替对真实对象的访问,这样可以在不修改原目标对象的前提下,提供额外的功能操作

  • 代理模式可隐藏客户端真正调用的对象实现代码解耦,增强系统的可维护性和可扩展性
  • 代理模式常用于需要控制对对象的访问,并提供远程访问、安全检查和缓存等功能 (2023/10/20 午)
🔥 推荐阅读:Java 代理模式详解 | JavaGuide(Java 面试 + 学习指南)

静态代理

实现步骤

  • 定义一个接口及其实现类(被代理类)
  • 创建一个代理类,同样实现这个接口
  • 将目标对象注入进代理类,在代理类的对应方法中调用目标类的对应方法,这样我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情

代码实现

  • 短信发送接口(2023/10/24 午)
1
2
3
4
5
6
/**
* 短信发送接口
*/
public interface SmsService {
void send(String message);
}
  • 短信发送实现类
1
2
3
4
5
6
7
8
/**
* 短信发送实现
*/
public class SmsServiceImpl implements SmsService {
public void send(String message) {
System.out.println("send message:" + message);
}
}
  • 短信发送代理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 短信发送代理类
*/
public class SmsProxy implements SmsService {

private final SmsService smsService;

public SmsProxy(SmsService smsService) {
this.smsService = smsService;
}

@Override
public void send(String message) {
// 调用方法之前,我们可以添加自己的操作
System.out.println("----------在send()方法之前执行的操作----------");
smsService.send(message);
// 调用方法之后,我们同样可以添加自己的操作
System.out.println("----------在send()方法之后执行的操作----------");
}
}
  • 实际使用
1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
SmsService smsService = new SmsServiceImpl();
SmsProxy smsProxy = new SmsProxy(smsService);
smsProxy.send("java");
}
}
  • 执行结果如下:

image-20231024163414514

动态代理

相较于静态代理来说,动态代理要更加灵活,我们不需要针对每个目标类都单独创建一个代理类,也不必需要实现接口,我们可以直接代理实现类

👏 实现方式:就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理CGLIB 动态代理

动态代理在我们日常开发中几乎用不到,或者说使用相对较少,但在框架中,几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助

🧯 使用场景Spring AOPRPC 框架

7000 字详解 动态代理(JDK 动态代理 CGLIB 动态代理)与静态代理_jdk17 代理-CSDN 博客

JDK 动态代理

JDK 动态代理的实现原理是:动态创建代理类,然后通过指定类加载器进行加载。在创建代理对象时,需要将 InvocationHandler 对象作为构造参数传入;当调用代理对象时,会调用 InvocationHandler.invoke() 方法,从而执行代理逻辑,最终调用真正业务对象的相应方法。

JDK 动态代理执行流程:通过 Proxy 类的 newProxyInstance() 创建的代理对象再调用方法的时候,实际会调用代理类(实现 InvocationHandler 接口的类)的 invoke() 方法。

还是那句话,有关 JDK 动态代理的实现原理,这里不做详尽的解释,我们主打不讲任何多余的废话,通过简单的代码演示:

🔥 推荐阅读Java 代理模式详解 | JavaGuide(Java 面试 + 学习指南)

  • 定义发送短信的接口(2023/10/24 晚)
1
2
3
4
5
6
/**
* 短信发送接口
*/
public interface SmsService {
String send(String message);
}
  • 实现发送短信的接口
1
2
3
4
5
6
7
8
9
/**
* 实现短信发送接口
*/
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
  • 定义 JDK 动态代理类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 定义 JDK 动态代理类
*/
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;

public DebugInvocationHandler(Object target) {
this.target = target;
}

public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
// 调用方法之前,我们可以添加自己的操作
System.out.println("----------在send()方法之前执行的操作----------");
Object result = method.invoke(target, args);
// 调用方法之后,我们同样可以添加自己的操作
System.out.println("----------在send()方法之后执行的操作----------");
return result;
}
}
  • 获取代理对象的工厂类
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 获取代理对象的工厂类
*/
public class JdkProxyFactory {
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目标类的类加载器
target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个
new DebugInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler
);
}
}
  • 实际使用
1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
smsService.send("java");
}
}
  • 执行结果如下:

image-20231024223700470

CGLIB 动态代理

CGLIB 动态代理执行流程:自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理增强的方法,通过 Enhancer 类的 create() 创建代理类。当代理类调用方法的时候,实际调用的是 MethodIntercept 中的 intercept 方法

有关 CGLIB 动态代理的实现原理,这里不做详尽的解释,我们主打不讲任何多余的废话,通过简单的代码演示:

🔥 推荐阅读Java 代理模式详解 | JavaGuide(Java 面试 + 学习指南)(2023/10/30 晚)

I/O 流

字节流

1
2
3
4
5
6
7
8
9
10
11
12
static void testFileInputStream() {
try {
FileInputStream fis = new FileInputStream("D:\\Project\\IDEA\\demo\\demo2\\demo\\algorisem\\IO\\IO流练习\\text\\hello.txt");

int content;
while ((content = fis.read()) != -1) {
System.out.println(content);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void testFileInputStream2() {
try {
FileInputStream fis = new FileInputStream("D:\\Project\\IDEA\\demo\\demo2\\demo\\algorisem\\IO\\IO流练习\\text\\hello.txt");

int len;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
System.out.print(new String(bytes, 0, len));
System.out.print(len);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
static void testFileReader() {
try {
FileReader fr = new FileReader("D:\\Project\\IDEA\\demo\\demo2\\demo\\algorisem\\IO\\IO流练习\\text\\hello.txt");

int content;
while ((content = fr.read()) != -1) {
System.out.println((char) content);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
static void testFileReader2() {
try {
FileReader fr = new FileReader("D:\\Project\\IDEA\\demo\\demo2\\demo\\algorisem\\IO\\IO流练习\\text\\hello.txt");

int len;
char[] chars = new char[1024];
while ((len = fr.read(chars)) != -1) {
System.out.println(new String(chars, 0, len));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
1
2
3
4
5
6
7
8
9
10
11
static void testBufferInputStream() {
try {
BufferedInputStream bis = new BufferedInputStream(new
FileInputStream("D:\\Project\\IDEA\\demo\\demo2\\demo\\algorisem\\IO\\IO流练习\\text\\hello.txt"));

String str = new String(bis.readAllBytes());
System.out.println(str);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void copy_pdf_to_another_pdf_with_byte_array_buffer_stream() {
// 记录开始时间
long start = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\Project\\IDEA\\demo\\demo2\\demo\\algorisem\\IO\\IO流练习\\text\\1.png"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\Project\\IDEA\\demo\\demo2\\demo\\algorisem\\IO\\IO流练习\\text\\111.png"))) {
int len;
byte[] bytes = new byte[4 * 1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("使用缓冲流复制PDF文件总耗时:" + (end - start) + " 毫秒");
}
1
2
3
4
5
6
7
8
9
10
static void testFileOutputStream() {
try {
FileOutputStream fos = new FileOutputStream("D:\\Project\\IDEA\\demo\\demo2\\demo\\algorisem\\IO\\IO流练习\\text\\hello2.txt");

byte[] bytes = "你好,这又是一段话".getBytes();
fos.write(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void copyInputStream() {
try {
FileInputStream fis = new FileInputStream("D:\\Project\\IDEA\\demo\\demo2\\demo\\algorisem\\IO\\IO流练习\\text\\hello2.txt");
FileOutputStream fos = new FileOutputStream("D:\\Project\\IDEA\\demo\\demo2\\demo\\algorisem\\IO\\IO流练习\\text\\hello3.txt");

int len;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

字符流

注解

🍖 推荐阅读:Spring Boot 自定义注解 - 掘金 (juejin.cn)(2024/01/07 晚)

元注解

@Target

  • 注解作用的范围

@Retention

  • 注解的生命周期

@Document

  • 注解生成在 Javadoc 生成的文档中

@Inherited

  • 注解可被子类继承

JDK 内置注解

@Override

@Deperecated

@SuppressWarnings

自定义注解

八股吟唱

谈一谈你对多态的理解

我们经常讲,面向对象有三大特征:封装、继承和多态(2023/11/18 早)

多态是面向对象编程中相当重要的一个概念,它允许通过父类类型的引用变量引用子类对象,在运行时根据实际的对象类型来确定调用哪个方法,一个对象能够根据不同的场景表现出多种形态

那多态具体是怎么实现的?在编译时,Java 编译器只能知道变量的声明类型,也就是父类类型,而无法确定实际的对象类型;而在运行时,Java 虚拟机会通过动态绑定解析出实际对象的类型,根据实际的对象类型调用被子类重写的方法。

也就是说,编译器会把方法的绑定,即方法的具体调用推迟到运行时,这就是动态绑定,这就是多态的实现原理

我们发现使用多态有这样的好处:我们通过父类类型的引用来访问子类对象的方法,统一对象的接口,这是接口统一性;子类对象可以随时替代父类对象,向上转型,这就是可替换性;我们可以通过添加新的子类,扩展系统功能,这就是可扩展性;通过多态,能够实现对象间的解耦,因为我们不再需要指定具体对象去实现具体方法了,这使得代码更加简洁通用、更加易于维护

我们在编码开发中接触到的方法重载、方法重写、接口实现就是多态的具体实现,方法重载体现的是编译时的多态,而方法重写和接口实现体现的是运行时的多态

String 内容不可变

🔥 有关 Java 中 String 内容不可变的解释(2023/11/19 午)

我们经常听到这样的定义:String 对象一旦被创建,其内容就一定不可变

这句话的意思是:对该对象的所有操作(如 replace()contact()substring())都将返回新的 String 对象,而不是在原 String 对象的内容上作修改。这些操作也都是不被允许的:

1
2
3
4
5
6
7
8
9
String s = "Hello";

s.charAt(0) = 'h'; // 编译错误:Cannot assign to 'char' field in an array of 'final' value

s.length() = 5; // 编译错误:Cannot assign to 'int' field in an array of 'final' value

String t = s.substring(0, 5); // 可以,但结果是新的 String 对象,不是对原有对象的修改

t = t + " World"; // 可以,但结果是新的 String 对象,不是对原有对象的修改

为什么是这样的?String 对象内容不可变是如何保证的,这样做又有什么好处呢?

String 类被设计出来是为了方便我们对字符串进行操作,我们常见的字符串拼接、比较字符串内容、字符串长度等等,应用十分广泛。而 String 类底层是通过 char [] 数组(Java 9 之后改为 byte [] 实现了)来维护字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {

/**
* The value is used for character storage.
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*
* Additionally, it is marked with {@link Stable} to trust the contents
* of the array. No other facility in JDK provides this functionality (yet).
* {@link Stable} is safe here, because value is never null.
*/
@Stable
private final byte[] value;

.................
}

需要注意的是,这个字符串使用了 final 关键字修饰。

我们都知道被 final 关键字修饰的类不能被继承、修饰的方法不能被重写、修饰的基本数据类型变量的值不能被改变,修饰的引用类型变量不能再指向其他对象。很显然,这里的 char [] 数组属于引用型变量,所以其内容是可以改变的

这就是很多人疑惑的点了:难道不是很奇怪吗?String 的内容是不可变的,但 String 底层是 final 修饰的 char[] 数组实现的,而这个数组内容是可变的。所以你给解释一下 String 内容不可变到底是怎么一回事

很多八股在这里都在扯淡,在这里我给出正确答案:这里的 char [] 数组属于引用型变量,理论上它的内容当然是可以改变的:

1
2
3
final String[] arr = new String[]{"Hello", "World"};

arr[0] = "Hi"; // 这里是可以的,因为arr[0]指向的对象(即"Hello")是可以改变的

但是这一点跟 String 内容是不可变的本身没有冲突,因为 String 并没有对外提供任何方法,去改变内置的 char [] 数组的内容,所以 String 对外表现出的 String 内容不可变,这就是:String 对象一旦被创建,其内容就一定不可变的正确解释

综上所述,String 类是不可变的,这意味着一旦一个 String 对象被创建,它的内容就不能被修改。即使 String 底层是通过 final 修饰的 char 数组实现的,但是这个 char 数组的内容也不能被修改,因为 String 并没有对外提供任何方法,允许我们去改变内置的 char [] 数组的内容。因此,即使我们可以访问到 String 对象的底层 char 数组,我们也不能通过改变这个数组来修改 String 对象的内容。任何尝试修改 String 对象内容的操作都会返回一个新的 String 对象,而原来的 String 对象保持不变

查看源码你就能清楚地看到这个过程了,当然源码很复杂,这里展示出 replace 的部分源码,你可以看到在执行这个操作的过程中,是 new 了新的 byte [] 的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static String replace(byte[] value, char oldChar, char newChar) {
if (canEncode(oldChar)) {
int len = value.length;
int i = -1;
while (++i < len) {
if (value[i] == (byte)oldChar) {
break;
}
}
if (i < len) {
if (canEncode(newChar)) {
byte buf[] = new byte[len];
for (int j = 0; j < i; j++) { // TBD arraycopy?
buf[j] = value[j];
}
while (i < len) {
byte c = value[i];
buf[i] = (c == (byte)oldChar) ? (byte)newChar : c;
i++;
}
return new String(buf, LATIN1);
} else {
byte[] buf = StringUTF16.newBytesFor(len);
// inflate from latin1 to UTF16
inflate(value, 0, buf, 0, i);
while (i < len) {
char c = (char)(value[i] & 0xff);
StringUTF16.putChar(buf, i, (c == oldChar) ? newChar : c);
i++;
}
return new String(buf, UTF16);
}
}
}
return null; // for string to return this;
}
  • 这样做有什么好处?这种不可变性是 Java String 类的一个重要特性,使得 String 可以安全地被共享和传递,而不需要担心其他部分的代码会修改它的内容

Java 基础

  • 本人太菜,不定时巩固 Java 基础,今天巩固如下操作:(2023/08/14 早)
    • Radom 随机数

    • Collections 排序

    • Stream 流遍历

    • System.currentTimeMillis() 计时

1
2
3
4
5
6
7
// Radom 随机数
Random random = new Random();
ArrayList<Double> arrayList = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
arrayList.add(random.nextDouble());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Collections 排序
// 第一种方法
Collections.sort(arrayList, new Comparator<Double>() {
@Override
public int compare(Double o1, Double o2) {
return o1.compareTo(o2);
}
});
// 第二种方法
Collections.sort(arrayList, (o1, o2) -> {
return o1.compareTo(o2);
});
// 第三种方法
Collections.sort(arrayList, (o1, o2) -> o1.compareTo(o2));
1
2
3
4
5
6
7
8
9
// Stream 流便利
// 第一种方法
arrayList.forEach(item -> {
System.out.println(item);
});

// 第二种方法
arrayList.stream().forEach(item -> System.out.println(item));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Integer> userList = new ArrayList<>();

Random rand = new Random();
for (int i = 0; i < 10000; i++) {
userList.add(rand.nextInt(1000));
}

List<Integer> userList2 = new ArrayList<>();
userList2.addAll(userList);

Long startTime1 = System.currentTimeMillis();
userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
System.out.println("stream.sort耗时:" + (System.currentTimeMillis() - startTime1) + "ms");

Long startTime = System.currentTimeMillis();
userList.sort(Comparator.comparing(Integer::intValue));
System.out.println("List.sort()耗时:" + (System.currentTimeMillis() - startTime) + "ms");

Date/Time API

  • 学习以下内容可以参考这篇博客:8 个你必须知道的 Java8 新特性,让你的代码变得优雅! - 掘金 (juejin.cn)
  • 在 Java 8 中引入了全新的日期时间处理类库,使得处理日期时间变得更加方便和易于理解

  • 以下是 java.time 包中最重要的一些类:(2023/08/18 午)
    • LocalDate:表示日期,如 2022 年 5 月 15 日。
    • LocalTime:表示时间,如 13:45:30。
    • LocalDateTime:代表日期和时间,比如 2022 年 5 月 15 日 13:45:30。
    • ZonedDateTime:代表带有时区的日期时间,在上述所有日期时间类型的基础上,还提供了时区信息。
    • Duration:在两个时间点之间表示时间量。
    • Period:在两个日期之间表示天、周、月或年的数量。
    • DateTimeFormatter:可以将日期时间对象按照指定的格式进行格式化或者解析。
1
2
3
4
5
6
7
8
9
10
11
12
static void test() {
// 获取日期
LocalDateTime dateTime = LocalDateTime.now();
System.out.println(dateTime);
// 日期格式
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String format = dateTime.format(pattern);
System.out.println(format);

String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒"));
System.out.println(now);
}
1
2
3
4
5
6
7
static void test2() {
// 获取日期
Duration between = Duration.between(LocalDate.of(2022, Month.JULY, 10), LocalDate.now());
System.out.println(between);
Duration between1 = Duration.between(LocalTime.of(12, 29, 10), LocalDate.now());
System.out.println(between1);
}

Optional 容器类型

null 不好,我真的推荐你使用 Optional - 掘金 (juejin.cn)

那么使用 Optional 容器类型 有什么好处呢?(2023/11/29 午)

  • 可以避免空指针异常,提高代码的健壮性和可读性。
  • 可以减少显式的空值检查和 null 的使用,使代码更简洁和优雅。
  • 可以利用函数式编程的特性,实现更灵活和高效的逻辑处理。
  • 可以提高代码的可测试性,方便进行单元测试和集成测试。
  • ofNullable() 方法:创建一个可能包含 null 值的 Optional 对象(2023/08/18 午)

  • isPresent() 方法:判断 Optional 中是否存在值。返回 ture 表示存在值 返回 false 表示为 null

  • get() 方法:如果 Optional 的值存在则返回该值,否则抛出 NoSuchElementException 异常。

1
2
3
4
5
6
7
8
9
static void test3() {
// 判断值是否为空
Optional<String> optional = Optional.of("hhh");
if (optional.isPresent()) {
System.out.println(optional.get());
}

Optional.of("hhh").ifPresent(System.out::println);
}
1
2
3
static void test4(User user) {
Optional.ofNullable(user).map(User::getName).ifPresentOrElse(out::println, user != null ? user.setName("ccc") : null);
}
1
2
// 创建一个空的 Optional 对象
Optional<String> empty = Optional.empty();
1
2
// 创建一个非空的 Optional 对象
Optional<String> hello = Optional.of("Hello");
1
2
// 创建一个可能为空的 Optional 对象
Optional<String> name = Optional.ofNullable("Hello");
1
2
3
4
5
6
7
8
// 使用 isPresent 和 get 方法
Optional<String> name1 = Optional.ofNullable("tom");
if (name.isPresent()) {
System.out.println("Hello, " + name1.get());
} else {
System.out.println("Name is not available");
}
// 输出:Hello tom
1
2
3
4
5
6
7

// 使用 ifPresent(Consumer<? super T> action)
Optional<String> name2 = Optional.ofNullable("tom");
name.ifPresent(s -> {
System.out.println("Hello, " + name2.get());
});
// 输出:Hello tom
1
2
3
4
5
6

// 使用 orElse(T other)
Optional<String> name3 = Optional.ofNullable(null);
String greeting = "Hello, " + name3.orElse("Guest");
System.out.println(greeting);
// 输出:Hello Guest
1
2
3
4
5

// 使用 orElseThrow(Supplier<? extends X> exceptionSupplier)
Optional<String> name4 = Optional.ofNullable("null");
String greeting2 = "Hello, " + name4.orElseThrow(() -> new NullPointerException("null"));
// 抛出 java.lang.NullPointerException: null 异常
1
2
3
4
5
6

// 使用 map(Function<? super T,? extends U> mapper)
Optional<String> name5 = Optional.ofNullable("tom");
String greeting3 = "Hello, " + name5.map(s -> s.toUpperCase()).get();
System.out.println(greeting3);
// 输出:Hello TOM
1
2
3
4
5
// 使用 flatMap(Function<? super T,Optional<U>> mapper)
Optional<String> name6 = Optional.ofNullable("tom");
String greeting4 = name6.flatMap(s -> Optional.of("Hello " + s)).get();
System.out.println(greeting4);
// 输出:Hello tom
1
2
3
4
5
// filter(Predicate<? super T> predicate)
Optional<String> name7 = Optional.ofNullable("tom");
String greeting5 = "Hello " + name7.filter(s -> !s.isEmpty()).get();
System.out.println(greeting5);
// 输出:Hello tom

BigDecimal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void test5() {
// 1.两个BigDecimal对象
BigDecimal dec1 = new BigDecimal("1.3");
BigDecimal dec2 = new BigDecimal("1.3");

// 2.加减乘除
BigDecimal add = dec1.add(dec2);
BigDecimal subtract = dec1.subtract(dec2);
BigDecimal multiply = dec1.multiply(dec2);

// 3.设置精度
BigDecimal divide = dec1.divide(dec2).setScale(3, RoundingMode.HALF_DOWN);

out.println(add);
out.println(subtract);
out.println(multiply);
System.out.println(divide);

// 4.去掉末尾的0
BigDecimal bigDecimal = divide.stripTrailingZeros();
out.println(bigDecimal);

// 5.转换为Double
BigDecimal value = new BigDecimal("1888977466432.1241341341413414");
double doubleValue = value.doubleValue();
out.println(doubleValue);
double doubleValue1 = value.toBigInteger().doubleValue();
out.println(doubleValue1);

// 6.比较大小
out.println(add.compareTo(subtract) > 0 ? "add大于sub" : "add小于sub");
}

数组转字符串

1
2
3
4
// 创建一个List
List<String> list = Arrays.asList("Apple", "Banana", "Cherry", "Date", "Elderberry");
// 创建一个String数组
String[] arr = {"0", "1", "2", "3", "4", "5"};
  • 如果是 List,则有如下转字符串的方法:(2023/09/20 晚)
1
2
// stream流
String collect = list.stream().collect(Collectors.joining(","));
1
2
//org.apache.commons.lang3包
String join = StringUtils.join(longList, ",");
1
2
3
4
5
6
7
8
// StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
sb.append(i);
if (i < list.size() - 1) {
sb.append(",");
}
}
1
2
// String.join
String join = String.join(",", list);
  • 以上方法,转换所消耗的时间越来越少效率越来越高

  • 如果是 String[],则有如下转字符串的方法:(2023/09/20 晚)
1
2
//org.apache.commons.lang3包
String join = StringUtils.join(longList, ",");
1
2
// String.join
String join = String.join(",", list);

Java8 中的 Map 函数

computeIfAbsent

  • 如果指定的 key 不存在于 Map 中,那么会执行指定的函数来计算并将结果作为 value 放入到 Map 中。
  • 如果指定的 key 已经存在于 Map 中,则不会执行计算函数,而是直接返回已存在的 value。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.HashMap;
import java.util.Map;

public class ComputeIfAbsentExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);

// 当key不存在时,执行计算函数并将结果放入Map
map.computeIfAbsent("orange", key -> {
System.out.println("Performing computation for orange");
return key.length();
});

// 当key已存在时,不执行计算函数,直接返回已存在的value
map.computeIfAbsent("apple", key -> {
System.out.println("Performing computation for apple");
return key.length();
});

System.out.println(map);
}
}

computeIfPresent

  • 如果指定的 key 存在于 Map 中,那么会执行指定的函数来计算并将结果作为新的 value 放入到 Map 中。
  • 如果指定的 key 不存在于 Map 中,则不会执行计算函数,什么也不做
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.HashMap;
import java.util.Map;

public class ComputeIfPresentExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);

map.computeIfPresent("apple", (key, value) -> value * 2);
map.computeIfPresent("orange", (key, value) -> value * 2);

System.out.println(map);
}
}

compute

  • compute()函数无论旧的 value 是否为null,都会调用计算函数来计算新的 value,并将计算结果更新到 Map 中。
  • 如果计算结果为null,则会将对应的 key 从 Map 中移除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.HashMap;
import java.util.Map;

public class ComputeExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", null);

// 使用computeIfPresent()
map.computeIfPresent("apple", (key, value) -> value * 2);
map.computeIfPresent("banana", (key, value) -> value * 2);
System.out.println(map); // 输出: {apple=2}

// 使用compute()
map.compute("apple", (key, value) -> value + 3);
map.compute("banana", (key, value) -> value + 3);
System.out.println(map); // 输出: {apple=5, banana=null}
}
}

merge

  • 这个目前还不太明白,按下不表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.HashMap;
import java.util.Map;

public class MergeExample {
public static void main(String[] args) {
Map<String, Integer> map1 = new HashMap<>();
map1.put("apple", 1);
map1.put("banana", 2);

Map<String, Integer> map2 = new HashMap<>();
map2.put("apple", 5);
map2.put("orange", 3);

map2.forEach((key, value) -> {
map1.merge(key, value, (oldValue, newValue) -> oldValue + newValue);
});

System.out.println(map1); // 输出: {orange=3, apple=6, banana=2}
}
}

getOrDefalut

  • 当 key 存在时,取对应 value,否则取默认值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int firstUniqChar(String s) {
if (s == null || s.length() == 0) {
return 0;
}
Map<Character, Integer> frequency = new HashMap<Character, Integer>();
for (int i = 0; i < s.length(); ++i) {
char ch = s.charAt(i);
frequency.put(ch, frequency.getOrDefault(ch, 0) + 1);
}
for (int i = 0; i < s.length(); ++i) {
if (frequency.get(s.charAt(i)) == 1) {
return i;
}
}
return -1;
}

putIfAbsent

  • 与 put 的区别:如果有重复 key,保存的是最早存入的键值对 (2023/10/08 早)

forEach

Java8 中的 Stream 流函数

groupingBy

  • 将集合中的元素,按某个属性进行分组返回的结果是一个 Map 集合 (2023/10/08 早)
1
2
3
4
5
6
7
8
9
public class Person {
private int age;
private String name;

public Person(int age, String name) {
this.age = age;
this.name = name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class GroupingByExample {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person(25, "Alice"));
people.add(new Person(30, "Bob"));
people.add(new Person(25, "Charlie"));
people.add(new Person(40, "David"));
people.add(new Person(30, "Emily"));

Map<Integer, List<Person>> groups = people.stream()
.collect(Collectors.groupingBy(Person::getAge));

System.out.println(groups);
}
}

泛型理解

  • 推荐学习:我真的了解泛型吗? - 掘金 (juejin.cn)
  • 学习 Java 泛型:(2023/10/13 晚)
    • 泛型类
    • 泛型接口
    • 泛型方法:返回值、入参位置、方法体中
    • 个人感受:泛型很好理解:我们经常讲到一个对象实例的就是以类作为模板创建的,那么也可以讲一个普通类可以以泛型类作为模板;那么泛型是用来干嘛的呢,我们为什么要使用泛型呢?其实,所有的泛型类在编译后生成的字节码与普通类无异,因为在编译前,所有泛型类型就被擦除了。所以我们可以把泛型看作一个语法糖,将类型转换的校验提前在编译时,减少类型转换错误的发生,使编写的程序更加具有健壮性。
  • 我觉得以下这段总结更妙:

泛型是 Java 语言中的一项强大的特性,它允许在编译时指定类、接口或方法的参数类型,从而在编译阶段就能够进行类型检查。这样可以减少类型转换的错误,并提高代码的安全性和可读性。

通过使用泛型,我们可以在编译时捕捉到一些类型错误,而不是在运行时才发现,这样可以避免一些潜在的 bug。泛型还可以增加代码的可重用性和灵活性,因为泛型类、接口和方法可以用于多种不同的类型,而无需针对每一种类型单独编写或重复编写相似的代码。

总的来说,通过使用泛型,我们可以在编写 Java 代码时更好地约束和使用类型信息,减少类型错误,提高代码的可读性和健壮性。

  • 了解泛型的实现原理,理解泛型的使用方式,更加加深了我对 Java 语言的理解

JVM

🥣 推荐阅读:jvm 内存模型(运行时数据区)简介 - 知乎 (zhihu.com) (2023/12/13)

锦绣收官


Java终极冒险:从基础到实战,解锁Java的神秘密码
https://test.atomgit.net/blog/2023/10/24/Java终极冒险:从基础到实战,解锁Java的神秘密码/
作者
Memory
发布于
2023年10月24日
更新于
2025年7月31日
许可协议