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 とラップしてくれています。

JDK 1.0 (Java 1.0.2) を入手したので、GitHub に置いておきました。

JDK 1.0 (Java 1.0.2) を入手したので、GitHub に置いておきました。 https://github.com/YujiSoftware/JDK1.0

なんと、Windows 10 上でも動きます。
あと、src.zip も展開して置いた ので、GitHub 上で読めます。

Java 考古学者のみなさま、ぜひご活用ください!

入手の経緯

前々から Java 1.0 を触ってみたいなーと思っていたのですが、OracleJava Archive にはなぜか Java 1.1 以降しか置いてありませんでした。

ところが、最近になってぐぐってみたら Reddit にこんな書き込みがあるのを発見しました。

On archive.org, I found Java Starter Kit 1.1 + JDK 1.0, which is a disc image of a CD that accompanied a book, and apparently includes JDK 1.0. I haven't check it myself though. It looks like you'll need something that can read an MDF file to view the disc image. Download of Java 1.0.2 : java

この書き込みに沿って、Internet Archive からイメージファイルをダウンロードし、展開したところ無事に Java 1.0 を入手できました。

再配布していいの?

展開したファイルの COPYRIGHT を読むとこんな記載がありました。

Sun grants to you ("Licensee") a non-exclusive, non-transferable license to use the HotJava and Java binary code versions (hereafter, "Binary Software") without fee. Licensee may distribute the Binary Software to third parties provided that the copyright notice and this statement appear on all copies.

Sun は、Hot Java および Java のバイナリコードバージョン(以下、「バイナリソフトウェア」)を無料で使用するための非独占的で譲渡不可能なライセンスをお客様(「ライセンシー」)に付与します。ライセンシーは、著作権表示とこの声明がすべてのコピーに記載されている場合に限り、バイナリソフトウェアを第三者に配布することができます。

JDK1.0/COPYRIGHTon GitHub

ということで、COPYRIGHT を含めれば1再配布しても問題ないようです。


  1. 著作権表示は COPYRIGHT の冒頭に記載されています。

jq の入っていない環境で、JSON をフォーマットする方法

JSON をフォーマットしたいけど、jq が入っていない!ということがよくあります。
そういう時は、RubyPHP を使いましょう!

Ruby の場合

ruby -rjson -e 'puts JSON.pretty_generate(JSON.parse(STDIN.read))'

↓ こんな風に表示されます!

[qiita@example ~]$ echo '{"key": {"format": ["json", "XML"]}}' \
    | ruby -rjson -e 'puts JSON.pretty_generate(JSON.parse(STDIN.read))'
{
  "key": {
    "format": [
      "json",
      "XML"
    ]
  }
}

また、JSON.parse(STDIN.read)["key"] とすることで、特定の項目だけ表示することもできます。

[qiita@example ~]$ echo '{"key": {"format": ["json", "XML"]}}' \
    | ruby -rjson -e 'puts JSON.pretty_generate(JSON.parse(STDIN.read)["key"])'
{
  "format": [
    "json",
    "XML"
  ]
}

PHP の場合

php -r 'echo json_encode(json_decode(stream_get_contents(STDIN)), JSON_PRETTY_PRINT);'

↓ こんな風に表示されます!

[qiita@example ~]$ echo '{"key": {"format": ["json", "XML"]}}' \
    | php -r 'echo json_encode(json_decode(stream_get_contents(STDIN)), JSON_PRETTY_PRINT);'
{
    "key": {
        "format": [
            "json",
            "XML"
        ]
    }
}

また、json_decode(stream_get_contents(STDIN))->key とすることで、特定の項目だけ表示することもできます。

[qiita@example ~]$ echo '{"key": {"format": ["json", "XML"]}}' \
    | php -r 'echo json_encode(json_decode(stream_get_contents(STDIN))->key, JSON_PRETTY_PRINT);'
{
    "format": [
        "json",
        "XML"
    ]
}

M5StickC と MH-Z19B を使って、二酸化炭素濃度計を作りました

ネットの情報をもとに、M5StickC とMH-Z19B (CO2センサー)を使って二酸化炭素濃度計を作りました。 作ったといっても、M5StickC と MH-Z19B をつないで、さくっとプログラミングしただけ。簡単でした。

一年ぐらい運用していますが、なかなか便利です。 P_20200525_095646_vHDR_Auto.jpg

グラフ表示モードも作ったので、こんな風に直近の濃度の変化もわかります。 P_20200525_095559_vHDR_Auto.jpg

ソースコードはこちら https://gist.github.com/YujiSoftware/9274366a93f1ac7f9208bd4abf096527

実装した機能

  • 現在の二酸化炭素濃度を表示
  • 過去1時間ぐらいの二酸化炭素濃度の推移をグラフで表示
  • 1,200ppm 1を超えたときは、LED 点滅してお知らせ
  • 画面表示のオン・オフ

用意したもの

  • M5StickC(1,980円)
  • CO2センサー MH-Z19B2 (2,019円)
  • ジャンプワイヤ

MH-Z19B は AliExpress で買いました。 買うときに、取り付けが簡単かなと思って「MH-Z19B with Cable」を選んだのですが、これはおススメしません。というのも、このピンと M5StickC をつなぐようなケーブルというのはない3らしく、コネクタとコンタクトピンを買ってきてケーブルを自作する羽目になりました。部品が細かくて、とてもめんどくさかったです…。

「MH-Z19B with Pin」ならば、オス-メスのジャンプワイヤ でつなぐだけだと思うので、こちらの方がよさそうです。

作り方

M5StickC と MH-Z19B のピンを以下の組み合わせでつなぎます。

M5StickC MH-Z19B
G36 Tx
G26 Rx
5V Vin
GND GND

あとは、Arduino IDE で冒頭のソースコードを M5StickC に流し込んで完成です。

感想

市販の二酸化炭素計と違って、自分で好きなようにプログラミングできるのが面白いです。

M5StickC なら、いろいろな機能(LED、LCD画面、ボタン、WiFiBluetooth、バッテリー など)が All in One で載っているので、難易度もかなり低いです。 みなさんにも、ぜひおススメです。


  1. 1,200ppm を超えると眠気を感じるといわれています。

  2. 後継機 MH-Z19C が出ているそうです。

  3. マルツ秋葉原本店で聞いて「ない」と言われました。