Sojiro’s Blog

This is nothing much.

値オブジェクトとコレクションオブジェクト

某先輩の書評を読んで気になったので 『現場で役立つシステム設計の原則』 という本を読んだ。

とあるプロジェクトでその先輩に自分の書くコードについて色々ご指摘をいただいていたタイミングであり、自分としてはかなり参考になった点があるので備忘も兼ねて書いてみる。

今回は特に「値オブジェクト」と「コレクションオブジェクト(ファーストクラスコレクション)」について。

値オブジェクト

値オブジェクトはアプリケーションに登場する様々な値に対してその値を扱うための専用クラスを作るという考え方。

値ごとにクラスを定義する

値オブジェクトの考え方をルール化すると、「プリミティブ型や String 型は使わない」という方針となる。

例えば、先述のプロジェクトのコードでは登場する様々な値を String 型で表現していた。

  • userId
  • nickname
  • address
  • etc…

これらはサーバーリクエストの口でバリデーションを通るものの、その後の値がアプリケーションコード内で保証されないため非常に不安定だった。

また、例えば nicknameaddress を引数として受け付けるメソッドがあったとすると

1
2
3
public User hoge(String nickname, String address) {
    ...
}

となるわけだが、この時このメソッドを使う側で間違えて address, nickname の順で引数を指定したとしてもこのメソッドはその引数を受け付けてしまう。

そこで Nickname クラスと Address クラスを定義するというのが値オブジェクトの考え方だ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Nickname {
    static final int MIN_LENGTH = 1;
    static final int MAX_LENGTH = 20;

    String nickname;

    Nickname(String nickname) {
        if (nickname.length() < MIN_LENGTH) {
            throw new IllegalArgumentException("nickname length must be at least" + MIN_LENGTH);
        }
        if (nickname.length() > MAX_LENGTH) {
          throw new IllegalArgumentException("nickname length must be under" + MAX_LENGTH);
        }
        this.nickname = nickname;
    }
}

address に対しても同様にクラスを定義して先ほどのメソッドを

1
2
3
public User hoge(Nickname nickname, Address address) {
    ...
}

とすればより堅い実装となる。

値オブジェクトを不変とする

値オブジェクトの値は上書きしないというのも値オブジェクトを扱う上で重要な考え方で、これをルール化すると、「値を変更する度にオブジェクトそのものを新しく(別のオブジェクトに)する」ということになる。

1
2
Nickname nickname = new Nickname("sojiro");
nickname.setNickname("shin-sojiro");

ではなく

1
2
Nickname nickname = new Nickname("sojiro");
Nickname updatedNickname = nickname.update("shin-sojiro");

とすることで、各オブジェクトがどの値をもっているのかが明確となり、知らないうちに値が変わっている、ということを防ぐことができる。

コレクションオブジェクト(ファーストクラスコレクション)

値オブジェクトの考え方を発展させ、コレクション型のデータとロジックを独自のクラスに切り出す考え方をコレクションオブジェクト、もしくはファーストクラスコレクションという。

1
2
List<User> users = new ArrayList<>();
users.add(user);

のように扱っていた User のコレクションを、 Users というクラスを定義し、そのクラスで操作が完結するようにする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Users {
    List<User> users;

    Users(List<User> users) {
        this.users = users;
    }

    Users add(User user) {
        List<User> userList = new ArrayList<>(users);
        return new Users(userList.add(user));
    }

    List<User> asList() {
        return Collections.unmodifiableList(users);
    }
}

ここでのポイントは、

  • 値オブジェクトで値に変更がある場合は常に新しいオブジェクトを返したように、コレクションオブジェクトでもコレクションに変更がある場合は新たなオブジェクトを返すようにすること
  • コレクションへの参照を返す場合はなるべく変更できない状態にして返すこと

コレクションへの操作を独自クラスに閉じ込め、同一オブジェクトが持つコレクションの内容が変化することのないようにすることでより堅い実装とすることができる。

感想

Perl からプログラミングを始めたこともあってか(言い訳)、オブジェクト指向と言っても型への意識は低く、如何にコンパイル時点でバグを見つけられる堅い実装とするか、その変数に今どのような値が入っているかをコードの書き方で明確にするか、というような意識も低かった。

