Java8 于2014年3月14号发布,距现在已经有快十年的时间了,相信大部分企业或者个人都还在使用 Java8,不过自从 Java21 发布之后,里面的虚拟线程实在是太香了,我个人觉得这个算是一个类似 Java8 一样的革命性的版本,所以,是时候升级到 Java21 啦。
下面主要是介绍 Java 中几个重要的版本:8、11、17、21。
不过需要注意的是,从 Java8 升级的话,可能会有较多兼容性的问题,所以各位如果要升级的话请慎重。
Java8
1、lambda 表达式
lambda 表达式可谓是 Java8 中最重要的更新之一了,使用起来 也很简单,其格式为:
// 无参
() -> {
// todo
}
// 单个参数
(p) -> {
// todo
}
// 多个参数
(p1, p2) -> {
// todo
}
// 示例
List<Integer> list = Arrays.asList(1,2,3,4);
list.forEach(e -> {
System.out.println(e);
});
注意:
1.Lambda 表达式中使用的局部变量可以不声明为 final,但是不允许被修改,也就是说 Lambda 表达式中使用的变量是隐性 final 的,例如
int n = 1;
List<Integer> list = Arrays.asList(1,2,3,4);
list.forEach(e -> {
// 这里会报错,提示:lambda 表达式中使用的变量应为 final 或有效 final
// 如果真的要改变n的值的话可以把n赋给一个临时变量
System.out.println(e + n);
});
n = 2;
2.在 Lambda 表达式中声明的参数不能与局部变量同名
int n = 1;
List<Integer> list = Arrays.asList(1,2,3,4);
// 这里会报错:范围中已定义变量 'n'
list.forEach(n -> {
System.out.println(n);
});
2、函数式接口
函数式接口:有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,这样的接口可以隐式转换为 Lambda 表达式
一般我们在创建函数式接口时,都会加上@FunctionalInterface
注解,声明这个接口是函数式接口
例如:
@FunctionalInterface
public interface DemoService {
void demo(String msg);
}
在 Java8 之前,我们只能通过匿名内部类进行使用:
DemoService demoService = new DemoService() {
@Override
public void demo(String msg) {
System.out.println(msg);
}
};
demoService.demo("hello, world");
但在 Java8 中,只需要一个 Lambda 表达式就 OK 了
DemoService demoService = msg -> {
System.out.println(msg);
};
demoService.demo("hello, world");
3、方法引用
方法引用指的是通过::
来调用某个类的方法,下面我会用个例子来简要的讲述如何使用
这里我编写了一个 User 类,然后定义了 2 个属性,并且加上了上面 3 个注解,分别是 get 、set 和 toString 方法,还有有参和无参的构造方法。
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user")
public class User {
@TableField("username")
private String username;
@TableField("age")
private Integer age;
}
下面是两种比较常用的用法:
// 用法1:构造方法
User user = null;
User u = Optional.ofNullable(user).orElseGet(User::new);
System.out.println(u);
// 用法2:成员方法
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper();
wrapper.eq(User::getUsername, "zhangsan");
用法 1: 上面我首先是定义了一个空的 user,然后通过 Optional 的 orElseGet 方法,里面接收一个supplier(函数式接口),这个supplier 只有一个 get 方法,用于获取结果。所以第三行的的意思是判断 user 是否是 null,如果不是则直接返回 user,如果是则通过无参构造方法创建一个 User。
用法 2:这个在 mybatis-plus 中很常见,也就是引用了 User 类的 Get 方法,在这里的作用是拿到这个类的这个字段
4、默认方法和静态方法
这个特性就是允许在接口中定义默认方法和静态方法。
public interface DemoInterface {
default void defaultFun() {
System.out.println("默认方法");
}
static void staticFun() {
System.out.println("静态方法");
}
}
5、stream 流
stream 算是 Java8 最大的更新了,stream 流就是把数据放到一个管道里,类似于流水线那样,每个节点都做各自的功能,最后再将处理好的结果输出。
1、生成流的方式
Collection体系的集合可以使用默认方法stream()生成流
List<String> list = new ArrayList<>();
Stream<String> listStream = list.stream();
Set<String> set = new HashSet<>();
Stream<String> setStream = set.stream();
Map体系的集合间接的生成流
Map<String, Integer> map = new HashMap<>();
Stream<String> keyStream = map.keySet().stream();
Stream<Integer> valueStream = map.values().stream();
Stream<Map.Entry<String,Integer>> entryStream = map.entrySet.stream();
数组可以通过Stream接口的静态方法of(T ... values)生成流
String strArray = {"hello", "world", "java"};
Stream<String> str1 = Stream.of(strArray);
Stream<Integer> str2 = Stream.of(10, 20, 30);
2、stream流常用中间方法
方法 | 作用 |
---|---|
filter(Predicate predivate) | 用于对流中的数据进行过滤 |
limit(long maxSize) | 返回此流中的元素组成的流,截取前指定参数个数的数据 |
skip(long n) | 跳过指定参数个数的数据,返回由该流的剩余元素组成的流 |
concat(Stream a, Stream b) | 合并a和b两个流为一个流 |
distinict() | 返回由该流不同的元素(equals())组成的流 |
sorted() | 返回由此流的元素组成的流,根据自然顺序排序 |
sorted(Comparator comparator) | 返回由该元素组成的流,根据提供的Comparator进行排序 |
map(Function mapper) | 返回由给定函数应用于此流的元素的结果组成的流 |
mapToInt(ToIntFunction mapper) | 返回一个IntStream其中包含将给定函数应用于此流的元素的结果 |
3、stream流常用终结方法
方法 | 作用 |
---|---|
forEach(Consumer action) | 对此流的每个元素执行操作 |
count() | 返回此流中的元素数 |
4、stream流的收集方法
1.收集方法
- collect(Collector collector)
- 但是这个收集方法的参数是一个Collector接口
2.工具类Collector
- Collectors.toList():把元素收集到List集合
- Collectors.toSet():把元素收集到Set集合
- Collectors.toMap(Fuction keyMapper, Fuction valueMapper):把元素收集到Map集合
5、stream 使用例子
// 筛选出包含"a"的字符串
List<String> list = Arrays.asList("aaa", "abb", "ccc");
List<String> l = list.stream().filter(x -> x.indexOf("a") != -1).collect(Collectors.toList());
System.out.println(l);
6、Optional 类
在 java 编码中遇到最多的就是空指针了,经常要用 if else 来判断,显得代码很臃肿,但是有了 Optional 之后,一切都变得优雅起来了。
1、ofNullable(T value)
此方法接受value作为类型T的参数,以使用此值创建Optional实例。可以为空。
此方法返回具有指定类型的指定值的Optional类的实例。如果指定的值为null,则此方法返回Optional类的空实例。
Optional<Integer> op1 = Optional.ofNullable(9455);
System.out.println("Optional 1: " + op1);
Optional<Integer> op2 = Optional.ofNullable(null);
System.out.println("Optional 2: " + op2);
// 结果
// Optional 1: Optional[9455]
// Optional 2: Optional.empty
2、isPresent(Consumer<? super T> consumer)
判断Optional类中的值是否存在,若存在则进行相应操作
User user = new User();
user.setUsername("zhangsan");
Optional.ofNullable(user).ifPresent(x -> {
// 如果不为空,则执行相应操作
System.out.println(x.getUsername());
});
3、orElse(T value)
判断前面放入Optional的值是否存在,存在则返回前面的值,不存在则返回放入orElse的值。
User user = null;
User u = new User();
u.setUsername("zhangsan");
User result = Optional.ofNullable(user).orElse(u);
7、新日期时间 API
原本的时间 Date 存在很多缺陷,例如:
- 存在并发问题
- 存在时区问题
所以在 Java8 中重新引入了一套时间日期类,用来解决上面的问题
1、Clock
Clock clock = Clock.systemUTC();
System.out.println(clock.millis());
System.out.println(clock.instant());
// 结果
// 1703664052237
// 2023-12-27T08:00:52.237157700Z
2、时间日期
System.out.println(LocalDate.now());
System.out.println(LocalTime.now());
System.out.println(LocalDateTime.now());
// 结果
// 2023-12-27
// 16:04:02.933348700
// 2023-12-27T16:04:02.933348700
Java 11
1、字符串 API 增强
// 用来判断字符串是不是""空字符或者" "字符
String s1 = " ";
System.out.println(s1.isBlank());
// 将字符串按换行符或者回车符进行分割,并转换为Stream流
String s2 = "hello\nworld\rand java.";
Stream<String> lines = s2.lines();
lines.forEach(System.out::println);
// 按照给定的次数重复字符串
String s3 = "hello.";
System.out.println(s3.repeat(3));
// 输出如下
// true
// hello
// world
// and java.
// hello.hello.hello.
2、集合转数组
以前把集合转成数组会比较麻烦,而现在只需要一行代码即可
List<String> list = Arrays.asList("1", "2");
String[] array = list.toArray(String[]::new);
for (String s : array) {
System.out.println(s);
}
3、var 关键字
java10 中加入了 var 关键字,它的功能是能根据右边的值推断出该变量的类型,这样我们可以直接通过 var 声明变量而不需要指定类型。
不过需要注意的是:
- 只能用在局部变量上
- 声明的值必须初始化
- 不能用作方法参数
- 不要用在数值上
var user = new User();
user.setUsername("张三");
user.setAge(21);
System.out.println(user);
在 Java11 中,对 var 关键字进行了加强,可以在 Lambda 表达式中的入参使用
List<String> list = Arrays.asList("aaa", "abb", "ccc");
List<String> collect = list
.stream()
.filter((var x) -> x.indexOf("a") != -1)
.collect(Collectors.toList());
System.out.println(collect);
4、文件中写入读取字符串
在 Java11 中可以通过 Files
类的静态方法 readString
和 writeString
来读取或写入字符串
String path = "E:\\symx";
// 写入字符串
Path test = Files.writeString(Files.createTempFile(Path.of(path), "test", ".txt"), "hello,world");
// 读取字符串
String s = Files.readString(test);
System.out.println(s);
5、新的 HttpClient
在 Java11 中,我们可以直接用 HttpClient 来进行 URL 的访问了,并且支持 HTTP2
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder(new URI("https://www.baidu.com")).GET().build();
HttpResponse<String> res = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
Java 17
1、Switch 增强
原本的 Switch 语句如果相同的情况太多也需要重复的写 case
LocalDate currentDate = LocalDate.now();
int value = currentDate.getDayOfWeek().getValue();
String status = "";
switch (value) {
case 1:
case 2:
case 3:
case 4:
case 5:
status = "工作日";
break;
case 6:
case 7:
status = "周末";
break;
default:
status = "未知";
}
System.out.println(status);
增强后与可以合并为一起,并且可以结合 lambda 表达式使用
LocalDate currentDate = LocalDate.now();
int value = currentDate.getDayOfWeek().getValue();
String status = switch (value) {
case 1, 2, 3, 4, 5 -> "工作日";
case 6, 7 -> "周末";
default -> "未知";
};
System.out.println(status);
更为强悍的是,我们可以写更复杂的逻辑了,最后再将结果返回
LocalDate currentDate = LocalDate.now();
int value = currentDate.getDayOfWeek().getValue();
String status = switch (value) {
case 1, 2, 3, 4, 5 -> {
System.out.println("要适当摸鱼哦");
yield "工作日";
}
case 6, 7 -> {
System.out.println("愉快的周末");
yield "周末";
}
default -> {
System.out.println("所以什么情况下会执行到这里呢");
yield "未知";
}
};
System.out.println(status);
2、文本块
之前我们在写一些需要换行的字符串时,都是通过+号进行连接的,这样看起来会很困难
String s = "SELECT\n" +
"\t*\n" +
"FROM\n" +
"\tuser";
有了多行文本块之后,再看就一目了然了
String s = """
SELECT
*
FROM
user
""";
3、record 关键字
通过 record 关键字声明的类,可以不需要编写构造器,属性,toString,equal,hashCode 方法,但是这样就没有了 set 方法,有一定的局限性。
// 声明
public record Dept(int id, String name) {
}
// 使用
Dept dept = new Dept(1, "总部");
System.out.println(dept.id());
System.out.println(dept.name());
System.out.println(dept);
4、随机数
Java17 中 增加了 RandomGenerator
接口,给所有的 伪随机数生成器(PRNG)提供了统一的 API。也提供了一个新类 RandomGeneratorFactory
用于构造各种 RandomGenerator
实例。
查看所有 PRNG 算法:
RandomGeneratorFactory.all().forEach(factory -> {
System.out.println(factory.group() + ": " + factory.name());
});
// 结果,可以看到,最基本的Random也在这里
// LXM: L32X64MixRandom
// LXM: L128X128MixRandom
// LXM: L64X128MixRandom
// Legacy: SecureRandom
// LXM: L128X1024MixRandom
// LXM: L64X128StarStarRandom
// Xoshiro: Xoshiro256PlusPlus
// LXM: L64X256MixRandom
// Legacy: Random
// Xoroshiro: Xoroshiro128PlusPlus
// LXM: L128X256MixRandom
// Legacy: SplittableRandom
// LXM: L64X1024MixRandom
生成随机数:
// 使用默认算法生成随机数
RandomGeneratorFactory<RandomGenerator> defaultGenerator = RandomGeneratorFactory.getDefault();
for (int i = 0; i < 5; i++) {
System.out.println(defaultGenerator.create().nextInt());
}
// 使用 L64X256MixRandom 算法
RandomGeneratorFactory<RandomGenerator> l64X256MixGenerator = RandomGeneratorFactory.of("L64X256MixRandom");
for (int i = 0; i < 5; i++) {
// 以当前时间戳作为随机数种子
System.out.println(l64X256MixGenerator.create(Clock.systemUTC().millis()).nextInt());
}
Java21
1、默认字符集为 UTF-8
JDK 默认是支持 UTF8 的,但是并不是默认的,而在 Java18 中,将默认字符集改成了 UTF8,这样就可以避免不通系统、环境和地区之间的差异导致的编码问题。
// 查看默认字符集
System.out.println(Charset.defaultCharset());
// Java17及之前
// GBK
// Java18之后
// UTF-8
2、字符串模板
Java17 中增加了个文本块功能,而 Java21 则是增加了字符串模板,目前仍属于预览版,需要等之后发布为正式版
其实就是类似于 js 中的模板字面量一样,下面的 STR
即为字符串模板
// 简单使用
String blog = "https://bk.gggd.club";
String s = STR."我的博客是:\{blog}";
System.out.println(s);
// 也支持文本块
String where = "where deleted = 0";
String sql = STR."""
SELECT
*
FROM
user
\{where}
""";
System.out.println(sql);
3、Switch 模式匹配
Java18,19,20 一直对 Switch 进行了预览版的升级,直到 21,终于正式发布了
// 根据传入的类型进行匹配
Object o = 1;
String s = switch (o) {
case Integer i -> String.format("int %d", i);
case Double d -> String.format("double %d", d);
case Long l -> String.format("long %d", l);
default -> o.toString();
};
System.out.println(s);
// 可以传入null
String o = null;
switch (o) {
case null -> System.out.println("未知");
case "猫", "狗" -> System.out.println("动物");
default -> System.out.println("生物");
};
4、虚拟线程
在认识虚拟线程之前,我们要知道传统的线程是叫做平台线程的,是运行在底层 OS 线程上的,并且在代码的整个生命周期中独占该 OS 线程,所以平台线程的多少取决于 OS 线程的数量,因此,平台线程的资源是宝贵的。
而虚拟线程,只是 Thread 的一个实例,虽然也是在 OS 线程上运行代码,但不会一直占用,所以,多个虚拟线程可以在同一个 OS 线程上运行代码,由此可以看出,虚拟线程的数量可以比平台线程要多得多。
但是也不是说虚拟线程就完胜平台线程,虚拟线程只是在最大化的利用 CPU 资源,也就是说,原本执行一个任务 CPU 只占用了 10%,但是使用虚拟线程后,我们可以让 CPU 占用达到 90%,那效率自然就变高了。所以,虚拟线程主要是用来处理一些 IO 密集型的任务,例如文件 IO,网络 IO 等,而对于一些计算型的任务,虚拟线程则没有什么优势。
使用:
// 1、直接启动
Thread.ofVirtual().start(() -> {
System.out.println("hello, world:1");
});
// 2、修改名字启动
Thread.ofVirtual().name("虚拟线程").start(() -> {
System.out.println("hello, world:2");
});
// 3、直接启动
Thread.startVirtualThread(() -> {
System.out.println("hello, world:3");
});
// 4、创建一个未启动的,后手动启动
Thread unstarted = Thread.ofVirtual().unstarted(() -> {
System.out.println("hello, world:4");
});
unstarted.start();
// 5、创建线程工厂启动
ThreadFactory factory = Thread.ofVirtual().factory();
Thread t = factory.newThread(() -> {
System.out.println("hello, world:5");
});
t.start();
// 6、线程池启动
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
executorService.submit(() -> {
System.out.println("hello, world:6");
});
我们可以看到虚拟线程的创建跟平台线程的很像,所以我们可以很简单的从平台线程迁移到虚拟线程。
注意:
- 虚拟线程是非常轻量级的资源,所以我们只需要在使用时创建,用完就扔掉,不需要通过线程池创建。
- 只有以虚拟线程方式运行的代码,才会在执行IO操作时自动被挂起并切换到其他虚拟线程。普通线程的IO操作仍然会等待
- 只有在 IO 密集型任务的时候使用虚拟线程才是最优的,计算密集型的不建议使用