2024年3月

Kafka简介

Apache Kafka是一个开源的分布式流处理平台,由LinkedIn公司开发和维护,后来捐赠给了Apache软件基金会。Kafka主要用于构建实时数据管道和流应用。它类似于一个分布式、高吞吐量的发布-订阅消息系统,可以处理消费者网站的所有动作流数据。这种动作流数据包括页面浏览、搜索和其他用户的行动。通过这些数据,Kafka能够实时地将数据流传输到系统和应用上。
Kafka的主要特性包括:

  1. 高吞吐量:Kafka以高吞吐量处理数据,即使是非常大量的数据也能轻松应对。
  2. 分布式:Kafka是分布式的,可以在多个节点上运行,从而实现高可用性和容错性。
  3. 持久性:Kafka将数据持久化到磁盘,因此即使系统崩溃,数据也不会丢失。
  4. 实时性:Kafka可以实时处理数据,为实时分析、监控和报警等应用提供了强大的支持。

Ubuntu安装Kafka

本文是在wsl2上的Ubuntu 22.04上安装Kafka。你需要先安装Java环境,因为Kafka是用Java编写的。然后,你可以从Apache Kafka的官方网站下载并安装Kafka。以下是安装步骤:

  1. 安装Java环境:你可以使用apt-get命令安装OpenJDK。

     sudo apt-get update  
     sudo apt-get install openjdk-17-jdk
    
  2. 下载Kafka:从Apache Kafka的官方网站下载适合你操作系统的版本。下载完成后,解压到指定目录。

       wget https://mirrors.aliyun.com/apache/kafka/3.7.0/kafka_2.13-3.7.0.tgz
       tar -xzf kafka_2.13-3.7.0.tgz  
       mv kafka_2.13-3.7.0 kafka
       cd kafka
    

3.启动Kafka:Kafka依赖于ZooKeeper,所以你需要先启动ZooKeeper,然后再启动Kafka。

 # 启动ZooKeeper  
 bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
 # 启动Kafka  
 nohup bin/kafka-server-start.sh config/server.properties 2>&1 &

4.使用kafka客户端连接

Spring Boot集成Kafka

  1. 添加依赖:在你的Spring Boot项目的build.gradle 文件中添加Kafka的依赖。

    dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-web'
     compileOnly 'org.projectlombok:lombok'
     annotationProcessor 'org.projectlombok:lombok'
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     runtimeOnly 'mysql:mysql-connector-java:8.0.17'
     implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
     implementation 'org.springframework.boot:spring-boot-starter-data-redis'
     implementation 'org.apache.commons:commons-pool2'
     implementation 'org.springframework.kafka:spring-kafka'
    }
    
  2. 配置Kafka:在application.properties或application.yml文件中配置Kafka的相关属性,如broker地址、端口、topic等。

    spring.kafka.bootstrap-servers=localhost:9092  
    spring.kafka.consumer.group-id=my-group  
    spring.kafka.consumer.auto-offset-reset=earliest  
    spring.kafka.template.default-topic=my-topic
    
  3. 创建生产者:使用KafkaTemplate发送消息到Kafka。

     package cn.daimajiangxin.springboot.learning.kafka;
    
     import jakarta.annotation.Resource;
     import org.springframework.kafka.core.KafkaTemplate;
     import org.springframework.stereotype.Service;
    
     @Service
     public class KafkaProducer {
         @Resource
         private KafkaTemplate<String, String> kafkaTemplate;
    
         public void sendMessage(String message) {
             kafkaTemplate.send("my-topic", message);
         }
     }
    
  4. 创建消费者:使用@KafkaListener注解监听Kafka中的消息。

    package cn.daimajiangxin.springboot.learning.kafka;
    
    import org.springframework.kafka.annotation.KafkaListener;
    import org.springframework.stereotype.Service;
    
    @Service
    public class KafkaConsumer {
        @KafkaListener(topics = "my-topic", groupId = "my-group")
        public void consume(String message) {
            System.out.println("Received message: " + message);
        }
    }
    
  5. 创建控制器:KafkaController

     package cn.daimajiangxin.springboot.learning.controller;
     import cn.daimajiangxin.springboot.learning.kafka.KafkaProducer;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.RestController;
    
     @RestController
         public class KafkaController {
    
         private final  KafkaProducer kafkaProducer;
    
             @Autowired
             public KafkaController(KafkaProducer kafkaProducer) {
                 this.kafkaProducer = kafkaProducer;
             }
    
             @GetMapping("/kafka")
             public void kafka() {
                 kafkaProducer.sendMessage("Hello World");
             }
         }
    
    

现在,你的Spring Boot应用已经集成了Kafka,你可以通过生产者发送消息,并通过消费者接收并处理这些消息了。
20240321131109
20240321131208

总结

以上就是关于从零开始学Spring Boot系列文章——集成Kafka的简介。Kafka作为一个强大的分布式流处理平台,与Spring Boot的集成可以极大地简化实时数据处理应用的开发。希望这篇文章能帮助你更好地理解Kafka及其在Spring Boot项目中的应用。


我是代码匠心,和我一起学习更多精彩知识!!!扫描二维码!关注我,实时获取推送。
公众号
源文来自:
https://daimajiangxin.cn


数据质量分析是数据预处理的
前提
,也是数据分析结论
有效性

准确性
的基础。
数据质量分析的主要任务是检查原始数据中是否存在脏数据。
脏数据一般包括;

缺失值分析

缺失值产生的原因、影响

原因:

  • 部分信息难以获取,或获取的代价太大
  • 因人为因素遗漏的、忘记写的、对数据理解错误的等
  • 属性值不存在

影响:

image.png

缺失值分析

常用函数:

# 结果返回TRUE,FALSE,TRUE为缺失值,FALSE为非缺失值
is.na()
# 结果返回TRUE,FALSE,FALSE为缺失值,TRUE为非缺失值
complete.cases()
# 形成矩阵或数据框式的表格,1和0显示缺失值模式,0表示变量的列中有缺失值,1表示没有
mice包中的md.pattern()
# 图形绘制每个变量的缺失值数,还绘制每个变量组合的缺失值数
aggr()

异常值分析

含义与方法

异常分析式检验数据是否有录入错误以及含有不合常理的数据,异常值也称作离群点。
异常值的分析方法主要有:

  • 简单统计量分析
  • 3
    \(\sigma\)
    原则
  • 箱型图分析

质量控制图

qcc包是专业的绘制质量控制图的算法包

# 该函数的基础形式如下
qcc(data,type,nsigmas=3,plot=TRUE,...)
# 以xbar为例绘图图形
library(qcc)
data(pistonrings)
attach(pistonrings)
newdata<-rep(74,30)+rnorm(30)/50
qcc(pistonrings$diameter,type="xbar.one")

Rplot.png

箱型图分析

image.png
绘制一个简单的箱型图:


library(ggplot2)

