基本情報技術者 2014年 秋期 午後 問11
次のJavaプログラムの説明及びプログラムを読んで、設問1,2に答えよ。
〔プログラムの説明〕
D君は、社内向けの応用プログラム(以下、アプリケーションという)で共通に使うライブラリを開発するチームに属している。ライブラリの次のバージョンで、時間に関するクラスを幾つか用意することになり、D君は期間(時間間隔)を表すクラスPeriodの開発を担当することになった。このクラスは、“有効期間は、今週月曜日の午前0時から金曜日の終わりまで(土曜日の午前0時以降は、無効)”など、ある日付及び時刻(以下、日時という)から別の日時までの時間間隔を表すことを想定している。期間の基準になる日時を始点、終わりを表す日時を終点という。また、ある始点から過去に遡って期間を表すことも想定している。D君が定めたクラスPeriodの外部仕様は、次のとおりである。
クラスPeriodは、始点から終点までの期間を表す。期間には、始点は含まれるが終点は含まれない。始点と終点が同じ日時の場合は、期間が空であると定義し、いかなる日時も含まないものとする。
(1) コンストラクタは、引数で与えられた始点(start)と終点(end)から期間を表すインスタンスを生成する。日時は、クラスDateのインスタンスで与えられ、協定世界時1970年1月1日午前0時(以下、この日時をエポックという)以降でなければならない。引数start又はendがnullの場合は、NullPointerExceptionを、日時がエポックよりも前の場合は、IllegalArgumentExceptionを投げる。
(2) メソッドgetStart:始点を返す。
(3) メソッドgetEnd:終点を返す。
(4) メソッドgetLength:期間の長さ(ミリ秒)を返す。終点が始点よりも前の場合は、負の値で返す。期間が空の場合は、0を返す。
(5) メソッドisBackward:終点が始点よりも前の場合は、trueを返す。それ以外は、falseを返す。期間が空の場合は、falseを返す。
(6) メソッドcontains:引数で与えられた日時が期間に含まれる場合は、trueを返す。それ以外は、falseを返す。期間が空の場合は、falseを返す。引数がnullの場合は、NullPointerExceptionを投げる。
この仕様でチームの承認を得て、次のプログラム1を作成した。

次に、D君は、テスト用にプログラム2を作成した。問題が検出されなければ、プログラム2は何も出力せずに終了する。問題を検出した場合は、RuntimeExceptionを投げる。

