新人エンジニア研修で知っておきたい文字列の使い方
なぜ、文字列の理解が重要なのか、その理由
この記事では、弊社の新人エンジニア研修の参考にJavaを解説します。
前回は配列の作成と使用について解説しました。
今回は文字と文字列の扱いについて解説します。文字列はコンピュータプログラム(すなわちそれを作った新人エンジニアであるあなた)とユーザーとのコミュニケーション手段ですからとても重要です。
1.文字と文字列の違い
まれに混同される新人エンジニアがいますが、Javaにおいて文字と文字列は別物です。
以下のサンプルプログラムを見てください。
package chap07;
public class Example01 {
public static void main(String[] args) {
char c1 = 'あ';
String str1 = "あ";
System.out.println(c1);
System.out.println(str1);
}
}
<実行結果>
あ あ |
どちらも実行結果は同じですが、c1にはひらがな「あ」の文字コードが、str1には文字列「あ」への参照が入っています。文字はプリミティブ型、文字列は参照型なのでした。
試しに以下のようなプログラムを実行するとどうなるでしょうか?
package chap07;
public class Example02 {
public static void main(String[] args) {
char c1 = 'あ';
int a = c1;
System.out.println(a);
}
}
<実行結果>
12354 |
この結果「12354」は、「あ」のUTF-8における文字コードを10進数で表現したものです。
IDEを使ってJavaのソースコードを見る事ができます。具体的な方法は講師から説明があります。ここで講師と一緒にJavaのソースコードツアーに行ってみましょう。
Stringクラスのソースコードを見ると156行目あたりに以下の記述があります。
Java17標準APIprivate final char value[];
このようにchar型の配列です。
※ちなみに、フィールドがfinal宣言されているのは、後で見るように文字列が一度作成したら中身を変えられない、ということを意味しています。
2.Stringクラス
String(標準API) は java.langパッケージに含まれるクラスです。そのためimport文なしでいきなりソースコード中にStringと記述できるのでした。
以下のExample04はnew演算子とString()というコンストラクタを使って文字列のインスタンス化をする例です。
※new演算子とコンストラクタについては9.インスタンスの活用のところで詳しく学びます。
package chap07;
public class Example04 {
public static void main(String[] args) {
String str = new String("Hello");
System.out.println(str);
}
}
しかし、文字列はとても頻繁に使いますので、以下のように簡単にインスタンス化する方法も用意されています。むしろこちらが一般的な文字列のインスタンス化です。
package chap07;
public class Example05 {
public static void main(String[] args) {
String str2 = "Hello";
System.out.println(str2);
}
}
※しかもこの方法は同じインスタンスを使い回すという点でパフォーマンスも優れています。
つまり、Stringは、new演算子を使わなくてもインスタンスを作れる特殊なクラスです。ただし、実はこの二つのインスタンスの作り方では微妙な違いがあります。
3.equals()メソッド
以下のExample06という文字列を比較しようとしているプログラムですが上手くいっていません。
package chap07;
public class Example06 {
public static void main(String[] args) {
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2);
String str3 = "Hello";
String str4 = "Hello";
System.out.println(str3 == str4);
}
}
<実行結果>
false true |
ここで、str1~4は参照です。参照が指し示しているのは"Hello"が格納されている"メモリのありか"です。
str1とstr2は異なる2つのインスタンスが作られています。ですから、それぞれが格納されているメモリのありかも違っていて、その結果上記のfalseが表示されたのでした。(下図参照)
対して、str3とstr4では同じ1つのインスタンスを参照しています。
実は、str4を=演算子でインスタンス化したとき、メモリの中を検索して、同じ文字列"Hello"があれば、それを再利用しているのです。(下図参照)その結果、trueが表示されたのでした。なにしろ、Javaの前身はOakという家電組み込み用のプログラミング言語だったのでメモリの節約を考慮したのです。
そうすると気をつけなければならないことがあります。例えば、ログインのシステムを考えてみましょう。ユーザーが画面から入力したIDとデータベースに格納されているIDを照合するといった処理を考えます。その際、2つのユーザーIDは別々のヒープ領域に格納されています。どのようにして同じであるという判断をしたら良いでしょうか?
==で比較するのではなく、Stringクラスのequals()メソッドを使います。
以下のExample07を見てください。
package chap07;
public class Example07 {
public static void main(String[] args) {
String str1 = new String("Hello");
String str2 = new String("Hello");
System.out.println(str1 == str2);
System.out.println(str1.equals(str2));
}
}
<実行結果>
false true |
下図はイメージです。
この後、研修が進むと、Webシステムでログインのシステムを作ります。その時のために2つの文字列が同一の文字列であるか確かめるためにはequals()メソッドを使わなければならないということを理解してください。
4.イミュータブル
もう少し、Stringクラスの話を続けたいと思います。実は、Stringクラスのインスタンスは一度作ったら中身を変えられないのです。
以下のExample08を見てください。
package chap07;
public class Example08 {
public static void main(String[] args) {
String str1 = "Hello";
str1 = str1 + " World";
System.out.println(str1);
}
}
<実行結果>
Hello World |
このとき、表示結果には影響ありませんが、7行目でstr1という変数が一旦捨てられて、同じ名前のstr1という変数が作り直されているのです。Stringクラスのインスタンスは一度作ったら中身を変えられないのです。このような性質を不変性:イミュータブル(immutable)といいます。
※ミュータント(mutant)は変異種という意味の名詞ですが、そこに否定の"im"がついて形容詞になっているのですね。
それを証明する次のExample09を見てください。
package chap07;
public class Example09 {
public static void main(String[] args) {
String str1 = "Hello"; //①
String str2 = str1; //②
System.out.println(str1 == str2);
str1 = "World"; //③
System.out.println(str1 == str2);
}
}
<実行結果>
true false |
もし③でstr1に”World”を代入した時点で参照はそのままで参照が指し示す値だけを上書きしたとしたら、2回ともtrueとなるはずです。ですが”World”を代入した時点で(名前は同じ)新たな参照が作成されたため、str1とstr2の参照値は異なりfalseとなった訳です。(下図参照)
上記のヒープ領域の"Hello"を"aloha"とか"Hey"に変更はできないのです。これがString型がイミュ―タブル(不変)【immutable】であるということです。Javaの設計者がStringをイミュ―タブルにした理由はいつくかありますが、一つはマルチスレッド対応です。(ウイルスが流行した時に話題になるのが【mutant】ですね)
プログラムを複数の処理の流れに分けることをマルチスレッドといいます。英語の【thread】には「糸」という意味があります。ある処理(thread)が文字列を処理している途中で別の処理(thread)がその文字列を書き換えてしまうとまずいのですね。そのため文字列はイミュ―タブルなのです。(さらに近年は様々なクラスをイミュータブルで作成することが多くなってきています)
さて、理由はさておき実務において、このことはどのような問題があるでしょうか?
例えば、大量の文字列の結合を繰り返す場合に、その都度新しいインスタンスを生成するためパフォーマンスが悪化します。何千何万回と文字列を結合するような場合には、Stringクラスを「+」で結合するのではなく、StringBuilderクラスのappend()メソッドを使うことをお勧めいたします。時間が許せば問題集で確認しましょう。
5.Stringクラスの便利なメソッド
Stringクラスには、(charのようなプリミティブ型とは違い)文字列を扱うための便利なメソッドが用意されています。
そのほんの一部を紹介します。
以下のExample10は「新人エンジニアのためのJava研修」という文字列を使って文字列の文字数を数えたり、任意の文字列の出現位置を調べたり、任意の文字列が含まれるか調べたり、任意の文字列を置き換えたりといったことをしています。
package chap07;
public class Example10 {
public static void main(String[] args) {
String str = "新人エンジニアのためのJava研修";
System.out.println(str.length() + "\n");
System.out.println(str.indexOf("Java"));
System.out.println(str.indexOf("Python") + "\n");
System.out.println(str.contains("研修"));
System.out.println(str.contains("Python") + "\n");
String str2 = str.replace("エンジニア", "SE");
System.out.println(str2);
}
}
<実行結果>
17 11 -1 true false 新人SEのためのJava研修 |
【length】は、「長さ」という意味なのでlength()メソッドは文字数を返します。
indexOf()メソッドは、文字列を配列としてみたときの添え字【index】の値のうち、実引数の文字列が最初に現れた添え字を返します。見つからない場合は-1を返しますので、正の数を返した場合はその文字列が存在するということで文字列の存在判定ができます。
配列の復習ですが、添字の数え方は下図の通り0始まりです。
【contains】は、「含む」という意味、【replace】は、「置き換え」という意味ですので文字通りですね。
他にも色々便利なメソッドがありますからStringの標準APIを探検してみてください。
また、後半のシステム開発演習では大きな数値を扱うとき、3桁カンマで表示するようにお願いすることが多いのですが、それは、以下のExample11のように書いて実現できます。
package chap07;
public class Example11 {
public static void main(String[] args) {
int price = 123456789;
System.out.println(String.format("%,d", price));
}
}
<実行結果>
123,456,789 |
業務システムでは金額を扱うことが多いですからStringクラスのformat()メソッドは重要です。3桁カンマ以外の書式についてはクラスFormatter(標準API)を調べてください。例えば、円周率の少数点第3位四捨五入2位表示であれば、
System.out.printf("%.2f", Math.PI);
のように書くことができます。
6.インスタンスを作らなくても仕事をしてくれるstaticメソッド
ここで、注目していただきたいのは、メソッドの呼び出し方です。
String.format("%,d", price)
Stringは先頭文字が大文字になっています。これは、クラスですね。クラスに属するメソッドということでクラスメソッドまたはstaticメソッドと呼ばれます。本書では以降staticメソッドで統一します。
【static】は静的という意味で、反意語は【dynamic】(動的)です。staticはクラスにあらかじめ用意してあるメソッドという意味です。動的に作り出したインスタンスが持つメソッドではないという意味です。
staticメソッドは、クラス名.メソッド名()という形で呼び出すことができます。
一方、
str.length()
のようにインスタンスを作ってから、その個々のインスタンスのメソッドを呼び出すのをインスタンスメソッドといいます。インスタンスメソッドは、変数名.メソッド名()という形で呼び出します。インスタンスは先頭が小文字、クラスは先頭が大文字というルールですから見分け方は簡単ですね。使い分けはベテランでも迷うところですから今は気にしなくて大丈夫です。
ここでは、なぜ、staticメソッドとインスタンスメソッドがあるのかを考察してみましょう。
str.length()は、str(その中身は"新人エンジニアのためのJava研修")という文字列自身の長さということですから、インスタンスメソッドがふさわしいのです。インスタンスが変われば文字列の長さも変わりますね。
オブジェクト指向には責務という考え方があります。文字列の文字数は誰(どのインスタンス)が知っているべきかというのが責務の例です。この場合は、個々の文字列が自分の文字数を知っているべきです。オブジェクト指向を一言で片付けると「自分のことは自分でしよう」という考え方といえるからです。
一方、String.format()はintを整形して(この例では)String(123,456,789)を得るメソッドです。この処理にはStringのインスタンスが作られる必然性がありません。インスタンスを生成することは、CPU時間とメモリ容量のムダです。最終的な結果として表示される“123,456,789”というStringのインスタンスが得られれば良いのです。そのため、String.format()はインスタンスではなくクラスに属すると考えて、staticメソッドであるべきなのです。intに対応するStringを生成するのは、個々のインスタンスには関係のない決まりきった内容の処理だからです。(下図参照)
ただし、staticメソッドは次回のテーマになっていますのでそこでも詳しく学ぶとしましょう。
7.エスケープシーケンス
例えばHello と World の間に改行を入れたい場合はどうしたらいいでしょうか?
以下Example12のように、改行を入れたいところには、“¥n”を入れる必要があります。
package chap07;
public class Example12 {
public static void main(String[] args) {
System.out.println("Hello\nWorld");
}
}
<実行結果>
Hello World |
特別な記号や出力方法を制御するためには「¥」記号を使います。この ¥nなどの文字を、「文字本来の意味から逃がした文字列」という意味で、エスケープシーケンス【escape sequence】と呼びます。
¥記号は日本語版のフォントを使用しているWindows環境の場合です。それ以外の環境ではバックスラッシュ記号(日本語キーボードだと「ろ」が刻印されている)を使います。
※もともと英語圏ではバックスラッシュ記号(ひらがなの”ろ”のキー)を使います。しかし、日本語圏では金額表示に円記号を使う必要があったため、まだ、使える文字数に制限があった時代、Windowsの前身のOSであるMS-DOS時代から、日本ではバックスラッシュ記号を円記号に置き換えて表示しているという経緯があります。バックスラッシュ記号を円記号に置き換えるのが一番影響が少ないと考えられたからのようです。実際には影響大ありですが。
当社の新人研修で使用する可能性のあるエスケープシーケンスを紹介します。
エスケープシーケンス | 意味 | |
1 | \n | 改行 |
2 | \t | 水平タブ |
3 | \' | ' |
4 | \" | " |
覚える必要はありませんが、上手く表示できない文字があった時に、ここに戻れるようにしておきましょう。
以下Example13はエスケープシーケンスを使ったサンプルコードです。
package chap07;
public class Example13 {
public static void main(String[] args) {
String str1 = "\"special\"price:";
System.out.print(str1);
}
}
<実行結果>
"special"price: |
8.nullは何もないことを表現する
ヒープ領域に確保された文字列のための領域を開放するにはどうしたらいいでしょうか?
参照型にnullを代入することで、その参照はインスタンスを参照しなくなります。nullとは「何もない」という意味の特別な意味を持った予約語です。ちなみに、Javaの場合nullは、数値の0、空文字””やスペース” ”とは全く別物として扱われますので気をつけましょう。(英語のイディオムでも【null and void:無効の】という表現がありました。voidもすぐあとの章で出てくるのであわせて覚えてください)
Javaにはガーベージコレクションという機構があり、どこからも参照されなくなったインスタンス(今回は文字列)は消去されメモリ領域が開放されます。
以下のExample14は参照をnullにして最終的にどこからも"ABC"という文字列のインスタンスを参照しないようにしています。
package chap07;
public class Example14 {
public static void main(String[] args) {
String str1 = new String("ABC");
String str2 = str1;
str1 = null;
System.out.println(str2);
str2 = null;
System.out.println(str2);
}
}
<実行結果>
ABC null |
"ABC"という文字列は1つですが、この文字列を指し示す参照は当初"str1"と"str2"の2つがあります。しかし、nullを代入することによってこれら2つの参照はどこも指さなくなりました。そして、どこからも参照されなくなった時に、この文字列はガーベージコレクションの対象になります。
JVMがガーベージコレクションを走らせるのですが、いつ、ガーベージコレクトをするのかをプログラマーの側で指示することはできません。あくまでプログラマーができるのはガーベージコレクトを依頼することだけです。
最後にnullまたは空文字をチェックする方法を確認しておきます。この知識は、例えば、JavaWebアプリケーションを作成する際に役に立つでしょう。なぜなら、データベースから取得した値がnullであるということは良くあることだからです。
以下のExample15は、ユーザーからの文字列を入力として受け取ります。その文字列がnullかどうかをチェックし、次にその文字列が空文字列(つまり、長さが0の文字列)かどうかをチェックします。もし文字列がnullまたは空であれば、その旨を出力します。それ以外の場合は、ユーザーが入力した文字列をそのまま出力します。
なお、Scanner
のnextLine
()メソッドはユーザーが何も入力せずにEnterを押してもnullを返すことはありません。しかし、Webアプリケーションを学ぶとnullチェックが必須になりますのでここで注意喚起しています。
package chap07;
import java.util.Scanner;
public class Example15 {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.print("文字列を入力してください: ");
String userInput = scanner.nextLine();
// nullチェック
if (userInput == null) {
System.out.println("入力がnullです。");
// 空文字チェック
} else if (userInput.isEmpty()) {
System.out.println("入力が空です。");
} else {
System.out.println("入力された文字列: " + userInput);
}
}
}
}
まとめができたら、アウトプットとして演習問題にチャレンジしましょう。
以上、今回は「文字列を扱ってユーザーにメッセージを伝える」方法について見てきました。
次回は、「staticメソッド定義して処理を再利用する」です。