ブログ

JDK

Java 19新機能

Java 19が公開されました。Java 19のLinux/RISC-V Port(JEP422)を除くとすべてPreviewかIncubatorとなっています。

  • レコードパターン(プレビュー)[JEP405]
  • Linux/RISC-Vポート[JEP422]
  • 外部関数と外部メモリAPI (プレビュー)[JEP424]
  • 仮想スレッド(プレビュー)[JEP425]
  • ベクターAPI(4次インキュベーター)[JEP426]
  • switch文パターンマッチ(3次プレビュー)[JEP427]
  • 構造化並列API(インキュベーター)[JEP428]

https://openjdk.org/projects/jdk/19/

JEP405: レコードパターン(プレビュー)

instanceof演算子の拡張みたいなものでしょうか。if ( obj instanceof A a)という書き方をして,objがクラスAのインスタンスであった場合はキャストされたオブジェクトが変数aに格納されるという記述ができました。

public class RecordPattern
{
    public static void main(String[] args)
    {
        Object obj = new String("Hello World");
        if(obj instanceof String str)
        {
            System.out.println(str);
        }
        else
        {
            System.out.println("It's not a instance of String");
        }
    }
}

Hello World

JEP424: 外部関数と外部メモリ(プレビュー)

java.lang.foreignパッケージに外部関数と外部メモリを扱うクラス等が用意されています。C言語の文字列長を検査するstrlen()をJavaから呼び出してみます。コンパイルには–enable-previewと -source 19という2つのオプションが必要です。

import java.lang.foreign.Linker;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.MemorySession;
import java.lang.foreign.SegmentAllocator;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;

public class ForeignFunction
{
    public static void main(String[] args) throws Throwable
    {
        Linker linker = Linker.nativeLinker();
        SymbolLookup stdlib = linker.defaultLookup();
        FunctionDescriptor functionDescriptor = FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);
        MethodHandle strlen = linker.downcallHandle(stdlib.lookup("strlen").get(), functionDescriptor);
        String javaStrings   = "Hello from Java 19";
        long len = -1L;
        try(MemorySession session = MemorySession.openConfined())
        {
            MemorySegment cString = MemorySegment.allocateNative(javaStrings.length() + 1, session);
            cString.setUtf8String(0, javaStrings);
            len = (long)strlen.invoke(cString);
        }
        System.out.printf("strlen()=%d\n", len);
    }
}

ノート:ForeignFunction.javaはJava SE 19のプレビュー機能を使用します。
ノート:詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
strlen()=18

JEP425: 仮想スレッド(プレビュー)

従来のプラットフォームによるスレッドに加えて,Java VMによる仮想スレッドが登場しています。性能が著しく向上しているとのことですが,パフォーマンスまでは確認できていません。java.util.concurrent.ExecutorsクラスにnewVirtualThreadPerTaskExecutor()というstaticメソッドが追加されており,これを使って仮想スレッドを生成することができます。

import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.stream.IntStream;

public class VirtualTrhead
{
    public static void main(String[] args) throws Exception
    {
        try(ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor())
        {
            IntStream.range(0, 10).forEach(i -> { executor.submit(new ExampleTask(i)); });
        }
    }
    static class ExampleTask implements Callable<Integer>
    {
        private int number;
        public ExampleTask(int arg)
        {
            number = arg;
        }
        @Override
        public Integer call() throws Exception
        {
            IntStream.range(0, 10).forEach(i -> System.out.printf("Hello %d\n", number));
            return number;
        }
    }
}

JEP426: ベクターAPI(インキュベーター)

ベクトル計算が容易に記述できるようになります。ベクトルAとベクトルBが存在するとして,(A×A+B×B)×(-1)の計算を行う例です。配列を使った場合はループを用いて計算をくり返す必要があります。ベクターAPIでGPUを用いた最適化が行われるような環境では高いパフォーマンスを期待できるのでしょうか・・・。

次のようにコマンドを実行してコンパイルしています。

javac –enable-preview -source 19 –add-modules jdk.incubator.vector

import java.util.Arrays;
import jdk.incubator.vector.DoubleVector;
import jdk.incubator.vector.VectorMask;
import jdk.incubator.vector.VectorSpecies;