# 创建示例数据
set.seed(123)  # 设置随机种子以便结果可复现
data <- data.frame(
  group = rep(c("A", "B", "C"), each = 200),  # 创建三个组
  value = c(rnorm(200, mean = 0, sd = 1),  # 为组A生成正态分布的随机数
            rnorm(200, mean = 3, sd = 2),  # 为组B生成正态分布的随机数
            rnorm(200, mean = 6, sd = 1.5)) # 为组C生成正态分布的随机数
)

# 查看数据的前几行
head(data)
# 绘制箱型图
p <- ggplot(data, aes(x = group, y = value)) +  # 设置数据和映射
  geom_boxplot(fill = "lightblue") +  # 添加箱型图图层,并设置填充颜色
  theme_minimal() +  # 使用简洁的主题
  labs(title = "箱型图示例",  # 添加标题
       x = "组别",  # x轴标签
       y = "值") +  # y轴标签
  scale_fill_brewer(palette = "Pastel1")  # 使用Brewer配色方案

# 显示图形
print(p)

Rplot011.png

Tcp是一个面向连接的流数据传输协议,用人话说就是传输是一个已经建立好连接的管道,数据都在管道里像流水一样流淌到对端。那么数据必然存在几个问题,比如数据如何持续的读取,数据包的边界等。

Nagle's算法

Nagle 算法的核心思想是,在一个 TCP 连接上,最多只能有一个未被确认的小数据包(小于 MSS,即最大报文段大小)
优势
减少网络拥塞:通过合并小数据包,减少了网络中的数据包数量,降低了拥塞的可能性。
提高网络效率:在低速网络中,Nagle 算法可以显著提高传输效率。
劣势
增加延迟:在交互式应用中,Nagle 算法可能导致显著的延迟,因为它等待 ACK 或合并数据包。
C#中如何配置?

 var _socket = new Socket(IPAddress.Any.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_serverSocket.NoDelay = _options.NoDelay;

连接超时

在调用客户端Socket连接服务器的时候,可以设置连接超时机制,具体可以传入一个任务的取消令牌,并且设置超时时间。

CancellationTokenSource connectTokenSource = new CancellationTokenSource();
connectTokenSource.CancelAfter(3000); //3秒
await _socket.ConnectAsync(RemoteEndPoint, connectTokenSource.Token);

SSL加密传输

TCP使用SSL加密传输,通过非对称加密的方式,利用证书,保证双方使用了安全的密钥加密了报文。
在C#中如何配置?

服务端配置
//创建证书对象
var _certificate  = _certificate = new X509Certificate2(_options.PfxCertFilename, _options.PfxPassword);

//与客户端进行验证
if (allowingUntrustedSSLCertificate) //是否允许不受信任的证书
{
    SslStream = new SslStream(NetworkStream, false,
        (obj, certificate, chain, error) => true);
}
else
{
    SslStream = new SslStream(NetworkStream, false);
}

try
{
    //serverCertificate:用于对服务器进行身份验证的 X509Certificate
    //clientCertificateRequired:一个 Boolean 值,指定客户端是否必须为身份验证提供证书
    //checkCertificateRevocation:一个 Boolean 值,指定在身份验证过程中是否检查证书吊销列表
    await SslStream.AuthenticateAsServerAsync(new SslServerAuthenticationOptions()
    {
        ServerCertificate = x509Certificate,
        ClientCertificateRequired = mutuallyAuthenticate,
        CertificateRevocationCheckMode = checkCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck
    }, cancellationToken).ConfigureAwait(false);

    if (!SslStream.IsEncrypted || !SslStream.IsAuthenticated)
    {
        return false;
    }

    if (mutuallyAuthenticate && !SslStream.IsMutuallyAuthenticated)
    {
        return false;
    }
}
catch (Exception)
{
    throw;
}

//完成验证后,通过SslStream传输数据
int readCount = await SslStream.ReadAsync(buffer, _lifecycleTokenSource.Token)
    .ConfigureAwait(false);
客户端配置
var _certificate = new X509Certificate2(_options.PfxCertFilename, _options.PfxPassword);

if (_options.IsSsl) //如果使用ssl加密传输
{
    if (_options.AllowingUntrustedSSLCertificate)//是否允许不受信任的证书
    {
        _sslStream = new SslStream(_networkStream, false,
                (obj, certificate, chain, error) => true);
    }
    else
    {
        _sslStream = new SslStream(_networkStream, false);
    }

    _sslStream.ReadTimeout = _options.ReadTimeout;
    _sslStream.WriteTimeout = _options.WriteTimeout;
    await _sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions()
    {
        TargetHost = RemoteEndPoint.Address.ToString(),
        EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12,
        CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
        ClientCertificates = new X509CertificateCollection() { _certificate }
    }, connectTokenSource.Token).ConfigureAwait(false);

    if (!_sslStream.IsEncrypted || !_sslStream.IsAuthenticated ||
        (_options.MutuallyAuthenticate && !_sslStream.IsMutuallyAuthenticated))
    {
        throw new InvalidOperationException("SSL authenticated faild!");
    }
}

KeepAlive

keepAlive不是TCP协议中的,而是各个操作系统本身实现的功能,主要是防止一些Socket突然断开后没有被感知到,导致一直浪费资源的情况。
其基本原理是在此机制开启时,当长连接无数据交互一定时间间隔时,连接的一方会向对方发送保活探测包,如连接仍正常,对方将对此确认回应

C#中如何调用操作系统的KeepAlive?

/// <summary>
/// 开启Socket的KeepAlive
/// 设置tcp协议的一些KeepAlive参数
/// </summary>
/// <param name="socket"></param>
/// <param name="tcpKeepAliveInterval">没有接收到对方确认,继续发送KeepAlive的发送频率</param>
/// <param name="tcpKeepAliveTime">KeepAlive的空闲时长,或者说每次正常发送心跳的周期</param>
/// <param name="tcpKeepAliveRetryCount">KeepAlive之后设置最大允许发送保活探测包的次数,到达此次数后直接放弃尝试,并关闭连接</param>
internal static void SetKeepAlive(this Socket socket, int tcpKeepAliveInterval, int tcpKeepAliveTime, int tcpKeepAliveRetryCount)
{
    socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
    socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, tcpKeepAliveInterval);
    socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, tcpKeepAliveTime);
    socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, tcpKeepAliveRetryCount);
}

具体的开启,还需要看操作系统的版本以及不同操作系统的支持。

粘包断包处理

Pipe & ReadOnlySequence

image
上图来自微软官方博客:
https://devblogs.microsoft.com/dotnet/system-io-pipelines-high-performance-io-in-net/

TCP面向应用是流式数据传输,所以接收端接到的数据是像流水一样从管道中传来,
每次取到的数据取决于应用设置的缓冲区大小,以及套接字本身缓冲区待读取字节数

