M.Hiroi's Home Page
http://www.geocities.jp/m_hiroi/

お気楽 Java プログラミング入門

第 5 回 オブジェクト指向の基礎知識

[ PrevPage | Java | NextPage ]

はじめに

プログラミングに興味のある方ならば、オブジェクト指向という言葉は聞いたことがあると思います。よく使われているオブジェクト指向プログラミング言語にC++があります。C++はオブジェクト指向プログラミングができるようにC言語を拡張したものですが、度重なる機能追加により複雑な言語仕様になってしまいました。このため、初心者がオブジェクト指向を学ぶには適していないと言われています。

Java のオブジェクト指向はC++よりも簡単だといわれていますが、C++と同じようにバージョンアップするたびに新しい機能が追加されるので、Java のオブジェクト指向機能もかなり複雑になりつつあります。初心者 (M.Hiroi を含む) からみると、どちらのオブジェクト指向も大変難しい、と思われている方が多いのではないでしょうか。オブジェクト指向を学ぶには Ruby のようなスクリプト言語のほうが適しているのかもしれません。

困ったことに、Java はオブジェクト指向プログラミング言語なので、オブジェクト指向を避けて通るわけにはいきません。そこで、簡単なプログラムを作りながら、少しずつステップアップしていきましょう。まずは最初に、一般的なオブジェクト指向について簡単に説明します。

●オブジェクトとは?

プログラムを作る場合、全体を小さな処理に分割して、一つ一つの処理を作成し、それらを組み合わせて全体のプログラムを完成させます。このとき、基本的な部品となるのが関数です。つまり、処理を関数単位で分割して、それらを組み合わせてプログラムを作るわけです。もともと関数の役割は、入力されたデータを処理してその結果を返すことです。つまり、関数は機能を表しているのです。このため、全体を小さな処理に分割するにしても、機能単位で行われることが普通です。

オブジェクト指向プログラミングでは、関数ではなく「オブジェクト (object)」を部品として扱います。たとえば、えんぴつを考えてみましょう。えんぴつには、色、長さ、固さ、などいろいろな性質があります。そして、えんぴつを使って文字を書いたり、絵を描いたりすることができます。プログラムでは、このような性質をデータで表し、機能を関数で表すことになります。オブジェクトとは、このデータと関数を結び付けたものと考えてください。

いままでのプログラミング言語では、データと関数を別々に定義するため、それを一つのオブジェクトとして表すことができません。えんぴつで文字を書くにも、えんぴつの種類をチェックして文字を書くようにプログラムしなければいけません。ところが、オブジェクトはデータと関数を結び付けたものなので、自分がなにをしたらよいかわかっています。えんぴつオブジェクトに文字を書けと命じれば、それが赤えんぴつのオブジェクトであれば文字は赤に、黒えんぴつのオブジェクトであれば黒い文字になるのです。

このように、オブジェクトはデータと関数を一つにまとめたものです。従来のプログラミングが全体を機能単位で分割するのに対し、オブジェクト指向プログラミングでは全体をオブジェクト単位に分割して、それを組み合わせることでプログラムを作成します。

ところで、データと関数を結び付けることは、従来のプログラミング言語でも可能です。オブジェクト指向はプログラミングの考え方の一つであり、C++のようなオブジェクト指向言語を使わなくても、たとえばC言語でもその考え方にしたがってプログラムを作成すれば、オブジェクト指向プログラミングになります。

実際、オブジェクト指向には様々な考え方があり、いろいろなオブジェクト指向プログラミング言語が存在します。ですが、データと関数を一つにまとめたものをオブジェクトとして扱うという基本的な考え方は、オブジェクト指向言語の元祖と言われる Smalltalk でも、C++ や Java でも同じです。

●クラスとインスタンス

次は、一般的なオブジェクト指向機能について簡単に説明します。

「クラス (class)」はオブジェクトの振る舞いを定義したものです。ここでデータを格納するための変数や、それを操作する関数が定義されます。Java はこの変数を「フィールド変数」といいます。他の言語ではメンバ変数とかインスタンス変数と呼ぶことがあります。そして、クラスの中で定義された関数を「メソッド (method)」といいます。メソッドはあとで説明します。

