20190502のJavaに関する記事は11件です。

Effective Java 第3版を読んでいて面白いと思ったことを抜粋して書いてみる

 Effective Java 第3版を読んでいて面白いと思ったものを抜粋して書いてみます。本自体まだ読んでる途中なので、今後も追加されるかもしれません。その場合に、当記事を更新するか新しい記事を書くかは未定です。

Item 42 : Prefer lambdas to anonymous classes

 Effective Java 第2版の「項目34 : 拡張可能なenumをインタフェースで模倣する」で紹介されていた、enumインスタンスごとに固有のメソッド実装を持たせる書き方がlambdaを使用したものに変わっていました。
 第2版での書き方はこちら。

EnumFunctionV2.java
public enum EnumFunctionV2 {
    PLUS("+") {
        @Override
        public double apply(double x, double y){ return x + y; }
    },
    MINUS("-") {
        @Override
        public double apply(double x, double y){ return x - y; }
    },
    TIMES("*") {
        @Override
        public double apply(double x, double y){ return x * y; }
    },
    DEVIDE("/") {
        @Override
        public double apply(double x, double y){ return x / y; }
    };

    private final String symbol;

    // コンストラクタ
    EnumFunctionV2(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }

    public abstract double apply(double x, double y);
}

 abstractメソッドを定義して、各enumインスタンスでOverrideするやり方です。便利には便利ですが、ちょっと見辛いですね・・・。
 第3版での書き方はこちら。

EnumFunctionV3.java
import java.util.function.DoubleBinaryOperator;

public enum EnumFunctionV3 {
    PLUS("+", (x, y) -> x + y),
    MINUS("-", (x, y) -> x - y),
    TIMES("*", (x, y) -> x * y),
    DEVIDE("/", (x, y) -> x / y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    // コンストラクタ
    EnumFunctionV3(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    @Override
    public String toString() {
        return symbol;
    }

    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }
}

 abstractが消えました。コンストラクタの第2引数にDoubleBinaryOperatorを持たせ、そこにlambdaを使ってメソッドの実装を直接書いています。実現している内容は第2版の時と同じですが、この方がすっきりして見やすくなったように感じます。
 呼び出し方はどちらも同じです。

EnumFunctionTest.java
public class EnumFunctionTest {
    public static void main(String... args) {
        // 2版の方を呼び出し
        System.out.println("【v2】");
        // ループしながら全インスタンスのメソッド呼び出し
        for (EnumFunctionV2 func2: EnumFunctionV2.values()) {
            System.out.print(func2 + ":");
            System.out.println(func2.apply(1, 2));
        }
        // 各インスタンスのメソッドを個別に呼び出したい場合はこんな感じ
        System.out.println(EnumFunctionV2.PLUS.apply(1, 2));

        // 3版の方を呼び出し
        System.out.println("【v3】");
        // ループしながら全インスタンスのメソッド呼び出し
        for (EnumFunctionV3 func3: EnumFunctionV3.values()) {
            System.out.print(func3 + ":");
            System.out.println(func3.apply(1, 2));
        }
        // 各インスタンスのメソッドを個別に呼び出したい場合はこんな感じ
        System.out.println(EnumFunctionV3.PLUS.apply(1, 2));
    }
}

実行結果
【v2】
+:3.0
-:-1.0
*:2.0
/:0.5
3.0
【v3】
+:3.0
-:-1.0
*:2.0
/:0.5
3.0

以上。lambdaの使い方で意外に思ったので書き留めてみました。

おわりに