C#中提供的Pipe就如上图一样,是一个管道
Pipe有两个对象成员,一个是PipeWriter,一个是PipeReader,可以理解为一个是生产者,专门往管道里灌输数据流,即字节流,一个是消费者,专门从管道里获取字节流进行处理。
可以看到Pipe中的数据包是用链表关联的,但是这个数据包是从Socke缓冲区每次取到的数据包,它不一定是一个完整的数据包,所以这些数据包连接起来后形成了一个C#提供的另外一个抽象的对象
ReadOnlySequence

但是这里还是没有提供太好的处理断包和粘包的办法,因为断包粘包的处理需要两方面
1.业务数据包的定义
2.数据流切割出一个个完整的数据包

假设业务已经定义好了数据包,那么我们如何从Pipe中这些数据包根据业务定义来从不同的数据包中切割出一个完整的包,那么就需要ReadOnlySequence,它提供的操作方法,非常方便我们去切割数据,主要是头尾数据包的切割。

假设我们业务层定义了一个数据包结构,数据包是不定长的,包体长度每次都写在包头里,我们来实现一个数据包过滤器。

//收到消息
 while (!_receiveDataTokenSource.Token.IsCancellationRequested)
 {
     try
     {
        //从pipe中获取缓冲区
         Memory<byte> buffer = _pipeWriter.GetMemory(_options.BufferSize);
         int readCount = 0;
         readCount = await _sslStream.ReadAsync(buffer, _lifecycleTokenSource.Token).ConfigureAwait(false);

         if (readCount > 0)
         {

             var data = buffer.Slice(0, readCount);
             //告知消费者,往Pipe的管道中写入了多少字节数据
             _pipeWriter.Advance(readCount);
         }
         else
         {
             if (IsDisconnect())
             {
                 await DisConnectAsync();
             }

             throw new SocketException();
         }

         FlushResult result = await _pipeWriter.FlushAsync().ConfigureAwait(false);
         if (result.IsCompleted)
         {
             break;
         }
     }
     catch (IOException)
     {
         //TODO log
         break;
     }
     catch (SocketException)
     {
         //TODO log
         break;
     }
     catch (TaskCanceledException)
     {
         //TODO log
         break;
     }
 }

 _pipeWriter.Complete();
//消费者处理数据
 while (!_lifecycleTokenSource.Token.IsCancellationRequested)
 {
     ReadResult result = await _pipeReader.ReadAsync();
     ReadOnlySequence<byte> buffer = result.Buffer;
     ReadOnlySequence<byte> data;
     do
     {
        //通过过滤器得到一个完整的包
         data = _receivePackageFilter.ResolvePackage(ref buffer);

         if (!data.IsEmpty)
         {
             OnReceivedData?.Invoke(this, new ClientDataReceiveEventArgs(data.ToArray()));
         }
     }
     while (!data.IsEmpty && buffer.Length > 0);
     _pipeReader.AdvanceTo(buffer.Start);
 }

 _pipeReader.Complete();
/// <summary>
/// 解析数据包
/// 固定报文头解析协议
/// </summary>
/// <param name="headerSize">数据报文头的大小</param>
/// <param name="bodyLengthIndex">数据包大小在报文头中的位置</param>
/// <param name="bodyLengthBytes">数据包大小在报文头中的长度</param>
/// <param name="IsLittleEndian">数据报文大小端。windows中通常是小端,unix通常是大端模式</param>
/// </summary>
/// <param name="sequence">一个完整的业务数据包</param>
public override ReadOnlySequence<byte> ResolvePackage(ref ReadOnlySequence<byte> sequence)
{
    var len = sequence.Length;
    if (len < _bodyLengthIndex) return default;
    var bodyLengthSequence = sequence.Slice(_bodyLengthIndex, _bodyLengthBytes);
    byte[] bodyLengthBytes = ArrayPool<byte>.Shared.Rent(_bodyLengthBytes);
    try
    {
        int index = 0;
        foreach (var item in bodyLengthSequence)
        {
            Array.Copy(item.ToArray(), 0, bodyLengthBytes, index, item.Length);
            index += item.Length;
        }

        long bodyLength = 0;
        int offset = 0;
        if (!_isLittleEndian)
        {
            offset = bodyLengthBytes.Length - 1;
            foreach (var bytes in bodyLengthBytes)
            {
                bodyLength += bytes << (offset * 8);
                offset--;
            }
        }
        else
        {

            foreach (var bytes in bodyLengthBytes)
            {
                bodyLength += bytes << (offset * 8);
                offset++;
            }
        }

        if (sequence.Length < _headerSize + bodyLength)
            return default;

        var endPosition = sequence.GetPosition(_headerSize + bodyLength);
        var data = sequence.Slice(0, endPosition);//得到完整数据包
        sequence = sequence.Slice(endPosition);//缓冲区中去除取到的完整包

        return data;
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(bodyLengthBytes);
    }
}

以上就是实现了固定数据包头实现粘包断包处理的部分代码。

关于TCP的连接还有一些,比如客户端连接限制,空闲连接关闭等。如果大家对于完整代码感兴趣,可以看我刚写的一个TCP库:EasyTcp4Net:
https://github.com/BruceQiu1996/EasyTcp4Net

image

image

image

前言

好久不用Arcgis,突然发现想用时,有点不会安装了,所以这里记录一下安装过程。

下载Arcgis

首先,下载一个arcgis版本,我这里下的是10.1。

推荐【 gis思维(公众号)】,【麻辣GIS(网站)】。

当然了,这都是很旧很旧的版本了,基本上没有三维功能。

一定要下载带注册机的。

arcgis的压缩包包含3个部分,arcgis安装程序,arcgis license证书,注册机。

安装Arcgis Engine

先安装证书。

安装完用注册机生成注册码。

然后创建一个service.txt文件,一般情况,压缩包里会有一个。

也可以自己建,要utf-8编码。

复制注册码到service.txt里,然后将该文件替换到arcgis证书安装目录,我这里是C:\Program Files (x86)\ArcGIS\License10.1\bin。

然后打开Arcgis lecense server administrator。

点重新读取许可证。

如果停止状态,别忘了点启动。

image

注1:我在安装的时候遇到一个问题,就是启动点完,发现服务中,arcgis的证书服务已经启动了,但桌面应用的界面,还是提示未启动。我始终解决不了,后来去别的网站下了新的证书安装包,就好用了。这说明了我之前下载的arcgis证书应用是有问题的。

注2:这个证书原理很简单,理解原理后,可以方便我们解决问题,他就是一个winform小程序,程序里会创建一个服务,这个服务会开启一个socket监听,然后我们正式的软件,比如arcgisEngine或者arcgisDesktop启动时,会向这个端口发请求,如果返回值是可以接受的,那就可以正常使用了。

最后我们安装ArcgisEngine,这里直接下一步即可。

image

安装完成后,修改许可证管理器的地址。

我们已经知道了,主程序会向window服务发送sokcet了,那自然就很好理解这里。

我们把noset改为localhost,即让这个请求向localhost发送,至于端口,那都是arcgis程序里写死的,我们不用关心。

image