現場で役立つシステム設計の原則 からは他にも学んだ点が多いので追って記事にできたらと思う。

参照

現場で役立つシステム設計の原則

WordPress on GAE

GCP の事前準備

以下の準備が事前に必要。ここでは割愛。

  • GCP アカウントの作成
  • GCP プロジェクトの作成
  • 課金の有効化
  • GCS(Google Cloud Storage)に default bucket を作成
    • default bucket: YOUR_PROJECT_NAME.appspot.com
  • Google Cloud SDK のインストール

Composer のインストール

Composer は PHP のパッケージ管理ツール

Composer is strongly inspired by node’s npm and ruby’s bundler.

node の npm、ruby の bundler に強く影響を受けているようだ。

手順

PHP がインストールされていることを確認

1
2
$ php -v
PHP 5.5.38 (cli) (built: Aug 21 2016 21:48:49)

こちら の手順通り Composer をインストール

1
2
3
4
$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ php -r "if (hash_file('SHA384', 'composer-setup.php') === '669656bab3166a7aff8a7506b8cb2d1c292f042046c5a994c43155c0be6190fa0355160742ab2e1c88d40d5be660b410') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
$ php composer-setup.php
$ php -r "unlink('composer-setup.php');"

グローバルに呼び出せるようにしておく

1
$ mv composer.phar /usr/local/bin/composer

GCS へのアクセス権限変更

作成したプロジェクトでログイン

1
2
$ gcloud auth login
$ gcloud config set project YOUR_PROJECT_NAME

GCS のアクセス権更新

1
$ gsutil defacl ch -u AllUsers:R gs://YOUR_PROJECT_NAME.appspot.com

DB の setup

Cloud SQL を使うので GCP の SQL メニューへ

  • wp という名前で MySQL のインスタンスを立てる
    • このとき第2世代を選択可
    • 日本向けがメインであればリージョンは asia-northeast1
    • マシンタイプは安さ重視なら db-f1-micro とする (このタイプで 30円/日 という印象)
    • 他の設定はお好みで…
  • 立てたインスタンスに wp という名前のデータベースを作る
  • wp というユーザーを作る
    • アクセス制御 → ユーザー → ユーザーアカウントを作成、とたどる

WordPress の setup

ローカルに WordPress 構築に必要な素材を用意する

1
2
3
$ git clone https://github.com/GoogleCloudPlatform/php-docs-samples.git
$ cd php-docs-samples/appengine/wordpress/
$ composer install

setup の開始

1
2
$ php wordpress-helper.php setup
# ここでいろいろインタラクティブに聞かれるので答える

※ リージョンを聞かれるが、 asia-northeast1 がない場合は適当に答えておく

DB_HOST 設定を変更

上記手順で WordPress setup 時に適切なリージョンが指定できなかった場合には自分で一部設定を変更する

1
2
cd YOUR_PROJECT_NAME/
vim wordpress/wp-config.php

/** Production login info */ 以下の DB_HOST を適切なリージョンを用いて変更する

Deploy

GAE インスタンスに WordPress を deploy する

1
$ gcloud app deploy --promote --stop-previous-version app.yaml cron.yaml

最初はかなり多くのファイルを GCS にあげるのでそこそこ時間がかかる

参照

https://github.com/GoogleCloudPlatform/php-docs-samples/tree/master/appengine/wordpress

Unix Timeの扱い

Unix time を日付及び時刻に変換したいこと、日付及び時刻を Unix time に変換したいことがある。

Unix time からの変換

1
2
$ date -r 616388399
1989年 7月14日 金曜日 11時59分59秒 JST

日付からの変換

1
2
$ date -jf '%Y-%m-%d %H:%M:%S' '1989-07-14 11:59:59' +%s
616388399

ファイルの内容を直接クリップボードに出力する

ファイルの内容を直接クリップボードにコピーしたり、クリップボードからファイルにペーストしたいことがある。

クリップボードにコピー

1
$ cat ~/Desktop/memo.text | pbcopy

クリップボードからペースト

1
$ pbpaste > ~/Documents/memo.text

Could Not Open the Requested Socket エラーが出たら

Android Studio で GAE に乗せるアプリを開発しているとき、ローカルで立ち上げようとすると以下のようなエラーが出た

1
2
Could not open the requested socket: Address already in use
Try overriding --address and/or --port.

