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. マルツ秋葉原本店で聞いて「ない」と言われました。

Java の + 演算子による文字列結合は、どのように処理されているのか

Java 9 以降 JEP 280: Indify String Concatenation に基づき、 + 演算子による文字列結合は以下のようにコンパイルされるように変わりました。

invokedynamic #7,  0
// InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/Object;I)Ljava/lang/String;

(InvokeDynamic を使うように変更された理由は、「Java の + 演算子による文字列結合で、StringBuilder は使われなくなりました。」を参照)

この InvokeDynamic によって、最終的にどのように文字列結合が行われているのでしょうか。

環境

Adopt OpenJDK 15 で調査しました。

openjdk version "15" 2020-09-15
OpenJDK Runtime Environment (build 15+36-1562)
OpenJDK 64-Bit Server VM (build 15+36-1562, mixed mode, sharing)

前提知識

Java 9 以降 JEP 254: Compact Strings に基づき、String クラスの内部で文字を以下のように保持するように変わりました。

  • すべての文字が LATIN1 の範囲内であれば、LATIN1(1文字 = 1byte)
  • (日本語など)LATIN1 の範囲外の文字があれば、UTF-16 (1文字 = 2byte)

これについての詳細は、下記の記事でご確認ください。 Java9 でも String クラスがリファクタリングされていました (JEP 254: Compact Strings 編) - 地平線に行く

処理の概要

invokedynamic #7,  0
// InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/Object;I)Ljava/lang/String;

この部分が最初に実行された時、ブートストラップメソッド StringConcatFactory.makeConcatWithConstants によって、文字列結合を行う匿名メソッドが作られます。 そして、以降は InvokeDynamic の代わりにその匿名メソッドが呼ばれるようになります。

なお、最終的にこの匿名メソッドは呼び元のメソッドにインライン展開されます(たぶん…)。

匿名メソッドの処理

匿名メソッドは、以下の処理を行います。

  1. double, float, Object 型の変数を文字列に変換
  2. (最終的に出来上がる)文字数と文字コードを調べる
  3. 文字を格納するのに必要な長さの byte 配列を作成
  4. byte 配列に、文字を詰る
  5. byte 配列を文字列にする

つまり、文字列結合というのは、最終的に 文字を byte 配列に詰める処理 になります。

具体例

下記のソースコードを例に、具体的に見ていきます。

public static String call(Object foo, int bar) {
    return "arg0: " + foo + ", arg1: " + bar;
}

このメソッドをコンパイルしたクラスファイルを、 javap で逆アセンブルします。 すると、以下のように invokedynamic が使われていることが分かります。

{
  public static java.lang.String call(java.lang.Object, int);
    descriptor: (Ljava/lang/Object;I)Ljava/lang/String;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/Object;I)Ljava/lang/String;
         7: areturn
}
BootstrapMethods:
  0: #26 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #32 arg0: \u0001, arg1: \u0001

この invokedynamic は、ブートストラップメソッドとして、 StringConcatFactory#makeConcatWithConstants(MethodHandles.Lookup lookup, String name, MethodType concatType, String recipe, Object... constants) が指定されています。

ここで重要なのは、引数の concatType, recipe です。 今回は、以下の値が渡されています。

  • concatType
    • MethodType という型
    • 作成する匿名メソッドのシグネチャの情報が保持されている
      • この中の引数の情報 parameterArray に、結合する変数の型情報が保持されている
        • 今回の場合 [ java.lang.Object.class, int.class ]
  • recipe
    • 変数に置換する箇所を \0001 に、それ以外を結合した文字列
    • 今回の場合、 "arg0: \u0001, arg1: \u0001"

このブートストラップメソッドによって、以下の動的メソッドが作られます。

String anonymous(Object foo, int bar) {
    // 引数を文字列に変換
    String t4 = StringConcatHelper.stringOf(foo);

    // 文字数と文字コードを調べる
    long t6 = StringConcatHelper.mix(14L, bar);
    long t8 = StringConcatHelper.mix(t6, t4);

    // 必要なサイズの byte 配列を作成
    byte[] t10 = StringConcatHelper.newArray(t8);

    // 配列に文字列を詰める
    long t12 = StringConcatHelper.prepend(t8, t10, foo, ", arg1: ");
    long t14 = StringConcatHelper.prepend(t12, t10, t4, "arg0: ");

    // 配列を文字列にする
    return StringConcatHelper.newString(t10, t14);
}