 冒頭にも書いた通り、現在(2019/5/2)、Effective Java 第3版は読んでいる途中です。しかも頭から読んでいるわけでもないので、今回(2019/5/2)Item 42について書きましたが、この後Item 1について追記する可能性もあります。第3版はItem 1~90まであります。
 Effective Javaは昔からJavaの名著として扱われているような本なので、Java使いの方は一度は読んだ方が良いと思います。第3版は、英語版は2018/1/6、日本語版は2018/10/30に発売されたものです。しばらく使えると思うので、興味ある方は是非読んでみてください。

参考文献

Effective Java (3rd Edition)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「Cannot add task 'wrapper' as a task with that name already exists.」が出た

原因

使用しているGradleのバージョンが5.X(5系)の場合、以下の「task wrapper」の記述は使えない。

task wrapper(type: Wrapper) {
    gradleVersion = '4.4'
    distributionUrl = distributionUrl.replace("bin", "all")
}

対応

「task wrapper」 → 「wapper」に記述を修正したら正常に動いた。
↓こんな感じ。

wrapper {
    gradleVersion = '4.4'
    distributionUrl = distributionUrl.replace("bin", "all")
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「Cannot add task 'wrapper' as a task with that name already exists.」が出た時

原因

使用しているGradleのバージョンが5.X(5系)の場合、以下の「task wrapper」の記述は使えない。

task wrapper(type: Wrapper) {
    gradleVersion = '4.4'
    distributionUrl = distributionUrl.replace("bin", "all")
}

対応

「task wrapper」 → 「wapper」に記述を修正したら正常に動いた。
↓こんな感じ。

wrapper {
    gradleVersion = '4.4'
    distributionUrl = distributionUrl.replace("bin", "all")
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Java] Wingdings を表示する方法

はじめに

  • 個人的なメモです(忘れても大丈夫なように)
  • Windows の Java での話です。それ以外の環境ではチェックしていません。
  • フォント名は Wingding ではなく Wingdings なので注意(最後に s がつく)
    • Java では不正なフォント名を指定してもエラーにならず Dialog にフォールバックされるため、間違いに気が付きにくい
  • 参考: wikipedia - Wingdings

結論

  • 一般的に Wingdings というフォントでは 0x21~0xfe (or 0xff) のあたりにグリフがあることになっているけれど、Windows の Java で Wingdings を表示するには U+f021~U+f0fe(or U+f0ff) を使う必要がある

経緯

古い Java アプリを使っていたら、Wingdings でうまく表示できなかった。
具体的には様々なサイトでは ! (0x21) が鉛筆マークになると記載されているのに、うまく表示ができていなかった(そのアプリでは四角で表示された。変換できない文字扱い)。

古いアプリのせいなのかと思って(Swing ベースのアプリだったので、おそらく Java7 以前のもの)調べてみましたが、どうやら Java8 or Java11 でも JavaFX でもシンボルがうまく表示できないのは同様な様子。

調査結果

awt の Font クラスを使って、該当のフォントで表示できる範囲をチェックしてみました。

WingdingsTest.java
import java.awt.Font;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class WingdingsTest {

    public static void main(String[] args) {
        final Font font = new Font("wingdings", Font.PLAIN, 32);
        final String codes = IntStream.range(0, 0x10000)
                .filter(font::canDisplay)
                .mapToObj(Integer::toHexString)
                .collect(Collectors.joining(","));
        System.out.println(codes);
    }
}

結果を見ると、U+0009(TAB), U+000a(LF), U+000d(CR) と U+20xx あたりと、U+f0xx あたりが表示可能になっているみたい。

実際に試してみると、U+f021~U+f0ff あたりが Wingdings の記号になっていました。つまり、0x21 の鉛筆マークを表示したい場合には U+f021 のコードポイントを表示すればよいらしい。

この辺りはUnicode のいわゆる外字エリア(私用領域; Private Use Area)のようです。

この wikipedia の「外字」の「JIS X 0221 (Unicode)における外字」の説明を見たら、以下のように書いてありますね。

WingdingsなどのシンボルフォントのグリフはUnicodeではU+F020〜U+F0FFの一部に対応付けられている

試したところ、Wingdings だけじゃなく、Webdings や Symbol のフォントも似た感じでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ABC - 028 - A&B&C

AtCoder ABC 028 A&B&C

AtCoder - 028

微妙にA-Cが簡単な回が続いている。。。

2019/05/03 追記
 B問題のコード修正

A問題

    private void solveA() {
        int numN = nextInt();
        if (numN <= 59) {
            out.println("Bad");
        } else if (numN <= 89) {
            out.println("Good");
        } else if (numN <= 99) {
            out.println("Great");
        } else {
            out.println("Perfect");
        }
    }

B問題

2019/05/03コード修正

  • Collectors調べないと
    private void solveB() {
        String wk = next();

        Map<String, Long> tmp = Arrays.stream(wk.split(""))
                .collect(Collectors.groupingBy(s -> s, Collectors.counting()));

        String[] ref = { "A", "B", "C", "D", "E", "F" };

        String res = Arrays.stream(ref).map(i -> tmp.getOrDefault(i, 0L)
                .toString()).collect(Collectors.joining(" "));

        /*
         * この問題は、空白が最後についているとWAになってしまうのでtrim
         */
        out.println(res.trim());

    }

C問題:総当たりした解法

  • 全部足すのを試す
    • 重複を省くためにsetを使う
      • sortしておいてほしいのでtreeset
    • 最後から三番目の要素を取り出して出力
    private void solveC2() {
        int[] wk = IntStream.range(0, 5).map(i -> nextInt()).toArray();

        Set<Integer> set = new TreeSet<Integer>();
        for (int i = 0; i < wk.length; i++) {
            for (int j = i + 1; j < wk.length; j++) {
                for (int k = j + 1; k < wk.length; k++) {
                    set.add(wk[i] + wk[j] + wk[k]);
                }
            }
        }

        List<Integer> list = new ArrayList<Integer>(set);

        out.println(list.get(list.size() - 3));
    }

C問題:editorialの解法

  • editorialのP.9から解説

  • $a<b<c<d<e$であるならば

    • 最大の値は
      • $C+D+E$
    • 次に大きいのは
      • $B+D+E$
    • その次に大きいのは次のどちらか
      • $A+D+E$
      • $B+C+E$
    private void solveC() {
        int[] wk = IntStream.range(0, 5).map(i -> nextInt()).toArray();

        int res = Integer.max(wk[4] + wk[3]+ wk[0], wk[4] + wk[2]+ wk[1]);
        out.println(res);
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Forge式BlockStateを利用してItemのJsonModelを削減する方法

概要

例えばmetadataをもつItemを追加するにはそのmetadataの個数(今回は4つ)だけのjsonファイルが必要だったが、それを2個で済ませる(同じModelを使いまわす)方法

環境

Minecraft-1.10.2
Minecraft-1.11.2
Minecraft-1.12.2

(*試していないだけでおそらく他バージョンでも可)

追加するJSONファイル

(以下、modidはそのItemを追加するmodのID)

まずassets/modid/models/block/に以下のJSONファイルを作る
このときitemではなくblockのModelの位置に追加するのを間違えないように

item_generated.json
{
  "parent": "item/generated"
}

これは普通の板状に表示されるItemの場合で、ツール等ちゃんと持たせたい場合はgeneratedhandheldに。

その他のmodel(自作も含む)の場合も同じようにassets/modid/models/block以下に置いてModelを指定すれば使える。

次にassets/modid/blockstates/に以下のJSONファイルを作る
(variantsには追加するmeta数だけ書き加える)

sample_item.json
{
  "forge_marker": 1,
  "defaults": {
    "model": "modid:item_generated",
    "transform": "forge:default-item",
    "uvlock": true
  },
  "variants": {
    "meta0": [{
      "textures": {
        "layer0": "modid:items/sample_item_0"
      }
    }],
    "meta1": [{
      "textures": {
        "layer0": "modid:items/sample_item_1"
      }
    }],
    "meta2": [{
      "textures": {
        "layer0": "modid:items/sample_item_2"
      }
    }],
    "meta3": [{
      "textures": {
        "layer0": "modid:items/sample_item_3"
      }
    }]
  }
}

テクスチャ指定はlayer0, layer1と数字が上がるごとに上に重ねて表示される(何枚いけるのかは不明)。

Modelの登録(kotlin)

次は先ほど追加したModelの登録。

ClientProxy.kt
for (i in 0 until 4) {
    ModelLoader.setCustomModelResourceLocation(sampleItem, i,
            ModelResourceLocation(ResourceLocation(modid, "sample_item"), "meta$i"))
}

javaの場合も同じように

ClientProxy.java
for (int i = 0; i < 4; i++) {
    ModelLoader.setCustomModelResourceLocation(sampleItem, i,
            new ModelResourceLocation(new ResourceLocation(modid, "sample_item"), "meta" + i));
}

最後に

おそらくModelResourceLocationの第2引数を弄ればBlockと同じように、

sample_item.json
{
  "forge_marker": 1,
  "defaults": {
    "model": "modid:item_generated",
    "transform": "forge:default-item",
    "uvlock": true
  },
  "variants": {
    "meta": {
      "0":{   },
      "1":{   },
      "2":{   },
      "3":{   }
  }
}

のような形にもできると思うけどめんどくさいからやめた

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaコードをスクリプト的に実行する

はじめに

皆さん、お風呂は好きですか?私は好きです。暖かいお風呂に浸かってリラックスする時間は良いものです。
不思議なもので、プログラミングから離れて別のことをしている時、プログラミングに関して何か閃くことってありますよね。今回もそうでした。
ある日、いつものようにお風呂に浸かってボーッとしていた時、突然閃きました。「JShellってJavaコードが書かれたファイルを読み込んで、その内容を実行させることはできるのだろうか?」「それができるのであれば、スクリプトのようにJavaコードを実行できるんじゃね?」って。

世の中にはお風呂に浸かりながらプログラミングする方もいらっしゃるそうですが、あいにく私はその域には達していないので、すぐに試すことはできません。仕方ないので、閃いたことを忘れないように意識しながら早めにお風呂場を出ました。

試す

部屋着を着て、MacBookを開き、それっぽいキーワードでググります。はたして、JShellはJavaコードが書かれたファイルを読み込んで、その内容を実行させることが可能であるようです。早速試してみます。
JShellを使うので、以降のサンプルはJava9以上でないと実行できません。あしからず。

コード

test.jsh
System.out.println("Kitty on your lap")
/ex

JShellなのでコードはこれだけで済みます。セミコロンも不要です。 /ex はJShellの終了コマンドで、これがないとJShellの対話式実行の状態になってしまうとのことです。

実行

jshell test.jsh

jshell コマンドにファイル名を渡すだけで実行できます。
Enter を押してから実行されるまでにちょっと時間がかかりましたが、これだけでちゃんと「Kitty on your lap」が表示されました。素晴らしいですね。
なお、ファイルの拡張子は後掲の参考リンクに倣って.jshとしましたが、他の拡張子でも動作するようです。なんなら.javaでもいいです。(紛らわしいですが…)

メリット

さて、この実行方法はJShellを使っているわけなので、JShell自体のメリットがそのままこの実行方法のメリットになるはずです。つまり、下記のようなメリットがあります。
(タブ補完のような、JShell上で直接実行する場合のメリットは除く)

  • javac でコンパイルするといった手順なしで実行できる
  • クラスとmainメソッドを書かなくてもJavaコードを実行できる
  • 行末のセミコロンを省略可能
  • よく使うパッケージについてはインポート済でそのまま使える(java.io.* や java.util.* など)
  • チェック例外を無視できる

素晴らしいですね。(語彙力)
風呂上がりでただでさえ火照っている胸がさらに熱くなりました。

もうちょっと試す

上記の4つ目や5つ目のメリットは前掲のサンプルでは確認できないので、別の例を書いてみます。
テキストファイルを読み込んで、「hoge」が含まれる行を表示するコードです。

test2.jsh
try(Stream<String> lines = Files.lines(Paths.get("test.txt"))) {
    lines.filter(line -> line.contains("hoge"))
            .forEach(System.out::println);
}
/ex

Stream、Files、Pathsについて、明示的にインポートしなくても使えています。また、Files.lines()はIOExceptionを投げる可能性がありますが、IOExceptionを処理するコードを書かなくても動作しています。Javaにしては短く書けてなかなかいい感じです。
ただ、この例だとセミコロンを省略できないようでした。(エラーとなりました。たぶん内部的なコンパイル時のエラー)

デメリット

体が冷えるにつれ、頭のほうも冷静になってきました。

Javaにしては短く書けて明示的なコンパイルなしで実行できるとはいえ、コードの短さや実行の手軽さという点ではPythonやRubyといったスクリプト言語には敵いません。
また、詳細は後掲の参考リンク先を読んでいただいたほうがよさそうですが、普通に java コマンドで実行するのと比べるとパフォーマンスの面で不利とのことです。

そうなると、あまりユースケースが思いつかないですね。ほとんどの場合はスクリプト言語を使ったほうがいいでしょうし、Javaのちょっとした動作確認のためにスニペットを実行する場合はJShell上での直接入力で事足りる場合が多いでしょうし。
Javaしか書けない人や、何かJavaにこだわりがある人がスクリプトを書きたい場合には使えるでしょうか。そのようなニッチな需要しかカバーできないのが大きなデメリットでしょうね…

まとめ

  • Java9以上なら、JShellを使うことでJavaコードをスクリプト的に実行可能
    • (明示的な)コンパイルが不要
    • 普通にJavaを書くのに比べると短く書ける
      • クラスやmainメソッドを省略可能
      • セミコロンを省略可能(省略できない場合もある)
      • よく使うパッケージは明示的にimportしなくてもそのまま使える
      • チェック例外を処理するコードを省略可能
  • 普通にPythonやRuby等を使えばいいと思う 何かスクリプトを書く時に、どうしてもJavaで書きたい or そもそもJavaしか書けないという場合は重宝するかも

参考リンク

http://d.hatena.ne.jp/bitter_fox/20160703/1467577784

余談

  • Java11以上なら、通常のJavaソースを java コマンドで直接実行可能とのこと
  • お風呂に浸かりながら寝るのは危険です。疲れがたまっている方は気をつけましょう
  • お風呂に浸かりながらプログラミングするのは危険です。やるなら自己責任で
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Scanner

Scanner

ここは、java.util.Scannerで提供されてる処理のメモです。
入力を区切り文字で分割し順次処理する場合に使えます。
通常は、空白文字や改行文字を区切り文字として利用します。

文字列型

next()

入力された文字列は、Scannerによって空白文字や改行文字で分割されます。
分割された各要素(文字列)は、next()を使うことで順次呼び出すことができます。
一度メソッドを実行し要素を呼び出すと、同じ要素を繰り返し呼び出すことはできません。
よって、次にメソッドを実行したときは、次の要素が呼び出されます。

空白文字は半角でも全角でも問題ありません。また、途中に改行が含まれていようが、空白文字や改行文字が連続していようが問題なく動作します。

// 入力:イ ロ ハ(区切り文字は空白)
String a = sc.next(); // a = "イ"
String b = sc.next(); // b = "ロ"
String c = sc.next(); // c = "ハ"

/* 入力1行目:イ
 * 入力2行目:ロ
 * 入力3行目:ハ
 * や
 * 入力1行目:イ      ロ
 * 入力2行目:
 * 入力3行目:
 * 入力4行目:ハ
 * でも同じ結果となる。
 */

nextLine()

入力された文字列を空白文字ではなく改行文字で分割したい場合は、nextLine()を使います。
同じ要素を繰り返し呼び出すことができない点、次にメソッドを実行したときに次の要素が呼び出される点はnext()と同じです。

ただ、nextLine()は空白文字や文字がない行(空行)も含めて読み込むため、途中に想定外な空白文字や改行文字が含まれていると、読み込む要素がズレます。

/* 入力1行目:イ
 * 入力2行目:ロ
 * 入力3行目:ハ
 */
String a = sc.nextLine(); // a = "イ"
String b = sc.nextLine(); // b = "ロ"
String c = sc.nextLine(); // c = "ハ"

/* 入力1行目:イ ロ
 * 入力2行目:
 * 入力3行目:ハ
 */
String a = sc.nextLine(); // a = "イ ロ"
String b = sc.nextLine(); // b = ""
String c = sc.nextLine(); // c = "ハ"

/* 入力1行目:イ      ロ
 * 入力2行目:
 * 入力3行目:
 * 入力4行目:ハ
 */
String a = sc.nextLine(); // a = "イ      ロ"
String b = sc.nextLine(); // b = ""
String c = sc.nextLine(); // c = ""
// この場合4行目は読み込まれない。
// 変数cに「ハ」をブチ込む想定であるならば、入力する空行を1つ減らさなければならない。

プリミティブ型

next~()

Scannerによって分割された各要素を文字列型ではなくプリミティブ型として取得したいときは、nextInt()nextDouble()など、プリミティブ型の名を冠したメソッドを使います。

// 入力:1 2 3.14(区切り文字は空白)
int a = sc.nextInt(); // a = 1
int b = sc.nextInt(); // b = 2
double c = sc.nextDouble(); // c = 3.14

挙動はnext()と同じようです。

next~()とnextLine()の挙動まとめ

次のような入力とコードを考えます。

入力
1行目:1□2□□abc□□def
2行目:
3行目:3□ghi
4行目:4

// □は空白文字とする。
// 2行目は空とする。
// すべての行末には改行文字があるものとする。
コード
int a = sc.nextInt(); // a = 1
int b = sc.nextInt(); // b = 2
String c = sc.next(); // c = "abc"
String d = sc.nextLine(); // d = "□□def"
String e = sc.nextLine(); // e = ""
int f = sc.nextInt(); // f = 3
String g = sc.next(); // g = "ghi"
int h = sc.nextInt(); // h = 4

ここで、想定する入力を空白文字又は改行文字1つ毎に分割し、それぞれの要素に対し先頭から順に番号(番地)を振っていきます。
0:「1」1:「□」2:「2」3:「□」4:「□」5:「abc」6:「□」7:「□」8:「def」9:「\n」10:「\n」11:「3」12:「□」13:「ghi」14:「\n」15:「4」16:「\n」
(□は空白文字とし、\nは改行文字とする。)

このとき、上のコードを実行すると、次のように処理が行われます。

  • 1行目のnextInt()
    • 0番地が空白文字又は改行文字であるか検査。
    • 0番地の「1」を変数aに代入し、次の番地へ移動。
  • 2行目のnextInt()
    • 1番地が空白文字又は改行文字であるか検査。
    • 2番地以降で直近にある空白文字又は改行文字でない番地を検査。
    • 2番地へ移動。
    • 2番地の「2」を変数bに代入し、次の番地へ移動。
  • 3行目のnext()
    • 3番地が空白文字又は改行文字であるか検査。
    • 4番地以降で直近にある空白文字又は改行文字でない番地を検査。
    • 5番地へ移動。
    • 5番地の「abc」を変数cに代入し、次の番地へ移動。
  • 4行目のnextLine()
    • 6番地が改行文字であるか検査。
    • 6番地以降で直近にある改行文字である番地の手前まで読み込み。
    • 6番地から8番地までの文字列「□□def」を変数cに代入。
    • 改行文字である9番地をスキップし、10番地へ移動。
  • 5行目のnextLine()
    • 10番地が改行文字であるか検査。
    • 改行文字なので、空文字を変数eに代入し、次の番地へ移動。
  • 6行目のnextInt()
    • 11番地が空白文字又は改行文字であるか検査。
    • 11番地の「3」を変数fに代入し、次の番地へ移動。
  • 7行目のnext()
    • 12番地が空白文字又は改行文字であるか検査。
    • 13番地以降で直近にある空白文字又は改行文字でない番地を検査。
    • 13番地へ移動。
    • 13番地の「ghi」を変数gに代入し、次の番地へ移動。
  • 8行目のnextInt()
    • 14番地が空白文字又は改行文字であるか検査。
    • 15番地以降で直近にある空白文字又は改行文字でない番地を検査。
    • 15番地へ移動。
    • 15番地の「4」を変数hに代入し、次の番地へ移動。

あー、ややこしや。

どうやらnext()nextInt()nextDouble()などのプリミティブ型の名を冠したメソッド(以下「next系メソッド」といいます。)は、自分自身がいる番地が空白文字又は改行文字でなければ自分自身がいる番地の文字列を、そうでなければ直近にある空白文字又は改行文字でない番地に移動してからその番地の文字列を読み込み、その後次の番地へ移動するという挙動のようです。
一方nextLine()は、自分自身がいる番地が改行文字ならば空文字を返し次の番地へ移動し、そうでなければ自分自身がいる番地を含め、直近にある改行文字である番地の手前まで読み込み、その改行文字である番地をスキップするという挙動のようです。

next系メソッドと~.parse~(nextLine())の使い分け

以上の考察を踏まえて、next系メソッドによってプリミティブ型の値を取得する方法ではうまくいかず、一度nextLine()で文字列を読み取ってからプリミティブ型の値に変換する方法を使わなければならない場面を考えることができます。

例えば、1行目を「1」とし、2行目を「a」とする入力から、int型変数aに「1」を、String型変数bに「a」をそれぞれ代入する場面を想定します。
ここで、次のようなコードを考えます。

例1
int a = sc.nextInt();
String b = sc.nextLine();

上のコードを想定する入力により実行すると、変数aには想定どおり「1」が代入されますが、変数bには空文字が代入され、うまくいきません。
なぜかは、先程の考察に則って考えるとわかります。

この入力の場合、0番地は「1」、1番地は「\n」、2番地は「a」となります。
1行目のnextInt()は自分自身がいる番地(0番地)が空白文字又は改行文字でないので、そのまま「1」を取得し、次の番地に移動します。
ここまでは想定内です。ここからが問題です。
次行のnextLine()では、自分自身がいる番地(1番地)が改行文字となってしまい、空文字を返してしまいます。
よって変数bには空文字が代入され、想定どおりとならないのです。

1番地が改行文字とならないように入力を変えるか、1番地の改行を読み込まないようにするコーディングに変える必要があります。

ここでは後者を例に取り、次のようにコードを修正します。

例2
int a = Integer.parseInt(sc.nextLine());
String b = sc.nextLine();

こうすれば想定どおり、int型変数aに「1」が、String型変数bに「a」がそれぞれ代入されます。
なぜうまくいくのか、ここでも先程の考察に則って考えてみます。

1行目のnextLine()は自分自身がいる番地(0番地)が改行文字でないので、そのまま「1」を取得し、その改行文字である番地(1番地)をスキップします。
これにより、安心して2番地以降を読み込もうとすることが可能となります。1
次行のnextLine()で、自分自身がいる番地(2番地)の「a」を返します。
よって変数bには「a」が代入され、想定どおりとなるのです。

next系メソッドには「改行」するという挙動がありません。またnextLine()は空白文字も含めて読み取ります。
だからこそ、入力が改行区切りで与えられていて、プリミティブ型変数に代入することを想定する場面では、面倒でもnextLine()で改行毎に読み取ってparseメソッドで囲う必要があるといえます。
それらの挙動を理解せず、parseメソッドを使うとコードが冗長になるからと安易にnext系メソッドに頼ってばかりいると、例1のように思わぬ出力を招いてしまうことがあります。

もくじに戻る

https://qiita.com/k73i55no5/items/9e0825cee4cc1cb078a6


  1. 例外がスローされることがあるかもしれませんので、確実に読み込めるとは言い切れません。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BufferedReaderとScanner

BufferedReader

ここからは、java.io.BufferedReaderクラスで提供されてる処理のメモです。
入力を改行で分割し順次処理し、途中でStreamに変換する場合に使っています。

Scanner

ここからは、java.util.Scannerで提供されてる処理のメモです。
入力を区切り文字で分割し順次処理する場合に使えます。
通常は、空白文字や改行文字を区切り文字として利用します。

文字列型

next()

入力された文字列は、Scannerによって空白文字や改行文字で分割されます。
分割された各要素(文字列)は、next()を使うことで順次呼び出すことができます。
一度メソッドを実行し要素を呼び出すと、同じ要素を繰り返し呼び出すことはできません。
よって、次にメソッドを実行したときは、次の要素が呼び出されます。

空白文字は半角でも全角でも問題ありません。また、途中に改行が含まれていようが、空白文字や改行文字が連続していようが問題なく動作します。

// 入力:イ ロ ハ(区切り文字は空白)
String a = sc.next(); // a = "イ"
String b = sc.next(); // b = "ロ"
String c = sc.next(); // c = "ハ"

/* 入力1行目:イ
 * 入力2行目:ロ
 * 入力3行目:ハ
 * や
 * 入力1行目:イ      ロ
 * 入力2行目:
 * 入力3行目:
 * 入力4行目:ハ
 * でも同じ結果となる。
 */

nextLine()

入力された文字列を空白文字ではなく改行文字で分割したい場合は、nextLine()を使います。
同じ要素を繰り返し呼び出すことができない点、次にメソッドを実行したときに次の要素が呼び出される点はnext()と同じです。

ただ、nextLine()は空白文字や文字がない行(空行)も含めて読み込むため、途中に想定外な空白文字や改行文字が含まれていると、読み込む要素がズレます。

/* 入力1行目:イ
 * 入力2行目:ロ
 * 入力3行目:ハ
 */
String a = sc.nextLine(); // a = "イ"
String b = sc.nextLine(); // b = "ロ"
String c = sc.nextLine(); // c = "ハ"

/* 入力1行目:イ ロ
 * 入力2行目:
 * 入力3行目:ハ
 */
String a = sc.nextLine(); // a = "イ ロ"
String b = sc.nextLine(); // b = ""
String c = sc.nextLine(); // c = "ハ"

/* 入力1行目:イ      ロ
 * 入力2行目:
 * 入力3行目:
 * 入力4行目:ハ
 */
String a = sc.nextLine(); // a = "イ      ロ"
String b = sc.nextLine(); // b = ""
String c = sc.nextLine(); // c = ""
// この場合4行目は読み込まれない。
// 変数cに「ハ」をブチ込む想定であるならば、入力する空行を1つ減らさなければならない。

プリミティブ型

next~()

Scannerによって分割された各要素を文字列型ではなくプリミティブ型として取得したいときは、nextInt()nextDouble()など、プリミティブ型の名を冠したメソッドを使います。

// 入力:1 2 3.14(区切り文字は空白)
int a = sc.nextInt(); // a = 1
int b = sc.nextInt(); // b = 2
double c = sc.nextDouble(); // c = 3.14

挙動はnext()と同じようです。

next~()とnextLine()の挙動まとめ

次のような入力とコードを考えます。

入力
1行目:1□2□□abc□□def
2行目:
3行目:3□ghi
4行目:4

// □は空白文字とする。
// 2行目は空とする。
// すべての行末には改行文字があるものとする。
コード
int a = sc.nextInt(); // a = 1
int b = sc.nextInt(); // b = 2
String c = sc.next(); // c = "abc"
String d = sc.nextLine(); // d = "□□def"
String e = sc.nextLine(); // e = ""
int f = sc.nextInt(); // f = 3
String g = sc.next(); // g = "ghi"
int h = sc.nextInt(); // h = 4

ここで、想定する入力を空白文字又は改行文字1つ毎に分割し、それぞれの要素に対し先頭から順に番号(番地)を振っていきます。
0:「1」1:「□」2:「2」3:「□」4:「□」5:「abc」6:「□」7:「□」8:「def」9:「\n」10:「\n」11:「3」12:「□」13:「ghi」14:「\n」15:「4」16:「\n」
(□は空白文字とし、\nは改行文字とする。)

このとき、上のコードを実行すると、次のように処理が行われます。

  • 1行目のnextInt()
    • 0番地が空白文字又は改行文字であるか検査。
    • 0番地の「1」を変数aに代入し、次の番地へ移動。
  • 2行目のnextInt()
    • 1番地が空白文字又は改行文字であるか検査。
    • 2番地以降で直近にある空白文字又は改行文字でない番地を検査。
    • 2番地へ移動。
    • 2番地の「2」を変数bに代入し、次の番地へ移動。
  • 3行目のnext()
    • 3番地が空白文字又は改行文字であるか検査。
    • 4番地以降で直近にある空白文字又は改行文字でない番地を検査。
    • 5番地へ移動。
    • 5番地の「abc」を変数cに代入し、次の番地へ移動。
  • 4行目のnextLine()
    • 6番地が改行文字であるか検査。
    • 6番地以降で直近にある改行文字である番地の手前まで読み込み。
    • 6番地から8番地までの文字列「□□def」を変数cに代入。
    • 改行文字である9番地をスキップし、10番地へ移動。
  • 5行目のnextLine()
    • 10番地が改行文字であるか検査。
    • 改行文字なので、空文字を変数eに代入し、次の番地へ移動。
  • 6行目のnextInt()
    • 11番地が空白文字又は改行文字であるか検査。
    • 11番地の「3」を変数fに代入し、次の番地へ移動。
  • 7行目のnext()
    • 12番地が空白文字又は改行文字であるか検査。
    • 13番地以降で直近にある空白文字又は改行文字でない番地を検査。
    • 13番地へ移動。
    • 13番地の「ghi」を変数gに代入し、次の番地へ移動。
  • 8行目のnextInt()
    • 14番地が空白文字又は改行文字であるか検査。
    • 15番地以降で直近にある空白文字又は改行文字でない番地を検査。
    • 15番地へ移動。
    • 15番地の「4」を変数hに代入し、次の番地へ移動。

あー、ややこしや。

どうやらnext()nextInt()nextDouble()などのプリミティブ型の名を冠したメソッド(以下「next系メソッド」といいます。)は、自分自身がいる番地が空白文字又は改行文字でなければ自分自身がいる番地の文字列を、そうでなければ直近にある空白文字又は改行文字でない番地に移動してからその番地の文字列を読み込み、その後次の番地へ移動するという挙動のようです。
一方nextLine()は、自分自身がいる番地が改行文字ならば空文字を返し次の番地へ移動し、そうでなければ自分自身がいる番地を含め、直近にある改行文字である番地の手前まで読み込み、その改行文字である番地をスキップするという挙動のようです。

next系メソッドと~.parse~(nextLine())の使い分け

以上の考察を踏まえて、next系メソッドによってプリミティブ型の値を取得する方法ではうまくいかず、一度nextLine()で文字列を読み取ってからプリミティブ型の値に変換する方法を使わなければならない場面を考えることができます。

例えば、1行目を「1」とし、2行目を「a」とする入力から、int型変数aに「1」を、String型変数bに「a」をそれぞれ代入する場面を想定します。
ここで、次のようなコードを考えます。

例1
int a = sc.nextInt();
String b = sc.nextLine();

上のコードを想定する入力により実行すると、変数aには想定どおり「1」が代入されますが、変数bには空文字が代入され、うまくいきません。
なぜかは、先程の考察に則って考えるとわかります。

この入力の場合、0番地は「1」、1番地は「\n」、2番地は「a」となります。
1行目のnextInt()は自分自身がいる番地(0番地)が空白文字又は改行文字でないので、そのまま「1」を取得し、次の番地に移動します。
ここまでは想定内です。ここからが問題です。
次行のnextLine()では、自分自身がいる番地(1番地)が改行文字となってしまい、空文字を返してしまいます。
よって変数bには空文字が代入され、想定どおりとならないのです。

1番地が改行文字とならないように入力を変えるか、1番地の改行を読み込まないようにするコーディングに変える必要があります。

ここでは後者を例に取り、次のようにコードを修正します。

例2
int a = Integer.parseInt(sc.nextLine());
String b = sc.nextLine();

こうすれば想定どおり、int型変数aに「1」が、String型変数bに「a」がそれぞれ代入されます。
なぜうまくいくのか、ここでも先程の考察に則って考えてみます。

1行目のnextLine()は自分自身がいる番地(0番地)が改行文字でないので、そのまま「1」を取得し、その改行文字である番地(1番地)をスキップします。
これにより、安心して2番地以降を読み込もうとすることが可能となります。1
次行のnextLine()で、自分自身がいる番地(2番地)の「a」を返します。
よって変数bには「a」が代入され、想定どおりとなるのです。

next系メソッドには「改行」するという挙動がありません。またnextLine()は空白文字も含めて読み取ります。
だからこそ、入力が改行区切りで与えられていて、プリミティブ型変数に代入することを想定する場面では、面倒でもnextLine()で改行毎に読み取ってparseメソッドで囲う必要があるといえます。
それらの挙動を理解せず、parseメソッドを使うとコードが冗長になるからと安易にnext系メソッドに頼ってばかりいると、例1のように思わぬ出力を招いてしまうことがあります。

s

また、Scannerによって読み込んだ文字列に対しStringクラスのsplitメソッドを使いStreamに流し込むことで、入力を空白又は改行以外の区切り文字で分割して順次読み込ませることができます。
split("")とすると入力を一文字ずつ区切って読み込ませることができます。

// 入力「123」
Iterator<Integer> tokens = Stream.of(sc.nextLine().split(""))
  .mapToInt(Integer::parseInt)
  .iterator();
int a = tokens.next(); // a = 1
int b = tokens.next(); // b = 2
int c = tokens.next(); // c = 3

  1. 例外がスローされることがあるかもしれませんので、確実に読み込めるとは言い切れません。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラム性能チューニングの入門ガイド

プログラム側の性能チューニングの中でも汎用的であり、どの言語・分野でも費用対効果が高そうな原則およびサンプルを紹介しています。

結論

以下の3つの原則を意識して、プログラムの性能チューニングおよび開発を行いましょう。

  • 原則1. 計算量を小さくする
    • 多重ループを改善する
    • アルゴリズムを改善する
  • 原則2. 高コスト処理をバッチ化する
    • SQL, REST API, 外部コマンドをまとめる
    • システムコール (ex. read, write)をまとめる
  • 原則3. 高コストなものを再利用する
    • TCP、HTTP、DB等のコネクションを再利用する
    • 外部サーバからの問い合わせ結果を再利用する
    • 確保したメモリ、オブジェクト、スレッド等を再利用する

なおシステムのアーキテクチャ、ハードウェア構成などのチューニング・最適化はこの記事の対象外です。(原則2,3の考え方は応用できますが、そこまでは踏み込みません)

背景

色々なシステム開発に携わっていると、設計や開発の中で性能を意識する人というのは意外と少ないように感じます。開発対象の目的や性能要件を考慮すれば明らかに後々の性能検証等で大きな問題となりそうな箇所であっても、何事もなく開発が進められたりします。

もちろん「中途半端な最適化は悪だから、後で必要な時にチューニングするべき」という格言は正しいです。しかしこの格言は設計やコーディングをしている時点である程度の性能ボトルネックについての仮説や予測を立てることも暗黙の内に期待されているのではないでしょうか?だからこそ「チューニングが必要そうなところはメソッド化やモジュール化して、後でチューニングしやすいようにしよう」という文言も添えられていることが多いのと考えています。

この事実を理解せずに性能ボトルネックについての仮説や予測を立てずにコーディングやユニットテスト等の作業を進めてしまい、いざシステムテストを実施すると致命的な性能ボトルネックが発生して右往左往してしまうのを目の当たりすることは珍しくありません。「後で必要な時に性能チューニングすれば良い」と言うのは結構ですが、そのために必要な指針(仮説や予測の立て方)、知識、スキル等を持たなければ机上の空論でしかありません。問題対応をただ単純に先延ばししているのと何ら変わりはありません。

NOTE:
Intel DPDK(ネットワークのパケットを超高速に処理するための開発キット)みたいものを開発するのであれば、当然より高度な最適化のための知識やスキルを総動員する必要があります (ex. 並列化、CPU等のAffinity、キャッシュヒット、キャッシュライン、CPUアーキテクチャ、コンパイラのオプション、ハードウェアオフロード等)。しかしこのような特殊な分野でない限りは、最優先すべき最適化は他にあります。

紹介する性能チューニングの特徴

本記事では性能チューニングの中でも以下の特徴を持った原則、および代表的なテクニックのサンプルを紹介します。

  • プログラム側の修正で実現できる
  • 特定のプログラミング言語やライブラリに依存しない

プログラミング言語、アーキテクチャ、ハードウェア等に特化したより専門的で高度な最適化も数多く存在しますが、まずはより汎用的な原則を押さえておくべきです。

NOTE:
コンパイラの最適化オプション、Linuxカーネル等のパラメータチューニング等に物凄く精通しているのに、本記事で扱っているような基本的な性能チューニングを一切知らない/適用できない人をたまに見かけます。
そういう人は自分自身の得意分野の性能チューニングで問題の解決を試みるため、性能ボトルネック次第では極めて効果の薄い性能チューニングを施しがちになります。周りがあまり詳しくない技術者の場合「これだけやっても駄目だから仕方ない」と誤解を蔓延させるオマケ付きです。

性能チューニングの原則

ここでは私自身が最も大事と考える3つの原則を紹介します。重要度は状況に応じて変わりますが、基本的に上から順番に原則を適用することをお勧めします。

  • 原則1. 計算量を小さくする
  • 原則2. 高コスト処理をバッチ化する
  • 原則3. 高コストなものを再利用する

NOTE: 他にも加えたいところですが、あまり多すぎると実践しづらいので3つに留めています。

原則1. 計算量を小さくする

性能チューニングで最も大事なものの一つが、プログラム処理の計算量を小さくすることです。
計算量、O記法などを知らない場合は、以下の記事などを参考にしてください。

[初心者向け] プログラムの計算量を求める方法
計算量オーダーの求め方を総整理! 〜 どこから log が出て来るか 〜

情報系やコンピュータサイエンスを専攻している人には常識レベルの話なのですが、たとえ知っていても実践できている人は残念ながら意外と少ないです。プログラムを書いてる時でも、DB向けのSQLを書いてる時などでも計算量は常に頭の片隅で意識しておく習慣をつけておきましょう。

NOTE: ちなみにDBのSQLの場合、その時点のDBのテーブルのレコード数、統計情報などによってオプティマイザが内部実行計画を変更する可能性があるためややこしいです。このため当初は計算量がO(N)で動作していたSQLが、例えばDB統計情報の精度不足によってO(N^2)になることもあります。

大まかな基準としては計算量がO(N)以上なら「性能の阻害要因になるかも」と警戒し、O(N^2)以上の場合は「性能の阻害要因に絶対になる」くらいの意識で確認することがお勧めです

  • ループ処理
  • 再帰処理
  • データ構造と操作 (追加、更新、削除、参照など)
  • その他 各種アルゴリズム
  • SQL (ex. サブクエリ、Join/Scanなどの実行計画)

判断基準の目安例

  • 計算量がO(N^2)以上、処理要素数N=数千~数万以上
  • 計算量がO(N)以上、処理要素N=数十~数百以上、各要素の処理が高コスト処理に相当
    • 高コスト処理については原則2を参照

補足: 計算量の係数

例えば計算回数を3*nの式で求められる場合、計算量はO(N)となります。係数である3は無視していいわけですね。

しかしプログラムの性能を考える場合はこの係数は意識することも大事です。例えば10*nの計算回数で処理時間が10秒だった場合、係数の部分を1にできれば処理時間を1秒に改善できます。もちろん開発対象の要件等にはよりますが、この係数部分の改善が極めて重要となるケースは十分にあることは想像できると思います。

後述する原則2, 原則3も、この係数部分を小さくすることに利用できます。

原則2. 高コスト処理をバッチ化する

プログラムが行う処理、要求される性能(ex. 処理時間)を考慮した場合、要求される性能を満たすために大きな阻害になる高コスト処理が存在することがあります。このような高コスト処理が何度も実行されるのは致命傷となる可能性が高いため、まとめて実行(バッチ化)することを検討しましょう。

高コスト処理と判断するかどうかはケースバイケースになります。しかし以下のような処理は「要求される性能を満たすための阻害要因となる高コスト処理」になる可能性が高いため、高コスト処理の候補として意識することをお勧めします。

  • 外部コマンド実行
    • プロセス生成コストが高い
  • サーバへのリクエスト(REST API, SQL, RPC, etc...)
    • サーバ間通信のコストが高い
    • リクエストあたりの処理時間が長い、あるいは長い可能性がある
  • システムコール
    • I/O処理、ユーザ空間とカーネル空間の切り替えコストが高い
    • 前述した2つ高コスト処理に比べればインパクトは小さめ
  • 未知のライブラリ
    • どのような処理をしてるかを理解するまではコストが高い可能性が残る

先ほども述べたように阻害要因になる可能性が高くても、状況によっては問題ない場合もあります。例えばある処理の支配項である「SQLのINSERT (1回あたり1msかかる)」が実行される場合のパターンを3つ考えてみます。

  • 3秒以内に行う必要がある処理の中で、SQLのINSERTを高々10回しか行わない
    • 1ms * 10 = 10msのため、性能ボトルネックになる可能性は極めて低いため問題なし
  • 3秒以内に行う必要がある処理の中で、SQLのINSERTが1万回行われる
    • 1ms * 10000 = 10sのため、致命的な性能ボトルネックになるため問題あり
  • 60秒以内に行う必要がある処理の中で、SQLのINSERTが1万回行われる
    • 1ms * 10000 = 10sのため、性能ボトルネックになる可能性は極めて低いため問題なし

上記の3つのパターンを見ればわかるように、性能要件と処理要素数などに応じて阻害要因になるかどうかは変わります。このためここで重要なのは、阻害要因になる可能性があるもの(高コスト処理の候補)に対して大まかな概算を行い、実際に問題がありそうかどうかをざくっと見極めることです。問題がないと断定できなければ、その部分は要確認ポイントとして当たりを付けます。

高コスト処理を特定することができた場合、あとはその部分をバッチ化する方法を検討することになります。一般的に上で述べたような高コスト処理は何らかの形でまとめて実行する手段が用意されており、まとめて実行した場合に処理時間が大幅に短縮できる場合が多いです。そのような手段がない場合は、まとめて実行する手段を呼び出し先に実装してもらう必要があるかもしれません。

NOTE:
Python, Ruby等のスクリプト言語は、C/C++/Java等のコンパイル型言語と比べてステップ単位の処理が何倍~何十倍も遅いです(JavaScriptについてはV8エンジンの目覚ましい進化などにより、やや事情が異なります)。
このため処理内容と性能要件によっては、ステップ単位の処理そのものが高コスト処理として顕在化する可能性があります。このような場合はできるだけ「C言語等で高速処理されるように実装された標準関数、ライブラリ」を活用してまとめて処理するようにしましょう。例えばPythonであれば巨大な配列の生成や処理については極力Numpy等のライブラリに任せるといった具合です。

判断基準の目安例

  • 数十~数百以上の外部コマンドを実行している
    • 外部コマンドの処理時間が長い場合、さらに注意が必要
  • 数百~数千以上のHTTPリクエスト、SQLなどを送信している
    • 送信先がインターネット上などの場合、さらに注意が必要
  • 数千~数万以上のシステムコールを実行している
    • 1秒間に数千~数万リクエストを処理したり、大量データを処理したりする場合などは要注意
    • μs, ns単位の処理時間を意識する場合、さらに注意が必要

原則3. 高コストなものを再利用する

例えばプログラムの処理を行う際には、スレッドや各種コネクション(ex. HTTP, SQL)を活用して実装することも多いと思います。これらを頻繁に利用する場合、毎回新しく作成して破棄するのは思いのほか大きなコストになります。このように高コストかつ使用頻度が高いものについては、再利用できるのであれば再利用するための仕組みを検討しましょう

もちろん高コストなものというのは、プログラムの目的や性能要件などによって大きく異なります。これは原則2と同様です。例えばHTTPリクエストを10分に一回しか送信しない場合はHTTPコネクションを再利用するメリットは小さいですが、1秒に数十~数百回送信する場合はHTTPコネクションを再利用するメリットは大きいといった具合です。プログラムの用途や性能要件によっては、毎回メモリを使う度に律儀にmallocで確保してfreeで解放すること自体が大きなコストとなる場合もあり、このような場合にはmallocしたメモリ領域自体を再利用する仕組みを導入するメリットも大きいです。

代表的な再利用対象を以下に示します。

  • HTTP, DB等のコネクション
    • コネクションプールを活用して再利用する
    • 例えばJDBC(JavaのDB API)ではTomcat JDBC Poolを活用する
  • スレッド
    • スレッドプールを活用して再利用する
    • 例えばJavaではExecutorServiceを活用する
  • サーバの問い合わせ結果
    • HTTPレスポンスをキャッシュする
    • DNSレスポンスを一定期間キャッシュする
    • ライブラリ等にキャッシュ機構が備わっている場合、これを活用する

判断基準の目安例

  • 1秒間に数十~数百以上のスレッドの生成破棄を行っている
  • コネクションプールやセッションを意識せずにHTTP、SQL等を大量に発行している
    • 使用している言語やライブラリによっては生成破棄が繰り返されてる可能性がある

原則を適用する流れ

最適化の原則を当てはめるのは以下の作業を行うときです。

  • 実装を行う時
  • 既存の実装に対してチューニングを行う時

コーディングを行う時に以下のチェックを行います。該当する原則がある場合は、後々最適化を施す必要がある箇所となり得ます。

  • 各原則に該当するかどうか当たりを付ける
  • 当たりを付けた箇所には以下の対応を行う
    • TODOコメント、ログ埋め込み
  • プログラムを動作させて性能ログを取得する
  • 性能ログをもとに処理時間を確認し、確認結果から性能チューニングが実際に必要か判断する

ちなみに当たりを付ける範囲が細かいほど手間がかかるため、「これとこれが怪しい」くらいの範囲をまとめてしまっても構いません。実際にその範囲が性能の阻害要因になったタイミングで範囲を細分化して問題箇所を特定するのも一つの手です。

該当箇所には以下のような対応をしましょう。各該当箇所を何も考えずに最適化しても効果が薄く部分最適化にしかならない可能性があるため、後々の最適化を行うための準備だけに留めます。

  • 処理時間を測定できるように性能関連ログを埋め込む
  • TODOコメントで重要な最適化候補であることを明記する

チューニングのサンプル

ここでは各原則を実現するためのサンプルをいくつか紹介します。
ソースコードは基本的にPython3.6を前提に記述しています。

NOTE: 測定結果の比較等はその都度環境(ex. OS, Host/VM/WSL)が異なるため、参考情報程度に留めてください。

原則1. 計算量を小さくする

多重ループにHashMap/Set等を活用する

あるコレクションから別のコレクションを検索するような処理では多重ループを使うことが多いです。しかし多重ループは計算量がO(N^2)、O(N^3)と大きくなるため、処理要素数が多くなると一気に性能ボトルネックとして顕在化します。

このような時には一方のコレクションからHashMapやSetを作成し、これを検索に活用することによって例えば計算量をO(N^2)からO(N)等に減らすことができます。コレクションの要素数分だけ新しいHashMap/Setを作成するコストが勿体ないと思うかもしれませんが、以下のテーブルの処理要素数を見ればそのコストは極めて小さいものだと分かります。

要素数 O(N)×2 O(N^2)
10 20 100
100 200 10,000
10,000 20,000 100,000,000

まず二重ループ処理になっている例を示します。(実行時間の測定等の処理は割愛)

bad_performance_no1.py
all_files = list(range(1, 100000))
target_files = list(range(50000, 60000))
matched_files = []

# 計算量がO(N^2) -> BAD
for f in all_files:
    if f in target_files:
        matched_files.append(f)

print(len(matched_files) 

ここで注意してもらいたいのがfor文の中にあるif f in target_files:の箇所です。この部分はループではないため処理が1回で終わっているように見えますが「リストのtarget_filesに要素sが含まれているか」を確認するために、合致する要素を見つけるまで平均でN/2の要素チェックの処理が行われることになります。Javaにおけるコレクション操作メソッドのcontains()等も同様です。このためプログラム文法上は多重ループになっていなくても、実際の処理では多重ループ相当の処理になっている場合があります

次にListからSetを作成して二重ループを改善した例を示します。(実行時間の測定等の処理は割愛)

good_performance_no1.py
NUM = 1000000
nums = list(range(1, NUM))
# TUNING: set関数によりListからSetを作成
# NOTE: //で割り算しているのは整数を保持するため
expected_nums = set(range(1, NUM//2))
matched_nums = []

# 計算量がO(N)に改善 -> GOOD
start = time.time()
for f in nums:
    if f in expected_nums:
        matched_nums.append(f)

print(len(matched_nums) 

ループ処理の部分の比較結果を以下に示します。

計算量 実行時間
O(N^2) 8,228 ms
O(N) 4 ms

処理要素数N=10万件の場合、計算量をO(N)に改善することで約2000倍高速化できました。
このように処理対象のレコード数が数万以上の場合は、このような簡単なループ処理でも大きな改善効果があります。

原則2. 高コスト処理をバッチ化する

外部コマンド大量実行をまとめる

プログラムの中で外部コマンドを実行する場合、その実行する回数が多いほどパフォーマンスが低下します。これは外部コマンド実行がプロセス生成などを伴うコストの高い処理だからです。

ここでは「/etc配下の全ファイルの総バイトサイズを表示する」ことを目的にしたプログラムを例として取り上げます。wcコマンドを--bytesオプションを指定することにより指定したファイルのバイトサイズを出力することができます。

NOTE:
ちなみにPythonを含む大半の汎用プログラミング言語では、ファイルのバイト数を取得するための標準関数等が用意されています。このためwcコマンドなんて使う必要はありません。このためこの例では「どうしても外部コマンドであるwcコマンドが必要」という前提で読み進めてください。

bad_performance_no_batch.py
import pathlib
from subprocess import check_output

cwp = pathlib.Path("/etc")
files = [f for f in cwp.glob("**/*") if f.is_file()]

total_byte = 0
for f in files:
    # 各ファイルに対してwcコマンドを実行 -> 極めて非効率
    result = check_output(["wc", "--byte", str(f)])
    size, file_path = result.split()
    total_byte += int(size)

print("file num:", len(files))
print("total byte:", total_byte)

結果は本節の最後に掲載していますが、ファイル毎にwcコマンドを実行しているため、対象ファイル数が数百~数千以上になると処理に数秒~数十秒以上かかるようになります。このため、wcコマンドの実行回数をできるだけ減らす工夫が必要となります。幸いwcコマンドは一回のコマンド実行で複数のファイルを指定することができます。このためwcコマンド実行回数をファイル数分から1回にバッチ化できます。

バッチ処理化した例を以下に示します。(実行時間の測定等の処理は割愛)

NOTE: 例にあるwcコマンド結果のパース処理はかなり雑かつ手抜きです。真似しないでください。

good_performance_with_batch.py
import pathlib
from subprocess import check_output
cwp = pathlib.Path("/etc")
files = [f for f in cwp.glob("**/*") if f.is_file()]

# wcコマンドに全ファイルを引数として渡してバッチ処理化
args = [str(f) for f in files]
# NOTE: Pythonでは*でargsのリストを引数として展開できる
result = check_output(["wc", "--byte", *args])
total_byte = int(str(result).split(r"\n")[-2].split()[0])

print("file num:", len(files))
print("total byte:", total_byte)
コマンド処理のバッチ化 実行時間
なし 12,600 ms
あり 338 ms

同じwcコマンドを--bytesオプション付きで使用しているにも関わらず、バッチ処理化することにより処理時間を約1/40に短縮することができました。

このようなバッチ処理を何らかの形で提供しているUnix/Linuxコマンド、各種ライブラリ(ex. SQL, HTTP, I/O)はそこそこ存在します。性能面で問題がある場合は積極的に活用しましょう。

ちなみに外部コマンドのバッチ処理化をより突き詰めたい人は、以下の記事あたりが参考になります。タイトルには「シェルスクリプト」とありますが、基本的に外部コマンドを如何に効率良く活用するかに関する記事です。

シェルスクリプトを何万倍も遅くしないためには —— ループせずフィルタしよう
続: シェルスクリプトを何万倍も遅くしないためには —— やはりパイプは速いし解りやすい

I/O処理にバッファを使用する (=システムコール回数を減らす)

性能要求が高い状況化では、ファイルの読み書き、ネットワークの送受信などのI/O処理周りのシステムコールは大きなボトルネックになりがちです。このためシステムコールを減らすためにI/O処理のバッファリングが重要となります。

NOTE: 重要なのは「システムコール回数を減らすために何ができるか?」であり、その有効な手段としてI/Oバッファリングを活用しているということです。

例えばファイルの読み書きの場合であれば、それぞれの言語によって適切なバッファリングのアプローチを取ります。

  • Javaの場合:BufferedReader, BufferedWriter等を使用する。
  • C言語の場合:fwrite, fread関数等を使用する。
  • Pythonの場合:open時にバッファリングがデフォルトで有効。

Pythonの例を以下に示します。なおPythonの場合はデフォルトでI/Oバッファが有効となっているため、あえて無効にすることでその効果を確認してみます。(実行時間の測定等の処理は割愛)

# buffering=0を指定した場合はI/Oバッファが無効となる
f = open("file.txt", "wb", buffering=0)
s = b"single line"
for i in range(500000):
    f.write(s)
f.close()

以下の比較結果でも、I/Oバッファを有効にすると約10倍のwrite処理の性能改善があったことが分かります。

バッファ有無 実行時間
なし 2,223 ms
あり 245 ms

for, whileループを使わない (スクリプト型言語)

Python, Ruby, Perl等のスクリプト型言語はその動作の仕組み上、C言語やJava等のようにネイティブコードとして実行されるプログラミング言語と比較すると処理速度が非常に遅いです。特に単純な演算、ループ処理などは数十倍~数百倍くらい遅くなるケースも珍しくありません。

スクリプト言語であっても処理性能が重要になってくるような場面では、ループ処理に相当する部分を以下の手法で委譲してしまいましょう。

  • 組み込み関数、言語機能を使用する
    • Pythonのリスト内包表現
    • map, filter, apply等の関数 (C/C++言語で実装されたものが望ましい)
  • C言語等で実装されたライブラリ、モジュールを使用する

NOTE: 少し分かりづらいかもしれませんが、「スクリプト型言語のループ処理を、C言語実装にバッチ処理させている」ということから原則2に関連する手法の一つです。こういう発想をできるようにするのも大切なので日頃から意識したいものです。

例としてPythonで数値リストの総和の処理をループ処理で行った場合と、sum関数で行った場合を比較してみます。(実行時間の測定等の処理は割愛)

nums = range(50000000)

# ループ処理の場合
total  = 0
for n in nums:
    total += n

# sum関数の場合
total = sum(nums)

比較結果を以下に示しています。約5倍くらい高速になっています。

計算方法 実行時間
ループ処理 3,339 ms
sum関数 612 ms

sum関数はC言語で実装されているため高速に処理しています。join関数やmap関数などもC言語で実装されているため、うまく活用することでPython側のループ処理を回避して高速化することができます。

原則3. 高コストのものを再利用する

適切な文字列結合を選択する

プログラミング言語によっては、複数の文字列を結合して一つの文字列を作る文字列結合処理は非常にコストがかかります。

Python, Javaの場合は文字列結合のたびに新しい文字列オブジェクトが生成されるため非常に無駄が多いです。このためJavaであればStringBuilder、Pythonの場合はjoin()を活用したテクニック等を使うようにしましょう。詳細については以下の記事が参考になります。

【Java】文字列結合の速度比較
Pythonの処理速度を上げる方法 その2: 大量の文字列連結には、join()を使う

ちなみにJava7になってからs = "hello" + "s" + "!!"のような1行の文字列結合に限り自動的にStringBuilderを使うように最適化してくれるようになっていますが、ループ外の変数に対する文字列結合処理にはこの最適化は適用されませんので注意しましょう。

同じコネクションを再利用する

HTTP等のリクエストを送信する際にHTTPライブラリを使うと、ライブラリによっては1リクエスト毎にコネクションを生成&破棄します。例えばPythonのrequetsライブラリのrequests.get()はこのパターンに該当します。

1秒に数十~数百リクエストを送信する場合は、必ずHTTP1.1以降のPersistent Connectionで同じコネクションを使いまわすようにしましょう。

PythonのrequestsライブラリでSessionを活用した場合とそうでない場合を比較してみます。(実行時間の測定等の処理は割愛)

import requests

NUM = 3000
url = 'http://localhost:8080/'

# Sessionなしの場合
def without_session():
    for i in range(NUM):
        response = requests.get(URL)
        if response.status_code != 200:
            raise Exception("Error")

# Sessionありの場合
def with_session():
    with requests.Session() as ses:
        for i in range(NUM):
            response = ses.get(URL)
            if response.status_code != 200:
                raise Exception("Error")

without_session()
with_session()

比較結果を以下に示しています。同一ホスト内(Local)にWebサーバがある場合は約1.2倍、インターネット(Internet)にWebサーバがある場合は約2倍高速化しています。サーバへの接続コストが高い条件下ほど効果が大きくなることが分かりますね。

計算量 実行時間
Internet + Sessionなし 7,000 ms
Internet + Sessionあり 3.769 ms
Local + Sessionなし 6,606 ms
Local + Sessionあり 5.538 ms

その他

原則とか直接関連しない性能チューニングのトピックです。

DEBUGログ出力時に処理を発生させない

DEBUGログで固定文字列以外を使っている場合、仮にログレベルでDEBUGログを出力しないように設定していても引数を渡す部分は計算されるため処理コストがかかります。特に1秒に数千、数万回実行されるような箇所にこのようなDEBUGログがあると性能インパクトは非常に大きくなります。

計算を伴うDEBUGログ出力を入れる場合、必ずログレベルチェックのif文を組み合わせて、DEBUGログ出力を行わないログレベルの時には計算処理が発生しないようにしましょう。

l = [1, 2, 3, 4, 5]

# BAD:
# ログレベルに関わらず、リストの総和計算と文字列結合を実施
logging.debug("Sum=" + sum(l))

# GOOD:
if logging.isdebug():
    # DEBUGレベルの場合は一切実施されない
    logging.debug("Sum=" + sum(l))    

# GOOD: 固定文字列の場合は計算が発生しないためOK
logging.debug("Entered the function A")

NOTE: 遅延評価されるプログラミング言語の場合はこの限りではありません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

セッション機能を使用してログイン認証画面を作成する

お久しぶりです!

今回はログイン画面の作成にチャレンジしました。
(まだそこかいっというツッコミは受けます)

とりあえず成果物を作りたい!と思って色々調べたのですが、
その通りにやっても出来なくて( T_T)\(^-^ )

がしかし!
以下のWebサイトを実践したところ出来ました〜!
https://www.ipentec.com/document/java-servlet-login-order-form

(手抜き感凄い)
(自分用のメモとして書いてるのでそこは許してほしい)

きちんと解説も載ってて分かりやすい!
アレンジもしやすい!

私みたいに、Javaで何か作りたいけど何から作ったら良いか分からない。。。
そういえばゲーム作れるんだっけ、やってみようかな、、、え?結構難しそうなんだが(そして挫折)
という方はこちらを参考にして見てくださ〜い!

ちなみに私は根気強く一から調べて...というのは途中で折れてしまって
/(^o^)\うわあああ無理ぃ...(真っ暗)
となってしまうので、成果物を真っ先に作成してモチベーションを高め、
そこから解説見るだとかソースコードをいじって理解する方が向いてるみたいです。

勉強する気になっただけでも自分としてはかなり偉い← ので、
自分のこのやる気を損ねないようにするのも一つのやり方なのでは!と前向きに捉えています。笑

初心者の皆さん!一緒に頑張りましょう!!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む