どうやら以前立ち上げた際のプロセスが生き残っていて邪魔しているらしい

以下のように対応した

Android Studio のメニューから RunEdit Configurations... とたどり、対象 module が使っている port を確認

ポートを指定してプロセスを確認、そして kill

1
2
3
$ lsof -i:8080
COMMAND   PID    USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    74792  sojiro   48u  IPv6 0xXXXXXXXXXXXXXXXX      0t0  TCP *:http-alt (LISTEN)
1
$ kill -9 74792

パーフェクトJava読書メモ Chapter 10 Javaプログラムの実行と制御構文

Javaを使うために改訂2版 パーフェクトJavaを読んだメモ

条件分岐

if-else文

インデントの取り方に関係なく else は最も近い(直近の) if にかかる

switch-case文

1
2
3
4
5
6
7
8
switch() {
case 定数1:
    0個以上の文
case 定数N:
    0個以上の文
default:
    0個以上の文
}

式の評価値は以下

  • int 型
  • int 型に暗黙に型変換される型
    • char
    • byte
    • short
  • 数値ラッパークラス
    • Integer
    • Character
    • Byte
    • Short
  • enum 型
  • String 型

式が null になると NullPointerException が発生する

break; 文がない限り処理を続ける

1
2
3
4
5
6
7
8
9
int i = 0;
switch (i) {
    case 0:
        System.out.println(0);
    case 1:
        System.out.println(1);
    default:
        System.out.println("default");
}
1
2
3
0
1
default

繰り返し

for文

1
2
3
for (初期化式; 条件式; 更新式) {
    
}

条件式は評価値が boolean もしくは Boolean

初期化式と更新式は複数の式を , で区切って書ける

1
2
for (int i = 0, j = 0; (i < 10 && j < 10); i++, j++) {}

異なる型の宣言と初期化を並べるとエラー

1
2
for (int i = 0, byte j = 0; (i < 10 && j < 10); i++, j++) {}

for文の外に出して回避

1
2
3
4
int i;
byte j;
for (i = 0, j = 0; (i < 10 && j < 10); i++, j++) {}

ジャンプ

break文

  • ループを抜ける
  • ループがネストしている場合、抜けるのは内側のループのみ

continue文

  • ループ内の文をスキップしてループの条件式の評価に戻る

ラベル

  • 繰り返しにラベルをつける
  • break文、continue文にラベルを渡すことでどの繰り返しを対象にするか定める
1
2
3
4
5
6
target_loop:
while (true) {
    while (true) {
        break target_loop;
    }
}
  • 外側の while ループに target_loop というラベルがつく
  • break target_loop で外側の while ループを抜ける
  • continue の場合は対象のループ内の文をスキップして条件式の評価に戻る)

参照

改訂2版 パーフェクトJava

パーフェクトJava読書メモ Chapter 9 文、式、演算子

Javaを使うために改訂2版 パーフェクトJavaを読んだメモ

Java の文法と文

Java で扱う文は以下の4種類

  • 制御文
  • ブロック文
  • 宣言文
  • 式文

宣言文と式文は終端にセミコロン必要

Java の演算子と式

  • &&||? : (三項演算子)を除くすべての演算子は演算前にすべてのオペランドを評価する
  • オペランドは左のものから評価する
  • メソッド及びコンストラクタ呼び出し式では引数を左から評価する

数値の演算

インクリメント/デクリメント演算の前置後置

前置演算は評価値が演算後の値、後置演算は評価値が演算前の値

1
2
3
4
5
6
int n = 10;
int m = ++n;
// m: 11, n: 11
int n = 10;
int m = n++;
// m: 10, n: 11
1
2
3
4
int n = 0;
while (++n < 10) {
    // ループが回る回数は 9 回
}

論理演算

遅延評価

&&|| は遅延評価を行う

  • && は左辺の評価値が偽であれば右辺の評価値に関係なく結果が偽になるため右辺を評価しない
  • || は左辺の評価値が真であれば右辺の評価値に関係なく結果が真になるため右辺の評価をしない

これらの演算子の右辺に副作用のある式を書くと、左辺の評価値により実行さるか否かが変わってくるので注意

その他の演算

instanceof 演算子