这样我们就安装完成了。

注:Arcgis提供的dll都是com组件,虽然我们都可以正常引用使用,但引用后,会在Interop.命名,Interop.是命名空间,它提供了将COM类型转换为C#类型的功能。

安装Arcgis Object

安装完 arcgis engine,我们还要安装arcgis object,这样才能让winform程序找到arcgis的相关组件。

但是,安装arcgis object要求我们的电脑要先安装 visual studio 2010。

由于我们早已经不用2010了,所以只能下一个安装。

visual studio 是支持安装多个版本的,我这里安装的是Visual studio 2022和2010。

推荐这个当快软件园下载,我就是这里下载的。
https://www.downkuai.com/

安装完 vs 2010,就可以正常安装arcgis object了。

然后,我用vs 2022创建项目,然后在工具箱里,就可以看到arcgis的工具了。

Arcgis开发传送门
通俗易懂的ArcGis开发快速入门


注:此文章为原创,任何形式的转载都请联系作者获得授权并注明出处!



若您觉得这篇文章还不错,请点击下方的【推荐】,非常感谢!

https://www.cnblogs.com/kiba/p/18085072

就在3月19日,Java22重磅发布。Java22新增了12项增强功能,其中包括七个预览特性和一个孵化器特性,这些功能都显著到足以引起JDK增强提案(JEPs)的关注。它们涵盖了Java语言、其API、性能以及JDK中包含的工具的改进。

真的卷不动了,,前段时间才将项目升级到Java17。。。。

接下来我们看看具体的新特性介绍。。。

image.png

Java语言上的改进

Unnamed Variables & Patterns - JEP 456

匿名变量和模式。当需要但未使用变量声明或嵌套模式时,提高了可读性。这两者都用下划线字符表示。

优化:

  1. 捕获开发人员意图,即给定的绑定或Lambda参数未使用,并强制执行该属性以澄清程序并减少错误的机会。

比如我们可以在循环中这样使用:

static int count(Iterable<Order> orders) {
    int total = 0;
    for (Order _ : orders)    // Unnamed variable
        total++;
    return total;
}

或者

for (int i = 0, _ = sideEffect(); i < 10; i++) { ... i ... }

或者
while
循环:

while (q.size() >= 3) {
    var x = q.remove();
    var _ = q.remove();       // Unnamed variable
    var _ = q.remove();       // Unnamed variable
    ... new Point(x, 0) ...
}
  1. 通过识别必须声明但未使用的变量(例如,在捕获子句中)来提高所有代码的可维护性。
String s = ...
try {
    int i = Integer.parseInt(s);
    ... i ...
} catch (NumberFormatException _) {        // Unnamed variable
    System.out.println("Bad number: " + s);
}

多个
catch

try { ... }
catch (Exception _) { ... }                // Unnamed variable
catch (Throwable _) { ... }                // Unnamed variable

或者这样使用
try...resource

try (var _ = ScopedContext.acquire()) {    // Unnamed variable
    ... no use of acquired resource ...
}

在lamba中我们可以这样使用:

...stream.collect(Collectors.toMap(String::toUpperCase,
                                   _ -> "NODATA"))    // Unnamed variable
  1. 允许在单个 case 标签中出现多个模式,如果它们都没有声明任何模式变量。

例如:

switch (ball) {
    case RedBall _   -> process(ball); // Unnamed pattern variable
    case BlueBall _  -> process(ball); // Unnamed pattern variable
    case GreenBall _ -> stopProcessing();  // Unnamed pattern variable
}

或者

switch (box) {
    case Box(RedBall _)   -> processBox(box);  // Unnamed pattern variable
    case Box(BlueBall _)  -> processBox(box);  // Unnamed pattern variable
    case Box(GreenBall _) -> stopProcessing(); // Unnamed pattern variable
    case Box(var _)       -> pickAnotherBox(); // Unnamed pattern variable
}

通过这种改进允许我们省略名称,未命名的模式变量使得基于类型模式的运行时数据探索在switch语句块以及使用instanceof运算符时,视觉上更加清晰明了。

  1. 通过省略不必要的嵌套类型模式来改善记录模式的可读性。

Statements before super - JEP 447

构造器中的前置语句。在构造函数中,允许在显式构造函数调用之前出现不引用正在创建的实例的语句。

优化:

  1. 为开发人员提供更大的自由度来表达构造函数的行为,从而使当前必须因子化为辅助静态方法、辅助中间构造函数或构造函数参数的逻辑能够更自然地放置。

有时我们需要验证传递给超类构造函数的参数。虽然我们可以在事后进行参数验证,但这意味着可能会进行不必要的操作。例如如下:

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        super(value);               // Potentially unnecessary work
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");
    }
}

Java22中的做法是声明一个能够快速失败的构造函数,即在调用超类构造函数之前先验证其参数。目前我们只能采用内联方式实现这一点,即借助于辅助静态方法:

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        super(verifyPositive(value));
    }

    private static long verifyPositive(long value) {
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");
        return value;
    }

}

我们还可以将验证逻辑直接包含在构造函数内部,这段代码将会更具可读性。

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");
        super(value);
    }

}
  1. 保留了构造函数在类实例化期间按自上而下顺序运行的现有保证,确保子类构造函数中的代码不能干扰超类实例化。

为了给超类构造函数提供参数,我们必须执行另外的计算,再次不得不借助于辅助方法:

public class Sub extends Super {

    public Sub(Certificate certificate) {
        super(prepareByteArray(certificate));
    }

    // 辅助方法
    private static byte[] prepareByteArray(Certificate certificate) { 
        var publicKey = certificate.getPublicKey();
        if (publicKey == null) 
            throw new IllegalArgumentException("null certificate");
        return switch (publicKey) {
            case RSAKey rsaKey -> ...
            case DSAPublicKey dsaKey -> ...
            ...
            default -> ...
        };
    }

}

超类构造函数接受一个字节数组作为参数,而子类构造函数接受一个
Certificate
对象作为参数。为了满足超类构造函数调用必须为子类构造函数中的第一条语句这一限制,我们声明了一个辅助方法
prepareByteArray
来为此调用准备参数。

如果能够将参数准备代码直接嵌入到构造函数中,这段代码会更具可读性。在Java22中我们可以这么做:

public Sub(Certificate certificate) {
        var publicKey = certificate.getPublicKey();
        if (publicKey == null) 
            throw new IllegalArgumentException("null证书");
        final byte[] byteArray = switch (publicKey) {
            case RSAKey rsaKey -> ... // RSA密钥转换为字节数组
            case DSAPublicKey dsaKey -> ... // DSA公钥转换为字节数组
            ...
            default -> ... // 其他情况处理逻辑
        };
        super(byteArray);
    }
  1. 不需要对Java虚拟机进行任何更改。这种 Java 语言特性仅依赖于 JVM 当前验证和执行构造函数中显式构造函数调用之前出现的代码的能力。

String Templates - JEP 459:

