Java 開発者の 99% が犯す 5 つの間違い
出典:
Medium この投稿では、多くの Java 開発者が犯す最も一般的な間違いについて学びます。 Java プログラマーとして、コードのバグの修正に多くの時間を費やすことがどれほど悪いことであるかを私は知っています。場合によっては、これには数時間かかることがあります。ただし、多くのエラーは、開発者が基本的なルールを無視しているために発生します。つまり、これらは非常に低レベルのエラーです。今日は、よくあるコーディングの間違いをいくつか取り上げ、それらを修正する方法を説明します。日常業務でのトラブルを回避する一助となれば幸いです。
Objects.equals を使用したオブジェクトの比較
皆さんもこの方法に精通していると思います。多くの開発者が頻繁に使用します。JDK 7 で導入されたこの手法は、オブジェクトを迅速に比較し、煩わしい null ポインタ チェックを効果的に回避するのに役立ちます。しかし、この方法は時々誤って使用されることがあります。私が言いたいのは次のとおりです。
Long longValue = 123L;
System.out.println(longValue==123);
System.out.println(Objects.equals(longValue,123));
== をObjects.equals()に置き換えると間違った結果が生じる のはなぜですか? これは、
==コンパイラが、 longValueパッケージ化型に対応する基礎となるデータ型を取得し、それをその基礎となるデータ型と比較するためです。これは、コンパイラが定数を基礎となる比較データ型に自動的に変換するのと同じです。
Objects.equals()メソッドを使用した後、コンパイラ定数のデフォルトの基本データ型は
intになります。
以下はObjects.equals() のソース コードです。ここで、
a.equals(b) はLong.equals()を使用してオブジェクトの型を決定します。これは、コンパイラが定数の型が
intであると想定しているため、比較の結果は false になる必要があるために発生します。
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
理由がわかれば、エラーを修正するのは非常に簡単です。
Objects.equals(longValue,123L)のように定数のデータ型を宣言するだけです。ロジックが厳密であれば上記の問題は発生しません。私たちがしなければならないのは、明確なプログラミング ルールに従うことです。
間違った日付形式
日常の開発では日付を変更する必要があることがよくありますが、多くの人が間違った形式を使用しており、それが予期せぬ事態を引き起こします。以下に例を示します。
Instant instant = Instant.parse("2021-12-31T00:00:00.00Z");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
System.out.println(formatter.format(instant));
これは、
YYYY-MM-dd形式を使用して日付を 2021 から 2022 に変更します。そんなことはすべきではありません。なぜ?これは、
Java DateTimeFormatter の「YYYY」パターンが、年を毎週木曜日として定義する ISO-8601 標準に基づいているためです。しかし、2021 年 12 月 31 日は金曜日だったため、プログラムは誤って 2022 年を示しています。
これを回避するには、 yyyy-MM-dd形式を使用して日付をフォーマットする必要があります。このエラーはめったに発生せず、新年の到来時にのみ発生します。しかし、私の会社ではそれが原因で生産障害が発生しました。
ThreadPool での ThreadLocal の使用
ThreadLocal 変数を作成すると、その変数にアクセスするスレッドはスレッド ローカル変数を作成します。こうすることで、スレッドの安全性の問題を回避できます。ただし、
スレッド プールで
ThreadLocal を使用している場合は注意が必要です。コードによって予期しない結果が生じる可能性があります。簡単な例として、電子商取引プラットフォームがあり、ユーザーが製品の購入完了を確認するために電子メールを送信する必要があるとします。
private ThreadLocal<User> currentUser = ThreadLocal.withInitial(() -> null);
private ExecutorService executorService = Executors.newFixedThreadPool(4);
public void executor() {
executorService.submit(()->{
User user = currentUser.get();
Integer userId = user.getId();
sendEmail(userId);
});
}
ThreadLocal を使用してユーザー情報を保存すると、隠れたエラーが表示されます。スレッドのプールが使用され、スレッドが再利用できるため、
ThreadLocal を使用してユーザー情報を取得すると、誤って他人の情報が表示される可能性があります。この問題を解決するには、セッションを使用する必要があります。
HashSet を使用して重複データを削除する
コーディングの際、重複排除が必要になることがよくあります。重複排除について考えるとき、多くの人が最初に考えるのは
HashSet の使用です。ただし、
HashSet を不注意に使用すると、重複排除が失敗する可能性があります。
User user1 = new User();
user1.setUsername("test");
User user2 = new User();
user2.setUsername("test");
List<User> users = Arrays.asList(user1, user2);
HashSet<User> sets = new HashSet<>(users);
System.out.println(sets.size());
注意深い読者の中には、失敗の理由を推測できる人もいるでしょう。
HashSet は、ハッシュ コードを使用してハッシュ テーブルにアクセスし、equals メソッドを使用してオブジェクトが等しいかどうかを判断します。ユーザー定義オブジェクトが hashcode メソッドと
等しいメソッドをオーバーライドしない場合は、親オブジェクトのhashcode メソッドと
等しいメソッドがデフォルトで使用されます。これにより、
HashSet はそれらが 2 つの異なるオブジェクトであると想定し、重複排除が失敗します。
「食べられた」プールスレッドの削除
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(()->{
double result = 10/0;
});
上記のコードは、スレッド プールで例外がスローされるシナリオをシミュレートします。
ビジネスコードはさまざまな状況を想定する必要があるため、何らかの理由でRuntimeExceptionがスローされる可能性が高くなります。ただし、ここで特別な処理が行われない場合、この例外はスレッド プールによって「食べられて」しまいます。そして、例外の原因を確認する方法さえありません。したがって、プロセス プールで例外をキャッチすることが最善です。
Java の文字列 - 内部ビュー
出典:
Medium この記事の著者は、Java での文字列の作成、機能、特徴を詳しく調べることにしました。
創造
Java の文字列は 2 つの異なる方法で作成できます。文字列リテラルとして暗黙的に作成する方法と、 new
キーワードを使用して明示的に作成する方法です。文字列リテラルは、二重引用符で囲まれた文字です。
String literal = "Michael Jordan";
String object = new String("Michael Jordan");
どちらの宣言でも文字列オブジェクトが作成されますが、これらのオブジェクトがヒープ メモリ上にどのように配置されるかには違いがあります。
内部表現
以前は、文字列はchar[]という形式で格納されていました。これは、各文字が文字配列内の個別の要素であることを意味していました。
これらはUTF-16文字エンコード形式で表現されているため、各文字が 2 バイトのメモリを占有することになります。使用統計によると、ほとんどの文字列オブジェクトは
Latin-1文字のみで構成されているため、これはあまり正確ではありません。Latin-1 文字は 1 バイトのメモリを使用して表現できるため、メモリ使用量を大幅に (最大 50%) 削減できます。新しい内部文字列機能は、Compact Strings と呼ばれる、
JEP 254に基づく JDK 9 リリースの一部として実装されました。このリリースでは、
char[] がbyte[]に変更され、使用されるエンコーディング (Latin-1 または UTF-16) を表すエンコーダー フラグ フィールドが追加されました。この後、文字列の内容に基づいてエンコードが行われます。値に Latin-1 文字のみが含まれている場合は、Latin-1 エンコーディング (
StringLatin1クラス) または UTF-16 エンコーディング (
StringUTF16クラス) が使用されます。
メモリ割り当て
前述したように、ヒープ上のこれらのオブジェクトにメモリが割り当てられる方法には違いがあります。JVM がヒープ上に変数用のメモリを作成して割り当てるため、明示的な new キーワードの使用は非常に簡単です。したがって、文字列リテラルの使用はインターニングと呼ばれるプロセスに従います。文字列インターンは、文字列をプールに入れるプロセスです。これは、個々の文字列値のコピーを 1 つだけ保存する方法を使用しており、これは不変である必要があります。個々の値は String Intern プールに保存されます。このプールは、リテラルとそのハッシュを使用して作成された各文字列オブジェクトへの参照を保存する
ハッシュテーブルストアです。文字列値はヒープ上にありますが、その参照は内部プールで見つけることができます。これは、以下の実験を使用して簡単に検証できます。ここには、同じ値を持つ 2 つの変数があります。
String firstName1 = "Michael";
String firstName2 = "Michael";
System.out.println(firstName1 == firstName2);
コードの実行中に、JVM が
firstName1を検出すると、内部文字列プールMichael内の文字列値を検索します。見つからない場合は、内部プール内のオブジェクトに対して新しいエントリが作成されます。
実行がfirstName2に達すると、プロセスが再度繰り返され、今度は、
firstName1変数に基づいてプール内で値を見つけることができます。このようにすると、新しいエントリを複製して作成する代わりに、同じリンクが返されます。したがって、等価条件が満たされます。一方、値
Michaelを持つ変数が new キーワードを使用して作成された場合、インターンは発生せず、等価条件は満たされません。
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);
インターンはfirstName3 intern()メソッド で使用できますが、これは通常は推奨されません。
firstName3 = firstName3.intern();
System.out.println(firstName3 == firstName2);
インターンは、 +演算子 を使用して 2 つの文字列リテラルを連結するときにも発生することがあります。
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");
ここでは、コンパイル時にコンパイラが両方のリテラルを追加し、式から
+演算子を削除して、以下に示すように単一の文字列を形成することがわかります。実行時には、
fullNameと「追加されたリテラル」の両方がインターンされ、等価条件が満たされます。
System.out.println(fullName == "Michael Jordan");
平等
上記の実験から、デフォルトでは文字列リテラルのみがインターンされることがわかります。ただし、Java アプリケーションはさまざまなソースから文字列を受け取る可能性があるため、文字列リテラルのみを持つわけではありません。したがって、等価演算子の使用は推奨されず、望ましくない結果が生じる可能性があります。
等価性テストは、 equalsメソッドによってのみ実行する必要があります。文字列が格納されているメモリ アドレスではなく、文字列の値に基づいて等価性を実行します。
System.out.println(firstName1.equals(firstName2));
System.out.println(firstName3.equals(firstName2));
また、 equalsIgnoreCase と呼ばれる、equals メソッドを少し変更したバージョンもあります。大文字と小文字を区別しない目的に役立つ場合があります。
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));
不変性
文字列は不変です。つまり、文字列の作成後に内部状態を変更することはできません。変数の値は変更できますが、文字列自体の値は変更できません。オブジェクトの操作を処理する
Stringクラスの各メソッド(
concat、
substringなど) は、既存の値を更新するのではなく、値の新しいコピーを返します。
String firstName = "Michael";
String lastName = "Jordan";
firstName.concat(lastName);
System.out.println(firstName);
System.out.println(lastName);
ご覧のとおり、 firstNameも
lastName も、どの変数にも変更は発生しません。
Stringクラスのメソッドは内部状態を変更せず、結果の新しいコピーを作成し、以下に示す結果を返します。
firstName = firstName.concat(lastName);
System.out.println(firstName);
GO TO FULL VERSION