これを細かく見ていきましょう。

文字列への変換

結合する変数が double, float, Object 型の場合、以下のメソッドを呼び出して文字列に変換します。

  • doubleString.valueOf(double)
  • floatString.valueOf(float)
  • ObjectStringConcatHelper.stringOf(Object)

なお、boolean, char, byte, short, int, long の場合、あとで直接 byte 配列に格納するため、ここで文字列に変換しません。

今回の例では、引数の Object 型の変数 fooStringConcatHelper.stringOf(Object) メソッドで文字列に変換しています。

String t4 = StringConcatHelper.stringOf(foo);  // args: Object

この StringConcatHelper.stringOf(Object) の実装はこのようになっています。

/**
 * We need some additional conversion for Objects in general, because
 * {@code String.valueOf(Object)} may return null. String conversion rules
 * in Java state we need to produce "null" String in this case, so we
 * provide a customized version that deals with this problematic corner case.
 */
static String stringOf(Object value) {
    String s;
    return (value == null || (s = value.toString()) == null) ? "null" : s;
}

引数が null ならば "null" という文字列を、引数が null でなければ value.toString() した文字列を返しています。

文字数と文字コードを調べる

次に、結合する各変数の文字数と文字コードを、StringConcatHelper#mix メソッドを使って後ろから順に調べていきます。 今回だと、bar → foo の順です。

// MEMO:
//   bar: 結合する int 型の変数
//   t4: 結合する Object 型の変数を文字列にしたもの (`foo.toString()`) 
long t6 = StringConcatHelper.mix(14L, bar);  // args: long, int
long t8 = StringConcatHelper.mix(t6, t4);  // args: long, String

StringConcatHelper#mix メソッドは、戻り値として「第一引数の値 + 第二引数で渡した変数の文字数(※)」を返します。 それを、次の第一引数に渡します。 (※ 正確には、後述の lengthCoder という、文字数と文字コードの両方を保持した値です。この点は、後述します)

なお、最初は recipe から \u0001 を取り除いた長さを渡します。今回は recipe = "arg0: , arg1: " なので、14 です。 匿名メソッドを作る際に計算しているので、ここでは定数になっています。

StringConcatHelper#mix メソッドの実装は、第二引数で渡す変数の型によって若干処理が異なります。 今回は、上記の例で使われている int 型と String1の処理をそれぞれ見ていきましょう。

int 型の場合

StringConcatHelper.mix(long, int) の実装はこのようになっています。

/**
 * Mix value length and coder into current length and coder.
 */
static long mix(long lengthCoder, int value) {
    return checkOverflow(lengthCoder + Integer.stringSize(value));
}

Integer.stringSize(int) というパッケージプライベートなメソッドを呼び出し、value を10進数の文字列にした時の長さを計算しています。

これを、第一引数と足した値を戻り値としています。 ただし、この値が int の範囲を超えていたら OutOfMemoryError("Overflow: String length out of range"); をスローします。

文字列型の場合

StringConcatHelper.mix(long, String) の実装はこのようになっています。

/**
 * Mix value length and coder into current length and coder.
 */
static long mix(long lengthCoder, String value) {
    lengthCoder += value.length();
    if (value.coder() == String.UTF16) {
        lengthCoder |= UTF16;
    }
    return checkOverflow(lengthCoder);
}

まず、文字列の長さを String#length() で取得し、lengthCoder に足しています。

次に、引数の文字列の文字コードを確認します。 もし文字コードUTF-16 ならば、lengthCoder の32ビット目を 1 にします。 このように「文字列の長さ」として使うのは long 型の下位32ビットだけで、32ビット目2はこのように文字コードUTF-16 かどうかのフラグとして使います。

そうしてできた値を、戻り値としています。 (オーバーフローの処理は先ほどと同様です)

必要なサイズの byte 配列を作成

// MEMO:
//   t8: さきほどの戻り値 (`lengthCoder`)
byte[] t10 = StringConcatHelper.newArray(t8);  // args: long

ここまでで確定した文字数と文字コードをもとに、byte 配列を作成しています。

この StringConcatHelper.newArray(long) の実装はこのようになっています。

/**
 * Allocates an uninitialized byte array based on the length and coder information
 * in indexCoder
 */
@ForceInline
static byte[] newArray(long indexCoder) {
    byte coder = (byte)(indexCoder >> 32);
    int index = (int)indexCoder;
    return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, index << coder);
}