public class VectorAPI
{
    private static final VectorSpecies<Double> SPECIES = DoubleVector.SPECIES_PREFERRED;
    public static void main(String[] args)
    {
        VectorAPI instance = new VectorAPI();
        double[] array = new double[] { 1.0d, 2.0d, 3.0d };
        double[] result = new double[array.length];
        instance.calculate_by_vector(array, array, result);
        Arrays.stream(result).forEach(f -> System.out.print(f));
    }
    private void calculate_by_vector(double[] array1, double[] array2, double[] result)
    {
        for(int i = 0; i < array1.length; i+= SPECIES.length())
        {
            VectorMask<Double> mask = SPECIES.indexInRange(i, array1.length);
            DoubleVector vector1 = DoubleVector.fromArray(SPECIES, array1, i, mask);
            DoubleVector vector2 = DoubleVector.fromArray(SPECIES, array2, i, mask);
            DoubleVector resultVector = vector1.mul(vector1).add(vector2.mul(vector2)).neg();
            resultVector.intoArray(result, i, mask);
        }
    }
}

警告:実験的なモジュールを使用しています: jdk.incubator.vector
警告1個
WARNING: Using incubator modules: jdk.incubator.vector
-2.0 -8.0 -18.0

JEP427: switch文パターンマッチ(プレビュー)

switch文は更に機能拡張されるようです。データ型による分岐に加えてその値を評価する機能が追加されています。複雑になりすぎて使いこなせない,あるいは,バグが紛れ込みやすくなってしまわないか心配されます。

import java.util.ArrayList;
import java.util.List;

public class SwitchPatternMatch
{
    public static void main(String[] args)
    {
        List<Object> list = makeList();
        for(Object obj : list)
        {
            System.out.printf("%s\t", (obj == null) ? "null" : obj.getClass().getName());
            switch(obj)
            {
                case null -> System.out.println("[A] null");
                case Integer i when i < 5 -> System.out.printf("[B] (%d)\n", i);
                case Integer i -> System.out.printf("[C] (%d)\n", i);
                case String s -> System.out.printf("[D] (\"%s\")\n", s);
                default -> System.out.printf("[E] (%s) \n", obj.toString());
            }
        }
    }
    private static List<Object> makeList()
    {
        List<Object> list = new ArrayList<>();
        list.add(null);
        list.add(0);
        list.add(1);
        list.add(10);
        list.add(20d);
        list.add(new String("Hello"));
        list.add(Boolean.TRUE);
        list.add(list);
        return list;
    }
}

ノート:SwitchPatternMatch.javaはJava SE 19のプレビュー機能を使用します。
ノート:詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
null [A] null
java.lang.Integer [B] (0)
java.lang.Integer [B] (1)
java.lang.Integer [C] (10)
java.lang.Double [E] (20.0)
java.lang.String [D] (“Hello”)
java.lang.Boolean [E] (true)
java.util.ArrayList [E] ([null, 0, 1, 10, 20.0, Hello, true, (this Collection)])

JEP428: 構造化並列API(インキュベーター)

マルチスレッドで動作させる場合に子スレッドを効果的に管理する仕組みのようである。StructuredTaskScopeというクラスが導入されており,子スレッドのうち1つでも失敗していた場合は全体を失敗として例外をスローするなどが行えます。

import java.util.concurrent.Callable;
import jdk.incubator.concurrent.StructuredTaskScope;
import jdk.incubator.concurrent.StructuredTaskScope.ShutdownOnSuccess;
import jdk.incubator.concurrent.StructuredTaskScope.ShutdownOnFailure;

