JavaRush /Java Blog /Random-JA /FindBugs は Java の学習に役立ちます
articles
レベル 15

FindBugs は Java の学習に役立ちます

Random-JA グループに公開済み
静的コード アナライザーは、不注意によって発生したエラーを発見するのに役立つため、人気があります。しかし、さらに興味深いのは、無知から生じた間違いを修正するのに役立つことです。たとえその言語の公式ドキュメントにすべてが書かれていたとしても、すべてのプログラマーがそれを注意深く読んだという事実はありません。そして、プログラマなら理解できると思いますが、すべてのドキュメントを読むのはうんざりするでしょう。この点において、静的アナライザーは、あなたの隣に座ってコードを書くのを見守ってくれる経験豊富な友人のようなものです。彼は、「ここはコピーして貼り付けたときに間違えた場所です」と言うだけでなく、「いいえ、そのように書くことはできません。自分でドキュメントを見てください。」とも言います。そのような友人は、ドキュメント自体よりも役に立ちます。なぜなら、彼は仕事で実際に遭遇する事柄だけを提案し、あなたにとって決して役に立たない事柄については沈黙するからです。この投稿では、FindBugs 静的アナライザーの使用から学んだ Java の複雑さのいくつかについて説明します。おそらく、あなたにとっても予想外のことが起こるかもしれません。すべての例が推測的なものではなく、実際のコードに基づいていることが重要です。

三項演算子 ?:

三項演算子ほど単純なものはないように思えますが、これには落とし穴があります。どちらのデザインにも基本的な違いはないと信じていましたが Type var = condition ? valTrue : valFalse;Type var; if(condition) var = valTrue; else var = valFalse; ここに微妙な点があることが分かりました。三項演算子は複雑な式の一部である可能性があるため、その結果はコンパイル時に決定される具象型である必要があります。したがって、たとえば、if 形式で true 条件を指定すると、コンパイラは valTrue を Type に直接導き、三項演算子の形式で、まず共通の型 valTrue と valFalse に導きます (valFalse はそうでないにもかかわらず)。評価されます)、その結果はタイプ Type につながります。式にプリミティブ型とそのラッパー (Integer、Double など) が含まれる場合、キャスト ルールは完全に自明ではありません。すべてのルールは JLS 15.25 で詳しく説明されています。いくつかの例を見てみましょう。 Number n = flag ? new Integer(1) : new Double(2.0); フラグが設定されている場合、n はどうなりますか? 値が 1.0 の Double オブジェクト。コンパイラは、オブジェクトを作成するという不器用な試みを面白いと判断します。2 番目と 3 番目の引数は異なるプリミティブ型のラッパーであるため、コンパイラはそれらをアンラップし、より正確な型 (この場合は double) を生成します。そして、代入に対して三項演算子を実行した後、再度ボックス化が行われます。本質的に、このコードは次と同等です。 Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); コンパイラの観点からは、コードには問題はなく、完全にコンパイルされます。しかし、FindBugs は次のように警告します。
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: TestTernary.main(String[]) のプリミティブ値がボックス化解除され、三項演算子に対して強制されます。 ラップされたプリミティブ値がボックス化解除され、条件付き三項演算子 (b? e1: e2 演算子) の評価の一部として別のプリミティブ型に変換されます。 )。Java のセマンティクスでは、e1 と e2 がラップされた数値の場合、値はボックス化されていない状態で共通の型に変換/強制されることが義務付けられています (たとえば、e1 が Integer 型で、e2 が Float 型である場合、e1 はボックス化されていません。浮動小数点値に変換され、ボックス化されます。JLS セクション 15.25 を参照してください。もちろん、FindBugs は、Integer.valueOf(1) が new Integer(1) よりも効率的であることも警告しますが、それは誰もがすでに知っています。
または次の例: Integer n = flag ? 1 : null; フラグが設定されていない場合、作成者は n に null を入れたいと考えています。うまくいくと思いますか?はい。しかし、事態を複雑にしましょう。 Integer n = flag1 ? 1 : flag2 ? 2 : null; 大きな違いはないようです。ただし、両方のフラグがクリアされている場合、この行は NullPointerException をスローします。右側の三項演算子のオプションは int と null であるため、結果の型は Integer になります。左側のオプションは int と Integer であるため、Java ルールに従って結果は int になります。これを行うには、例外をスローする intValue を呼び出してボックス化解除を実行する必要があります。コードはこれと同等です。 Integer n; if( flag1 ) n = Integer.valueOf(1); else { if( flag2 ) n = Integer.valueOf(Integer.valueOf(2).intValue()); else n = Integer.valueOf(((Integer)null).intValue()); } ここで、FindBugs はエラーを疑うのに十分な 2 つのメッセージを生成します。
BX_UNBOXING_IMMEDIATELY_REBOXED: ボックス化された値がボックス化解除され、TestTernary.main(String[]) ですぐに再ボックス化されます。 NP_NULL_ON_SOME_PATH: TestTernary.main(String[]) で null の null ポインター逆参照が発生する可能性があります。 実行すると、 null 値は逆参照されるため、コードの実行時に NullPointerException が生成されます。
さて、このトピックに関する最後の例です。 double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } このコードが機能しないのは驚くべきことではありません。プリミティブ型を返す関数はどのようにして null を返すのでしょうか? 驚いたことに、問題なくコンパイルできます。そうですね、コンパイルが完了する理由はすでに理解できました。

日付形式

