【Java】 変数初期化のまとめ
こんにちは。のわです。
今回は、Javaの変数初期化についてまとめます。
◇変数初期化の前提
変数は大きく分けて3つです。
・クラス変数
・インスタンス変数
・ローカル変数
この3つのうち、クラス変数・インスタンス変数は
『変数の初期化を自動的に行う』ようになっています。
残りのローカル変数は
『変数の初期化は明示的に行う』必要があります。
public class Main { public static int a; //クラス変数 public int b; //インスタンス変数 public static void main(String[] args) { int c = 0; //初期化を明示したローカル変数 int d; //初期化していないローカル変数 System.out.println(a); System.out.println(new Main().b); System.out.println(c); System.out.println(d); } }
上記コードを実行すると以下のような結果になります。
・a(クラス変数) → 自動初期化される。int型のため、「0」が出力される
・b(インスタンス変数) → aと同じ。
・c(ローカル変数) → 初期化を「0」と明示している。「0」が出力される
・d(ローカル変数) → 初期化せず出力している。コンパイルエラー。
このようにローカル変数については
初期化を行っていない変数を使用するとコンパイルエラーとなります。
◇自動初期化の値
前項でクラス変数とインスタンス変数は『初期化が自動的に行われる』ことを記述しました。
この初期化ですが、『どの型を使うか』で値が決まります。
代表的なものを並べます。
public class Main { public static int a; public static String b; public static boolean c; public static double d; public static void main(String[] args) { System.out.println(a); //0 System.out.println(b); //null System.out.println(c); //false System.out.println(d); //0.0 } }
上記のように、どの型を使っているかで初期値が決まります。
・int, short, long → 0
・String → null
・boolean → false
・double, float → 0
◇ローカル変数の初期化
ローカル変数の初期化は2パターンあります。
・宣言時
・宣言後
public class Main { public static void main(String[] args) { int a = 0; //宣言時に初期化する int b; b = 1; //宣言後に初期化する } }
ここで注意したいポイントがあります。
それは、『ローカル変数をどのタイミングで初期化するか』です。
以下のコードを見てください。
public class Main { public static void main(String[] args) { int a = 0; int b; if(a == 0) { b = 1; } System.out.println(b); //結果は...? } }
このコードでは、変数bの初期化をif文内で行っています。
一見すると出力結果は「1」になりそうです。
しかし、このコードの結果は「コンパイルエラー」になります。
if文内で初期化を行った場合、変数bの初期化は『if文内』のみで有効となります。
このブロック内でのみ、有効な処理の考えを『スコープ』と呼びます。
コードの出力をif文内におさめると「1」と出力されます。
public class Main { public static void main(String[] args) { int a = 0; int b; if(a == 0) { b = 1; System.out.println(b); // 1 } } }
今回は、変数初期化についてまとめました。
まとめです
・インスタンス変数とクラス変数は初期化は自動的にデフォルト値で設定される
・ローカル変数の初期化は明示的に設定する必要がある。
・ローカル変数の初期化時はスコープ内か注意する。
最後まで読んでいただきありがとうございました!
【Java】 オーバーロード・オーバーライドのまとめ
こんにちは。のわです。
今回は、Javaのオーバーライド・オーバーロードについてまとめます。
◇オーバーロード
まずは、オーバーロードからです。
オーバーロードは『類似する複数のメソッドを定義』することです。
オーバーロードの条件は以下になりす。
・シグニチャが異なる
・メソッド名が同じ
※シグニチャとは
・メソッド名
・引数の型
・引数の順番
・引数の数
を組み合わせたものです。
つまり『メソッド名が同じで、引数が異なればオーバーロード』です。
下記の様なコードは適切なオーバーロードになります。
//基準となるコード public void test() { //処理 } //パターン① 引数が異なる public void test(int i, String s) { //処理 } //パターン② ①と引数の順番が異なる public void test(String s, int i) { // 処理 }
パターン①とパターン②は、一見同じようにみえます。
このパターンは、シグニチャが異なる定義の一つである
・引数の順番が違う
に該当します。
そのため、コンパイルエラーになることなく、オーバーロードが可能です。
逆にいうなら、シグニチャ以外の要素はオーバーロードに関係ないことになります。
具体的には
・アクセス修飾子
・戻り値
・static要素
・引数名
これらはオーバーロードには関係ありません。
//基準となるコード public void test(int i, String s) { //処理 } //NGパターン① アクセス修飾子を変更 private void test(int i, String s) { //処理 } //NGパターン② 戻り値を変更 public String test(int i, String s) { // 処理 } //NGパターン③ 引数名を変更 public String test(int i1, String s1) { // 処理 }
上記コードは基準となるコードと同一メソッドと捉えられます。
そのため、コンパイルエラーとなります。
◇オーバーライド
次に、オーバーライドについてです。
オーバーライドは『サブクラスがスーパークラスのメンバを上書き』することです。
オーバーライドの条件は以下になります。
・シグニチャが同じ
・戻り値型が同じ、もしくはサブクラス型
・アクセス修飾子が同じ、もしくはゆるい
・throws宣言は同じ、もしくはサブクラス型
具体例をもとに説明します。
//スーパー(親)クラス public class Sample { protected void test(String str) { //処理 } } //サブ(子)クラス public class SubSample extends Sample{ //パターン① 条件を変更せずオーバーライド protected void test(String str) { //内容 } //パターン② アクセス修飾子をゆるくする public void test(String str) { //内容 } }
パターン②ではアクセス修飾子をゆるくしています。
この変更であればオーバーライドとみなされます。
//スーパー(親)クラス public class Sample { protected void test(String str) { //処理 } } //サブ(子)クラス public class SubSample extends Sample{ //NGパターン① 戻り値を変更 protected String test(String str) { //内容 } //NGパターン② アクセス修飾子が厳しくなる void test(String str) { //内容 } }
NGパターンです。
上記のように条件から外れてしまうとコンパイルエラーとなります。
なお、引数を変えるとオーバーライドではなくなります。
そのとき、オーバーロードの条件を満たしていれば
オーバーロードとして記述することができます。
//スーパー(親)クラス public class Sample { protected void test() { //処理 } } //サブ(子)クラス public class SubSample extends Sample{ //サブクラスでオーバーロード protected void test(String str) { //内容 } }
今回は、オーバーロードとオーバーライドについてまとめてみました。
最後まで読んでいただきありがとうございました!
Java 変数の種類(ローカル・クラス・インスタンス変数)
こんにちは。のわです。
今回は、Javaから
『変数名と優先順位』をまとめてみます。
はじめに今回のポイントです
・メソッド内に記述された変数は『ローカル変数』と呼ぶ
・クラス定義直下、メソッド外に定義された変数を『インスタンス変数』『クラス変数』と呼ぶ
・インスタンス変数名やクラス変数名はローカル変数名と同一でも構わない
・同名の変数名がある場合、変数名だけ記述するとローカル変数として処理される
では、例をあげて、まとめてみます。
例は『下記コードをコンパイル・実行した際何が起きるか』
という内容です。
実際のコードがこちら
【Main.java】
public class Main { static int num = 10; public static void main(String[] args) { int num = num; System.out.println(num); } }
皆さんこのコードの実行結果はわかりましたか?
私は悩んだ挙げ句、答えを『10』にしました。
結果、見事に間違えました。(泣)
では、解説しながらポイントをまとめてみます。
私が悩んだのは問題コードのこの部分
int num = num;
このコードの右辺にある『num』
これを私は
static int num = 10;
のnumだと思っていました。
ここで、1つ目のポイントになります。
public class Main { static int num = 10; public static void main(String[] args) { int num = num; System.out.println(num); } }
『static int num = 10』はメソッドの外で宣言されています。
このようにクラスの中にあり、メソッドの外で宣言された変数を『メンバ変数』と呼びます。
なお、メンバ変数には2種類あり
『クラス変数』→staticあり
『インスタンス変数』→staticなし
で大別されます。
今回はstaticがありなので『クラス変数』となります。
2つ目のポイントです。
このメンバ変数は使うときは『明示的に使うことを記述する』のが大切になります。
つまり『この変数名はクラス変数なんだよ〜』って教えてあげることです。
例えば、先程のクラス変数を使うコードを記述します。
public class Main { static int num = 10; public static void main(String[] args) { System.out.println(Main.num); } }
このように、クラス変数を使用する場合は
『クラス名.クラス変数』で記述する必要があります。
では、私が悩んだ、このコードは何を意味しているのか
int num = num;
これはmainメソッドの中で宣言されている変数となります。
このようにクラスの中にあり、メソッドの中で宣言された変数を『ローカル変数』といいます。
ローカル変数は『記述されたブロック内で有効』な変数です。
上記コードを言葉で表すなら
『ローカル変数numを宣言し、その中にnumを参照代入する』
という意味にまります。
では、右辺のnumは結局どのnumになるのかですが
このnumには何の明示もありません。
ここで3つ目のポイントです。
・メンバ変数名とローカル変数名が同一
・変数に明示がない
この2つに当てはまる場合、変数は『ローカル変数』として処理されます。
つまり、上記コードは
『自分自身を参照代入』している状態です。
その後にコンソール出力しようとしていますが
自分自身を参照代入している状態 = 初期化が終わっていない状態です
初期化していないもの使用しているコードだと、コンパイルエラーになります。
つまり、答えは『コンパイルエラー』になる。
です。
では、上記コードを私が考えていた『10』にするにはどうすればいいか
右辺のnumをクラス変数だと明示してあげることです。
public class Main { static int num = 10; public static void main(String[] args) { int num = Main.num; System.out.println(num); } }
因みに、インスタンス変数だと少し記述が変わります。
public class Main { int num = 10; public static void main(String[] args) { int num = new Main().num; System.out.println(num); } }
この場合、一度newでインスタンスを生成しています。
(mainメソッドがstaticなためthisで明示できない)
今回は変数についてまとめてみました。
まとめです。
◇変数には大きく分けて『メンバ変数』と『ローカル変数』が存在する
◇メンバ変数には『インスタンス変数』と『クラス変数』があり、その使い分けはstatic有無できまる
◇メンバ変数とローカル変数が
・同一名である
・変数に『this.変数名』や『クラス名.変数名』のように明示がない
この条件を満たした場合、明示がない変数は『ローカル変数』で処理される
今回も最後まで読んでいただきありがとうございました!!
JavaSilver_躓いた問題をまとめてみた④ 配列その1
こんにちは。のわです。
今回は、Javaの配列についてまとめたいと思います。
最初に配列の基本的なルールについてまとめます。
先に今回のポイントを記述します。
・配列定義時は要素数・または要素を明確にする
・変数宣言に要素数を入れない
実際のコードを見ながら確認していきます。
配列の生成方法にはいくつかパターンがあります。
public class Main { public static void main(String[] args) { // パターン1 配列インスタンスを生成後に要素を代入する int[] array1 = new int[2]; array1[0] = 1; array1[1] = 2; for(int i : array1) { System.out.println(i); } // パターン2 配列インスタンス生成と同時に要素を代入する int[] array2 = new int[]{3,4}; for(int i : array2) { System.out.println(i); } // パターン3 パターン2を簡略化したもの int[] array3 = {5,6}; for(int i : array3) { System.out.println(i); } //パターン4 配列変数を宣言してから、配列インスタンスを生成 int[] array4; array4 = new int[]{7,8}; for(int i : array4) { System.out.println(i); } } }
コードの解説です。
このコードでは、4つのパターンで配列を生成しています。
生成した配列に要素を入れ、for文で一個ずつ取り出して出力するものです。
記述方法はそれぞれ違いますが、やっていることは一緒です。
なお、配列を記述する際に使われる『[]』大カッコですが、変数名のあとにつけても大丈夫です。
Java以外の言語から来た人でも、柔軟にコードをかけるようにしている為だそうです。
public class Main { public static void main(String[] args) { // パターン5 パターン3の大カッコを変数のあとにつける int array5[] = {9,10}; for(int i : array5) { System.out.println(i); } } }
ここで注意したいのが
どのパターンも『配列インスタンス生成時、要素数・または要素が確定している』ことです。
つまり、配列インスタンスを生成するとき
『要素数がいくつ入っているか不明だとコンパイルエラー』になります。
実際のNGコードでみてみます。
public class Main { public static void main(String[] args) { // NGパターン1 配列宣言時に要素数が確定していない int[] arrayNg1 = new int[]; } }
このコードでは配列インスタンス生成時に要素数を確定させていません。
そのため、コンパイルエラーになります。
もう一つ別の例をみてみましょう
public class Main { public static void main(String[] args) { // NGパターン2 要素数と要素を同時に定義している int[] arrayNg2 = new int[2]{1,2}; } }
このパターンは配列インスタンス生成時に要素数と要素どちらも定義しています。
配列インスタンス生成時は『要素数または要素どちらかを明確にする』ことになっています。
そのため、どちらも定義しているとコンパイルエラーになります。
次に、ちょっと特殊なコードを紹介します。
public class Main { public static void main(String[] args) { // パターン6 要素数が0の配列を生成する その1 int[] array6 = new int[0]; // パターン7 要素が無い配列を生成する その2 int[] array7 = {}; } }
この配列は要素数が0、要素が無い配列を生成しています。
一見するとコンパイルエラーになりそうです。
こちらのコード、実はコンパイルが通ります。
なぜなら
『要素数が0と明記されている配列』
『要素が無いと明記されている配列』
であり、配列定義の要件を満たしているからです。
ただし、この配列はなんの意味も持ちませんけど...
次に、別なNGコードです。
public class Main { public static void main(String[] args) { // NGパターン3 配列変数側に要素数を入れている int[2] arrayNg3 = {5,6}; } }
このNGパターンは配列宣言側に要素数を入れています。
変数宣言側は『私はこういった型です』といった宣言をするものです。
そのため、変数宣言側に具体的な数値をいれるとコンパイルエラーになります。
次に多次元配列についてまとめます。
多次元配列は『配列の中にさらに配列がある』状態です。
実際にコードを見てみましょう。
public class Main { public static void main(String[] args) { // 多次元配列パターン1 int[][] arrays1 = new int[2][3]; arrays1[0] = new int[]{1, 2, 3}; arrays1[1] = new int[]{4, 5, 6}; for(int[] i : arrays1) { //1次元目の配列を取り出す for(int ii : i) { //2次元目の配列を取り出す System.out.println(ii); } } // 多次元配列パターン2 int[][] arrays2 = new int[][]{{7, 8, 9}, {10, 11, 12}}; for(int[] i : arrays2) { for(int ii : i) { System.out.println(ii); } } } }
多次元配列も変数宣言の[]大カッコはどこに置いても大丈夫です
public class Main { public static void main(String[] args) { // 多次元配列パターン3 大カッコの場所が変わった int[] arrays3[] = new int[2][3]; arrays3[0] = new int[]{1, 2, 3}; arrays3[1] = new int[]{4, 5, 6}; for(int[] i : arrays3) { for(int ii : i) { System.out.println(ii); } } // 多次元配列パターン4 int arrays4[][] = new int[][]{{7, 8, 9}, {10, 11, 12}}; for(int[] i : arrays4) { for(int ii : i) { System.out.println(ii); } } } }
多次元配列の特徴として、
『1次元配列の要素数が決まっていれば2次元要素数は不明でもOK』というルールがあります。
それが次のコードです。
public class Main { public static void main(String[] args) { // 多次元配列パターン5 生成時に1次元だけ宣言しておく int[] arrays5[] = new int[2][]; arrays5[0] = new int[]{1, 2, 3}; arrays5[1] = new int[]{4, 5, 6}; for(int[] i : arrays5) { //1次元目の配列を取り出す for(int ii : i) { //2次元目の配列を取り出す System.out.println(ii); } } } }
上記コードの場合、1次元配列の要素数だけ先に決め
その後で2次元配列の要素を入れています。
なお、その逆はコンパイルエラーになります。
public class Main { public static void main(String[] args) { // NG多次元配列パターン 生成時に2次元だけ宣言しておく int[] arraysNg[] = new int[][3]; } }
今回は配列についてまとめました。
おさらいです
・配列生成時は『要素数または要素が決まっている』状態にする。
・要素数0、要素が無い配列を作成することも可能
・多次元配列は『1次元配列の要素数が決まっている』ことが生成の条件
JavaSilverの勉強をしていて一番理解が足りていない箇所だと痛感しまとめてみました。
配列を理解したい方への助けになれば幸いです。
今回も最後まで読んでいただきありがとうございました!!
JavaSilver_躓いた問題をまとめてみた③ 例外処理その2
こんにちは。のわです。
今回は現在勉強中のJavaSilverから
例外について引き続きまとめたいと思います。
前回の記事で簡単に構文について記載しました。
nowa-0402.hatenablog.com
今回は例外の伝播と例外を発生させる方法について記述します。
例外の伝播とは
「あるメソッドが違うメソッドを呼び出したとき
そのメソッドで例外が発生し、例外処理されなかった場合
呼び出し元まで例外がたらい回しにされる状況」のことです。
言葉だと長いですね...
例えば mainメソッドがsubメソッドを呼び出すコードを例にしてみましょう。
public class Main{ public static void main(String[] args) { sub(); System.out.println("処理を終了します"); } public static void sub() { String a = null; System.out.println(a.length()); } }
上記コードでは
①mainメソッドがsubメソッドを呼び出す
②subメソッドは変数aに入っている文字列数を出力する
③「処理を終了します」と出力される
しかし、subメソッドの変数aにはnullが入っています。
lengthメソッドでnullを参照しようとしているため
② の処理中に「NullPointerException」の例外が発生します。
この時、上記コードでは何が起きるか
①subメソッドは発生した例外をキャッチしようとするが、try-catch文が無いためキャッチできない
②キャッチ出来なかった例外をmainメソッドにわたす
③mainメソッドはsubメソッドから例外が来るが、try-catch文がないためキャッチできない
④誰もキャッチできずプログラムが強制終了する
という処理になっています。
まさに「例外がたらい回し」にされている状態です。
「処理が終了します」までたどり着けませんでした。
上記のような単純なコードなら強制終了でもいいですが
インフラシステムのようなプログラムで簡単に強制終了されたら大変なことになりますね。。。
では、どうすればよいか
解決策は2つです。
①subメソッドが例外を処理する
②呼び出し元のmainメソッドが処理する
①subメソッドが例外を処理する。
この場合だと下記の様なコードになります。
public class Main{ public static void main(String[] args) { sub(); System.out.println("処理を終了します"); } public static void sub() { try{ String a = null; System.out.println(a.length()); } catch(NullPointerException e) { System.out.println("エラーが発生しました"); } } }
このとき、subメソッドは『自ら起こした例外を自ら処理する』状態ともいえます。
②呼び出し元のmainメソッドが処理する
だとどうなるでしょうか。
public class Main{ public static void main(String[] args) { try { sub(); } catch (NullPointerException e) { System.out.println("subメソッドで異常が発生しました"); } System.out.println("処理を終了します"); } public static void sub() throws NullPointerException{ String a = null; System.out.println(a.length()); } }
見慣れないワードが出てきました
『throws』はスロー宣言と呼ばれるものです。
この宣言をしているメソッドはtry-catch文の記述をしなくても良くなります。
その代わり、スロー宣言をしているメソッドを呼び出す側でtry-catch文を使う必要があります。
簡単にいうなら
「私を呼び出して何かあったら、呼び出したあなたが責任持って処理してくださいね」
という状態です。
※上記コードですが、try-catch文を必ずしも書く必要は無い例外です。
try-catchが必須なのはException系になります。
NullPointerExceptionはRuntimeException系と呼ばれる例外種類になります。
この例外は必ずしもtry-catch文を書く必要がないものです。
今回は簡単コードを記述するためにRuntimeException系を例にあげました。
では、次に例外を自力で発生させる方法を見ていきましょう。
public class Main{ public static void main(String[] args) { String a = null; if(a == null) { throw new NullPointerException("値がnullです"); } } }
『throw』は『例外的状況の報告』をする構文になります。
上記コードの場合
throw new 例外クラス("任意のエラーメッセージ")
という記述になります。
この例外的状況の報告は「私達がJVMに例外を知らせるコード」です。
逆に、今まで記述してきた例外は「JVMが私達に例外を知らせるコード」でした。
これを『例外を投げる』といいます。
今回は例外についてまとめてみました。
要点のまとめです。
◇例外が発生する可能性がある処理をおこなうとき以下のどちらかで対応する必要がある
・呼び出し先でtry-catch文を記述する
・呼び出し先でスロー宣言(throws)を行い、呼び出し元で例外を処理させる
◇例外を投げるためにはthrow 文を使う
今回も最後まで読んでいただきありがとうございました!!
以下余談です。
JavaSilver用です。
例外の伝播は永遠にたらい回しにすることができます。
public class Main { public static void main(String[] args) { try{ sub(); } catch(Exception e) { System.out.println("異常発生"); } System.out.println("処理を終了します"); } public static void sub() throws Exception { System.out.println("subsubメソッドを呼び出します"); subsub(); } public static void subsub() throws Exception { System.out.println("例外を作り出します"); throw new Exception("例外です"); } }
Exception系の例外はどこかしらでキャッチする必要があります。
このコードでは例外処理をmainメソッドで行っています。
では、下記コードはどうでしょうか
public class Main { public static void main(String[] args) throws Exception{ sub(); System.out.println("処理を終了します"); } public static void sub() throws Exception { System.out.println("subsubメソッドを呼び出します"); subsub(); } public static void subsub() throws Exception { System.out.println("例外を作り出します"); throw new Exception("例外です"); } }
上記コードを見ると最終的な呼び出し元のmainメソッドもスロー宣言をしています。
この場合、コンパイルエラーになりそうですが、ならずに実行することができます。
ただし、「処理を終了します」まで処理が行われる前に強制終了になります。
うーん...例外って難しいですね...
JavaSilver_躓いた問題をまとめてみた② 例外処理その1
こんにちは。のわです。
今回は現在勉強中のJavaSilverから
例外についてまとめたいと思います。
例外とはなにか
一言でいうなら『プログラム実行中に想定外の状況になる』という現象です。
そして、例外処理とは
『例外に対する対策』をすることです。
早速コードを見てみましょう。
import java.io.*; public class Main { public static void main(String[] args) { try { FileReader fw = new FileReader("date.txt"); } catch (IOException e) { System.out.println("エラーが発生しました"); System.out.println(e); } } }
例外の基本文法は「try-catch」文です。
try文が通常処理
catch文が例外発生時の処理内容になります。
上記コードの解説です。
FileReaderを使用し、date.txtのファイルを読み出そうとしています。
このコード自体は問題がないのでコンパイルエラーとはなりません。
例外が発生するのは実行時です。
プログラムを実行した際、try文の中を処理しします。
プログラムはdate.txtを読み出します。
その時、date.txtが存在しないと例外発生となります。
(java.io.FileNotFoundException: date.txt (No such file or directory))
上記例外が出た際に、プログラムはcatch文に移行します。
catch文の構文は以下のとおりです。
catch(例外クラス 変数名)
変数名には例外内容が入ります。(今回だとFileNotFoundException)
その後は任意の処理内容を記載します。
では、次に上記コードに文を追加しましょう。
import java.io.*; public class Main { public static void main(String[] args) { try { FileReader fw = new FileReader("date.txt"); } catch (IOException e) { System.out.println("エラーが発生しました"); System.out.println(e); } finally { System.out.println("処理を終了します"); } } }
finallyという文が追加されました。
finallyは「必ず処理される文」です。
try文で例外が発生するとcatch文に移行するのは先程解説いたしました。
catch文で処理されたあと、プログラムは強制終了となるため
後片付けの処理ができなくなります。
この時に使用するのがfinally文です。
finally文は必ず処理される内容を記述します。
必ず処理されるので
・try文がうまく行った場合
・try文で例外が発生した場合
の二段階の対応が可能になります。
今回は例外処理についてまとめてみました。
・例外とは「プログラム実行中に想定外の事態が発生した状況のこと」
・例外を処理する文法は「try-catch」文
・必ず処理させたい内容は「finally」文に記述
次回は例外処理の具体的な内容をまとめたいと思います!
今回も最後まで読んでいただきありがとうございました!
JavaSilver_躓いた問題をまとめてみた① コンスタントプール
こんにちは。のわです。
今回からしばらくの間、現在勉強中のJavaSilverから躓いた問題をまとめてみたいと思います。
第一回目はコンスタントプールです。
早速ですが、問題です。
String a = "abc"; String b = "abc"; System.out.println(a == b);
上記コードを実行時、出力結果は何になるでしょうか。
答えは【true】です。
私は【false】だと思っていました。
では下記コードだとどうなるでしょうか。
String a = "abc"; String b = new String("abc"); System.out.println(a == b);
答えは【false】です。
この違いはなんでしょうか。
解説です。
なぜ私が最初のコードの実行結果を【false】だと思っていたのか。
それは『String 変数 = "文字列";』を、都度Stringインスタンスが生成されているものだと勘違いしていたからです。
私の脳内イメージは
「Stringでaとbを別々に生成されたインスタンス」
「a == bは同じインスタンスかどうかみているから答えはfalse」
となっていました。
通常の考え方であれば上記の認識で間違いはありません。
ただ、Stringは違っていたのです。
では、Stringインスタンス生成時の特別ルールとはなにか。
キーワードは『コンスタントプール』になります。
コンスタントプールは
『コード中に同じ文字列リテラルが登場した場合、Stringインスタンスへの参照先が使いまわしされる仕組み』
になります。
言い換えるなら
『同じ言葉で格納されたStringインスタンスは同じ参照先をみている=同一インスタンス』
となります。
なぜコンスタントプールが存在するのか
文字列リテラルはプログラム上にたくさんでてきます。
そのため、同じ文字列を何度も生成するのはメモリをたくさん使う羽目になります。
上記理由から
「同じ文字列だったら、できる限り使いまわししてメモリ処理を軽減させようね」という目的で存在しているわけです。
では、もう一度最初のコードをみてみましょう。
String a = "abc"; String b = "abc"; System.out.println(a == b);
この問題では同文字列リテラルのインスタンスを2つ生成しています。
上記解説に当てはめるなら『String b はString aと一緒の文字列だから、参照先を一緒にしている』状態です。
そのため、最後の同一判定でtrueとなるわけです。
そうすると違う疑問が出てきます。
なぜ、2つ目のコードでは【false】になるのでしょうか
String a = "abc"; String b = new String("abc"); System.out.println(a == b);
この場合、String bはnew演算子でインスタンスを生成しています。
new演算子で明示的にインスタンスを生成すると
コンスタントプールを参照せずに、インスタンスを生成することができます。
つまり、String a でabcの文字列リテラルはコンスタントプール内に出来ているのですが
String bではコンスタントプールを参照しないで生成している状態です。
上記理由からSting a とString b は参照先が異なるインスタンスになるため【false】になります。
では、次に先程のコードにあるメソッドを追記します。
この場合、答えはどうなるでしょうか。
String a = "abc"; String b = new String("abc"); System.out.println(a == b.intern());
この場合、答えは【true】になります。
intern()メソッドは『コンスタントプールを含むメモリ内の文字列を探して再利用する』メソッドです。
因存在しない場合は、文字列をコンスタントプールに登録したのち、その登録先を参照先として格納します。
このことから、b.intern()で何が行われているか
b,intern()によって
『bに格納されている文字列をコンスタントプール含むメモリ内から検索し、あればその参照先を戻り値とする』
という処理が行われています。
この場合の文字列は「abc」
「abc」はString aが先にコンスタントプールに登録しています。
そのため、b.intern()にはString aと同じ参照先が格納されるため同一参照先をみることになります。
結果
『a == bは異なる参照先だから【false】』
『a == b.intern()はbの参照先がaと同じになったので【true】』
になります。
今回のまとめです。
◇コンスタントプールとは『文字列を保存し、同文字列出現時、使い回しできるようにする仕組みのこと』
◇Stringインスタンス生成時の右辺への記載を
・文字列だけにする⇒コンスタントプールの対象
・new演算子で明示的にする⇒コンスタントプールの対象外
◇intern()メソッドの機能『コンスタントプールを含むメモリ内の文字列を探す』
・あればその文字列の参照先が戻り値となる
・なければ文字列をコンスタントプールに登録した後、そこを参照先して戻り値を返す
今回も最後まで読んでいただきありがとうございました!