出典: DZone このガイドには、コードの読みやすさと信頼性を向上させるための Java のベスト プラクティスとリファレンスが含まれています。開発者は毎日正しい決定を下すという大きな責任を負っています。正しい決定を下すのに最も役立つのは経験です。また、全員がソフトウェア開発の豊富な経験を持っているわけではありませんが、誰もが他の人の経験を活用することができます。私の Java の経験から得た推奨事項をいくつか用意しました。これらが Java コードの読みやすさと信頼性の向上に役立つことを願っています。
プログラミングの原則
ただ動作するだけのコードは書かないでください。あなただけでなく、将来そのソフトウェアに取り組むことになる他の人にとっても保守しやすいコードを書くように努めてください。開発者は時間の 80% をコードの読み取りに費やし、20% をコードの作成とテストに費やします。したがって、読みやすいコードを書くことに集中してください。コードの動作を理解するために、コードにコメントは必要ありません。優れたコードを作成するには、ガイドラインとして使用できるプログラミング原則が多数あります。以下に最も重要なものを列挙します。- • KISS – 「Keep It Simple, Stupid」の略。開発者の初期段階では、複雑で曖昧な設計を実装しようとしていることに気づくかもしれません。
- • DRY - 「同じことを繰り返さないでください。」重複を避け、代わりにシステムまたはメソッドの 1 つの部分に重複を含めるようにしてください。
- • YAGNI - 「それは必要ありません。」突然、「さらに (機能、コードなど) を追加したらどうだろう?」と自問し始めた場合は、実際に追加する価値があるかどうかを考える必要があるでしょう。
- •スマート コードの代わりにクリーン コード- 簡単に言うと、エゴを戸外に置いて、スマート コードを書くことを忘れます。スマートなコードではなく、クリーンなコードが必要です。
- •時期尚早の最適化を避ける- 時期尚早の最適化の問題は、ボトルネックがプログラムのどこにあるか、それが現れるまでわからないことです。
- •単一の責任- プログラム内の各クラスまたはモジュールは、特定の機能の 1 ビットを提供することのみを考慮する必要があります。
- •実装の継承ではなく構成 - 複雑な動作を持つオブジェクトには、クラスを継承して新しい動作を追加するのではなく、個別の動作を持つオブジェクトのインスタンスを含める必要があります。
- •オブジェクト体操は、9 つのルールのセットとして設計されたプログラミング演習です。
- •フェイルファスト、ファスト停止- この原則は、予期しないエラーが発生した場合に現在の操作を停止することを意味します。この原則に従うことで、より安定した動作が得られます。
パッケージ
- 技術レベルではなく、主題分野ごとにパッケージを構築することを優先します。
- 技術的な理由でクラスを編成するのではなく、誤用を防ぐためにカプセル化と情報の隠蔽を促進するレイアウトを優先します。
- パッケージを不変の API があるかのように扱います。内部処理のみを目的とした内部メカニズム (クラス) を公開しないでください。
- パッケージ内でのみ使用することを目的としたクラスを公開しないでください。
クラス
静的
- 静的クラスの作成を許可しないでください。常にプライベート コンストラクターを作成します。
- 静的クラスは不変のままである必要があり、サブクラス化やマルチスレッド クラスを許可しないでください。
- 静的クラスは向きの変更から保護され、リスト フィルタリングなどのユーティリティとして提供される必要があります。
継承
- 継承よりも合成を選択してください。
- 保護されたフィールドを設定しないでください。代わりに、安全なアクセス方法を指定してください。
- クラス変数をFinalとしてマークできる場合は、そうしてください。
- 継承が期待されない場合は、クラスをFinalにします。
- サブクラスによるメソッドのオーバーライドが許可されないことが予想される場合は、メソッドをFinalとしてマークします。
- コンストラクターが必要ない場合は、実装ロジックなしでデフォルトのコンストラクターを作成しないでください。デフォルトのコンストラクターが指定されていない場合、Java は自動的にデフォルトのコンストラクターを提供します。
インターフェース
- 定数パターンのインターフェイスは使用しないでください。クラスが API を実装して汚染する可能性があるためです。代わりに静的クラスを使用してください。これには、静的ブロックでより複雑なオブジェクトの初期化 (コレクションの作成など) を実行できるという追加の利点があります。
- インターフェースの過度の使用は避けてください。
- インターフェイスを実装するクラスが 1 つだけあると、インターフェイスの過剰使用につながり、良いことよりも害が大きくなる可能性があります。
- 「実装ではなくインターフェイス用のプログラム」とは、各ドメイン クラスを多かれ少なかれ同一のインターフェイスにバンドルする必要があるという意味ではありません。これを行うと、YAGNIが壊れることになります。
- クライアントが関心のあるメソッドのみを認識できるように、インターフェイスは常に小さく具体的なものにしてください。SOLID の ISP を確認してください。
ファイナライザー
- #finalize()オブジェクトは、リソースをクリーンアップするとき (ファイルを閉じるなど) の失敗から保護する手段としてのみ、慎重に使用する必要があります。常に明示的なクリーンアップ メソッド ( close()など) を提供してください。
- 継承階層では、常にtryブロックで親のFinalize()を呼び出します。クラスのクリーンアップは、 finallyブロック内で行う必要があります。
- 明示的なクリーンアップ メソッドが呼び出されず、ファイナライザーがリソースを閉じた場合は、このエラーを記録します。
- ロガーが使用できない場合は、スレッドの例外ハンドラーを使用します (最終的に、ログにキャプチャされる標準エラーが渡されます)。
一般的なルール
ステートメント
アサーションは通常、前提条件チェックの形式で、「フェイルファスト、ストップファスト」の契約を強制します。可能な限り原因に近いプログラミング エラーを特定するために、これらを広く使用する必要があります。オブジェクトの状態:- • オブジェクトを作成したり、無効な状態にしたりしてはなりません。
- • コンストラクターとメソッドでは、常にテストを使用してコントラクトを記述し、強制します。
- • Java キーワードのアサートは、無効にすることができ、通常は脆弱な構造であるため、使用しないでください。
- • Assertionsユーティリティ クラスを使用して、前提条件チェックの詳細なif-else条件を回避します。
ジェネリック
完全かつ非常に詳細な説明は、「Java Generics FAQ」で参照できます。以下は、開発者が知っておくべき一般的なシナリオです。- 可能な限り、基本クラス/インターフェイスを返すのではなく、型推論を使用することをお勧めします。
// MySpecialObject o = MyObjectFactory.getMyObject(); public
T getMyObject(int type) { return (T) factory.create(type); } - 型を自動的に決定できない場合は、インライン化します。
public class MySpecialObject extends MyObject
{ public MySpecialObject() { super(Collections.emptyList()); // This is ugly, as we loose type super(Collections.EMPTY_LIST(); // This is just dumb // But this is beauty super(new ArrayList ()); super(Collections. emptyList()); } } - ワイルドカード:
構造体から値を取得するだけの場合は拡張ワイルドカードを使用し、構造体に値のみを入れる場合はスーパーワイルドカードを使用し、その両方を行う場合はワイルドカードを使用しないでください。
- みんな大好きPECS!(プロデューサー-拡張、コンシューマー-スーパー)
- プロデューサー T にはFooを使用します。
- コンシューマ T にはFoo を使用します。
シングルトン
シングルトンは決して古典的なデザイン パターンスタイルで記述すべきではありません。これは C++ では問題ありませんが、Java では適切ではありません。適切にスレッドセーフであるとしても、次のことは決して実装しないでください (パフォーマンスのボトルネックになる可能性があります)。public final class MySingleton {
private static MySingleton instance;
private MySingleton() {
// singleton
}
public static synchronized MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
}
遅延初期化が本当に必要な場合は、これら 2 つのアプローチを組み合わせて使用できます。
public final class MySingleton {
private MySingleton() {
// singleton
}
private static final class MySingletonHolder {
static final MySingleton instance = new MySingleton();
}
public static MySingleton getInstance() {
return MySingletonHolder.instance;
}
}
Spring: デフォルトでは、Bean はシングルトン スコープで登録されます。これは、コンテナーによって 1 つのインスタンスのみが作成され、すべてのコンシューマーに接続されることを意味します。これにより、パフォーマンスやバインディングの制限なしに、通常のシングルトンと同じセマンティクスが提供されます。
例外
-
修正可能な状態にはチェック例外を使用し、プログラミング エラーには実行時例外を使用します。例: 文字列から整数を取得します。
悪い例: NumberFormatException は RuntimeException を拡張するため、プログラミング エラーを示すことを目的としています。
-
例外は、ドメイン レベルの適切な場所で処理する必要があります。
間違った方法 - データ オブジェクト層は、データベース例外が発生したときに何をすべきかを知りません。
class UserDAO{ public List
getUsers(){ try{ ps = conn.prepareStatement("SELECT * from users"); rs = ps.executeQuery(); //return result }catch(Exception e){ log.error("exception") return null }finally{ //release resources } }} 推奨される方法- データ層は単純に例外を再スローし、例外を処理するかどうかの責任を正しい層に渡す必要があります。
=== RECOMMENDED WAY === Data layer should just retrow the exception and transfer the responsability to handle the exception or not to the right layer. class UserDAO{ public List
getUsers(){ try{ ps = conn.prepareStatement("SELECT * from users"); rs = ps.executeQuery(); //return result }catch(Exception e){ throw new DataLayerException(e); }finally{ //release resources } } } -
通常、例外は発行時ではなく、実際に処理されたときにログに記録される必要があります。ログ例外がスローまたは再スローされると、ログ ファイルがノイズでいっぱいになる傾向があります。また、例外スタック トレースには、例外がスローされた場所が記録されることにも注意してください。
-
標準例外の使用をサポートします。
-
リターンコードではなく例外を使用してください。
次のことは行わないでください。
// String str = input string
Integer value = null;
try {
value = Integer.valueOf(str);
} catch (NumberFormatException e) {
// non-numeric string
}
if (value == null) {
// handle bad string
} else {
// business logic
}
正しい使用方法:
// String str = input string
// Numeric string with at least one digit and optional leading negative sign
if ( (str != null) && str.matches("-?\\d++") ) {
Integer value = Integer.valueOf(str);
// business logic
} else {
// handle bad string
}
Equals と HashCode
適切なオブジェクトとハッシュ コードの等価性メソッドを作成する際には、考慮すべき問題が数多くあります。使いやすくするには、 java.util.Objects のequalsとhashを使用します。public final class User {
private final String firstName;
private final String lastName;
private final int age;
...
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (!(o instanceof User)) {
return false;
}
User user = (User) o;
return Objects.equals(getFirstName(), user.getFirstName()) &&
Objects.equals(getLastName(),user.getLastName()) &&
Objects.equals(getAge(), user.getAge());
}
public int hashCode() {
return Objects.hash(getFirstName(),getLastName(),getAge());
}
}
資源管理
リソースを安全に解放する方法: try-with-resourcesステートメントは、ステートメントの最後に各リソースが確実に閉じられるようにします。java.lang.AutoCloseable を実装する任意のオブジェクト ( java.io.Closeableを実装するすべてのオブジェクトを含む) をリソースとして使用できます。private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path)))
try {
// business logic
}
}
シャットダウンフックを使用する
JVM が正常にシャットダウンするときに呼び出されるシャットダウン フックを使用します。(ただし、停電などによる突然の中断には対応できません) これは、System.runFinalizersOnExit()が true (デフォルトは false)の場合にのみ実行されるFinalize()メソッドを宣言する代わりに推奨される代替方法です。 。public final class SomeObject {
var distributedLock = new ExpiringGeneralLock ("SomeObject", "shared");
public SomeObject() {
Runtime
.getRuntime()
.addShutdownHook(new Thread(new LockShutdown(distributedLock)));
}
/** Code may have acquired lock across servers */
...
/** Safely releases the distributed lock. */
private static final class LockShutdown implements Runnable {
private final ExpiringGeneralLock distributedLock;
public LockShutdown(ExpiringGeneralLock distributedLock) {
if (distributedLock == null) {
throw new IllegalArgumentException("ExpiringGeneralLock is null");
}
this.distributedLock = distributedLock;
}
public void run() {
if (isLockAlive()) {
distributedLock.release();
}
}
/** @return True if the lock is acquired and has not expired yet. */
private boolean isLockAlive() {
return distributedLock.getExpirationTimeMillis() > System.currentTimeMillis();
}
}
}
リソースをサーバー間で分散することで、リソースが完全になる (更新可能になる) ようになります。(停電などの突然の中断からの復旧が可能となります。)ExpiringGeneralLock (すべてのシステムに共通のロック) を使用する上記のコード例を参照してください。
日付時刻
Java 8 では、java.time パッケージに新しい Date-Time API が導入されています。Java 8 では、古い Date-Time API の非スレッド、貧弱な設計、複雑なタイムゾーン処理などの欠点に対処するために、新しい Date-Time API が導入されています。平行度
一般的なルール
- 次のライブラリはスレッドセーフではないので注意してください。オブジェクトが複数のスレッドで使用されている場合は、常にオブジェクトと同期します。
- 日付 (不変ではありません) - スレッドセーフな新しい日付/時刻 API を使用します。
- SimpleDateFormat - スレッドセーフな新しい Date-Time API を使用します。
- 変数をvolatileにするよりも、java.util.concurrent.atomicクラスを使用することを好みます。
- アトミック クラスの動作は平均的な開発者にとってより明白ですが、volatileではJava メモリ モデルを理解する必要があります。
- アトミック クラスは、揮発性変数をより便利なインターフェイスにラップします。
- volatileが適切な使用例を理解します。(記事を参照)
- 呼び出し可能を使用する
チェック例外が必要だが戻り値の型がない場合。Void はインスタンス化できないため、意図を伝達し、安全に nullを返すことができます。
ストリーム
- java.lang.Thread は廃止されるべきです。これは正式には当てはまりませんが、ほとんどの場合、java.util.concurrentパッケージが問題に対するより明確な解決策を提供します。
- java.lang.Threadを拡張することは悪い習慣であると考えられています。代わりにRunnableを実装し 、コンストラクター内でインスタンスを使用して新しいスレッドを作成します (継承に対する合成ルール)。
- 並列処理が必要な場合は、エグゼキュータとスレッドを優先します。
- 作成されたスレッドの構成を管理するには、独自のカスタム スレッド ファクトリを指定することを常にお勧めします (詳細はこちら)。
- クリティカルでないスレッドのエグゼキュータで DaemonThreadFactory を使用すると、サーバーのシャットダウン時にスレッド プールがすぐにシャットダウンできるようになります (詳細はこちら)。
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
thread.setDaemon(true);
return thread;
});
- Java の同期はそれほど遅くなくなりました (55 ~ 110 ns)。ロックを二重チェックするなどのトリックを使用して回避しないでください。
- ユーザーはクラス/インスタンスと同期できるため、クラスではなく内部オブジェクトとの同期を優先します。
- デッドロックを避けるために、複数のオブジェクトを常に同じ順序で同期してください。
- クラスと同期しても、その内部オブジェクトへのアクセスは本質的にブロックされません。リソースにアクセスするときは、常に同じロックを使用してください。
- synchronized キーワードはメソッド シグネチャの一部とみなされないため、継承されないことに注意してください。
- 過剰な同期は避けてください。これにより、パフォーマンスの低下やデッドロックが発生する可能性があります。synchronizedキーワードは、同期が必要なコード部分にのみ使用してください。
コレクション
- 可能な限り、マルチスレッド コードで Java-5 並列コレクションを使用してください。安全で優れた特性を持っています。
- 必要に応じて、synchronizedList の代わりに CopyOnWriteArrayList を使用します。
- Collections.unmodifiable list(...) を使用するか、パラメータとして受け取ったときにコレクションをnew ArrayList(list)にコピーします。クラスの外部からローカル コレクションを変更しないでください。
- new ArrayList (list)を使用してリストを外部から変更することを避け、常にコレクションのコピーを返します。
- 各コレクションは個別のクラスでラップする必要があるため、コレクションに関連付けられた動作にはホームが設定されます(例: メソッドのフィルタリング、各要素へのルールの適用)。
その他
- 匿名クラスではなくラムダを選択してください。
- ラムダではなくメソッド参照を選択してください。
- int 定数の代わりに列挙型を使用します。
- 正確な答えが必要な場合は、float と double の使用を避け、代わりに Money などの BigDecimal を使用してください。
- ボックス化されたプリミティブではなくプリミティブ タイプを選択します。
- コード内でマジック ナンバーを使用することは避けてください。定数を使用します。
- Null を返さないでください。「Optional」を使用してメソッド クライアントと通信します。コレクションについても同様です。null ではなく、空の配列またはコレクションを返します。
- 不要なオブジェクトの作成を避け、オブジェクトを再利用し、不必要な GC クリーンアップを避けてください。
遅延初期化
遅延初期化はパフォーマンスの最適化です。何らかの理由でデータが「高価」であると考えられる場合に使用されます。Java 8 では、これに機能プロバイダー インターフェイスを使用する必要があります。== Thread safe Lazy initialization ===
public final class Lazy {
private volatile T value;
public T getOrCompute(Supplier supplier) {
final T result = value; // Just one volatile read
return result == null ? maybeCompute(supplier) : result;
}
private synchronized T maybeCompute(Supplier supplier) {
if (value == null) {
value = supplier.get();
}
return value;
}
}
Lazy lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
今のところはここまでです。お役に立てば幸いです。
GO TO FULL VERSION