必要な byte 配列の長さは、文字コードによって異なります。

  • LATIN なら 1文字 = 1byte なので、必要な byte 配列の長さ = 文字数
  • UTF-16 なら 1文字 = 2byte なので、必要な byte 配列の長さ = 文字数 × 2

その計算をやっているのが index << coder です。 coder には、LATIN1 なら 0UTF-16 なら 1 が格納されているので、UTF-16 の時だけ1ビット左シフト、つまり値が倍になるという仕組みです。

なお、配列の作成は new byte[length] ではなく、UNSAFE.allocateUninitializedArray(Class<?>, int) で行っています。 なぜかというと、new byte[length] だと配列を作った後に要素をゼロで初期化する処理を行ってしてしまうためです。 今回の処理では 必ず あとですべての要素に書き込むので、ゼロ初期化するのは無駄です。 そのため、アンセーフなメソッドを使ってゼロ初期化を省いて配列を作っています。

配列に文字列を詰める

出来上がった配列に、結合する各変数を StringConcatHelper#prepend メソッドを使って後ろから順に詰めていきます。 今回だと、bar → foo の順です。

// MEMO:
//   t8: さきほどの戻り値 (`lengthCoder`)
//   t10: 文字列を詰め込む byte 配列
//   bar: 結合する int 型の変数
//   t4: 結合する Object 型の変数を文字列にしたもの (`foo.toString()`) 
long t12 = StringConcatHelper.prepend(t8, t10, bar, ", arg1: ");  // args: long, byte[], int, String
long t14 = StringConcatHelper.prepend(t12, t10, t4, "arg0: ");  // args: long, byte[], String, String

StringConcatHelper#prepend メソッドは、第二引数で渡された byte 配列に第一引数で指定された位置のひとつ前から文字を詰め込みします。 詰め込む内容は、第三引数で渡された変数、および第四引数で渡された文字列です。

このように、結合する変数と\u0001 で区切った recipe をまとめて渡すことで、メソッドの呼び出し回数を削減しています。

StringConcatHelper#prepend メソッドの実装は、第三引数で渡す変数の型によって若干処理が異なります。 今回は、上記の例で使われている int 型と String 型[^primitive]の処理をそれぞれ見ていきましょう。

int 型の場合

StringConcatHelper.prepend(long, byte[], int, String) の実装はこのようになっています。

/**
 * Prepends constant and the stringly representation of value into buffer,
 * given the coder and final index. Index is measured in chars, not in bytes!
 */
static long prepend(long indexCoder, byte[] buf, int value, String prefix) {
    indexCoder = prepend(indexCoder, buf, value);
    if (prefix != null) indexCoder = prepend(indexCoder, buf, prefix);
    return indexCoder;
}

この prepend(long, byte[], int) の実装はこのようになっています。

/**
 * Prepends the stringly representation of integer value into buffer,
 * given the coder and final index. Index is measured in chars, not in bytes!
 */
private static long prepend(long indexCoder, byte[] buf, int value) {
    if (indexCoder < UTF16) {
        return Integer.getChars(value, (int)indexCoder, buf);
    } else {
        return StringUTF16.getChars(value, (int)indexCoder, buf) | UTF16;
    }
}

Integer.getChars または StringUTF16.getChars で、byte配列に変数の値を詰めています。 詳しい解説は省きますが、これらの中では面白い実装によって int 型から10進数文字列に変換しています。 → Tech Tips: Integer.getCharsが面白い

文字列型の場合

StringConcatHelper.prepend(long, byte[], String, String) の実装はこのようになっています。

/**
 * Prepends constant and the stringly representation of value into buffer,
 * given the coder and final index. Index is measured in chars, not in bytes!
 */
static long prepend(long indexCoder, byte[] buf, String value, String prefix) {
    indexCoder = prepend(indexCoder, buf, value);
    if (prefix != null) indexCoder = prepend(indexCoder, buf, prefix);
    return indexCoder;
}

この prepend(long, byte[], String) の実装はこのようになっています。

/**
 * Prepends the stringly representation of String value into buffer,
 * given the coder and final index. Index is measured in chars, not in bytes!
 */
private static long prepend(long indexCoder, byte[] buf, String value) {
    indexCoder -= value.length();
    if (indexCoder < UTF16) {
        value.getBytes(buf, (int)indexCoder, String.LATIN1);
    } else {
        value.getBytes(buf, (int)indexCoder, String.UTF16);
    }
    return indexCoder;
}

