Javaの「クラス」の理解を深めるいまから始めるJava(11)

» 2003年11月26日 00時00分 公開
[平井玄@IT]

 String型はJavaの言語仕様そのものには含まれませんが、Javaのプログラミングに欠かせない最も基本的なクラスの1つです。これまでの連載でも、文字列を格納するための変数の型として何度となく登場しました。

 前回「Javaのクラスをグループ化するパッケージ」も少しふれましたが、String型は文字列を扱うためのクラスとして、そのソースコードがJavaに付属しています。今回はString型クラスのソースを読みながら、これまで紹介していなかったJavaの言語仕様についてまとめて説明します。

 その前にまず、String型について確認しておきましょう。String型の役割は、リードオンリーの文字列を扱うこと(後で書き換えたい場合はStringBuffer型を使う)です。Javaのちょっと面白い特徴として、単なる文字列もString型のオブジェクトとして扱われることがあります。したがって、C/C++を経験したプログラマには違和感のある以下のようなコードを書くことができます。

文字列の長さを表示する
public class ShowStringLength {
  public static void main( String args[] ) {
    System.out.println( "文字列".length() );
  }
}

C:\>javac ShowStringLength.java
C:\>java ShowStringLength
3

 このコードでは、「"文字列"」の部分がString型のオブジェクトとして扱われるため、変数ではないにも関わらずString型のメソッドlength()が使えるのです。

 ここで、String型のソースがどのようになっているのか、想像してみましょう。Javaの文字列がString型のオブジェクトとして扱われるといっても、コンピュータの中ではあるメモリ領域に格納されるはずです。メモリを直接扱うのはプリミティブ型ですから、String型のソースには文字列を実際に格納するための変数が定義されているはずです。もちろん、メモリをどう扱うのかをプログラマがあまり意識しなくてもよいのがJavaの長所ですので、こうした変数はクラス内部の作業用としてprivateに指定されているはずです。また、String型のメンバであるlength()や第8回のHTMLパーサを実現するために使ったcharAt()などのメソッドは、privateに指定されたプリミティブ型の変数に直接アクセスしているはずです。では、String型のソースがどうなっているのか、実際に見ていきましょう。

配列

 前回も少しだけふれましたが、String型で実際に文字列を格納しているのはvalueという名前の変数です。

String.javaの93行目〜
/** The value is used for character storage. */
private char value[];

 予想通りvalue はprivateに指定されていますので、プログラマが直接には操作できません。ただ、最後に付いている「[ ]」という記号は何を意味するのでしょう。

 「[ ]」は配列を表す記号です。配列とは、ある型の変数がメモリ上で連続していることです。配列型の変数valueは、連続したメモリの先頭アドレスを参照するために使います。ただし、配列型はクラス型と同様に参照型の変数ですので、配列型の変数そのものはメモリの先頭アドレスを格納するために使うものですので、配列は別に生成しなければなりません。

char value[] = new char[3];

 この場合、char型の変数を3個格納するのに必要なメモリ領域が確保され、その先頭アドレスが変数valueに格納されます。配列の要素にアクセスするには以下のように添え字を使います。

char value[] = new char[3];
value[0] = '文';
value[1] = '字';
value[2] = '列';

 1行目では、char型3個分のメモリ領域が確保され、その先頭アドレスがvalueに格納されます。2行目では、確保した1番目の部分に「文」という文字を書き込んでいます。配列の添え字は1番目が0、2番目は1というように0から始まることに注意してください。

 0から始まることに違和感を覚える方は、次のように考えるとよいでしょう。まず、char型のサイズは2バイトですので、「new char[3]」という文で2バイト×3個=6バイトのメモリ領域が確保される*ことになります。同じ参照型であるクラスの場合、クラスのメンバ変数は同じ型とは限りません。しかし配列は同じ型の変数が連続したものですので、先頭アドレスと何番目なのかが分かれば、目的の要素にアクセスできます。したがってn番目の要素のアドレスは、配列の先頭アドレス+(n×配列の型のサイズ)です。この例の場合、「文」というデータが格納されているのは「配列の先頭アドレス+(0×2バイト)」、つまり先頭アドレス+0バイトです。同様に「字」は先頭アドレス+2バイト、「列」は先頭アドレス+4バイトです。つまり、クラスのメンバ変数と同様、配列の要素も先頭アドレスからの相対値でアクセスしているわけです。

 冒頭でふれたchatAt()も、配列に添え字を使ってアクセスして値を取り出しています。