クラスはオブジェクトの設計図にあたるもので、オブジェクトの「雛形」と呼ぶこともあります。クラスはオブジェクトの振る舞いを定義するだけで、アクセスできる実体はなにも生み出していない、ということに注意してください。ただし、プログラミング言語によってはクラスに実体を持たせていることもあります。Java のフィールド変数やメソッドを static 宣言すると、クラスに直接アクセスすることができます。

このクラスから実体として作り出されるのが「インスタンス (instance)」です。このインスタンスを「オブジェクト」と考えてください。インスタンスを生成する方法は、当然ですがプログラミング言語によって違います。たとえば C++や Java は new を使います。図 1 を見てください。

┌─ class Foo  ─┐                        ┌─ instance ─┐
│                │                        │              │
│     設計図     │─ インスタンスの生成 →│   実体       │
│                │                        │              │
└────────┘                        └───────┘
          │
          │
          │                                ┌─ instance ─┐
          │                                │              │
          └───── インスタンスの生成 →│   実体       │
                                            │              │
                                            └───────┘

             図 1 : クラスとインスタンスの関係

クラスはオブジェクトの定義を表すものですから、Foo というクラスは一つしかありません。これに対し、インスタンスはクラスから生み出されるオブジェクトです。たとえば、クラス Foo に new を適用することで、いくつでもインスタンスを生み出すことができるのです。クラスは設計図であり、それに従って作られるオブジェクトがインスタンスと考えるとわかりやすいでしょう。

●メソッド

メソッドはオブジェクトと結びついた関数です。オブジェクト指向プログラミングでは、ほかの関数から直接オブジェクトを操作することはせず、メソッドを呼び出すことで行います。メソッドは、クラスが異なっていれば同じ名前のメソッドを定義することができます。たとえば、クラス Foo1 にメソッド bar() が定義されていても、クラス Foo2 に同名のメソッド bar() を定義することができます。

そして、ここからが重要なのですが、あるオブジェクトに対してメソッド bar() を呼び出した場合、それが Foo1 から作られたオブジェクトであれば、Foo1 で定義された bar() が実行され、Foo2 から作られたオブジェクトであれば、Foo2 で定義された bar() が実行されるのです。このように、オブジェクトが属するクラスによって、実行されるメソッドが異なるのです。この機能を「ポリモーフィズム(polymorphism)」と呼びます。これにより、オブジェクトは自分が行うべき適切な処理を実行できるわけです。

クラス、インスタンス、メソッドの関係は図 2 のようになります。

┌─ class Foo1 ─┐                    ┌─ instance ─┐
│                │                    │              │
│    設計図      │─── 生成 ───→│   実体       │
│                │                    │              │
│                │                    └───────┘
│┌─ method ─┐│                          ↑
││            ││                          │
││   bar()←─┼┼─── アクセス ─────┘
││            ││
│└──────┘│
└────────┘

       図 2 : クラス、インスタンス、メソッドの関係

クラスという設計図が中心にあり、そこからインスタンスが生み出され、メソッドを使ってインスタンスを操作する、という関係になります。

●Java のクラス

さて、一般的な話はここまでにして、Java のオブジェクト指向機能に目を向けてみましょう。Java は class 文でクラスを定義します。class 文の構文を図 3 に示します。

class ClassName extends SuperClassName {
    ...
}

      図 3 : class 文の構文

class の次にクラス名を指定し、その次の extends で他のクラスを指定すると、そのクラスの機能を引き継ぐことができます。この機能を「継承 (inheritance)」といいます。継承は次回以降で詳しく説明します。extends を省略すると暗黙のうちに Object というクラスが継承されます。Java のすべてのクラスは直接的もしくは間接的に Object を継承しています。そのあとのブロック { } の中でフィールド変数やメソッドを定義します。

class の前には public を付けることができます。public 宣言されたクラスは他のどのクラスからでも使用することができます。無指定の場合は、同じファイル内のクラスからしか使用することができません。

一番簡単なクラス定義を示しましょう。リスト 1 を見てください。

リスト 1 : クラス定義

class Foo { }

public class sample50 {
  public static void main(String[] args) {
    Foo a = new Foo();
    System.out.println(a);
  }
}

