その他の言語#
プログラミング言語
教科書該当なし
講義1回分
今日の講義
今日は動画形式で実施します.非同期です.
期末試験#
中間とほぼ同じスタイルで実施します.実施形態は講義の概要#試験を参照すること.一部記述の問題を出します.
期末試験の範囲は以下のとおりです.
- 関数型言語
- 中間試験とほぼ同じ形式です
- 論理型言語
- 中間試験とほぼ同じ形式です
- C言語
- 配列と副作用を持つ単項演算子は含めない
- SCコードの暗記は不要(試験中に補足資料として配布する)
- C言語プログラムがスタック計算機の上でどのように動いているかを理解すること
- Java言語
- GCは含めない
- JVMコードの暗記は不要(試験中に補足資料として配布する)
- ヒープとスタック,およびプリミティブ型と参照型を理解しておくこと
- その他の言語
- 試験範囲には含めない
今日の講義#
本ページの内容.
前回の宿題解説#
略.特に大学院進学の話.
序論#
本ページではこれまで触れなかった言語機能をいくつか紹介する.特に1,000行規模のシステムを構築する際に知っておくと有益なトピックを採り上げる.
toString()equals()- 不変オブジェクトと可変オブジェクト
- 例外処理
前半3つのトピックは動画を参照せよ.
例外処理#
例外処理は多くのプログラミング言語で採用される基本的な機能であり,実践的なプログラミングにおいて必須知識である.特にJavaでは検査例外が存在しているため,例外を知らずにJavaを使いこなすことはできない.
また例外の理解はデバッグの効率化にも繋がる.例外が発生した際のスタックトレースを理解できれば,エラー発生の原因となった箇所を極めて効率的に絞り込むことができる.
本設ではまず例外処理が必要となる状況を共有し,メソッドの提供者側と利用者側の2つの観点から例外処理の使い方を説明する.
例外の必要性#
Javaによるスタックの実装を例題に,例外の必要性を考える.
ArrayStackはint型の値を格納するスタッククラスであり,Mainがそのスタックを利用している.この例では,Mainクラスがスタックに対する過剰なpop()操作を呼び出している.ArrayStackクラスでは過剰pop()の対策を行っていないため,return elements[--sp]の部分で配列外参照が発生してしまいプログラムはクラッシュする.
この場合,ArrayStackクラスはどのように振る舞うのが適切だろうか? pop()の操作をどのように修正すればよいだろうか? まずはスタック利用(Main)側ではなく,スタック自体(ArrayStack)がどう振る舞うべきかを考える.
Note
プログラム開発時には,利用者(呼び出し側,caller)と提供者(呼び出される側,callee)の2つの異なる視点が存在している点を認識せよ.今回の場合,Mainクラスがスタックの利用者であり,ArrayStackクラスがスタックの提供者である.
演習のような小さなプログラムの開発中は,利用者側と提供者側の振る舞いを自由に変更できる場合が多い.この場合,提供者側を修正せずとも利用者側で過剰pop()が発生しないように気をつけるだけで良い.
他方,実際のプログラム開発では利用者≠提供者であるケースが多々ある.例えばStringクラスに対しては,Javaランタイム自体がStringの提供者であり,我々はその利用者である.この場合,提供者は利用者がどのような使い方をしても良いようにプログラムを提供するべきである.
以下のような対策が考えられるが,いずれも問題点がある.
対策1:何も返さない#
👎:そもそもコンパイルできない.int値を返しておらずint pop()というシグネチャのルールに従っていない.
対策2:強制終了する#
👎:危険すぎる.利用者が誤って過剰pop()をしてしまうとプログラム全体が強制終了する.配列外参照によるクラッシュとほぼ同様の振る舞いであり,問題は何一つ改善されていない.
対策3:特殊な値を返す#
👎:-1という値がスタック中にあったのか,過剰pop()してしまったのか判別できない.また「-1は特殊な値だ」という特殊ルールを利用者側が認識しておく必要がある.
対策4:特殊フラグを設ける#
ArrayStack側にエラーを表す特殊フラグフィールドを作る.
public class ArrayStack {
private int sp;
private int[] elements;
boolean error = false; // 特殊フラグフィールド
...
public int pop() {
if (sp - 1 <= 0) {
this.error = true; // エラーが発生したらフラグtrue
return -1; // この場合返り値は意味をなさない
}
return elements[--sp];
}
Main側ではpop()後にフラグを確認する.
public static void main(String[] args) {
stack.pop();
if (stack.error) { ... }
stack.pop();
if (stack.error) { ... }
👎:「pop()後にフラグを確認せよ」という特殊ルールを利用者が認識しておく必要がある.
プログラム処理中のエラー発生は,プログラミングにおいて極めて一般的な現象である.
- 配列外参照を行った(
elements[--sp]elements[elements.size()]) - 0除算を行った(
y = 1 / x) - ファイルの作成処理で,すでに同名のファイルが存在していた
- ネットワーク処理中に,何かしらの理由でコネクションが切断された
エラーが発生する条件のチェック自体は難しい問題ではない.スタックに対するpop()の場合if (sp - 1 <= 0)がエラー条件のチェック処理に該当する.問題は「エラーが発生した」という状況を提供者側がどのように表現し,クラスの利用者に伝えるかである.
多くのプログラミングでは,プログラム処理中のエラーの発生を表す方法が言語仕様として設けられている.このエラーのことを例外(exception),例外のハンドル方法を例外処理(exception handling)と呼ぶ.直感的には,returnによる正常時の返り値とは独立に異常時の返り値を定義する方法である.
例外の処理方法:提供者側#
Javaにおける例外処理方法を説明する.対策4(特殊フラグ)と同様,提供者側と利用者側の双方で例外の対策が必要である.提供者は例外を発生させ,利用者が例外を受け取るという処理を加える.
まず提供者側では,特定の状況下で例外を発生させる.
public int pop() throws EmptyStackException { // 例外を宣言に追加
if (sp - 1 <= 0) {
throw new EmptyStackException(); // 実際に例外を投げる
// return ??? // throwしたらreturn不可(到達不可)
}
return elements[--sp];
}
エラー条件if (sp - 1 <= 0)を確認した後,throw節によって例外インスタンスを投げる.throw節は例外を投げた直後にメソッドを終了する命令であり,例外専用のreturn節だと認識しても良い(1).
- 正常時は
returnで,異常時はthrowで値を返す,と解釈しても良い.
throw対象であるnew EmptyStackException()はnew節を用いた単なるオブジェクトのインスタンス化である.Javaはオブジェクト指向言語であり,例外そのものもクラスとして表現されている.throw時には例外をインスタンス化して投げる(1).
throw対象のクラスはjava.lang.Throwableクラスを継承している必要がある.
今回の例では,java.utilパッケージのEmptyStackExceptionクラスを投げている.Javaには様々な例外が用意されており,エラーの状況を表す適切な名前の例外クラスを探すと良い.スタック利用者が例外の名前だけを見て,何が起きているのか予想可能な例外クラスを選ぶ.
メソッド内でthrowする際には,必ずシグネチャにthrowsを加え,どのような例外を発生させ得るのかを宣言する必要がある.return節の返り値がint型であることをint pop()のように宣言するのと同じ理由である.シグネチャとはメソッドの仕様である(1).
- 複数の例外をメソッド内で
throwしても構わない.この場合,throws Exception1, Exception2のように複数種類の例外が発生する可能性があることを宣言する.
Quiz
以下のプログラムはthrowsによる宣言と,throwによる実際の例外クラスが異なるにも関わらずコンパイルエラーは発生しない.その理由は何か?
public int pop() throws Exception {
if (sp - 1 <= 0) {
throw new EmptyStackException();
}
return elements[--sp];
}
Answer
EmptyStackExceptionクラスがExceptionクラスを継承しているから.
EmptyStackException is a Exceptionの関係が成り立つ.
Object o = new String("a");が成り立つのと同じ理由である.
String is a Objectの関係が成り立つ.
例外の処理方法:利用者側#
利用者側ではtry catch節によって例外を掴む必要がある.
例外を発生させ得るメソッドpop()の呼び出しは,必ずtry { ... }ブロックで囲む必要がある.tryは「失敗するかもしれない処理を試す」というニュアンスである.もしpop()内で例外が投げられた場合,catch() { ... }側に処理が移る.「発生した例外を掴む」というニュアンスである.提供者側がthrow節で投げた例外インスタンスはcatch(例外の型 e)の変数eとして受け取ることができる.
例外発生時にcatch内でどのような処理を行うべきかは状況によって異なる.回復不能な致命的な状況であれば強制終了catch (..) { System.exit(1); }とすることもある.あるいは過剰pop()を無視して良ければcatch (..) {}としても良い.これを例外を握りつぶすとも言う.
例外クラスはprintStackTrace()というメソッドを必ず持っている(継承している).例外が発生した瞬間のメソッドの呼び出し系列(1)を標準出力に書き出すメソッドである.例外発生の状況把握に役立つため,ひとまずcatch後にはe.printStacktrace()してみると良い.
- スタックフレームの情報のこと.
標準出力には以下のような内容が書き込まれる.日常的によく見るスタックトレースである.
java.util.EmptyStackException // ← 発生した例外の名前
at jp.osakau.ArrayStack.pop(ArrayStack.java:18)
at jp.osakau.Main.main(Main.java:13)
// ↗ ↗ ↑ ↖
// パッケージ名 クラス名 メソッド名 ファイル名:行番号
これまで説明した例外処理を適用済みのMainクラスとArrayStackクラスを以下に示す.
public class Main {
public static void main(String[] args) {
ArrayStack stack = new ArrayStack();
stack.push(1);
stack.push(2);
System.out.println(stack);
try { // まとめてtry-catchも可,ただし範囲はでできるだけ狭く
stack.pop();
stack.pop();
System.out.println(stack);
stack.pop();
} catch (EmptyStackException e) {
e.printStackTrace(); // 適切な対策が不明なのでひとまずprint
// TODO // TODOをつけておくと良い
}
}
}
検査例外と非検査例外#
Javaには検査例外と非検査例外の2種類の例外がある.検査例外は検査が必須となる例外である.検査していない場合はコンパイルエラーとなる.
検査例外を発生させるメソッドを呼び出す際は,try catchで例外を直接検査するか,throws宣言を加えて例外をさらに親メソッドへ投げる必要がある.他方,非検査例外は例外の検査がオプショナルであり,利用者側で例外の発生を無視することができる(1).
- 先の例のEmptyStackExceptionクラスは非検査例外である.つまりMainクラス側での
try catchを省略してもコンパイル・実行が可能である.
Javaではファイル周りの操作で検査例外が多数設けられている.よってJavaによるファイル操作はプログラムの記述量が多くなりがちである(1).
- 他の言語と比較して厳格だといえる.
try {
Files.delete(Paths.get("tmp.txt"));
} catch (NoSuchFileException e) { // fileがないとき(検査例外)
...
} catch (DirectoryNotEmptyException e) {// dirが空でないとき(検査例外)
...
}
スレッド周りの処理も検査例外が多い.
Pythonは全て非検査例外であり検査例外が存在しない.つまり利用者側による例外検査は必須ではない.
Quiz
以下のプログラムを実行したときに,f2()が生成し投げた例外がf1()でどう扱われるかを考えよ.Exceptionクラスは検査例外である.
| クイズ | |
|---|---|
Quiz
以下のプログラムを実行したときに何が起こるか考えよ.
| クイズ | |
|---|---|
Answer
出力は以下の通りである.先のクイズのスタックトレースにいくつかの情報が加えられている.
ERROR!
Exception in thread "main" java.lang.Exception:
at Main.f2(Main.java:10)
at Main.f1(Main.java:7)
at Main.main(Main.java:4)
発生した例外を誰も掴まなかった場合,JavaランタイムがERROR! Exception in ..というメッセージとともに,その例外に対してprintStackTrace()を呼び出す.
先述の通り,検査例外を検査しなければコンパイルエラーとなる.このコンパイルエラーを避けるためにthrowsを適当に追加すると,上記のような状況になりやすい.Eclipse等のIDEのコード補完はこのthrowsによる解決を推奨してくることがあるが,検査例外は可能な限りtry catchを用いて適切に処理することを推奨する.
例外が発生しても回復可能なケースが多々ある.例えば,ファイル削除の際には「指定したファイルが存在しない」状況を保証できれば良い場合がある.この場合,以下のように例外を処理すれば良い.
API仕様の読み方#
オブジェクト指向言語と上記例外の内容を理解できれば,JavaのAPI仕様を読めるようになる.Google等で下手に検索するよりも,公式のAPI仕様を読んだほうが問題解決の近道となる場合が多い.ドキュメントを読む上手さもプログラミングの必須能力だと心がけよ.
以下はjava.nio.file.Files#delete()のAPI仕様である.
delete#ファイルを削除します。
(略)
ファイルがディレクトリの場合、ディレクトリは空である必要があります。 (略)
パラメータ:#
path- 削除するファイルへのパス例外:#
NoSuchFileException- ファイルが存在しない場合(オプションの固有例外)DirectoryNotEmptyException- ファイルがディレクトリで、ディレクトリが空でないために削除できなかった場合(オプションの固有例外)- (略)
Quiz
おそらく最も目にする例外はNullPointerException(NPE)である.null要素に対してメソッド呼び出しなどの操作を行うと発生する例外である.
String s = null;
s.toUpperCase(); // NPE発生
Stack stack = null;
stack.pop(); // NPE発生
List l = null;
l.size(); // NPE発生
ERROR!
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "s" is null
at Main.main(Main.java:8)
NullPointerExceptionは検査例外か?非検査例外か?
Quiz
以下6種類のメソッドについて,コンパイルエラーとなるメソッドはどれか考えよ.Exceptionは検査例外,RuntimeExceptionは非検査例外である.
void f1a() {
throw new Exception();
}
void f1b() {
throw new RuntimeException();
}
void f2a() throws Exception {
throw new Exception();
}
void f2b() throws RuntimeException {
throw new RuntimeException();
}
void f3a() {
try {
throw new Exception();
} catch(Exception e) {}
}
void f3b() {
try {
throw new RuntimeException();
} catch(RuntimeException e) {}
}
Answer
f1a()のみコンパイルできない.検査例外を検査していないためである.
《宿題》#
Homework
今日は宿題はありません.試験勉強をがんばってください.