Java 18が2022/3/22にリリースされました。
https://mail.openjdk.org/pipermail/jdk-dev/2022-March/006458.html
機能的に多くのプログラマに関係ありそうな変更はUTF-8がデフォルトになったくらい。あとは簡単なWebサーバーが入ったことかな。
詳細はこちら
JDK 18 Release Notes
Java SE 18 Platform JSR 393
OpenJDK JDK 18 GA Release
APIドキュメントはこちら
Overview (Java SE 18 & JDK 18)
変更点まとめはこちら
https://docs.oracle.com/en/java/javase/18/docs/api/new-list.html
APIの差分はこちら。
https://cr.openjdk.java.net/~iris/se/18/latestSpec/apidiffs/overview-summary.html
MacやLinuxでのインストールにはSDKMAN!をお勧めします
Oracle OpenJDK以外に無償で商用利用できるディストリビューションとしては、次のようなものがあります。(まだ用意されていないものもあります。3/23時点ではOracle JDK, Corretto 18だけかな)
- Oracle JDK ※対外サーバーの運用には有償ライセンスが必要です
- Adoptium Temurin
- Azul Zulu
- Liberica JDK
- Amazon Corretto 18
- Microsoft Build of OpenJDK
アップデートは4月に18.0.1が、7月に18.0.2がリリースされることになります。
JEP
大きめの変更はJEPでまとまっています。
https://openjdk.java.net/projects/jdk/18/
今回は9個のJEPが取り込まれました。すでにプレビューなどで出ていたものが3つあり、新たに取り込まれたものは6です。非推奨が1つあるので、新機能としては5つですね。
JEP 400: UTF-8 by Default
JEP 408: Simple Web Server
JEP 413: Code Snippets in Java API Documentation
JEP 416: Reimplement Core Reflection with Method Handles
JEP 417: Vector API (Third Incubator)
JEP 418: Internet-Address Resolution SPI
JEP 419: Foreign Function & Memory API (Second Incubator)
JEP 420: Pattern Matching for switch (Second Preview)
JEP 421: Deprecate Finalization for Removal
ツール
ツール関係の変更としては次の3つのJEPがあります。
JEP 400: UTF-8 by Default
JEP 408: Simple Web Server
JEP 413: Code Snippets in Java API Documentation
JEP 400: UTF-8 by Default
APIのデフォルトエンコーディングがUTF-8になりました。これでネットでダウンロードしたファイルを表示するプログラムがWindowsだけ文字化けするみたいなことが起きにくくなります。
環境変数では、file.encodingがUTF-8になります。
C:\Users\naoki>java\jdk\jdk-18\bin\java -XshowSettings:properties -version
Property settings:
    file.encoding = UTF-8
...
    line.separator = \r \n
    native.encoding = MS932
...
    sun.jnu.encoding = MS932
...
    sun.stderr.encoding = ms932
    sun.stdout.encoding = ms932
...
openjdk version "18" 2022-03-22
OpenJDK Runtime Environment (build 18+36-2087)
OpenJDK 64-Bit Server VM (build 18+36-2087, mixed mode, sharing)
Java 17ではOSごとに違いWindowsではMS932でした。
C:\Users\naoki>java\jdk\jdk-17\bin\java -XshowSettings:properties -version
Property settings:
    file.encoding = MS932
...
    line.separator = \r \n
    native.encoding = MS932
...
    sun.jnu.encoding = MS932
...
    sun.stderr.encoding = ms932
    sun.stdout.encoding = ms932
...
openjdk version "17" 2021-09-14
OpenJDK Runtime Environment (build 17+35-2724)
OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)
ただ、いままで通りOSごとに違う設定にしたい場合というのもあると思います。その場合は、環境変数file.encodingにCOMPATを指定します。そうすると、実際のfile.encodingの値が環境ごとのエンコーディング設定になります。
C:\Users\naoki>java\jdk\jdk-18\bin\java -Dfile.encoding=COMPAT -XshowSettings:properties -version
Property settings:
    file.encoding = MS932
native.encodingがJava 17で導入されているので、COMPATを選ぶときはfile.encodingにその値が設定されます。
C:\Users\naoki>javac C:\Users\naoki\IdeaProjects\untitled3\src\main\java\projava\WebServer.java
C:\Users\naoki\IdeaProjects\untitled3\src\main\java\projava\WebServer.java:29: エラー: この文字(0x83)は、エンコーディン
グwindows-31jにマップできません
                        <body><h1>Hello</h1>It works!<br>縺ッ繧阪?シ</body></html>
                                                             ^