一般に、クラス名は英大文字から始めることが多いので、名前は Foo としました。Foo はクラス名しかありませんが、これでも立派なクラスなのです。実行結果は次のようになります。

C>java sample50
Foo@1db9742

Java は new を付けて Foo() と呼び出すと、そのクラスのインスタンスを生成して返します。Foo() を「コンストラクタ」といって、呼び出すときに引数を指定することができます。これはあとで説明します。

ところで、Foo のインスタンスを println() で出力していますが、これは暗黙のうちに Foo が Object を継承しているからできることなのです。println() はオブジェクトを文字列に変換するときメソッド toString() を呼び出します。ところが、Foo には定義されていないので、スーパークラス Object の toString() を呼び出して変換し、それを画面へ出力しています。

●Java のインスタンス

Java はクラス内で宣言された変数をフィールド変数として扱います。他の言語ではインスタンス変数とかメンバ変数といいます。フィールド変数の実体はインスタンスに割り当てられます。static を付けるとクラス変数として扱われるため、インスタンスには割り当てられません。クラス変数についてはあとで説明します。

Java のフィールド変数は public, protected, private という 3 通りのアクセス制御を行うことができます。public はどのクラスからでもアクセスすることできます。protected は同じクラスとそれを継承したクラス (サブクラス) からアクセスすることができ、private は同じクラスからしかアクセスすることができません。指定を省略した場合、同じファイル内で定義されたフィールド変数であれば、異なるクラスであってもアクセスすることができます。

フィールド変数のアクセスは次の形式で行います。

object.variable

インスタンス object の後ろにドット ( . ) を付けて、その後ろにフィールド変数名を指定します。同じクラス内であれば、object を省略してフィールド変数名だけでアクセスすることができます。簡単な例を示しましょう。

リスト 2 : フィールド変数のアクセス

class Foo {
  int x = 1;
}

public class sample51 {
  public static void main(String[] args) {
    Foo a = new Foo();
    Foo b = new Foo();
    System.out.println(a.x);
    System.out.println(b.x);
    a.x = 10;
    b.x = 100;
    System.out.println(a.x);
    System.out.println(b.x);
  }
}
C>java sample51
1
1
10
100

クラス Foo のインスタンスを生成して変数 a と b にセットします。インスタンス a, b にあるフィールド変数 x は、それぞれ a.x, b.x でアクセスすることができます。a.x = 10 とすると a の x に 10 が代入され、b.x = 100 とすると b の x に 100 が代入されます。

ただし、オブジェクト指向でプログラムを作る場合、フィールド変数に直接アクセスすることはあまり行われません。フィールド変数の値を参照するメソッド (リーダー: reader) と値を更新するメソッド (ライター: writer) を用意して、それらのメソッドを経由してアクセスするのが一般的で、お行儀の良いプログラミングスタイルとされています。

●Java のメソッド

Java はクラス内で定義された関数をメソッドとして扱います。static を付けるとクラスメソッドになります。インスタンスを操作するメソッドを定義する場合は static を付けないよう注意してください。Java のメソッドは public, protected, private という 3 通りのアクセス制御を行うことができます。これはフィールド変数のアクセス制御と同じです。指定を省略した場合のアクセス制御もフィールド変数と同じです。

簡単な例として、クラス Foo にフィールド変数 x のリーダーメソッドとライダーメソッドを定義しましょう。次のリストを見てください。

リスト 3 : リーダーメソッドとライターメソッド

class Foo {
  int x = 1;
  // リーダーメソッド
  int getX() { return x; }
  // ライターメソッド
  void setX(int n) { x = n; }
}

public class sample52 {
  public static void main(String[] args) {
    Foo a = new Foo();
    Foo b = new Foo();
    System.out.println(a.getX());
    System.out.println(b.getX());
    a.setX(20);
    b.setX(200);
    System.out.println(a.getX());
    System.out.println(b.getX());
  }
}
C>java sample52
1
1
20
200

メソッド getX() がリーダーで、setX() がライターです。Java の場合、リーダーメソッドには接頭辞に get を、ライターメソッドには set を付けることが一般的なようです。これらのメソッドは static を付けていないので、インスタンスを操作する「インスタンスメソッド」になります。インスタンスメソッドは次の形式で呼び出します。