字符串模板。字符串模板通过将文本文字与嵌入表达式和模板处理器相结合,以产生专门的结果,来补充 Java 的现有字符串文字和文本块。

优化:

  1. 通过简化在运行时计算值的字符串的表达方式,简化了编写 Java 程序。
  2. 通过使文本和表达式混合的表达更易于阅读,无论文本是否适合单个源行(如字符串文字)或跨越多个源行(如文本块)。
  3. 通过支持模板及其嵌入表达式的验证和转换,改进了由用户提供的值组成字符串并将其传递给其他系统(例如,构建数据库查询)的 Java 程序的安全性。
  4. **保持了灵活性,允许Java库定义在字符串模板中使用的格式化语法。
  5. 简化了接受非Java语言(例如
    SQL

    XML

    JSON
    )编写的字符串的 API 的使用。
  6. 允许创建从文本文字和嵌入表达式计算出的非字符串值,而无需通过中间字符串表示转换。

字符串的模板可以直接在代码中表达,就像注释字符串一样,Java 运行时会自动将特定于模板的规则应用于字符串。从模板编写字符串将使开发人员不必费力地转义每个嵌入表达式、调用
validate()
整个字符串或使用
java.util.ResourceBundle
来查找本地化字符串。

比如我们可以构造一个表示JSON文档的字符串,然后将其提供给JSON解析器:

String name    = "Joan Smith";
String phone   = "666666-123-4567";
String address = "1 Maple Drive, Anytown";
String json = """
    {
        "name":    "%s",
        "phone":   "%s",
        "address": "%s"
    }
    """.formatted(name, phone, address);

JSONObject doc = JSON.parse(json);

字符串的 JSON 结构可以直接在代码中表达,Java运行时会
JSONObject
自动将字符串转换为。无需通过解析器进行手动绕行。
我们使用基于模板的字符串组合机制,我们就可以提高几乎每个Jav 程序的可读性和可靠性。这种功能将提供插值的好处,就像在其他编程语言中看到的那样,但不太容易引入安全漏洞。它还可以减少使用将复杂输入作为字符串的库的繁琐。

我们还可以使用模板
STR
处理器,
STR
是 Java 平台中定义的模板处理器。它通过将模板中的每个嵌入表达式替换为该表达式的(字符串化)值来执行字符串插值。
STR

public
static
final
自动导入到每个Java源文件中的字段。

// Embedded expressions can be strings
String firstName = "Bill";
String lastName  = "Duck";
String fullName  = STR."\{firstName} \{lastName}";
| "Bill Duck"
String sortName  = STR."\{lastName}, \{firstName}";
| "Duck, Bill"

// Embedded expressions can perform arithmetic
int x = 10, y = 20;
String s = STR."\{x} + \{y} = \{x + y}";
| "10 + 20 = 30"

// Embedded expressions can invoke methods and access fields
String s = STR."You have a \{getOfferType()} waiting for you!";
| "You have a gift waiting for you!"
String t = STR."Access at \{req.date} \{req.time} from \{req.ipAddress}";
| "Access at 2022-03-25 15:34 from 8.8.8.8"

模板表达式的模板可以跨越多行源代码,使用类似于文本块的语法。

String title = "My Web Page";
String text  = "Hello, world";
String html = STR."""
        <html>
          <head>
            <title>\{title}</title>
          </head>
          <body>
            <p>\{text}</p>
          </body>
        </html>
        """;
| """
| <html>
|   <head>
|     <title>My Web Page</title>
|   </head>
|   <body>
|     <p>Hello, world</p>
|   </body>
| </html>
| """

String name    = "Joan Smith";
String phone   = "666666-123-4567";
String address = "1 Maple Drive, Anytown";
String json = STR."""
    {
        "name":    "\{name}",
        "phone":   "\{phone}",
        "address": "\{address}"
    }
    """;
| """
| {
|     "name":    "Joan Smith",
|     "phone":   "666666-123-4567",
|     "address": "1 Maple Drive, Anytown"
| }
| """

Implicitly Declared Classes and Instance Main Methods - JEP 463:

隐式声明的类和实例主方法。这项Java增强引入了隐式声明的类以及实例主方法的功能,允许开发人员在不明确显式声明类的情况下编写类结构,并能够在类实例上直接定义和执行类似于传统
main
方法的入口点。这一特性旨在简化编程模型,特别是对于初学者和小型脚本场景,使得无需了解大型程序设计所需的完整类声明结构也能快速编写可运行的Java代码。

优化:
总体来说可以快速学习Java。

  1. 提供了平稳的入门 Java 编程的途径,因此教Java的可以逐渐介绍概念。
  2. 帮助初学者以简洁的方式编写基本程序,并随着他们的技能增长而逐渐增加他们的代码。
  3. 减少了编写简单程序(如脚本和命令行实用程序)的仪式感。
  4. 不会引入单独的 Java 语言初学者方言。
  5. 不会引入单独的初学者工具链;初学者的生程序应该使用编译和运行任何Java程序的相同工具。

我们以入门Java的第一行代码
Hello World
为例:

class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}

Java22还可以隐式声明一个类:

void main() {
    System.out.println("Hello, World!");
}

还可以这样:

String greeting() { return "Hello, World!"; }

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

或者,使用字段,如:

String greeting = "Hello, World!";

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

Java API库上的改进

Foreign Function & Memory API - JEP 454:

外部函数和内存API。允许Java程序与Java运行时之外的代码和数据进行交互。通过高效地调用外部函数(即JVM外部的代码)和安全地访问外部内存(即JVM不管理的内存),该API使Java程序能够调用本地库并处理本地数据,而无需JNI的脆弱性和危险性。

优化

  1. 生产率 —— 用简洁、可读和纯 Java 的 API 替换原生方法和 Java 本机接口(JNI)的脆弱机制。
  2. 性能 —— 提供与 JNI 和 sun.misc.Unsafe 相当甚至更好的外部函数和内存访问开销。
  3. 广泛的平台支持 —— 在 JVM 运行的每个平台上启用本地库的发现和调用。
  4. 统一性 —— 提供对结构化和非结构化数据的操作方式,无限大小,多种内存类型(例如,本机内存、持久内存和托管堆内存)。
  5. 健全性 —— 即使在多个线程之间分配和释放内存时,也保证不会出现使用后释放的错误。
  6. 完整性 —— 允许程序执行与本地代码和数据有关的不安全操作,但默认情况下向用户警告此类操作。

Java22提供外部函数和内存API(FFM API)定义类和接口,以便开发者使用他们可以

  • 控制外部内存

    MemorySegment

    Arena

    SegmentAllocator
    )的分配和释放,
  • 操作和访问结构化的外部存储器
    MemoryLayout

    VarHandle
  • 调用外部函数

    Linker

    SymbolLookup

    FunctionDescriptor

    MethodHandle
    )。

FFM API在
java.lang.foreign
包中。

Class-File API - JEP 457:

类文件API。提供了用于解析、生成和转换 Java 类文件的标准 API。

