1. 概要

この記事では、FindBugs、PMD、CheckStyleなどのコード分析ツールに搭載されている重要なルールのいくつかに焦点を当てます。

2. 循環的複雑度

2.1. 循環的複雑度とは何ですか?

コードの複雑さは重要ですが、測定するのは難しいメトリックです。 PMDは、コードサイズルールセクションの下に一連の堅牢なルールを提供します。これらのルールは、メソッドのサイズと構造の複雑さに関する違反を検出するように設計されています。

CheckStyleは、コーディング標準やフォーマット規則に照らしてコードを分析する機能で知られています。 ただし、複雑さメトリックを計算することにより、クラス/メソッド設計の問題を検出することもできます。

両方のツールで機能する最も関連性の高い複雑度測定の1つは、CC(循環的複雑度)です。

CC値は、プログラムの独立した実行パスの数を測定することで計算できます。

たとえば、次の方法では循環的複雑度が3になります。

public void callInsurance(Vehicle vehicle) {
    if (vehicle.isValid()) {
        if (vehicle instanceof Car) {
            callCarInsurance();
        } else {
            delegateInsurance();
        }
    }
}

CCは、条件ステートメントとマルチパートブール式のネストを考慮に入れます。

一般的に、CCに関して11より大きい値を持つコードは、非常に複雑であり、テストと保守が難しいと見なされます。

静的分析ツールで使用される一般的な値を以下に示します。

  • 1-4:複雑さが低い–テストが簡単
  • 5-7:中程度の複雑さ–許容できる
  • 8-10:高度な複雑さ–テストを容易にするためにリファクタリングを検討する必要があります
  • 11 +非常に複雑–テストが非常に難しい

複雑さのレベルは、コードのテスト容易性にも影響します。 CCが高いほど、関連するテストの実装が難しくなります。 実際、循環的複雑度の値は、100%のブランチカバレッジスコアを達成するために必要なテストケースの数を正確に示しています。

callInsurance()メソッドに関連するフローグラフは次のとおりです。

可能な実行パスは次のとおりです。

  • 0 => 3
  • 0 => 1 => 3
  • 0 => 2 => 3

数学的に言えば、CCは次の簡単な式を使用して計算できます。

CC = E - N + 2P
  • E:エッジの総数
  • N:ノードの総数
  • P:出口点の総数

2.2. 循環的複雑度を減らす方法は?

大幅に複雑でないコードを作成するために、開発者は状況に応じてさまざまなアプローチを使用する傾向があります。

  • デザインパターンを使用して、長いswitchステートメントを記述しないようにします。 ビルダーと戦略パターンは、コードサイズと複雑さの問題に対処するための良い候補かもしれません
  • コード構造をモジュール化し、単一責任原則を実装することにより、再利用可能で拡張可能なメソッドを記述します。
  • 他のPMDコードサイズルールに従うと、CCに直接影響を与える可能性があります。 過剰なメソッド長ルール、単一クラスのフィールドが多すぎる、単一メソッドの過剰なパラメーターリスト…など

また、コードのサイズと複雑さに関する次の原則とパターンを検討することもできます。 KISS(Keep It Simple and Stupid)の原則、および DRY(Do n’t Repeat Yourself)。

3. 例外処理ルール

例外に関連する欠陥は通常のことかもしれませんが、それらのいくつかは非常に過小評価されており、本番コードの重大な機能障害を回避するために修正する必要があります。

PMDとFindBugsは、例外に関するいくつかのルールセットを提供します。 例外を処理するときにJavaプログラムで重要と見なされる可能性のあるものを以下に示します。

3.1. 最後に例外をスローしないでください

ご存知かもしれませんが、Javaの finally {} ブロックは通常、ファイルを閉じたりリソースを解放したりするために使用されます。他の目的で使用すると、コードの臭いと見なされる場合があります。

一般的なエラーが発生しやすいルーチンは、 finally{}ブロック内で例外をスローします。

String content = null;
try {
    String lowerCaseString = content.toLowerCase();
} finally {
    throw new IOException();
}

