JJUG CCC 2021 Fall ( #jjug_ccc ) - セッション資料の一覧

JJUG CCC 2021 Fall に参加しました!

JJUG CCC オンラインも3回目ということで、JJUG運営の方も参加者の方もお互い慣れてきた感じがします。 自分も、今回は夕方から用事があったので、時間差でセッションを見ながら電車で移動ということをやりました。ラジオの生放送と Radiko のオンデマンドの組み合わせみたいな感じで、リアルタイムでみんなで一緒にわいわいできるのも、時間と場所にとらわれずに見れるのもどっちもいいなーと思うようになりました。

だから、オフラインで集まりつつ1、今回のように配信でも見れるといいのかな、と。
コストが不安になりますがw

セッションは、最近のお仕事に関連しそうな「エキサイトブログ刷新に向けて序章」など、パフォーマンス改善やリファクタリングの話を中心に聞いていました。 教科書的な話だけでなく、なぜか歴史的経緯で不思議な構成になっているという話に「あるあるw」と共感したり、現状をもとにした工夫を聞いて何か自分の仕事にも取り入れられないかと考えたりと、いつものことながら楽しく役に立つ時間でした。


さて、最後にいつものを。

今回、残念ながら時間がかぶってしまって参加できなかったセッションがいっぱいあったので、あとで読むために現時点で発表者の方が公開されている資料一覧をまとめしました。2
(あとで JJUG CCC 2021 Fall のページにもリンクが載るかもしれませんが、とりあえず自分の方で調べました)

10:00

11:00

12:30

13:30

15:00

16:00

17:00

  • A: Getting the Most from Modern Javaf:id:chiheisen:20210523185503p:plain:w16:h16 動画) / Simon Ritter
  • B: Building for Resiliency, Fault-Tolerance, Scalability and Your User's Expectation(f:id:chiheisen:20210523185503p:plain:w16:h16 動画) / Chris Engelbert
  • D: Javaとコミュニティの歩み / 日本Javaユーザーグループ 鈴木雄介/杉山貴章

過去記事


  1. オンラインカンファレンス会場 reBako.io があったものの、うまく使いこなせず…。

  2. 発表者のお名前は敬称略とさせていただきました。

Outlook のメールファイル(.msg ファイル)をテキストに展開するツールを用意しました

たまに、Outlook のメールファイル(.msg ファイル)が添付で送られてくるのですが、手元に Windows 版の Outlook がなくて1内容を確認できなくて困ることが多々…。

そこで、.msg ファイルをテキストファイルに展開するツールを用意しました。
(添付ファイルも展開できます)

YujiSoftware/msg2txt: Extract .msg file (Outlook mail file) to a text file.

インストーラ版の方には Java ランタイムが含まれていますので、どの環境でもすぐに使えます。
自分のお気に入りの Java ランタイムを使いたいという場合は、スタンドアローン版をご利用ください! 2

実装について

Apache POI-HSMFJava API To Access Microsoft Outlook MSG Files)のサンプルプログラムをビルドしただけです。

頑張ったところ

それだけだと面白みがないので、Java 15 で正式リリースされた Packaging Tool を使って、インストーラ版も作ってみました。
… といっても、適切な引数を指定して jpackage コマンドを実行するだけでいい感じのインストーラができあがるので、とても簡単でした。

なお、手元に macOS のビルド環境はないので、インストーラの作成には GitHub Actions を使いました。3
runs-on: macOS-latest と指定するだけでいい感じの環境でビルドしてくれるので、マニュアル通りに書いていくだけでサクッと使えました。
これ、便利ですね(いまさら感)。

まとめ

というわけで、ぜひご利用ください!


  1. macOS 版の Outlook だと、.msg ファイルを開けないです。

  2. スタンドアローン版は Java 8 以上に対応しています。

  3. 作れはしたけど、動作確認ができていないです…。どなたか、サンプルファイルを展開できるかどうか確認していただけると嬉しいです。

いつから touch コマンドでファイルを作れるようになったのか

答え: 最初から

最初の touch コマンド

FreeBSD の touch のマニュアル曰く、touch コマンドは Version 7 の AT&T UNIX で登場したそうです。