优化:

  1. 提供用于处理类文件的API,该类文件跟踪Java虚拟机规范定义的文件格式。class
  2. 使JDK组件能够迁移到标准 API,并最终迁移到标准API删除第三方ASM库的JDK内部副本。

Java22为 Class-File API 采用了以下设计目标和原则。

  • 类文件实体由不可变对象表示
    所有类文件 实体,例如字段、方法、属性、字节码指令、注释等, 由不可变对象表示。这有利于在以下情况下进行可靠共享 正在转换类文件。

  • 树结构表示
    类文件具有树结构。一个类 有一些元数据(名称、超类等)和可变数量的字段, 方法和属性。字段和方法本身具有元数据,并进一步 包含属性,包括属性。属性 further 包含指令、异常处理程序等。用于 导航和生成类文件应反映此结构。CodeCode

  • 用户驱动的导航
    我们通过类文件树的路径是 由用户选择驱动。如果用户只关心字段上的注释,那么 我们只需要解析结构内部的注释属性;我们不应该研究任何一个类 属性或方法的主体,或字段的其他属性。用户 应该能够处理复合实体,例如方法,无论是作为单个实体 单位或作为其组成部分的流,根据需要。field_info

  • 懒惰
    用户驱动的导航可显著提高效率,例如 不解析超过满足用户要求的类文件 需要。如果用户不打算深入研究方法的内容,那么我们 不需要解析比需要更多的结构 下一个类文件元素开始的位置。我们可以懒洋洋地膨胀和缓存, 用户请求时的完整表示形式。method_info

  • 统一的流式处理和具体化视图
    与 ASM 一样,我们希望同时支持两者 类文件的流式处理和实例化视图。流视图是 适用于大多数用例,而物化视图更 一般,因为它支持随机访问。我们可以提供一个物化的观点 通过懒惰比 ASM 便宜,这是由不变性实现的。我们可以, 此外,对齐流式视图和实例化视图,以便它们使用通用的 词汇表,可以协调使用,因为每个用例都很方便。

  • 紧急转换
    如果类文件解析和生成 API 是 充分对齐,那么转换可以是一个紧急属性,可以 不需要自己的特殊模式或重要的新 API 图面。(ASM实现 这是通过为读者和作者使用通用的访客结构来实现的。如果类, 字段、方法和代码体是可读和可写的,作为 元素,则可以将转换视为对此的平面映射操作 流,由 lambda 定义。

  • 细节隐藏
    类文件的许多部分(常量池、引导方法 表、堆栈图等)派生自类文件的其他部分。它 要求用户直接构建这些是没有意义的;这是额外的工作 对于用户来说,并增加了出错的机会。API 将自动 生成与其他实体紧密耦合的实体 添加到类文件中的字段、方法和指令。

Class-File API

java.lang.classfile
包和子包中。 它定义了三个主要抽象:

  • 元素是对类文件某部分的一种不可变描述,可能是一个指令、属性、字段、方法,甚至是整个类文件。有些元素,如方法,是复合元素;除了本身是元素外,它们还包含自身的元素,可以作为一个整体处理,也可以进一步分解。

  • 每种类型的复合元素都有一个对应的构建器,该构建器拥有特定的构建方法(例如,ClassBuilder::withMethod),并且也是相应元素类型的消费者。

  • 最后,变换代表了一个函数,它接收一个元素和一个构建器,并调解如何(如果有的话)将该元素转换为其他元素。

Stream Gatherers - JEP 461:

流收集器。增强了 Stream API,以支持自定义中间操作。这将允许流管道以不易通过现有内置中间操作实现的方式转换数据。

优化:

  • 使流管道更加灵活和富有表现力。
  • 尽可能允许自定义中间操作来操作无限大小的流。

流(Stream)::gather(Gatherer) 是一种新的中间流操作,通过应用用户自定义实体——收集器(Gatherer)来处理流中的元素。利用gather操作,我们可以构建高效且适用于并行处理的流,实现几乎所有的中间操作。Stream::gather(Gatherer) 在中间操作中的作用类似于Stream::collect(Collector)在终止操作中的作用。

Gatherer用于表示对流中元素的转换,它是java.util.stream.Gatherer接口的一个实例。Gatherer可以以一对一、一对多、多对一或多对多的方式转换元素。它可以跟踪已处理过的元素以影响后续元素的转换,支持短路操作以将无限流转换为有限流,并能启用并行执行。例如,一个Gatherer可以从输入流中按条件转换一个输入元素为一个输出元素,直到某一条件变为真,此时开始将一个输入元素转换为两个输出元素。

Gatherer由四个协同工作的函数定义:

  1. 可选初始化函数提供了在处理流元素过程中维持私有状态的对象。例如,Gatherer可以存储当前元素,以便下次应用时比较新元素和前一个元素,并仅输出两者中较大的那个。实际上,这种Gatherer将两个输入元素转换为一个输出元素。

  2. 整合函数整合来自输入流的新元素,可能检查私有状态对象,并可能向输出流发出元素。它还可以在到达输入流末尾之前提前终止处理;例如,一个搜索整数流中最大值的Gatherer在检测到Integer.MAX_VALUE时可以终止处理。

  3. 可选组合函数可用于在输入流标记为并行时并行评估Gatherer。若Gatherer不支持并行,则仍可作为并行流管道的一部分,但在评估时将以顺序方式进行。这对于某些本质上有序因而无法并行化的操作场景非常有用。

  4. 可选完成函数在没有更多输入元素要消费时被调用。该函数可以检查私有状态对象,并可能发出额外的输出元素。例如,在输入元素中搜索特定元素的Gatherer在其完成器被调用时,若未能找到目标元素,可以通过抛出异常等方式报告失败。

当调用Stream::gather时,执行以下等效步骤:

  1. 创建一个Downstream对象,当接收到Gatherer输出类型的元素时,将其传递到管道中的下一阶段。

  2. 通过调用其initializer的get()方法获取Gatherer的私有状态对象。

  3. 通过调用其integrator()方法获取Gatherer的整合器。

  4. 当存在更多输入元素时,调用整合器的integrate(...)方法,传入状态对象、下一个元素和下游对象。若该方法返回false,则终止处理。

  5. 获取Gatherer的完成器并使用状态对象和下游对象对其调用。

现有Stream接口中声明的所有中间操作都可以通过调用带有实现该操作的Gatherer的gather方法来实现。例如,对于一个T类型元素的流,Stream::map通过应用一个函数将每个T元素转换为U元素并将其向下传递;这实质上就是一个无状态的一对一Gatherer。另一个例子是Stream::filter,它采用一个谓词决定输入元素是否应向下传递;这只是一个无状态的一对多Gatherer。事实上,从概念上讲,每一个流管道都可以等同于:

source.gather(...).gather(...).gather(...).collect(...)

Structured Concurrency - JEP 462:

结构化并发。简化了并发编程。结构化并发将在不同线程中运行的相关任务组视为单个工作单元,从而简化了错误处理和取消,提高了可靠性并增强了可观察性。

优化:

  1. 促进一种并发编程风格,能够消除由于取消和关闭产生的常见风险,例如线程泄露和取消延迟。
  2. 提升并发代码的可观测性。

结构化并发API的主要类是
java.util.concurrent
包中的
StructuredTaskScope
类。此类允许开发人员将任务结构化为一组并发子任务,并将它们作为一个整体进行协调管理。子任务通过分别创建新线程(fork)并在之后作为一个整体等待它们完成(join)和可能的情况下作为一个整体取消它们。子任务的成功结果或异常将被父任务聚合并处理。
StructuredTaskScope
确保了子任务的生命周期被限定在一个清晰的词法作用域内,在这个作用域内,任务与其子任务的所有交互,包括分叉(
fork
)、同步(
join
)、取消(
cancellation
)、错误处理和结果合成都在此发生。

使用StructuredTaskScopes实例:

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Supplier<String>  user  = scope.fork(() -> findUser());
        Supplier<Integer> order = scope.fork(() -> fetchOrder());

        scope.join()            // 同步两个子任务
             .throwIfFailed();  // 并传播错误信息

        // 这里,两个子任务都已经成功,所以组合它们的结果
        return new Response(user.get(), order.get());
    }
}

