453
501

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

15分でわかる かんたんオブジェクト指向

Last updated at Posted at 2014-08-22

最近、オブジェクト指向プログラミング1について説明する機会が多く、これからもっと多くなりそうなので、文章にまとめることにしました。

オブジェクト指向とは何かという説明はたくさんありますが、 何のためにオブジェクト指向プログラミングがあり、どのように使うのか を、具体例を交えて説明した文章がなかなか見つからなかったので自分で書いてみました。個人差はあると思いますが、目安として 15分程度で読了 2できる分量を想定しました。

本投稿は継続的にアップデートして改良していきたいと考えています。わかりづらい点やおかしな点などあれば、コメントか @koher までツイートいただけるとうれしいです。

対象

  • オブジェクト指向の考え方がよくわからない人
  • プログラミングの初歩(条件分岐、ループ、配列、関数など)はわかっている人
  • 作りたいものを自分で考えてある程度作れる人

一人のデータ

プログラムで会員や顧客、ユーザーなど人のデータを扱うことはよくあります。そのようなケースを例に考えてみましょう。

話を簡単にするために、「人」は次の三つのデータだけを持っていると考えます。

  • 姓 (familyName)
  • 名 (givenName)
  • 年齢 (age)

プログラムで書くと次のような感じでしょうか3String は文字列、 Integer は整数を表す型、 println は文字列を表示して改行する関数だとします。

(A)
// 変数を宣言して値を代入
String givenName = "Albert";    // 名
String familyName = "Einstein"; // 姓
Integer age = 26;               // 年齢

println(givenName + " " + familyName); // フルネームを表示

実行結果は次のとおりです。

Albert Einstein

複数人のデータ

(A) では一人だけでしたが、複数人のデータを扱うには配列にします。

(B)
String[] givenNames = ["Albert", "Isaac", "Galileo"];
String[] familyNames = ["Einstein", "Newton", "Galilei"];
Integer[] ages = [26, 43, 46];

// 全員のフルネームを表示
for (Integer i = 0; i < 3; i++) {
    println(givenNames[i] + " " + familyNames[i]);
}

実行結果は次の通りです。

Albert Einstein
Isaac Newton
Galileo Galilei

オブジェクトにまとめる(クラスとフィールド)

(B) のコードは少し読みづらいです。 givenName, familyName, age は人に属しているのに、それらがばらばらの配列に格納されているからです。そこで、三つの値を一つの オブジェクト にまとめたいという気持ちになります。

(C)
class Person { // 人を表す型を宣言
    String givenName;
    String familyName;
    Integer age;
}

// Person型の変数personを宣言し
// givenName, familyName, ageの値を設定
Person person = {
    givenName: "Albert",
    familyName: "Einstein",
    age: 26
};

// フルネームを表示
println(person.givenName + " " + person.familyName);

// 年齢を1増やす
person.age = person.age + 1;

このコードでは、 givenName, familyName , age を持った Person (人)という新しい型を宣言し、そのオブジェクトに "Albert", "Einstein", 26 という値を持たせています。

Person を宣言するときに class というキーワードを使っていますが、オブジェクト指向では Person のような型を規定するものを クラス と呼びます。

また、givenName, familyName, age のことを Person クラスの フィールド と呼びます。フィールドはオブジェクトにひも付いた変数のようなもので、 メンバ変数 と呼ばれることもあります。

メソッド

先程から何度もフルネームを表示していますが、よく書く処理は関数にすると便利です。

(D)
// フルネームを返す関数
String getFullName(Person person) {
    return person.givenName + " " + person.familyName;
}

// フルネームを表示
println(getFullName(person));

これでフルネームを表示するのが楽になりました。しかし、 person.givenNameperson.familyName に対して、 getFullName(person) というのは統一感がありません。同じ person. を使って、 person.getFullName() と書けるとより良いように感じられます。たとえば、次のような感じです。

(E)
class Person {
    String givenName;
    String familyName;
    Integer age;
    
    // このオブジェクトのフルネームを返す
    String getFullName() {
        return this.givenName + " " + this.familyName;
    }
}

// フルネームを表示
println(person.getFullName());

フィールドがクラスに紐付いた変数なら、この getFullName はクラスに紐付いた関数です。そのような関数を メソッドメンバ関数 )と呼びます。

getFullName は内部で givenNamefamilyName にアクセスできないといけません。 (D) では getFullName(person) のように person が引数として渡されるので、 person.givenName のようにしてフィールドにアクセスできます。

