オブジェクト & 3次元CG(その2)


          
   


 Javaは オブジェクト指向言語の代表的なものである。その「オブジェクト」とは何であろうか? 今回は、そのことを中心に学びたい。
 また、グラフィックに関しては、3次元CG入門のその2として、空間における回転について学ぶ。

 オブジェクトとは

 今回はやっと、Java の Java らしい一面である「オブジェクト」という概念について学ぶ。これは、BASIC, FORTRAN, C といった「手続き型」プログラミング言語や、LISP などの「関数型」プログラミング言語、あるいは Prolog などに代表される「論理型」プログラミング言語とは言語設計の思想がまったく異なる、「オブジェクト指向型」と総称されるプログラミング言語を特徴付ける本質的な一面である。
 オブジェクトというものの基本が理解できたら、すでに基本を学んだ「配列」と「文字列」についても、オブジェクトという観点から再度見なおしてみよう。これらはデータ型ではなく、オブジェクトの一種である(これらは、「参照型」という概念で統一される)。

 もっと詳しいことは ここの後半(§1.3)ここ


 オブジェクト指向とは
 「オブジェクト指向」は object-oriented の訳語である。すなわち、オブジェクトというものに「適合する」、オブジェクトというものに「向いている」、オブジェクトを「ベースにしている」という意味である。したがって、オブジェクトとは何かということがわからないと、オブジェクト指向型言語の(したがって Java の)本質は理解できないし、なぜ「そのように書くのか」に対する理屈も理解できない。

 オブジェクト指向プログラミング(object-oriented programming)では、プログラムが対象としようとしている世界に属す「もの」(=オブジェクト、object)を中心に据えて、それらの特徴を記述することがプログラミングである。
 例えば、レストランという世界を考えよう。レストランでは個々の客、個々のウェイトレス、個々のコックなどがそれぞれオブジェクトであり、個々の客やウェイトレスやコックは誰も決められた一定の仕事(動作)をする(言い換えれば、それぞれに特徴的な独自の機能=役割を持っている)。例えば、客は料理の注文をし、ウエイトレスは客からの注文をコックに伝えたり、調理が終った料理を客に出したりし、コックは料理を作る。このような機能(動作)のそれぞれを Java ではメソッド(method)という。
 一方、こういった仕事をするためにはいろいろな種類の道具や材料(コックは鍋や包丁など、ウエイトレスはメニューや注文伝票やPDAなど)が必要であるし、個々のウエイトレスや個々の客や個々のコック(すなわち、個々のオブジェクト)を区別する名前や年齢、性別などの性質・特性(属性、attribute)を持っている。このような属性を保持するもの(変数)を Java ではフィールドという(他のオブジェクト指向言語ではデータメンバーとかメンバー変数とか、単にメンバーとか、スロットともいう)。

 こういったオブジェクトの属性・性質、すなわち、仕事・動作(機能・能力)や道具(情報・データ)をきちんと決めることによって、抽象的概念としての「客」とか「ウエイトレス」とか「コック」といったものを特徴付けることができる。 オブジェクト指向言語では、

 クラスは一種のテンプレート(template、雛形、鋳型)であると考えることもできる。 各クラスの具体的オブジェクトは、その鋳型を使って生成する。このようなことをインスタンス化(instantiation)という。

 例えば、レストランにおいては、客、ウェイトレス、コックという3つのクラスがあり、各クラス(例えば、ウェイトレスというクラス)に具体的に A, B, C という3人のウェイトレスがいるならば、A, B, C それぞれはウェイトレスというクラスのインスタンス(オブジェクト)である。客 D (客というクラスのオブジェクトの1つ)がウェイトレス C というオブジェクトに渡すメッセージは注文する料理であり、そのメッセージを受けてウェイトレスはコックに料理の注文を出す(この動作がウェイトレスというオブジェクトが備えているメソッドの1つである)。客は、ウェイトレスがどのような知識を持っているかとか、受けた注文をどのようにコックに伝えるのかは知らない(知る必要がない)。
 このように、あるオブジェクトの持っている情報・データや機能が外部から分からないようにすること(情報隠蔽)をカプセル化(capsulation)といい、これもオブジェクト指向言語がもつ特徴の1つである。外部から見えてもよい情報(クラス、メソッド、フィールドなど)には public という属性を与え、外部から見えないようにしたい情報には private という属性を与える。属性 についてはすでに学んだ。


 データ型の拡張としてのクラス

 Java のデータ型の基本についてはすでに学んだ:

  基本型
    論理型      boolean
    数値型
      整数型    byte, short, int, long

      文字型    char
      浮動小数点型 float, double
  参照型        省略(ここを参照のこと)
  null型        同上
 先ずここでは、Java のクラスという概念をデータ型の一般化として捉える視点からオブジェクトについて説明する。

 Java をはじめとするオブジェクト指向型言語では、データ型という概念はもっと一般化されてクラスというものになり、個々のデータ型の値を取る '変数' という「もの」もオブジェクトという概念に一般化される。すなわち、オブジェクトは、変数のように単に一定のタイプのデータを保持できるというだけでなく、複数のタイプのデータを同時に持つことができるし、一定の機能(つまり、何らかの処理を実行できる能力)もいくつか併せ持っている。したがって、オブジェクトは単なる「変数」以上に一般的な「もの」であるが、抽象的な概念ではなく、具体的に存在する「もの」である
