zjjfly's blog

Java,Clojure,Scala...

0%

从Java18到21

从Java18到21

随着Java最新版本21的推出,其中的新特性之一:虚拟线程,受到广泛关注。笔者觉得有必要对从Java18到21的所有主要的新特性做一个盘点,就像之前的《学习Java9~11》《学习Java12~17》

虚拟线程

毫无疑问,虚拟线程是所有的新特性中最重要的一个。在此之前,Java中的线程实际是对操作系统的线程的包装,而操作系统的线程的创建代价比较高昂,如果你在线程中发送HTTP请求到另一个服务器,其中发送请求和处理响应只占线程阻塞的时间的一小部分,大部分时间是在等待响应。避免这种情况的一个方式是使用异步方式,但这种方式的缺点是实现较复杂。而有了虚拟线程,你能够在不改变实现的情况下获得和异步一样的伸缩性。

创建虚拟线程的方式:

1
Thread.startVirtualThread(() -> System.out.println("foo"));

或者:

1
Thread.ofVirtual().start(() -> System.out.println("bar"));

但一般不会直接这样创建线程,而是使用ExecutorService或者ThreadFactory来管理线程。

1
2
3
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()

ThreadFactory threadFactory = Thread.ofVirtual().name("my-thread", 0).factory();

顺序集合

序列集合填补了Java中缺少的表示具有确定顺序的元素序列的集合类型。除此之外,没有一个统一的应用于这样的集合的操作集。社区对此抱怨已久,现在通过引入一些新的集合接口解决了这个问题。可以参考下面的图,其中的绿色接口就是新加入的:

sequencedcollections

初次之外,SequencedSet,SequencedCollectionSequencedMap都有对应的不可变版本:

1
2
3
Collections.unmodifiableSequencedCollection(sequencedCollection)
Collections.unmodifiableSequencedSet(sequencedSet)
Collections.unmodifiableSequencedMap(sequencedMap)

SequencedCollection

顺序集合是一个其元素有预定义顺序的集合。新的接口SequencedCollection是这样的:

1
2
3
4
5
6
7
8
9
10
11
interface SequencedCollection<E> extends Collection<E> {
// new method
SequencedCollection<E> reversed();
// methods promoted from Deque
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}

下面是使用这些方法的一些代码片段:

1
2
3
4
5
6
7
8
List<String> sc = Stream.of("Alpha", "Bravo", "Charlie", "Delta").collect(Collectors.toCollection(ArrayList::new));
System.out.println("Initial list: " + sc);
System.out.println("Reversed list: " + sc.reversed());
System.out.println("First item: " + sc.getFirst());
System.out.println("Last item: " + sc.getLast());
sc.addFirst("Before Alpha");
sc.addLast("After Delta");
System.out.println("Added new first and last item: " + sc);

输出是:

Initial list: [Alpha, Bravo, Charlie, Delta]
Reversed list: [Delta, Charlie, Bravo, Alpha]
First item: Alpha
Last item: Delta
Added new first and last item: [Before Alpha, Alpha, Bravo, Charlie, Delta, After Delta]

SequencedSet

顺序集是一个实现了SequencedCollection的集。新的接口是这样的:

1
2
3
interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
SequencedSet<E> reversed(); // covariant override
}

下面是使用其实现TreeSet的一些代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SortedSet<String> sortedSet = new TreeSet<>(Set.of("Charlie", "Alpha", "Delta", "Bravo"));
System.out.println("Initial list: " + sortedSet);
System.out.println("Reversed list: " + sortedSet.reversed());
System.out.println("First item: " + sortedSet.getFirst());
System.out.println("Last item: " + sortedSet.getLast());
try {
sortedSet.addFirst("Before Alpha");
} catch (UnsupportedOperationException uoe) {
System.out.println("addFirst is not supported");
}
try {
sortedSet.addLast("After Delta");
} catch (UnsupportedOperationException uoe) {
System.out.println("addLast is not supported");
}

Initial list: [Alpha, Bravo, Charlie, Delta]
Reversed list: [Delta, Charlie, Bravo, Alpha]
First item: Alpha
Last item: Delta
addFirst is not supported
addLast is not supported

SequencedCollection的区别是,初始化的时候放入的元素是按照字母顺序排列的,但不支持addFirstaddLast方法。原因是因为TreeSet是根据其比较器来决定元素的顺序的。所以你无法保证你放入的元素必定会排在第一个或最后一个。但如LinkedHashSet类就支持这两个方法,这只是具体实现的区别。

SequencedMap

顺序Map是一个其元素有预定义顺序的Map。新的接口SequencedMap是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface SequencedMap<K,V> extends Map<K,V> {
// new methods
SequencedMap<K,V> reversed();
SequencedSet<K> sequencedKeySet();
SequencedCollection<V> sequencedValues();
SequencedSet<Entry<K,V>> sequencedEntrySet();
V putFirst(K, V);
V putLast(K, V);
// methods promoted from NavigableMap
Entry<K, V> firstEntry();
Entry<K, V> lastEntry();
Entry<K, V> pollFirstEntry();
Entry<K, V> pollLastEntry();
}

下面是其实现LinkedHashMap的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LinkedHashMap<Integer,String> hm = new LinkedHashMap<Integer,String>();
hm.put(1, "Alpha");
hm.put(2, "Bravo");
hm.put(3, "Charlie");
hm.put(4, "Delta");
System.out.println("== Initial Map ==");
printMap(hm);
System.out.println("== Reversed Map ==");
printMap(hm.reversed());
System.out.println("First item: " + hm.firstEntry());
System.out.println("Last item: " + hm.lastEntry());
System.out.println(" == Added new first and last item ==");
hm.putFirst(5, "Before Alpha");
hm.putLast(3, "After Delta");
printMap(hm);