String#getBytes メソッドを使って、byte配列に変数の値を詰めています。

配列を文字列にする

// MEMO:
//   t10: 文字列を詰め込んだ byte 配列
//   t14: 文字列の先頭を指した状態の `indexCoder`
return StringConcatHelper.newString(t10, t14);

この StringConcatHelper.newString(byte[], long) の実装はこのようになっています。

/**
 * Instantiates the String with given buffer and coder
 */
static String newString(byte[] buf, long indexCoder) {
    // Use the private, non-copying constructor (unsafe!)
    if (indexCoder == LATIN1) {
        return new String(buf, String.LATIN1);
    } else if (indexCoder == UTF16) {
        return new String(buf, String.UTF16);
    } else {
        throw new InternalError("Storage is not completely initialized, " + (int)indexCoder + " bytes left");
    }
}

このnew String(byte[], byte) の実装はこのようになっています。

/*
 * Package private constructor which shares value array for speed.
 */
String(byte[] value, byte coder) {
    this.value = value;
    this.coder = coder;
}

引数の byte 配列はコピーすることなく、そのままフィールドに格納しています。3

落穂拾い

以下、雑多な点を記載します。

最適化

以下の量帆の条件を満たす場合、処理が最適化されます。

  • 文字列と1つの変数を結合する、または 2つの変数を結合する
  • 変数はプリミティブ型ではない
public String optimized(String foo) {
    // 文字列と1つの変数を結合する
    return "Hello " + foo;
}

public String optimized(String foo, String bar) {
    // 2つの変数を結合する
    return foo + bar;
}

この場合、StringConcatHelper#simpleConcat(Object, Object) であらかじめ用意されたパターンが使われます。 (出来上がる匿名メソッドの処理の流れは、通常の場合と同じ)

    /**
     * Perform a simple concatenation between two objects. Added for startup
     * performance, but also demonstrates the code that would be emitted by
     * {@code java.lang.invoke.StringConcatFactory$MethodHandleInlineCopyStrategy}
     * for two Object arguments.
     */
    @ForceInline
    static String simpleConcat(Object first, Object second) {
        String s1 = stringOf(first);
        String s2 = stringOf(second);
        // start "mixing" in length and coder or arguments, order is not
        // important
        long indexCoder = mix(initialCoder(), s1);
        indexCoder = mix(indexCoder, s2);
        byte[] buf = newArray(indexCoder);
        // prepend each argument in reverse order, since we prepending
        // from the end of the byte array
        indexCoder = prepend(indexCoder, buf, s2);
        indexCoder = prepend(indexCoder, buf, s1);
        return newString(buf, indexCoder);
    }

文字列に \0001 を含む場合

上記した通り、 StringConcatFactory#makeConcatWithConstants(MethodHandles.Lookup lookup, String name, MethodType concatType, String recipe, Object... constants)recipe に、結合する文字列のフォーマットを渡します。 このフォーマットは、変数を埋め込む箇所に \u0001 を指定するというものでした。

しかし、下記のように文字列の中に \u0001 が使われている事がありえます。

"Hello\u0001 " + foo + "!";

この場合は、該当の文字列を \u0002 に置き換えて、 \u0001 を含む文字列を引数の constants に格納します。

  • recipe: "\u0002\u0001!"
  • constants: [ "Hello\u0001 " ]

これを受け取った StringConcatFactory は、recipe をパースする際に \u0002 となっている個所に constants の値を埋め込み、フォーマットを再構築します。

引数の上限

結合する変数が200個以上の場合、処理が分割されます。 例えば、300個の変数を文字列結合する場合は以下の順で処理されます。

  1. 199個の変数を結合
  2. 101個の変数を結合
  3. 1 と 2 で出来上がった文字列を結合

この点は、JavaDoc にも明記されています。

APIのノート: JVMの制限があります(クラス・ファイル構造制約): 255個以上のスロットで呼び出すことはできません。 これは、ブートストラップ・メソッドに渡すことができる静的および動的引数の数を制限します。 MethodHandleコンビネータを使用する可能性のある連結ストラテジがあるので、一時的な結果をキャプチャするためにパラメータ・リストに空のスロットをいくつか予約する必要があります。 これは、このファクトリのブートストラップ・メソッドが200を超える引数スロットを受け入れない理由です。 連結で200以上の引数スロットを必要とするユーザーは、大きな連結をより小さな式で分割することが予想されます。 StringConcatFactory (Java SE 15 & JDK 15)

