【Java】for文のnew宣言箇所に翻弄された話

始めに

今日は仕事でJavaのメモリ関連でドハマりしたので、忘備録を残しておく。

(さっそくゲーム以外の記事じゃん!とかは言わないこと)

 

問題

さて、今回問題になったのは「ループ内でインスタンスを使用する際の初期化位置」について。

以下のパターンのどれにするか?という話。

※daoは事前に定義済み。

 

【パターンA:ループ内定義パターン】

for(int count=0; count < N; count++){

  testDto dto = new testDto();

  dto.setHoge();

  dao.insert(dto);

}

 

【パターンB:ループ外定義パターン】

testDto dto = new testDto();

for(int count=0; count < N; count++){

  dto.setHoge();

  dao.insert(dto);

}

 

【パターンC:AB折衷案】

testDto dto = null;

for(int count=0; count < N; count++){

  dto = new testDto();

  dto.setHoge();

  dao.insert(dto);

}

 

解答

本件、いろんな人に聞いてみたが想像以上に意見がばらけていた。

では、それぞれの主張(メリット・デメリット)を列記。

 

【パターンA:ループ内定義パターン】

メリット:スコープが明確

デメリット:ループ回数が多いとメモリリーク

 

少ないかな?と思いきやかなり支持層がいてびっくりした。

(なんなら、検索したTOPのブログ記事はこの方式だった。)

for文など特定のブロック内でしか使用しない変数・インスタンスはそのブロック内で定義すること!というスコープ意識が強い人に支持されている印象。

デメリットとして、インスタンスの宣言の度に新しいメモリが確保してしまうことがある。

確保されたメモリは「全てのループが完了するまでGCの対象にならない」※ので、ループ回数が多いとどんどんメモリを食い潰してしまい、最悪メモリ不足で異常終了してしまう。

 

※ぶっちゃけ、1ループごとにGCの対象になるかと思っていたのだが、実際に検証してみたらGC対象外になっていた。ちょっと意外。もしかしたら原因が他にあるかもしれないが…

 

【パターンB:ループ外定義パターン】

メリット:メモリ確保が少ない

デメリット:初期化処理の内容次第でバグの原因に

 

負荷の高いnew処理をループの中で呼ぶんじゃねぇ!という派閥。

ループの前で1回しかnewを行わないため、とにかくメモリ確保量が少なく処理も早い。

…が、同じメモリ領域を使いまわすため、ループの度に中身をちゃんと綺麗にしないと悲惨なことに。

最初にコーディングした人はそんなミスしないと思うが、その後何も知らない保守担当者が中途半端に修正しちゃってバグを作った例を知っている。

StringBuilderなどの「高速&確実な初期化処理」を持っているクラスであれば、この方式を採用するのが良いかもしれない。

 

【パターンC:折衷案パターン】

メリット:メモリ確保が少ない&初期化がしっかりされる

デメリット:newの回数は多いので処理時間はAと同じ&スコープ的にはやや汚い

 

変数をループ外、インスタンスをループ内で定義することでnewの度に同じメモリ領域を初期化するパターン。

確保したメモリは全ての処理が終了するまでGC対象とならないが、同一メモリしか使用しないのでおそらくメモリリークを引き起こすことはないと思う。

AとBの良いとこどりであり、AとBの悪いところもしっかり引き継いでいる。

 

今回は短い処理のため目立っていないがループの外で変数宣言しようとすると、「実際の使用箇所とめっちゃ離れる」か「処理の途中に急に変数宣言が出てくる」ことになる。

可読性を意識する人は上記を嫌うと思う。

 

まとめ

それぞれのパターンにメリット・デメリットがあり、従事しているプロジェクトが「何を」重視するかでどのパターンを選ぶかは変わってくる。

「絶対に〇〇じゃないと嫌!」という思考ではなく、上記のメリット・デメリットを踏まえた上で柔軟&適切なパターンを選択できるようになりたいね。

 

でも、メモリリークするコードは残すなよ!!

 

おまけ

上記以外で以下のパターンも話に出てきたので、追記。

 

【パターンD:変数の定義とインスタンスの生成をプライベートメソッド化】

 

for(int count=0; count < 100000; count++){

  testPrivateMethod();

}

 

private void testPrivateMethod(){

  testDto dto = new testDto();

  dto.setHoge();

  dao.insert(dto);

}

 

 

メリット:1ループごとに領域はGC対象&スコープが明確

デメリット:引数が増えがち&毎回新しいメモリ領域を確保してる

 

プライベートメソッドなので1ループごとに変数やインスタンスGCの対象となり、メモリリークの心配はなし。見ての通りスコープも明確。

…ではあるのだが、実際はこんなきれいなメソッドにはならずに大量の引数を渡したりすることになって、スコープ周りはゴチャゴチャしそうな気もする。

(それは設計の問題だろ!と言われれば返す言葉もない)

 

それと、GCの対象になるというだけで毎回新しいメモリを確保しているという点はパターンAと同じなのでループ中のメモリ使用量はかなり大きくなるし、メソッドの呼び出しやnewの回数も多いので決して「早い」処理ではない。

 

これもまた、一長一短といった感じだろうか?

個人的にはメソッドは細かい方が好きなのでこのパターンは割と好き。