ダウンキャストを安全に行えるかを事前にチェックする

参照

改訂2版 パーフェクトJava

GAE Task Queue について

GAE の Task Queue についてざっくり。

具体的な実装に関しては別エントリに書こうと思います。

Overview

  • アプリケーションの処理(task)を Task Queue API に渡す(queue)ことでユーザーからのリクエスト外で非同期に処理させることができる
  • task はスケーラブルな GAE の Worker モジュールによってバックグラウンドで処理される

Task Queue には以下の2つのタイプがある

Push queues

  • queue を処理する間隔をあらかじめスケジュールすることができる
  • queue は GAE のモジュールへのリクエストとして処理される
  • task の処理には期限がある
    • 自動的にスケーリングするモジュールで処理するものは 10 分以内
    • そうでないモジュールで処理するものは 24 時間以内

Pull queues

  • GAE は queue を処理せず、外部の Worker が queue を取得(lease)して処理する
  • 外部の Worker で、どのような間隔で queue を処理するか管理する必要がある
  • lease のタイミングで外部 Worker に対して queue の処理期限が渡される
  • 期限内に queue の処理が完了するか、 queue が削除されない場合は同一 queue に対する他の Worker プロセスからの lease を許可する

queue の処理は非同期で行われるので task を作成したアプリケーションは処理の結果を知ることができないが、処理に失敗した場合は自動でリトライ処理が走る

Use cases

Push queues

  • SNS メッセージアプリケーションでユーザーがメッセージを送るたびにフォロワーの更新を非同期で行う
  • キャンペーン広告の送信をあらかじめスケジュールしておいて決められた時間に送る

Pull queues

  • バッチジョブに効果がある
  • task に tag をつけることで外部 worker が lease する時に同じ tag がつく task をまとめて処理することができる
  • 実装例としては複数のゲームのリーダーボードが典型的
    • ハイスコアの更新があるたびに game id を tag として、 score と user 名を enqueue する

Push queue

  • Push queue は GAE の Worker モジュールに HTTP リクエストで task を渡す
  • このリクエストは一定の間隔で実行され、失敗すると新たなリクエストをもってリトライされる
  • タスクの種類ごとにハンドラを書く必要がある
  • 1つのモジュールに各種類ごとのハンドラを用意することができる
  • task の種類ごとに別々のモジュールを使うこともできる

Working with push queues

  • task を作って push queue に enqueue するプログラムを書く
  • task リクエストを受け取って GAE モジュールに渡すハンドラを書く
  • quota を気にする必要がある

Pull queue

  • Pull queue を使うことで独自のシステムから GAE の task を処理することができる
  • ここでいう独自のシステムには GAE で構成されたシステムも含まれる
  • GAE アプリケーションからは com.google.appengine.api.taskqueue パッケージを使って task を処理できる
  • その他のアプリケーションからは Task Queue REST API を使う
  • Push queue では GAE がやってくれる以下の処理を自前で用意する必要がある
  • アプリケーション側で worker スケールの管理をする
  • アプリケーション側で処理の終わった task を削除する
  • Pull queue は queue.xml という設定ファイルが必要

Pull queue による task 処理の流れ

  1. アプリケーションが task を lease する
  2. GAE が task データを返す
  3. アプリケーションが task を処理する
    1. もし lease の期限以内に処理を完了できなかった場合は再度 lease できる
    2. retry できる最大の回数はあらかじめ設定できる
    3. この回数を超えると GAE が task を削除する
  4. アプリケーションは task への処理が完了したら必ずその task を削除する

参照

GAE Datastore について

GAE の Datastore についてざっくり

Overview

  • スキーマのないオブジェクトデータベース
  • オートスケーリング
  • ディスクへの書き込み時に自動で暗号化、読み出し時に自動で復号
  • 計画的ダウンタイムなし
Concept Cloud Datastore Relational database
オブジェクトが所属するカテゴリ Kind Table
オブジェクト Entity Row
オブジェクトがもつデータ Property Field
オブジェクトを特定する ID Key Primary key
  • 同一 kind の entity でも違う property を持ちうる
  • それぞれの entity は同名の property でも型の違うデータを持ちうる