object.method(args, ...)

インスタンス object の後ろにドット ( . ) を付けて、メソッド名と引数を続けて書きます。同じクラス内であれば、object を省略してメソッド名だけで呼び出すことができます。

●コンストラクタ

インスタンスを生成するとき、フィールド変数に初期値を代入できると便利です。Java はクラスと同じ名前のメソッドが定義されていると、インスタンスを生成するときにそのメソッドを呼び出して、インスタンスの初期化処理を行うことができます。このメソッドを「コンストラクタ (constructor)」といいます。

コンストラクタの構文を次に示します。

アクセス制御 クラス名(データ型 引数, ...) {
  処理;
  ...
}

コンストラクタは値を返さないので、返り値のデータ型を指定する必要はありません。あとは通常のメソッドと同じです。もちろん、多重定義も可能です。次のリストを見てください。

リスト 4 : コンストラクタ

class Foo {
  int x;
  // コンストラクタ
  Foo() { x = 1; }
  Foo(int x) { this.x = x; }
  // リーダーメソッド
  int getX() { return x; }
  // ライターメソッド
  void setX(int x) { this.x = x; }
}

public class sample53 {
  public static void main(String[] args) {
    Foo a = new Foo();
    Foo b = new Foo(2);
    System.out.println(a.getX());
    System.out.println(b.getX());
    a.setX(10);
    b.setX(100);
    System.out.println(a.getX());
    System.out.println(b.getX());
  }
}
C>java sample53
1
2
10
100

引数なしのコンストラクタと引数が一つのコンストラクタを定義します。引数なしのコンストラクタでは、フィールド変数 x を 1 に初期化します。引数が一つのコンストラクタでは、引数 x の値でフィールド変数 x を初期化します。

メソッドの局所変数とフィールド変数名が同じ場合、Java は局所変数のアクセスを優先します。フィールド変数にアクセスする場合は this を付けてください。this はそのメソッドを呼び出したインスタンスを表す特別な変数です。コンストラクタ Foo(int x) とメソッド setX(int x) の仮引数は x なので、その値をフィールド変数 x に代入するには this.x = x とします。

変数 a のインスタンスは、コンストラクタ Foo() で生成されるので x の値は 1 になります。変数 b のインスタンスは Foo(2) で生成されるので x の値は引数で渡された 2 になります。

コンストラクタが未定義の場合、引数なしで何も処理しないコンストラクタが自動的に生成されます。これをデフォルトコンストラクタといいます。コンストラクタが一つでも定義されていると、デフォルトコンストラクタは自動的に生成されません。たとえば、コンストラクタ Foo(int n) { ... } を一つだけを定義すると、引数なしのコンストラクタは未定義になるので、Foo() の呼び出しはコンパイルエラーになります。ご注意ください。

ところで、コンストラクタ、リーダーメソッド、ライターメソッドをまとめて「アクセスメソッド」といいます。フィールド変数に直接アクセスせず、アクセスメソッドを経由してデータの参照や代入を行うことを「データ抽象」とか「カプセル化」といいます。わざわざアクセスメソッドを用意するのは面倒なようですが、そのことによりクラスの詳細な内容をアクセスメソッドで包み隠すことができます。それだけプログラムも読みやすくなり、修正にも強いプログラムを作ることができます。

●Point クラス

簡単な例として、点を表すクラスを作ってみましょう。名前は Point にしました。x 座標をインスタンス変数 x に、y 座標を変数 y に格納します。リスト 5 を見てください。

リスト 5 : Point クラス

class Point {
  double x = 0.0, y = 0.0;
  // コンストラクタ
  Point() { }
  Point(double x, double y) {
    this.x = x;
    this.y = y;
  }
  // 距離を求める
  double distance(Point p) {
    double dx = x - p.x;
    double dy = y - p.y;
    return Math.sqrt(dx * dx + dy * dy);
  }
}

public class Point0 {
  public static void main(String[] args) {
    Point p1 = new Point();
    Point p2 = new Point(10.0, 10.0);
    System.out.println(p1.distance(p2));
  }
}

