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

此外还有很多 api,包括一些获取年份月份等等

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 类的静态方法 readStringwriteString 来读取或写入字符串

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 密集型任务的时候使用虚拟线程才是最优的,计算密集型的不建议使用