这里理解涉及线程的生命周期变得简单:在所有情况下,它们的生命周期都被限制在一个词法作用域内,即
try-with-resources
语句的主体内。此外,使用
StructuredTaskScope
确保了一系列有价值的特性:

  1. 错误处理与短路机制——如果
    findUser()

    fetchOrder()
    子任务之一失败,尚未完成的另一个子任务将被取消。(这是由
    ShutdownOnFailure
    实现的关闭策略管理的,也有可能实现其他策略)。

  2. 取消传播——如果在调用
    join()
    之前或期间执行
    handle()
    方法的线程被中断,则当线程退出作用域时,两个子任务都将自动取消。

  3. 清晰性——上述代码具有清晰的结构:设置子任务,等待它们完成或被取消,然后决定是否成功(并处理已完成子任务的结果)或失败(子任务已经结束,因此无需进一步清理)。

  4. 可观测性——如下面所述,线程转储能够清晰地显示任务层级关系,执行
    findUser()

    fetchOrder()
    的线程在转储中显示为scope的子线程。

Scoped Values - JEP 464:

作用域值优化。在线程内和跨线程之间有效共享不可变数据。这个Java增强它旨在提供一种机制,允许开发者在Java应用程序中安全地在线程内部以及跨线程之间共享不可变数据。该特性旨在替代或改善现有的ThreadLocal机制,提供一种更加可控、易于管理和高效的解决方案,尤其是在涉及大规模并发处理和跨层数据传递场景时。通过范围值,开发人员可以更好地组织和管理在特定作用域内有效的变量,同时确保资源的有效利用和数据的安全共享。

优化:

  • 易用性 — 理解数据流应当轻松直观。
  • 可理解性 — 共享数据的生命周期可以从代码的语法结构中清晰可见。
  • 健壮性 — 调用者共享的数据只能被合法的被调用者获取。
  • 性能 — 数据能够有效地在大量线程间高效共享。

作用域值是一种容器对象,允许方法在同一个线程内安全高效地将其数据值与直接和间接的被调用者共享,同时也能与子线程共享,而无需依赖方法参数。它是一个类型为
ScopedValue
的变量,通常被声明为
final static
字段,并设置为
private
访问权限,以防止其他类的代码直接访问。

类似线程局部变量,作用域值关联了多个值,每个线程对应一个。具体使用的值取决于哪个线程调用了它的方法。不同于线程局部变量,范围限定值只被写入一次,并且在线程执行期间只能在一定时间段内可用。

作用域值的使用如下所示。一些代码调用
ScopedValue.where
,提供一个范围限定值及其要绑定的对象。调用run方法会绑定该范围限定值,为当前线程提供一个特定的副本,然后执行作为参数传递的
lambda
表达式。在run方法执行期间,
lambda
表达式或从中直接或间接调用的任何方法,都可以通过值的
get()
方法读取范围限定值。当run方法执行完毕后,该绑定关系会被销毁。

final static ScopedValue<...> NAME = ScopedValue.newInstance();

// 在某个方法中
ScopedValue.where(NAME, <value>)
           .run(() -> { ... NAME.get() ... 调用方法 ... });

// 在lambda表达式中直接或间接调用的方法中
... NAME.get() ...

代码的结构明确了线程可以读取其作用域值副本的时间段。这个有限的生命周期极大地简化了对线程行为的推理。数据从调用者单向传输至直接和间接的被调用者,一眼就能看出。不存在能让远端代码随时改变范围限定值的set方法。这也有助于提高性能:无论调用者和被调用者的栈距离如何,通过get()方法读取作用域值的速度常常与读取局部变量一样快。

Vector API - JEP 460:

矢量API。一个能够在支持的CPU架构上运行时可靠编译为最优矢量指令的API,从而实现优于等效标量计算的性能。
本JEP提议在JDK 22中重新孵化该API,相比于JDK 21版本,API进行了些许增强。实现内容包括bug修复和性能优化。主要变更如下:
支持通过任意原始元素类型的数组支持的堆内存
MemorySegments
进行矢量访问。在此之前,访问仅限于由字节数组支持的堆内存
MemorySegments

优化:

  1. 清晰简洁的API
    API应该能够清晰简洁地表述一系列由循环内矢量操作序列组成的各种矢量计算,可能还包括控制流程。应支持针对矢量大小或每矢量的通道数进行泛型表达,从而使这类计算能够在支持不同矢量大小的硬件之间移植。

  2. 平台无关性
    API应独立于CPU架构,支持在多种支持矢量指令的架构上实现。按照Java API的一贯原则,在平台优化和可移植性产生冲突时,我们会倾向于使API更具可移植性,即使这意味着某些特定于平台的惯用法无法在便携代码中表达。

  3. 在x64和AArch64架构上的可靠运行时编译和高性能
    在具备能力的x64架构上,Java运行时环境,特别是HotSpot C2编译器,应将矢量操作编译为相应的高效矢量指令,比如Streaming SIMD Extensions (SSE) 和Advanced Vector Extensions (AVX)支持的那些指令。开发者应有信心他们所表达的矢量操作将可靠地紧密映射到相关的矢量指令上。在具备能力的ARM AArch64架构上,C2同样会将矢量操作编译为NEON和SVE支持的矢量指令。

  4. 优雅降级
    有时矢量计算可能无法完全在运行时表述为矢量指令序列,可能是因为架构不支持所需的某些指令。在这种情况下,Vector API实现应能够优雅降级并仍然正常运作。这可能包括在矢量计算无法高效编译为矢量指令时发出警告。在不支持矢量的平台上,优雅降级将生成与手动展开循环相竞争的代码,其中展开因子为所选矢量的通道数。


  5. Project Valhalla
    项目的契合
    Vector API
    的长期目标是利用
    Project Valhalla
    对Java对象模型的增强功能。主要来说,这意味着将
    Vector API
    当前基于值的类更改为值类,以便程序能够处理值对象,即缺乏对象标识性的类实例。因此,
    Vector API
    将在多个版本中孵化,直至
    Project Valhalla
    的必要特性作为预览功能可用。一旦这些Valhalla特性可用,我们将调整Vector API及其实现以使用这些特性,并将
    Vector API
    本身提升为预览功能。