public class StructuredTaskAPI
{
    public static void main(String[] args) throws Exception
    {
        successExample();
        try
        {
            failExample();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }
    private static void successExample() throws InterruptedException
    {
        try(StructuredTaskScope.ShutdownOnSuccess<String> scope = new StructuredTaskScope.ShutdownOnSuccess<>())
        {
            scope.fork(() -> { return "Hello World";});
            scope.fork(() -> { return "Bye";});
            scope.join();
            String result = scope.result(e -> new RuntimeException("発生しないはず"));
            System.out.println(result);
        }
    }
    private static void failExample() throws Exception
    {
        try(StructuredTaskScope.ShutdownOnFailure scope = new StructuredTaskScope.ShutdownOnFailure())
        {
            scope.fork(new Callable<String>() {
                @Override
                public String call() throws InterruptedException
                { 
                    Thread.sleep(30); 
                    return "Hello World";
                }
            });
            scope.fork(new Callable<Integer>() {
                @Override
                public Integer call() throws InterruptedException
                {
                    throw new RuntimeException("ダミーのエラー");
                }
            });
            scope.join();
            scope.throwIfFailed();
        }
    }
}

警告:実験的なモジュールを使用しています: jdk.incubator.concurrent,jdk.incubator.vector
警告1個
WARNING: Using incubator modules: jdk.incubator.vector, jdk.incubator.concurrent
Hello World
java.util.concurrent.ExecutionException: java.lang.RuntimeException: ダミーのエラー
at jdk.incubator.concurrent/jdk.incubator.concurrent.StructuredTaskScope$ShutdownOnFailure.throwIfFailed(StructuredTaskScope.java:1125)
at StructuredTaskAPI.failExample(StructuredTaskAPI.java:51)
at StructuredTaskAPI.main(StructuredTaskAPI.java:13)
Caused by: java.lang.RuntimeException: ダミーのエラー
at StructuredTaskAPI$2.call(StructuredTaskAPI.java:47)
at StructuredTaskAPI$2.call(StructuredTaskAPI.java:43)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at java.base/java.lang.VirtualThread.run(VirtualThread.java:287)
at java.base/java.lang.VirtualThread$VThreadContinuation.lambda$new$0(VirtualThread.java:174)
at java.base/jdk.internal.vm.Continuation.enter0(Continuation.java:327)
at java.base/jdk.internal.vm.Continuation.enter(Continuation.java:320)

雑記

低レベルWebサーバ

WebAPI試験用のモックを作るついでに,Socketを使った低レベルのWebサーバをPythonで作りました。http://localhost:8080/*.htmlでアクセスするとindex.htmlファイルを返して,それ以外はNot Found(404)を返します。またhttp://localhost:8080/quitをリクエストするとサーバが停止します。

※セキュリティに関しては一切考慮していないので,インターネット公開するサーバでは実行しないでください。ディレクトリトラバーサルは簡単に実行できるでしょう。脆弱サーバの実験用です。

import re
import time
import socket

HOST = '127.0.0.1'
PORT = 8080
BUFFER_SIZE = 4096

def main():
  print("Server Listening")
  while True:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
      sock.bind((HOST, PORT))
      sock.listen(2)
      sock.settimeout(10)
      connection = None
      try:
        connection, address = sock.accept()
        print("connection from {} has been established!".format(address))
        recv = connection.recv(BUFFER_SIZE)
        print(recv)
        data = parse(recv.decode('UTF-8'))
        if data['url'] == '/quit':
          break
        response = create_response(data)
        print(response)
        connection.send(response.encode('UTF-8'))
      except TimeoutError as e:
        continue
      except Exception as e:
        print(str(e))
        response = format_response(500, "Internal Server Error", "text/plain",  str(e))
        if connection:
          connection.send(response.encode('UTF-8'))
      finally:
        if connection:
          connection.shutdown(socket.SHUT_RDWR)
          connection.close()
        time.sleep(1)

def parse(recv):
  array = recv.splitlines()
  data = {}
  if len(array) > 0:
    m = re.match(r'(GET|POST|PATCH|PUT|DELETE|HEAD)\s+(.*)\s+(.*)', array[0])
    if m:
      data['method'] = m.group(1)
      data['url'] = m.group(2)
      data['payload'] = array[len(array) - 1]
  return data


def create_response(data):
  if re.match(r'.*\.html', data['url']):
    with open('index.html', 'r', encoding='utf-8') as f:
      body = f.read()
      return format_response(200, "OK", "text/html; charset=utf-8", body)
  return format_response(404, "Not Found", "text/plain", "{} Not Found".format(data['url']))

def format_response(code, status, type, body):
  response = "HTTP/1.0 {} {}\nContent-Type: {}\n\n{}\n"
  return response.format(code, status, type, body);

if __name__ == '__main__':
  main()

簡単なhtmlファイルを置いて実行してみます。

<!DOCTYPE html>
<html lang="ja">
<head>
	<title>Hello</title>
</head>
<body>
	<h1>Hello World</h1>
</body>
</html>

Server Listening
connection from (‘127.0.0.1’, 55843) has been established!
b’GET /aaa.html HTTP/1.1\r\nHost: localhost:8080\r\nConnection: keep-alive\r\nsec-ch-ua: ” Not A;Brand”;v=”99″, “Chromium”;v=”101″, “Opera”;v=”87″\r\nsec-ch-ua-mobile: ?0\r\nsec-ch-ua-platform: “Windows”\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36 OPR/87.0.4390.45\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nSec-Fetch-Site: none\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-User: ?1\r\nSec-Fetch-Dest: document\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: ja,en-US;q=0.9,en;q=0.8\r\n\r\n’
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8

<!DOCTYPE html>
<html lang=”ja”>
<head>
<title>Hello</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>


connection from (‘127.0.0.1’, 55848) has been established!
b’GET /favicon.ico HTTP/1.1\r\nHost: localhost:8080\r\nConnection: keep-alive\r\nsec-ch-ua: ” Not A;Brand”;v=”99″, “Chromium”;v=”101″, “Opera”;v=”87″\r\nsec-ch-ua-mobile: ?0\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36 OPR/87.0.4390.45\r\nsec-ch-ua-platform: “Windows”\r\nAccept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\nSec-Fetch-Site: same-origin\r\nSec-Fetch-Mode: no-cors\r\nSec-Fetch-Dest: image\r\nReferer: http://localhost:8080/aaa.html\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: ja,en-US;q=0.9,en;q=0.8\r\n\r\n’
HTTP/1.0 404 Not Found
Content-Type: text/plain

/favicon.ico Not Found

情報セキュリティ

CSPヘッダとFacebook SDK

近年はXSS対策などのためサーバ上でCSPヘッダを付加することが増えている。詳しいことはリンク先に書かれているが,簡単に言うとHTTPヘッダでjavascriptやStylesheetを読み込んで実行する対象を制限することができる。これによって,不正なスクリプトが実行されることを防止しようというものだ。

ところが,Facebookと連携させようとすると,CSPが邪魔をしてしまう。Facebookのsdkはjavascriptの中で更に別のjavascriptを読込み,動的にscriptタグをDOM内に追加するのである。CSPヘッダのscript-srcに’unsafe-inline’を指定することで実行できるのであるが,これではXSSに対して無防備となる。インラインのjavascriptには毎回nonce属性にランダムな文字列を生成してセットし,CSPヘッダにも’nonce-{設定したnonce値}’という文字列を設定した方がよい。ただし,Facebookのようにjavascript内で動的に生成されるscriptタグにnonce属性は設定されていない。そのためにブラウザで読み込まなくなってしまうのである。

CSPによりJavascriptがブロックされている

動かすためには,動的に追加されるscriptタグにnonceを設定する必要がある。facebookのjavascriptを書き換えられればよいのであるが,それはできない。どうするか迷った末に,scriptタグを追加する関数を書き換えてみることにした。

htmlのDOMからheadノードを取得して,そのappendChild()メソッドを書き換えて,子要素を追加する際に追加される子要素にnonce属性を追加してしまおうという作戦である。次のようにhead要素を取得(1行目)して,そのappendChildメソッドの参照を取得ておく(2行目)。そして,head要素のappendChildメソッドに新しくfunctionを定義する(3-6行目)。このfunctionの中では子要素にsetAttributeメソッドを使って”nonce”属性を設定する。その後,2行目で取得しておいた元々のappendChildメソッドの参照を使って,元の関数を呼び出す。