Other storages

  • 複数の table の join や 複数のカラムに対する不等号比較など、すべての SQL 操作が必要なら Google Cloud SQL
  • ACID transaction を必要としないスキーマレスなデータを扱うなら Google Bigtable
  • オンラインで分析されるデータを扱うなら Google BigQuery
  • 画像や動画などの変更がない大きなデータを扱うなら Google Cloud Storage

Entities

  • ひとつの entity は1つ以上の property をもつ
  • property は1つ以上の値を取りうる

Keys

  • key は entity を特定する
  • key は以下を含む
    • entity の kind
    • 以下のいずれかの識別子
      • 文字列の key name
      • 数値の ID
    • ancestor path (optional)
  • 識別子は entity が生成されたタイミングで設定される
  • 一度設定された識別子は変更されない
  • 識別子は以下の2つの方法で設定できる
    • アプリケーションで特定の key name を設定する
    • Datastore が自動で発行する数値の ID を使う

Ancestor paths

  • Datastore はファイルシステムのディレクトリ構成に似た階層構造をもつ
  • entity 作成時に parent entity を指定することができる
  • parent entity が指定されない entity は root entity と呼ぶ
  • 一度親子関係ができた entity はその関係が変更されることはない
  • 同じ parent をもつ2つの entity に対して Datastore は同一の ID を払い出すことはない
  • 同様に2つの root entity に対して同一の ID を払い出すこともない
  • parent や parent の更に parent を ancestor と呼ぶ
  • 逆に children や children の更に children を descendant と呼ぶ
  • root entity とその descendant は同一の entity group に属する
  • ancestor path は root entity からたどって該当の entity に到達するまでの親子関係で構成される

Transactions and entity groups

  • entity に対する create, update, delete はトランザクションで管理される
  • トランザクションにはこれらの複数の操作が内包される
  • トランザクションは一貫性の担保のため、そこに含まれる操作をひとまとまりとして適用する、あるいはいずれかの操作が失敗するとすべての操作を適用しない
  • commit を試みてエラーが返ってきたとしても、トランザクションが失敗したとは限らない
    • DatastoreTimeoutExceptionDatastoreFailureException といったエラーが返ってきたとしても、 commit は成功している可能性がある
    • このため、 Datastore は可能な限り同一のトランザクションを複数回適用しても最終的な結果が変わらないように設計すべきである
  • entity group とトランザクションの関係
    • 1つのトランザクションで扱うデータは 25 の entity group 内に収まっていなければならない
    • トランザクション内でクエリを発行する場合には正しいデータにマッチする ancestor filter を指定できるように entity group 内のデータを設計する必要がある
    • 広域に渡って各 entity group をレプリケーションするために、1つの entity group ごとに1秒あたりの書き込みスループットの上限値が定められている

Understanding write costs

  • 発生する書き込み
    • entity 自身
    • EntitiesByKind という index
    • 1つの property value ごとに EntitiesByPropertyEntitiesByPropertyDesc の各 index

以下のような entity を考える

1
2
3
4
Key: 'Foo:1' (kind = 'Foo', id = 1, no parent)
A: 1, 2
B: null
C: 'this', 'that', 'theOther'

このとき発生する書き込み

  • 1: entity 自身
  • 1: EntitiesByKind index
  • 4: A property の2つの値にそれぞれ2つの index
  • 2: B property の値に2つの index (null でも必要)
  • 6: C property の3つの値にそれぞれ2つの index

よって発生する書き込みの合計は 1 + 1 + 4 + 2 + 6 = 14

上記の entity に複合 index を追加することを考える

1
2
Kind: 'Foo'
A ▲, B ▼, C ▼

このとき、各 property の値の組み合わせ分の書き込みが発生する

1
2
(1, null, 'this') (1, null, 'that') (1, null, 'theOther')
(2, null, 'this') (2, null, 'that') (2, null, 'theOther')

したがって発生する書き込みの合計は 1 + 1 + 4 + 2 + 6 + 6 = 20

次にこれまでと同様に ancestor が存在する以下のような entity を考える

1
2
3
4
Key: 'GreatGrandpa:1/Grandpa:1/Dad:1/Foo:1' (kind = 'Foo', id = 1, parent = 'GreatGrandpa:1/Grandpa:1/Dad:1')
A: 1, 2
B: null
C: 'this', 'that', 'theOther'
1
2
3
Kind: 'Foo'
A ▲, B ▼, C ▼
Ancestor: True