例えば、文字列 "abc" や "uvwxyz" はそれぞれオブジェクトである。一方、文字列全体(あるいは別の言い方をすれば、文字列という概念)のことを Java では String クラスと言う。つまり、 "abc" や "uvwxyz" などの個々の文字列はどれも String クラスのオブジェクトである。このように、クラスは、ある一定のタイプのデータを保持できると同時に一定の機能を有したオブジェクト全体を呼ぶ呼称であり、そういったオブジェクトとは何かということを特徴付ける抽象的な概念である



 文字列(String クラス)

 我々はすでに前回見たように、Java アプリケーションにおける main 関数の引数としてや入出力の際に String クラスを使っている。String クラスは文字列を扱うクラスであり、

    java.lang    
の中の1つのクラスとして定義されている。 java.lang パッケージは Java のパッケージの中でも最も基本的なクラスの定義を含んでいるものなので、わざわざ
    import java.lang.*;    
と書かなくても暗黙に取り込まれる(よって、書かなくてもよい)ことになっている。

@ String クラスのオブジェクトの宣言の仕方

 String はクラスであり、クラスはデータ型の一般化であるから、int 型の変数 n を

    int n;  
と宣言するのと同様に、s が String クラスのオブジェクトであることを "宣言" するには
    String s;                 // @
のように書く(オブジェクトは変数の一般化であることに注意)。
ただし、宣言しただけではオブジェクトは生成されない(s は文字列の名前だよ、ということが宣言されるだけであって、s が値として持つ具体的な文字列は生成されない)。配列の宣言を思い出すとよい(やはり、宣言だけでは配列の実体は生成されなかった)。

A String クラスのオブジェクトの初期化/生成の仕方

    String s;
のように、すでに宣言されている String オブジェクト s の実体を生成する(すなわち、s を初期化して、具体的文字列を値として持たせる)ためには、配列の生成・初期化と同様に
    new演算子  
を使って次のように書く:
    s = new String("abcd");   // A
これも、やはり配列の初期化の場合と同様に、new を省略して、
    s = "abcd";               // A’
と書いてもよい。

B String クラスのオブジェクトの宣言と初期化・生成を同時に行なうには

 やはり配列の場合と同様に、宣言@と初期化・生成A(またはA’)を一緒にして

    String s = new String("abcd");   // B
と書くことや、さらに new を省略して
    String s = "abcd";               // B’
と書くことも許されている。

● 文字列についての詳細は、「Javaの基本(9) オブジェクト超入門」 文字列 の項を参照のこと。 を参照のこと。



 クラスとオブジェクト

 これまで何度も述べてきたように、オブジェクト指向言語の観点から言うと、クラスは、なんらかの一定のまとまりのある "もの" がどんなものであるかを抽象化したもの(すなわち、概念を具体化したもの)である
 一方、クラスは "データ型" の概念を一般化したものでもある。 データには文字(char)、整数(byte, int, long)、実数(float, double)、論理値(boolean)、参照(ポインタ)などのような基本的なものから、それらをもとにして定義される配列(同じ型のデータの集まり)や文字列(String, 文字の並び)のようなものだけに限らず、さらに複雑な構造のものがある。