向量由抽象类
Vector<E>
表示。类型变量E被实例化为矢量覆盖的标量基本整数或浮点元素类型的装箱类型。一个向量还具有形状属性,该属性定义了矢量的大小(以位为单位)。当矢量计算由
HotSpot C2
编译器编译时,向量的形状决定了
Vector<E>
实例如何映射到硬件矢量寄存器。向量的长度,即车道数或元素个数,等于矢量大小除以元素大小。

支持的一系列元素类型(E)包括Byte、Short、Integer、Long、Float和Double,分别对应于标量基本类型byte、short、int、long、float和double。

支持的一系列形状对应于64位、128位、256位和512位的矢量大小,以及最大位数。512位形状可以将字节打包成64个车道,或者将整数打包成16个车道,具有这种形状的矢量可以一次性操作64个字节或16个整数。max-bits形状支持当前架构的最大矢量尺寸,这使得API能够支持
ARM SVE
平台,该平台实现可以支持从128位到2048位,以128位为增量的任何固定尺寸。

性能上的改进

Regional Pinning for G1 - JEP 423:

区域固定。通过在G1中实现区域固定(regional pinning),从而在Java Native Interface (JNI) 临界区域内不需要禁用垃圾收集,以此来减少延迟。

优化:

  1. 不会因JNI临界区域导致线程停滞。
  2. 不会因JNI临界区域而导致垃圾收集启动时增加额外延迟。
  3. 当没有JNI临界区域活动时,GC暂停时间不会出现倒退。
  4. 当JNI临界区域活动时,GC暂停时间只会有最小程度的倒退。

工具类

Launch Multi-File Source-Code Programs - JEP 458:

启动多文件源代码程序。允许用户在不首先编译程序的情况下运行由多个 Java 源代码文件提供的程序。

优化:

  1. 通过使从小型程序向大型程序的过渡更加渐进,使开发人员能够选择何时以及何时费力地配置构建工具,提高了开发人员的生产力。

Java22增强了Java启动器的源文件模式,使其能够运行以多份Java源代码文件形式提供的程序。

举例来说,假设一个目录包含了两个文件:Prog.java和Helper.java,每个文件各声明一个类:

// Prog.java
class Prog {
    public static void main(String[] args) { Helper.run(); }
}

// Helper.java
class Helper {
    static void run() { System.out.println("Hello!"); }
}

运行命令
java Prog.java
将会在内存中编译Prog类并调用其main方法。由于Prog类中的代码引用了Helper类,启动器会在文件系统中查找Helper.java文件,并在内存中编译Helper类。如果Helper类中的代码又引用了其他类,例如HelperAux类,那么启动器还会找到HelperAux.java并对其进行编译。

当不同.java文件中的类互相引用时,Java启动器并不保证按照特定顺序或时间点编译这些.java文件。例如,启动器可能先编译Helper.java再编译Prog.java。有些代码可能在程序开始执行前就已经被编译,而其他代码可能在需要时才懒加载编译。

只有被程序引用到的类所在的.java文件才会被编译。这样一来,开发者可以在尝试新版本代码时不必担心旧版本会被意外编译。例如,假设目录中还包含OldProg.java文件,其中包含Progr类的一个旧版本,该版本期望Helper类有一个名为go的方法而不是run方法。当运行Prog.java时,存在包含潜在错误的OldProg.java文件并不会影响程序执行。

一个.java文件中可以声明多个类,且会被一起编译。在一个.java文件中共声明的类优先于在其他.java文件中声明的类。例如,假设上面的Prog.java文件扩展后也在其中声明了Helper类,尽管Helper.java文件中已有一个同名类。当Prog.java中的代码引用Helper时,会使用在Prog.java中共同声明的那个类;启动器不会去搜索Helper.java文件。

源代码程序中禁止重复的类声明。也就是说,同一个.java文件内或构成程序的不同.java文件之间的类声明,如果名称相同,则不允许存在。假设经过编辑后,Prog.java和Helper.java最终变成以下形式,其中类Aux意外地在两个文件中都被声明:

// Prog.java
class Prog {
    public static void main(String[] args) { Helper.run(); Aux.cleanup(); }
}
class Aux {
    static void cleanup() { ... }
}

// Helper.java
class Helper {
    static void run() { ... }
}
class Aux {
    static void cleanup() { ... }
}

运行命令
java Prog.java
会编译Prog.java中的Prog和Aux类,调用Prog类的main方法,然后——由于main方法引用了Helper——查找并编译Helper.java中的Helper和Aux类。Helper.java中对Aux类的重复声明是不允许的,所以程序会停止运行,启动器报告错误。

当通过Java启动器传递单个.java文件名称时,就会触发其源文件模式。如果提供了额外的文件名,它们会成为主方法的参数。例如,运行命令
java Prog.java Helper.java
会导致字符串数组"Helper.java"作为参数传给Prog类的main方法。

其他特性

除了JEP中描述的更改之外,发行说明中还列出了许多较小的更新,这些更新对许多应用程序开发者有重要意义。其中包括废弃过时的API和移除先前已经弃用的API。

  1. 向keytool和jarsigner添加了更多算法。
  2. 垃圾回收器吞吐量方面的改进,特别是在“年轻代”垃圾回收方面。
  3. 改进了系统模块描述符的版本报告功能。
  4. 提高了对原生代码“等待”处理选项的完善。
  5. Unicode通用区域数据仓库已更新至版本44。
  6. 支持从字节码加载的类型上的类型注解。
  7. ForkJoinPool和ForkJoinTask现在能更好地处理不可中断任务。
  8. 对客户端与服务器TLS连接属性配置提供了更多的灵活性。
  9. 提高了对原生内存跟踪的功能,包括峰值使用情况的报告。
  10. 最后,如同所有特性发布版一样,JDK 22包含了数百项性能、稳定性和安全性更新,包括适应底层操作系统和固件更新及标准变化。用户和应用程序开发者通常在不知不觉中受益于这些变化。

最后,JDK 22是通过六个月的发布节奏按时交付的13th功能版本。由于预期改进源源不断,这种程度的可预测性使开发人员能够轻松管理创新的采用。Oracle 不会为 JDK 22 提供长期支持,在 2023 年 9 月之前提供更新,之后它将被 Oracle JDK 23 取代。最近的长期维护版本是Java 21。

image.png

本文已收录于我的个人博客:
码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等