このとき各 property の値と各 ancestor 及び自身の entity の組み合わせ分の書き込みを必要とする

1
2
3
4
5
6
(1, null, 'this', 'GreatGrandpa') (1, null, 'this', 'Grandpa') (1, null, 'this', 'Dad') (1, null, 'this', 'Foo')
(1, null, 'that', 'GreatGrandpa') (1, null, 'that', 'Grandpa') (1, null, 'that', 'Dad') (1, null, 'that', 'Foo')
(1, null, 'theOther', 'GreatGrandpa') (1, null, 'theOther', 'Grandpa') (1, null, 'theOther', 'Dad') (1, null, 'theOther', 'Foo')
(2, null, 'this', 'GreatGrandpa') (2, null, 'this', 'Grandpa') (2, null, 'this', 'Dad') (2, null, 'this', 'Foo')
(2, null, 'that', 'GreatGrandpa') (2, null, 'that', 'Grandpa') (2, null, 'that', 'Dad') (2, null, 'that', 'Foo')
(2, null, 'theOther', 'GreatGrandpa') (2, null, 'theOther', 'Grandpa') (2, null, 'theOther', 'Dad') (2, null, 'theOther', 'Foo')

したがって発生する書き込みの合計は 1 + 1 + 4 + 2 + 6 + 24 = 38

参照

パーフェクトJava読書メモ Chapter 7 インターフェース

Javaを使うために改訂2版 パーフェクトJavaを読んだメモ

クラスとインターフェースの違い

  • インターフェースは雛形としての役割を持たない
  • インターフェースは実体化できず、型定義に特化している
  • インターフェースの継承の目的は多様性のみ(クラスの拡張継承には実装コードの共有という側面もある)

インターフェース宣言

1
2
3
[修飾子] interface インターフェース名 {
    メンバ宣言
}

インターフェースの修飾子

modifier description
public グローバルにアクセス可(書かないとパッケージ内に限定)
abstract インターフェースは暗黙的に abstract なので書かなくても同じ
strictfp インターフェース内に記述した浮動小数点演算を厳密に評価
アノテーション 省略

インターフェースのメンバ

  • 抽象メソッド(実装なし)
  • default メソッド
  • static メソッド
  • 定数フィールド
  • static なネストしたクラス
  • static なネストしたインターフェース

メソッド宣言

modifier description
public 暗黙的に public なので書かなくても同じ
default デフォルトメソッド
static static メソッド
abstract 暗黙的に abstract なので書かなくても同じ
  • default メソッドはインスタンスメソッド
  • インターフェースを継承した具象クラスのインスタンスメソッドになる
  • フィールド変数は暗黙的に public static final

インターフェースと実装クラス

インターフェース継承

1
2
3
[修飾子] class クラス名 implements インターフェース名 {
    クラス本体
}

複数のインターフェースを同時に継承することができる

1
2
3
[修飾子] class クラス名 implements インターフェース名, インターフェース名 {
    クラス本体
}
  • クラスの拡張継承とインターフェースの継承を同時にできる
  • このとき implementsextends より後に書く
1
2
3
[修飾子] class クラス名 extends 親クラス名 implements インターフェース名 {
    クラス本体
}
  • インターフェースを継承したクラスはインターフェースの抽象メソッドをすべてオーバーライドする必要がある
  • インターフェースから継承したメソッドのアクセス制御は public 修飾子がないとコンパイルエラー
  • メソッドのオーバーライドはクラスの拡張継承と同様に行う

ネストしたインターフェース

  • クラス内のネストしたインターフェース
    • public protected 無指定 private のいずれかを指定する
  • インターフェース内のネストしたインターフェース
    • 常に public
  • インターフェース内のネストしたクラス
    • 常に public かつ static
  • ネストして宣言されたインターフェースは常に static

インターフェース自体の拡張継承

  • インターフェースも拡張継承できる
  • インターフェースは複数の親インターフェースを指定可能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Parent {
    void print();
}

interface Child extends Parent {
    // void print() を継承
}

interface Father {
    void print();
}

interface Mother {
    void exec();
}

interface Child2 extends Father, Mother {
    // void print(), void exec() を継承
}

参照

改訂2版 パーフェクトJava