メソッド distance() は Point クラスのインスタンス p を受け取り、その距離を計算します。sqrt() は平方根を求めるメソッドで、パッケージ java.lang のクラス Math に定義されています。このほかにも Math には便利な数学関数が多数用意されています。

あとは Point のインスタンスを生成して、変数 p1, p2 にセットして p1.distance(p2) で p1 と p2 の距離を計算します。実行結果は次のようになります。

C>java Point0
14.142135623730951

ここで、インスタンスメソッドの呼び出しは、インスタンスによって適切なメソッドが選択されることに注意してください。たとえば、3 次元の座標を表す Point3D クラスを考えてみましょう。リスト 6 を見てください。

リスト 6 : Point3D クラス

class Point {
  double x = 0.0, y = 0.0;
  // コンストラクタ
  Point() { }
  Point(double x, double y) {
    this.x = x;
    this.y = y;
  }
  // 距離を求める
  double distance(Point p) {
    double dx = x - p.x;
    double dy = y - p.y;
    return Math.sqrt(dx * dx + dy * dy);
  }
}

class Point3D {
  double x = 0.0, y = 0.0, z = 0.0;
  // コンストラクタ
  Point3D() { }
  Point3D(double x, double y, double z) {
    this.x = x;
    this.y = y;
    this.z = z;
  }
  // 距離を求める
  double distance(Point3D p) {
    double dx = x - p.x;
    double dy = y - p.y;
    double dz = z - p.z;
    return Math.sqrt(dx * dx + dy * dy + dz * dz);
  }
}

public class Point1 {
  public static void main(String[] args) {
    Point p1 = new Point();
    Point p2 = new Point(10.0, 10.0);
    Point3D p3 = new Point3D();
    Point3D p4 = new Point3D(10.0, 10.0, 10.0);
    System.out.println(p1.distance(p2));
    System.out.println(p3.distance(p4));
  }
}

クラス Point3D は Point を 3 次元に拡張しただけです。Point でも Point3D でも距離を計算するメソッド distance() が定義されていることに注目してください。それでは、メソッド distance() を呼び出してみましょう。

C>java point1
14.142135623730951
17.320508075688775

ドットの左側のインスタンス p1, p3 によって適切なメソッドが呼び出され、ポリモーフィズムが働いているようにみえます。Perl, Python, Ruby などのように、変数を使用するときデータ型の宣言が不要なプログラミング言語を「動的型付け言語」といい、C/C++や Java などのように変数を使用するときデータ型の宣言が必要なプログラミング言語を「静的型付け言語」といいます。

たとえば Ruby の場合、p1.distance(p3) で呼び出されるメソッド distance() は、プログラムを実行するとき変数 p1 に格納されているオブジェクトの型 (クラス) によって決定されます。p1 が Point のインスタンスであれば、Point で定義されたメソッド distance() が呼び出されます。つまり、動的型付け言語のメソッド呼び出しはポリモーフィズムが働いている、と考えることができます。

これに対して、C/C++や Java などの静的型付け言語は、コンパイルの時点で呼び出すメソッドを可能な限り決定します。たとえば、p1.distance(p2) の呼び出しは Point クラスのメソッドで、p3.distance(p4) は Point3D クラスのメソッドと決めることが可能です。この場合、動的型付け言語のようなポリモーフィズムは働いていませんが、オブジェクトのクラスによって呼び出すメソッドが決定される、ということにかわりはありません。

Java でプログラムの実行時にポリモーフィズムを働かせるには「継承」もしくは「インターフェース」という機能を使います。これは次回以降で説明します。

●クラス変数

インスタンス変数は個々のインスタンス(オブジェクト)に割り当てられる変数です。その値はインスタンスによって変わります。クラスで共通の変数や定数を使いたい場合は、class 文の中で static 変数を定義します。これを「クラス変数」といいます。簡単な例を示しましょう。

リスト 7 : クラス変数

class Foo {
  int x = 1;
  static int y = 2;
  // コンストラクタ
  Foo() { }
  Foo(int x) { this.x = x; }
  // アクセスメソッド
  int getX() { return x; }
  void setX(int x) { this.x = x; }
}

