はじめに
新卒1年目の時、Java Silverの問題集に掲載されていたある問題にかなり悩まされた。その際Javaの仕様とコンピュータのメモリについて勉強できたので、自分なりに記しておく。
目次
問題設定
前提知識
コピーの方法
解答
感想
問題設定
問題:A~Dのコードを実行したとき、True
になるのはどれか
char[][] array1 = {{'a', 'b'}, {'c', 'd'}};
char[][] array2 = array1.clone();
char[] array3 = array1[1].clone();
-
A:
array1[1] == array3
-
B:
array1[1] == array3[1]
-
C:
array1[1] == array2[1]
-
D:
array1.equals(array2)
clone
メソッドの詳細がよくわからなくても、Bは次元が異なるので不正解(False
になる)であることがわかる。残りの3つについて以下で見ていく。
前提知識
同一性と同値性
プログラミングを勉強し始めるとすぐにこの概念に辿り着く。
- 同値性:変数の中の値が同じかどうか
- 同一性:変数の参照先(アドレス)が同じかどうか
これらはよく==
とequals
メソッドの問題として出題されるので、仕様も含めてサンプルコードで確認する。
String str1 = "aaa";
String str2 = new String("aaa");
System.out.println(str1 == str2); //false
System.out.println(str1.equals(str2)); //true
String[] arr1 = {"bbb"};
String[] arr2 = new String[]{"bbb"};
String[] arr3 = arr1;
System.out.println(arr1 == arr2); //false
System.out.println(arr1.equals(arr2)); //false
System.out.println(arr1 == arr3); //true
System.out.println(arr1.equals(arr3)); //true
-
str1 == str2
は同一性をチェックする。str1
とstr2
はインスタンスが異なる(メモリ上のアドレスが異なる)ためFalse
が返る。 -
str1.equals(str2)
は同値性をチェックする。str1
とstr2
のメモリ上のアドレスは異なるが、文字列は同じであるためTrue
が返る。
単純な文字列の例は簡単だが、これが配列になると少しややこしい。
-
arr1 == arr2
も同一性をチェックする。str1
とstr2
はインスタンスが異なるためFalse
が返る。 -
arr1.equals(arr2)
のequals
は同一性をチェックする。これは配列に用いるequals
メソッドがObject
クラスのメソッドであり、それは同一性をチェックするからだ。よってarr1
とarr2
はインスタンスが異なるためFalse
を返す。 -
arr1 == arr3
はもちろんTrue
-
arr1.equals(arr3)
は上で見た通り同一性をチェックする。arr1
とarr3
は同じインスタンスだからTrue
を返す。
これは二重配列になっても同じである。
String[][] arr4 = {{"a"}, {"b"}};
String[][] arr5 = new String[][]{{"a"}, {"b"}};
System.out.println(arr4 == arr5); //false
System.out.println(arr4.equals(arr5)); //false
cloneメソッド
その名の通り、自分自身をコピーする。何をやっているかは下のコードを見ていただきたい。
String[][] arr6 = arr4.clone();
for(int i=0;i<2;i++){
System.out.println("arr6の" + i + "0成分は" + arr6[i][0]);
}
上で定義した二重配列arr4
のcloneをarr6
に代入した。コピーであればfor文も実行できるはずである。実際このコードは正しく処理され、想定通りの結果が返ってくる。
arr6の00成分はa
arr6の10成分はb
さて、今度はこれらの同一性や同値性を考えてみよう。
System.out.println(arr4 == arr6);
System.out.println(arr4.equals(arr6));
System.out.println(arr4[0] == arr6[0]);
System.out.println(arr4[0].equals(arr6[0]));
false
false
true
true
この結果には当時かなり違和感を持った。arr4 == arr6
という"全体"は同一でないのに、arr4[0] == arr6[0]
という"部分"は同一といっているからだ。
この疑問を解消するためにはコピーのされ方ついて知っておく必要がある。
コピーの方法
ここで、コピーにはシャローコピーとディープコピーの2種類があることをおさえておきたい。特に、Javaのclone
メソッドは内部でシャローコピーを行っている。
-
シャローコピー
メモリ上にある実体はコピーせず、アドレスだけコピーしている。 -
ディープコピー
メモリ上にある実体とそのアドレスを再帰的にコピーする。
arr4
とarr6
を使ってシャローコピーの性質をみてみると次のようになる。
System.out.println(arr4 == arr6); //false
System.out.println(arr4[0] == arr6[0]); //true
通常はコピーを行うとメモリ上の別のアドレスに同じ実体が作成される。すると、==
で同一性をチェックするとFalse
となりそうだ。しかし、arr4[0] == arr6[0]
はそうではない。これはarr4
のアドレスのみがメモリ上でコピーされ、arr4[0]
の実体はコピーされていないからだ。なお、arr4[0]
とarr6[0]
は同じ実体を参照しているため、どちらかに変更を加えるともう一方にも影響が出る。
解答
ここまでを踏まえて最初の問題に解答してみる。
-
A:
array1[1] == array3
-
array1[1] == array1[1].clone()
ということであるが、clone
元とそのコピーはアドレスが異なるため不正解。
-
-
B:
array1[1] == array3[1]
- 次元が異なるので不正解。
-
C:
array1[1] == array2[1]
-
array1[1] == array1.clone()[1]
であり、同じアドレスを指しているため正解となる。
-
-
D:
array1.equals(array2)
- 上で述べた通り、配列の
equals
は同一性をチェックするため不正解。
- 上で述べた通り、配列の
というわけで正解はCだった。試験としてはJavaの仕様を理解していれば合格できるが、この問題はそこから少しだけはみ出た内容となっている。
感想
頭の中でメモリをイメージして、コピーの挙動を理解できれば解きやすくなると思う。この記事を書くために過去に勉強した内容を振り返っていたが、Javaの仕様だけでなくその他のコンピュータの基本部分にも興味が湧いた。