String.javaの444行目〜
public char charAt(int index) {
  if ((index < 0) || (index >= count)) {
    throw new StringIndexOutOfBoundsException(index);
  }
  return value[index + offset];
}

注:実際に確保されるメモリ領域のサイズはJavaの仮想マシンを実行するCPUによって異なる。

 なお、lengthという配列型のメンバ変数を使うと、配列型のオブジェクトが保持している要素の数が分かります。

文字列の長さを表示する
public class ShowStringLength2 {
  public static void main( String args[] ) {
    char value[] = new char[3];
    value[0] = ‘文’;
    value[1] = ‘字’;
    value[2] = ‘列’;
    System.out.println(value.length);
  }
}

C:\>javac ShowStringLength2.java
C:\>java ShowStringLength2
3

 ちなみに、配列型の変数の宣言方法は2つあります。1つはすでに説明したように、

型名 変数名[];(例:char value[])

というスタイルです。もう1つは

型名[] 変数名;(例:char[] value;)


というスタイルです。後者の方がJavaの正式の書き方なのですが、C/C++プログラマには前者の方がなじみがあるでしょう。String.javaでは後者のJavaの正式の記法ではなく、C/C++流の書き方が使われています。しかし、以下のような文もあります。

String.javaの545行目
char[] val = value;   /* avoid getfield opcode */

 本来、少なくとも同じソースファイルの中では書き方を統一すべきなのですが、String.javaではなぜか両方の書き方が混在しています。

thisとsuper

 String型にはいくつかのコンストラクタが用意されています。配列がどのように生成されているのか、そのうちの1つを見てみましょう。

String.javaの142行目〜
public String(String original) {
  this.count = original.count;
  if (original.value.length > this.count) {
    this.value = new char[this.count];
    System.arraycopy(original.value, original.offset, this.value, 0, this.count);
  } else {
   this.value = original.value;
  }
}

 このコンストラクタは、String型のオブジェクトに新しい文字列を代入するときに呼び出されます。すでに文字列が割り当てられている場合は、新しい文字列に置き換えられます。

 最初のif文は新旧の文字列の長さを比較するものです。新旧の文字列の長さが違う場合は、配列を新たに生成しています。ただ、「this」という見慣れないキーワードがあります。

this.count = original.count;
if (original.value.length > this.count) {
   this.value = new char[this.count];

 thisは、オブジェクト自身を表す特殊な変数です。例えば、String型のオブジェクトを以下のように生成したとしましょう。

String s = "文字列";

 このとき、String型オブジェクト内のthisは、変数sと同じく「"文字列"」というString型オブジェクトの先頭アドレスを表しています。つまりthisという変数は、クラスがオブジェクトとして生成されたときに、オブジェクト内部から自分自身の先頭アドレスにアクセスするために使うのです。したがって、「original.value.length > this.count」は、新しい文字列を格納している配列の長さと、現在の文字列の長さを比較している条件式です。

 新旧の文字列の長さが異なる場合は、新しい文字列の長さに合わせてchar型の配列を生成しています。一方、新旧の文字列の長さが同じ場合、新しい文字列を格納しているメモリの先頭アドレスを代入することで、無駄を省いています。

} else {
  this.value = original.value;
}

 valueはprivateに指定されていますが、String型同士なのでアクセスできます。

 なお、thisの仲間にsuperという変数があります。thisが自分自身を表すのに対して、superは自身のスーパークラスを表します。

 thisとsuperは、サブクラスからスーパークラスの同名メソッドを呼び出すときにも使えます。

