最近、オブジェクト指向プログラミング1について説明する機会が多く、これからもっと多くなりそうなので、文章にまとめることにしました。
オブジェクト指向とは何かという説明はたくさんありますが、 何のためにオブジェクト指向プログラミングがあり、どのように使うのか を、具体例を交えて説明した文章がなかなか見つからなかったので自分で書いてみました。個人差はあると思いますが、目安として 15分程度で読了 2できる分量を想定しました。
本投稿は継続的にアップデートして改良していきたいと考えています。わかりづらい点やおかしな点などあれば、コメントか @koher までツイートいただけるとうれしいです。
対象
- オブジェクト指向の考え方がよくわからない人
- プログラミングの初歩(条件分岐、ループ、配列、関数など)はわかっている人
- 作りたいものを自分で考えてある程度作れる人
一人のデータ
プログラムで会員や顧客、ユーザーなど人のデータを扱うことはよくあります。そのようなケースを例に考えてみましょう。
話を簡単にするために、「人」は次の三つのデータだけを持っていると考えます。
- 姓 (familyName)
- 名 (givenName)
- 年齢 (age)
プログラムで書くと次のような感じでしょうか3。String
は文字列、 Integer
は整数を表す型、 println
は文字列を表示して改行する関数だとします。
// 変数を宣言して値を代入
String givenName = "Albert"; // 名
String familyName = "Einstein"; // 姓
Integer age = 26; // 年齢
println(givenName + " " + familyName); // フルネームを表示
実行結果は次のとおりです。
Albert Einstein
複数人のデータ
(A) では一人だけでしたが、複数人のデータを扱うには配列にします。
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
は人に属しているのに、それらがばらばらの配列に格納されているからです。そこで、三つの値を一つの オブジェクト にまとめたいという気持ちになります。
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
クラスの フィールド と呼びます。フィールドはオブジェクトにひも付いた変数のようなもので、 メンバ変数 と呼ばれることもあります。
メソッド
先程から何度もフルネームを表示していますが、よく書く処理は関数にすると便利です。
// フルネームを返す関数
String getFullName(Person person) {
return person.givenName + " " + person.familyName;
}
// フルネームを表示
println(getFullName(person));
これでフルネームを表示するのが楽になりました。しかし、 person.givenName
や person.familyName
に対して、 getFullName(person)
というのは統一感がありません。同じ person.
を使って、 person.getFullName()
と書けるとより良いように感じられます。たとえば、次のような感じです。
class Person {
String givenName;
String familyName;
Integer age;
// このオブジェクトのフルネームを返す
String getFullName() {
return this.givenName + " " + this.familyName;
}
}
// フルネームを表示
println(person.getFullName());
フィールドがクラスに紐付いた変数なら、この getFullName
はクラスに紐付いた関数です。そのような関数を メソッド ( メンバ関数 )と呼びます。
getFullName
は内部で givenName
や familyName
にアクセスできないといけません。 (D) では getFullName(person)
のように person
が引数として渡されるので、 person.givenName
のようにしてフィールドにアクセスできます。
(E) の場合は person.getFullName()
のように、 person
は引数として渡されません。その代わりに this
を介して givenName
や familyName
にアクセスしています。この this
とは何でしょうか。 this
は引数として明示的に書かれていませんが、 person.getFullName()
のようにしてメソッドを呼び出した場合、隠れた引数 this
があって、暗黙的に this
に 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;
}
...
}
// コンストラクタでオブジェクトを生成と同時に初期化
// (コンストラクタは「new クラス名(...)」で呼び出す言語が多い)
Person person = new Person("Albert", "Einstein", 26);
カプセル化
これまでは person.familyName
のように、フィールドに直接アクセスしてきました。しかし、それはオブジェクト指向において望ましくないこととされています。
それでは、フィールドに格納された値にアクセスするにはどうすればよいのでしょうか。答えは↓です。
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()
のようにメソッドを介してフィールド値にアクセスするようにします。しかし、こんなことをして何がうれしいのでしょうか。ただコードが長くなり、冗長になっただけではないでしょうか。
次のような例を考えてみます。
姓や名を単独で表示することに比べてフルネームを表示する方がずっと多いシステムを考えます。毎回フルネームを生成する処理がボトルネックになっているとしましょう。
そうすると、 Person
が givenName
と familyName
をばらばらに持ち、 getFullName
が呼び出されたときに毎回フルネームを生成するよりも、 Person
の内部ではフルネームを保持しておいて、 必要に応じて名や姓を生成する方が効率的です。 (G) を書き換えると次のようになります4。
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
がカプセル化されておらず、「実装」と「振る舞い」が分離されていない場合、上記のようにパフォーマンス向上のために Person
に fullName
を持たせようとすると大変です。 person.familyName
となっていた箇所を、すべて person.fullName.split(" ")[1]
に書き換えなければなりません。カプセル化によって「実装」と「振る舞い」を分離することで、「実装」の変更による影響をカプセルの中に閉じ込めることができ、コードの修正が容易になります。
アクセサ
↑で見た getXxx
のようなメソッドのことを getter 、 setXxx
のようなメソッドのことを setter と呼びます。また、getterとsetterをあわせて アクセサ や アクセサメソッド と呼びます。
アクセサはカプセル化を助けるだけでなく、フィールドにはできないことを可能にします。
たとえば、 setAge
メソッドは年齢をセットしますが、負の数を渡されると困ります。負の数を渡された場合のエラー処理方法は色々考えられますが、ここでは0に丸めて値を格納したいとしましょう。
そのためには setAge
メソッドを次のように実装します。
class Person {
...
void setAge(Integer age) {
if (age < 0) {
this.age = 0;
} else {
this.age = age;
}
}
...
}
このように、アクセサを介してフィールドにアクセスするなら、単に代入したり参照したりするだけでなく付加的な処理を加えることができます。
継承
ここまで getFullName
は西洋式に『名 姓』の順に文字列を結合していました。しかし、日本人のように『姓 名』の順に表示したいこともあります。そこで、フルネームが『名 姓』の順になる WesternPerson
(西洋人)クラスと、『姓 名』の順になる EasternPerson
(東洋人)クラスを考えてみます。
WesternPerson
と EasternPerson
を別々に実装することもできますが、二つのクラスは getFullName
以外はまったく同じです。同一の処理を複数箇所に書くのは無駄ですし、片方にバグがあればもう片方も修正しなければならないのでメンテナンス性も下がります。何とか共通部分を一つのコードで書くことはできないでしょうか。
それを実現するのが 継承 です。クラスは別のクラスを継承することで、そのクラスの持つ機能を自分自身に取り込むことができます。
WesternPerson
と EasternPerson
の共通機能を Person
として実装し、それを継承して getFullName
だけを別々に実装するようにします。
// 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()); // 織田 信長
このように、継承を使えば共通処理を一箇所にまとめて書くことができます。
クラスがあるクラスを継承した場合、継承元のクラスを スーパークラス 、継承先のクラスを サブクラス と呼びます。上の例では、 Person
は WesternPerson
と EasternPerson
のスーパークラスであり、 WesternPerson
と EasternPerson
は Person
のサブクラスです。
ポリモーフィズム
WesternPerson
と EasternPerson
をまとめて扱いたいときはどうすれば良いでしょうか。例えば、西洋人と東洋人が混ざった会員一覧を表示したいとしましょう。
実は、 WesternPerson
オブジェクトと EasternPerson
オブジェクトは Person
型の変数に代入することができます。
// Person型変数に代入可
Person westernPerson = new WesternPerson("Albert", "Einstein", 26);
Person easternPerson = new EasternPerson("信長", "織田", 47);
異なる型の変数にオブジェクトを代入できるのは気持ち悪いですね。こんなことがなぜ許されるのでしょうか。
クラスを継承したときにメソッドを追加することはできますが、スーパークラスが持っているメソッドをなくすことはできません。そのため、サブクラスはスーパークラスのすべてのメソッドを持っていることが保証されます。つまり WesternPerson
も EasternPerson
も、 Person
の持つどのメソッドを呼ばれても、そのメソッドを持っているので Person
として振る舞うことができるのです。だから、 Person
型の変数に WesternPerson
オブジェクトや EasternPerson
オブジェクトが代入できても問題は起こりません。
このことを、
- WesternPerson is a Person.
- EasternPerson is a Person.
と言えることから、is-a関係 と呼ぶこともあります。
WesternPerson
も EasternPerson
も Person
として扱うことができるのなら、まとめて Person
型の配列に入れて会員一覧を表示できそうです。
// 西洋人と東洋人が混ざった会員の配列
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
型の person
は getFullName
を持っていないので「 Person
クラスには getFullName
というメソッドはありません。」というコンパイルエラーになってしまいます。 person
には WesternPerson
オブジェクトや EasternPerson
オブジェクトが代入されるので実際には getFullName
を持っているのですが、 person
に代入されるオブジェクトが何かは実行時に決まることであり、コンパイル時にそれを知ることはできません。コンパイラには person
に代入されるオブジェクトが本当に getFullName
を持っているかわからないので、コンパイルエラーになってしまうのです。
そこで、次のように Person
クラスにも getFullName
メソッドを実装してみるとどうでしょう?そうすればコンパイラは Person
クラスにも getFullName
メソッドがあると知ることができます。
class Person {
...
String getFullName() {
return null; // 無意味な値を返す
}
}
このような状態で次のコードを実行すると何が起こるでしょうか。
Person person = new WesternPerson("Albert", "Einstein", 26);
println(person.getFullName()); // null? "Albert Einstein"?
Person
の getFullName
は null
を、 WesternPerson
では『名 姓』を返します。 Person
型変数 person
に代入された WesternPerson
オブジェクトの getFullName
を呼ぶと、 Person
か WesternPerson
か、どちらの getFullName
が実行されるでしょうか。
実行結果は次のようになります。
Albert Einstein
スーパークラス( Person
)とサブクラス( WesternPerson
, EasternPerson
)で同名のメソッドが実装されている場合、スーパークラスのメソッドを上書きして挙動を変更することができます。このことを、メソッドの オーバーライド と呼びます。また、上の例のように、メソッドがオーバーライドされていると変数の型に関係なくその変数に代入されているオブジェクトのメソッドが呼ばれます。このような挙動を示す性質のことを ポリモーフィズム と呼びます。
(M) のように Person
にも getFullName
を宣言し、それをオーバーライドすることで (L) のコードはコンパイルエラーを回避できます。その状態でコードを実行すると、ポリモーフィズムによって正しく会員一覧を表示することができます(↓)。同じ Person
型の変数 person
に対して getFullName
を呼んでいますが、 WesternPerson
では『名 姓』の順に、 EasternPerson
では『姓 名』の順に表示されます。
Albert Einstein
織田 信長
Isaac Newton
豊臣 秀吉
Galileo Galilei
徳川 家康
ポリモーフィズムによって WesternPerson
と EasternPerson
をより抽象的な Person
としてまとめて扱えるようになりました( Person
では getFullName
が『名 姓』だろうと『姓 名』だろうと、とにかくフルネームを返せば良いという意味で抽象的です)。
このような抽象化は、カプセル化・継承・ポリモーフィズムの組み合わせによって実現されています。カプセル化によって「実装」と「振る舞い」が分離され、継承時のオーバーライドによって「振る舞い」が書き換えられ、ポリモーフィズムによって実行時の「振る舞い」が変化することで抽象化が実現されているわけです。
カプセル化・継承・ポリモーフィズムはそれぞれ単体で意味を持ちますが、これらが組み合わさって抽象化が実現されることは、オブジェクト指向プログラミングにおいて特に重要なことだと筆者は考えています。
抽象クラスと抽象メソッド
(M) では、 getFullName
で null
を返すように実装しました。しかし、 Person
オブジェクトを直接使うことはありません。 Person
クラスは型として利用するだけで、オブジェクトを生成するのは必ず WesternPerson
か EasternPerson
なら、 Person
クラスの getFullName
メソッドの実装は何でもいいはずです。何でもいいのであれば次のように書きたいです。
class Person {
...
String getFullName(); // 実装自体を書かない
}
このように実装のないメソッドのことを 抽象メソッド と呼びます。また、抽象メソッドを持つクラスのことを 抽象クラス と呼びます5。 Java や多くの言語では、抽象クラスには abstract class
と、抽象メソッドには abstract String getFullName();
というように、明示的に abstract
のようなキーワードを付与します。
抽象クラスのオブジェクトを生成しようとするとコンパイルエラーになります。なぜなら、もし Person
オブジェクトが生成できてしまったら、 getFullName
メソッドが呼ばれたときに実装がないので困ってしまうからです。
// Personは抽象クラスなのでPersonオブジェクトを生成することはできない
Person person = new Person("Albert", "Einstein", 26);
抽象クラスはそのオブジェクトを生成することができないので、継承して、抽象メソッドをオーバーライドして使うことが前提のクラスとなります。ただし、 Person
オブジェクトは生成できませんが、 Person
型の変数は宣言できることに注意して下さい。 次のコードは正しいコードです。
// Personは抽象クラスだけどPerson型の変数は宣言できる
Person person = new WesternPerson("Albert", "Einstein", 26);
Person
を型として使えることは、先程見たように、抽象化を実現するためには欠かせません。
抽象クラスに対して、抽象メソッドを持たない普通のクラスのことを 具象クラス と呼びます。この例では、 Person
は抽象クラス、 WestenPerson
と EasternPerson
は具象クラスになります。
抽象化の何がうれしいのか
Person
と getFullName
の例は単純すぎて抽象化の利点がわかりづらいかもしれません。より現実的な例として、ZIPファイルを展開して元のバイト列を復元する関数を考えてみましょう。
// ZIPファイルを展開してバイト列を得る関数
Byte[] unzip(File file) {
...
}
もし、ファイルだけでなくZIP形式のバイト列を入力として展開したい場合はどうでしょう。次の関数も必要です。
// ZIP形式のバイト列を展開してバイト列を得る関数
Byte[] unzip(Byte[] bytes) {
...
}
圧縮されたファイルを展開するには符号理論に基づいた複雑な処理が必要です。これらの二つの関数をバラバラに実装するのは大変です。
しかし、片方の unzip
の中でもう片方の unzip
を呼んで共通化することはできません。 (R) の unzip
の中で (S) の unzip
を呼ぶにはファイルの全体をメモリ上に読み込まないといけないですが、その場合。メモリに乗らないような巨大なファイルを処理できなくなってしまいます。逆に (S) の unzip
の中で (R) の unzip
を呼ぼうとすると、引数 bytes
の中身を一度ファイルに書き出さないといけません。そのような無駄なファイルは作りたくありません。結局、二つの unzip
をバラバラに実装することになってしまいます。
unzip
を実装する上で重要なのは、引数で渡されたものがファイルであれバイト列であれ、先頭から順番にバイトデータを読み出せるということです。そこで、「先頭から順番にバイトデータを読み出せる」という振る舞いに InputStream
という名前を付けて抽象化してみましょう。
class InputStream {
// 次の1バイトを取得するメソッド
Byte read();
}
また、これを継承して FileInputStream
と ByteArrayInputStream
も作ります。名前の通り、前者はファイルから1バイトずつデータを読み出し、後者はバイト配列から1バイトずつデータを読み出す機能を提供します。
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
関数は次のようにまとめられます。
// InputStreamから得られたZIP形式のデータを展開してバイト列を得る関数
Byte[] unzip(InputStream in) {
...
}
この方法であれば、二つの unzip
関数を別々に実装する必要はありません。加えて、 HttpInputStream
や StandardInputStream
を実装すれば、サーバーにおかれたZIPファイルや標準入力で渡されたZIPファイルなども処理できるようになります。 unzip
関数に一切手を加えることなしにです。
このように、プログラムの再利用性が飛躍的に高まることが抽象化のうれしいところです。
まとめ
会員情報などを想定して人の情報(姓、名、年齢)を持つ Person
クラスを例に挙げ、 何のためにオブジェクト指向があり、どのように使うのか を説明しました。特に、 カプセル化 、 継承 、 ポリモーフィズム を用いたプログラムの抽象化について説明しました。
本投稿では、オブジェクト指向の本質ではない次の項目を意図的に省略しています。そのうち補足の投稿を書くかもしれません。
- アクセス制御
- プロパティ
- 多重継承
- インタフェース
- 静的メソッド
-
オブジェクト指向プログラミングの定義は明確ではないですが、本投稿では、カプセル化、継承、ポリモーフィズムによるオブジェクト指向について説明します。 ↩
-
経験則的に15分程度であれば電車移動などで連続して確保しやすいこと、あまり気合を入れなくても読み始められること、オブジェクト指向を説明するのに必要不可欠な分量から15分としました。本投稿の序文と「対象」、「まとめ」、コードを除いた文字数がおよそ6千数百字、本文中にアルファベットが多いことを考えると日本語換算で6000字程度の分量と思われます。日本人の平均読書スピードは600字/分ほどのようなので、本文に10分、コードに5分で想定しています。 ↩
-
本投稿のコードはJavaをベースにしていますが、オブジェクト指向を説明するのに都合が良いように改変した架空の言語で書かれています。 ↩
-
実はこのコードは正しくありません。
givenName
やfamilyName
にスペースを含む文字列が与えられるとおかしなことになります。しかし、そんなケースを考えても本筋とは関係なく話が複雑になるだけなのでここでは無視します。 ↩ -
抽象メソッドを一つも持たなくても、
abstract
などのキーワードを明示的に付与することで抽象クラスにできる言語もあります。 ↩