はじめに
javaシルバーの資格取得に向けて学習を進めていく中で、String型の扱いについて理解を深めるために学習した内容のメモです。
解釈に誤りがあれば指摘いただければ幸いです。
String型は参照型であって基本データ型ではないようだ
変数を定義する際に、int
とかdouble
とかboolean
とかchar
とかデータ型を指定して、変数を定義してました。
同じように文字列を扱う変数を定義する時も、String
型を指定して変数を定義する事はわりと当たり前のようにやってました。
こんな感じ↓
int i = 100
// -> 整数を扱えるint型に整数100を代入
char c = 'あ'
/* -> 文字を扱えるchar型に`あ`を代入
char型の場合はダブルクォーテーションではなくてシングルクォーテーション*/
double d = 10.0
// -> 小数点を扱える「浮動小数点数型」に10.0を代入
String s = "文字列です。"
/* -> 文字列を扱えるString型に"文字列です。"を代入
Stringの場合はシングルクォーテーションではなくてダブルクォーテーション*/
ただ、ずっと疑問だった事が。
それは**「なんでString型だけ大文字で始まるん?」**ってこと
基本データ型と参照型があるんや
「なんでString型だけ大文字で始まるん?」
この答えは、ずばりString型は基本データ型ではないからという事にありました。
言い換えると、String型の正体はクラスだったってことです。
確かに、クラスは最初の文字が大文字だし、なるほど!と納得です。
Stringクラスの中身を覗いてみる
Stringはクラス。であれば中身もみれる。
って事で、Stringクラスの中身をみると、インスタンスフィールドの一つにこんな定義がされているんです。
private final char value[];
これってつまり、基本データ型char型
の配列ですね!
なので、Stringの正体は基本データ型char型
の配列一まとまりだったみたいです。
さっきの変数の定義.java
で定義したString s = "文字列です。"
は言い換えると
private final char value[] = {'文','字','列','で','す','。'}
ってことだったんですね。
String型だけでなく、StringBuilder型もあるらしいぞ
そんなこんなで学習を進めていくと、String以外にもStringBuilderっていうものもあるんですね。
##何が違うんだ?
現時点で理解しているのはこんな感じです。
String型 | StringBuilder型 | |
---|---|---|
変数の宣言 | newなしでもありでもいける | newを使わないとダメ宣言できない |
変数への再代入 | 参照先が変わる | 中身を書き換える |
表にする程の内容じゃなかったな・・・
データの扱い方もそうですが、そもそもクラスが異なることもあって、用意されているメソッドも違います。
##変数の宣言
StringとStringBuilderとでは変数定義の方法が決まってます。
String a = "あいうえお"; // -> できる
String b = new String("かきくけこ"); // -> できる
StringBuilder c = "さしすせそ"; // -> できない
StringBuilder d = "たちつてと"; // -> できる
###String型の宣言方法による違いは?
文字列の管理の仕方が異なるようです。
newを使わない場合は、Stringクラスが管理している領域に対して、文字列を用意してくれます。
newを使うとStringクラスが管理している領域とは別に個別に文字列が作成されます。
なので、同じString型であっても、newで定義すると、一匹狼のString変数ができあがるようです。
どう違ってくるのか、おそらくStringクラスが持っているメソッドのintern()を使うとイメージつくかもしれません。(後述)
##変数への再代入について
「変数への再代入ができない?いやいや。できるじゃん!」そうお思いの方。
ですね。確かにできている気がします
String a = "あいうえお";
a = "かきくけこ";
// -> aの中身は"かきくけこ"になる
けど、これって書き換わっているのではなくて、aの参照先が"あいうえお"から"かきくけこ"に切り替わっているみたいです。
なので、厳密には書き換えているのではなく、切り替えている。みたいです。
#String型の比較について
実装をする上で、String型の文字列比較が必要となる事があると思います。
そんな時、比較演算子を使うのが主流ですが、String型の場合は比較演算子ではなく、equals()
メソッドを使います。
##ナゼなのか
結論から話しますと、比較演算子の場合は参照元を比較してしまうようです。
もっとわかりやすく言うと、s1とs2が同じ"あいうえお"だけど、 s1 == s2 とした時、場合によってはfalseになってしまうのです。
package sample20200728;
public class Sample02 {
public static void main(String[] args){
String s1 = new String("あ");
String s2 = "あ";
String s3 = "あ";
//s1とs2を比較演算子で比較
if(s1 == s2){
System.out.println("s1とs2の文字列は一緒だよ");
}else{
System.out.println("s1とs2の文字列は違うよ");
}
//s1とs3を比較演算子で比較
if(s1 == s3){
System.out.println("s1とs3の文字列は一緒だよ");
}else{
System.out.println("s1とs3の文字列は違うよ");
}
//s2とs3を比較演算子で比較
if(s2 == s3){
System.out.println("s2とs3の文字列は一緒だよ");
}else{
System.out.println("s2とs3の文字列は違うよ");
}
}
}
出力結果
文字列が一緒なのに、一部違うよ。と否定されちゃってます。これは比較演算子だと、文字列を比較せずに、参照元を比較してしまうようです。
参照元が一緒かどうか、を判断する時はこれで良いと思いますが、実運用を考えると、文字列が一致しているかどうか。を確認する事が多いのかな?と思います。
そうなると、文字列一緒なのに参照元が違うっていう理由でfalseを返されちゃうとバグの原因になっちゃいますよね?
細かい事は気にしない。文字列さえ一致してればそれでいい!という男前メソッドequals()さんが活躍します。
public class Sample02 {
public static void main(String[] args){
String s1 = new String("あ");
String s2 = "あ";
String s3 = "あ";
//s1とs2をequalsメソッドで比較
if(s1.equals(s2)){
System.out.println("s1とs2の文字列は一緒だよ");
}else{
System.out.println("s1とs2の文字列は違うよ");
}
//s1とs3をequalsメソッドで比較
if(s1.equals(s3)){
System.out.println("s1とs3の文字列は一緒だよ");
}else{
System.out.println("s1とs3の文字列は違うよ");
}
//s2とs3をequalsメソッドで比較
if(s2.equals(s3)){
System.out.println("s2とs3の文字列は一緒だよ");
}else{
System.out.println("s2とs3の文字列は違うよ");
}
}
}
うん。文字列をしっかり比較してくれてますね!
#おまけ internメソッド
Stringクラスにはinternメソッドというものが用意されています。
メソッドの説明には「文字列プールの中にある一意の文字列を返す」とありました。
日本語でおk。
でもきっとjavaの試験に出てくるだろうと思い向き合いました。
internメソッドを理解するためにソースコード書いてみた
「文字列プールの中にある一位の文字列を返す」がぜんっぜんピンとこなかったので、実際にソースを書いて挙動を確認してみました。
###理解できたことは、こんな感じ
1.intern()メソッドは、対象の文字列がString型の持つ領域内に存在しているかどうかを調べてくれる。
同一の文字列がある場合
-> それを参照するようにする。 使い回すようなイメージ
同一の文字列がない場合
-> String型の持つ領域内にその文字列を用意する。
2.String型の変数を定義する時にnewを使って値を定義すると、String型の持つ領域の管轄外となる。
-> intenr()を使っても参照元を同一にすることができない
public class Sample01 {
public static void main(String[] args){
String s1 = new String("テスト"); //String型クラスの持つ領域"外"でs1変数"テスト"を作成
String s2 = s1.intern(); /* s1.intern()の返り値の"テスト"という文字列をs2に代入する。
ただ、s1はnewで作成しているため、String型クラスの領域外で作った文字列のため、String型クラス内に"テスト"という文字列は存在していない。
この場合は、intern()を使ったタイミングで、String型クラスの領域内に"テスト"という文字列を用意する*/
String s3 = "むむむ"; //String型クラスの持つ領域"内"でs3変数"むむむ"を作成
String s4 = s3.intern(); /*s3.intern()の返り値の"むむむ"という文字列をs3に代入する。
s1と違いs3はString型クラスの領域内に"むむむ"を用意しているので、既に作っている"むむむ"を参照する(つまり文字列だけじゃなくて参照する場所も同じ)*/
System.out.println("s1はnewを使用した変数" + s1);
System.out.println("s2はs1.intern()の返り値を代入した変数" + s2);
System.out.println("s3は\"むむむ\"を代入した変数" + s3);
System.out.println("s4はs3.intenr()の返り値を代入した変数" + s4);
//newで作った"テスト"とs1.intern()で作った"テスト"を比較
if(s1 == s2){
System.out.println("true: s1とs2の参照先は同じです");
}else{
// s1とs2の参照先が違うので、falseになる
System.out.println("false: s1とs2の参照先は異なります");
}
//String型が管理している領域を使って用意したs3"むむむ"と、s3.intern()で拾ってきた"むむむ"を代入したs4を比較
if(s3 == s4){
//s3,s4ともにString型の領域から生成されている、s4についてはs3.intern()でs3の文字列を参照して代入している
System.out.println("true: s3とs4の参照先は同じです");
}else{
System.out.println("false: s3とs4の参照先は異なります");
}
/*s2は"テスト"という文字列をString型クラスが持つ領域内に作っている
そのため、s5 = s2.intern()とした時、s5にはs2と同じものを参照することになる*/
String s5 = s2.intern();
//s2とs5の参照元が同じかどうかを確認する
if(s2 == s5){
//s2.intern()を使ってs5に代入しているので、参照元は同一のものとなる。
System.out.println("true: s2とs5の参照先は同じです");
}else{
System.out.println("false: s2とs5の参照先は異なります");
}
System.out.println("============");
}
}
どう足掻いてもnewで定義したs1は仲間外れになります。
#おわり
以上が、String型と向き合った内容でした。
まだまだ理解がおいついていない事も多いので、きっと誤った解釈もあると思います。
その場合はお手数をおかけしますが、ご指摘いただけますと幸いです。
それでは、ありがとうございましたm(_ _)m