コンパイルオプション(実装の選択)

コンパイル時に、オプション-XDstringConcat=inline を付けると、過去の StringBuilder を使った実装を使うようにもコンパイルできます。

ちなみに、このオプションには以下の値を指定できます。

処理内容
inline StringBuilder で処理する。
indy InvokeDynamic で動的に処理を作る。
ブートストラップメソッドとしてStringConcatHelper#makeConcat を使う。
indyWithConstants InvokeDynamic で動的に処理を作る。
ブートストラップメソッドとしてStringConcatHelper#makeConcatWithConstants を使う。

デフォルトは、indyWithConstants です。

indy を指定した場合も、indyWithConstants とほぼ同じ処理が行われます。 違いは recipe を動的に作るかどうかです。最終的には makeConcatWithConstants が呼ばれます。 → makeConcat​(MethodHandles.Lookup, String, MethodType) の JavaDoc)

public static CallSite makeConcat(MethodHandles.Lookup lookup,
                                  String name,
                                  MethodType concatType) throws StringConcatException {
    // This bootstrap method is unlikely to be used in practice,
    // avoid optimizing it at the expense of makeConcatWithConstants

    // Mock the recipe to reuse the concat generator code
    String recipe = "\u0001".repeat(concatType.parameterCount());
    return makeConcatWithConstants(lookup, name, concatType, recipe);
}

実行時オプション(作成する匿名メソッドの選択)

Java 9 ~ Java 14 まで、Java の実行時に VM オプション –D:java.lang.invoke.stringConcat=(ストラテジー名) を付けることで、上記以外の実装も選ぶことがでました。

ストラテジー 概要​
BC_SB​ StringBuilderを使ったバイトコードを生成する(既存と同様)​
BC_SB_SIZED​ StringBuilderを使ったバイトコードを生成する。​
加えて、必要な配列のサイズを「推定」する。​
BC_SB_SIZED_EXACT​ StringBuilderを使ったバイトコードを生成する。​​
加えて、必要な配列のサイズを「正確に計算」する。​
MH_SB_SIZED​ MethodHandleベースのジェネレータを使って、最終的にStringBuilder を呼び出す。​​
加えて、必要な配列のサイズを「推定」する。​
MH_SB_SIZED_EXACT​ MethodHandleベースのジェネレータを使って、最終的にStringBuilder を呼び出す。​​
加えて、必要な配列のサイズを「正確に計算」する。​
MH_INLINE_SIZED_EXACT​ MethodHandleベースのジェネレータを使って、独自にbyte配列を構築する。​​
必要な配列のサイズを「正確に計算」する。​

デフォルトは、MH_INLINE_SIZED_EXACT​ です。 ただし、このオプションは Java 15 で削除されました。

参考資料

ここまで読んだうえで、 StringConcatFactory の JavaDocソースコードを読めば、文字列結合について詳しくなれる… ハズ!


  1. float, double, Object 型の変数は最初に文字列に変換しているので、ここでは文字列型の処理に入ります。

  2. 33ビット目では?と思われるかもしれませんが、0-indexed (最初を0ビットとして数える)ので32ビット目になります。

  3. これはアンセーフな処理です。なぜなら、文字列の配列を外側からいじれるようになってしまい、String が不変でなくなってしまうためです。そのため、通常であれば配列をコピーしてから格納するのですが、今回は配列をいじることがないと保証できるため、コピーをしないアンセーフなコンストラクタを使っています。

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

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

今回は、初のオンライン開催ということで、いつもと違って自宅からの視聴。
でも、twitterハッシュタグを見るとみんなでわいわいやってる感は変わらず、質疑応答もありでいつもと変わらず楽しめました。

それに加えて、気軽にセッションを行ったり来たりできたり、放送中に登壇者の方が随時コメントをしていたりと、オンラインならではというところがあって、これがとてもよかったです。
とはいえ、懇親会がないので、、、ちょっといつもよりもモチベーションが低めでした。

自分の主な参加動機は一度にいろんな話を聞けるってところだし、人見知りだし、懇親会がないならないでもいいかなとか最初は思っていたんですが…。
いざなくなってみると、全国からいろんな人に会える機会というのは貴重だし、自分自身それを楽しみにしていたんだなって改めて気づきました。