public class sample54 {
  public static void main(String[] args) {
    Foo a = new Foo();
    Foo b = new Foo(10);
    System.out.println(Foo.y);
    System.out.println(a.y);
    System.out.println(b.y);
    Foo.y = 20;
    System.out.println(Foo.y);
    System.out.println(a.y);
    System.out.println(b.y);
    a.y = 30;
    System.out.println(Foo.y);
    System.out.println(a.y);
    System.out.println(b.y);
  }
}
C>java sample54
2
2
2
20
20
20
30
30
30

変数 y は static 宣言されているのでクラス変数になります。クラス変数は次のようにアクセスすることができます。

class.variable
object.variable

class はクラス名、object はインスタンス、vaiable はクラス変数名を表します。クラス変数はインスタンスからでもアクセスすることができます。インスタンスを生成して変数 a, b にセットします。a.y と b.y とすると、クラス変数 y にアクセスします。Foo.y = 20 とすると、a.y と b.y の値も 20 になります。a.y = 30 とすると、Foo.y と b.y の値も 30 になります。

●final 修飾子

フィールド変数に final 修飾子を付けると、その変数は初期化されたあと値を書き換えることはできなくなります。つまり、定数として扱うことができます。簡単な例を示しましょう。

リスト 8 : fianl 修飾子

class Foo {
  static final int x = 100;
}

public class sample55 {
  public static void main(String[] args) {
    System.out.println(Foo.x);
    Foo.x = 200;    // コンパイルエラー
  }
}

クラス変数 x は final 宣言されているので定数として扱われます。クラス変数 x の値を参照することはできますが、値を代入するコードは次のようにコンパイルエラーになります。

C>javac sample55.java
sample55.java:8: エラー: final変数xに値を代入することはできません
    Foo.x = 200;    // コンパイルエラー
       ^
エラー1個

●クラスメソッド

メソッドは個々のインスタンスを操作する関数です。一般に、ユーザが定義するメソッドは引数のインスタンスを操作対象とし、クラスの動作にかかわることはありません。インスタンスを操作するメソッドを「インスタンスメソッド」といいます。これに対し、クラスの動作にかかわるメソッドを考えることができます。これを「クラスメソッド」といいます。Java はメソッドを static 宣言するとクラスメソッドになります。

たとえば、クラス Foo のクラス変数 y を操作するクラスメソッド getY(), setY() を作りましょう。次のリストを見てください。

リスト 9 : クラスメソッド

class Foo {
  int x = 1;
  static int y = 2;
  // クラスメソッド
  static int getY() { return y; }
  static void setY(int y) { Foo.y = y; }  // this は参照できない
  // コンストラクタ
  Foo() { }
  Foo(int x) { this.x = x; }
  // アクセスメソッド
  int getX() { return x; }
  void setX(int x) { this.x = x; }
}

public class sample56 {
  public static void main(String[] args) {
    Foo a = new Foo();
    Foo b = new Foo(10);
    System.out.println(Foo.getY());
    System.out.println(a.getY());
    System.out.println(b.getY());
    Foo.setY(20);
    System.out.println(Foo.getY());
    System.out.println(a.getY());
    System.out.println(b.getY());
    a.setY(30);
    System.out.println(Foo.getY());
    System.out.println(a.getY());
    System.out.println(b.getY());
  }
}
C>java sample56
2
2
2
20
20
20
30
30
30

クラスメソッドの呼び出しは次のように行います。

class.method(args, ...)
object.method(args, ...)

class はクラス名、object はインスタンス、method() はクラスメソッドを表します。クラスメソッドはインスタンスからでも呼び出すことができます。インスタンスを生成して変数 a, b にセットします。Foo.getY(), a.getY(), b.getY() のどれでもクラスメソッド getY() を呼び出すことができます。

なお、クラスメソッド内で this を参照することはできません。局所変数とクラス変数が同じ名前の場合は、クラス名を指定してクラス変数にアクセスしてください。

初版 2009 年 4 月 18 日
改訂 2016 年 11 月 12 日

Copyright (C) 2009-2016 Makoto Hiroi
All rights reserved.

[ PrevPage | Java | NextPage ]