输出:

== Initial Map ==
1 Alpha
2 Bravo
3 Charlie
4 Delta
== Reversed Map ==
4 Delta
3 Charlie
2 Bravo
1 Alpha
First item: 1=Alpha
Last item: 4=Delta
== Added new first and last item ==
5 Before Alpha
1 Alpha
2 Bravo
4 Delta
3 After Delta

Record的模式匹配

Java在之前版本中加入的record类型,在Java 21中加入了对record的模式匹配,实现了对record值的结构。这使得对record中的字段的访问更容易。

以下面的record类作为例子:

1
2
record GrapeRecord(Color color, Integer nbrOfPits) {
}

模式匹配的用法:

1
2
3
4
Object o = new GrapeRecord(Color.BLUE, 2);
if (o instanceof GrapeRecord(Color color, Integer nbrOfPits)) {
System.out.printf("This grape has %d pits.%n", nbrOfPits);
}

这种模式匹配是可以嵌套的:

1
2
3
4
5
6
7
Object o = new SpecialGrapeRecord(new GrapeRecord(Color.BLUE, 2), true);
if (o instanceof SpecialGrapeRecord(GrapeRecord grape, boolean special)) {
System.out.println("This grape has " + grape.nbrOfPits() + " pits.");
}
if (o instanceof SpecialGrapeRecord(GrapeRecord(Color color, Integer nbrOfPits), boolean special)) {
System.out.println("This grape has " + nbrOfPits + " pits.");
}

Swith的模式匹配

Switch的模式匹配在之前的版本中已作为预览的特性存在了一段时间,在Java 21中,这一特性正式发布。下面是几个使用场景。

类型匹配

1
2
3
4
5
6
7
8
private static void patternMatchingSwitch(Object obj) {
switch(obj) {
case Integer i -> System.out.println("Object is an integer:" + i);
case String s -> System.out.println("Object is a string:" + s);
case Point p -> System.out.println("Object is a point: " + p);
default -> System.out.println("Object is not recognized");
}
}

匹配null

在之前,传入switch的表达式如果是null,会抛出NullPointerException,现在不会在抛出这个异常了。

1
2
3
4
5
6
7
8
9
private static void patternMatchingSwitch(Object obj) {
switch(obj) {
case Integer i -> System.out.println("Object is an integer:" + i);
case String s -> System.out.println("Object is a string:" + s);
case Point p -> System.out.println("Object is a point: " + p);
case null -> System.out.println("Object is null");
default -> System.out.println("Object is not recognized");
}
}

case细化

现在可以在case中加入进一步的判断语句来细化想要匹配的目标,使用when关键字,称为guard

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void patternMatchingSwitch(Object obj) {
switch (obj) {
case String s -> System.out.println("Object is a string:" + s);
case Color c when (c == Color.BLACK) -> {
System.out.println("Object is an apple");
}
case Color c when (c == Color.WHITE) -> {
System.out.println("Object is an avocado");
}
case null -> System.out.println("Object is null");
default -> System.out.println("Object is not recognized");
}
}

枚举常量

枚举类型可以在switch中使用,但只限于某个枚举的常量,现在可以支持多个枚举类型的常量。

1
2
3
4
5
6
7
8
9
10
11
12
private static void patternMatchingSwitch(Object obj) {
switch (obj) {
case String s -> System.out.println("Object is a string:" + s);
case FruitType.APPLE -> System.out.println("Object is an apple");
case FruitType.AVOCADO -> System.out.println("Object is an avocado");
case FruitType.PEAR -> System.out.println("Object is a pear");
case FruitType.ORANGE -> System.out.println("Object is an orange");
case CarType.CABRIO -> System.out.println("Object is a cabrio");
case null -> System.out.println("Object is null");
default -> System.out.println("Object is not recognized");
}
}

简易Web服务器

它是一个最小化的HTTP服务器,可以基于一个文件夹(以及其子文件夹)提供服务。要使用它也很简单,首先在当前文件夹中新建一个文件index.html

1
Welcome to Simple Web Server

使用代码启动服务器的例子:

1
2
3
4
5
6
public static void main(String[] args) {
var server = SimpleFileServer.createFileServer(new InetSocketAddress(8080),
Path.of("./").toAbsolutePath(),
SimpleFileServer.OutputLevel.VERBOSE);
server.start();
}

验证输出:

$ curl http://localhost:8080

Welcome to Simple Web Server

你可以修改文件的内容,这可以立即生效,只要刷新一下网页。还可以实现HttpHandler来自定义对特定请求的响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyHttpHandler implements com.sun.net.httpserver.HttpHandler {

@Override
public void handle(HttpExchange exchange) throws IOException {
if ("GET".equals(exchange.getRequestMethod())) {
OutputStream outputStream = exchange.getResponseBody();
String response = "It works!";
exchange.sendResponseHeaders(200, response.length());
outputStream.write(response.getBytes());
outputStream.flush();
outputStream.close();
}
}
}

这个处理器对所有GET请求返回字符串It works!。使用这个处理器,并为其添加一个context path。

1
2
3
4
5
6
7
try {
var server = HttpServer.create(new InetSocketAddress(8081), 0);
server.createContext("/custom", new MyHttpHandler());
server.start();
} catch (IOException ioe) {
System.out.println("IOException occured");
}

$ curl http://localhost:8081/custom
It works!

还可以使用jwebserver来启动服务器,具体用法可以使用jwebserver --help查看。