エラー1個
C:\Users\naoki>java\jdk\jdk-18\bin\javac C:\Users\naoki\IdeaProjects\untitled3\src\main\java\projava\WebServer.java
Charset.forName("default")はJava 17ではUS-ASCIIを返していました。
C:\Users\naoki>java\jdk\jdk-17\bin\jshell
|  JShellへようこそ -- バージョン17
|  概要については、次を入力してください: /help intro
jshell> java.nio.charset.Charset.forName("default")
$1 ==> US-ASCII
これがJava 18ではUnsupportedCharsetExceptionを投げるようになります。
C:\Users\naoki>java\jdk\jdk-18\bin\jshell
|  JShellへようこそ -- バージョン18
|  概要については、次を入力してください: /help intro
jshell> java.nio.charset.Charset.forName("default")
|  例外java.nio.charset.UnsupportedCharsetException: default
|        at Charset.forName (Charset.java:527)
|        at (#1:1)
JEP 408: Simple Web Server
ファイルを扱う簡単なWebサーバーが用意されました。
jwebserverコマンドで、カレントディレクトリを扱うWebサーバーが起動します。
C:\Users\naoki>echo ^<h1^>hello^</h1^>It^ works > index.html
C:\Users\naoki>java\jdk\jdk-18\bin\jwebserver
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving C:\Users\naoki and subdirectories on 127.0.0.1 port 8000
URL http://127.0.0.1:8000/
127.0.0.1 - - [05/3譛・2022:22:14:03 +0900] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [05/3譛・2022:22:14:03 +0900] "GET /favicon.ico HTTP/1.1" 404 -
起動オプションには次のようなものがあります。
| オプション | 用途 | 
|---|---|
| -h / -? / --help | ヘルプメッセージの表示 | 
| -b addr / --bind-addess addr | アドレスをバインドする。デフォルトはループバック | 
| -d dir / --directory dir | 提供するディレクトリ。デフォルトはカレントディレクトリ | 
| -o level or --output level | 出力フォーマット。 none / info / verbose デフォルトはinfo | 
| -p port or --port port | ポート番号。デフォルトは8000 | 
| -version or --version | バージョンの出力 | 
API
APIから起動することもできます。SimpleFileServer
jshell> import com.sun.net.httpserver.SimpleFileServer
jshell> import com.sun.net.httpserver.SimpleFileServer.OutputLevel
jshell> var server = SimpleFileServer.createFileServer(new InetSocketAddress(8000), Path.of("c:/Users/naoki"), OutputLev
el.VERBOSE)
server ==> sun.net.httpserver.HttpServerImpl@2f7c7260
jshell> server.start()
jshell> 127.0.0.1 - - [05/3譛・2022:23:08:05 +0900] "GET / HTTP/1.1" 200 -
Resource requested: c:\Users\naoki
> Cookie: _redmine_session=YWNTYi9CVTNqclBQcG9WOFZ4d3o5bHJZTDNIWlUwcnJhZml0c05ac0tCQzZQVVlYelJPY01PaGxrMjdNbDhvazkwL1lEQnhTNSs3SDBSVm5HT3hERGQ4MVc2K2hRV1ptNE4yTjMzbFlKWkpQZmI0aWhVWFJLNkJhMHFtOS8zVURnbUJVZGJyOTdIL0lVd1g5YlJ0OEVTVW90WGs5MTdMRDRkOU1SOUxqYnJ2clJ4bTQzMkt1am4zSlFSa0pFWC9KcUZoSnJqelcrMTJzQ3djUEs4ZXp0dGJSRnBNVTFSYXRwUnFFUGVTMlBEMDRvZGdOZVc2ejVuYXZjY09Fdnp4WjNkUTIydStTMFh4RUtpRnAway9vOHJKbkNzNFFIbTZJc0dOY3ZlWUFTbENRSlRKbjBROU55ZVJDaGtoSW9WNnV2VnpLc1BIYkZySmg3Z2ZwajM1dkM0V3JNemN6b1NaQTBMN0NHRjdUYjcyajlYcm5lOW9RbjN5eVNyUnVOYVRQdUJobjNmY2hZUFlQT3E1WkdROW5vWGc1eDVPZnVhOERwM09PazhibVp0TGYreTVId2tuMDdxUmlKNU5DRFVXOVlMWHV3S08ydCtpaExxdElPdEZDc0hhMzhSUTRKTDBjdDZETjhPT0R2eCtoOGFkR2NqcWtXMHlRVUFPZ1BuQ2szZGVKM1Z3MHp6Ky9KS0NmUkdNNVMxbVdDQUxxbkUybk96L2Y4ZytteGdIQ3BhaXBPeWlWNnBKc2ltMUZJalpsSy93K3R1SjUzRGRCQUJwWnkweSs3UT09LS1IUE1VSmN1dC9ZR1hJTytSSFlrQW5RPT0%3D--672fdb2f2f192abdf0e3b3d62c6eb9001a1110eb
> Accept-encoding: gzip, deflate
> Sec-fetch-dest: document
> Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
> Sec-fetch-user: ?1
> Connection: keep-alive
> Sec-fetch-site: none
> Host: localhost:8000
> Sec-fetch-mode: navigate
> User-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0
> Accept-language: ja,en-US;q=0.7,en;q=0.3
> Upgrade-insecure-requests: 1
>
< Date: Sat, 05 Mar 2022 14:08:05 GMT
< Last-modified: Sat, 5 Mar 2022 13:44:43 GMT
< Content-type: text/html
< Content-length: 25
<
createFileServerに与えるPathは絶対パスで指定します。
JEP 413: Code Snippets in Java API Documentation
Javadocにコードスニペットを含むことができます。使う機会があるかどうかは別として、わりと面白い。
基本は{@snippet :から}で囲むものです。
/**
 * The following code shows how to use {@code Optional.isPresent}:
 * {@snippet :
 * if (v.isPresent()) {
 *     System.out.println("v: " + v.get());
 * }
 * }
 */
制約として、/*~*/のコメントは使えません。また中カッコ{}は対応している必要があります。
スニペットとして外部ファイルを指定することもできます。
/**
 * The following code shows how to use {@code Optional.isPresent}:
 * {@snippet file="ShowOptional.java" region="example"}
 */
// @startから// @endまでがスニペットとして使われる感じですね。
public class ShowOptional {
    void show(Optional<String> v) {
        // @start region="example"
        if (v.isPresent()) {
            System.out.println("v: " + v.get());
        }
        // @end
    }
}
@highlightで強調を指定することもできます。\bは単語境界を示すので、\barg\bの場合argsは強調されずargだけが強調されます。
/**
 * {@snippet :
 *   public static void main(String... args) {
 *       for (var arg : args) {                 // @highlight region regex = "\barg\b"
 *           if (!arg.isBlank()) {
 *               System.out.println(arg);
 *           }
 *       }                                      // @end
 *   }
 *   }
 */
@replaceで、コンパイルが通らない文字列に置き換えることもできますね。
/**
 * A simple program.
 * {@snippet :
 * class HelloWorld {
 *     public static void main(String... args) {
 *         System.out.println("Hello World!");  // @replace regex='".*"' replacement="..."
 *     }
 * }
 * }
 */
@linkでJavadocへのリンクを設定することもできます。
/**
 * A simple program.
 * {@snippet :
 * class HelloWorld {
 *     public static void main(String... args) {
 *         System.out.println("Hello World!");  // @link substring="System.out" target="System#out"
 *     }
 * }
 * }
 */
言語機能
言語機能の変更としては、Pattern Matching for switchが2ndプレビューになっています。
JEP 420: Pattern Matching for switch (Second Preview)
パターンマッチがswitchで使えるようになります。
2ndプレビューになりました。今回大きな変更がなければJava 19で正式機能になりそうです。
Java 18では、使うときに--enable-previewが必要です。
「といってもパターンマッチってそんなに出番ないのでは?」
と思うかもしれませんが、大きな影響が。
switchでcase nullが書けるようになります。
jshell> String s = null
s ==> null
jshell> switch (s) {
   ...>     case "test" -> "テスト";
   ...>     case null -> "ぬるぽ";
   ...>     default -> "hello";
   ...> }
$13 ==> "ぬるぽ"
case nullがない場合は従来どおりNullPointerExceptionです。
jshell> switch (s) {
   ...>     case "test" -> "テスト";
   ...>     default -> "hello";
   ...> }
|  例外java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "<local0>" is null
|        at (#12:1)
""とnullで同じ処理を行う場合などに併記できるのが便利です。
jshell> switch(s){
   ...>     case null, "" -> "empty";
   ...>     default -> s;
   ...> }
$17 ==> "empty"
defaultとnullで同じ処理したいときにアロースタイルで書けないんでは?という疑問に応えて、defaultがcaseに書けるようになっています。
jshell> switch (s) {
   ...>     case "one" -> 1;
   ...>     case "two" -> 2;
   ...>     case null, default -> -1;
   ...> }
$21 ==> -1
Java 17の1st previewの時点ではswitchにnullリテラルを渡すとjavacが落ちていました。
>jshell --enable-preview
|  JShellへようこそ -- バージョン17
|  概要については、次を入力してください: /help intro
jshell> switch(null) {
   ...>     default -> "hello";
   ...> }
コンパイラで例外が発生しました(17)。Bug Database (http://bugs.java.com)で重複が...
Java 18で修正されて正しくぬるぽが出るようになっています。
jshell> switch (null) {
   ...>     default -> "hello";
   ...> }
|  例外java.lang.NullPointerException
|        at Objects.requireNonNull (Objects.java:208)
|        at (#33:1)
で、パターンマッチですが、型と変数をswtichに書けるようになりました。これをタイプパターンといいます。
jshell> Object o = "abc"
o ==> "abc"
jshell> switch (o) {
   ...>     case String s -> "--%s--".formatted(s);
   ...>     case Integer i -> "%03d".formatted(i);
   ...>     default -> o.toString();
   ...> }
$35 ==> "--abc--"
タイプパターンのあとに&&でつなげて条件を書くことでガード節とすることができます。
jshell> o = "helo"
o ==> "helo"
jshell> switch (o) {
   ...>     case String s && s.length() < 5 -> "--%s--".formatted(s);
   ...>     case String s -> s.toUpperCase();
   ...>     case Integer i -> "%05d".formatted(i);
   ...>     default -> o.toString();
   ...> }
$42 ==> "--helo--"
ただし、3rdプレビューでは&&ではなくwhenを使うようです。
switch (o) {
    case String s when s.length() < 5 -> "--%s--".formatted(s);
    case String s -> s.toUpperCase();
    case Integer i -> "%05d".formatted(i);
    default -> o.toString();
}
3rdプレビューが来たということは、Java 19での正式化はないということでもあります。
http://openjdk.java.net/jeps/8282272
定数パターンとタイプパターンをひとつのswitchで使うこともできます。定数パターンに使えるのはこれまでどおり整数、文字列、enumです。
jshell> s = "helo"
s ==> "helo"
jshell> switch (s) {
   ...>     case "test" -> "テスト";
   ...>     case String s && s.length() < 5 -> s.toUpperCase();
   ...>     default -> "too long";
   ...> }
$29 ==> "HELO"
case句のタイプパターンで定義した変数のスコープは、case句の中だけです。
ここではswitchの外にあるのと同じく変数sを定義してcase句で上書きしていますが、switchの外に影響はありません。
jshell> s = "helo"
s ==> "helo"
jshell> switch (s) {
   ...>     case String s && s.length() < 5 -> (s="Hello").toUpperCase();
   ...>     default -> "too long";
   ...> }
$30 ==> "HELLO"
jshell> s
s ==> "helo"
Java 19ではレコードに対するパターンがプレビューとして導入される予定です。
https://openjdk.java.net/jeps/405
return switch(n) {
    case IntExpr(int i) -> i;
    case NegExpr(Expr n) -> -eval(n);
    case AddExpr(Expr left, Expr right) -> eval(left) + eval(right);
    case MulExpr(Expr left, Expr right) -> eval(left) * eval(right);
    default -> throw new IllegalArgumentException(n);
};
API
[JEP 416: Reimplement Core Reflection with Method Handles]
JEP 421: Deprecate Finalization for Removal
JEP 418: Internet-Address Resolution SPI
JEP 417: Vector API (Third Incubator)
[JEP 419: Foreign Function & Memory API (Second Incubator)]
小さいもの
java.lang.Math / java.lang.StrictMath
いろいろ追加されています。
ceilDiv
ceilMod
divideExact
floorDivExact
unsignedMultiplyHigh
Duration.isPositive
Duration.isZeroやDuration.isNegativeはこれまであったのにisPositiveがなかったので追加されました。
HttpRequest.Builder.HEAD()
メソッドをHEADとしてビルダーを作ります。
JEP 416: Reimplement Core Reflection with Method Handles
java.lang.reflect.Method、java.lang.reflect.Constructor、java.lang.reflect.FieldをMethod Handlesを使って実装しなおします。
APIへの変更はなく、実装の変更です。
JEP 421: Deprecate Finalization for Removal
OSリソースの開放漏れを防ぐために導入されたファイナライザですが、オブジェクトがGC対象になってからfinalizeがいつ呼び出されるかわからなかったり、GC対象になったはずのオブジェクトを再び参照させたり、スレッドや呼び出し順をコントロールできなかったり、いろいろな問題がありました。
セキュリティ脆弱性の原因になったり、パフォーマンス悪化につながったりということで、非推奨、そのうち削除ということになりました。
--finalization=disabledをつけて実行することで、ファイナライザが無効になって、GC時にも実行されなくなります。デフォルトではファイナライザは有効ですが、今後のバージョンでデフォルト無効になり、さらにその先のバージョンでファイナライザは取り除かれます。
Object.finalize()やGraphics.finalize()のようなfinalizeメソッドは@Deprecatedになります。
JEP 418: Internet-Address Resolution SPI
InetAddress.getByNameのようなAPIはシステムに組み込まれた名前解決を使っていましたが、SPIとして提供できるようになりました。
JEP 417: Vector API (Third Incubator)
AVX命令のような、複数のデータに対する計算を同時に行う命令をJavaから利用できるようになります。
Project Panamaのひとつとして開発されていました。
Java 16でインキュベータとして導入されましたが、今回3rdインキュベータになりました。今回、ArmのSVE命令をサポートしています。
使うためには実行時やコンパイル時に--add-modules jdk.incubator.vectorをつける必要があります。
import jdk.incubator.vector.*;
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256;
void vectorComputation(float[] a, float[] b, float[] c) {
    for (int i = 0; i < a.length; i += SPECIES.length()) { // SPECIES.length() = 256bit / 32bit -> 8
        VectorMask<Float> m = SPECIES.indexInRange(i, a.length); // 端数がマスクされる
                                                                 // a.lengthが11でiが8のとき最初の3つしか要素がないので [TTT.....]
		// FloatVector va, vb, vc;
        FloatVector va = FloatVector.fromArray(SPECIES, a, i, m);
        FloatVector vb = FloatVector.fromArray(SPECIES, b, i, m);
        FloatVector vc = va.mul(va).
                    add(vb.mul(vb)).
                    neg();
        vc.intoArray(c, i, m);
    }
}
利用できるのは次の6つの型です。それぞれに対応するVector型があって、これが基本になります。
| 型 | bit幅 | Vector | 
|---|---|---|
| byte | 8 | ByteVector | 
| short | 16 | ShortVector | 
| int | 32 | IntVector | 
| long | 64 | LongVector | 
| float | 32 | FloatVector | 
| double | 64 | DoubleVector | 
ただ、利用するにはVectorSpeciesが必要です。利用したいVectorにSPECIES_*という定数が用意されているので、それを使います。*は一度に計算するbit数ですね。
jshell> FloatVector.SP
SPECIES_128         SPECIES_256         SPECIES_512         SPECIES_64          SPECIES_MAX         SPECIES_PREFERRED   
MAXではそのハードウェアで使える最大、PREFERREDは推奨ビット数だけど、同じになるんじゃないのかな。ここでは256bitが推奨されて、floatが8個同時に計算できるようになっていますね。
jshell> FloatVector.SPECIES_PREFERRED
$11 ==> Species[float, 8, S_256_BIT]
ハードウェアで使えるbit数は搭載CPUに依存しますが、普通のIntel/AMDであれば256、XEONとか つよつよCPUなら512かな。M1は128でした。ハードウェアでサポートされないbit数を使おうとするとソフトウェア処理になるので遅くなります。
実際のVectorはfrom*というメソッドで取得します。fromArray、fromByteArray、fromByteBufferが用意されています。インキュベータに入る前はfromValuesがあったのですが、なくなってますね。
Vectorを得られたら、用意されたメソッドで計算します。ひととおりの算術命令はあります。
jshell> va.
abs()                   add(                    addIndex(               bitSize()               blend(                  
broadcast(              byteSize()              compare(                div(                    elementSize()           
elementType()           eq(                     equals(                 fma(                    getClass()              
hashCode()              intoArray(              intoByteArray(          intoByteBuffer(         lane(                   
lanewise(               length()                lt(                     max(                    min(                    
mul(                    neg()                   notify()                notifyAll()             pow(                    
rearrange(              reduceLanes(            reduceLanesToLong(      reinterpretAsBytes()    reinterpretShape(       
selectFrom(             shape()                 slice(                  sqrt()                  sub(                    
test(                   toArray()               toDoubleArray()         toIntArray()            toLongArray()           
toShuffle()             toString()              unslice(                viewAsFloatingLanes()   viewAsIntegralLanes()   
wait(                   withLane(      
ところで、こういったメソッド呼び出しの内部でAVX命令などを呼び出すのでは遅くなるんではという気がしますが、実際にはJVM intrinsicsという仕組みでJITコンパイラがこれらのメソッド呼び出しをネイティブ関数呼び出しに置き換えます。
JEP 419: Foreign Function & Memory API (Second Incubator)
2ndインキュベータですが、Foreign Function APIの前身であるForeign Linker APIはJava 16から、Foreign Memory APIはJava 14からインキュベータで、結構ながくインキュベータやってます。
Foreign Memory API
ヒープ外のメモリをアクセスする方法としては、ByteBufferを使う方法やUnsafeを使う方法、JNIを使う方法がありますが、それぞれ一長一短があります。
ByteBufferでdirect bufferを使う場合、intで扱える範囲の2GBまでに制限されたり、メモリの解放がGCに依存したりします。
Unsafeの場合は、性能もいいのですが、名前が示すとおり安全ではなく、解放済みのメモリにアクセスすればJVMがクラッシュします。
JNIを使うとCコードを書く必要があり、性能もよくないです。
ということで、ヒープ外のメモリを直接扱うAPIがJava 14でインキュベータモジュールとして導入され、今回5バージョン目のインキュベータです。
次のようなコードになります。
import jdk.incubator.foreign.*;
import java.nio.ByteOrder;
VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   for (int i = 0 ; i < 25 ; i++) {
        intHandle.set(seg, i * 4, i);
   }
}
コンパイル、実行では--add-modules jdk.incubator.foreignを付けてモジュールを読み込む必要があります。
パッケージもモジュール名と同様、jdk.incubator.foreignになっています。
MemoryHandlesからwithOffsetとwithStrideが消えましたね。
jshell> import jdk.incubator.foreign.*
jshell> MemoryHandles.
asAddressVarHandle(   asUnsigned(           class                 collectCoordinates(   dropCoordinates(
filterCoordinates(    filterValue(          insertCoordinates(    permuteCoordinates(   varHandle(
Java 15ではMemoryHandlesからVarHandleを得るメソッドが拡充してました。
jshell> MemoryHandles.
asAddressVarHandle(   asUnsigned(           class                 collectCoordinates(   dropCoordinates(
filterCoordinates(    filterValue(          insertCoordinates(    permuteCoordinates(   varHandle(
withOffset(           withStride(
Java 14では次の3つのメソッドしかありませんでした。
jshell> MemoryHandles.
class         varHandle(    withOffset(   withStride(
Java 16ではMemoryAccessというユーティリティが用意されて、VarHandleを使わずにシンプルにかけるようになっています。
import jdk.incubator.foreign.*;
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   for (int i = 0 ; i < 25 ; i++) {
        MemoryAccess.setIntAtOffset(i * 4, i);
   }
}
Foreign Function API
ネイティブライブラリの呼び出しを行う。
外部メモリのアクセスにはForeign Memory Access APIを使う
JNIの代替
Java 16でForeign Linkerとして1stインキュベータになり、今回3バージョン目のインキュベータです。
たとえばこんな感じのC関数があって。
size_t strlen(const char *s);
こんな感じでMethodHandleを取り出して。
CLinker linker = CLinker.systemCLinker();
MethodHandle strlen = linker.downcallHandle(
    linker.lookup("strlen").get(),
    FunctionDescriptor.of(JAVA_LONG, ADDRESS)
);
こんな感じで呼び出すようです。
MemorySegment str = implicitAllocator().allocateUtf8String("Hello");
long len          = strlen.invoke(cString);  // 5