(E) の場合は person.getFullName() のように、 person は引数として渡されません。その代わりに this を介して givenNamefamilyName にアクセスしています。この this とは何でしょうか。 this は引数として明示的に書かれていませんが、 person.getFullName() のようにしてメソッドを呼び出した場合、隠れた引数 this があって、暗黙的に thisperson が渡されると考えて下さい。

コンストラクタ

クラスにはフィールドやメソッドと並ぶ重要な構成要素があります。それが コンストラクタ です( イニシャライザ と呼ばれることもあります)。

コンストラクタはオブジェクトが生成されるときに呼ばれるメソッドのようなものです。コンストラクタでフィールドを初期化することで、フィールドが未初期化なオブジェクトが生成されるのを防げます。

(F)
class Person {
    String givenName;
    String familyName;
    Integer age;
    
    // コンストラクタの実装
    Person(String givenName, String familyName, Integer age) {
        // 引数で与えられた値でフィールドを初期化
        this.givenName = givenName;
        this.familyName = familyName;
        this.age = age;
    }
    
    ...
}

// コンストラクタでオブジェクトを生成と同時に初期化
// (コンストラクタは「new クラス名(...)」で呼び出す言語が多い)
Person person = new Person("Albert", "Einstein", 26);

カプセル化

これまでは person.familyName のように、フィールドに直接アクセスしてきました。しかし、それはオブジェクト指向において望ましくないこととされています。

それでは、フィールドに格納された値にアクセスするにはどうすればよいのでしょうか。答えは↓です。

(G)
class Person {
    String givenName;
    String familyName;
    Integer age;

    ... // コンストラクタとgetFullNameメソッドを省略
    
    // givenNameを返すだけのメソッド
    String getGivenName() {
        return this.givenName;
    }
    
    // givenNameを変更するだけのメソッド
    void setGivenName(String givenName) {
        this.givenName = givenName;
    }
    
    // familyNameを返すだけのメソッド
    String getFamilyName() {
        return this.familyName;
    }
    
    // familyNameを変更するだけのメソッド
    void setFamilyName(String familyName) {
        this.familyName = familyName;
    }
    
    // ageを返すだけのメソッド
    Integer getAge() {
        return this.age;
    }
    
    // ageを変更するだけのメソッド
    void setAge(Integer age) {
        this.age = age;
    }
}

// 姓と年齢を表示
println(person.getFamilyName() + " (" + person.getAge() + ")");

// 年齢を1増やす
person.setAge(person.getAge() + 1);

クラスの外部からは person.getFamilyName() のようにメソッドを介してフィールド値にアクセスするようにします。しかし、こんなことをして何がうれしいのでしょうか。ただコードが長くなり、冗長になっただけではないでしょうか。

次のような例を考えてみます。

姓や名を単独で表示することに比べてフルネームを表示する方がずっと多いシステムを考えます。毎回フルネームを生成する処理がボトルネックになっているとしましょう。

そうすると、 PersongivenNamefamilyName をばらばらに持ち、 getFullName が呼び出されたときに毎回フルネームを生成するよりも、 Person の内部ではフルネームを保持しておいて、 必要に応じて名や姓を生成する方が効率的です。 (G) を書き換えると次のようになります4

(H)
class Person {
    // givenNameとfamilyNameをばらばらに持つ代わりにfullNameで持つ
    String fullName;
    Integer age;
    
    ... // コンストラクタを省略
    
    String getFullName() {
        // 単にfullNameを返すだけで良い
        return this.fullName;
    }
    
    String getGivenName() {
        // fullNameのスペースより前の部分を返す
        // (splitはStringを分割してString[]を返すStringクラスのメソッドとする)
        return this.fullName.split(" ")[0];
    }
    
    void setGivenName(String givenName) {
        // 与えられたgivenNameを使ってfullNameを更新
        this.fullName = givenName + " " + getFamilyName();
    }
    
    String getFamilyName() {
        // fullNameのスペースより後の部分を返す
        return this.fullName.split(" ")[1];
    }
    
    void setFamilyName(String familyName) {
        // 与えられたfamilyNameを使ってfullNameを更新
        this.fullName = getGivenName() + " " + familyName;
    }
    
    ... // getAge, setAgeを省略
}

// 姓と年齢を表示
println(person.getFamilyName() + "(" + person.getAge() + ")");

// 年齢を1増やす
person.setAge(person.getAge() + 1);

(G)(H) では Person の実装が全く異なりますが、最後の姓と年齢を表示するコード(↓)は同じです。

