いよいよクラスについて学習します。
ここからC言語にはなかった新しい用語が数多く出てきます。
最初からすべて覚えることは大変でしょうが、頑張って進めていきましょう。
クラスについて簡単に説明しておきましょう。
クラスとは、「データ」と「手続き」をひとまとまりにしたものです。
また、Javaは全てクラスの組み合わせで作られています。
データには、メンバ変数、手続きにはメソッドとコンストラクタがあります。
オブジェクト指向プログラミングにおいて、クラスが重要な働きするので、しっかり覚えていきましょう。
まずはクラスの定義(書き方)から説明します。
上の図のように書いてクラスを宣言します。
クラス名は、第3回のプログラム解説で、ファイル名と同じにすると説明しました。
これは、基本的にJavaでは、1つのファイルに1つのクラスを作るからです。
このコンテンツのプログラムはそれほど多くないため、1つのファイルに記載することにします。
それでは、クラスの中身について説明します。
クラスの { } の中にメンバ変数、コンストラクタ、メソッドの3つの内容を定義します。
メンバ変数、メソッド、コンストラクタは必要なだけいくつでも定義できます。
また、クラスの中にメンバ変数だけ、メソッドだけ、コンストラクタだけを定義することもできます。
さらに、不要なら定義しないということも可能です。
まずは、クラスのイメージをつかみましょう。
次にクラスを構成する要素を詳しく説明します。
クラスを構成しているのは、メンバ変数、コンストラクタ、メソッドです。
それでは、1つずつ説明していきます。
まずは、クラスを構成する要素の1つ、メンバ変数についてです。 メンバ変数とはクラスが保持できるデータです。
int型、double型もしくはオブジェクト型などさまざまな型を保持できます。
データ型 メンバ変数;
普通の変数の宣言方法と同じように行います。
次に、クラスの構成要素の2つ目を説明します。それはメソッドです。
メソッドとはクラスの機能を定義します。書き方は次の通りです。
戻り値 メソッド名(引数1, 引数2......){
このメソッドの処理内容を記述
}
メソッドには基本的にC言語の関数と類似しており、引数と戻り値があります。
「オブジェクト名.メソッド名」と記載することで使えます。
しかし、JavaのメソッドとC言語の関数は類似していても別物です。
今は詳しくは説明しません。少しずつ進めていくうちに理解できると思います。
最後はクラスを構成する要素のコンストラクタです。
実はコンストラクタもメソッドの1種なのですが、少し異なっている部分があります。
コンストラクタとはオブジェクトを生成したとき、自動的に呼び出される特殊なメソッドです。
基本的に、オブジェクトの初期化を行います。
クラス名(引数1, 引数2・・・){
オブジェクトの初期化処理
}
コンストラクタは、特殊なメソッドだと書きましたが、通常のメソッドと違って戻り値はありません。
コンストラクタはオブジェクトの初期化を行うものなので、戻り値は必要ありません。
コンストラクタの名前はクラス名と同じにします。
引数はなくても構いません。ここは間違えないようにして下さい。
また、利用手順は特にありません。インスタンス化した時に自動的に行われます。
基本的な作り方は説明しました。
では、さっそくクラスを作成してみましょう。
ここでは、人間クラスを例にして作成します。
どのような人間クラスを作成するかを決めます。
人間の情報として、名前、性別、年齢、出身地、性格等があります。
人間の動作として、話す、歩く、走る、寝ると様々なことがあります。
すべてを定義するのは大変なので、次の内容だけ定義します。
・名前
・年齢
・自己紹介をする
以上の3つにします。
次に、書き方を決めます。
クラスにはメンバ変数、コンストラクタ、メソッドの3つの内容を定義できると説明しました。
それぞれをどれにするのか決めなくてはなりません。
「名前」と「年齢」は人間のデータ(情報)です。よって、メンバ変数になります。
名前は文字列なのでString型、年齢は数値なのでint型にします。
「自己紹介をする」は人間の動作に当たります。これは、メソッドになります。
「自己紹介をする」の内容は「名前」と「年齢」を名乗ることにします。
ここまでで、3つの内容を以下のように割り振りました。
コンストラクタは使わないように思えますが、コンストラクタの働きは初期化です。
この例だと名前と年齢の初期化に使います。
クラスの定義ができたところで、プログラミングをしてみましょう
class Human { // メンバ変数 String name; int age; // コンストラクタ Human(String n, int a) { name = n; age = a; } // メソッド void introduce() { System.out.println("私の名前は" + name + "で年齢は" + age + "です。"); } }
上で決めた内容をそのままクラスにしました。
classを作る時は、class クラス名{...}でした。
人間クラスを作るわけですから、わかりやすいように、Humanとしました。
次に名前と年齢の値をいれるメンバ変数を作成します。
String name; //名前用
int age; //年齢用
名前と年齢の「自己紹介をする」のはメソッドでした。
戻り値は必要がないのでvoid、「自己紹介をする」メソッドは分かりやすいように、introduceにします。
void introduce(){...}
引数もいらないので、何も記述は行いません。
コンストラクタについてですが、前にも説明した用のオブジェクト生成した時に実行されます。
Human (String n, int a){...}
関数のように引数を受取り、実行されます。
これで人間クラスを定義することができました。
ここまででクラスの定義ができました。
しかし、このままでは使うことができません。
定義の後はオブジェクト生成する必要があります。
オブジェクトを生成することをインスタンス化といいます。
これを行うことにより定義したクラスを使うことができます。
インスタンス化する方法は次の通りです。
クラス名 インスタンス名; // 変数の宣言(この時点ではオブジェクトはできていない)
インスタンス名 = new コンストラクタ名; // オブジェクトができた
まとめて
クラス名 インスタンス名 = new コンストラクタ名;
と記述することもできます。
まず、作りたいクラスの変数を宣言します。
class Human {....}
そして、new演算子を使ってオブジェクトを生成します。
new演算子はオブジェクトを作るための演算子です。
Human human = new Human("taro",10);
このとき、作りたいクラスのコンストラクタを呼び出し、初期化も行っています。
上記の例では、オブジェクトHuman、「名前がtaro、年齢が10歳」が生成されます。
これまでに「クラス定義」と「オブジェクト生成」の基本的な説明は終了しました。
最後に、前に定義したHumanクラスをインスタンス化するプログラムを記述します。
そして、実際にクラスに定義したメソッドを実行してみましょう。
ファイル名「Java08_01.java」に以下のプログラムを記述して実行してください。
public class Java08_01 { public static void main(String args[]) { // オブジェクトの生成 Human human1 = new Human("太郎", 10); Human human2 = new Human("ホップ", 10); // 自己紹介をさせる human1.introduce(); human2.introduce(); } } // 人間クラス class Human { // メンバ変数 String name; int age; // コンストラクタ Human(String n, int a) { name = n; age = a; } // メソッドの定義 void introduce() { System.out.println("私の名前は" + name + "で年齢は" + age + "です。"); } }
1つ説明を忘れていましたが、どちらのクラスを先に書いても問題ありません。
C言語ではmainの下に書く場合に宣言のみ行いましたが、Javaでは必要ありません。
メイン文より、2つのオブジェクト(人間)を生成します。
インスタンス化は以下のように書きました。
クラス名 インスタンス名 = new コンストラクタ
Humanクラスを元にインスタンスhuman1を生成します。
Human human1 = new Human("太郎", 10);
インスタンス化と同時にコンストラクタを実行し、その時引数として値を渡しています。
Human human1 = new Human("太郎", 10);
String n に太郎を渡し、int aに10を渡しています。
そして、コンストラクタの処理が実行されます。
name = n; //n(太郎)がnameに代入されます。
age = a; //a(10)がageに代入されます。
では、"ホップ"の場合はどうなのかというと、
同じようにnameに"ホップ"を代入して初期化しています。
でも、これだと同じ変数に代入していない?と思いますが、大丈夫です。
それは次のページで説明することにします。
そして、メソッドを呼び出して自己紹介をさせています。
メソッドの実行方法を説明していませんでした。メソッドを使うには
インスタンス名.メソッド名
となりますので、このプログラムでは、
human1.introduce();
human2.introduce();
これで、クラスの基本的な使い方は終了です。
ここで、ひとつ補足を入れておきます。
第3回で、基本的にファイル名はクラス名にしておくと説明したのですが、
上記のような複数のクラス(Java08_01, Human)を持つファイルを作成するときは注意して欲しいことがあります。
複数のクラスを1つに記載している時のファイル名は、mainメソッドを持つクラス名と同じにする
上記の場合ですと、mainメソッドを持つのは、Java08_01なので、ファイル名が「Java08_01.java」となります。
例外として、クラスが複数あり、mainメソッドがない場合に限り、ファイル名を自由に付けて構いません。
読みやすさを重視するならば、クラス名と同じにしておけば探す時に楽になります。
Javaでは、上記のプログラムを2つのファイルに書くべきなのですが、
プログラムの長さが短いため、1つのファイルに記載してあります。
少し分かりにくかったかもしれませんが、続けて練習をしていけば覚えていけるはずです。
これで終わりといいたいですが、クラスはまだまだ続きます。
次はカプセル化についてやります。言葉くらいは聞いたことがあるかもしれません。
第7回でカプセル化の説明を簡単にしました。
そして、前のページでクラスの基本的な作成方法を説明しました。
では、いよいよJavaプログラムを用いたカプセル化を学びましょう。
カプセル化を行うために必要なものは、クラスとデータと手続きの3つです。
Javaでは、データをメンバ変数、手続きをメソッドと前回説明しました。
メンバ変数とメソッドをクラスが包み込み、カプセル化を実現させます。
ここで、前のページで作成した、メンバ変数、メソッドを使ったHumanクラスを見てみましょう。
class Human { // メンバ変数 String name; int age; // コンストラクタ Human(String n, int a) { name = n; age = a; } // メソッド void introduce() { System.out.println("私の名前は" + name + "で年齢は" + age + "です。"); } }
このクラスにはカプセル化が適応されておらず、実は全くのダメクラスです。
では、このクラスの問題点は何か、どうすればカプセル化が適応されるかを次の項目以降で学びましょう。
今のクラスのままではダメクラスのままなので、カプセル化を行わなくてはなりません。
その前に、カプセル化とはなんなのか学びましょう。
プログラムミスの減少(プログラムの影響範囲の削減)
データの隠蔽(誤ったデータアクセスの禁止)
カプセル化により、データなどを隠蔽することで、誤ったデータの書き換えを未然に防ぐことができます。
また、アクセスできる範囲を限定することによりプログラムミスの減少が期待できます。
以上のことを念頭に置き、Humanクラスの問題を考えていきましょう。
このクラスでは、メンバ変数に年齢をつけました。
int age;
年齢はコンストラクタで設定し、前回は「10歳の太郎」を生成しました。
一見、問題はなさそうですが、次のようにオブジェクトを生成(インスタンス化)することもできてしまいます。
Human human = new Human("太郎", -1);// 年齢を-1歳に設定
「-1歳の太郎」。・・・・・・・・そんな人はありえません。
人として、年齢がマイナスになるなんてことはありません。
なんだ、そんなことかと、ばからしい間違いに見えるかもしれませんが、
実際に何千行・何万行という長いソースを書くと、このような間違いが起こる可能性があります。
また、商品の在庫数がマイナス表記なんてありえません。
このような場合はデータの入力場所に修正を加えます。
今回の場合はコンストラクタで誤ったデータを未然に防ぎます。
これで、1つ目の問題が解決できます。次のように変更をしてください。
// コンストラクタ Human(String n, int a) { name = n; if (a < 0) { age = 0;// マイナスの年齢はすべて0に初期化する } else { age = a; } }
上のようにマイナスの年齢という誤った年齢が入力できないように制御します。
0よりも小さい値が来た時にifを使って0にしています。
それ以外の値のときはそのままageに代入を行います。
これで「-1歳の太郎」問題が解決しました。
今回はコンストラクタでしたが、もし、年齢を入力するメソッドがあったら、そのメソッド内で上のような処理を行います。
1つ解決できましたが、問題はまだあります。
それは、Humanクラスの年齢へのデータ入力の方法が、コンストラクタ以外にもあるということです。
つまり、次のようにメンバ変数を参照できます。
今まで説明していませんでしたが、メンバ変数への参照は以下のように行います。
インスタンス名.メンバ変数名
前のページで太郎とホップの2人を生成しましたが、
実はこのように各オブジェクトによって使い分けられているのです。
前のプログラムではこのように使うことになります。
human1.age = 20; //インスタンスhuman1.ageにアクセス
System.out.println(human1.age);
このようにすることでint ageにアクセスすることができます。
また、System.out.println()で値を表示することもできます。
メソッド名の部分がメンバ変数になっただけですので覚えやすいでしょう。
では、いったいこのままだと何がいけないのでしょうか。
Human human = new Human("太郎", 10) // 10歳の太郎を作成
ここまでは特に問題ありません。しかし・・・
human.age = -1; // 年齢を10歳から-1歳に変更
何の制御もなく、簡単に年齢が変更できてしまいます。
これでは、また、「-1歳の太郎」が出来上がってしまいます。
折角、コンストラクタで制御したのに、これではなんの意味もありません。
しかし、対処方法はあります。それがアクセス修飾子です。
次の項目では、このアクセス修飾子について説明します。
アクセス修飾子とは、アクセスを制御するものです。
これを使うことで、カプセル化を適応したHumanクラスが作成できます。
アクセス修飾子の種類は3つあります。
これをメンバ変数、コンストラクタ、メソッドに付け加えます。
アクセス修飾子 データ型 変数名;
アクセス修飾子 コンストラクタ名 (引数・・・) {....}
アクセス修飾子 戻り値 メソッド名(引数・・・) {....}
先頭にアクセス修飾子をつけましょう。
アクセス修飾子をつけた場合とつけない場合の機能を覚えてください。
では、アクセス修飾子にはどのような種類があるのか次の表で確認してください。
アクセス修飾子 | 制御の強さ | 機能 |
---|---|---|
public | 弱い | 全てのクラスからアクセスを許可 |
protected | 少し弱い | 同じパッケージ、継承先からのアクセスを許可 |
private | 強い | 同じのクラスからのアクセスを許可 |
なし | 少し強い | 同じパッケージからのアクセスを許可 |
アクセス修飾子の機能にパッケージ、継承と書いてありますが、
今は気にしないでください。これらは後の回で説明します。
ここで、覚えてもらいたいことは、アクセス制御の強さが
private > アクセス修飾子なし > protected > public
強い←・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・→弱い
の順であることです。
そして、アクセス修飾子「private」を用いれば、
他クラスからのアクセスを全て禁止することができるということです。
他からアクセス(参照)されたくないものに「private」をつけましょう。
これで、先ほど太郎が-1歳になってしまった処理を防ぐことができるようになります。
ところで、「protected」と「指定なし」は今は使いません。
まずは、「private」と「public」の使い分けを覚えてください。
「protected」、「指定なし」は第11回「パッケージ」で学習します。
ここで、話をHumanクラスに戻し、メンバ変数ageにprivateを適用させます。
以下のように今のプログラムに変更を加えてください。
class Human { // メンバ変数 private int age; // アクセス修飾子privateを適用 : : }
これでメンバ変数ageは他のクラスから参照されなくなります。
もし、他のクラスから次のような記述をしても、コンパイルエラーが起きます。
Human human = new Human("太郎", 10) // 10歳の太郎を作成
human.age = -1; // ここでコンパイルエラーが起きる
これで、「-1歳太郎」問題は無事に解決できました。
さっそくカプセル化を適応したHumanクラスを作成しましょう。
すべてのメンバ変数・コンストラクタ・メソッドにアクセス修飾子を付けます。
1.メンバ変数nameとageにアクセス修飾子「private」をつける
private String name;
private int age;
メンバ変数は勝手に変更できないように「private」をつけます。
これで勝手な代入を防ぐことができます。
-1歳太郎を生成することができなくなるわけです。
2.コンストラクタHumanにアクセス修飾子「public」をつける
public Human(String n, int a){...}
3.メソッドvoid introduce()にアクセス修飾子「public」をつける
public void introduce(){....}
コンストラクタ、メソッドの2つは、どのクラスから参照されても問題ないからです。
これで終わりと言いたいところですが、アクセス修飾子をつけたことで1つ問題が起きました。
それは、後からメンバ変数の変更ができないということです。
× human1.age = 11;
このプログラムを例にとると、10歳の太郎を生成したら、ずっと10歳のままです。
これも-1歳がありえないように、年齢(メンバ変数)が増えない人間はいません。
名前は変更できないとしても、太郎に誕生日がきたら、年齢(メンバ変数)が変わるのは当然です。
そのとき、年齢(メンバ変数)を変更させなくてはなりません。
"private int age"を変更する方法はメソッドにあります。
メソッド、すなわち手続きとは、カプセル化の出入り口にあたると説明しました。
次のメソッドをHumanクラスに追加します。
// 入口 public void setAge(int a) { if (a < 0) { age = 0; } else { age = a; } } // 出口 public int getAge() { return age; }
これでメンバ変数ageにアクセス制御をかけつつ、値の変更、値の取り出しができるようになります。
もちろん、値を変更する場合は誤った値を入力できないように制御します。
「private」にしたメンバ変数は同じクラス内からアクセスできないため、そのクラス内で変更、参照できるようにします。
メソッドは基本的にこのような使い方をします。
どうですか、C言語の関数とは別物でしょう。
ところで、コンストラクタもSetAgeメソッドも同じことをやっています。
そこで、コンストラクタを次のように書き直します。
public Human(String n, int a) { name = n; setAge(a); // 年齢の設定はメソッドに任せる }
変更点は以上です。
最後に変更し終えたHumanクラスのソースを示します。
カプセル化が適応されようやくまともなクラスを作成することができました。
class Human { // メンバ変数 private String name; private int age; // コンストラクタ public Human(String n, int a) { name = n; setAge(a); // 年齢の設定はメソッドに任せる } // メソッド public void introduce() { System.out.println("私の名前は" + name + "で年齢は" + age + "です。"); } // 入口 public void setAge(int a) { if (a < 0) { age = 0; } else { age = a; } } // 出口 public int getAge() { return age; } } public class Java08_02{ public static void main(String args[]){ Human human1 = new Human("太郎",-5); human1.introduce(); //human2.age = 10; privateがついているので直接代入できない //変わりに年齢を代入する専用のメソッドを使う human1.setAge(10); human1.introduce(); } }
以上で、問題を解決することができました。
カプセル化はオブジェクト指向プログラミングの重要な概念ですので、忘れないでください。
ここでは、オーバーロードを説明します。
オブジェクト指向の概念であるポリモーフィズムに関わってくる内容です。
知っていると、わかりやすいソースが書けるようになります。
まずはオーバーロードがどういうものなのかを簡単に説明をしておきます。
オーバーロードとは、全く同じ名前を持つメソッド(もしくはコンストラクタ)を複数定義すること
を言います。次のプログラムを参考にメソッドのオーバーロードについて説明をします。
public class Java08_03{ public static void main(String args[]){ overload test = new overload(); //オーバーロードメソッドを実行 test.func(); test.func(10); test.func(10,20); test.func("test"); } } class overload{ // 引数なし void func() { System.out.println("引数なしメソッド"); } // int型の引数1個 void func(int a) { System.out.println("int型の引数1個メソッド"); } // int型の引数2個 void func(int a, int b) { System.out.println("int型の引数2個メソッド"); } // String型の引数1個 void func(String a) { System.out.println("String型の引数1個メソッド"); } }
func()というメソッドを4つ作成しています。
しかし、Javaではこの4つとも定義することが可能です。それは、
引数の個数と引数の型が異なれば、同じ名前を複数定義できる
このように4つのメソッドを定義しても引数が異なるため、同じクラス内に定義することができます。
オーバーロードで定義したメソッドがどのように判断されるのかについては引数で決定しています。
同じ名前でも引数で区別することでどの引数のメソッドを実行するのか選択することができます。
オーバーロードは同じ名前のメソッド(コンストラクタ)で、引数の型と個数が異なることにより、複数使えると説明しました。
では、オーバーロードの利点とは何なのでしょうか。
文字を出力する方法(ここでは、printメソッドとする)で考えていきます。
○ print(10);
× print("ほぷしぃ");
○ printString("ほぷしぃ");
同じ名前をつけることができないのでメソッド名を変えなくてはいけません。
上の例では1つ目が数字を引数として画面に出力するメソッドです。
2つ目は文字列ですが、オーバーロードがないため使うことができません。
そこで、3つ目のメソッドで文字列を画面に出力するメソッドを作ります。
これが5個、10個のメソッドになれば、それだけ管理が難しく大変になります。
わざわざ引数の違いによって異なるメソッド名で呼び出す必要まであります。
引数がどのような型でも、引数をコンソール出力するという意味では同じなのにも関わらずです。
画面に出力する方法が1つだけのほうがずいぶん便利だと感じると思います。
print(10);
print("ほぷしぃ");
ない場合と比べて画面に出力するといったらprintメソッドを使うとなると大変楽です。
実は、これまでのソースにすでにオーバーロードは登場していました。
つまり、オーバーロードのありがたみをすでに体験していたのです。
画面に出力するときに"System.out.println();"という処理を使っていましたが、実はこれもオーバーロードがあるメソッドの1つなのです。
オーバーロードがあれば、同じメソッド名で異なる処理ができる
また、このように同じ名前で異なる処理を行うことをポリモーフィズムと言います。
オブジェクト指向の重要な概念ですのでしっかり覚えてください。
ポリモーフィズムときたら、同じ名前で異なる処理を行うです。
C言語でも静的変数はありました。staticを使った変数です。
Javaも同様にstaticを使います。JavaとC言語は似ているようで違います。
紛らわしいですが、しっかり覚えてください。
記述方法をここで説明しておきますが
アクセス修飾子 static データ型 変数名;
アクセス修飾子 static 戻り値 メソッド名(引数)
変数の場合には、アクセス修飾子とデータ型の間
メソッドの場合には、アクセス修飾子と戻り値の間
に書きます。それ以外はこれまでのメンバ変数とメソッドとの違いはありません。
staticがあるとないでの違いを見ていきましょう。
public class Java08_04{ public static void main(String args[]){ //staticがないクラスの場合 NoStaticClass nsc = new NoStaticClass(); nsc.a = 20; nsc.print(); //staticがあるクラスの場合 StaticClass.a = 10; StaticClass.print(); } } class StaticClass{ static int a; public static void print(){ System.out.println("printメソッド実行:" + a); } } class NoStaticClass{ int a; public void print(){ System.out.println("printメソッド実行:" + a); } }
今までのメンバ変数とメンバメソッドは次のように使いました。
Test test = new Test(); // インスタンス化
test.a = 10; // インスタンス名.変数名
test.method1(); // インスタンス名.メソッド名
必ず、インスタンス化を行ってから、変数の参照、メソッドの呼び出しを行っていました。
ところが、静的変数と静的メソッドは次のように使います。
Test.b = 10; // クラス名.変数名
Test.method2(); // クラス名.メソッド名
上の2つの手順を見比べれば何がないのかはすぐに分かります。
staticがある場合にはインスタンス化をしなくても使える。
そして、クラス名.~という形式になります。
これはstaticの効果の1つ目になります。
staticがある場合とない場合での違いがほかにもあります。それは、
そのクラスで唯一の変数、あるいはメソッドを作成するとき
に使用します。これを活用すると、メソッドでC言語風の関数が作成できます。
public class Java08_05 { // 静的メソッド private static void func() { System.out.println("C言語風の関数です"); } public static void main(String args[]) { func(); } }
メインメソッドで静的メソッドを呼び出しています。
インスタンス化が不要なので、呼び出し方法はまさに関数と言ってよいでしょう。
ただし、何度も説明しているようにメソッドはメソッドですので、C言語の関数と混合しないようにしてください。
また、これまでmainメソッドに「static」をつけていましたが、これも同じ意味です。
インスタンス化なしで呼び出せるようにするためです。
ただし、mainメソッドは通常のメソッドと異なり、形は常に固定で
public static void main(String args[])
にしてください。次にstaticの効果3つ目について説明します。
次のプログラムの実行結果を予想してみてください。
class Sample { private int a; private static int b; public Sample(int aa, int bb) { a = aa; b = bb; } public void put() { System.out.println("a = " + a + " b = " + b); } } public class Java08_06 { public static void main(String args[]) { Sample test1 = new Sample(1, 2); Sample test2 = new Sample(3, 4); test1.put(); test2.put(); } }
このようになりました。実行結果の予想は合っていましたか。
2つオブジェクトを生成しました。test1とtest2です。
そして、それぞれの値に別に値を入れたわけですから
test1.put()でa =1, b = 2
test2.put()で a = 3, b = 4
となりそうですが、実行結果を見てわかるようにそのようにはなっていません。
staticがついていると複数インスタンス化しても共通なものと認識する
インスタンス化を行ったことで全ての変数が4つあるように思えますが
実際には「static」はいくつ作っても1つしかないので事実上全部で3つとなります。
そして、「static」のある変数はこのプログラムでは後に代入されたほうが表示されるようになります。
つまりtest2が後に実行されるために、メンバ変数bは4となります。
では、なぜインスタンス化する必要がないのでしょうか。
静的変数、静的メソッドには別に言い方があります。
クラス変数(静的変数)、クラスメソッド(静的メソッド)という呼び方です。
実は、この「static」がつけられた変数とメソッドはクラスが持っています。
そのために、クラス変数、クラスメソッドという呼び方がされるのです。
このため、「static」で宣言した変数とメソッドはクラスに1つだけ存在することになり
インスタンス化を行わなくても利用することが可能になるわけです。
「this」の説明をします。
これを用いることで、プログラムを書く上でソースの簡単化が期待できます。
いくつか使い道があるので、活用できるようにしてください。
「this」とは自身のオブジェクトを示します。
例えば、オブジェクトAの中でthisを用いた場合、「this」は「A」を示します。
つまり、オブジェクト自身で自分を指すということです。
説明を聞いただけでは、少しわかりづらいと思います。
最初に私も説明を聞いた時は意味が分かりませんでした。
これに関しては、実際の使い方を見て学んでください。
以降の項目で「this」の使い方を説明します。
そのとき上記のことを何となく思い浮かべつつ、使い方を覚えてください。
まず、メンバ変数とローカル変数について説明します。
メンバ変数はクラス内で保持される変数です。
ローカル変数とは、一時的な変数です。
ローカル変数はメソッドなどの中括弧{}の中のみで使用されます。
処理が中括弧から外に出ると変数は消えてなくなります。これがローカル変数です。
これらはC言語にもあるので、理解しやすいと思います。
制御文のfor文で、for(int i = 0; i < 5; i++)のような書き方をしました。
これも同じように{}内のみで扱われる変数なのでローカル変数です。
この{}を出た先で表示しようとするとエラーになりました。
ちなみに、引数で渡された変数もローカル変数です。
重要なことは、メンバ変数とローカル変数に同じ名前を使用できるということです。
それでは、次のソースを実行して確認してみましょう。
class ThisTest { // メンバ変数「value」の宣言 private int value = 20; // メソッド public void func() { // ローカル変数「value」の宣言 int value= 10; System.out.println("value="+value); } } public class Java08_07{ public static void main(String args[]){ ThisTest test1 = new ThisTest(); test1.func(); } }
例に挙げたソースでは「value」という共通の名前で宣言しました。
classの下のprivate int valueと、func()メソッド内の「value」は同じ名前です。
この場合、func()メソッド内では「value」をローカル変数として自動的に処理されます。
また、メンバ変数よりもローカル変数の優先順位が高いので、
メンバ変数のvalueとfunc()メソッドのローカルvalueをvalueで表示したときには、
ローカル変数のvalueが表示されます。
C言語でローカル変数とグローバル変数のことを思い出してください。
同じように有効範囲(スコープ)がありました。あれと同じことです。
では、メンバ変数のvalueをfuncメソッド内で使いたい場合はどうすればよいでしょう?
先ほどのプログラムを次のように変更します。
class ThisTest { // メンバ変数「value」の宣言 private int value = 20; // メソッド public void func() { // ローカル変数「value」の宣言 int value= 10; System.out.println("ローカル変数のvalue="+value); System.out.println("メンバ変数のvalue="+this.value); } } public class Java08_07{ public static void main(String args[]){ ThisTest test1 = new ThisTest(); test1.func(); } }
ローカル変数を使いたいときは、そのまま「value」
メンバ変数を使いたいときは、「this.value」とします。
「this」はそのクラス自身を示します。
「this.value」は「そのクラスのメンバ変数value」という意味になります。
これで、メンバ変数とローカル変数を識別できました。
ところで、「メンバ変数とローカル変数に違う名前をつければよいのでは?」と思う方がいると思います。
確かにそれでも識別はできますが、ソースによっては、同じ名前の方が見やすい場合があります。
最初で少し触れましたが、コンストラクタもオーバーロードが可能です。
コンストラクタは初期化を行うものですが、初期化の方法は場合によって異なります。
そのため、複数のコンストラクタを用意する必要があります。
次のソースを見てください。
class ConstructorTest { private int value1; private int value2; // コンストラクタ1(自動で値を初期化する場合) public ConstructorTest() { value1 = 10; value2 = 20; } // コンストラクタ2(value1のみ任意の値で初期化する場合) public ConstructorTest(int value1) { this.value1 = value1; value2 = 20; } // コンストラクタ3(value1、value2共に任意の値で初期化する場合) public ConstructorTest(int value1, int value2) { this.value1 = value1; this.value2 = value2; } public void print() { System.out.println(this.value1); System.out.println(this.value2); } } public class Java08_08{ public static void main(String args[]){ ConstructorTest test1 = new ConstructorTest(); ConstructorTest test2 = new ConstructorTest(50); ConstructorTest test3 = new ConstructorTest(100, 200); test1.print(); test2.print(); test3.print(); } }
コンストラクタによって、メンバ変数value1とvalue2を初期化する方法を決めています。
初期化方法は複数考えられるため、オーバーロードにより、複数個のコンストラクタを定義しています。
特に指定しなければ、value1は10、value2は20で初期化されます。
注目すべき点はコンストラクタごとに値を代入するという共通の処理を書いていることです。
同じ処理なのに、すべてに記述することは面倒です。そして、書き間違えによるミスにもなります。
そこで、1つのコンストラクタに共通の処理を書き、他のコンストラクタはそのコンストラクタを呼び出すという方法を行います。
コンストラクタを呼び出すには、
this(); // ()内は呼び出したいコンストラクタと同等の引数を与える
と書きます。決してコンストラクタ名();ではないことに注意してください。
では、上記のテストクラスを書きなおします。
class ConstructorTest { private int value1; private int value2; // コンストラクタ1(自動で値を初期化する場合) public ConstructorTest() { this(10, 20); // コンストラクタ3を呼び出す } // コンストラクタ2(value1のみ任意の値で初期化する場合) public ConstructorTest(int value1) { this(value1, 20); // コンストラクタ3を呼び出す } // コンストラクタ3value1、value2共に任意の値で初期化する場合 public ConstructorTest(int value1, int value2) { this.value1 = value1; this.value2 = value2; } } public class Java08_08{ public static void main(String args[]){ ConstructorTest test1 = new ConstructorTest(); ConstructorTest test2 = new ConstructorTest(50); ConstructorTest test3 = new ConstructorTest(100, 200); test1.print(); test2.print(); test3.print(); } }
コンストラクタ3に共通処理を書き、コンストラクタ1と2がコンストラクタ3を呼び出しています。
これで、ソースがすっきりしたと思います。
もし、コンストラクタが4個、5個とたくさんあったら、それだけこの書き方のありがたみがわかると思います。
なお、コンストラクタはインスタンス化したとき初めに処理されるという決まりがありました。
そのため、コンストラクタの呼び出しはコンストラクタの一番初めに行う必要があります。
つまり、上記のプログラムのコンストラクタ1を次のように書いてはいけません。
// コンストラクタ1(自動で値を初期化する場合) public Test() { System.out.println("エラー"); // コンストラクタの呼び出しより先に処理する this(10, 20); // コンストラクタ3を呼び出す }
これまで「オブジェクトはクラスより生成される」という説明しかしていませんでした。
オブジェクトの生成とはどういうことか、int型などと何が違うのかを説明します。
基本データ型とは以下のようなもののことをいいます。
char型、boolean型、byte型、short型、int型、long型、float型、double型です。
それに対し、new演算子を使って生成するものをオブジェクトと言います。
オブジェクトは基本データ型と異なる点があります。その1つが参照です。
では、参照とはどういうものか、以下の図を見てください。
図のようにオブジェクトは参照型です。C言語のポインタに近いものがあります。
Javaには、ポインタがないと最初に説明を行いましたが実はポインタに近い要素は使っているのです。
しかし、意識的に使う必要はないので安心してください。
これにより、JavaはC言語よりバグが起きにくくなっています。
Javaでは、常にオブジェクト=参照型という関係があることを覚えておいてください。
「値渡しと参照渡し」という言葉を聞いたことがあるはずです。それは、C言語にありました。
関数に値を渡す場合、関数内で値を変更しても呼び出し元に反映はありませんが、
ポインタを渡す場合、呼び出し元に反映されるというものです。
JavaもC言語と同じように、値渡し、参照渡しをすることができます。
先ほど説明しましたが、オブジェクトは参照型です。
つまり、メソッドやコンストラクタにオブジェクトを渡した場合に、
呼び出し先でオブジェクトを変更した結果が、呼び出し元に変更されます。
次に値渡しと参照渡しの例を示します。
class Value { private int value; public Value(int value) { setValue(value); } public int getValue() { return value; } public void setValue(int value) { this.value = value; } } public class Java08_09 { public static void main(String args[]) { int value1 = 10; Value value2 = new Value(10); put(value1, value2); // 値を表示(1回目) change(value1, value2); // 値渡しと参照渡し put(value1, value2); // 値を表示(2回目) } private static void change(int value1, Value value2) { value1 = 20; value2.setValue(20); } private static void put(int value1, Value value2) { System.out.print("基本データ型の値:" + value1); System.out.println(" オブジェクトの値:" + value2.getValue()); } }
このように表示されました。少しソースが長いですが、やっていることは単純です。
メソッドに基本データ型とオブジェクトを渡します。
メソッド内で値を変更した結果、呼び出し元側でどのように反映されるかどうかです。
結果は、予想通りオブジェクトの値のみが変更されます。
null(ヌル)とは、オブジェクトを参照していないことを意味します。
C言語にも「NULL」がありました。
C言語では、ポインタがどこも指していないことを意味し、Javaの「null」と似た意味を持ちます。
Javaでは、次のようにオブジェクトのインスタンス化を行いました。
クラス名 インスタンス名;
インスタンス名 = new コンストラクタ;
このインスタンス名がオブジェクトを参照する変数です。
1行目では、変数を宣言しているだけで、まだ何も参照していません。
つまり、nullの状態(インスタンス名=null)であると言えます。
続いて2行目でオブジェクトを生成し、ようやくインスタンス名がオブジェクトを参照するようになります。
nullにはいくつかの役割があります。その1つはオブジェクトの削除です。
C言語ではメモリを確保した場合、明示的に削除する必要がありました。
Javaのオブジェクトも同様にメモリを確保したものですが、
C言語と異なり、自動的に不要になったオブジェクトを削除してくれます。
この仕組みをガーベジコレクションと言います。もし、プログラマが意図的に削除したいオブジェクトがあれば、
オブジェクトを参照する変数にnullを代入することで、オブジェクトが削除される候補に挙がります。
ただし、いつ削除するかはJVMによって決まります。もう1つはオブジェクトがない場合です。
例えば、次のようなメソッドがあるとします。
public String method(int value) { if (value > 0) return "正の整数"; else return null; // nullを返す }
ちょっと強引な処理ですが、引数の値でオブジェクトを返すかどうかを決めています。
引数が整数なら文字列を返し、それ以外なら何も返しません。
ただし、戻り値をStringと指定している以上何かを返さなくてはいけません。
その場合、何も返すものがないという意味でnullを使うことがあります。
int型などの基本データ型が等しい値かどうかを判定する場合、次のように記述しました。
a == b
C言語でもおなじみの等価演算子です。これをオブジェクトに使うと次のようになります。
public class Java08_10{ public static void main(String args[]){ String str1 = new String("ほぷしぃ"); String str2 = new String("ほぷしぃ"); System.out.println(str1 == str2); } }
同等の文字列を2つ作って「==」で判定しています。しかし、結果は「false」と出力されてしまいます。
なぜでしょうか?理由は
オブジェクトそのものを比較しているのではなく、オブジェクトの参照を比較している
からです。C言語風に言うとアドレスを比較しています。
同じ文字列のオブジェクトを作っても、メモリ上に別々に作られてしまうため、
参照先が異なり、「false」が出力されたのです。
これを解決する方法がequalsです。これは、オブジェクトそのものの比較を行ってくれます。
public class Java08_10{ public static void main(String args[]){ String str1 = new String("ほぷしぃ"); String str2 = new String("ほぷしぃ"); System.out.println(str1.equals(str2)); System.out.println(str1 == str2); } }
equalsの戻り値は「true」か「false」です。上記のソースは「true」になります。
参照先が異なっても、オブジェクトそのものが同じだと判定されたためです。
1つ注意点があります。これまで文字列は「String str = "文字列"」と書きました。
ところが、今回のソースでは「new演算子」を使っています。
Stringはクラスなので、このような書き方があります。
今まで書いてきた方法が特殊でした。そして、もう1つ特殊なことがあります。
2種類の宣言方法があっても、生成するものが同じように見えますが、違いがあります。
前者は同じオブジェクトのコピーが渡され、後者は新しくオブジェクトを作成しています。
せっかくですから、プログラムで確認してみましょう。
public class Java08_11{ public static void main(String args[]){ String str1 = "ほぷしぃ"; String str2 = "ほぷしぃ"; System.out.println(str1 == str2); } }
このプログラムでは「true」と出力されます。少しわかりにくかったかもしれません。
しかし、このようなケースはStringクラスぐらいのものです。よって、
オブジェクトの参照先を比較したいなら「==」
オブジェクトそのものを比較したい場合は「equals」
を使うようにしてください。
今まで使ったクラスとオブジェクトについて少しだけ詳しく説明します。
今まで曖昧だったところがよりわかるようになるでしょう。
今までよく出てきた記述がありました。「Sytstem.out.println()」です。
この構文の意味について説明します。
「.」が2つも付いて分かりにくいですが、これまでの知識があれば理解できます。
少しずつ見ていきましょう。まず、「System」とはSystemクラスのことです。
そして「System.out」とはSytemクラスが保持するoutという静的変数のことです。
out変数の実体はOutputStreamクラスであり、出力に関する処理をサポートします。
そして、「out.println()」でOutputStreamクラスのprintln()メソッドを使ってコンソール出力を行います。
まとめると、「System.out.pritnln()」とは「Systemクラスの静的変数outのprintln()メソッドを使って出力する」という意味です。
これまでJavaの配列をC言語の配列と同じように使ってきたと思います。
しかし、それは基本データ型(int型など)しか扱わなかったためです。
では、オブジェクトの配列を作成するにはどのようにすればよいのでしょうか?
次のソースを見てください。初めに書いておきますが、次のソースはよくある誤った書き方です。
class Obj { public int value; } public class Java08_12 { public static void main(String args[]) { Obj obj[] = new Obj[3]; obj[0].value = 10; // ここでエラーが起きる System.out.println("obj[0].value = " +obj[0].value); } }
Objクラスを定義し、このクラスの配列を宣言します。
そして、要素[0]のvalueに10を代入し、その値を出力しています。
一見、問題はなさそうですが、実行すると、このようなエラーが表示されてしまいます。
その理由は、これまでの学習を振り返ればわかると思います。
配列を宣言するとは「領域を確保」するだけで、オブジェクトを作っていません。
空っぽの配列の要素を使えるわけがなく、エラーが発生したということです。
ちなみに、エラーメッセージの「java.lang.NullPointerException」とは、「nullです」という意味です。
このエラーメッセージについては第13回「例外処理」で行います。
では、先ほどのJava08_12クラスを以下のように変更してください。
class Obj { public int value; } public class Java08_12 { public static void main(String args[]) { Obj obj[] = new Obj[3]; obj[0] = new Obj();// 追加文(オブジェクトを作る) obj[0].value = 10; System.out.println("obj[0].value = " +obj[0].value); } }
オブジェクトを生成し、配列の要素[0]に参照を渡しています。
これで正しく実行するようになります。
以下の指示に従ってプログラムを作成しなさい。
なお、ファイル名はEx08_01.javaとする。
まずは、次のような三角形の面積を求めるTriangleクラスを作成しなさい。
・メンバ変数 :「底辺」と「高さ」を宣言
・コンストラクタ :底辺と高さの値を引数から受け取り、メンバ変数を初期化する
・メソッド :メンバ変数より三角形の面積を計算し、結果を返す
・Triangleクラスが作成出来たら、このクラスをインスタンス化する。
・底辺3.5、高さ5.2の三角形の面積を求めて、次のように計算結果を表示するプログラムを作りなさい。
簡単なクラスが書けるかを確認する問題です。以下より、詳しく解説します。
Triangleクラスを宣言します。
メンバ変数「底辺」と「高さ」を宣言します。
ここで注意することは、長さが小数点になることを考慮して、double型を使います。
(float型でもかまいません)
コンストラクタを宣言します。thisキーワードを使って、メンバ変数と引数を同一の名前で扱います。
その方が、同じデータを表していることがわかりやすくなります。
三角形の面積を計算して、その結果を返すメソッドです。
当然、戻り値は小数点になることを考慮してdouble型にします。
Triangleクラスを使って、底辺3.5、高さ5.2の三角形オブジェクトを生成します。
そして、areaメソッドを使って、面積を計算し、結果を出力します。
以下の指示に従って問題を解きなさい。
以下のプログラムの実行結果を答えなさい。
「static」の理解度を確認する問題です。
もし、value1とvalue2の値がすべて「1」になると思ったら、staticを理解していません。
ここで、もう一度、staticについて説明します。
まず、value1はstaticの付いていない変数です。
オブジェクトごとに生成される変数です。
それに対し、value2にはstaticが付いた変数です。
これは、クラスに1つのみ存在し、いくつオブジェクトを生成しても、1つしかありません。
つまり、1つめのオブジェクトで、vlaue2を「1」プラスします。
そして、2つめのオブジェクトで、もう一度、value2を「1」プラスします。
結果、value2は1つしか存在しないため、value2の「2」になったということです。
以下のプログラムは実行時にエラーが起こります。 エラーが起きた行番号と、その理由を答えなさい。
オブジェクトの配列についての理解度を確認する問題です。
よくあるエラーの1つなので、しっかり覚えてください。
まず、問題のプログラムで、Printクラスの配列を作成しました。
しかし、この時点ではPrintクラスのオブジェクトは生成されておらず、Printクラスを格納するための「メモリ領域」を確保しただけです。
つまり、配列の中身は「null」です。
そのため、問題のプログラムの15行目で、nullを参照したために、エラーが起きます。
このエラーを回避するには、配列の宣言後、次のように記述します。
Print ps[] = new Print[3];
ps[0] = new Print();
ps[1] = new Print();
ps[2] = new Print();
これで、配列の各要素の参照先がPrintオブジェクトになり、エラーがなくなります。
Copyright (C) 2011 ほぷしぃ. All Rights Reserved.