次回は、オフライン開催ができるようになってるといいですね!
(人数制限で、オンライン&オフラインという可能性もあるのかな…)


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

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

10:00

11:00

12:00

13:00

14:00

15:00

16:00

17:00

18:00

過去記事

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

ド・モルガンの法則は大事!大事!超大事!

「ド・モルガンの法則」ってなにそれ?おいしいの?って聞かれたことがあります。
おいしくはないのですが、プログラムを書く上では超大事!です。

ここでは、プログラムを読み書きするうえでド・モルガンの法則をどのように使えばいいかを紹介します。

ド・モルガンの法則とは?

(知っている人は、次の章まで読み飛ばして構いません)

ド・モルガンの法則とは、以下の規則性のことです。

!(A || B) は (!A && !B) に等しい
!(A && B) は (!A || !B) に等しい

(ここで、|| は OR, && は AND, ! は NOT )

確認してみましょう。

!(A || B) は (!A && !B) に等しい
 +-------+-------+-------+
 |       | True  | False |
 +-------+-------+-------+
 | True  | False | False |
 | False | False | True  |
 +-------+-------+-------+
!(A && B) は (!A || !B) に等しい
 +-------+-------+-------+
 |       | True  | False |
 +-------+-------+-------+
 | True  | False | True  |
 | False | True  | True  |
 +-------+-------+-------+

簡単ですね。

プログラミングで役立つ場面

その1: if-else

例えば、以下の処理を考えてみます。

private void logic(boolean a, boolean b){
    if (a && b) {
        // 処理1
    } else {
        // 処理2
    }
}

ここで、処理1が実行される条件が何かというと「a が true、かつ b が true のとき」
では、処理2が実行される条件は?

elseif が成立しないときに実行されます。
つまり、 「NOT(if 条件)」のときです。
「(a が true、かつ b が true のとき)ではないとき」

よくわかりませんね。
そこで、ド・モルガンの法則を使います。

!(a && b)
→ !a || !b

つまり、「a が false、または b が false のとき」

これならわかりやすいですね。

その2: if - else if

続いて、次の処理を考えてみます。

private void logic(boolean a, boolean b){
    if (a && b) {
        // 処理1
    } else if (b) {
        // 処理2
    }
}

ここで、処理2が実行される条件は?

「b が true のとき」ではないです。
else if はそれまでの if 条件が成立しないときに実行されるからです。

つまり、「NOT(if条件)かつ(else if 条件)」のときに実行されます。
「(a が true、かつ b が true のとき)ではないとき、かつ b が true のとき」

よくわかりませんね。
そこで、再びド・モルガンの法則を使います。

!(a && b) && b
→ (!a || !b) && b

さらに論理を展開します。

(!a || !b) && b
→ (!a && b) || (!b && b)
→ (!a && b) || false
→ !a && b

つまり、「a が false かつ b が true のとき」

最初から、以下のように書いてくれていればわかりやすかったのですが…。1

private void logic(boolean a, boolean b){
    if (b) {
        if (a) {
           // 処理1
        } else {
            // 処理2
        }
    }
}

その3: while

ループを考えています。

boolean a = true;
boolean b = true;
while (a && b) {
    // 処理(この中で、a, b を変更)
}

このループの継続条件は、 while 式の通り「a が true、かつ b が true のとき」

では終了条件は?
継続条件が満たされないとき、つまり NOT(継続条件)です。

!(a && b)
→ !a || !b

つまり、終了条件は「a が false、または b が false のとき」
else のときと、考え方は同じですね。

 まとめ

ソースコードには、「else の実行条件」や「 while の終了条件」など、直接書かれていない条件が存在します。
特に、いいコードほどこうした論理的に導きだせることは書かれていません。冗長になるからです。

それらを明確に理解しながら読むには、論理演算の法則を適用して処理をひとつづつ読み解いていくことが大事です。

その中でも、ド・モルガンの法則は役に立つことが多い法則です。
覚えておいて損はないと思います。

--
え、ド・モルガンの法則なんて知らなくてもコードは読めるし書ける? そういう人は、天才です。たまにいます。


  1. 最初は if だけがあって、あとから仕様変更で else if を継ぎ足すという経緯があると、最初に提示したようなコードになりがちです。

8進数リテラルはプログラミング言語ごとに異なる