設問1:
問題文を見る プログラム1及び2中のに入れる正しい答えを、解答群の中から選べ。ただし、プログラム2を実行したとき、例外は発生せず正常に終了するものとする。
a〜cに関する解答群
ア:!=
イ:<
ウ:<=
エ:==
オ:>
カ:>=
d, eに関する解答群
ア:IllegalArgumentException
イ:IllegalArgumentException.class
ウ:IllegalArgumentException.getClass()
エ:new IllegalArgumentException()
オ:new NullPointerException()
カ:NullPointerException
キ:NullPointerException.class
ク:NullPointerException.getClass()
模範解答
a:ウ、b:オ、c:イ、d:キ、e:イ
解説
解答の導き方
まず、期間に日時が含まれる条件を考えます。期間は始点を含み、終点を含みません。つまり、始点から終点へ進む期間では「始点以上、終点未満」が条件です。一方、終点が始点より前の期間では、時間を逆向きにたどるため「終点より後、始点以下」が条件になります。
JavaのDateクラスは日時を表すクラスです。compareToは、日時を比べた結果として、呼び出し元の日時が相手より前なら負の値、同じなら0、後なら正の値を返します。
プログラム1のcontainsには、まず通常の向きの条件が書かれています。time.compareTo(start)が0以上なら、引数の日時は始点と同じか後です。また、time.compareTo(end)が0未満なら、終点より前です。この2つを満たせば、日時は期間に含まれます。
次に、逆向きの期間を考えます。この場合は、日時が始点と同じか前であり、終点より後であれば含まれます。したがって、aには「以下」を表す<=が入り、解答群ではウです。bには「より大きい」を表す>が入り、解答群ではオです。
この条件が仕様に合うことは、テストの日時からも確認できます。始点をnow、終点をnow + DELTAとする通常の期間では、始点は含み、終点は含みません。逆向きの期間では、始点のnowは含み、終点のnow - DELTAは含みません。プログラム2のvalidとinvalidには、始点や終点の直前・直後の日時が含まれており、境界での判定も確かめています。
始点と終点が同じ日時の空の期間では、通常の向きの条件も逆向きの条件も同時には成立しません。そのため、どの日時も含まれず、仕様どおりfalseになります。また、引数がnullならtime.compareTo(start)を実行する時点でNullPointerExceptionが発生します。
次にcを考えます。getLengthは、終点の時刻から始点の時刻を引いています。そのため、終点が始点より前なら長さは負になります。isBackwardがtrueになる条件も「終点が始点より前」です。よって、testConsistencyで長さの符号から逆向きかどうかを確かめるには、lengthが0より小さいかを調べます。cには<が入り、解答群ではイです。長さが0の空の期間では、length < 0はfalseとなり、isBackwardもfalseなので一致します。
最後に、dとeを確認します。testExceptionのexpectedにはClass型が指定されています。これは、発生した例外の種類を表す値を受け取るためです。Javaでは、例外のクラス名の後ろに.classを付けると、そのクラスを表す値になります。
コンストラクタにnullを渡したときに必要なのはNullPointerExceptionなので、dにはNullPointerException.classが入ります。解答群ではキです。エポックより前の日時を渡したときに必要なのはIllegalArgumentExceptionなので、eにはIllegalArgumentException.classが入ります。解答群ではイです。new NullPointerException()のように書くと、例外を表すクラスではなく、例外のインスタンスを作ることになるため、expectedの型には合いません。
誤りやすいポイント
- 終点を含むと考えてしまうことです。この問題では「始点は含まれるが終点は含まれない」ため、終点との比較は通常の向きでも逆向きでも厳密な不等号になります。
- 逆向きの期間でも通常の条件をそのまま使ってしまうことです。逆向きでは、始点以下かつ終点より後が条件です。
- 期間の長さが0のときに、逆向きだと判断してしまうことです。isBackwardがtrueなのは終点が始点より前のときだけなので、長さが0ならfalseです。
- 例外のクラス名と、例外のインスタンスを混同することです。expectedにはクラスを表す値が必要なので、.classを付けます。
- 境界の日時を見落とすことです。始点は含み、終点は含まないので、始点と終点そのものを使ったテストが判定の確認に役立ちます。
FAQ
Q: 始点と終点が同じ日時なら、なぜその日時も含まれないのですか?
A: 仕様で、その場合は期間が空であり、いかなる日時も含まないと定められているためです。通常の向きの条件では、日時が始点以上かつ終点未満である必要がありますが、始点と終点が同じなら両方を満たせません。逆向きの条件も同様に成立しません。
A: 仕様で、その場合は期間が空であり、いかなる日時も含まないと定められているためです。通常の向きの条件では、日時が始点以上かつ終点未満である必要がありますが、始点と終点が同じなら両方を満たせません。逆向きの条件も同様に成立しません。
Q: 例外のクラス名だけではなく、なぜ.classを付けるのですか?
A: testExceptionは、発生した例外のクラスとexpectedを比較しています。expectedはクラスを表す値を受け取るため、NullPointerExceptionやIllegalArgumentExceptionというクラス名だけでなく、NullPointerException.classやIllegalArgumentException.classと指定します。
A: testExceptionは、発生した例外のクラスとexpectedを比較しています。expectedはクラスを表す値を受け取るため、NullPointerExceptionやIllegalArgumentExceptionというクラス名だけでなく、NullPointerException.classやIllegalArgumentException.classと指定します。
Q: 期間の長さが負になるのは、計算に失敗しているからですか?
A: いいえ。この問題では、終点が始点より前の期間も扱います。終点の時刻から始点の時刻を引くため、逆向きの期間では長さが負になり、その符号で向きも確認できます。
A: いいえ。この問題では、終点が始点より前の期間も扱います。終点の時刻から始点の時刻を引くため、逆向きの期間では長さが負になり、その符号で向きも確認できます。
関連キーワード: 半開区間、比較演算子、例外処理、クラスリテラル、境界値テスト
設問2:
問題文を見る 次の記述中のに入れる正しい答えを、解答群の中から選べ。
チーム内でのコードレビューも終わり、クラスPeriodを含むライブラリが評価版として社内にリリースされた。それからしばらくして、アプリケーション開発チームから、アプリケーションの実行中にPeriodのインスタンスが表す期間が変わってしまうという指摘を受けた。D君は、プログラムを見直したがクラスもフィールドも最終(final)なので、生成後にインスタンスの状態が変化することはないと考えた。そこで、D君は、アプリケーション開発チームと一緒にデバッグを行った。
その結果、アプリケーションでは、PeriodのメソッドgetStartで始点を取得し、そのDateインスタンスの表す日時を変更して別のPeriodのインスタンスを生成するときの引数に使用していることが分かった。これが原因で、先に生成済みのPeriodのインスタンスの始点が変更されていた。すなわち、Periodインスタンス自体は不変でも、それから参照されるDateインスタンスは可変なので、表している期間が変わってしまうことがある。したがって、Periodのインスタンスで始点及び終点を保持する場合は、外部からの変更ができないようにする必要がある。また、クラスDateは、最終(final)ではないので、どのような下位クラスでも作成可能であり、上書きしたメソッドについて挙動の変更が可能である。これらは、データの改ざんを許すことになるので、セキュリティ上の問題でもある。これらの問題を修正するために、D君は、クラスPeriodに次の変更を加えた。
(1) コンストラクタの処理において、this.startの代入文の右辺をnew Date(start.getTime())に変更した。this.endの代入文についても同様の変更をした。この変更によって、フィールドstart及びendは、それぞれ引数と同じ値をもつ別のjava.util.Dateのインスタンスを参照する。
(2) メソッドgetStartの返却値をfに変更した。メソッドgetEndについても同様の変更をした。
D君は、プログラム2のメソッドmainのαで示した部分に次のプログラムを追加し、正しく修正されたことを確認した。