// 姓と年齢を表示
println(person.getFamilyName() + "(" + person.getAge() + ")");

Person の実装が異なるにも関わらず、 Person を使うコード(↑)に変化がないのはなぜでしょうか。それは、 Persopn の実装と振る舞いが分離されているからです。

Person がどのようなフィールドを持っているかや、メソッドをどのように実装するかというのは「実装」に当たります。一方で、どんなメソッドを持っていてどんな引数を渡すとどのような戻り値が返されるかというのは Person の「振る舞い」です。「振る舞い」とは、そのクラスに何ができるのかということです。

(G)(H) では Person の「実装」が大幅に変更されていますが、「振る舞い」は一切変更されていません。このように、「実装」を「振る舞い」から分離し、隠蔽することを カプセル化 と呼びます。

カプセル化されたクラスを使う人にとっては、目に見えるのは「振る舞い」だけです。クラスを使う人は、そのクラスを使って何ができるのかに興味があり、それがどのように実装されているのかについては興味がありません。

これは、関数の考え方と似ています。関数を使う人が知りたいのはその関数で何ができるのか、その関数をどのように使うのかということであって、関数の実装の詳細を知りたいとは思いません。 println を日頃から使っていても、 println の中身を知っている人は少ないでしょう。

それと同じように、データ(フィールド)と関数(メソッド)がセットになったクラスにおいても、「実装」と「振る舞い」を分離して「実装」を隠蔽したのがカプセル化です。カプセル化されたクラスを利用する人は、「実装」について気にする必要がありません。また、「実装」が変更されても「振る舞い」が変わらなければ、そのクラスを利用している箇所を変更する必要はありません。

もし Person がカプセル化されておらず、「実装」と「振る舞い」が分離されていない場合、上記のようにパフォーマンス向上のために PersonfullName を持たせようとすると大変です。 person.familyName となっていた箇所を、すべて person.fullName.split(" ")[1] に書き換えなければなりません。カプセル化によって「実装」と「振る舞い」を分離することで、「実装」の変更による影響をカプセルの中に閉じ込めることができ、コードの修正が容易になります。

アクセサ

↑で見た getXxx のようなメソッドのことを gettersetXxx のようなメソッドのことを setter と呼びます。また、getterとsetterをあわせて アクセサアクセサメソッド と呼びます。

アクセサはカプセル化を助けるだけでなく、フィールドにはできないことを可能にします。

たとえば、 setAge メソッドは年齢をセットしますが、負の数を渡されると困ります。負の数を渡された場合のエラー処理方法は色々考えられますが、ここでは0に丸めて値を格納したいとしましょう。

そのためには setAge メソッドを次のように実装します。

(I)
class Person {
    ...
    void setAge(Integer age) {
        if (age < 0) {
            this.age = 0;
        } else {
            this.age = age;
        }
    }
    ...
}

このように、アクセサを介してフィールドにアクセスするなら、単に代入したり参照したりするだけでなく付加的な処理を加えることができます。

継承

ここまで getFullName は西洋式に『名 姓』の順に文字列を結合していました。しかし、日本人のように『姓 名』の順に表示したいこともあります。そこで、フルネームが『名 姓』の順になる WesternPerson (西洋人)クラスと、『姓 名』の順になる EasternPerson (東洋人)クラスを考えてみます。

WesternPersonEasternPerson を別々に実装することもできますが、二つのクラスは getFullName 以外はまったく同じです。同一の処理を複数箇所に書くのは無駄ですし、片方にバグがあればもう片方も修正しなければならないのでメンテナンス性も下がります。何とか共通部分を一つのコードで書くことはできないでしょうか。

それを実現するのが 継承 です。クラスは別のクラスを継承することで、そのクラスの持つ機能を自分自身に取り込むことができます。

WesternPersonEasternPerson の共通機能を Person として実装し、それを継承して getFullName だけを別々に実装するようにします。

(J)
// getFullName以外をPersonに実装
class Person {
    String givenName;
    String familyName;
    Integer age;

    Person(String givenName, String familyName, Integer age) {
        this.givenName = givenName;
        this.familyName = familyName;
        this.age = age;
    }
    
    String getGivenName() {
        return this.givenName;
    }
    
    void setGivenName(String givenName) {
        this.givenName = givenName;
    }

    String getFamilyName() {
        return this.familyName;
    }
    
    void setFamilyName(String familyName) {
        this.familyName = familyName;
    }
    
    Integer getAge() {
        return this.age;
    }
    