HISTORY A touch utility appeared in Version 7 AT&T UNIX. touch(1) - FreeBSD Manual Pages

このときの man を読むと、すでに「ファイルが存在しなければ作成する」ということが記載されています。

DESCRIPTION

Touch attempts to set the modified date of each file. This is done by reading a character from the file and writing it back. If a file does not exist, an attempt will be made to create it unless the -c option is specified.

Touchは、それぞれのファイルの修正日時の設定を試みます。 これは、ファイルから文字を読み取り、それを書き戻すことによって行われます。 ファイルが存在しない場合、-c オプションが指定されていない限り、ファイルの作成を試みます。

unix-history-repo/touch.1 at Research-V7 · dspinellis/unix-history-repo · GitHub

ソースコード1を読むと、たまたまファイルがなければ作るようになったのではなく、あえてそのように実装していたことが分かりました。
具体的には、まず stat(3) でファイルの存在を確認して、なければ create(3) でファイルを作るという実装をしていました。 unix-history-repo/touch.c at Research-V7 · dspinellis/unix-history-repo · GitHub

#include <sys/types.h>
#include <sys/stat.h>

touch(force, name)
int force;
char *name;
{
struct stat stbuff;
char junk[1];
int fd;

if( stat(name,&stbuff) < 0)
    if(force)
        goto create;
    else
        {
        fprintf(stderr, "touch: file %s does not exist.\n", name);
        return;
        }

if(stbuff.st_size == 0)
    goto create;

if( (fd = open(name, 2)) < 0)
    goto bad;

if( read(fd, junk, 1) < 1)
    {
    close(fd);
    goto bad;
    }
lseek(fd, 0L, 0);
if( write(fd, junk, 1) < 1 )
    {
    close(fd);
    goto bad;
    }
close(fd);
return;

bad:
    fprintf(stderr, "Cannot touch %s\n", name);
    return;

create:
    if( (fd = creat(name, 0666)) < 0)
        goto bad;
    close(fd);
}

では、なぜファイルが存在しない場合にエラーにするのではなく、ファイルを作るようにしたのでしょうか。