thisとsuper
class ClassA {
  public void WhoAreYou() {
    System.out.println("I am A.");
  }
}

class ClassB extends ClassA {
  public void WhoAreYou() {
    super.WhoAreYou();
    System.out.println("I am B.");
  }
}

public class SampleForThisAndSuper {
  public static void main( String args[] ) {
    ClassB b =new ClassB();
    b.WhoAreYou();
  }
}

C:\>javac SampleForThisAndSuper.java
C:\>java SampleForThisAndSuper
I am A.
I am B.

 上記の例では、ClassAで定義されたメソッドWhoAreYou()と同名のメソッドを、ClassAを継承したClassBで再定義(=オーバーライド)しています。ClassBのWhoAreYou()内でClassAのWhoAreYou()を呼び出すために、変数superを使っています。

finalとstatic

 クラスを使うことのメリットとして、あるクラスを継承して別のクラスを作れることがあります。元のクラスと異なる部分だけを定義すればよく、同じ機能をゼロから作るよりも作業の手間が省けるとされています。しかし、プログラマはString型を継承して、新たなクラスを作ることはできません。なぜなら、String型にはfinalという修飾子が付いているからです。

String.javaの90行目〜

public final class String
{

 クラスの宣言にfinalを付けると、そのクラスは他のクラスのスーパークラスになれなくなります。つまり、継承できなくなるのです。なお、finalは変数の宣言に付けることもできます。

final double pi = 3.1415926;

 クラスにfinalを付けると継承して機能を変更できなくなりますが、変数の宣言にfinalを付けると後から値を変更できなくなります。したがってfinal付きの変数は宣言時に値を代入して初期化しておきます。final付きの変数は一種の定数になるのです。

 finalはクラスか変数に付く修飾子ですが、変数とメソッドに付くstaticという修飾子もあります。

String.javaの2234行目〜
public static String valueOf(boolean b) {
  return b ? "true" : "false";
}

 staticとして宣言されたメソッドは、インスタンスを生成しなくても利用できます。クラスはそれ自体がオブジェクトでもあるので、staticの付いたメソッドはクラスそのものに所属するメソッドです。もちろん、それぞれのインスタンスからも利用できます。

staticメソッド
public class SampleForStaticMethod {
  public static void main( String args[] ) {
    System.out.println(String. valueOf(true));
  }
}

C:\>javac SampleForStaticMethod.java
C:\>java SampleForStaticMethod
true

 上記の例では、String型のオブジェクトを生成せずにvalueOf()というString型のメソッドを使っています。valueOf()がstaticメソッドなので、オブジェクトを生成しなくても使えるのです。

 一方、メンバ変数にstaticを付けると、その変数はクラスに所属する変数になり、やはりオブジェクトを生成しなくても使えます。別の言い方をすれば、あるクラスのインスタンスで共通して使える変数が実現できるのです。

static変数
class ClassWithStaticVariable {
  public static int count;
}

public class SampleForStaticVariable {
  public static void main( String args[] ) {
    ClassWithStaticVariable.count = 1;
    
    ClassWithStaticVariable a = new ClassWithStaticVariable();
    a.count = a.count + 10;
    
    ClassWithStaticVariable b = new ClassWithStaticVariable();
    b.count = b.count + 100;

    System.out.println(ClassWithStaticVariable.count);
  }
}

C:\>javac SampleForStaticVariable.java

C:\>java SampleForStaticVariable
111

 上記の例では、ClassWithStaticVariable内のstatic変数countを別々のやり方で加算していますが、すべて同じ変数にアクセスしているので計算結果は111になります。

 String型のソースは全体で2000行以上もある膨大なものですが、プログラムの内容はそれほど複雑ではありません。これまで説明した変数の型や制御構造についての知識と、今回紹介した配列やthisとsuper、finalとstaticについての知識があれば、何をしているのかおおよそは読み取れるはずです。しかし、いくつかの文はまだ説明していないJavaの機能を使っています。次回は「例外」という機能について紹介します。


Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。