    head = document.getElementsByTagName('head')[0];
    _appendChild = head.appendChild;
    head.appendChild = function(child) {
      child.setAttribute('nonce', "<?php echo $nonce; ?>");
      _appendChild.apply(this, arguments);
    } 

このJavascriptを組み込んで再度Webブラウザで読み込むと,ブロックされずにJavascriptが実行された。


実験に用いたソースファイル(index.php)

<?php
function getNonce() {
  return base64_encode(openssl_random_pseudo_bytes(20)); 
}
function createCSPHeader($nonce) {
   $nonces = array( 
       'default-src' => "'self'",
       'img-src' => "'self' data:",
       'script-src' => "'unsafe-eval' 'strict-dynamic' 'nonce-".$nonce."'" ,
       'style-src' => "'nonce-".$nonce."'"
   );
   $csp = 'Content-Security-Policy: ';
   $delimiter = '';
   foreach($nonces as $key => $value) {
     $csp = $csp.$delimiter.$key.' '.$value;
     $delimiter = ';';
   }     
   return $csp;      
}
$nonce = getNonce();
$csp_header = createCSPHeader($nonce);
header($csp_header);
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <title>PHP TEST</title>
    <style type="text/css" nonce="<?php echo $nonce ?>">
    .container {
        background-color: #F3F3F3;
        width: 100%;
        height; 100%;
    }
    </style>
    <script nonce="<?php echo $nonce; ?>">
    window.fbAsyncInit = function() {
      FB.init({
        appId   :     'YOUR_APPLICATION_ID',
        cookie  :     true,
        oauth   :     true,
        version :     'v2.9'
      });
    }
    </script>    
    <script nonce="<?php echo $nonce; ?>">
    head = document.getElementsByTagName('head')[0];
    _appendChild = head.appendChild;
    head.appendChild = function(child) {
      child.setAttribute('nonce', "<?php echo $nonce; ?>");
      _appendChild.apply(this, arguments);
    } 
    </script>
    
</head>
<body>
    <div class="container">
        <h1>Hello World.</h1>
        <p>Nonce: <?php echo $nonce; ?></p>
        <p><?php echo $csp_header; ?></p>
    </div>
</body>
</html>
JDK

Java 18リリース

Java 18がリリースされました。新機能は以下のようなものがアナウンスされていますが,目立った機能追加はないようです。

https://openjdk.java.net/projects/jdk/18/