それは、わかりませんでした(ぇ

まとめ

いかがでしたでしょうかw

ソースコードにコメントはなく、なぜこのような仕様にしたのかはこのコードを書いたDennis M. Ritchie2しか知る由がなさそうです。ただ、2011年に亡くなられたので、もはや聞くことはできません…。

おそらく、その方が便利だからだとは思うのですが、確証がありません。
もしかしたら本か何かに、その意図が残されているかもしれません。
ご存じの方がいましたら、情報をお寄せいただければと思います。


  1. たったの70行しかありません。

  2. C言語の生みの親。当時はベル研究所に所属していた。

JJUG CCC 2021 Spring ( #jjug_ccc ) - セッション資料の一覧

JJUG CCC 2021 Spring に参加しました!

前回に引き続き、今回もオンライン開催となりました。 オンラインだと移動が楽というメリットがあって、これはどっちのセッション見ようかなーというときに、途中でさっとほかの動画に移れるのは便利でした。
あとは、家なので適当にお菓子つまんだり休憩時間中にちょっと横になって昼寝したりと、気軽に楽しめました。
(オフラインと違い、コーヒースポンサーがないのが残念w)

今回、一番おもしろかったのはとださんの「Java8〜16におけるバイトコード生成の変化」でした。
コンパイラーが変なクラスファイル吐くけど⇒Java言語仕様に則っているよ!とか、短いながらもへぇ!という内容が満載でした。
JJUG CCC は、こういう Java の奥深い話が聞けるのが個人的に大好です。

次回の秋も、オンライン開催予定とのことです(日程未定)。
2022年の JJUG CCC こそは、オフラインもできるようになっているといいですね…。また懇親会でお話したいです。


さて、最後にいつものを。

今回、残念ながら時間がかぶってしまって参加できなかったセッションがいっぱいあったので、あとで読むために現時点で発表者の方が公開されている資料一覧をまとめしました。1
(あとで JJUG CCC 2021 Spring のページにもリンクが載るかもしれませんが、とりあえず自分の方で調べました)

10:00

11:00

  • A: 今どき?のJavaにおける例外処理についての考察(f:id:chiheisen:20210523185503p:plain:w16:h16 動画) / 松下正嗣
  • B: Java 開発者のための Kubernetes パッケージマネージャー: Helm(f:id:chiheisen:20210523185503p:plain:w16:h16 動画) / 三宅 剛史
  • C: Jakarta EE 9.1とパッケージ名変更のためのツールEclipse Transformerについて(f:id:chiheisen:20210523185503p:plain:w16:h16 動画) / 髙橋 博実
  • D: レガシーなシステムにモダンなAPIを導入した話(f:id:chiheisen:20210523185503p:plain:w16:h16 動画) / Koji Kishiura

12:00

13:00

14:00

15:00

16:00

17:00

  • A: Plug-in Architectures with the Java Module System(f:id:chiheisen:20210523185503p:plain:w16:h16 動画) / Andres Almiray
  • B: Migrate Spring Boot app to Quarkus. Stage unlocked(f:id:chiheisen:20210523185503p:plain:w16:h16 動画) / Jonathan Vila
  • C: Framewars: the battle between NoSQL and Java in the cloud a / Otavio Santana
  • D: How Should Java Developers Build Front-Ends Today?(f:id:chiheisen:20210523185503p:plain:w16:h16 動画) / Karsten Silz

過去記事


  1. 発表者のお名前は敬称略とさせていただきました。

「正しい番号が入力されていること」という難しさ

バリデーションの難しさについて、少し考えてみましょう。
(結論だけ読みたい人は、こちら

例えば、通販サイトの基本設計書で「入力フォームの電話番号の項目に、正しい電話番号が入力されていない場合はエラーとすること」と書かれていたとします。
では、正しい電話番号が入力されているかどうかの判定とは、どのように実装すればいいのでしょうか。

「正しい電話番号」とは?

電話番号とはどのようなものでしょうか。
総務省のホームページに分かりやすくまとめられていました。1
総務省|電気通信番号制度|電話番号に関するQ&A

この中から一般的2に使われている市外局番から始まる電話番号3、または携帯電話4の番号で検討を進めます。
上記のページから関連する部分をピックアップすると、このようになっています。

◆電話番号とは、通話の相手を識別するために使われる、0から9までの数字を組み合わせた番号です。

「0A0から始まる番号(Aは0以外)」 ◆携帯電話とPHS「070」、「080」又は「090」から始まる11桁の番号です。

「0ABCから始まる番号(A、B、Cは0以外)」 ◆普通の固定電話の電話番号は、次のようになっています。 固定電話の桁数 国内プレフィックス「0」市外局番「1~4桁」市内局番「1~4桁」加入者番号「4桁」 ※市外局番と市内局番は合計5桁 5

これを基にすると、(ハイフンを取り除いて)以下の正規表現にマッチすれば「正しい電話番号」と言えそうです。

((090|080|070)[0-9]{11}|0[0-9]{9})

しかし、この判定には問題があります。

有効な電話番号の判定

例えば、"0998-76-5432" は正しい番号番号と判定します。
しかし、今日時点で 998 という市外局番は存在しません。そのため、この電話番号は存在しない無効なものだと分かります。
つまり、厳密に正しい番号かどうかを判定するには、市外局番が存在するものかどうかも判定しなければいけません。

また、"060-4321-1234" は正しくない電話番号と判定します。
ですが、060 は携帯電話への割り当てが検討されており6、これはいずれ有効になる番号です。これを、正しくないものとしてエラーにしてしまうと、今後改修が必要になってしまいます。

しかも、ここまで考えても、まだ使われていない無効な電話番号という可能性が残されています。
「正しい番号 = 存在する有効な番号」なのかは微妙なところですが世間一般としてはそういう認識のようです。

存在する有効な番号の判定

では、「正しい番号 = 存在する有効な番号」を判定するにはどうすればいいのでしょうか。
解決策は、実際にその電話番号にかけてみるということです。

これならば、存在する有効な番号であることが確認できます。
銀行などは、実際にこの方式をとっています。

ただし、単純に実装すると脆弱性となります。
本の虫: ダイヤルQ2風の電話番号でInstagramやGoogleやMicrosoftから金をむしりとれる脆弱性

国内ではダイヤルQ2が終了しているので、このままの方法は取れません7が、ひたすら電話をかけさせて相手に莫大な電話料金を支払わせる、他人の番号を入力して DDoS させるといったことが考えられます。
このような可能性を考慮して、慎重に実装を行う必要があります。

簡単な判定

しかし、ここまで厳密な判定は必要なのでしょうか。
「存在しうる番号かどうかの判定」や「有効な電話番号の判定」を行うのはとても大変です。実装にも時間もかかります。

一番手っ取り早く、確実な方法は「正しい電話番号が入力されていること」という判定をしないことです。
シンプルに「数字のみが入力されていること」だけ判定すれば、考えなくてはいけないことがぐっと減ります。

その代わり、架空の番号で登録できてしまうことを許容する必要があります。

どこまでを求めるかは、要件次第です。

ちなみに

「正しい電話番号の判定」はこれでも簡単な方です。
なぜなら、電話局という(仮想的な)一つのデータベースに問い合わせすれば、存在する正しい番号かどうかわかるからです。

もし複数のデータベースに分散していて、それぞれに問い合わせる必要がある場合は、とても大変です。
分散しているすべてのデータベースに接続する必要があるためです。もしくは、番号を一つのデータベースに集約するという方法もありますが、これだと更新をどのように行うかを考えなくてはいけません。

例えば、ワクチンの接種番号は全国の市町村(今日時点で 1,7188)が管理しています。東京都区部に限っても23の区がそれぞれ管理しています。 これが正しいかどうかを確認するには、すべての市町村のデータベースと接続するか、番号を集約し常に更新し続ける必要があります。 参考:新型コロナウイルスワクチンに係る接種券等の印刷及び発送について - 総務省

それをやろうと思うと、時間も費用もかかると思います。


  1. 仕様としては ITU-TE.164 勧告ですが、これは国別割り当て電話番号「国番号+国内電話番号を合わせた最大15桁」とだけ定められており、国内電話番号の部分(090-XXXX-XXXX、03-XXXX-XXXX など)は各国の方針となっています。

  2. 119 や 110 といった 1XY 特番、フリーダイヤルといった電話サービスの番号は除く

  3. 中継する電話会社を明示する番号を指定しない番号

  4. 衛星携帯電話は、国際電話扱いのようなのでここでは検討しない。衛星電話(インマルサット)あての電話のかけ方を教えてください。【インマルサット】

  5. 市外局番と市内局番の合計が4桁の地域もありましたが、現在はなくなりました。 9桁の電話番号 ‐ 通信用語の基礎知識

  6. FMC(Fixed-Mobile Convergence)にすでに使われていますが、利用が伸びないので携帯電話にも使用しようという見当が行われています。 携帯電話の「060」がまもなく開放 そもそも「090」「080」「070」の違い、知ってる?|TIME&SPACE by KDDI

  7. 情報料代理徴収サービスの一種である災害募金サービスは残っているので、意図しない募金を行うということは可能です。

  8. 総務省|地方自治制度|広域行政・市町村合併

Javaのバージョン別、テキストファイルを一括で読み込む方法まとめ

前回の記事(Javaのバージョン別、1行ずつファイルを読む方法まとめ)への感想で、このような話がありました。

というわけで、今回はテキストファイルを一括で読み込む方法をまとめました。
(前回と被っている点は省略しているので、まだ読んでない人は先に前回の記事をどうぞ)

Java 1.1, 1.2, 1.3

private static String readString(File file) throws IOException {
    Reader reader = null;
    try {
        reader = new InputStreamReader(new FileInputStream(file), "UTF-8");

        StringBuffer sb = new StringBuffer();
        int len;
        char[] buffer = new char[1024 * 8];
        while ((len = reader.read(buffer)) != -1) {
            sb.append(buffer, 0, len);
        }
        return sb.toString();
    } finally {
        if (reader != null) {
            reader.close();
        }
    }
}

まだこのころはめんどくさいです。

FileInputStream でファイルを読み込み、InputStreamReader で指定した文字コードにデコードするようにしています。
読み込む際は、char[] でバッファして StringBuffer に追加していっています。

ちなみに、char[] でバッファしながら読み込んでいるので、 BufferedReader でラップする必要はありません。

Java 1.4, 1.5, 6

private static String readString(File file) throws IOException {
    CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();

    try (FileInputStream stream = new FileInputStream(file)) {
        FileChannel channel = stream.getChannel();
        ByteBuffer bb = channel.map(MapMode.READ_ONLY, 0, channel.size());

        CharBuffer cb = decoder.decode(bb);

        return cb.toString();
    }
}

ループがなくなって、だいぶ処理がすっきりしました。

このバージョンで追加された Charset クラスから、CharsetDecoder というデコーダを取得できます。
これに FileChannel#map​(FileChannel.MapMode mode, long position, long size) で取得した MappedByteBuffer を渡すことで、ファイルを読み込めます。

MappedByteBuffer はファイルの内容をメモリにマッピングして扱う(メモリマップドファイル)ので、大きなファイルの時に効率的に処理が行われます。
逆に言うと、小さいファイルを読み込む場合は性能が悪いです。
その場合は、今まで通り InputStream で読み込みましょう。

ほとんどのオペレーティング・システムでは、ファイルをメモリーマッピングするほうが、通常のreadメソッドまたはwriteメソッドを使って数十キロバイトのデータの読み込みまたは書込みを行うよりも負荷が大きくなります。 性能を重視するなら、比較的大きめのファイルだけをマッピングすることをお薦めします。 FileChannel (Java SE 15 & JDK 15))

----
ちなみに、この FileChannelmap メソッドを使うやり方はひしだまさんの記事を読んで知りました!
Javaバッファークラスメモ(Hishidama's Java Buffer Memo)

Java 7, 8, 9, 10

private static String readString(Path path) throws IOException {
    byte[] bytes = Files.readAllBytes(path);
    
    return new String(bytes, StandardCharsets.UTF_8);
}

Files クラスが追加され、これに一括でファイルを byte[] として読み込むメソッドが用意されました。
これを使ってファイルを読み込めば、あとは String クラスを new するだけです。

ちなみに、このバージョンでファイルからすべての行を読み取る Files#readAllLines​(Path, Charset) も追加されました。
この戻り値は List<String> です。行ごとに処理をしたい場合はこちらが便利です。

Java 11~

private static String readString(Path path) throws IOException {
    return Files.readString(path, StandardCharsets.UTF_8);
}

ひとつのメソッドでファイルをすべて読み込めるようになりました。 1

このメソッドの実装では StringCoding というインターナルなクラスを使っているので、new String(bytes, StandardCharsets.UTF_8) とするよりも効率的な処理になっています。

まとめ

めんどくさかった処理も、Java のバージョンが上がるごとに簡単に書けるようになっています。
なので、積極的に新しい Java を使っていきましょう!

関連記事


  1. Google で検索して一番上に出てくる JavaDocJava 8 なせいで、この Java 11 で追加されたメソッドを忘れられがちです…。

Javaのバージョン別、1行ずつファイルを読む方法まとめ

Java でファイルを読み込む処理は、バージョンが上がるごとにどんどん簡単に書けるようになっていきました。
今回は、どれだけ簡単になっていったかを Java のバージョンごとにまとめて説明します。

なお、ここでは以下の処理を行うコードをもとにしています。

  • そこそこ大きいテキストファイルを一行ずつ読み込む
  • 文字コードUTF-8

Java 1.1 ~ 1.3

public static void main(String[] args) throws IOException {
    File file = new File(args[0]);
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(
                    new InputStreamReader(
                            new FileInputStream(file)
                            , "UTF-8"));
        
        String line;
        while((line = reader.readLine()) != null) {
            // 処理
        }
    } finally {
        if (reader != null) {
            reader.close();
        }
    }
}

長いですね。

この時代に FileReader というファイルからテキストを読み込むクラスがあるにはあります。
ただ、このクラスでは 問答無用でシステムデフォルトの文字コードが使われてしまいます。
そのため、Windows から Linux に持っていった場合に文字化けが起きる問題がよく起こっていました。

それを避けるためには、文字コードの指定ができる InputStreamReader を使う必要があります。
InputStreamReader 自体ではファイルの読み込みができないので、FileInputStream を使う必要があります。

さらに、一行ずつ読み込むためには BufferedReader を使う必要があります。これ自体はバッファしかしてくれません。

なので、結果として FileInputStreamInputStreamReaderBufferedReader とラップしていく必要があります。

Java 1.4 ~ 1.5

public static void main(String[] args) throws IOException {
    File file = new File(args[0]);
    Charset charset = Charset.forName("UTF-8");

    BufferedReader reader = null;
    try {
        reader = new BufferedReader(
                    new InputStreamReader(
                        new FileInputStream(file), charset));

        String line;
        while ((line = reader.readLine()) != null) {
            // 処理
        }
    } finally {
        if (reader != null) {
            reader.close();
        }
    }
}

Charset クラスができました。
これにより、文字コードを型で表せるようになりました。

ただ、UTF-8 を使う場合には Charset.forName("UTF-8") とする必要がありました。
なので、型安全ではあるけれど、文字列で指定した方が楽という状況でした。

Java 7

public static void main(String[] args) throws IOException {
    Path path = Paths.get(args[0]);
    Charset charset = StandardCharsets.UTF_8;

    try (BufferedReader reader = Files.newBufferedReader(path, charset)){
        String line;
        while ((line = reader.readLine()) != null) {
            // 処理
        }
    }
}

一気にシンプルになりました。

まず、StandardCharsets クラスが追加されました。これにより、わざわざ標準の文字コード1であれば文字列で指定せずに済むようになりました。
また、Files クラスと newBufferedReader(path, charset) メソッドが追加され、いちいちいろんなクラスを new する必要がなくなりました。2

あと、このバージョンで Path クラスが追加されました。
ファイルを読み込むだけだと File クラスと大差ないですが、パスの操作が簡単になりました。

さらに、try-with-resources が追加され、めんどくさかった close 処理をいい感じにやってくれるようになりました。

----
ちなみに…。 このバージョンから まとめて読み込んで List<String> に格納してくれる Files.readAllLines​(Path, Charset) も使えるようになりました。 ただし、これは大きなファイルに使うとメモリを食ってしまうので要注意です。

Java 8 ~ 10

public static void main(String[] args) throws IOException {
    Path path = Paths.get(args[0]);

    try (Stream<String> stream = Files.lines(path)){
        stream.forEach(line -> /* 処理 */);
    }
}

Stream クラスによって、行の読み込みと処理を反復させることができるようになりました。
これのおかげで、1行ずつ処理をしたいという場合にとてもシンプルに書けるようになりました。

また、Files クラスを使う際に、UTF-8 であれば文字コードの指定を省略できるようになりました。
InputStreamReader などは省略時にシステムのデフォルト文字コードが指定されたものとみなされますが、こちらは UTF-8 を指定したものとみなされる という点には注意が必要です。 ただ、最近のテキストファイルが UTF-8 であることを踏まえると、妥当な判断かなと思います。

Java 11 ~

public static void main(String[] args) throws IOException {
    Path path = Path.of(args[0]);

    try (Stream<String> stream = Files.lines(path)){
        stream.forEach(line -> /* 処理 */);
    }
}

Path.of​(String first, String... more) というメソッドが追加されました。
内部の処理は Paths.get(String first, String... more) とまったく同じですが、このおかげで Path クラスを使うにはどうすればいいんだっけと迷わずに済むようになりました。

----
上記のサンプルでは使っていないのですが…。 なんと、このバージョンで FileReader クラスのコンストラクタで Charset が指定できるようになりました。 Java 1.1 のときに欲しかった…。

なお、InputStreamReader クラスのように文字コードを "UTF-8" のような文字列で指定することはできません。 既存のクラスは互換性のために文字列でも Charset でも指定できるようになっていますが、今後追加されるクラスは Charset のみになるようです。

まとめ

Java はファイルを読み込むだけでもなんて面倒くさいんだと言われていました。 でも、それは昔のお話。

今ではとても簡単になっているんだよ、というのが伝われば幸いです。


  1. US-ASCII, ISO-8859-1, UTF-8, UTF-16BE, UTF-16LE, UTF-16。これらは、どの Java 実装でも必ずサポートされています。

  2. Files.newBufferedReader の中で、FileInputStreamInputStreamReaderBufferedReader とラップしてくれています。