本文最后更新于: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 { ZhuZi zhuZi = new ZhuZi (1 , "黄金竹子" , "A级" ); byte [] serializeBytes = serialize(zhuZi); System.out.println("JDK序列化后的字节数组长度:" + serializeBytes.length); ZhuZi deserializeZhuZi = deserialize(serializeBytes); System.out.println(deserializeZhuZi.toString()); } private static byte [] serialize(ZhuZi zhuZi) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (bos); oos.writeObject(zhuZi); return bos.toByteArray(); } 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、FastJson
,SpringMVC
框架中默认使用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级" ); String json = JSONObject.toJSONString(zhuZi); System.out.println(json); System.out.println("Json序列化后的体积:" + json.getBytes().length); ZhuZi zhuZiJson = JSONObject.parseObject(json, ZhuZi.class); System.out.println(zhuZiJson); }
使用起来特别简单,重点来看看输出结果里的体积,比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
中默认的序列化机制,用于服务提供者与消费者之间进行数据传输,这里咱们也简单过一下。
Hessian
和JDK
原生的序列化技术,兼容度很高,相较于使用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 { ZhuZi zhuZi = new ZhuZi (1 ,"黄金竹子" , "A级" ); byte [] serializeBytes = serialize(zhuZi); System.out.println("Hessian序列化后字节数组长度:" + serializeBytes.length); ZhuZi deserializeZhuZi = deserialize(serializeBytes); System.out.println(deserializeZhuZi.toString()); } 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(); } 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代码助手 代码解读复制代码public class BioServer { public static void main (String[] args) throws IOException { System.out.println(">>>>>>>...BIO服务端启动...>>>>>>>>" ); ServerSocket server = new ServerSocket (8888 ); Socket socket = server.accept(); InputStream inputStream = socket.getInputStream(); BufferedReader readBuffer = new BufferedReader (new InputStreamReader (inputStream)); String msg; while ((msg = readBuffer.readLine()) != null ) { System.out.println("收到信息:" + msg); } OutputStream outputStream = socket.getOutputStream(); PrintStream printStream = new PrintStream (outputStream); printStream.println("Hi!我是竹子~" ); printStream.flush(); outputStream.close(); inputStream.close(); socket.close(); inputStream.close(); outputStream.close(); socket.close(); server.close(); } }public class BioClient { public static void main (String[] args) throws IOException { System.out.println(">>>>>>>...BIO客户端启动...>>>>>>>>" ); Socket socket = new Socket ("127.0.0.1" , 8888 ); OutputStream outputStream = socket.getOutputStream(); PrintStream printStream = new PrintStream (outputStream); printStream.println("Hello!我是熊猫~" ); socket.shutdownOutput(); InputStream inputStream = socket.getInputStream(); BufferedReader readBuffer = new BufferedReader (new InputStreamReader (inputStream)); String msg; while ((msg = readBuffer.readLine()) != null ) { System.out.println("收到信息:" + msg); } printStream.flush(); 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
的通信过程中,如若客户端一直没有发送消息过来,服务端则会一直等待下去,从而服务端陷入阻塞状态。同理,由于客户端也一直在等待服务端的消息,如若服务端一直未响应消息回来,客户端也会陷入阻塞状态。
简单了解了选择器的基础概念后,那如何使用它实现非阻塞模型呢?如下:
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代码助手 代码解读复制代码public class NioServer { public static void main (String[] args) throws Exception { System.out.println(">>>>>>>...NIO服务端启动...>>>>>>>>" ); ServerSocketChannel ssc = ServerSocketChannel.open(); Selector selector = Selector.open(); ByteBuffer buffer = ByteBuffer.allocate(1024 ); ssc.bind(new InetSocketAddress ("127.0.0.1" ,8888 )); ssc.configureBlocking(false ); ssc.register(selector, SelectionKey.OP_ACCEPT); while (selector.select() > 0 ){ Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKey next = iterator.next(); if (next.isAcceptable()){ SocketChannel channel = ssc.accept(); channel.configureBlocking(false ); int event = SelectionKey.OP_READ | SelectionKey.OP_WRITE; channel.register(selector,event); System.out.println("客户端连接:" + channel.getRemoteAddress()); } else if (next.isReadable()){ SocketChannel channel = (SocketChannel)next.channel(); int len = -1 ; while ((len = channel.read(buffer)) > 0 ){ buffer.flip(); System.out.println("收到信息:" + new String (buffer.array(),0 ,buffer.remaining())); } buffer.clear(); } } iterator.remove(); } } }public class NioClient { public static void main (String[] args) throws Exception { System.out.println(">>>>>>>...NIO客户端启动...>>>>>>>>" ); SocketChannel channel = SocketChannel.open( new InetSocketAddress ("127.0.0.1" ,8888 )); channel.configureBlocking(false ); ByteBuffer buffer = ByteBuffer.allocate(1024 ); String msg = "我是熊猫!" ; buffer.put(msg.getBytes()); buffer.flip(); channel.write(buffer); buffer.clear(); channel.close(); } }
在如上案例中,即实现了一个最简单的NIO
服务端与客户端通信的案例,重点要注意:注册到选择器上的通道都必须要为非阻塞模型,同时通过缓冲区传输数据时,必须要调用flip()
方法切换为读取模式。
OK,最后简单叙述一下缓冲区、通道、选择器三者关系:  如上图所示,每个客户端连接本质上对应着一个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("/**" ) .allowedOrigins("http://xxx.com" ) .allowedMethods("GET" , "POST" , "PUT" , "DELETE" ) .allowedHeaders("*" ) .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) { 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(); }); } 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自带的这些函数式接口呢?来个例子感受一下。
需求:实现两个数字的加减乘除计算。
如果用之前的思维来实现,要么就分别定义加、减、乘、除四个方法,要么就传一个运算符,在用if
或switch
判断,以此实现不同的计算逻辑,但现在可以用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); } }); } }
来看上面这个回调事件的例子,其中的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代码助手 代码解读复制代码public class BeanCopyUtil { 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; } 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代码助手 代码解读复制代码User user = userMapper.selectById(userId);UserVO result = BeanCopyUtil.copy(user, UserVO.class); 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
写法,和之前的唯一区别在于:如果对应的函数没有入参,那么参数列表部分就用()
小括号代替即可 。再来看看多参数:
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 + "个点赞!" ); }); } }
与无参数的写法对比,如果函数存在多个入参,只需要用()
将参数列表包起来、多个参数用,
逗号隔开就行,函数存在多少个入参,这里就需要定义多少个参数,顺序与函数定义的入参列表一一对应。好了,再回去看到只有一个入参的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(); 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(); 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 { 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 ; 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 { 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()); } }
回到 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 -> { String key = "item" + ThreadLocalRandom.current ().nextInt (TOTAL_ELEMENTS); synchronized (concurrentHashMap) { if (concurrentHashMap.containsKey (key )) { concurrentHashMap.put (key , (Long) concurrentHashMap.get (key ) + 1 ); } else { concurrentHashMap.put (key , 1 L); } } })); forkJoinPool.shutdown (); forkJoinPool.awaitQuiescence (1 , TimeUnit.HOURS ); return concurrentHashMap; } }
这段代码在功能上没有问题,但无法充分发挥 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); concurrentHashMap.computeIfAbsent(key, k -> new LongAdder ()).increment(); } )); forkJoinPool.shutdown(); forkJoinPool.awaitTermination(1 , TimeUnit.HOURS); 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" ); IntStream.rangeClosed(1 , loopCount).parallel().forEach(__ -> copyOnWriteArrayList.add(ThreadLocalRandom.current().nextInt(loopCount))); stopWatch.stop(); stopWatch.start("Write:synchronizedList" ); 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; }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" ); IntStream.rangeClosed(1 , loopCount).parallel().forEach(__ -> copyOnWriteArrayList.get(ThreadLocalRandom.current().nextInt(count))); stopWatch.stop(); stopWatch.start("Read:synchronizedList" ); 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 private void addAll (List<Integer> list) { list.addAll(IntStream.rangeClosed(1 , 1000000 ).boxed().collect(Collectors.toList())); }
新特性
2024 年 6 月 20 日
JShell:快速验证简单的小问题 挺有意思的小功能,可以用来做简单的输出和运算:
反射机制 什么是反射
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;long id;public int age;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 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 Class<Person> personClass1 = Person.class;Person person = new Person (); Class<? extends Person > personClass2 = person.getClass(); Class<?> personClass3 = Class.forName("反射.Person" ); System.out.println("-----------获取Class对象-----------" ); System.out.println(personClass1); System.out.println(personClass2); System.out.println(personClass3);
获取属性
1 2 3 4 5 6 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 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)); }
获取方法
1 2 3 4 5 6 System.out.println("-----------获取方法(所有)-----------" ); Method[] methods = personClass1.getMethods();for (Method method : methods) { System.out.println("方法名: " + method.getName()); }
1 2 3 4 5 System.out.println("-----------调用指定方法(指定方法名)-----------" );Method sayHello = personClass1.getMethod("sayHello" ); System.out.print("方法名: " + sayHello.getName() + " | 调用结果: " ); sayHello.invoke(person);
1 2 3 4 5 6 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 System.out.println("-----------调用带参方法-----------" );Method sayName = personClass1.getDeclaredMethod("sayName" , String.class); sayName.setAccessible(true ); System.out.print("方法名: " + sayName.getName() + " | 调用结果: " ); sayName.invoke(person, "邓哈哈" );
获取构造器
1 2 3 4 5 6 7 System.out.println("-----------获取构造器(Public)-----------" ); Constructor<?>[] constructors = personClass1.getConstructors();for (Constructor constructor1 : constructors) { System.out.println(constructor1); }
1 2 3 4 5 6 System.out.println("-----------获取构造器(所有)-----------" ); Constructor[] constructors01 = personClass1.getDeclaredConstructors();for (Constructor constructor1 : constructors01) { System.out.println(constructor1); }
踩坑记录
代理模式 什么是代理
Java 中的代理模式是指:使用代理对象来代替对真实对象的访问,这样可以在不修改原目标对象的前提下,提供额外的功能操作
代理模式可隐藏客户端真正调用的对象 ,实现代码解耦 ,增强系统的可维护性和可扩展性
代理模式常用于需要控制对对象的访问,并提供远程访问、安全检查和缓存 等功能 (2023/10/20 午)
静态代理 实现步骤
定义一个接口及其实现类(被代理类)
创建一个代理类,同样实现这个接口
将目标对象注入进代理类,在代理类的对应方法中调用目标类的对应方法,这样我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情
代码实现
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" ); } }
动态代理
相较于静态代理来说,动态代理要更加灵活,我们不需要针对每个目标类都单独创建一个代理类,也不必需要实现接口,我们可以直接代理实现类
👏 实现方式 :就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理 、CGLIB 动态代理
动态代理在我们日常开发中几乎用不到,或者说使用相对较少,但在框架中,几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助
🧯 使用场景 :Spring AOP 、RPC 框架
7000 字详解 动态代理(JDK 动态代理 CGLIB 动态代理)与静态代理_jdk17 代理-CSDN 博客
JDK 动态代理 JDK 动态代理的实现原理是:动态创建代理类,然后通过指定类加载器进行加载 。在创建代理对象时,需要将 InvocationHandler 对象作为构造参数传入;当调用代理对象时,会调用 InvocationHandler.invoke() 方法,从而执行代理逻辑,最终调用真正业务对象的相应方法。
JDK 动态代理执行流程 :通过 Proxy
类的 newProxyInstance()
创建的代理对象再调用方法的时候,实际会调用代理类(实现 InvocationHandler
接口的类)的 invoke()
方法。
还是那句话,有关 JDK 动态代理的实现原理,这里不做详尽的解释,我们主打不讲任何多余的废话,通过简单的代码演示:
🔥 推荐阅读 :Java 代理模式详解 | JavaGuide(Java 面试 + 学习指南)
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; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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) ); } }
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" ); } }
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
@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' ; s.length() = 5 ; String t = s.substring(0 , 5 ); t = t + " World" ;
为什么是这样的?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 { @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" ;
但是这一点跟 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++) { 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(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 ; }
这样做有什么好处?这种不可变性是 Java String 类的一个重要特性,使得 String 可以安全地被共享和传递,而不需要担心其他部分的代码会修改它的内容
Java 基础
本人太菜,不定时巩固 Java 基础,今天巩固如下操作:(2023/08/14 早)
1 2 3 4 5 6 7 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.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 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
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<String> empty = Optional.empty();
1 2 Optional<String> hello = Optional.of("Hello" );
1 2 Optional<String> name = Optional.ofNullable("Hello" );
1 2 3 4 5 6 7 8 Optional<String> name1 = Optional.ofNullable("tom" );if (name.isPresent()) { System.out.println("Hello, " + name1.get()); } else { System.out.println("Name is not available" ); }
1 2 3 4 5 6 7 Optional<String> name2 = Optional.ofNullable("tom" ); name.ifPresent(s -> { System.out.println("Hello, " + name2.get()); });
1 2 3 4 5 6 Optional<String> name3 = Optional.ofNullable(null );String greeting = "Hello, " + name3.orElse("Guest" ); System.out.println(greeting);
1 2 3 4 5 Optional<String> name4 = Optional.ofNullable("null" );String greeting2 = "Hello, " + name4.orElseThrow(() -> new NullPointerException ("null" ));
1 2 3 4 5 6 Optional<String> name5 = Optional.ofNullable("tom" );String greeting3 = "Hello, " + name5.map(s -> s.toUpperCase()).get(); System.out.println(greeting3);
1 2 3 4 5 Optional<String> name6 = Optional.ofNullable("tom" );String greeting4 = name6.flatMap(s -> Optional.of("Hello " + s)).get(); System.out.println(greeting4);
1 2 3 4 5 Optional<String> name7 = Optional.ofNullable("tom" );String greeting5 = "Hello " + name7.filter(s -> !s.isEmpty()).get(); System.out.println(greeting5);
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 () { BigDecimal dec1 = new BigDecimal ("1.3" ); BigDecimal dec2 = new BigDecimal ("1.3" ); BigDecimal add = dec1.add(dec2); BigDecimal subtract = dec1.subtract(dec2); BigDecimal multiply = dec1.multiply(dec2); BigDecimal divide = dec1.divide(dec2).setScale(3 , RoundingMode.HALF_DOWN); out.println(add); out.println(subtract); out.println(multiply); System.out.println(divide); BigDecimal bigDecimal = divide.stripTrailingZeros(); out.println(bigDecimal); BigDecimal value = new BigDecimal ("1888977466432.1241341341413414" ); double doubleValue = value.doubleValue(); out.println(doubleValue); double doubleValue1 = value.toBigInteger().doubleValue(); out.println(doubleValue1); out.println(add.compareTo(subtract) > 0 ? "add大于sub" : "add小于sub" ); }
数组转字符串
1 2 3 4 List<String> list = Arrays.asList("Apple" , "Banana" , "Cherry" , "Date" , "Elderberry" ); String[] arr = {"0" , "1" , "2" , "3" , "4" , "5" };
如果是 List,则有如下转字符串的方法:(2023/09/20 晚)
1 2 String collect = list.stream().collect(Collectors.joining("," ));
1 2 String join = StringUtils.join(longList, "," );
1 2 3 4 5 6 7 8 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("," , list);
以上方法,转换所消耗的时间越来越少 ,效率越来越高
如果是 String[],则有如下转字符串的方法:(2023/09/20 晚)
1 2 String join = StringUtils.join(longList, "," );
1 2 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 ); map.computeIfAbsent("orange" , key -> { System.out.println("Performing computation for orange" ); return key.length(); }); 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 ); map.computeIfPresent("apple" , (key, value) -> value * 2 ); map.computeIfPresent("banana" , (key, value) -> value * 2 ); System.out.println(map); map.compute("apple" , (key, value) -> value + 3 ); map.compute("banana" , (key, value) -> value + 3 ); System.out.println(map); } }
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); } }
泛型理解
学习 Java 泛型:(2023/10/13 晚)
泛型类
泛型接口
泛型方法:返回值、入参位置、方法体中
个人感受:泛型很好理解:我们经常讲到一个对象实例的就是以类作为模板创建的,那么也可以讲一个普通类可以以泛型类作为模板;那么泛型是用来干嘛的呢,我们为什么要使用泛型呢?其实,所有的泛型类在编译后生成的字节码与普通类无异,因为在编译前,所有泛型类型就被擦除了。所以我们可以把泛型看作一个语法糖,将类型转换的校验提前在编译时,减少类型转换错误的发生,使编写的程序更加具有健壮性。
我觉得以下这段总结更妙:
泛型是 Java 语言中的一项强大的特性,它允许在编译时指定类、接口或方法的参数类型,从而在编译阶段就能够进行类型检查。这样可以减少类型转换的错误,并提高代码的安全性和可读性。
通过使用泛型,我们可以在编译时捕捉到一些类型错误,而不是在运行时才发现,这样可以避免一些潜在的 bug。泛型还可以增加代码的可重用性和灵活性,因为泛型类、接口和方法可以用于多种不同的类型,而无需针对每一种类型单独编写或重复编写相似的代码。
总的来说,通过使用泛型,我们可以在编写 Java 代码时更好地约束和使用类型信息,减少类型错误,提高代码的可读性和健壮性。
了解泛型的实现原理,理解泛型的使用方式,更加加深了我对 Java 语言的理解
JVM
🥣 推荐阅读:jvm 内存模型(运行时数据区)简介 - 知乎 (zhihu.com) (2023/12/13)
锦绣收官