8進数を表記する文法は、主に以下の3つがあります。 (二番目と三番目は分かりにくいですが ゼロ オー1 です)

これらのうち、各プログラミング言語がどの文法を採用しているかを表にまとめました。

言語 0 0o 0O
C × ×
PHP × ×
Perl × ×
Java × ×
Scala (~2.9) × ×
Scala (2.10~) × × ×
Kotlin × × ×
C# × × ×
Rust × ×
Swift × ×
Go
Ruby
Python2
Python3 ×
JavaScript1

考察

「8進数は 0123 って書くんだよ」というのは、もはや古いのかもしれません。


  1. ただし、0 のあとに 8 か 9 を含むなら10進数。参考:字句文法 - JavaScript | MDN

  2. octet (8個組) の o

SELECT文で本番環境を落としたお話

本番環境でやらかしちゃった人 Advent Calendarで、このパターンのやらかしはなかったのでキーボードを叩くことにしました。
番外編のつもりでお楽しみください。

この記事が、新たな障害発生を防ぐことにつながれば幸いです。

何をやったのか

ある日、ちょっとした調査のために本番データベースのデータを確認することになりました。
(個人情報が格納されているようなシステムではなかったので、必要であれば本番データベースへのアクセスが許されていました)

もしメンテナンスがあればそのタイミングでやればよかったのですが、直近では特に予定はないとのことでした。そのため、システムが動いている状態のまま作業をすることにしました。
ごく単純な SELECT を実行するだけのつもりだったので、システムに影響がないと判断したためです。

その際、万が一コピペをミスって更新系の SQL を実行してしまったら怖いので、念のためトランザクションをかけてからSQLを実行することにしました。
具体的には、psqlPostgreSQL のターミナル)で本番データベースに繋いで、以下の SQL を実行しました。

BEGIN;
SELECT * FROM user_setting WHERE xxx = 1;

結果はすぐに帰ってきました。確か2行程度だったと思います。

続けてさらに SQL を実行しようとしました。しかし、ここで同僚から「ソースコードでわからないところがあるんですが…」と声をかけられました。
こちらは急ぎの作業ではなかったので、ターミナルをそのままにして同僚の質問に回答することにしました。

そして約10分後…。

「システムがダウンしてるー!」

本番障害となりました。

何が悪かったのか

トランザクションをかけて SELECT 文を打ったお前が悪い」ということになりました。

何が起きていたのか

ログからシステムの動きを確認したところ、あるスレッドで user_setting テーブルをロックしようとしていたことが分かりました。具体的には、以下の SQL が発行されていました。

LOCK TABLE user_setting;

この SQL には、ロックモードの指定がありません。この場合、PostgreSQLACCESS EXCLUSIVE ロックが指定されたものとみなされます。
明示的ロック - PostgreSQL 9.4.5文書

この ACCESS EXCLUSIVE ロックは最も強いテーブルロックです。
SELECT 実行時に自動的に獲得される最も弱いテーブルロックである ACCESS SHARE ロックとも競合します。

つまり、システムが LOCK TABLE 文によって user_setting テーブルの ACCESS EXCLUSIVE ロックを獲得しようとしましたが、私が先に SELECT 文によって ACCESS SHARE ロックを獲得していたことで、ロック解除待ちに入って処理が止まってしまいました。

さらに、このあと別のスレッドが user_setting テーブルに対し SELECT を実行しようとしていました。しかし、user_setting テーブルは ACCESS EXCLUSIVE ロックの獲得待ちが発生しているので、この SELECT 文も止まってしまっていました。

結果、一つのスレッドが LOCK TABLE で、多数のスレッドが SELECT で止まってしまい、データベースとのコネクションプールが枯渇。システムダウンに至りました。

どうすればよかったのか

二度と惨劇を起こさないために、以下の知見を得ました。

  • SELECT しかしないとはいえ、油断しない
  • トランザクションを開始したなら、放置しない
    • なるべく早く COMMITROLLBACK をする

また、私がシステムを設計する際に以下の点に気を付けるようになりました。

  • ロック粒度に注意する
  • LOCK TABLE は極力使用しない
    • やむを得ず LOCK TABLE を使用するなら、可能な限り弱いロックモードを使用する
      • その際は、LOCK TABLE との競合について周知する1

MySQL では

MySQL だとテーブルロックのロックモードには READ と WRITE の2種類があります。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.3.5 LOCK TABLES および UNLOCK TABLES 構文

