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

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

以前总谈理想,现在只想睡个好觉。

破冰

🥇 推荐阅读:

🍖 反射:

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

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

☕ 代理:

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

🥣 注解:

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

🌮 语法糖:

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

🍛 集合:

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

思维碰撞

新特性

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日
更新于
2024年2月12日
许可协议