fに関する解答群
ア:(Date) start.clone()
イ:null
ウ:start.clone()
エ:start.getTime()
オ:start.setTime(start.getTime())
gに関する解答群
ア:0
イ:DELTA
ウ:DELTA * 2
エ:DELTA * 3
オ:DELTA / 2
カ:DELTA / 3
模範解答
f:ア、g:イ
解説
解答の導き方
この設問では、コンストラクタとメソッドが日時をどう扱うかを確認し、fとgを判断します。
まず、プログラムの変更点(1)では、コンストラクタが受け取った日時を、そのままフィールドに保存せず、別のDateインスタンスとして保存します。これは、受け取ったDateが後から変更されても、Periodが表す期間を変えないためです。
ただし、コンストラクタでコピーするだけでは十分ではありません。メソッドgetStartがフィールドのDateをそのまま返すと、呼び出した側がそのDateを変更できるからです。そこで、getStartもフィールドとは別のDateを返す必要があります。
選択肢のうち、Dateの複製を返すのはアの「(Date) start.clone()」です。cloneはオブジェクトの複製を作るメソッドです。ここでは複製をDateとして扱うため、(Date)という型変換も付けます。したがって、fはアです。
次に、gを考えます。プログラム2では、最初にstartをnow、endをnow + DELTAとしてPeriodを作っています。この期間の長さは、次のとおりです。
Periodを作った後、元のstartをnow - DELTAに、endをnow + DELTA * 2に変更します。しかし、変更後のプログラムでは、Periodのフィールドは元のDateとは別のインスタンスを参照しています。そのため、Periodの始点と終点は変わらず、長さもDELTAのままです。
続いて、getStartとgetEndで取得したDateも、それぞれフィールドとは別の複製です。その複製を変更しても、Periodの始点と終点には影響しません。したがって、2回目のtestConsistencyでも長さはDELTAのままです。
つまり、どちらのtestConsistencyにも渡すgは、選択肢のイのDELTAです。
誤りやすいポイント
- finalを付けても、参照先のオブジェクトの中身まで変更できなくなるわけではありません。finalが防ぐのは、フィールドが別のオブジェクトを指すように再代入することです。
- コンストラクタでコピーすれば十分だと考えないようにします。getStartやgetEndからフィールドのDateを直接返すと、呼び出した側がそのDateを変更できます。
- getStartで返すDateを複製しても、コンストラクタでコピーしていなければ、コンストラクタに渡したDateを変更することで期間が変わる可能性があります。外からの変更を防ぐには、受け取るときと返すときの両方でコピーが必要です。
- gは、変更後のstartとendの差ではありません。変更後もPeriodが元の日時を保持できているかを確かめる値なので、元の期間の長さであるDELTAを選びます。
FAQ
Q: なぜフィールドをfinalにするだけでは、期間を変えられないようにできないのですか?
A: finalはフィールドが参照するDateを別のDateに置き換えることを防ぎます。しかし、Date自体は変更できるオブジェクトです。そのため、同じDateを参照する別の箇所から変更される可能性があります。
A: finalはフィールドが参照するDateを別のDateに置き換えることを防ぎます。しかし、Date自体は変更できるオブジェクトです。そのため、同じDateを参照する別の箇所から変更される可能性があります。
Q: コンストラクタでDateを複製するだけでは、なぜ不十分なのですか?
A: getStartやgetEndがフィールドのDateをそのまま返すと、その返却値を通じてフィールドのDateを変更できるからです。返すときにも複製し、外部からフィールドのDateを直接変更できないようにします。
A: getStartやgetEndがフィールドのDateをそのまま返すと、その返却値を通じてフィールドのDateを変更できるからです。返すときにも複製し、外部からフィールドのDateを直接変更できないようにします。
Q: getTimeは何を返しますか?
A: Dateが表す日時を、ミリ秒数として返します。コンストラクタの変更では、その値を使って新しいDateを作ることで、元のDateとは別のインスタンスを用意しています。
A: Dateが表す日時を、ミリ秒数として返します。コンストラクタの変更では、その値を使って新しいDateを作ることで、元のDateとは別のインスタンスを用意しています。
関連キーワード: 可変オブジェクト、防御的コピー、参照、不変オブジェクト、型変換