このうち WRITE ロックは SELECT と競合します。
また、MySQL のテーブルロックはセッション単位です。ドキュメントにあるように、ROLLBACK しただけではロックが解除されないので注意が必要です。

  • トランザクションを (たとえば、START TRANSACTION で) 開始すると、現在のトランザクションはすべて暗黙的にコミットされ、既存のテーブルロックが解放されます。
  • (中略)
  • ROLLBACK は、テーブルロックを解放しません。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.3.5.1 テーブルロックとトランザクションの通信

後日談

この件などでストレスが重なったことで胃痙攣をやらして、今度は私がダウンしました2。人間の体も本番障害が起きるんですね…。

みなさんも、障害発生後の体調管理には十分ご注意ください!


  1. 今回の場合、私が LOCK TABLE するコードを書いたわけではなかったので、このシステムでこのような競合が起きることを知りませんでした。

  2. 布団の上で数時間のたうち回りました。

Object#clone() メソッドからスローされる CloneNotSupportedException はどのようにハンドリングするべきか

JavaObject#Clone() メソッドは throws CloneNotSupportedException が宣言されています。

protected native Object clone() throws CloneNotSupportedException;

しかし、クラスが Cloneable インタフェースを実装していれば CloneNotSupportedException はスローされません。 それにもかかわらず CloneNotSupportedException はキャッチ例外のため1、スローするかキャッチする必要があります。

                            /* 👇実装している! */
public class Example implements Cloneable {
    @Override
    public Object clone() {
        // エラー: 例外CloneNotSupportedExceptionは報告されません。
        // スローするには、捕捉または宣言する必要があります
        return super.clone();
    }
}

正しく実装していれば何もしなくてもいいハズなのですが…。 このようなとき、どのようにハンドリングすればいいのでしょうか。

そこで、Java API ではこのよう場合にどのように実装されているかを確認してみました。 Search · CloneNotSupportedException path:/src/java.base/share/classes/java/

InternalError でラップする

圧倒的に多いのが、CloneNotSupportedExceptionInternalError でラップするという実装です。 Cloneable インタフェースを実装しているのに CloneNotSupportedException がスローされたということは、VM 内で何か問題があったということでこのようにしているようです。

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

AssertionError でラップする

ArrayDeque, EnumMap, EnumSetCloneNotSupportedExceptionAssertionError でラップしていました。 プログラムがバグっているということを示すために、AssertionError を使用しているようです。直接、この例外を投げるのは珍しい気がします。2

public ArrayDeque<E> clone() {
    try {
        @SuppressWarnings("unchecked")
        ArrayDeque<E> result = (ArrayDeque<E>) super.clone();
        result.elements = Arrays.copyOf(elements, elements.length);
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

RuntimeException でラップする

発生しないはずだから、とりあえず非キャッチ例外にしてしまえ!ということのようです。 ただ、RuntimeException だと型でエラーの概要が分からない、catch(Excetption e) で捕まってしまうという問題があります。

public Object clone() {
    try {
        return super.clone();
    } catch (CloneNotSupportedException e) {
        throw new RuntimeException(e.getMessage());
    }
}

握りつぶして、null を返す

java.util.Date では、キャッチして握りつぶしていました。 ただ、これは発生しないとはいえ null が返されてしまうルートができてしまうので、紛らわしいと思います。

public Object clone() {
    Date d = null;
    try {
        d = (Date)super.clone();
        if (cdate != null) {
            d.cdate = (BaseCalendar.Date) cdate.clone();
        }
    } catch (CloneNotSupportedException e) {} // Won't happen
    return d;
}

ちなみに

Cloneable インタフェースを実装しているクラスの clone メソッドで、throws CloneNotSupportedException を宣言しているのは見当たりませんでした。

まとめ

いかがでしたか。3
Cloneable インタフェースを実装していれば CloneNotSupportedException はスローされないので、どのようにハンドリングしても問題はないです。とはいえ、throws してしまうと呼び元に余計な手間をかけてしまいます。そのため、キャッチして何かしらのハンドリングをしておいた方がいいと思います。

そして、Java API の実装に合わせるのであれば InternalError にラップしてスローするのがいいようです。


  1. 設計ミスだと思います。

  2. 本来は、-enableassertions オプション付きで Java を実行しているときに、assert 文の条件が成立していないときにスローされるエラーです。

  3. これ書くとアフィリエイトブログっぽいですね。