このメソッドはNullPointerExceptionをスローすることになっていますが、驚くべきことに IOException をスローするため、呼び出し元のメソッドが間違った例外を処理する可能性があります。

3.2. 最終的にブロックに戻る

finally {} ブロック内でreturnステートメントを使用すると、混乱を招く可能性があります。 このルールが非常に重要である理由は、コードが例外をスローするたびに、returnステートメントによって破棄されるためです。

たとえば、次のコードはエラーなしで実行されます。

String content = null;
try {
    String lowerCaseString = content.toLowerCase();
} finally {
    return;
}

NullPointerException はキャッチされませんでしたが、finallyブロックのreturnステートメントによって破棄されました。

3.3. 例外でストリームを閉じられない

ストリームを閉じることがfinallyブロックを使用する主な理由の1つですが、それは簡単な作業ではないようです。

次のコードは、finallyブロック内の2つのストリームを閉じようとします。

OutputStream outStream = null;
OutputStream outStream2 = null;
try {
    outStream = new FileOutputStream("test1.txt");
    outStream2  = new FileOutputStream("test2.txt");
    outStream.write(bytes);
    outStream2.write(bytes);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        outStream.close();
        outStream2.close();
    } catch (IOException e) {
        // Handling IOException
    }
}

outStream.close()命令が IOException をスローした場合、 outStream2.close()はスキップされます。

簡単な修正は、別のtry/catchブロックを使用して2番目のストリームを閉じることです。

finally {
    try {
        outStream.close();
    } catch (IOException e) {
        // Handling IOException
    }
    try {
        outStream2.close();
    } catch (IOException e) {
        // Handling IOException
    }
}

連続するtry/ catch ブロックを回避するための優れた方法が必要な場合は、ApachecommonsのIOUtils.closeQuiety メソッドを確認してください。これにより、をスローせずにストリームを閉じる処理が簡単になります。 IOException

5. 悪い習慣

5.1. クラスcompareto()を定義し、Object.equals()を使用します

compareTo()メソッドを実装するときはいつでも、 equals()メソッドで同じことを行うことを忘れないでください。そうしないと、このコードによって返される結果が混乱する可能性があります。

Car car = new Car();
Car car2 = new Car();
if(car.equals(car2)) {
    logger.info("They're equal");
} else {
    logger.info("They're not equal");
}
if(car.compareTo(car2) == 0) {
    logger.info("They're equal");
} else {
    logger.info("They're not equal");
}

結果:

They're not equal
They're equal

混乱を解消するために、 Compareable、を実装するときに、 Object.equals()が呼び出されないようにすることをお勧めします。代わりに、次のようなものでオーバーライドしてみてください。

boolean equals(Object o) { 
    return compareTo(o) == 0; 
}

5.2. ヌルポインタの間接参照の可能性

NullPointerException (NPE)は、Javaプログラミングで最も遭遇する Exception と見なされ、FindBugsはNullPointeDの逆参照について文句を言ってスローを回避します。

NPEをスローする最も基本的な例は次のとおりです。

Car car = null;
car.doSomething();

NPEを回避する最も簡単な方法は、nullチェックを実行することです。

Car car = null;
if (car != null) {
    car.doSomething();
}

ヌルチェックはNPEを回避する可能性がありますが、広範囲に使用すると、コードの可読性に確実に影響します。

したがって、ヌルチェックなしでNPEを回避するために使用されるいくつかの技術は次のとおりです。

  • コーディング中はキーワードnullを避けてください:このルールは単純です。変数を初期化するとき、または値を返すときにキーワードnullを使用しないでください
  • @NotNullおよび@Nullableアノテーションを使用します
  • java.util.Optionalを使用します
  • Nullオブジェクトパターンを実装する

6. 結論

この記事では、静的分析ツールによって検出された重大な欠陥のいくつかについて、検出された問題に適切に対処するための基本的なガイドラインとともに、全体的な調査を行いました。

FindBugs PMD にアクセスすると、それぞれのルールの完全なセットを参照できます。