忙しい人の為のJava講座
この記事では、Javaに関する基礎文法を図解やシンプルなコード例等を用いて解説しています。
そして、技術書より圧倒的に安い価格帯で丁度良いボリュームの記事をお届けします。
対象読者は、これからJavaを勉強しようとしている人やプログラミングをやってみたいという人が対象です。しかしながらレベル的には「一度でもプログラミング言語を多少勉強したことがある」位のレベルですが、まったくの初学者にもわかるような解説を心がけていますので是非興味のある人も目を通していただければと思います。
以下、目次です。
・Javaを学ぶ上で心得ておくこと
・Javaとは
・Javaで単純なプログラムを作成する
・Javaにおけるメソッドとは
・Javaで使える基本型
・Javaの基本的な演算子
・オブジェクト指向についてもう少し
・Javaでインスタンスを生成してみる
・インスタンスの操作とインスタンス変数
・Javaの配列について学ぶ
・配列の使用方法
・クラスの「情報隠蔽」
・基本型と参照型
・基本型と参照型の違いについて押さえる
・実引数と仮引数
・文字列はクラスのひとつ!?
・等号演算子「==」の注意点
・カプセル化について
・メソッドのオーバーロード
・メソッドの多重定義による注意点
・コンストラクタについて学ぶ
・コンストラクタのオーバーロード
・デフォルト・コンストラクタ
・クラス変数
・定数宣言
・クラスメソッドについて学ぶ
・クラスの継承
・サブクラスのコンストラクタ
・superメソッドの罠
・protected修飾子について
・継承と型
・メソッドのオーバーライド
・抽象クラスと抽象メソッド
・superによる親クラスメソッドの呼び出し
・インスタンスの型変換
・定数以外のfinal
・Javaにおける継承の落とし穴
・Javaのインターフェースとは
・インターフェースを実装する
・インターフェースのデフォルトメソッド
・インターフェースの型
隙間時間にも勉強できるように細かく目次を構成しました。一連の流れになっていますので、上から順に読んでいく形になるかと思います。
この記事を読破できればプログラミング初心者~中級者あたりの基礎文法の知識がつきます。オブジェクト指向などの概念も学べますので興味のある方は是非ご購入下さい。
現在は全編で2万3000文字程度あります。かなりボリュームがあり、読み応えバッチリなのでやる気のある方にお勧めです。Java文法のリファレンス的にも使えます!(笑)
技術書は高いのが難点ですよね。ですので、せっかく勉強するならnoteにまとめて、知識を安く提供出来たら良いなと考えたのでこの記事を執筆いたしました。
また、状況に応じて追記予定ありです。
はじめに
プログラミング言語が複数ある中で、人気が衰える事のない言語として「Java」が挙げられます。汎用性のある言語であり、その用途は多岐に渡ります。
代表的なのがAndroidアプリであったり、デスクトップアプリケーション、ゲーム開発なども可能です。JavaのフレームワークであるHadoop(大規模データの分散処理を支えるオープンソースのフレームワーク)が多数の企業で採用されているのもJavaが人気である理由の1つです。
さて、そんな実用性のあるJavaですが、他のプログラミングを学ぶ上で非常に手助け(土台)となる概念や考え方が含まれています。(クラス・インスタンス・継承・インターフェースなど)よって、プログラミングを始める際にJavaを選択することは非常に有意義だと思います。
この記事ではJavaをこれから勉強していきたい!という人のためにJavaの基礎文法~その考え方までを体系的にまとめて解説していきます。プログラミングに全く触れたことのない人も、是非チャレンジしてみて下さい!
Javaを学ぶ上で心得ておくこと
Javaは決して簡単な言語ではありません。プログラミングにおいて非常にコアとなる概念が詰まっているため、使いこなすとなるとどうしても時間が掛かってしまいますが、その知識は必ず役に立つと思います。
Javaに限らずですが、プログラミング言語を勉強する上では
①行き詰ったら分かる人に聞く
②わからない部分は推測して試す
③どうしてもわからなければまずはインターネットで調べる
④いろんな書籍を読んで概念を的確に把握する
⑤時間をかけることをいとわない
こういった項目を満たすように意識して下さい。特にプログラミングを初めて勉強する人は時間がかかるでしょう。しかし諦めずに自分で考えることも多少は必要です。自分で考えるだけでは効率も悪く、概念も偏った知識を付けてしまうので、時にはわかる人に質問する事も非常に重要になってきます。
Javaとは
Javaとは、オブジェクト指向を取り入れた静的型付き言語です。少し難しいですね。これが何を意味するかは勉強していくうちに分かってくるのですが簡単に解説しましょう。
まず、「オブジェクト指向」とはプログラミングにおける概念の1つであり決まった定義などは存在しません。オブジェクト指向は「モノ」を操作する概念であるということをぼんやりと理解しておけば大丈夫です。プログラムは基本的に各行の処理を1つづつ見ていき、その処理を理解する必要がありますが、オブジェクト指向では一定のコードをひとまとめにした「モノ」を作り(=これをオブジェクトという。英語と同じ。)、それを使って何かを作ろうという概念があります。これがオブジェクト指向です。
次に「静的型付き言語」ですが、ここでいう静的とはプログラムを実行していない段階で決まる性質を持つプログラミング言語だということです。また、型付きとはそれぞれの変数に対して必ず型を定義しなければなりません。(ここでは意味を完全に理解しなくて良いです。)
プログラミング言語の中には型を付けなくて良いものなんかもあったりして、それと比較するとJavaのコード量にはうんざりするかもしれません。しかし、エラーチェックが容易であったり、より正確に情報を付加できるという点ではメリットがあります。
Javaで単純なプログラムを作成する
Javaが何たるやを理解したところで早速プログラムを作成していきましょう。
まず、適当な場所に「java」というディレクトリを作っておきましょう。その中に、各々のディレクトリでjavaファイルを管理しておくと非常に楽です。(この理由は後になってわかります。)僕はAtomとコマンドプロンプトでJavaの開発環境を整えているので今回はそれで解説していきます。(開発環境は適宜自身のもので対応してください!)
javaフォルダの中にとりあえず新規でjavaファイルを作っていきましょう。
拡張子は「.java」です。今回は「HelloWorld.java」とでもしておきましょう。
作成したら、中身をこのように埋めて下さい。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
ではこれを実行していきます。実行の仕方は、他のプログラミング言語に比べて少々難がありますので初めてJavaを触る人は注意して下さい。
C:\note>cd java
C:\note\java>dir
2018/05/16 15:56 <DIR> .
2018/05/16 15:56 <DIR> ..
2018/05/16 15:57 126 HelloWorld.java
1 個のファイル 126 バイト
2 個のディレクトリ 175,244,931,072 バイトの空き領域
cdコマンドでjavaフォルダの中まで移動して下さい。対象のファイルが見つかれば、これを「javac」コマンドでコンパイルしていきます。
するとこのように、何も反応が返ってきません。実はこれでコンパイルできています。dir(Macの方はls)コマンドでディレクトリの中を見てみましょう。
2018/05/16 16:08 426 HelloWorld.class
HelloWorld.classというクラスファイルが出来ているのが確認できたはずです。クラスファイルの中身は「バイトコード」で書かれています(バイトコードについては各自調査してみて下さい。知らなくても問題ないです)。このファイルを「java」コマンドで再度実行します。この時、指定ファイルの拡張子部分である「.class」は打ち込む必要がありません。
C:\note\java>java HelloWorld
Hello World!
Hello World!の出力が確認できました!これがJavaファイルの実行の一連の流れです。理解していただけたでしょうか。
では、再度上のコードを見てみましょう。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
このコードについて簡単に説明します。まず、Javaファイル内のコードは「クラス」と呼ばれるもので管理します。最も外側の枠である部分で示されている部分がHello Worldというクラスです。
そしてそのクラスの中にmainというメソッドがあります。メソッドはクラスの中にいくつも書くことができますが、クラス内のメソッドの中で一番初めにmainメソッドが実行されます。よって、複数クラスを扱う場合でもmainメソッドは必ず1つ必要なんだという事を頭の片隅に置いておきましょう。逆に複数mainメソッドが定義されているとエラーになる可能性もあるので気を付けて下さい。
また、コンパイルされたものが同じディレクトリ上に展開されることもしっかりと理解しておきましょう。ここから先はコードのみで解説していくので、適宜任意のフォルダやファイルを作ってJavaファイルを実行して下さい。
Javaにおけるメソッドとは
メソッドとは、通常他のプログラミングで言われている「関数」と微妙に異なるようです。以下、IT用語辞典より引用しました。
関数とは、引数と呼ばれるデータを受け取り、定められた通りの処理を実行して結果を返す一連の命令群。
必ずしも数字を扱うものだけを指すのではなく、「入力された文字を画面に表示する関数」なども存在する。
言語によっては引数を取らない関数や結果を返さない関数を作成できるものもあるが、通常はそうしたものは関数とは呼ばないことが多い。
メソッドとは、オブジェクト指向プログラミングにおいて、各オブジェクトが持っている自身に対する操作。
オブジェクトは「データ」と「手続き」から成っているが、その「手続き」の部分に当たる。
プログラミング言語によっては「メンバ関数」と呼ばれることもある。
一体何が違うのかよく分からない部分もありますがメソッドというのはオブジェクト指向言語特有の名称のようです。クラス内にある、そのオブジェクトに対する処理をまとめたもの、それがメソッドです。
このあたりの用語の概念は難しく、理解してもしなくても今後のプログラムを見ていくうえで何ら変わりはありませんので「こんなものか」と雰囲気を理解していただければ大丈夫かと思います。
Javaで使える基本型
Javaで用いる基本型を簡単に羅列しておきます。
boolean...真偽値で、trueまたはfalseをとる
byte...8ビットの2の補数をとる
short...16ビットの2の補数をとる
int...32ビットの2の補数をとる
long...64ビットの2の補数をとる
float...32ビットの浮動小数点表示
double...64ビットの浮動小数点表示
char...文字を扱うときに使う。16ビットの文字コード
これらの基本型とは、変数を宣言するときに変数の前につけることが文法規則として決まっています。
変数とは、データを格納する箱のようなものだと思ってください。自分で定義した変数に、様々なタイプのデータを入れることができるようになるのですが、そのタイプを決定するために必要なのが型宣言です。
int a; //int型のaという変数を宣言する。
char moji; //char型のmojiという変数を宣言する。
こんな感じに変数を定義します。型を付ける事によってその変数の中に入るデータのタイプを決定できます。例えば、int型の変数aには3や10などの整数を入れることができ、char型の変数mojiにはクォーテーション(またはダブルクォーテーション)で囲んだ文字を入れることができます。変数にデータを入れる行為の事を「代入」と呼びます。
a = 3; //aに3を代入する。
moji = "あああ" //mojiに「あああ」を代入する。
Javaの基本的な演算子
Javaの基本的な演算子を簡単に羅列しておきます。
+(プラス)
-(マイナス)
++(インクリメント)
--(デクリメント)
*(乗算)
/(除算)
==(等しい)
!=(等しくない)
<(小なり), >(大なり)
<=(小なりイコール), >=(大なりイコール)
これらの算術演算子と比較演算子の他に、論理演算子もあります。
&&(論理積), ||(論理和), ^(排他的論理和)
これら以外にも演算子は存在しますが、全て紹介していたら面倒なので省きます。自分が使用したい演算が出てきたときに、各々で適切な演算子を選択しましょう。
プログラミングは基本、現実世界と同様の文法で数値計算が可能ですが、等号に関しては気をつけましょう。
a = 1というのはプログラミングの世界では「代入」を意味します。右辺と左辺が等しいという状態を表したければ「==」を使用してください。初めてプログラミングをする人はここで躓きやすいので是非覚えておいてください。
オブジェクト指向についてもう少し
さて、まだオブジェクト指向についてピンと来ていない人に対して図解とともにもう少しイメージを深めておきましょう。
オブジェクト指向プログラミングでは「クラス」「インスタンス」「オブジェクト」の関係を理解する必要がありますが、インスタンスとオブジェクトはほぼ同義です。という事で「クラス」と「インスタンス」の関係について押さえておけばまぁ大丈夫かなといった感じです。例えばリモコンを作るとしましょう。
「クラス」はリモコンの「設計図」です。チャンネルが何個あるとか(データ部分)、このボタンを押すとどうなるか(メソッド部分)などといったことが定義されています。
「インスタンス」はクラスを元にして作られたモノ(オブジェクト)であり、実体です。料理でもレシピがあれば同じような料理が作れますよね。その例をとるとレシピがクラスで料理がインスタンスだとイメージすると分かりやすいですね。ここではリモコンそれ自体です。クラスがあれば何個でもインスタンスを生成することができます。
...なんとなくイメージできたでしょうか?ここから先は実際にコードで理解していきましょう。
Javaでインスタンスを生成してみる
プログラミングにおける基礎的な部分を理解したところでJavaの中核のひとつである「インスタンス」について学んでいきます。
インスタンスとはすなわちオブジェクトの事で、オブジェクト指向の世界ではインスタンス≒オブジェクトです。ここの違いは長年議論されており、厳密な定義が有るようですが、さほど違いはないのでインスタンス=オブジェクトとして認識しておくと良いでしょう。
Javaではファイルの中に必ずクラスを作成しますが、そのクラスは「設計図」の段階であり実態は作られていません。クラスはあくまでモノを作るための設計図であり、インスタンスを生成しないと実際に操作することができません。
では、任意のフォルダ内でインスタンスを生成するためのクラスを作っていきます。まず、Pointクラス(設計図)を作っておきましょう。
//ファイル名:Point.java
public class Point {
int x, y;
double getArea() {
return x * y;
}
}
このファイルを作成した同ディレクトリ内にもう一つjavaファイルを作成することで、そのファイルからPointクラスにアクセスする事が可能です。では、インスタンスを作成していきます。
Point p1 = new Point(); //Pointクラスのインスタンスp1を生成
このような書き方でインスタンスを生成する事が可能です。クラスが型になり、任意の変数に対して new クラス名(); を代入することでインスタンスを生成します。
(追記:この処理を丁寧に説明すると、まず、Point型のオブジェクトが生成されます。そしてその「参照」がPoint型の変数p1に代入されます。非常に細かいですが、オブジェクト自体が代入されるわけではないことに注意してください。)
sPoint p2 = new Point(); //Pointクラスのインスタンスp2を生成
このように同じクラスから何個もインスタンスを生成する事が可能です。Pointクラスを参照しているという点は同じですが、p1とp2は異なるインスタンスです。
生成されたインスタンスごとにクラスで宣言された変数である「インスタンス変数」が与えられます。このインスタンス変数は、インスタンスごとに異なる点に注意してください。例えば、
p1.x = 1;
p1.y = 1;
p2.x = 2;
p2.x = 2;
System.out.println(p1.getArea());
System.out.println(p2.getArea());
とすると、結果は
1
4
と返ってきます。これが理解できれば、インスタンス変数を理解できたのも同然です。異なるインスタンスに対して、インスタンス変数が割り当てられるという考えを分かっていただけたでしょうか。
このプログラムの動きが分からない人は下の項目を読んでみて下さい。
インスタンスの操作とインスタンス変数
インスタンスの操作について解説していきます。まず、変数宣言のみではインスタンスは生成されないことに注意してください。
Point p1; //インスタンスの変数宣言をする。
これだけではp1にnull(ヌル:何も入っていない状態)が代入されてしまい、インスタンスが生成されません。インスタンスを生成する場合は必ずnew クラス名(); を書きましょう。
インスタンス変数を読み書きする(アクセスする)にはこのようにします。
p1.x = 1;
p1.y = 1;
このプログラムを実行するとインスタンスp1のインスタンス変数x、yにそれぞれ1が代入されます。
次に、メソッドの呼び出しについて見ていきます。メソッドの呼び出しもインスタンス変数のアクセスの様に「.(ドット)」によりアクセス可能です。
double area = p1.getArea(); //メソッドの呼び出し結果をareaに代入
System.out.println("areaは" + area + "です");
これを実行すると
areaは1です
と出力されます。System.out.println();の中身ですが、文字列と数値が混在しています。型が異なりますが、実は+で繋げて表示することが可能です。何が起こっているかというと、double型であるareaが周りの文字列と結合し、結果的にareaの数値が数字(文字)となって出力されているわけです。
さて、インスタンス変数についておさらいしていきましょう。
インスタンス変数とは、クラス内で定義され、インスタンス(オブジェクト)ごとに別々の場所で記憶されます。(上で書いた通り)
インスタンス変数を定義したクラス内からはこのように普通にアクセス可能です。
public class Point {
int x, y;
double getArea() {
return x * y;
}
}
インスタンス変数を定義したクラス外からは、「.(ドット)」によってアクセスできるという事は、先程確認したばかりです。
public class AreaUser {
public static void main(String[] args) {
Point p1 = new Point();
p1.x = 2;
p1.y = 1;
System.out.println("x = " + p1.x + "y = " + p1.y);
}
}
そして、インスタンス変数はインスタンスごとに存在するという事も忘れてはいけません。
Point p1 = new Point(); //Pointクラスのインスタンスp1を生成
Point p2 = new Point(); //Pointクラスのインスタンスp2を生成
p1.x = 1;
p1.y = 1;
p2.x = 2;
p2.y = 2;
System.out.println(p1.getArea());
System.out.println(p2.getArea());
この実行結果は
1
4
となります。ここまでが整理できてればインスタンスの基本は完璧です。
Javaの配列について学ぶ
インスタンスの基本が押さえれたところで、Javaの配列について解説していきます。Javaでの配列の宣言は以下の様になります。
int[] a; //int型の配列a
double[] b; //double型の配列b
Point[] p; //Point型(クラス)の配列p
多次元配列にすることも可能です。
int[][] a;
double [][][] b;
ここで注意してほしいのが、配列は宣言しただけでは使えないという事です。宣言しただけでは配列が生成されないからです(理由は配列が「参照型」だから)。配列に値を代入していくためには、配列の実体を生成する必要があります。少し面倒ですがこれがJavaです。
int[] a = new int[要素数]; //配列の生成
aという配列を生成する例です、要素数には10や5などの要素の数を入力しましょう。10であれば、添え字は0~9, 5であれば0~4と、配列それぞれの要素の番号は0から始まる点は他のプログラミング言語と同様です。
このように配列を生成することで各要素の型はint型と決まり、そして各要素の値は0に初期化されます。インスタンス(オブジェクト)配列の場合はnullに初期化されます。
int[] a = new int[]{1,2,3,4,5};
とすると好みの値で配列を初期化する事も可能です。ここで、
// a[0] ⇒ 1
// a[1] ⇒ 2
// a[2] ⇒ 3
...
となることを今一度確認しておきましょう。配列の添え字は0スタートです。同様に、少し難しいですがインスタンスの配列も見ていきます。
Point[] p = new Point[]{new Point(), new Point()};
pという配列を生成するのですが、各要素の型はPoint型です。
// p[0] ⇒ Pointオブジェクト1
// p[1] ⇒ Pointオブジェクト2
p[0], p[1]は新しく生成された別々のインスタンス(オブジェクト)を指します。少し考えればすんなり理解できるはずです。一気に大量のインスタンスを生成したい場面で配列でのインスタンス生成が役に立ちます。
配列の使用方法
C言語を学習した人であれば基本的にはC言語の使い方と同じです。
配列のアクセスには添え字を用いて特定の要素にアクセスすることが可能です。
int[] a = new int[] {1,2,3,4,5};
int b = num[1];
System.out.println(b);
//実行結果
2
また、配列の長さは.lengthを用いて取得可能です。例えば...
int sum;
int[] a = new int[] {5,5,5,5};
for(int i = 0; i < a.length; i++) {
sum += a[i];
}
として配列の要素の合計を求めるプログラムも作成できるわけです。
また、配列の宣言と生成ですが
型名 配列変数名[] = {値1, 値2, 値3, ... };
と行うと、newしなくとも宣言と生成が行えるのであらかじめ配列にセットする値が分かっている状態ではこちらの方法をオススメします。
クラスの「情報隠蔽」
さて、ここまで変数、クラス、メソッド、インスタンス、配列...と見ていきましたが、ここで少し概念的な話が入ります。それはクラスの役割です。
何のためにクラスを定義するのでしょうか?その理由には色々あるのですが、その中の1つが「情報隠蔽」です。
情報隠蔽の基本的な考え方は、「クラスの内部がどのようになっているか」という情報を隠すようにすることで、そのクラスがどのように実装されているかを気にしなくて済むというメリットから発生しました。
これまでの宣言方法では、変数をクラス外から簡単にアクセスできました。しかし複雑なプログラムでは外部のクラスからアクセスしてほしくない変数も出てきます。その問題を解決できるのがクラスであり、「privete修飾子」の出番です。
public class PrivateSet {
private int[] a; //クラス外からアクセス不可能
private int b; //クラス外からアクセス不可能
...
public void add(int num) {
if(b < 3) a[b++] = num; //クラス内からはアクセス可能
}
}
変数にprivateを付けて宣言する事で、クラス外からのインスタンス変数へのアクセスを禁止することができます。このprivate修飾子はメソッドにも付ける事が可能で、そういったメソッドはプライベートメソッドと呼ばれています。クラス外からは呼び出すべきではないメソッドに対して宣言します。
ここでJavaにおける重要な考え方の1つを言うと、原則としてインスタンス変数はprivateにしておく、という事です。
インスタンス変数をどうやって確認したり、変更したりするのかというと、基本的にはメソッドを介して行います。詳細はまた後程書きますが、とにかく
「インスタンス変数はprivateを付ける」
という原則を覚えておきましょう。
基本型と参照型
Javaの型は基本型と参照型に分けることができます。
⇒基本型
・boolean
・byte, short, int, long, char, float, double
⇒参照型
・クラス(クラスも型の1つです!)
・文字列(String型はクラスの1つ!)
・配列
・インターフェース
このような感じで分類されるみたいです。
基本型と参照型の違いについて押さえる
基本型と参照型は一体何がことなるのでしょうか?「参照」の意味が分かる人やC言語などでポインタの概念を勉強した人はすぐにわかるかもしれません。丁寧に解説しますのでしっかり理解していきましょう。
まずは、問題です。次のクラスを用意します。
public class Point {
private int x, y;
int getX() {return x;}
int getY() {return y;}
void setX(int X) {x = X;}
void setY(int Y) {y = Y;}
}
f次に、このようなクラスを用いて結果を出力します。
public class PointPrint {
public static void main(String[] args) {
Point p1 = new Point(); //1
Point p2 = new Point(); //2
p1.setX(1); //3
p1.setY(3); //4
p2 = p1; //5
p1.setX(3); //6
System.out.println("p2.x is " + p2.getX());
}
どういう出力結果になるでしょうか?時間に余裕のある人は是非考えてみて下さい。正解はこうなります。
p2.x is 3
ん?p2に値をセットしたという行はどこにもありません。なのになぜp2.xに3が格納されているのでしょうか?
直ぐにわかった人は大変優秀ですがプログラミングを学習し始めたばかりの人には理解しにくい部分かもしれません。では例によって図解で解説していきます。
まずは//1 ~ //4 までの状況をみていきましょう。
図を見てわかる通り、p1,p2のインスタンスがそれぞれ生成され、インスタンス変数もインスタンスの数だけ生成されます。では、//5 , //6 でどのような処理が起きているのでしょうか?
p2 = p1 という式に注目してください。クラスは参照型なので、クラスからできたインスタンスも参照型になります。参照、というのは変数に対する「番地」のようなものだと理解すると分かりやすいかもしれません。p2が指していた矢印は、p1のものを参照するように変更されます。
という事で、出力結果が3になるのも納得いただけたかと思います。ここの基本型と参照型の違いは厄介なので、先にしっかりと理解を深めておいた方が後々役にたつかもしれません。
実引数と仮引数
引数とは何か、すぐにわかる人はプログラミング経験者の方かと思われますのでこの項目は飛ばして結構です。
引数が分からないという人はご安心ください。これから簡単に解説します。まずは引数が何処に書かれるのか、その形式をみていきます。
初めに紹介したHello Worldのプログラムをもう一度みてみましょう。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
ここで、String[] args に当たる部分が「引数」と呼ばれるものであり、メソッド名の横の()内に記述されます。文字で表すとこのような関係になっています。
クラス名 {
public static void メソッド名(引数1の型 引数1, 引数2の型 引数2,...){
処理
}
}
もう少し簡単な具体例をみておきましょう。まずは次のプログラムを用意しておきます。
public class Point {
public static int hikihiki(int x) {
return x + 1;
}
}
そして、次のプログラムを実行します。この結果がどうなるか予想してみて下さい。
public class AddOne {
public static void main(String args[]) {
Point p = new Point();
System.out.println(p.hikihiki(5));
}
}
出力結果は「6」になることがわかるでしょうか。Point型であるインスタンスpを作成し、そのインスタンスからhikihikiメソッドを呼び出しています。
hikihikiメソッドは5を実引数としてメソッド内に仮引数xとしてそのコピーを取り込み、xに対して1を足した結果を返しています。よって5 + 1 (= 6)という結果が出力されるのです。
ここでいう実引数とは5のことで、実引数とは「メソッドを呼び出す側の引数」のことです。
また、ここでいう仮引数とはxのことで、仮引数とは「呼び出されるメソッド側の引数」のことです。
以上が引数の基本中の基本なので、理解出来たら次へ進みましょう。
文字列はクラスのひとつ!?
さて、上の項(基本型と参照型)で
・文字列(String型はクラスの1つ!)
と記述したのを覚えていますでしょうか。文字列なのにクラスのひとつ?不思議に感じる方もいるかと思いますが、Javaの世界では文字列Stringはクラスのオブジェクト(インスタンス)であるという了解があります。
しかし、通常のインスタンスのようにnewする必要がありません。newなしでインスタンスを生成することができるのです。
public static void main(String[] args) {
String s = "これはインスタンス" //この時点でインスタンスが生成される。
System.out.println(s);
}
変数sにはインスタンスである"これはインスタンス"という文字列が代入されます。
文字列の連結についても補足しておきましょう。
文字列と文字列は+で連結できます。
"test" + "01"
"test01" //これらは2つとも同じ
また、基本型との連結の場合、基本型の値は文字列に変換されます。以前の項目で少し触れた気がしますが確認しておきましょう。ちなみに、参照型の値を文字列に連結すると意味不明な文字列が出力されます。余力のある人は実際に確かめてみて下さい。
String s = "結果は" + true;
System.out.println(s);
↓
//出力結果:
結果はtrue
文字列sの内容を比較するには.equalsメソッドを用います。これはJavaで定義しなくとも標準で使えるメソッドとなります。
s.equal("これはインスタンス"); //sの内容が"これはインスタンス"かどうかを調べる。
間違っても等号演算子である「==」を使わないでください。文字列で等号演算子は使えません。何故だか少し考えてみて下さい。分からない方は、以下の項目を読んでみましょう。
等号演算子「==」の注意点
等号演算子を扱う場合は少し注意点が存在します。
基本型のものを比較する場合(値が等しいかどうかを調べる場合)、基本的には等号演算子により比較可能です。これは良いですよね。
int a = 1;
int b = 2
int c = 1;
a == b; //これは偽(false)
a == c; //これは真(true)
しかし参照型の場合、同じインスタンスを参照しているかどうかを調べるため注意が必要です。
Point p = new Point();
Point q = new Point();
Point r = new Point();
r = p;
p == q //これは偽(false)
p == r //これは真(true)
q == r //これは偽(false)
上のコードを見てわかるように、pとrは同じインスタンスを参照しているため(pはpのインスタンス、rはpのインスタンスを参照)このような結果になるわけです。
カプセル化について
「インスタンス変数はprivateを付ける」
という事を話したと思いますが、クラスは基本的に「カプセル化」するという考えが定着しています。ではこのカプセル化とは何なのか説明しましょう。
クラスはインスタンスの設計図ですが、その中身はデータ(構造)部分とデータ構造を処理するコード部分に分けることが出来ます。
public class PointComp {
private int x; //データ構造部分
private int y; //データ構造部分
int getX() {...} //以下、処理(コード)部分
int getY() {...}
void setX() {...}
void setY() {...}
}
getX, getYはxとyの値を取得するメソッドで一般的に「ゲッター」と呼ばれます。また、setX, setYはxとyに値を代入するメソッドで一般的に「セッター」と呼ばれます。このようにゲッターとセッターを定義することでprivateで宣言されたxとyに間接的にアクセスするという構造がクラスの基本です。こうする事により、直接変数にアクセスされることを防ぐことができます。では、もう少し具体的にプログラムをみていきましょう。
public class PointComp {
private int x;
private int y;
public int getX() { return x; }
public int getY() { return y; }
public void setX(int x) { this.x = x; } //xは引数から渡された値が入る
public void setY(int y) { this.y = y; } //this.yはインスタンス変数のyが入る
}
補足:this.~という文法は、同じクラス内の変数~にアクセスするという事を示しています。
なるほど、こんな感じです。しかし、これだけではセッターによって値が簡単に書き換えられてしまいます。外部からの入力により変更されてほしくない変数に対して「それでは意味が無い」という事がわかります。
では、どうするのかというと、値を外部からの入力に対して直接的に変更されてほしくない値にはメソッドを介して値を操作します。何でもかんでも、ゲッターとセッターを付ければよいという考え方は非常に安易です。
言葉だけでは説明がしにくいので、1つ例を出してコードを見ていきましょう。ここでは、車のクラスであるCarクラスを例に説明していきます。
public class Car {
private int fuel;
public int getFuel() { return this.fuel; }
public void setFuel(int n) { this.fuel = n; }
}
単純にゲッターとセッターを定義しています。
public class CarCreate{
public static void main(String[] args){
Car c1 = new Car();
c1.setFuel(100);
c1.setFuel(0);
}
}
単純にセッターを定義しただけでは車のガソリン(変数fuel)を簡単に外部から操作されてしまいます。これではprivateで宣言してもしなくても同じことになります。ガソリンはガソリンスタンドに行くことで補給され、車が走ると減ります。そのような構造を予め定義することが重要です。ガソリンの増減は外部から操作してほしくないので、Carクラス内に事前にガソリンスタンドでガソリンを補給するというgoStandメソッドと、走ってガソリンが減るというrunメソッドを作成します。
public class Car {
private int fuel = 0;
public int getFuel() { return this.fuel; }
public void goStand() {
this.fuel += 30;
}
public void run() {
this.fuel -= 10;
}
}
これで外部から直接ガソリンの値を入力する事は不可能になりました。しかしこの方法でも、このような方法を思いつく人がいるかもしれません。
public class CarCreate{
public static void main(String[] args){
Car c1 = new Car();
c1.run();
c1.run();
c1.run();
c1.run();
...
}
}
ガソリンが減り続けてしまう恐れがあり、現実ではありえないマイナス数値が出てきます。また、ガソリンが満タンなのにさらに追加する事も出来てしまうわけです。これを防ぐには下限と上限を決定しておきます。
public class Car {
private int fuel = 0;
public int getFuel() { return this.fuel; }
public void goStand() {
this.fuel += 30;
if(this.fuel > 100){
this.fuel = 100;
}
}
public void run() {
this.fuel -= 10;
if(this.fuel < 0){
this.fuel = 0;
}
}
}
さらに、ガソリンが満タンであれば補充できないように・ガソリンが十分無ければ走れないように条件を追加しておきます。
public class Car {
private int fuel = 0;
public int getFuel() { return this.fuel; }
public void goStand() {
if(this.fuel < 100){
this.fuel += 30;
if(this.fuel > 100){
this.fuel = 100;
}
}
}
public void run() {
if(this.fuel >= 10){
this.fuel -= 10;
if(this.fuel < 0){
this.fuel = 0;
}
}
}
}
なんとなく意味が分かったでしょうか?単純にセッターを付けるだけではうまくカプセル化したとは言えません。上の例の様に、状況やデータ構造に合わせて適切なメソッドを準備する事が良いプログラムを書く上で欠かせない要素となるのです。
メソッドのオーバーロード
同一クラス内で同じ名前のメソッドは扱えるのでしょうか。結論から言えば扱うことが可能です。しかし少し条件があります。
メソッドのオーバーロードは別名多重定義とも呼ばれているのですが、同一名のメソッドを定義するにあたって引数の個数や型が異なれば別のメソッドとして認識されます。これをメソッドのオーバーロードと言います。
public class Sum {
int Addnum(int x, int y) {
return x + y;
}
int Addnum(int x, int y int z) {
return x + y + z;
}
}
このAddnumメソッドを呼び出す際、引数を2つ与えれば上のメソッド、引数を3つ与えれば下のメソッドが、コンパイル時に自動的に判別されて呼び出される仕様になっています。
public class SunTest {
public static void main(String[] args) {
Sum s = new Sum();
System.out.println("1 + 2 = " + s.Addnum(1, 2));
System.out.println("1 + 2 + 3 = " + s.Addnum(1, 2, 3));
}
}
出力はそれぞれ
1 + 2 = 3
1 + 2 + 3 = 6
となります。
メソッド名、引数の数、引数の型のことをシグネチャと読んだりします。コンパイラはこのシグネチャの違いを判別してメソッドを選び、実行するということです。
メソッドの多重定義における注意点
メソッドのオーバーロードは便利な機能ですが、実際に開発する段階としては多用すべきでない方法です。メソッド名が同じだと、他の開発者がそのプログラムを見た時に混乱を招きやすいコードになってしまうからです。
次の2つのメソッドは多重定義可能です。
int Addnum(int x, int y) {...}
double Addnum(double x, double y) {...}
このように、引数の型と返値の型がどちらも異なるものであれば使い分けることができます。
しかし、返値の型が異なるだけのメソッドは多重定義不可能です。
int Addnum(int x, int y) {...}
double Addnum(int x, int y) {..}
メソッドを呼び出す際に、どちらのメソッドに実引数を入れればよいのか判別不可能であるため、このAddnumメソッドを呼び出すとエラーが出ます。各自で確認してみて下さい。
コンストラクタについて学ぶ
ここでは、特別なメソッドである「コンストラクタ」について学びます。
コンストラクタとは、インスタンスを生成する際に自動的に呼ばれるメソッドのことです。インスタンス生成時に初期値を代入したいときに非常に役に立ちます。
コンストラクタを定義するにはクラス名と同じ名前でメソッドを定義すると、それが自動的にコンストラクタになります。
コンストラクタの中に、初期化したい処理を入力しておくと、インスタンス生成時にそれぞれの変数に対して初期値が代入されます。例えば
public class Car {
private int fuel;
private String color;
//コンストラクタ
public Car() {
fuel = 50;
color = "red";
}
}
ここで、Car car = new Car(); とするとコンストラクタ Car() が自動的に呼ばれ、fuelには50、colorがredに決まります。初期値を設定する手間が省けるわけです。
上のプログラムではコンストラクタに引数を与えていませんが、コンストラクタにも引数を与えることができます。
public class Car {
private int fuel;
private String color;
//コンストラクタ
public Car(int fuel, String color) {
this.fuel = fuel;
this.color = color;
}
}
引数をfuelとcolorにする必要はないと思うのですが、メンバ変数(インスタンス変数と同じでメンバ変数とも呼びます!)と同じ名前にしたならばもとのメンバ変数に値を代入する際、thisを付ける点に注意して下さい。ここら辺少し難しいですがこれまでの解説を理解している人であればすんなり頭に入ってくるはずです。thisを付ける事で、同じ変数名であってもインスタンス変数と仮引数を区別することが可能になります。特にこういった引数とインスタンス変数を同じメソッドで用いる場合によく使われたりします。ここら辺はコードを書いていくうちに慣れるかと思われます。
コンストラクタのオーバーロード
コンストラクタもメソッドの1つですから、オーバーロードする事が可能です。すなわち同クラス内にコンストラクタを多重定義することができます。
public class Car {
private int fuel;
private String color;
//コンストラクタ1
public Car() {
fuel = 50;
color = "red";
}
//コンストラクタ2
public Car(int fuel, String color) {
this.fuel = fuel;
this.color = color;
}
}
デフォルト・コンストラクタ
コンストラクタを定義しなかったらどうなるのでしょうか?何も無いように思われますが、実はバックグラウンドで「デフォルト・コンストラクタ」というコンストラクタが定義されるのです。
デフォルト・コンストラクタは以下の処理を行います。
public クラス名(引数なし) {
boolean型 ⇒ falseに初期化される
int, double, float, byte, long, short ⇒ 0に初期化される
配列やクラス等の参照型 ⇒ nullに初期化される
}
デフォルト・コンストラクタは、自分で1つ以上コンストラクタを定義すると消えて無くなる仕様になっています。
クラス変数
これまで見てきた変数は、インスタンスごとに生成される「インスタンス変数」でしたが、この項目ではクラス変数について解説していきます。
クラス変数とは、クラスごとに作られる変数の事でstatic付きで宣言されます。特徴として、コンパイル時に作成される変数であり、複数のインスタンスに対して参照元が1つしか存在しないというルールのもとクラス変数を宣言します。
static private int count; //クラス変数countを宣言
クラス変数にアクセスするにはクラス名と変数名を指定してアクセスできます。例えば、Pointクラスのクラス変数countにアクセスするには
Point.cout //クラス変数にアクセスする
という風にしてアクセスする。同クラス内であれば例によって「this」を用いてアクセスするという事も覚えておきましょう。
インスタンス変数との違いは、インスタンスは「インスタンス名.変数名」としてアクセスしますが、クラス変数はインスタンスの数に関わらず一意なので、「クラス名.変数名」としてアクセスするという違いがあります。ここら辺を混同せずに整理して覚えておいて下さい。
クラス変数を用いたプログラムとして代表的なものに「作成されたインスタンスの数を数える」というものがあります。
public class Point {
static private int count = 0;
public Point() {
count = count + 1;
}
}
コンストラクタを用いて、生成されたオブジェクトの数を確認する事ができます。
定数宣言
Javaでは定数を宣言する事ができます。finalという修飾子を付ける事によって、整数を扱えるようになるのです。
勿論、定数なので初期化後は値を変更できないようになっています。
Javaの暗黙の了解として、定数は大文字にするというルールも存在しますので、意識的に定数を設定する場合は大文字で変数名を定義しましょう。
public class Cons {
static final int CONSTANT = 1; //定数宣言
public static void main(String[] args) {
CONSTANT = 10; //エラーが出る
}
}
クラスメソッドについて学ぶ
これまで扱ってきたメソッドは全て「インスタンスメソッド」と呼ばれるもので、インスタンス変数の様にインスタンスを介してメソッドを呼び出してきましたね。そして、インスタンス変数もクラス変数も学習済なので、クラスメソッドが一体どういうものなのか想像できる人が多いでしょう。
クラスメソッドとは、インスタンスを指定せずに呼び出すメソッドのことで、クラス変数同様staticを付加することにより宣言可能です。
public class Point {
static private int count = 0;
public Point() {
count = count + 1;
}
static int getCount() { //クラスメソッドの宣言
return count;
}
}
クラスメソッドを呼び出すには、インスタンスを作る必要はありません。「クラス名.メソッド名」という書式により簡単にアクセス可能です。
Point.getCount(); //クラスメソッドにアクセスする
クラスメソッドはインスタンスを作らずにアクセスできるという性質を保有しているので、インスタンスを生成してアクセスできるインスタンス変数や、同じクラス内の変数に対してアクセスするthisといったものをメソッドの中に記述できません。なぜなら、どのインスタンスに対してどのインスタンス変数にアクセスしたら良いかわからないからです。
インスタンス変数との性質の違いについてしっかり整理し、理解しておきましょう。
クラスの継承
クラスについて学んできましたが、似たようなクラスを作る際にいちいち同じ部分をコードに書いていたら時間も無駄ですし、スペルミスをする確率も増加します。例えば、同じ車でも普通の車と自動運転の機能を持つ車をクラスで定義するとしましょう。
public class NormalCar {
public void run() {
System.out.println("run");
}
}
public class AutoRunCar {
public void run() {
System.out.println("run");
}
public void autorun() {
System.out.println("auto run");
}
}
NormalCarクラスと、AutoRunCarクラスを見比べてみて下さい。コードに無駄が多いと思いませんか?AutoRunCarクラスを定義する際に、共通の動作であるrunメソッドを、もう一度書かなければならないのです。
このコードの無駄をなくすために設計された文法が「継承」です。クラスは別のクラスを継承することができ、クラスの変数やメソッドを継承されたものに引き継ぐことができる非常に便利な機能です。オブジェクト指向言語にはこのように継承の機能が付いているものが殆どです。クラスの継承は、ソフトウェアの柔軟性を高める強力な仕組みです。
さて、改めて説明すると、「継承」とはあるクラスを元に、インスタンス変数やメソッドを追加する事ができるという仕組みです。上の例を使うと、このように定義できます。
public class AutoRunCar extends NormalCar {
public void autorun() {
System.out.println("auto run");
}
クラスを宣言したら、クラス名の後に「extends」を記述し、その後ろに継承したいクラス名を記述します。これで、AutoRunCarにNormalCarのメソッドが継承されました。継承する事で、NormalCarクラスのメソッド(と、あれば変数)がAotoRunCarクラスでも用いる事が可能になります。すなわち、上のプログラムは一つ上のプログラムと同値になっているわけです。
上の例は継承の使い方の1例ですが、このような例を「差分プログラミング」と言います。あるクラスに追加で変数やメソッドを定義したいときに、そのクラスの子クラス(サブクラス)を作ることです。違いのみを追加するという意味で差分プログラミングと呼ばれます。
また、継承されるクラスの事を「親クラス(スーパークラス)」、継承先のクラスの事を「子クラス(サブクラス)」と言います。親子の関係になっているので分かりやすい命名だと思います。
サブクラスのコンストラクタ
上で子クラスは親クラスのメソッドを継承すると書きましたが、親クラスのコンストラクタに限っては例外があります。
親クラスでコンストラクタを定義しても、子クラスではコンストラクタは定義されません。よって、子クラスでコンストラクタを用いる場合は子クラス内でコンストラクタを定義する必要があります。子クラスでコンストラクタを定義しなければ、自動的にデフォルト・コンストラクタが割り当てられます。
しかし、親クラスと似ているもしくは同じコードを書くのは労力を割きます。同じようなものをもう一度書くのはコードに無駄も出ますね。では一体どうすればよいのでしょうか。
実は、子クラスには親クラスのコンストラクタを呼び出す仕組みがあります。その方法は、子クラスのコンストラクタでsuperメソッドを呼び出すという何とも簡易で便利な方法が与えられているのです。これを使わない手はありません。
public class NormalCar {
public NormalCar() {
System.out.println("親クラスのコンストラクタです");
}
public void run() {
System.out.println("run");
}
}
public class AutoRunCar extends NormalCar {
public AutoRunCar() {
super(); //親クラスのコンストラクタの呼び出し
}
public void autorun() {
System.out.println("auto run");
}
}
//子クラスのインスタンスを作成した際の出力結果
親クラスのコンストラクタです
ただし、superメソッドには条件があります。それは子クラスの先頭でしか呼び出せないという事です。先頭に書かずに、適当なメソッドを先頭に入れて2行目にsuperメソッドを書くと、コンパイル時にエラーがでます。
また、親クラスでコンストラクタをオーバーロードして複数コンストラクタがある場合、子クラスでは複数のコンストラクタを呼び出すことはできません。子クラスにおける親クラスのコンストラクタの呼び出しは、「先頭に書いて、1つしか呼び出すことができない」この原則をしっかりと覚えておきましょう。
superメソッドの罠
子クラスのコンストラクタで親クラスのコンストラクタを呼び出すにはsuperメソッドを用いるということが分かりました。では、super() を書かないとどうなるのでしょうか。
親クラスでコンストラクタを定義している場合、super() を書かないと、親クラスの引数のないコンストラクタが自動的に呼ばれます。そして、「先頭に書いて、1つしか呼び出すことができない」というルールに則って、呼ばれるのは引数のないコンストラクタのみであります。
また、親クラスに引数のないコンストラクタが無いとエラーになります。superメソッドを使う際にはこのあたり注意してコードを書きましょう。
protected修飾子について
変数の型の前に付けるpublicやstaticという修飾子を使ってきましたが、ここで新たにprotectedという修飾子について紹介しようと思います。
protectedは、クラス外からのアクセスは禁止ですが、子クラスに限ってはアクセスを可能にする修飾子です。
public class SuperClass {
protected int p;
...
}
public class SubClass extends SuperClass {
public static void main(String[] args) {
System.out.println("p = " + p); //子クラスなのでアクセス可能
}
}
継承と型
次のプログラムを見て下さい。不自然な点はありませんか?
public class X {
public void method() {
System.out.println("X");
}
}
public class Y extends X {
}
public class Obj {
public static void main() {
X a = new X();
X b = new Y(); //??
}
}
??の行に注目してください。型が異なるものをインスタンス化しているようにみえませんか?しかし、このプログラムをコンパイルしてみると何もエラーは出ません。しっかりとコンパイルできてしまうのです。
ここでは、継承と型の関係性によって型変換が起きているということになります。子クラスのオブジェクトは、親クラスの型に自動的に型変換されるのです。このように宣言すると、
X a;
変数aに代入可能なのは、クラスXのオブジェクトだけではなくクラスXの子クラスのオブジェクトも代入できるのです。
即ち、上のコードに間違っている部分はありません。
X b = new Y();
ここでXはbの「見かけの型」、Yはbの「実際の型」と呼ばれます。少し難しいので雰囲気で理解できればOKです。
補足:Javaのクラスは全てObjectクラスのサブクラスです。extendsを使わずにクラスを定義するとそのスーパークラスはObjectクラスになる
インスタンスのクラスを調べるための演算子もここで紹介しておきましょう。
式 instanceof クラス名
この演算結果はtrueまたはfalseになります。trueになる場合は、式で表したインスタンスがクラス名で指定されたインスタンス、またはクラス名で指定されたクラスの子孫のクラス(つまり子クラス)のインスタンスである場合です。falseになるのはそれ以外になります。
メソッドのオーバーライド
メソッドのオーバーライドとは、親クラスのメソッドを子クラスで定義し直す事です(オーバーロードと異なるので注意!)。実際、子クラスでシグネチャが全く同じものでもメソッドを定義し直すことが可能です。シンプルな例をみていきましょう。
例えば、ある施設の入園料を取得するクラスがあったとします。
public class Person {
public int getFee() {
return 100;
}
}
標準の料金を100円と設定し、その値を返すようなgetFeeメソッドをもつPersonクラスです。これを大人、子供の場合において場合分けしたいとします。
public class Adult extends Person {
public int getFee(){
return 200;
}
}
public class Child extends Person {
public int getFee(){
return 50;
}
}
このようにメソッドをオーバーライドすることで同名のメソッドを場合分けすることが可能になります。
public class TestFee {
public static void main(String[] args) {
Adult a = new Adult();
System.out.println("Fee is " + a.getFee());
}
}
このプログラムの実行結果は「200」になります。こうすることによってプログラムの拡張性が増していることが分かります。このような構造を「ポリモーフィズム」と言います。これは、インスタンスのクラスに応じて異なったメソッドを呼び出せるという性質、概念のことです。気になる方は是非調べてみて下さい。
抽象クラスと抽象メソッド
上のプログラムは、子供と大人によって料金を返すメソッドをオーバーライドしましたが、通常はPersonクラスそれ自体の料金は必要ないはずです。誰もが子供か、大人に割り振られるとしたら料金は子供料金の50円と大人料金の200円という設定だけで大丈夫ですよね。
こういった場合に役立つのが「抽象メソッド」です。抽象メソッドは以下の様に宣言します。
abstract 戻り値の型名 メソッド名(引数);
↓
//Personクラス内で抽象メソッドを宣言
abstract int getFee(); //子クラスで後で定義するため中身は書かない。
getFee()の後にブロック({})が無い部分に注意しましょう。抽象メソッドは、メソッドの本体は書きません。抽象メソッドを含むクラスのことを「抽象クラス」と呼びます。
抽象メソッドはabstractのついたメソッドの事で、抽象メソッドは子クラスで必ずオーバーライドしないといけません。オーバーライドを忘れると、コンパイル時にエラーが出ます。
また、抽象メソッドを含むクラスである抽象クラスにもabstractを付ける必要があることに注意しましょう。結果、Personクラスは以下の様に定義できます。
public class abstract Person { //抽象クラスを宣言
abstract int getFee(); //抽象メソッドを宣言
}
抽象クラスのインスタンスを作ろうとするとエラーが出ます。これは、抽象メソッドが含まれているため、メソッドを指定して呼び出してもコンパイラが理解できないために起こるエラーです。よって、抽象クラスのインスタンスを作ることができないという点も押さえておきましょう。
superによる親クラスメソッドの呼び出し
親クラスのコンストラクタを呼び出すsuper() と異なる文法なので気を付けて下さい。メソッドをオーバーライドしているときに使える用法で、親のメソッドを呼び出すのに使います。
public class X {
void method() {
System.out.println("X");
}
}
public class Y extends X {
void method() {
System.out.println("Y");
super.method(); //親メソッドの呼び出し
}
public static void main(String[] args) {
Y y = new Y();
y.method();
}
}
//実行結果
Y
X
インスタンスの型変換
子クラスから親クラスへの型変換は自動的に行われます。上でも少し触れましたがもう一度確認しておきましょう。
public class X {
X method(X p) {
return p;
}
public class Y extends X {
X a = new X();
Y b = new Y();
b = a.method(b); //エラーが出る
}
}
このままのプログラムではエラーが出てしまいます。なぜか考えてみましょう。
まず、a、bというインスタンスをそれぞれX型、Y型のインスタンスとして生成します。aはX型、bはY型です。
クラスYはXを継承しているので、methodメソッドの型Xの仮引数pの中に代入できます。そしてmethodメソッド内でp、つまり型がYであるオブジェクトであるp(ここではb)を返します。しかし、methodメソッドの返値はX型です。X型に対してY型のオブジェクトを返しているので、勿論エラーが出る訳です。
エラーを出さないためにはメソッドの型を変える必要があります。親クラスXから、子クラスのYに型変換をします。
public class X {
X method(X p) {
return p;
}
public class Y extends X {
X a = new X();
Y b = new Y();
b = (Y)a.method(b); //メソッドの型変換!
}
}
これで、エラーは出ません。型変換はキャスト同様(変換したい型)メソッド名()とします。
定数以外のfinal
final修飾子は変数に付けることによる「定数の宣言」以外にも他に2つほど用法があります。
一つ目が、「オーバーライドの禁止」です。通常であれば子クラスは全てのメソッドをオーバーライドできます。つまり子クラスを作れば全てのメソッドの挙動を変更する事が出来てしまうわけです。しかしながら親クラスのメソッドのオーバーライドを禁止したい!という時があったとします。そんなときにfinalの出番です。メソッドにfinalを付けると、オーバーライドする事を禁止できます。
public class X {
final void method() { //methodメソッドはオーバーライドできない。
...
}
...
}
public class Y extends X {
void method() {
... //methodメソッドをオーバーライドするとエラーが出る。
}
}
二つ目が、「継承の禁止」。通常、どのクラスにも子クラスを定義でき、その中でインスタンス変数やメソッドを追加できます。しかし、独立したクラスとして継承を禁止したい時があるとします。そんなときもfinalの出番です。クラスにfinalを用いると継承を禁止する事ができます。
public final class X { //クラスXの継承を禁止する。(子クラスを定義できない)
...
}
public class Y extends X { //クラスXを継承するとエラーが出る。
...
}
このように、final修飾子は様々な用法で使われます。付けるものによって用途が変化するので整理して覚えておきましょう。
final 変数名; //定数宣言
final メソッド名(){} //メソッドのオーバーライドの禁止
final クラス名 {} //クラスの継承の禁止
Javaにおける継承の落とし穴
さて、便利なクラスの継承の性質をこれまで見てきたわけですが、この継承にも落とし穴、すなわち不便な点が存在します。なんだと思いますか?
それは、継承する際に一つのメソッドしか継承できないということです。プログラムの世界では2つ以上の親クラスを持つことを多重継承というのですが、Javaではこの多重継承の機能はありません。例えば、次のようなプログラムがあったとします。
public class X {
public void methodX() {
...
}
}
public class Y {
public void methodY() {
...
}
}
このクラス X, Y の機能をそれぞれどちらも継承したいクラスTestがあったとします。
public class Test extends X, Y {
...
}
このような書き方を思いつくかもしれませんが、この方法は使うことができません。文法として正しく機能していないためです。
少し賢い人はこのような方法を思いついた人もいるかもしれません。
public class Y extends X {
...
}
public class Test extends Y {
...
}
これだとJavaの文法も守っており、うまく継承できているように思えます。しかしこの方法ではクラスに歪みが生じてしまいます。どういうことかというと、本来であればクラスYが持っていないmethodXを持ってしまうという事です。それぞれのクラスに関連したメソッドしか保有してほしくないクラスを定義する際は、この方法は有効ではありません。
では複数のクラスを継承したい場合どうすればよいのでしょうか?それが、次に話す「インターフェース」を用いた方法で解決できます。
Javaのインターフェースとは
Javaには多重継承に似た文法機能として「インターフェース」なるものが存在します。多重継承は微妙な問題をはらみ、言語使用が複雑になるためあまり好まれません。(C++が難しいと言われている理由のひとつもこれ)
オブジェクト指向の言語研究から生まれたアイデアとして、インターフェースというものが誕生しました。それがJavaに採用されたようです。
インターフェースを使うと継承とは無関係に、1つのクラスに2つ以上の型を持たせることが出来ます。継承の場合、親クラスの型しか持つことができませんが、インターフェースを使うとこの問題を解消できます。
インターフェースに定義できるのは以下の3つのみです。
①定数
②抽象メソッド
③デフォルトメソッド(※)
※補足:デフォルトメソッドはJava8以降に実装された性質
そして、以下の様に宣言します。
interface インターフェース名 {
型名 変数名 = 値; //①
戻り値の型 メソッド名(引数の型); //②
default 戻り値の型 メソッド名(引数の型) { //③
メソッドの定義
}
}
①は自動的に「public static final」となります。
②は自動的に「public abstract」となります。
③は自動的に「public」となります。(static修飾子を付けて、クラスメソッドにすることも可能)
インターフェースで定義できるものは決められているので、自動的にこのように修飾子が定まるわけです。よって省略した記法でインターフェース内部を記述します。
上のクラスXと、クラスYをインターフェースとして再定義するとこのようになります。
interface X {
void methodX();
}
interface Y {
void methodY();
}
インターフェースを実装する
子クラスを作る場合は、親クラスを「継承する」という言い方でしたが、インターフェースでは「実装する」という表現が使われます。次の例は、Testクラスが2つのインターフェース X, Y を実装しているプログラム例です。
public class Test implements X, Y {
public void methodX() {
//メソッドの定義
}
public void methodY() {
//メソッドの定義
}
}
public class クラス名 implements インターフェース1,インターフェース2,...{} //実装方法
methodX, methodYは抽象メソッドなので、インターフェースの実装先のクラス内で再定義(実装)することを忘れてはいけません。
インターフェースのデフォルトメソッド
デフォルトメソッドについては、メソッド本体を定義してあるので、オーバーライドしなくても良いことになっています。しかし、2つ以上のインターフェースに同じシグネチャのデフォルトメソッドがある場合、
1.オーバーライドする
又は
2.明示的にどちらを利用するのか指定する
ということをやらなければなりません。
次のインターフェースを用意します。
public interface A {
default void print() {
System.out.println("Aです");
}
public interface B {
default void print() {
System.out.println("Bです");
}
このインターフェースを実装したクラスを作ってみました。
public class Print implements A, B {
public void print() {
//処理内容は?
}
}
このような場合、Printのインスタンスからprintメソッドを呼び出すとどちらが呼ばれるのでしょうか?
class TestAB {
public static void main(String[] args) {
Print p = new Print();
p.print();
}
}
//実行結果
Aです
こうなります。これは、実装されたインターフェースに呼び出しの優先順位がついているためです。Bのインターフェースのデフォルトメソッドを呼び出すには次のように、明示的に指定する必要があります。
public void print() {
B.super.print() //明示的な指定
}
//実行結果
Bです
A, B 以外の処理をしたければ、デフォルトメソッドを実装先のクラスでオーバーライドすることでその問題は解決されます。
インターフェースの型
クラスの継承の部分でも触れましたが、クラスが型であるのと同様、インターフェースも型になります。上の例でいうと、class TestABは型Aにもなれるし、型Bにもなり得ます。
おさらい
以下に目次を再掲してみました。
Javaを学ぶ上で心得ておくこと
Javaとは
Javaで単純なプログラムを作成する
Javaにおけるメソッドとは
Javaで使える基本型
Javaの基本的な演算子
オブジェクト指向についてもう少し
Javaでインスタンスを生成してみる
インスタンスの操作とインスタンス変数
Javaの配列について学ぶ
配列の使用方法
クラスの「情報隠蔽」
基本型と参照型
基本型と参照型の違いについて押さえる
実引数と仮引数
文字列はクラスのひとつ!?
等号演算子「==」の注意点
カプセル化について
メソッドのオーバーロード
メソッドの多重定義による注意点
コンストラクタについて学ぶ
コンストラクタのオーバーロード
デフォルト・コンストラクタ
クラス変数
定数宣言
クラスメソッドについて学ぶ
クラスの継承
サブクラスのコンストラクタ
superメソッドの罠
protected修飾子について
継承と型
メソッドのオーバーライド
抽象クラスと抽象メソッド
superによる親クラスメソッドの呼び出し
インスタンスの型変換
定数以外のfinal
Javaにおける継承の落とし穴
Javaのインターフェースとは
インターフェースを実装する
インターフェースのデフォルトメソッド
インターフェースの型
いかがでしょうか。目次を見て、「ああ、こんなのもあったな」とか、「ここはこういう内容だったな」とか思えるレベルになれば基礎文法の殆どは終了しています。分からない部分や理解できていない部分があれば、もう一度後ろに戻って確認してみて下さい。
おわりに
Javaは勉強し始めてあまり期間も経っておらず、時間もそんなに割いていないのですがなかなか奥深く面白い言語です。僕もJavaに関しては勉強中なので誤字脱字などあったら申し訳ありません。できるだけ、丁寧かつシンプルに文法を解説したつもりです。
Javaはその拡張性の広さから人気が衰えない言語の1つで、まだまだ需要があります。その上オブジェクト指向についても学ぶことができます。
プログラミングを学ぶ上でこのような静的言語を学ぶのは非常に理解が深まり、他のプログラミング言語の学習速度を上げる一つの効果的な手法だと思っています。
最後まで読め、理解できた方は是非他のプログラミング言語にも触れてみるか、このままJavaを極めて行くのも良いでしょう。
拙い文章でしたが、このnoteが誰かの役に立てていれば幸いです。リファレンス的にも使えるので、基礎文法を忘れた際には読み終わった後でも活用すると良いかと思います。
この記事が気に入ったらサポートをしてみませんか?