    void setAge(Integer age) {
        this.age = age;
    }
}

// Personを継承してWesternPersonを作る
// (extendsは継承を表すときに使われることが多いキーワード)
class WesternPerson extends Person {
    // WesternPersonのコンストラクタ
    WesternPerson(String givenName, String familyName, Integer age) {
        // 継承元のPersonのコンストラクタを呼んで初期化
        // (superは継承元のクラスを表すときに使われることが多いキーワード)
        super(givenName, familyName, age);
    }

    // PersonではなくここでgetFullNameを実装
    String getFullName() {
        return this.givenName + " " + this.familyName; // 名 姓
    }
}

// Personを継承してEasternPersonを作る
class EasternPerson extends Person {
    // EasternPersonのコンストラクタ
    EasternPerson(String givenName, String familyName, Integer age) {
        // 継承元のPersonのコンストラクタを呼んで初期化
        super(givenName, familyName, age);
    }
    
    // PersonではなくここでgetFullNameを実装
    String getFullName() {
        return this.familyName + " " + this.givenName; // 姓 名
    }
}

WesternPerson westernPerson = new WesternPerson("Albert", "Einstein", 26);
EasternPerson easternPerson = new EasternPerson("信長", "織田", 47);

println(westernPerson.getFullName()); // Albert Einstein
println(easternPerson.getFullName()); // 織田 信長

このように、継承を使えば共通処理を一箇所にまとめて書くことができます。

クラスがあるクラスを継承した場合、継承元のクラスを スーパークラス 、継承先のクラスを サブクラス と呼びます。上の例では、 PersonWesternPersonEasternPerson のスーパークラスであり、 WesternPersonEasternPersonPerson のサブクラスです。

ポリモーフィズム

WesternPersonEasternPerson をまとめて扱いたいときはどうすれば良いでしょうか。例えば、西洋人と東洋人が混ざった会員一覧を表示したいとしましょう。

実は、 WesternPerson オブジェクトと EasternPerson オブジェクトは Person 型の変数に代入することができます。

(K)
// Person型変数に代入可
Person westernPerson = new WesternPerson("Albert", "Einstein", 26);
Person easternPerson = new EasternPerson("信長", "織田", 47);

異なる型の変数にオブジェクトを代入できるのは気持ち悪いですね。こんなことがなぜ許されるのでしょうか。

クラスを継承したときにメソッドを追加することはできますが、スーパークラスが持っているメソッドをなくすことはできません。そのため、サブクラスはスーパークラスのすべてのメソッドを持っていることが保証されます。つまり WesternPersonEasternPerson も、 Person の持つどのメソッドを呼ばれても、そのメソッドを持っているので Person として振る舞うことができるのです。だから、 Person 型の変数に WesternPerson オブジェクトや EasternPerson オブジェクトが代入できても問題は起こりません。

このことを、

  • WesternPerson is a Person.
  • EasternPerson is a Person.

と言えることから、is-a関係 と呼ぶこともあります。

WesternPersonEasternPersonPerson として扱うことができるのなら、まとめて Person 型の配列に入れて会員一覧を表示できそうです。

(L)
// 西洋人と東洋人が混ざった会員の配列
Person[] people = [
    new WesternPerson("Albert", "Einstein", 26),
    new EasternPerson("信長", "織田", 47),
    new WesternPerson("Isaac", "Newton", 43),
    new EasternPerson("秀吉", "豊臣", 61),
    new WesternPerson("Galileo", "Galilei", 46),
    new EasternPerson("家康", "徳川", 73),
];

// peopleの要素を一つずつpersonに代入して実行されるfor-eachループ
for (Person person : people) {
    println(person.getFullName()); // PersonはgetFullNameを持たないのでコンパイルエラー!!
}

さて、困りました。上記のコードでは Person 型の persongetFullName を持っていないので「 Person クラスには getFullName というメソッドはありません。」というコンパイルエラーになってしまいます。 person には WesternPerson オブジェクトや EasternPerson オブジェクトが代入されるので実際には getFullName を持っているのですが、 person に代入されるオブジェクトが何かは実行時に決まることであり、コンパイル時にそれを知ることはできません。コンパイラには person に代入されるオブジェクトが本当に getFullName を持っているかわからないので、コンパイルエラーになってしまうのです。

そこで、次のように Person クラスにも getFullName メソッドを実装してみるとどうでしょう?そうすればコンパイラは Person クラスにも getFullName メソッドがあると知ることができます。

