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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