例えば、型が異なるデータの集まり(一般に、そのようなものをレコード(record)と呼ぶ)は配列という概念を拡張したものであるし、スタック、キュー、リスト、木、グラフ、等々のいわゆるデータ構造(data structure)と呼ばれるものは、レコードという概念をさらに拡張したもの(データ構造=データの集合+その上の操作の集まり)であり、こういったものはすべて "データ型" という概念の一般化である(データ型という概念を抽象化したものなので、抽象的データ型(abstract data type)とも言う)。
一般に、レコードを構成する要素のことをフィールド(項目)と呼ぶが、クラスの構成要素の一つであるフィールドという名前(後述する)はここに由来する。

 クラスは、このように2つの側面(同じものを違う方向から見たもの)をもつ。

 オブジェクトとは何か、クラスとは何か、オブジェクト(抽象的なものを具体化して生成するものなので、インスタンス(instance, 「具体例」という意)ともいう)を生成して使うとはどういうことか、といったことを学びたい。さらに、自分でクラスを定義するにはどのように書けばよいのかを学び、そのクラスのオブジェクトを生成して使ってみたい。これによって、Java のオブジェクト指向的側面の基本的なところを自分で初めて体験することができる。


 クラスの書き方

 一般に、クラスとはどう書くべきものなのかをここであらためて述べる。クラスの一般的な形は

    クラス修飾子 class クラス名 {
        フィールド宣言
        コンストラクタ宣言
        メソッド宣言
    }               
である。
  1. フィールドの宣言、コンストラクタの宣言、メソッドの宣言、の「宣言」は「定義」のことである。
  2. フィールドの宣言、コンストラクタの宣言、メソッドの宣言を書く順序は任意である。ただし、この順序で書くことが多い(意味的に、そのようになることが多いため)。
  3. フィールドの宣言、コンストラクタの宣言、メソッドの宣言はあってもなくてもよい。例えば、フィールド宣言しかなくてもよいし、どれもなくてもよい。
  4. クラス修飾子は public, final, abstract のどれか(詳細は後述)。1つも付けなくてもよい(これまで作ったプログラム例では、修飾子は必ず public とした)。
  5. フィールドの宣言の仕方と、メソッドの定義の仕方の基本はすでにやった通り。ただし、これらにも public, private, static, final などの修飾子を付けることがある(これまでは、すべてのメソッドに public を付けてきた)。その違いは以下で述べる。
  6. コンストラクタ(constructor)は、その名の通り、このクラスのオブジェクトを生成する(作る=construct)ためのメソッドである。書き方は以下で述べる。
 @ クラス修飾子

 すでに学んだように、クラス修飾子は public, abstract, final のいずれか。修飾子は、クラス名の前に付けてクラスの性格の一端を指定するほか、フィールドやメソッドの前にも付けて同様にその性格の一端を指定する。
 修飾子の種類とそのおよその意味を次の表に示す。

修飾子の種類 キーワード 意味(機能)
アクセス修飾子 なし 同一パッケージ内でのみ利用可能
private 同じクラス内でのみ使用可能
protected 同一パッケージとサブクラスで使用可能
public どこからも使用可能
メソッドタイプ static クラス固有のメソッドとする
abstract 実装を含まない抽象的メソッドとする
final サブクラスでオーバーライド(上書き変更)不可のメソッドとする
synchronized オブジェクトの同期を行うメソッドとする
native 特定のマシン固有の言語で提供されるメソッド

 この中では、private, public, static, final だけわかれば十分である。 クラスのアクセス修飾子は public, abstract, final のいずれかだけである。

 A フィールドの宣言

 フィールドの宣言は

      データ型 変数名 ;         (1) 変数の宣言    
      データ型[] 配列名 ;        (2) 配列の宣言    
      クラス名 変数名 ;         (3) クラス型変数の宣言    
を列挙したものである(順序、個数は任意)。「変数の宣言」と「配列の宣言」は
      int   n = 123;
      float x = 45.67e-3;    
      double[] a = new double[10];
      int[]    b = int[]{1, 23, 456}; 
      char[]   c = {'a', 'b', 'c', 'd', 'e'};    
のように、生成や初期値設定まで行なってもよい。

 3つ目の宣言

      クラス名 変数名;  
により、この変数は指定されたクラスのオブジェクトを指す変数となる(取る値はクラスのオブジェクトへのポインタ)、要するにオブジェクトの名前のようなものとなる(インスタンスとオブジェクトは同義語であることに注意)。

 (1)や(3)のような、フィールドの1つとして宣言される変数(例えば x とする)は、オブジェクト(=インスタンス。例えば Obj とする)を生成するとそのオブジェクトのフィールドとして Obj . x のように使うことができる(ただし、異なるオブジェクト同士では異なる変数である。つまり、Obj1, Obj2 が同じクラスのオブジェクトであっても Obj1.xObj2.x は異なるものである)ので、インスタンス変数とも言う(後述のクラス変数と比べよ)。

 上のような宣言の書き方は、変数のデータ型宣言と同じ書き方であることに注意したい(すでに述べたように、クラスはデータ型という概念を拡張した概念である)。

 オブジェクトを指す変数を宣言しても、その変数は具体的な(=実体のある)オブジェクトを指しているわけではない。具体的なオブジェクトを生成することによって、その変数は初めて実体をもつ。

 オブジェクト(=インスタンス)の生成の仕方

 オブジェクトの生成はインスタンス化ともいい、次のように書く:

    変数名 = new クラス名 (); 
 フィールドにも修飾子 public, private, static, final を付けることができる。
 B メソッドの宣言

 メソッドの一般的な形は

      修飾子 結果型 メソッド名 ( 型 仮引数 , ... , 型 仮引数 ) ブロック	  
である。
(例) メソッドの書き方
(1)
    public static int sum ( int[] a, int first, int last ) {
        int s = 0;	
	for (int i=first; i<last; i++) {
            s += a[i];
        }
        return s;
    } 

(2)
    public static void nothing() { }  // 何もしない

(3) 
    private static int floor(double x) { 

    // double 型の変数(の値)を int 型 へ変換(整数化)するメソッド
       return (int)x;
    } 

(4) 
    static double power(double x, int n) { 
    // xn を計算して返すメソッド
        double p = 1.0;
        for (int i=0; i<n; i++)
            p *= x;
        return p;
    } 

(4)
    public static void printArray(int[] a) { 
    // 引数は配列、値を返さない
        for (int i=0; i<a.length; i++) {
            System.out.print(a[i] + " ");
        }
    } 

 C コンストラクタ

 コンストラクタはオブジェクトを初期化するための特別なメソッドである



 メソッドには2種類ある
 すでに何度も述べているように、メソッドは大きく分けて2種類ある:
   クラスメソッド (class method)
   インスタンスメソッド (instance method)  
  1. クラスメソッドは、修飾子 static を付けて定義されるメソッドのことである。
  2. このタイプのメソッドはクラス定義だけに直接関係付けられ(クラスから生成されるオブジェクトには反映されない)、クラス名を指定するだけで呼び出して使うことができる(同一のクラス内であれば、クラス名の指定はいらない)。
  3. それに対し、インスタンスメソッドは、修飾子 static を付けずに定義されるメソッドであり、
  4. このタイプのメソッドは、クラスのインスタンス(すなわち、オブジェクト)を生成することによって、そのインスタンスの中のメソッドとして初めて実体が存在するようになり、そのインスタンスを介してだけ使うことができる。
  5. クラスメソッドがインスタンスメソッドを参照することはできない(非 static なメソッドはその時点ではまだ存在していないから)。
 クラス型変数(クラスのオブジェクトを指す変数)の宣言の仕方とオブジェクトの生成の仕方についてはすでに学んだ:
クラス型変数の宣言:

  クラス名 変数名;  
オブジェクトの生成:

  クラス名 変数名 = new クラス名 (コンストラクタへの実引数); 


(例) メソッドのタイプ別利用法

(1) 同じクラス内のクラスメソッドを呼び出す例
public class ex1 { // クラス ex1 内のクラスメソッド main public static void main(String[] args) { int n = 123; int sum = add(n, 45); // クラスメソッド add の呼び出し // add と main は同じクラス内なので、クラス名を付けなくてもよい System.out.println(n + "+45 = " + sum); } // クラス ex1 内のクラスメソッド add public static int add(int x, int y) { return x+y; } } (2) 他のクラスのクラスメソッド(String クラスのメソッド valueOf)を呼び出す例
import java.lang.*; // String クラスの定義はこのパッケージ内にある //(ただし、この import は不要) public class ex2 { public static void main(String[] args) { String str; str = String.valueOf(123); System.out.println(str); } } (3) 他のクラスのインスタンスメソッド(String クラスのメソッド toUpperCase)を呼び出す例 public class ex3 { public static void main(String[] args) { String str, STR; // String クラスのオブジェクト str と STR を生成する str = new String("komoji"); // まとめて、String str = new String("komoji"); でもよい // String str = "komoji"; でもよい STR = str.toUpperCase(); System.out.println(STR); // KOMOJI と出力される } } (4) クラスメソッド・インスタンスメソッドを定義して使う例 public class EX4_1 { EX4_1 { } // これは、このクラスのコンストラクタであるが // この例のように「何も定義しない」コンストラクタは // 書かずに省略してもよい public int DOUBLE(int x) { // インスタンスメソッド return 2*x; } public static int TRIPLE(int x) { // クラスメソッド return 3*x; } } public class EX4_2 { public static void main(String[] args) { EX4_1 xx; xx = new EX4_1(); EX4_1 xxx = new EX4_1(); // オブジェクト xxx の生成 int n = xx.DOUBLE(123); // オブジェクト xx のメソッドDOUBLE int m= xxx.DOUBLE(321); System.out.println(EX4_1.TRIPLE(n)); // クラス EX4_1 のクラスメソッド TRIPLE を呼び出している } } (5) オブジェクトを生成して使う例(ソーティング)
ソーティングを行なうアルゴリズムを3つ定義したクラス SORTER を考えよう(ソースコードは ここ)。 この例では、コンストラクタは無い(コンストラクタのある例は次回考える)。


課題13でやること



1.課題13のためのディレクトリ

    ~/public_html/infomath6/

2.今回やること

 前回、チューリップを描いたときに、後ろに位置するチューリップをそれより手前に位置するチューリップよりも後で描くと(それらが重なる場合には後ろのチューリップが手前のチューリップを上書きするという変なことが生じる(この例 では、花の間隔が大きいので、そのようなことは最初の1個所でしか起こっていない)。これをなくすためには、チューリップの根の位置の y 座標をソートして、y 座標が大きいものを先に描くようにするとよい。上記の SORTER クラスのソーティングメソッドを使ってこれを行なってみよう (「回転なし」の ソースコード1 または ソースコード2 を使うとよい)。
 完成した Java のソースファイルを task13.java とし、それを表示する html ファイルを task13.html とし、ディレクトリ ~/public_html/infomath6/ に置くこと。

3.プラスα

 今回のプラスαは、3次元CGに関することのうち、3次元空間内における回転についてである。
 次の左側のたんぽぽの花は正面を向いているものであるが、多くの場合、右側のたんぽぽのように上向きになっているであろう。この右側のようなたんぽぽを描くためには、x 軸の周りに回転させることが必要になる。

 x 軸の周り(あるいは、y 軸の周り)に theta ラジアン回転させたときの座標変換は次の式で与えられる(x, y, z は旧座標。theta は回転角度(単位はラジアン))。

    3次元CGで x 軸の回りに theta ラジアン回転させたとき

    新 x 座標
       x

    新 y 座標
       y*Math.cos(theta)+z*Math.sin(theta)

    新 z 座標
       -y*Math.sin(theta)+z*Math.cos(theta)

    3次元CGで y 軸の回りに theta ラジアン回転させたとき

    新 x 座標
       x*Math.cos(theta)-z*Math.sin(theta)

    新 y 座標
       y

    新 z 座標
       x*Math.sin(theta)+z*Math.cos(theta)

    3次元CGで z 軸の回りに theta ラジアン回転させたとき

    新 x 座標
       x*Math.cos(theta)+y*Math.sin(theta)

    新 y 座標
       -x*Math.sin(theta)+y*Math.cos(theta)

    新 z 座標
       z
で与えられる。
 もっと一般の場合(任意の直線周りの回転)は、平行移動と2次元の回転の組合わせで表現できる。  たんぽぽの花ソースコード を与えるので、これを基にして x 軸の周りに回転させたもの を描いてみよ。
上記の回転のための座標変換を与えるメソッドは graphics クラスに組み込んでおいたので、~moriya/graphics.java をコンパイルして使うこと。メソッド名と引数は以下の通り(x, y, z は旧座標。theta は回転角度(単位はラジアンではなく「度」)):
    x 軸の回りに theta 度回転
        新 x 座標: Rotate3X_x(double x, double y, double z, double theta)
        新 y 座標: Rotate3X_y(double x, double y, double z, double theta)
        新 z 座標: Rotate3X_z(double x, double y, double z, double theta)

    y 軸の回りに theta 度回転
        新 x 座標: Rotate3Y_x(double x, double y, double z, double theta)
        新 y 座標: Rotate3Y_y(double x, double y, double z, double theta)
        新 z 座標: Rotate3Y_z(double x, double y, double z, double theta)

    z 軸の回りに theta 度回転
        新 x 座標: Rotate3Z_x(double x, double y, double z, double theta)
        新 y 座標: Rotate3Z_y(double x, double y, double z, double theta)
        新 z 座標: Rotate3Z_z(double x, double y, double z, double theta)