(M)
class Person {
    ...
    String getFullName() {
        return null; // 無意味な値を返す
    }
}

このような状態で次のコードを実行すると何が起こるでしょうか。

(N)
Person person = new WesternPerson("Albert", "Einstein", 26);

println(person.getFullName()); // null? "Albert Einstein"?

PersongetFullNamenull を、 WesternPerson では『名 姓』を返します。 Person 型変数 person に代入された WesternPerson オブジェクトの getFullName を呼ぶと、 PersonWesternPerson か、どちらの getFullName が実行されるでしょうか。

実行結果は次のようになります。

Albert Einstein

スーパークラス( Person )とサブクラス( WesternPerson, EasternPerson )で同名のメソッドが実装されている場合、スーパークラスのメソッドを上書きして挙動を変更することができます。このことを、メソッドの オーバーライド と呼びます。また、上の例のように、メソッドがオーバーライドされていると変数の型に関係なくその変数に代入されているオブジェクトのメソッドが呼ばれます。このような挙動を示す性質のことを ポリモーフィズム と呼びます。

(M) のように Person にも getFullName を宣言し、それをオーバーライドすることで (L) のコードはコンパイルエラーを回避できます。その状態でコードを実行すると、ポリモーフィズムによって正しく会員一覧を表示することができます(↓)。同じ Person 型の変数 person に対して getFullName を呼んでいますが、 WesternPerson では『名 姓』の順に、 EasternPerson では『姓 名』の順に表示されます。

Albert Einstein
織田 信長
Isaac Newton
豊臣 秀吉
Galileo Galilei
徳川 家康

ポリモーフィズムによって WesternPersonEasternPerson をより抽象的な Person としてまとめて扱えるようになりました( Person では getFullName が『名 姓』だろうと『姓 名』だろうと、とにかくフルネームを返せば良いという意味で抽象的です)。

このような抽象化は、カプセル化・継承・ポリモーフィズムの組み合わせによって実現されています。カプセル化によって「実装」と「振る舞い」が分離され、継承時のオーバーライドによって「振る舞い」が書き換えられ、ポリモーフィズムによって実行時の「振る舞い」が変化することで抽象化が実現されているわけです。

カプセル化・継承・ポリモーフィズムはそれぞれ単体で意味を持ちますが、これらが組み合わさって抽象化が実現されることは、オブジェクト指向プログラミングにおいて特に重要なことだと筆者は考えています。

抽象クラスと抽象メソッド

(M) では、 getFullNamenull を返すように実装しました。しかし、 Person オブジェクトを直接使うことはありません。 Person クラスは型として利用するだけで、オブジェクトを生成するのは必ず WesternPersonEasternPerson なら、 Person クラスの getFullName メソッドの実装は何でもいいはずです。何でもいいのであれば次のように書きたいです。

(O)
class Person {
    ...
    String getFullName(); // 実装自体を書かない
}

このように実装のないメソッドのことを 抽象メソッド と呼びます。また、抽象メソッドを持つクラスのことを 抽象クラス と呼びます5。 Java や多くの言語では、抽象クラスには abstract class と、抽象メソッドには abstract String getFullName(); というように、明示的に abstract のようなキーワードを付与します。

抽象クラスのオブジェクトを生成しようとするとコンパイルエラーになります。なぜなら、もし Person オブジェクトが生成できてしまったら、 getFullName メソッドが呼ばれたときに実装がないので困ってしまうからです。

(P)
// Personは抽象クラスなのでPersonオブジェクトを生成することはできない
Person person = new Person("Albert", "Einstein", 26);

抽象クラスはそのオブジェクトを生成することができないので、継承して、抽象メソッドをオーバーライドして使うことが前提のクラスとなります。ただし、 Person オブジェクトは生成できませんが、 Person 型の変数は宣言できることに注意して下さい。 次のコードは正しいコードです。

(Q)
// Personは抽象クラスだけどPerson型の変数は宣言できる
Person person = new WesternPerson("Albert", "Einstein", 26);

Person を型として使えることは、先程見たように、抽象化を実現するためには欠かせません。

抽象クラスに対して、抽象メソッドを持たない普通のクラスのことを 具象クラス と呼びます。この例では、 Person は抽象クラス、 WestenPersonEasternPerson は具象クラスになります。

抽象化の何がうれしいのか

PersongetFullName の例は単純すぎて抽象化の利点がわかりづらいかもしれません。より現実的な例として、ZIPファイルを展開して元のバイト列を復元する関数を考えてみましょう。