Java で日付と時刻をフォーマットするには、DateFormat インターフェイスを実装するクラスを使用することをお勧めします。たとえば、次のようになります。 public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } 多くの場合、クラスは同じ形式を繰り返し使用します。多くの人は最適化というアイデアを思いつくでしょう。共通のインスタンスを使用できるのに、なぜ毎回フォーマット オブジェクトを作成するのでしょうか? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } とても美しくてクールですが、残念ながら機能しません。より正確に言えば、動作しますが、時々壊れます。実際、DateFormat のドキュメントには次のように書かれています。
日付形式は同期されません。スレッドごとに個別の形式インスタンスを作成することをお勧めします。複数のスレッドが同時にフォーマットにアクセスする場合は、外部で同期する必要があります。
これは、SimpleDateFormat の内部実装を見ると当てはまります。format() メソッドの実行中、オブジェクトはクラス フィールドに書き込むため、2 つのスレッドから SimpleDateFormat を同時に使用すると、一定の確率で不正な結果が発生します。これについて FindBugs は次のように書いています。
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: TestDate.getDate() の静的 java.text.DateFormat メソッドの呼び出し JavaDoc に記載されているように、DateFormat は本質的にマルチスレッドでの使用には安全ではありません。検出器は、静的フィールドを介して取得された DateFormat のインスタンスへの呼び出しを検出しました。これは疑わしいようです。詳細については、Sun Bug #6231579 および Sun Bug #6178997 を参照してください。

BigDecimal の落とし穴

BigDecimal クラスを使用すると任意の精度の小数を格納できることを学び、それに double のコンストラクターがあることを見て、すべてが明確であり、次のように実行できると判断する人もいます。 System.out.println(new BigDecimal( 1.1)); これを実際に禁止している人はいませんが、結果は予期せぬように見えるかもしれません: 1.100000000000000088817841970012523233890533447265625。これは、原始 double が IEEE754 形式で格納されているために発生します。IEEE754 形式では、1.1 を完全に正確に表すことは不可能です (2 進数系では、無限の周期分数が得られます)。したがって、1.1 に最も近い値がそこに格納されます。それどころか、BigDecimal(double) コンストラクターは正確に機能します。IEEE754 の指定された数値を 10 進数形式に完全に変換します (最後の 2 進数の小数部は、常に最後の 10 進数として表現できます)。BigDecimal として 1.1 を正確に表したい場合は、new BigDecimal("1.1") または BigDecimal.valueOf(1.1) を記述できます。番号をすぐに表示せずに、それを使用して何らかの操作を行うと、エラーの原因がわからない可能性があります。FindBugs は、同じアドバイスを与える警告 DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE を発行します。もう 1 つの点があります。 BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); 実際、d1 と d2 は同じ数値を表しますが、equals は数値の値だけでなく現在の順序 (小数点以下の桁数) も比較するため、false を返します。これはドキュメントに書かれていますが、equals などのおなじみのメソッドのドキュメントを読む人はほとんどいないでしょう。このような問題はすぐには起こらないかもしれません。残念ながら、FindBugs 自体はこれについて警告しませんが、このバグを考慮した人気のある拡張機能 fb-contrib があります。
MDM_BIGDECIMAL_EQUALS 2 つの java.math.BigDecimal 数値を比較するために、equals() が呼び出されます。これは通常間違いです。2 つの BigDecimal オブジェクトは、値とスケールの両方が等しい場合にのみ等しいため、2.0 は 2.00 と等しくなりません。BigDecimal オブジェクトの数学的等価性を比較するには、代わりに CompareTo() を使用します。

改行と printf

多くの場合、C の後に Java に切り替えたプログラマーは、 PrintStream.printf (およびPrintWriter.printfなど) を喜んで見つけます。なるほど、C と同じように、新しいことを学ぶ必要がないことはわかりました。実際には違いがあります。そのうちの 1 つは行の翻訳にあります。C 言語はテキスト ストリームとバイナリ ストリームに分かれています。何らかの方法で「\n」文字をテキスト ストリームに出力すると、システム依存の改行 (Windows では「\r\n」) に自動的に変換されます。Java にはそのような分離はありません。正しい文字シーケンスが出力ストリームに渡される必要があります。これは、たとえば、PrintStream.println ファミリのメソッドによって自動的に行われます。ただし、printf を使用する場合、フォーマット文字列で '\n' を渡すと、システムに依存する改行ではなく、単なる '\n' になります。たとえば、次のコードを書いてみましょう: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); 結果をファイルにリダイレクトすると、次のことがわかります: FindBugs は Java の学習に役立ちます - 1 このように、1 つのスレッド内で改行の奇妙な組み合わせが得られる可能性があり、これはぞんざいに見え、一部のパーサーの心を驚かせる可能性があります。特に主に Unix システムで作業している場合、このエラーは長い間気づかれない可能性があります。printf を使用して有効な改行を挿入するには、特殊な書式設定文字「%n」が使用されます。これについて FindBugs は次のように書いています。
VA_FORMAT_STRING_USES_NEWLINE: TestNewline.main(String[]) では書式文字列に \n ではなく %n を使用する必要があります。この書式文字列には改行文字 (\n) が含まれています。フォーマット文字列では、一般に、プラットフォーム固有の行区切り文字を生成する %n を使用することをお勧めします。
おそらく一部の読者にとっては、上記の内容はすべて長い間知られていたことでしょう。しかし、静的アナライザからは、使用されているプログラミング言語の新しい機能が明らかになる興味深い警告が彼らに届くことはほぼ確実です。
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION