Java Language
Javaの落とし穴 - 言語の構文
サーチ…
前書き
いくつかのJavaプログラミング言語の誤用は、正しくコンパイルされているにもかかわらず不正な結果を生成するプログラムを実行する可能性があります。このトピックの主な目的は、一般的な落とし穴をその原因とともに挙げ、そのような問題を避ける正しい方法を提案することです。
備考
このトピックでは、エラーが発生しやすいJava言語構文、または特定の方法で使用しないJava言語構文の特定の側面について説明します。
陥没 - メソッドの可視性を無視する
経験豊富なJava開発者でも、Javaには3つの保護修飾子しかないと考える傾向があります。言語は実際に4つです! パッケージプライベート (別名デフォルト)の可視性レベルはしばしば忘れられます。
どんな方法で公開するか注意を払う必要があります。アプリケーションのパブリックメソッドは、アプリケーションの可視APIです。これは、特に再利用可能なライブラリを作成している場合は、できるだけ小さく、コンパクトにする必要があります( SOLID原理も参照してください)。同様に、すべてのメソッドの可視性を考慮し、必要に応じて保護されたプライベートアクセスのみを使用することが重要です。
privateとして公開するメソッドを宣言すると、クラスの内部実装の詳細が公開されます。
これには、クラスのパブリックメソッドを単体テストでテストすることしかできません。実際は、パブリックメソッドのみをテストできます。これらのメソッドに対して単体テストを実行できるようにするために、プライベートメソッドの可視性を高めることは悪い習慣です。より限定的な可視性を持つメソッドを呼び出すパブリックメソッドをテストすることは、API全体をテストするのに十分なはずです。単体テストを可能にするためだけに、公開メソッドを使用してAPIを拡張するべきではありません 。
落とし穴 - 「スイッチ」ケースで「ブレーク」がない
これらのJavaの問題は非常に厄介なことがあり、プロダクションで動作するまで未解決のままになることがあります。 switchステートメントでの不安定な動作はしばしば役に立ちます。しかし、そのような動作が望ましくないときに "休憩"キーワードがないと、悲惨な結果につながる可能性があります。下のコード例で "case 0"に "break"を入れるのを忘れた場合、プログラムは "Zero"と "One"を書きます。ここでの制御フローは "switch"ステートメント全体を通過するでしょう。それは "休憩"に達する。例えば:
public static void switchCasePrimer() {
int caseIndex = 0;
switch (caseIndex) {
case 0:
System.out.println("Zero");
case 1:
System.out.println("One");
break;
case 2:
System.out.println("Two");
break;
default:
System.out.println("Default");
}
}
ほとんどの場合、より洗練されたソリューションは、インターフェイスを使用し、特定の動作を持つコードを個別の実装( 継承を超えた構成 )に移動することです。
switch文がやむを得ない場合は、「予想される」フォールススルーが発生した場合に文書化することをお勧めします。そうすれば、仲間の開発者にあなたが欠けていることを認識していることを示すことができます。これは予想される動作です。
switch(caseIndex) {
[...]
case 2:
System.out.println("Two");
// fallthrough
default:
System.out.println("Default");
落とし穴 - 間違ったセミコロンと欠け括弧
これは、少なくともJava初心者にとっては本当に混乱を招く間違いです。これを書く代わりに:
if (feeling == HAPPY)
System.out.println("Smile");
else
System.out.println("Frown");
彼らは誤ってこれを書いています:
if (feeling == HAPPY);
System.out.println("Smile");
else
System.out.println("Frown");
Javaコンパイラがelse
が間違っていると伝えると困惑します。 Javaコンパイラは、上記を次のように解釈します。
if (feeling == HAPPY)
/*empty statement*/ ;
System.out.println("Smile"); // This is unconditional
else // This is misplaced. A statement cannot
// start with 'else'
System.out.println("Frown");
他のケースでは、コンパイルエラーは発生しませんが、コードはプログラマが意図するものを実行しません。例えば:
for (int i = 0; i < 5; i++);
System.out.println("Hello");
一度だけ "こんにちは"を印刷します。もう一度、偽のセミコロンは、 for
ループの本体が空文であることを意味します。これは、後続のprintln
呼び出しが無条件であることを意味します。
別のバリエーション:
for (int i = 0; i < 5; i++);
System.out.println("The number is " + i);
これにより、 i
に対して「シンボルを見つけることができません」というエラーが表示されます。疑似セミコロンの存在は、 println
呼び出しがそのスコープの外でi
を使用しようとしていることを意味します。
これらの例では、簡単な解決策があります。偽のセミコロンを削除するだけです。しかし、これらの例から引き出されるいくつかの深い教訓があります:
Javaのセミコロンは "統語的なノイズ"ではありません。セミコロンの有無は、プログラムの意味を変えることができます。すべての行末にそれらを追加するだけではありません。
コードのインデントを信頼しないでください。 Java言語では、行頭の余分な空白はコンパイラーによって無視されます。
自動圧子を使用します。すべてのIDEと多くの単純なテキストエディタは、Javaコードを正しくインデントする方法を理解しています。
これが最も重要な教訓です。最新のJavaスタイルのガイドラインに従い、 "then"ステートメントと "else"ステートメントとループのbodyステートメントの前後に中カッコを入れてください。中括弧(
{
)を改行してはいけません。
プログラマーがスタイル・ルールに従ったif
、誤ったセミコロンを含むif
例は次のようになります。
if (feeling == HAPPY); {
System.out.println("Smile");
} else {
System.out.println("Frown");
}
経験豊かな目には奇妙に見えます。そのコードを自動インデントした場合は、次のようになります。
if (feeling == HAPPY); {
System.out.println("Smile");
} else {
System.out.println("Frown");
}
初心者にも間違っているはずです。
落とし穴 - 中括弧を残して:「ぶら下がっている」と「他人が抱いている」問題
Oracle Javaスタイルガイドの最新バージョンでは、 if
ステートメントの "then"ステートメントと "else"ステートメントを常に "中かっこ"または "中括弧"で囲む必要があります。さまざまなループ文の本体にも同様の規則が適用されます。
if (a) { // <- open brace
doSomething();
doSomeMore();
} // <- close brace
これはJava言語の構文では実際には必要ありません。実際、if文の "then"部分が単一の文であるif
、中括弧を省略することは合法です
if (a)
doSomething();
あるいは
if (a) doSomething();
しかし、Javaスタイルのルールを無視して中カッコを省いてしまう危険性があります。具体的には、誤ったインデントのコードが間違っているというリスクを大幅に増加させます。
「ぶら下がっている」問題:
上の例のコードを、中括弧なしで書き直してみましょう。
if (a)
doSomething();
doSomeMore();
このコードは、への呼び出しと言っているようだ doSomething
とdoSomeMore
両方の場合にのみ発生しますですa
true
。実際、コードは間違ってインデントされています。 doSomeMore()
呼び出しがif
ステートメントに続く別のステートメントであるJava言語仕様。正しい字下げは次のとおりです。
if (a)
doSomething();
doSomeMore();
"ぶら下がっている"問題
ミックスにelse
を追加すると、2番目の問題が発生します。次の中括弧がない例を考えてみましょう。
if (a)
if (b)
doX();
else if (c)
doY();
else
doZ();
上記のコードは、 a
がfalse
ときにdoZ
が呼び出されると言い false
。実際には、インデントが間違っています。コードのインデントは次のとおりです。
if (a)
if (b)
doX();
else if (c)
doY();
else
doZ();
コードがJavaスタイルのルールに従って記述されている場合、実際は次のようになります。
if (a) {
if (b) {
doX();
} else if (c) {
doY();
} else {
doZ();
}
}
なぜそれが良いのかを説明するために、誤ってコードを間違ってインデントしたとします。あなたはこのようなもので終わるかもしれません:
if (a) { if (a) {
if (b) { if (b) {
doX(); doX();
} else if (c) { } else if (c) {
doY(); doY();
} else { } else {
doZ(); doZ();
} }
} }
しかし、いずれの場合も、間違ってインデントされたコードは、経験豊富なJavaプログラマーの目には間違っています。
落とし穴 - オーバーライドではなくオーバーロード
次の例を考えてみましょう。
public final class Person {
private final String firstName;
private final String lastName;
public Person(String firstName, String lastName) {
this.firstName = (firstName == null) ? "" : firstName;
this.lastName = (lastName == null) ? "" : lastName;
}
public boolean equals(String other) {
if (!(other instanceof Person)) {
return false;
}
Person p = (Person) other;
return firstName.equals(p.firstName) &&
lastName.equals(p.lastName);
}
public int hashcode() {
return firstName.hashCode() + 31 * lastName.hashCode();
}
}
このコードは期待どおりに動作しません。問題は、 Person
equals
メソッドとhashcode
メソッドがObject
定義された標準メソッドをオーバーライドしないことです。
-
equals
メソッドのシグネチャが間違っています。equals(Object)
notequals(String)
として宣言する必要があります。 -
hashcode
メソッドの名前が間違っています。これはhashCode()
なければなりませんhashCode()
大文字のCに注意してください)。
これらの間違いは、偶発的なオーバーロードを宣言したことを意味し、 Person
が多相コンテキストで使用されている場合は使用されません。
しかし、これに対処する簡単な方法があります(Java 5以降)。使用@Override
あなたがオーバーライドするあなたの方法を意図したときに注釈を:
public final class Person {
...
@Override
public boolean equals(String other) {
....
}
@Override
public hashcode() {
....
}
}
@Override
アノテーションをメソッド宣言に追加すると、コンパイラはメソッドがスーパークラスまたはインタフェースで宣言されたメソッドをオーバーライド(または実装)するかどうかをチェックします。上の例では、コンパイラは2つのコンパイルエラーを返します。コンパイルエラーは私たちに間違いを警告するのに十分なはずです。
落とし穴 - 8進リテラル
次のコードスニペットを考えてみましょう。
// Print the sum of the numbers 1 to 10
int count = 0;
for (int i = 1; i < 010; i++) { // Mistake here ....
count = count + i;
}
System.out.println("The sum of 1 to 10 is " + count);
Javaの初心者は、上記のプログラムが間違った答えを出力していることに驚くかもしれません。実際には1から8の数字の合計を出力します。
その理由は、数字ゼロ( '0')で始まる整数リテラルは、Javaコンパイラによって期待されるように10進リテラルではなく8進リテラルとして解釈されるためです。従って、 010
は8進数10であり、これは10進数で8である。
落とし穴 - 標準クラスと同じ名前のクラスを宣言する
時には、Javaを初めて使用するプログラマーは、広く使われているクラスと同じ名前のクラスを定義するのを間違えます。例えば:
package com.example;
/**
* My string utilities
*/
public class String {
....
}
そして、彼らは予期せぬエラーが発生する理由を知ります。例えば:
package com.example;
public class Test {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
上記のクラスをコンパイルして実行しようとすると、エラーが発生します:
$ javac com/example/*.java
$ java com.example.Test
Error: Main method not found in class test.Test, please define the main method as:
public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application
誰かがTest
クラスのコードを見て、 main
の宣言を見て、その署名を見て、 java
コマンドが何を不平にしているのか疑問に思います。しかし、実際には、 java
コマンドは真実を伝えています。
Test
と同じパッケージにString
バージョンを宣言すると、このバージョンがjava.lang.String
自動インポートよりも優先されjava.lang.String
。したがって、 Test.main
メソッドのシグネチャは実際には
void main(com.example.String[] args)
の代わりに
void main(java.lang.String[] args)
java
コマンドはentrypointメソッドとしてそれを認識しません。
レッスン: java.lang
既存のクラスと同じ名前を持つクラス、またはJava SEライブラリの他のよく使用されるクラスを定義しないでください。あなたがそうするならば、あなたはあらゆる種類のあからさまなエラーのために自分自身を開いています。
落とし穴 - '=='を使ってブール値をテストする
新しいJavaプログラマは、次のようなコードを書くことがあります。
public void check(boolean ok) {
if (ok == true) { // Note 'ok == true'
System.out.println("It is OK");
}
}
経験豊富なプログラマーは、それが不器用で、それを次のように書き直したいと思うでしょう:
public void check(boolean ok) {
if (ok) {
System.out.println("It is OK");
}
}
しかし、単純な不器用さよりもok == true
方が間違っています。このバリエーションを考えてみましょう。
public void check(boolean ok) {
if (ok = true) { // Oooops!
System.out.println("It is OK");
}
}
ここでプログラマーは==
as =
...を間違って入力しましたが、コードには微妙なバグがあります。式x = true
無条件にx
true
を代入してからtrue
評価しtrue
。言い換えれば、 check
メソッドは、パラメータが何であっても "それはOK"を表示するようになりました。
ここでの教訓は、 == false
と== true
を使用する習慣から抜け出すこと== true
。冗長であることに加えて、コーディングはエラーを起こしやすくなります。
注意:落とし穴を避けるためのok == true
代わりに、 Yoda条件を使用することもできます 。すなわち、 true == ok
ように、リテラルを関係演算子の左側に置く。これはうまくいくが、ほとんどのプログラマーはヨーダの条件が奇妙に見えることにおそらく同意するだろう。確かにok
(または!ok
)はより簡潔で自然です。
落とし穴 - ワイルドカードのインポートによってコードが壊れやすくなる
次の部分的な例を考えてみましょう。
import com.example.somelib.*;
import com.acme.otherlib.*;
public class Test {
private Context x = new Context(); // from com.example.somelib
...
}
最初にsomelib
バージョン1.0とsomelib
バージョン1.0に対してコードを開発したときをotherlib
。その後、ある時点で依存関係を新しいバージョンにアップグレードし、 otherlib
バージョン2.0を使用することにします。また、1.0と2.0の間でotherlib
加えた変更の1つがContext
クラスを追加することだったとします。
Test
を再コンパイルすると、 Context
があいまいなインポートであることを示すコンパイルエラーが発生します。
あなたがコードベースに精通しているなら、これはおそらくほんの少しの不便です。そうでなければ、あなたはこの問題に取り組むためにいくつかの作業を行います。
ここでの問題は、ワイルドカードのインポートです。一方では、ワイルドカードを使用すると、クラスを数行短くすることができます。一方:
コードベースの他の部分、Java標準ライブラリ、またはサードパーティのライブラリへの上位互換の変更は、コンパイルエラーにつながる可能性があります。
読みやすさに問題があります。 IDEを使用している場合を除き、ワイルドカードのインポートのどれが名前付きクラスを取得しているかを把握するのは難しいことがあります。
教訓は、長期間使用する必要があるコードでワイルドカードのインポートを使用することは悪い考えです。特定の(ワイルドカードではない)インポートは、IDEを使用している場合には保守にあまり努力するものではなく、その努力は価値があります。
落とし穴:引数やユーザー入力の検証に 'assert'を使う
StackOverflowでは、メソッドに渡された引数を検証するためにassert
を使用assert
が適切であるか、ユーザーによって提供される入力さえ適切かどうかという疑問があります。
簡単な答えは適切でないということです。
より良い選択肢は次のとおりです。
- カスタムコードを使用してIllegalArgumentExceptionをスローします。
- Google Guavaライブラリで使用可能な
Preconditions
メソッドの使用。 - Apache Commons Lang3ライブラリで使用可能な
Validate
メソッドの使用。
これは、 Java言語仕様(JLS 14.10、for Java 8)がこの点についてアドバイスしているものです。
通常、アサーションのチェックは、プログラムの開発およびテスト中に有効になり、展開を無効にしてパフォーマンスを向上させます。
アサーションが無効になる可能性があるため、プログラムはアサーションに含まれる式が評価されることを想定してはいけません。したがって、これらのブール式は一般に副作用がないはずです。このようなブール式を評価しても、評価が完了した後に表示される状態には影響しません。アサーションに含まれるブール式が副作用を持つことは不正ではありませんが、アサーションが有効か無効かによってプログラムの動作が異なる可能性があるため、通常は不適切です。
これに照らして、パブリックメソッドの引数チェックにアサーションを使用すべきではありません。引数チェックは、通常、メソッドのコントラクトの一部であり、アサーションが有効であるか無効であるかに関わらず、この規約を維持する必要があります。
引数チェックにアサーションを使用する場合の2番目の問題は、誤った引数が適切な実行時例外(
IllegalArgumentException
、ArrayIndexOutOfBoundsException
、またはNullPointerException
)になることです。アサーションの失敗は適切な例外をスローしません。繰り返しますが、パブリックメソッドの引数チェックにアサーションを使用することは違法ではありませんが、通常は不適切です。AssertionError
は決して捕まえられないことを意図していますが、そうすることができます。したがって、tryステートメントのルールは、現在のthrowステートメントの扱いと同様に、tryブロックに現れるアサーションを処理する必要があります。
プリミティブへのヌルオブジェクトの自動アンボクシングの落とし穴
public class Foobar {
public static void main(String[] args) {
// example:
Boolean ignore = null;
if (ignore == false) {
System.out.println("Do not ignore!");
}
}
}
ここでの落とし穴は、 null
がfalse
と比較されるfalse
です。プリミティブboolean
とBoolean
boolean
比較するので、JavaはBoolean
Object
をプリミティブ相当Boolean
にアンボックスしようとします。これは比較の準備ができています。ただし、その値はnull
であるため、 NullPointerException
がスローされます。
Javaでは、プリミティブ型とnull
値を比較できないため、実行時にNullPointerException
します。条件false == null
プリミティブの場合を考えてみましょう。これは、 コンパイル時に 、 incomparable types: int and <null>
。