(R)
// ZIPファイルを展開してバイト列を得る関数
Byte[] unzip(File file) {
    ...
}

もし、ファイルだけでなくZIP形式のバイト列を入力として展開したい場合はどうでしょう。次の関数も必要です。

(S)
// ZIP形式のバイト列を展開してバイト列を得る関数
Byte[] unzip(Byte[] bytes) {
    ...
}

圧縮されたファイルを展開するには符号理論に基づいた複雑な処理が必要です。これらの二つの関数をバラバラに実装するのは大変です。

しかし、片方の unzip の中でもう片方の unzip を呼んで共通化することはできません。 (R)unzip の中で (S)unzip を呼ぶにはファイルの全体をメモリ上に読み込まないといけないですが、その場合。メモリに乗らないような巨大なファイルを処理できなくなってしまいます。逆に (S)unzip の中で (R)unzip を呼ぼうとすると、引数 bytes の中身を一度ファイルに書き出さないといけません。そのような無駄なファイルは作りたくありません。結局、二つの unzip をバラバラに実装することになってしまいます。

unzip を実装する上で重要なのは、引数で渡されたものがファイルであれバイト列であれ、先頭から順番にバイトデータを読み出せるということです。そこで、「先頭から順番にバイトデータを読み出せる」という振る舞いに InputStream という名前を付けて抽象化してみましょう。

(T)
class InputStream {
    // 次の1バイトを取得するメソッド
    Byte read();
}

また、これを継承して FileInputStreamByteArrayInputStream も作ります。名前の通り、前者はファイルから1バイトずつデータを読み出し、後者はバイト配列から1バイトずつデータを読み出す機能を提供します。

(U)
class FileInputStream extends InputStream {
    // Fileを受け取り、InputStreamとして働く
    FileInputStream(File file) {
        ...
    }

    Byte read() {
        ... // ファイルから次の1バイトを取得
    }
}

class ByteArrayInputStream extends InputStream {
    // Byte配列を受け取り、InputStreamとして働く
    ByteArrayInputStream(Byte[] bytes) {
        ...
    }

    Byte read() {
        ... // 配列から次の1バイトを取得
    }
}

この InputStream クラスを使えば、 unzip 関数は次のようにまとめられます。

(V)
// InputStreamから得られたZIP形式のデータを展開してバイト列を得る関数
Byte[] unzip(InputStream in) {
    ...
}

この方法であれば、二つの unzip 関数を別々に実装する必要はありません。加えて、 HttpInputStreamStandardInputStream を実装すれば、サーバーにおかれたZIPファイルや標準入力で渡されたZIPファイルなども処理できるようになります。 unzip 関数に一切手を加えることなしにです。

このように、プログラムの再利用性が飛躍的に高まることが抽象化のうれしいところです。

まとめ

会員情報などを想定して人の情報(姓、名、年齢)を持つ Person クラスを例に挙げ、 何のためにオブジェクト指向があり、どのように使うのか を説明しました。特に、 カプセル化継承ポリモーフィズム を用いたプログラムの抽象化について説明しました。

本投稿では、オブジェクト指向の本質ではない次の項目を意図的に省略しています。そのうち補足の投稿を書くかもしれません。

  • アクセス制御
  • プロパティ
  • 多重継承
  • インタフェース
  • 静的メソッド
  1. オブジェクト指向プログラミングの定義は明確ではないですが、本投稿では、カプセル化、継承、ポリモーフィズムによるオブジェクト指向について説明します。

  2. 経験則的に15分程度であれば電車移動などで連続して確保しやすいこと、あまり気合を入れなくても読み始められること、オブジェクト指向を説明するのに必要不可欠な分量から15分としました。本投稿の序文と「対象」、「まとめ」、コードを除いた文字数がおよそ6千数百字、本文中にアルファベットが多いことを考えると日本語換算で6000字程度の分量と思われます。日本人の平均読書スピードは600字/分ほどのようなので、本文に10分、コードに5分で想定しています。

  3. 本投稿のコードはJavaをベースにしていますが、オブジェクト指向を説明するのに都合が良いように改変した架空の言語で書かれています。

  4. 実はこのコードは正しくありません。 givenNamefamilyName にスペースを含む文字列が与えられるとおかしなことになります。しかし、そんなケースを考えても本筋とは関係なく話が複雑になるだけなのでここでは無視します。

  5. 抽象メソッドを一つも持たなくても、 abstract などのキーワードを明示的に付与することで抽象クラスにできる言語もあります。

453
501
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
453
501

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?