こんにちは。zuka(@beginaid)です。
この記事では自分がJavaを復習した際につまずいたポイントをまとめていきます。今回は,参照型としてのStringに関してです。
なお,本記事では分かりやすさを優先するため,用語を正確に使わない部分や理解が曖昧な部分を残すことがあります。予めご了承ください。間違いがございましたら,お問い合わせフォームまたは最下部コメント欄よりご指摘いただけますと助かります。
リテラルとデータ型
プログラミングを学んでいく中で「これを理解できると脱初心者だ!」というようなTipsは多々あります。特に,リテラルとデータ型について理解し,他人に説明できるようになれば,もう立派な中級者だと思います。
5
,3.14
,Hello
といったような具体的な値のことをリテラルと呼び,リテラルにはデータ型が設定されています。Javaを例にとると,5
はint型,3.14
はdouble型(又はfloat型),Hello
はString型になっています。
ここで,気付く人は気付くかもしれませんが,代表的なデータ型の中でもStringだけ1文字目が大文字になっています。これは何故なのでしょうか。答えは,Stringだけ参照型だからです。文字列はStringクラスのインスタンスなのです。他のデータ型はプリミティブ型(基本型)に分類されます。
プリミティブ型と参照型
データ型は大きくプリミティブ型(基本型)と参照型に分類されます。
プログラミング言語の仕様によって,最小単位となるデータの記述方法を元から定めてしまっているものを,プリミティブ型(基本型)と呼びます。以下に複数サイトからの定義を引用します。
そのデータ型の定義の中に部分として他の型を含まないような型
Wikipedia「primitive data type」
プログラミング言語などが仕様として提供する基本的なデータ型。または,定義に他のデータ型を用いない独立したデータ型。
e-Words「プリミティブ型」
Stringはプリミティブ型に分類されず,参照型として定義されています。参照型というのは,実際にデータを格納しているメモリの先頭番地を保持するようなデータ型のことを指します。
なお,Javaでは以下の8種類のプリミティブ型が準備されています。
名前 | bit数 |
---|---|
boolean | 1bit |
char | 16bit |
byte | 8bit |
short | 16bit |
int | 32bit |
long | 64bit |
float | 32bit |
double | 64bit |
Stringクラス
Stringクラスはjava.langパッケージの中でAPIとして定義されています。Stringクラスには,プリミティブ型であるchar型の配列からStringオブジェクトを生成するコンストラクタが定義されています。
つまり,
String str = "abc";
は,次と同じです。
char data[] = {'a', 'b', 'c'};
String str = new String(data);
特例
java.lang
パッケージに所属するクラスを利用する場合は,特例としてimport文を記述する必要がありません。更に,通常はインスタンスを生成するにはnew
演算子を利用する必要がありましたが,文字列を生成するたびにnew
を使っていては途方に暮れてしまうため,特例として二重引用符" "
で囲まれた文字情報はStringインスタンスとして利用できるようになっています。
// 本来は...
java.lang.String s = new String("abc");
// 特例を利用すれば...
String s = "abc";
参照型としてのString
配列はインスタンスのために確保してあるメモリの先頭番地を保持しています。ですので,Stringクラスのインスタンスを格納した変数もメモリの先頭番地を表しているのです。ここでは,諸事情(後述)のためint
型を要素にもつ配列で確認してみましょう。
int [] x = {1, 2, 3};
System.out.println(x); // [I@722c41f4
Javaにおけるポインタ
C言語ではポインタという概念が存在していて,実際にStringの先頭番地を取り出すことができましたが,Javaではそのような操作が封印されてしまいました。強いて言うならhashCode
メソッドを用いてハッシュ値を取得することができますが,この値はデータが格納されている先頭番地とは厳密には異なります。
String s = "abc";
System.out.println(s.hashCode()); // 96354が表示されるがこれは"abc"が保存されている先頭番地ではない
String自体は配列ではない
一点,注意いただきたいのは,String自体はchar型の配列ではないということです。配列であれば[]
で要素にアクセスできるはずなのですが,Stringでは[]
では要素にアクセスすることはできません。
String S = "abc";
System.out.println(S[0]); // コンパイルエラー
immutable
配列は参照渡しによりデータを書き換えることができますが,Stringはデータの中身を変えることは許されていません(immutableと呼びます)。その代わりに,データを書き換えようとしたら新しいメモリ領域を自動で確保してくれます。
// 配列の場合...
char[] s = {'a', 'b', 'c'}; // sは先頭番地を表す
char[] t = s; // tもsと同じく先頭番地を表す
t[1] = 'B'; // sとtのデータを書き換える
System.out.println(s); // "aBc"
System.out.println(t); // "aBc"
System.out.println(s==t); // true
// Stringの場合...
char[] s = {'a', 'b', 'c'};
String S = new String(s); // Stringは参照型なのでSには先頭番地が格納されているはず
String T = S; // Stringは参照型なのでTには先頭番地が格納されているはず
T = "ABC"; // Sと同じ先頭番地のメモリの中身を"ABC"に変えたのでSも変わっているはず
System.out.println(S==T); // false,つまり先頭番地が異なるということ
System.out.println(S.equals(T)); // false,つまり格納されたデータが異なるということ
ちなみに,コード中でStringクラスのインスタンス同士を==
で比較している部分がありますが,これは先頭番地を比較していることに注意してください。格納されているデータ自体を比較したい場合はequals
メソッドを利用します。
上記コード例でも分かる通り,配列を格納した変数s
は先頭番地を表しているので,その先頭番地をt
にコピーした上でtのデータを書き換えれば,s
のデータも書き換わります。一方で,Stringの場合は書き換えが許されていないため,S
の先頭番地をコピーしたT
を書き換えると,自動的にメモリ領域を確保してくれて,先頭番地も変更してくれるという仕組みをJavaは持っています。
Javaは更に賢くて,同じ値をもつStringを生成しようとすると,自動的に同じ先頭番地を取得してきてくれます。
String S = "ABC";
String T = "ABC";
System.out.println(S==T); // true,つまり先頭番地が同じということ
System.out.println(S.equals(T)); // true,つまり格納されたデータも同じということ
ただし,new
演算子を用いて明示的に異なるインスタンスを生成すれば,同じ値をもつStringを生成しても先頭番地は異なるように設定されます。
String S = new String("ABC");
String T = new String("ABC");
System.out.println(S==T); // false,つまり先頭番地が異なるといこと
System.out.println(S.equals(T)); // true,つまり格納されたデータが同じということ
コメント