Java Language
一般的なJavaの落とし穴
サーチ…
前書き
このトピックでは、Javaでの初心者の間違いの概要を説明します。
これには、Java言語の一般的な間違いやランタイム環境の理解が含まれます。
特定のAPIに関連する間違いは、それらのAPIに固有のトピックで記述することができます。文字列は特殊なケースです。それらはJava言語仕様でカバーされています。一般的な間違い以外の詳細は、このトピックの「文字列」で説明することができます。
落とし穴:==を使用して整数などのプリミティブラッパーオブジェクトを比較する
(この落とし穴は、すべてのプリミティブラッパータイプに等しく適用されますが、 Integer
およびint
についてはこれを説明します)。
Integer
オブジェクトを扱うときには、 ==
を使用して値を比較することが魅力的です。なぜなら、これはint
値を使用するためです。場合によってはこれがうまくいくように見えます:
Integer int1_1 = Integer.valueOf("1");
Integer int1_2 = Integer.valueOf(1);
System.out.println("int1_1 == int1_2: " + (int1_1 == int1_2)); // true
System.out.println("int1_1 equals int1_2: " + int1_1.equals(int1_2)); // true
ここでは、値1
を持つ2つのInteger
オブジェクトを作成して比較します(この場合、 String
とint
リテラルの1つを作成します)。また、2つの比較方法( ==
とequals
)が両方ともtrue
であることがわかりtrue
。
この動作は、異なる値を選択すると変化します。
Integer int2_1 = Integer.valueOf("1000");
Integer int2_2 = Integer.valueOf(1000);
System.out.println("int2_1 == int2_2: " + (int2_1 == int2_2)); // false
System.out.println("int2_1 equals int2_2: " + int2_1.equals(int2_2)); // true
この場合、 equals
比較のみが正しい結果をもたらす。
この動作の違いは、JVMがInteger
オブジェクトのキャッシュを保持しているためです。(上位の値は、システムプロパティ "java.lang.Integer.IntegerCache.high"でオーバーライドできます。 JVM引数 "-XX:AutoBoxCacheMax = size")。この範囲の値の場合、 Integer.valueOf()
は新しい値を作成するのではなく、キャッシュされた値を返します。
したがって、最初の例では、 Integer.valueOf(1)
およびInteger.valueOf("1")
呼び出しは同じキャッシュされたInteger
インスタンスを返しました。対照的に、2番目の例では、 Integer.valueOf(1000)
とInteger.valueOf("1000")
両方とも新しいInteger
オブジェクトを作成して返しました。
参照型の==
演算子は、参照の等価性(つまり、同じオブジェクト)を検定します。したがって、最初の例では、参照が同じでtrue
ためint1_1 == int1_2
はtrue
です。 2番目の例では、参照が異なるため、 int2_1 == int2_2
はfalseです。
落とし穴:リソースを忘れる
プログラムがファイルやネットワーク接続などのリソースを開くたびに、リソースの使用が完了したらリソースを解放することが重要です。そのようなリソースの操作中に例外がスローされる場合、同様の注意が必要です。 FileInputStream
は、ガベージコレクションイベントでclose()
メソッドを呼び出すファイナライザがあると主張できます。ガベージコレクションのサイクルがいつ開始されるかはわからないので、入力ストリームはコンピュータリソースを無期限に消費する可能性があります。 try-catchブロックのfinally
セクションでリソースを閉じる必要があります。
private static void printFileJava6() throws IOException {
FileInputStream input;
try {
input = new FileInputStream("file.txt");
int data = input.read();
while (data != -1){
System.out.print((char) data);
data = input.read();
}
} finally {
if (input != null) {
input.close();
}
}
}
Java 7以降、Java 7では、特にこの場合、try-with-resourcesと呼ばれる便利で便利なステートメントが導入されています。
private static void printFileJava7() throws IOException {
try (FileInputStream input = new FileInputStream("file.txt")) {
int data = input.read();
while (data != -1){
System.out.print((char) data);
data = input.read();
}
}
}
try-with-resourcesステートメントは、 Closeable
またはAutoCloseable
インターフェイスを実装するすべてのオブジェクトで使用できます。ステートメントの最後まで各リソースが閉じられることを保証します。 2つのインタフェースの違いは、 Closeable
close()
メソッドが何らかの方法で処理されなければならないIOException
をスローすることです。
リソースが既に開かれているが、使用後に安全に閉じなければならない場合は、リソースをtry-with-resources内のローカル変数に割り当てることができます
private static void printFileJava7(InputStream extResource) throws IOException {
try (InputStream input = extResource) {
... //access resource
}
}
try-with-resourcesコンストラクタで作成されたローカルリソース変数は、実質的に最終的なものです。
落とし穴:メモリリーク
Javaはメモリーを自動的に管理します。メモリを手動で解放する必要はありません。ヒープ上のオブジェクトのメモリは、オブジェクトがライブスレッドによって到達できなくなったときにガベージコレクタによって解放される可能性があります。
ただし、不要になったオブジェクトに到達できるようにすることで、メモリの解放を防ぐことができます。これをメモリリークまたはメモリパックラッチングと呼んでも、結果は同じです。割り当てられたメモリが不必要に増加します。
Javaでのメモリリークはさまざまな方法で発生する可能性がありますが、最も一般的な理由は、ガベージコレクタはヒープからオブジェクトを削除できないため、永続的なオブジェクト参照です。
静的フィールド
オブジェクトのコレクションを含むstatic
フィールドを持つクラスを定義し、コレクションが不要になった後にstatic
フィールドをnull
に設定するのを忘れることによって、そのような参照を作成できます。 static
フィールドはGCルーツとみなされ、決して収集されません。別の問題は、 JNIが使用されているときにヒープ以外のメモリにリークがあることです。
クラスローダーのリーク
しかし、はるかに陰気なタイプのメモリリークは、 クラスローダのリークです。クラスローダーは、ロードしたすべてのクラスへの参照を保持し、すべてのクラスはそのクラスローダーへの参照を保持します。すべてのオブジェクトはそのクラスへの参照も保持しています。したがって、クラスローダによってロードされたクラスの単一のオブジェクトでもガベージではない場合、そのクラスローダがロードした単一のクラスを収集することはできません。各クラスは静的フィールドも参照するため、いずれも収集できません。
累積リーク累積リークの例は、次のようになります。
final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);
scheduledExecutorService.scheduleAtFixedRate(() -> {
BigDecimal number = numbers.peekLast();
if (number != null && number.remainder(divisor).byteValue() == 0) {
System.out.println("Number: " + number);
System.out.println("Deque size: " + numbers.size());
}
}, 10, 10, TimeUnit.MILLISECONDS);
scheduledExecutorService.scheduleAtFixedRate(() -> {
numbers.add(new BigDecimal(System.currentTimeMillis()));
}, 10, 10, TimeUnit.MILLISECONDS);
try {
scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
この例は、2つのスケジュールされたタスクを作成します最初のタスクは、呼び出された両端キューから最後の番号を取るnumbers
、そして、数は51で割り切れるならば、それは数や両端キューのサイズを出力します。 2番目のタスクは、両端キューに数値を代入します。両方のタスクは固定レートでスケジュールされ、10ミリ秒ごとに実行されます。
コードが実行されると、両端キューのサイズが永久に増加することがわかります。これにより、最終的に両端キューが使用可能なすべてのヒープ・メモリーを消費するオブジェクトでいっぱいになります。
このプログラムのセマンティクスを維持しながらこれを防止するために、deque: pollLast
から数値を取得する別の方法を使用できます。 peekLast
メソッドとはpeekLast
、 pollLast
は要素を返し、dequeから削除しますが、 peekLast
は最後の要素のみを返します。
落とし穴:==を使って文字列を比較する
Java初心者にとってよくある間違いは、 ==
演算子を使用して2つの文字列が等しいかどうかをテストすることです。例えば:
public class Hello {
public static void main(String[] args) {
if (args.length > 0) {
if (args[0] == "hello") {
System.out.println("Hello back to you");
} else {
System.out.println("Are you feeling grumpy today?");
}
}
}
}
上記のプログラムは、最初のコマンドライン引数をテストし、それが "hello"という単語ではないときに異なるメッセージを出力することになっています。しかし問題は、それがうまくいかないことです。そのプログラムは "あなたは今日は気難しい気がしますか?"最初のコマンドライン引数が何であっても。
この特定のケースでは、 String
String
args [0]がヒープ上にある間にString
String
"hello"が文字列プールに入れられます。これは、同じリテラルを表す2つのオブジェクトがあり、それぞれが参照を持つことを意味します。 ==
、実際の等価ではなく、参照をテストするので、比較はほとんどの場合、偽を生成します。これは常にそうすることを意味するものではありません。
==
を使用して文字列をテストする場合、実際にテストするのは、2つのString
オブジェクトが同じJavaオブジェクトである場合です。残念ながら、これはJavaでは文字列の平等が意味するものではありません。実際、文字列をテストする正しい方法はequals(Object)
メソッドを使うことequals(Object)
。文字列のペアについては、通常、同じ文字で同じ順序で構成されているかどうかをテストします。
public class Hello2 {
public static void main(String[] args) {
if (args.length > 0) {
if (args[0].equals("hello")) {
System.out.println("Hello back to you");
} else {
System.out.println("Are you feeling grumpy today?");
}
}
}
}
しかし、実際には悪化しています。問題は、いくつかの状況で==
が期待される答えを出すということです。例えば
public class Test1 {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
if (s1 == s2) {
System.out.println("same");
} else {
System.out.println("different");
}
}
}
面白いことに、文字列を間違った方法でテストしていても、これは "同じ"と表示されます。何故ですか? Java言語仕様(セクション3.10.5:文字列リテラル)では 、同じ文字からなる2つの文字列>>リテラル<<は実際には同じJavaオブジェクトによって表されると規定されているためです。したがって、 ==
testは等しいリテラルに対して真を与えます。 (文字列リテラルは "interned"され、コードがロードされるときに共有 "文字列プール"に追加されますが、実際は実装の詳細です)。
この混乱に加えて、Java言語仕様では、2つの文字列リテラルを連結するコンパイル時の定数式がある場合、それは1つのリテラルに相当すると規定されています。従って:
public class Test1 {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hel" + "lo";
String s3 = " mum";
if (s1 == s2) {
System.out.println("1. same");
} else {
System.out.println("1. different");
}
if (s1 + s3 == "hello mum") {
System.out.println("2. same");
} else {
System.out.println("2. different");
}
}
}
これは、 "1. same"と "2. different"を出力します。最初のケースでは、コンパイル時に+
式が評価され、1つのString
オブジェクトとそれ自身を比較します。 2番目のケースでは、実行時に評価され、2つの異なるString
オブジェクト
要約すると、Javaで文字列をテストするために==
を使用することはほとんど常に間違っていますが、間違った答えを与えることは保証されません。
落とし穴:ファイルを開こうとする前にテストします。
ファイルを開く前に、さまざまなテストをファイルに適用して、より良い診断を提供したり、例外を回避したりすることをお勧めします。たとえば、このメソッドは、 path
が読み取り可能なファイルに対応するかどうかをチェックしようとします。
public static File getValidatedFile(String path) throws IOException {
File f = new File(path);
if (!f.exists()) throw new IOException("Error: not found: " + path);
if (!f.isFile()) throw new IOException("Error: Is a directory: " + path);
if (!f.canRead()) throw new IOException("Error: cannot read file: " + path);
return f;
}
上記の方法を以下のように使うことができます:
File f = null;
try {
f = getValidatedFile("somefile");
} catch (IOException ex) {
System.err.println(ex.getMessage());
return;
}
try (InputStream is = new FileInputStream(file)) {
// Read data etc.
}
最初の問題は、 FileInputStream(File)
シグネチャにありFileInputStream(File)
なぜなら、コンパイラはここでIOException
を捕まえようとしているか、スタックをさらにIOException
うとしているからです。
2番目の問題は、 getValidatedFile
によって実行されるチェックが、 FileInputStream
が成功することを保証しないことです。
競合条件:別のスレッドまたは別のプロセスが、
getValidatedFile
が復帰した後でファイルの名前を変更したり、ファイルを削除したり、読み取りアクセス権を削除することができます。これは、カスタムメッセージのない「普通の」IOException
つながります。これらのテストでカバーされていないエッジケースがあります。たとえば、SELinuxが "強制"モードになっているシステムでは、
canRead()
true
返しても、ファイルを読み込もうとしても失敗する可能性がありtrue
。
第3の問題は、テストが非効率的であることです。たとえば、 exists
、 isFile
、およびcanRead
各呼び出しは、それぞれが必要なチェックを実行するためにシステムコールを作成します 。別のシステムコールがファイルを開くように作られ、同じチェックがバックグラウンドで繰り返されます。
要するに、 getValidatedFile
ようなgetValidatedFile
が間違っています。単にファイルを開いて例外を処理しようとすると良いでしょう:
try (InputStream is = new FileInputStream("somefile")) {
// Read data etc.
} catch (IOException ex) {
System.err.println("IO Error processing 'somefile': " + ex.getMessage());
return;
}
開いて読み込んだときにスローされたIOエラーを区別したい場合は、ネストされたtry / catchを使用できます。開いている障害に対してより良い診断をしたい場合は、ハンドラ内のexists
、 isFile
、およびcanRead
チェックを実行できます。
落とし穴:オブジェクトとしての変数の考え方
Java変数はオブジェクトを表しません。
String foo; // NOT AN OBJECT
いずれのJava配列にもオブジェクトは含まれません。
String bar[] = new String[100]; // No member is an object.
誤って変数をオブジェクトと考えると、Java言語の実際の動作はあなたを驚かせるでしょう。
プリミティブ型(
int
やfloat
)を持つJava変数の場合、変数には値のコピーが保持されます。プリミティブ値のすべてのコピーは区別できません。つまり、数値1のint
値は1つだけです。プリミティブ値はオブジェクトではなく、オブジェクトのように動作しません。参照型(クラスまたは配列型のいずれか)を持つJava変数の場合、変数には参照が保持されます。参照のすべてのコピーは区別できません。参照はオブジェクトを指すかもしれないし、オブジェクトが
null
れていないことを意味するnull
かもしれない。しかし、それらはオブジェクトではなく、オブジェクトのように動作しません。
いずれの場合でも変数はオブジェクトではなく、いずれの場合もオブジェクトは含まれません。それらはオブジェクトへの参照を含むかもしれませんが、それは何か違うものを言っています。
サンプルクラス
次の例では、このクラスを使用しています。このクラスは、2D空間内のポイントを表します。
public final class MutableLocation {
public int x;
public int y;
public MutableLocation(int x, int y) {
this.x = x;
this.y = y;
}
public boolean equals(Object other) {
if (!(other instanceof MutableLocation) {
return false;
}
MutableLocation that = (MutableLocation) other;
return this.x == that.x && this.y == that.y;
}
}
このクラスのインスタンスは、 int
型の2つのフィールドx
とy
を持つオブジェクトです。
私たちは、 MutableLocation
クラスの多くのインスタンスを持つことができます。いくつかは2D空間で同じ場所を表します。すなわち、 x
とy
それぞれの値が一致する。他は異なる場所を表します。
複数の変数が同じオブジェクトを指すことができる
MutableLocation here = new MutableLocation(1, 2);
MutableLocation there = here;
MutableLocation elsewhere = new MutableLocation(1, 2);
上記では、三つの変数宣言しているhere
、 there
とelsewhere
への参照を保持することができますMutableLocation
オブジェクトを。
あなたが(間違って)これらの変数をオブジェクトであると考えるならば、あなたはそのステートメントを次のように間違って読む可能性があります:
- 場所「[1、2]」を
here
コピーしてhere
- 場所「[1、2]」を
there
コピーしthere
- 場所「[1、2]」を
elsewhere
の場所にコピーする
これから、3つの変数に3つの独立したオブジェクトがあると推測できます。実際には、上記で作成されたオブジェクトは2つだけです。変数here
とthere
、実際に同じオブジェクトを参照してください。
これを実証することができます。変数の宣言を上記のように仮定します:
System.out.println("BEFORE: here.x is " + here.x + ", there.x is " + there.x +
"elsewhere.x is " + elsewhere.x);
here.x = 42;
System.out.println("AFTER: here.x is " + here.x + ", there.x is " + there.x +
"elsewhere.x is " + elsewhere.x);
これは次のように出力されます:
BEFORE: here.x is 1, there.x is 1, elsewhere.x is 1
AFTER: here.x is 42, there.x is 42, elsewhere.x is 1
私たちはhere.x
新しい値を割り当て、そこから見る値を変更しthere.x
。彼らは同じ目的を指しています。しかし、 elsewhere.x
参照される値elsewhere.x
は変更さelsewhere.x
ていないため、 elsewhere
別のオブジェクトを参照する必要があります。
変数がオブジェクトの場合、ここの割り当てhere.x = 42
はthere.x
変更されません。
等価演算子は、2つのオブジェクトが等しいことをテストしません
等価( ==
)演算子を参照値に適用すると、値が同じオブジェクトを参照しているかどうかがテストされます。 2つの(異なる)オブジェクトが直感的な意味で「等しい」かどうかはテストされません 。
MutableLocation here = new MutableLocation(1, 2);
MutableLocation there = here;
MutableLocation elsewhere = new MutableLocation(1, 2);
if (here == there) {
System.out.println("here is there");
}
if (here == elsewhere) {
System.out.println("here is elsewhere");
}
これは "here is there"と印刷されますが、 "here is other is"は印刷されません。 ( here
とelsewhere
の参照は、2つの異なるオブジェクト用です。)
対照的に、上記で実装したequals(Object)
メソッドを呼び出すと、2つのMutableLocation
インスタンスの位置が同じかどうかをテストします。
if (here.equals(there)) {
System.out.println("here equals there");
}
if (here.equals(elsewhere)) {
System.out.println("here equals elsewhere");
}
両方のメッセージが表示されます。特に、 here.equals(elsewhere)
は、2つのMutableLocation
オブジェクトの等価性のために選択した意味的基準が満たされているため、 true
返しtrue
。
メソッド呼び出しはオブジェクトをまったく渡さない
Javaメソッドの呼び出しは、引数を渡すと、結果を返すために、 値 1 でパスを使用します。
メソッドに参照値を渡すと、実際にはオブジェクトへの参照を値で渡しています。つまり、オブジェクト参照のコピーを作成しています。
両方のオブジェクト参照が同じオブジェクトを指している限り、そのオブジェクトをいずれかの参照から変更することができます。これが一部のオブジェクトの混乱の原因となります。
しかし、参照によってオブジェクトを渡すことはありません 2 。区別は、オブジェクト参照コピーが別のオブジェクトを指すように変更された場合、元のオブジェクト参照が依然として元のオブジェクトを指し示すことである。
void f(MutableLocation foo) {
foo = new MutableLocation(3, 4); // Point local foo at a different object.
}
void g() {
MutableLocation foo = MutableLocation(1, 2);
f(foo);
System.out.println("foo.x is " + foo.x); // Prints "foo.x is 1".
}
オブジェクトのコピーを渡すこともありません。
void f(MutableLocation foo) {
foo.x = 42;
}
void g() {
MutableLocation foo = new MutableLocation(0, 0);
f(foo);
System.out.println("foo.x is " + foo.x); // Prints "foo.x is 42"
}
1 - PythonやRubyのような言語では、オブジェクト/参照の "値渡し"には "共有で渡す"という言葉が優先されます。
2 - 「参照渡し」または「参照による呼び出し」という用語は、プログラミング言語の用語では非常に具体的な意味を持ちます。つまり、呼び出されたメソッドが仮引数に新しい値を代入するときに、元の変数の値を変更するように、変数または配列要素のアドレスを渡すことを意味します。 Javaはこれをサポートしていません。パラメータを渡すためのさまざまなメカニズムの詳細については、 https://en.wikipedia.org/wiki/Evaluation_strategyを参照してください 。
落とし穴:割り当てと副作用を組み合わせる
時にはStackOverflowのJavaの質問(およびCまたはC ++の質問)で、次のような質問をすることがあります。
i += a[i++] + b[i--];
i
、 a
、 b
既知の初期状態についていくつか評価します。
一般的に言えば:
- Javaの場合、答えは常に1と指定されていますが、明白ではなく、しばしば把握が難しい
- CとC ++の場合、答えはしばしば不定です。
このような例は、学生や面接者がJavaプログラミング言語で表現評価が実際にどのように機能するかを理解するための試みとして、試験や面接でよく使用されます。これはおそらく「知識のテスト」として正当なものですが、実際のプログラムでこれを行うべきではありません。
説明するために、以下の一見単純な例がStackOverflowの質問( このような質問)に数回出現しました。場合によっては、誰かのコードに本物の間違いとして表示されます。
int a = 1;
a = a++;
System.out.println(a); // What does this print.
これらのステートメントをすばやく読んでいるプログラマー(Java専門家を含む)のほとんどは、 2
出力すると言います。実際には、 1
出力します。理由の詳細については、 この回答をお読みください。
しかし、これと同様の例から、実際のお持ち帰りは、 任意の Java文は両方ともに割り当てると副作用が同じ変数が最高の状態でハードを理解すること、そして最悪の場合実に誤解を招くことになるだろうということです。このようなコードを書くのは避けるべきです。
1 - 変数またはオブジェクトが他のスレッドから見える場合は、 Javaメモリモデルの潜在的な問題をモジュロにします。
落とし穴:Stringが不変クラスであると理解していない
新しいJavaプログラマは、Java String
クラスが不変であることをしばしば忘れるか、完全に理解しない。これは、次の例のような問題につながります。
public class Shout {
public static void main(String[] args) {
for (String s : args) {
s.toUpperCase();
System.out.print(s);
System.out.print(" ");
}
System.out.println();
}
}
上のコードはコマンドラインの引数を大文字で表示することになっています。残念ながら、それは動作しません、引数の場合は変更されません。問題は次のとおりです。
s.toUpperCase();
toUpperCase()
を呼び出すと、 s
が大文字の文字列に変更されると考えるかもしれません。それはしません。それはできません! String
オブジェクトは不変です。彼らは変更することはできません。
実際には、 toUpperCase()
メソッドは String
オブジェクトを返します toUpperCase()
オブジェクトは、それを呼び出すString
大文字バージョンです。これはおそらく新しいString
オブジェクトになりますが、 s
がすでにすべて大文字であった場合、結果は既存の文字列になります。
したがって、このメソッドを効果的に使用するには、メソッド呼び出しによって返されたオブジェクトを使用する必要があります。例えば:
s = s.toUpperCase();
実際、「文字列は変更されません」ルールはすべてのString
メソッドに適用されます。あなたがそれを覚えているならば、初心者の間違い全体を避けることができます。