この記事は静大情報LT大会 Advent Calendar 2019 3日目の記事です。
#はじめに
レベルの低い話です
Javaがとりあえず普通並みにできると読みやすいです。
文字列比較したことがあればとりあえず読めます。
#復習(?)
Javaで文字列比較を行うときは、 「==」で比較してはいけません。
String#equalsメソッドを使いましょう。
public class Test {
public static void main(String[] args) {
String a = "HelloWorld";
String b = "Hello";
b += "World";
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
}
}
#ちょっとまってくれ「==」でもtrueになるけど
public class Test {
public static void main(String[] args) {
String a = "HelloWorld";
String b = "HelloWorld";
System.out.println(a == b); // true
System.out.println(a.equals(b)); // true
}
}
なんでだ?
#「==」の仕組み
##JVM
「==」はよくアドレスの場所の比較といわれていますが詳しく見ていきましょう。
Javaにおいて「==」は、多くの場合コンパイル時に、JVMの命令の一つ、「if_acmpne」や「if_acmpeq」などに変換されます。
JVMというのはJavaのファイルをコンパイルしたときにclassファイルというのがたくさん生成されますよね?そのclassファイルを解釈して実行するのが、Java仮想マシン(JVM)というものです。
classファイルは多くの人間が読めない(読める人もまれによくいる)フォーマットですが、JVM(コンピュータ)には読みやすいようになっています。
JVMはシンプルな命令を複数組み合わせて複雑なプログラムを実現しているため、Javaでは一つの命令に過ぎないものが、JVMでは多くの場合、複数の命令に変換されます。
##JVM命令とオペランドスタック
では、「if_acmpne」や「if_acmpeq」がどういう命令なのかというと、オペランドスタックから二つ取り出してそれらが一致しているかを調べ、指定のプログラムの行へと番号にジャンプする命令です。
オペランドスタック……?
JVMはスタックマシンと言われ、レジスタを使わず、「スタック」という装置を用いて各種演算を行います。
※スタック……上から値を入れて入れたものから順に取り出すことのできる装置(トランプの山札のようなもの)
オペランドスタックは作業領域のようなものです。
四則演算などにはちょうど良いため、よく使われます。
(逆ポーランド記法など)
例えば以下のような仕組みで5+12を計算することができます。
しかし、Javaでのオペランドスタックは1つ分の要素が32bitまでしかありません。
※32bit=2^32の数まで扱えるという意味です。
1文字をJavaで表すとき、このオペランドスタックの1要素分(=32bit)消費します。
文字列は可変サイズです。1文字を扱う時もあれば、この記事のように数百文字扱う場合があります。つまり、オペランドスタック1要素分(=32bit)では到底、すべての文字を扱うことができません。
そのため、文字列は別のメモリに保存しておき、そのメモリのアドレス(コンピュータ上の住所)をオペランドスタックに保存します。これによりオペランドスタック1要素分(=32bit)で足りるようになります。
※別のメモリとそれを用いたオペランドスタックのイメージ(0xに特に意味はないです)
##if_acmpeq / if_acmpne
先ほど述べた通り、「if_acmpne」や「if_acmpeq」はオペランドスタックから二つ取り出し、それら2つが、「if_acmpeq」なら等しい、「if_acmpne」では等しくない時、別の指定された行へとジャンプします。(ジャンプはよくあるGOTO命令みたいなやつです)
つまり、「if_acmpeq」なら、オペランドスタック上にあるアドレスの数値が等しければ、ジャンプ命令が発生するわけです。
このページ2つめのソースコード(「ちょっとまってくれ」のところ)でいうと、同じアドレスに"HelloWorld"が格納されていたため、trueと表示されるわけです。
#trueになるってことは……
##同じところに格納されているのか
そうだ。
じゃあこれはどうなると思いますか?
命名が適当なのは勘弁してください
public class Test {
public static void main(String[] args) {
String a = "HelloWorld";
method1(a);
}
public static void method1(String c) {
String k = "HelloWorld";
System.out.println(k == c);
}
}
答えは、「true」になります。
##クラス定数領域
Javaのクラスファイルには定数領域があります。
この定数領域には、コンパイルするときに、主に文字列などが格納されます。マジックナンバーやマジック文字列(?)のようなものは、この定数領域に格納され、JVMがクラスファイルを読み込んだ時に、メモリに読み込まれ利用されます。
たとえば
System.out.println("Hello Ja! Ja!");
のような、"Hello Ja! Ja!" などは、定数領域に格納されて、そこから文字列が読み込まれます。
また、Javaのコンパイラは賢いので、同じ文字列が2回出てきたとしても、前に1回以上使われているなら、同じ定数領域から読み込まれます。
つまり、先ほどのコードでは、たとえメソッドが違ったとしても、同じクラスの定数領域の「Hello World」という文字列を参照していたため、アドレスが同じになっていたのです。
先ほどのクラスファイルの定数領域、バイナリで見るとちゃんと「HelloWorld」が保存されている
余談だが、このページの1こめのソースコードは、わざと格納領域を別にするために"Hello"と"World"の2つに分けて文字列を書いた。
Javaのコンパイラではそこまでは計算してくれないようだ。Cのコンパイラなどではここまでやってくれるのもよくある。
##equalsは?
Stringクラスでオーバーライドしていて、中身をしっかり比較するコードになっている
イメージコードは下のような感じ。原文ママではない。
@Override
public boolean equals(String str) {
if(this.length() != str.length()) {
return false;
}
for(int i = 0; i < str.length(); i++) {
if(this.charAt(i) != str.charAt(i)) { //charAtで任意のn文字目をとりだせる
return false;
}
}
return true;
}
線形でちゃんと前から順に文字を1つ1つ比較している。
1文字は32bitに収まるので問題ない。
#まとめ
文字列を比較するときはequalsメソッドを使おう。
「==」でtrueになるときは、同じところに格納されてるんだね。ふ~~~ん。
明日のアドベントカレンダーの記事もよろピ!。
静大情報LT大会 Advent Calendar 2019