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

Java 22

1、增强的模式匹配

jdk22 对 instanceof进一步进行了增强,可以与模式匹配相结合,支持更复杂的条件判断。

在下面的例子中,instanceof 不仅判断了 obj 是否为字符串,还能直接赋值到 s 中进行使用。

public class Demo {

    static void main() {
        Object obj = "hello, world!";
        if (obj instanceof String s) {
            System.out.println("此对象为字符串,长度为:" + s.length());
        }
    }
}

同样的,在 switch的模式匹配中也做了增强,不仅能直接判断 obj 的类型,还能直接转换并使用。

public class Demo {

    static void main() {
        Object obj = 1;
        String result = switch (obj) {
            case Integer i -> "这是一个整数:" + i;
            case String s -> "这是一个字符串:" + s;
            default -> "未知";
        };
        System.out.println(result);
    }
}

2、虚拟线程增强

在 jdk22 中,对虚拟线程的调度进行了优化,能跟高效的利用 CPU,减少上下文开销。并在ExecutorService中新增 了一些方法,方便开发者操作。

在下面的例子中,我们使用ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()创建了一个虚拟线程池,接着从线程池中执行任务,最后关闭线程池再等待所有任务执行完毕。

public class Demo {

    static void main() {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10; i++) {
                executor.submit(() -> {
                    System.out.println("Hello, world!");
                });
            }
            executor.shutdown();
            executor.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3、Record 的增强

在 jdk22 中,Record 类允许开发者自定义 toStringhashCodeequals 方法。

public record Person(String name, int age) {

    @Override
    public String toString() {
        return "姓名:" + name + ", 年龄:" + age;
    }

    @Override
    public int hashCode() {
        return 31 * name.hashCode() + Integer.hashCode(age);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Person person)) {
            return false;
        }
        return age == person.age && name.equals(person.name);
    }

}

Java 23

1、隐式声明

简化了 main 方法的写法,也就是说无需再写public static

public class Demo {

    void main() {
        System.out.println("hello, world!");
    }
}

2、Markdown 文档注释

在文档注释中,可以使用 Markdown 语法进行编写注释了

public class Demo {

    void main() {
        int i1 = test1(1, 2);
        int i2 = test2(1, 2);
    }

    /**
     * 传统文档注释.
     * 计算两个数的和
     * @param a 第一个数
     * @param b 第二个数
     * @return 两个数的和
     */
    int test1(int a, int b) {
        return a + b;
    }
    
    /// Markdown文档注释.
    /// # 计算两个数的和
    /// 例如:`test2(1, 2) == 3`
    /// - a:第一个数
    /// - b:第二个数
    /// 计算两个数的和
    /// @param a 第一个数
    /// @param b 第二个数
    /// @return 两个数的和
    int test2(int a, int b) {
        return a + b;
    }
}

在 idea 上的显示效果:

Java 25

1、Scoped Values 作用域值

在传统线程中,如果想要在线程内共享数据,那么就需要用 ThreadLocal,但是ThreadLocal也是存在许多问题的:

  1. 容易内存泄露
  2. 可以随意修改数据

而且如果想让子线程也共享数据,那么就需要每个子线程都复制一份数据,如果线程太多,那么复制的数据也会很多,性能极差。

Scoped Values 出来后,就可以解决这个问题了。

Scoped Values可以允许方法在线程内以及子线程间安全高效的共享不可变数据

public class Demo {

    private static final ScopedValue<Integer> TASK_ID = ScopedValue.newInstance();

    void main() {
        task();
    }

    void task() {
        // 通过where 调用,run执行,自动管理作用域
        where(TASK_ID, 1).run(this::test);
    }

    void test() {
        // 获取作用域中的值
        Integer taskId = TASK_ID.get();
        System.out.println("Task ID: " + taskId);
    }

}

在虚拟线程中使用:

public class Demo {

    void main() throws InterruptedException {
        var thread = Thread.ofVirtual().start(() -> {
            ScopedValue<String> sv = ScopedValue.newInstance();
            String value = "value";
            where(sv, value).run(() -> {
                System.out.println("作用域的值:" + sv.get());
            });
        });
        thread.join();
    }

}

在子线程中共享,需要用到 StructuredTaskScope 共享给子线程:

public class Demo {

    void main() {
        ScopedValue<String> sv = ScopedValue.newInstance();
        String value = "value";

        ScopedValue.where(sv, value).run(() -> {
            // 仍在预览中,谨慎使用
            try (var scope = StructuredTaskScope.open()) {
                for (int i = 0; i < 100; i++) {
                    scope.fork(() -> {
                        System.out.println("作用域的值:" + sv.get());
                    });
                }
                scope.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }

}

也可以用支持返回值的:

public class Demo {

    void main() {
        System.out.println(test());
    }

    String test() {
        ScopedValue<String> sv = ScopedValue.newInstance();
        String value = "value";

        return ScopedValue.where(sv, value).call(() -> {
            return "作用域值:" + sv.get();
        });
    }

}

2、模块导入

在 jdk25 之前,我们导入包都需要每个都导入,这样就会导致有很多的导入语句,但引入模块导入后,我们可以直接导入一整个模块,这样就不需要那么多的导入语句了

// 原本需要导入这几个
import java.util.List;
import java.util.Map;
import java.util.Set;

// 模块导入后
import module java.base;

3、紧凑源文件和实例主方法

jdk25 中新增了一个 IO 类,用于提供更简单的控制台 IO 操作。

java.base 模块导入,可以直接使用 List,Map,Set,Stream 等常用的类。

void main() {
    IO.println("hello, world!");
    String name = IO.readln("请输入你的姓名");
    List.of();
    Set.of();
    Map.of();
    Stream.empty();
}