  • JEP-400 UTF-8がデフォルトに
  • JEP-408 シンプルウェブサーバ
  • JEP-413 Java APIドキュメントにおけるコードスニペット
  • JEP-416 メソッドハンドルを用いたコアリフレクションの再実装
  • JEP-417 ベクターAPI(3次インキュベーター)
  • JEP-418 インターネットアドレス解決SPI
  • JEP-419 外部関数と外部メモリAPI(2次プレビュー)
  • JEP-420 switchにおけるパターンマッチング(2次プレビュー)
  • JEP-421 削除のためのfinalize()非推奨

UTF-8がデフォルトに

他の言語では既に多くがUTF-8がデフォルトになっていますが,JavaにおいてもようやくUTF-8がデフォルトのエンコーディングになったようです。Javaでテキストファイルを扱う時には毎回エンコーディングを指定していました。今後はUTF-8を用いる場合は省略できるでしょう。標準入出力は対象外となっているので,コンソールの扱いには影響はなさそうです。

シンプルウェブサーバ

SDKに簡易なウェブサーバが同梱されるようになりました。jwebserverという新たなコマンドが導入されます。静的なファイルを提供するプロトタイピング,デバッグ,テスト用のサーバと説明されています。–helpオプションでヘルプを表示させることができます。

Java APIドキュメントにおけるコードスニペット

{@snippet …} によってJavaDocコメントにJavaコードを埋め込めるようになります。

メソッドハンドルを用いたコアリフレクションの再実装

java.lang.reflectパッケージのMethod, Constructor, Fieldの実装が見直されました。43%-57%高速化されたそうです。

ベクターAPI(3次インキュベーター)

SIMD命令により効率的にベクトル演算を行うことができるAPIのようです。

インターネットアドレス解決SPI

インターネット上のアドレスを解決するためのインタフェースを提供するAPIのようです。java.net.InetAddressというクラスが提供されています。

外部関数と外部メモリAPI(2次プレビュー)

JNIに替わる新たな外部関数を呼び出すAPIです。

switchにおけるパターンマッチング(2次プレビュー)

switch文が拡張されます。当初は数値のみという制約がありましたが,オブジェクトの型によって分岐できるよう拡張され,記述方法も変更されます。従来の書き方も可能なようです。次のような書き方ができます。

private void(Object arg) {
    switch(arg) {
        case String s -> System.out.println("String");
        case int[] a -> System.out.println("Integer Array");
}

削除のためのfinalize()非推奨

finalize()メソッドが非推奨になります。GCによりオブジェクトが破棄される直前に呼び出されるのですが,GCが行われるまで実行されず,そのタイミングが制御できないため使わない方がよいと言われていました。ついに削除されます。

雑記

ウクライナ軍支援

ウクライナ軍をサポートしているNGOへ寄付することができました。先日Patron経由で送金したものの,Patronの規約に違反するとしてアカウントが削除されてしまったようです(注1)。”TO CONTRIBUTE”というボタンから直接送金しようとしましたが,クレカの追加認証が通りませんでした。そこで,FONDYから送金してみたところ,ようやく送金できました。

https://savelife.in.ua/en/donate/


まずは上のURLへアクセスします。図の赤枠で囲んであるFondy.euというリンクをクリックします。

画面が表示されたら,右上のセレクトボックスで言語を選択します。日本語は選択肢にないので,英語を選びましょう。

英語の表示になったらクレジットカード情報とメールアドレス,金額を入力してCHECKOUTボタンを押します。2月25日のレートで1UAH = 3.8946JPYでした。


カード会社によっては追加の認証が入ります。成功するとPayment is approvedと表示されます。

注1)https://www.businessinsider.in/politics/world/news/patreon-took-down-an-account-run-by-one-of-ukraines-biggest-charitable-foundations-because-the-organization-uses-donations-to-supply-equipment-for-the-countrys-military/articleshow/89839104.cms