トップ

発展プログラミング演習

2024 年度 春学期 発展プログラミング演習の講義資料です. 以下の内容を読み,授業時間までに練習問題に取り組んでください.

なお,2024 年度 春学期は対面で実施します. 詳細は講義の進め方をご参照ください.

補講

以降の内容は応用的な内容であり本講義の範囲を超えるため,実施しなくても成績に影響はありません. 一方,実施すると少し成績に加味されますので,ぜひ挑戦してみてください.

トップのサブセクション

シラバス

担当教員

  • 玉田 春昭
  • 宮森 恒
  • 林原 尚浩

授業概要

本演習では,1年生次に学習した基礎プログラミング演習I, II の知識を前提に,Java言語によるプログラミング の演習を行う.演習の主題は,プログラミング言語そのものではなく,プログラミング言語を使ったアプリケーショ ンの構築にある.そのため,プログラミングに関する基礎的な知識は,各自での復習/予習が必須である.

なお,この科目に先立ち,以下の科目の履修・習得を強く推奨する.

  • 情報化社会論
  • コンピュータ概論
  • 基礎プログラミング演習I, II

また,この科目の内容は,以下の科目の習得に役立つ.

  • ソフトウェア工学I
  • アルゴリズムとデータ構造
  • 言語オートマトン
  • ソフトウェア工学II
  • プログラミング言語
  • プロジェクト演習
  • コンピュータ理工学特別研究I,IIA, IIB(研究室問わず)
  • 応用プログラミング(Java)
  • 応用プログラミング(Python)
  • 応用プログラミング(C)
  • 応用プログラミング(Web)
  • 応用プログラミング(データ解析)
  • 応用プログラミング(アルゴリズム)

授業方法

  • 演習(アクティブ・ラーニング授業 (形態:反転授業))
    • 授業では,全体に対して細かな説明は行わない.各自のわからなかった箇所について個別に説明する.

授業内容・授業計画

この講義の授業は次の通り進行する予定である.なお,それぞれの単元が2コマ分である.

  1. Java言語の基礎1
    • Java言語の基礎の基礎
      • コンパイル方法
      • 実行方法
    • 基礎文法
      • 条件分岐
      • 繰り返し
  2. Java言語の基礎2
    • コメント
    • クラス定義
    • 変数
    • 配列
    • コマンドライン引数
  3. Java言語の基礎3
    • メソッド
      • メソッドの呼び出し方.
      • メソッドの定義方法.
      • 仮引数と実引数.
      • メソッド内での変数の更新.
      • 変数のスコープ.
  4. Java言語の基礎4
    • 配列とList
    • 独自の型の作成
  5. 数値計算(1/2)
    • 複素数型(独自の型の作成)
    • 任意桁の計算(BigInteger
  6. 数値計算(2/2)
    • 再帰呼び出し
  7. ファイル
    • ファイルを扱う型.
  8. 画像操作
    • 画像の書き込み
    • 例外機構
    • 例外の責任転嫁
    • 画像の変換(アフィン変換)
  9. ファイル入出力
    • ストリーム
    • ReaderWriter
  10. 連想配列(Map
    • 連想配列の概念
    • HashMap
  11. 応用1−1
    • 今までの学習した内容に従い,実用的な小さなプログラムを多数作成する.
  12. 応用1−2
    • 今までの学習した内容に従い,実用的な小さなプログラムを多数作成する.
  13. 応用2−1
    • 今までの学習した内容に従い,大きな一つのプログラムを作成する.
  14. 応用2−2
    • 今までの学習した内容に従い,大きな一つのプログラムを作成する.
  15. 最終課題
    • 最終課題の内容説明.
  16. 試験,まとめ

準備学習等(事前・事後学習)

事前学習

授業に必要な資料は全てWebページ上に掲載するので,毎回の授業当日までに十分に読み込んでおくこと. また,必要に応じて実際に作業を行い,自分が理解できない箇所をチェックしておくこと. 特にキー入力操作が不慣れで授業中の作業時間で間に合わないと感じる場合は, 事前に作業しておくなどの準備をすること.

事後学習

授業終了後に資料のWebページおよび授業中に取ったノートや演習成果物を見直し,学習内容を確認して理解を深めること.

本演習で扱う知識は基礎的なものであり,いずれが欠けても今後の学部の専門授業の多くに支障を来す. そのため,理解不十分なまま放置しないこと.

授業の到達目標

  • Java言語の基礎知識を身に付け,簡単なJavaプログラムが書けるようになる.
  • 与えられた仕様に従って,簡単なJavaプログラムが書けるようになる.
  • 基礎的なアルゴリズムが理解できる.
  • 基礎的なアルゴリズムが利用できる.

身に付く力

  • プログラムを書くことによる,論理的思考力(創造力,論理的分析力,総合的判断力)
  • プログラムの内容を説明することによる,コミュニケーションスキル(傾聴力,発信力)
  • 演習に参加することによる,態度・志向性(規律性,ストレスコントロール力)

履修上の注意

  • Java言語とC言語は異なる言語であるものの,基礎プログラミング演習I, IIで学習したC言語の内容理解は必須である. この内容理解が不十分であれば,本科目の習得は難しい.特に,関数,変数のスコープ,型,配列について,各自しっかりと復習しておくこと.
  • タイピング速度は習熟度に大きく影響する. タッチタイピングは必須であり,1分間に120字程度(日本語入力で60字程度)が入力できるようにしておくこと. 入力速度が120字/分以下の場合は,単位を認めない場合もある.タイピング速度は,次のサイトで測定できる.
  • 演習資料は事前によく読んでおくこと.また,演習資料中の例題は必ずコンパイル・実行すること.
  • 疑問点は積極的に質問すること.
  • レポートを課す場合がある.講義中の指示に従って,提出期限までに提出すること. レポートとして提出するすべてのプログラムに,必ず指名,学生証番号,課題番号を記載すること. これらが記載されていない場合,未提出として扱う.

評価方法

  • 最終課題(30~70%),筆記試験(30%~70%)を総合して評価する.
  • 講義中に随時行う課題(小テスト,練習問題)を評価(0%~40%)に加える場合もある.
  • 詳細は講義中に説明する.
  • 課題は提出期限終了後,模範解答とともに解説を配布する.

教材

参考書

Javaで学ぶオブジェクト指向プログラミング入門

その他

オフィスアワーについて

玉田のオフィスアワー

  • 木曜日3時限目
    • 第2実験室棟3階 68研究室
      • 上記の時間でなくても,在室中は基本的に質問を受け付ける.
        • 他の用事で不在の場合もあるので,在室を確認してから訪ねてくると良い.
        • メールアドレス:tamada-f@ke-cc.kyoto-su.ac.jp (-f@ke-をアットマーク(@)に置き換えること)
    • Microsoft Teamsのチャットにて問い合わせても良い.

昨年度までの講義資料について

講義の進め方

簡単なまとめ

2024年度春学期の発展プログラミング演習は対面で実施します.

  • 発展プログラミング演習では,練習問題を多く解くことにより学習を進めていきます.
    • 教員は小テスト,練習問題の解説を中心に行い,知識学習は各自で行ってもらいます.
  • 各自講義資料を読み,掲載されている練習問題(課題)に取り組んでください.
    • 今年度も昨年度に引き続き,Microsoft Teamsによるサポートを実施します.
    • 練習問題はMoodleにて提出してください.
  • 授業中の9:00〜12:15の間にMoodle上で小テストを実施します.各自受講してください.
  • まとめのまとめ(皆さんが行わないといけないこと)
    • Java環境の準備を確認し,Javaの実行環境を用意してください.
    • 各授業開始時(9:00〜12:15)に行われる小テストに取り組んでください.
    • それぞれの回の練習問題に取り組み,Moodle 上で提出してください.
  • ChatGPT を使うのは構いませんが,使い方には気をつけましょう.
    • 課題を解いてもらうために使うのではなく,次のように使いましょう.
      • プログラムのわからない箇所を説明してもらう.
      • バグの場所を見つけてもらう.

授業の実施形態について

実施形態

  • 対面で実施します.
    • これまでと同様に練習問題を解くことを中心に演習は進みます.
    • 教員は小テスト,練習問題の解説を中心に行います.
    • 知識学習は,オンラインで提供されている教材を読むことで各自で進めてください.
  • わからない箇所は,挙手やMicrosoft Teamsで質問,相談してください.
    • 質問の内容はなんでも構いません.質問の内容が成績に繋がることは決してありません.
      • どんなにつまらないと思う内容であっても,質問内容で評価が下がることはありませんし,叱責することもありません.
      • ただし,自分で調べて欲しい内容など,そのまま解答を伝えない場合もあります.
    • Microsoft Teamsでのテキストチャットは時間を気にせず質問を投稿できます.ただし,授業時間中以外は迅速に返信できないことをご了承ください.
      • チャットにおいて「質問いいですか?」のような枕詞は不要です.そのまま唐突に質問を投稿してください.
    • ビデオ通話が必要な場合は,必要に応じてテキストチャットで接続情報を送付します.
  • 授業中の9:00〜12:15の間にMoodle上で小テストを実施します.各自受講してください.
    • 小テストという名前ですが,チャット,挙手などでの質問,相談はOKです.
    • 10:00より,教員により解説が行われます.
      • すでに提出した人は内容を確認し,不明な点を解消するようにしてください.
      • 小テストに手も足も出ない人は,解説を確認し,書き方を学びましょう.
      • もう少しで解決できそうだけど,なかなか解決できない人は,教員,TAに質問しましょう.
    • 小テストの解説が終わり次第,前回の課題の解説が行われます(前回の課題の提出期限はその時間内です).
      • すでに提出できている人は内容を確認し,不明な点を解消するようにしてください.
      • 提出できていない人は,解説を確認し,書き方を学びましょう.

講義資料

練習問題について

  • 完成した練習問題は,決められた期日までに次の方法にて提出してください.
    • 提出方法
    • それぞれの課題の次の週の12:15までに提出してください.
      • 例えば,第1講は2024-04-11に開催されます. そのため,第1講の課題の〆切は,2023-04-18 12:15 であり,それまでに Moodle の決められた場所に提出してください.
  • 小テストや練習問題に取り組むにあたり,友人との相談やインターネットでの調べ物は自由に行ってもらって構いません.
    • ただし,ソースコードの丸ごとコピー&ペーストはだけは行わないようにしてください.
    • コピーチェックは特に行いませんが,内容を順番に理解しないと最終課題で何も行えなくなるためです.
    • もし,例題や練習問題,小テストに手も足もでない場合は,教員やTAに早めに相談することをお勧めします.

ChatGPTについて

2022-11-30 に OpenAIChatGPT という非常に汎用的なチャットシステムが公開されました. 様々なブログや記事などでも提示されているように,ChatGPT にプログラムを書かせることも可能です. ただし,ChatGPT で作成されたプログラムが理解できなければ利用する意味がありません.

一方で,プログラムの説明やバグ修正にChatGPTを利用するのは良い利用方法です. 作成したプログラムをChatGPTに投げ,説明するよう求めると,各行で何が行われているのかが説明されます. また,期待通りの動作にならない場合に,ChatGPT に助けを求めると教えてくれる場合があります.

プログラムが書けるようになるのが基礎プログラミング演習や発展プログラミング演習の目標です. 自身の代わりにプログラムを作成した場合,プログラミング演習科目の目標が達成できない可能性が高いです. そのため,この目標を達成するために様々な道具を利用するようにしましょう.

授業開始前(2024-04-11)までに行うべきこと

  • このページをみたら,まず初回授業の開始前に行っておくことを確認し,環境構築をしておいてください.
  • また,第0講を読み,練習問題に取り組んでおいてください.
    • 第0講の練習問題の提出期日は初回授業日(2024-04-11 (Thu) )の 12:15 です.

授業の準備

授業の準備のサブセクション

授業に臨むにあたって

タッチタイピングについて

シラバスにも書いていますが,タッチタイピングができない人はできるように練習してください. タッチタイピングができないことのみで,単位を落とすことはありませんが, タッチタイピングができないことは,単位取得に非常に不利になります.

タッチタイピングができるよう練習しておいてください. 1分間に120タイプ程度できることが望ましいです.

インデントについて

例年,インデントをしっかり行なっていない学生が多く見られます. その多くは,あまりプログラムが得意でない学生が多いようです.

インデントは,変数の有効範囲や,ブロックの範囲を視覚的に見るための基本的なテクニックです. そのため,こまめにインデントを行いましょう.

もちろん,プログラムを書き進めるうちにインデントが崩れるのは仕方のないことです. それを手作業で戻り,インデントし直すのは面倒なのもわかります. だからやらない,のではなく,Q&Aにも掲載しているように, 簡単にできる方法を用いてインデントするようにしてください.

近年のエディタには一括インデントの方法が必ず存在します. 例えば,Visual Studio Codeでは Option+Shift+F で, Emacs では M-C-\ で選択範囲の一括インデントが行えます. それを調べ,こまめにインデントを行うようにしましょう.

⚓ インストール

この講義では,Javaを使ったプログラミングの演習科目です. そのため,Javaの開発環境とエディタが必要です. 以下の内容を読み,必要なソフトウェアをインストールしてください. なお,本講義では,Javaとエディタの利用に 仮想環境を利用する方法ローカル環境にインストールする方法の2通りを紹介しますが, 仮想環境を利用する方法を推奨します.

演習環境のインストール方法

エディタのインストール

この講義では,基礎プログラミング演習I, IIと同様に,エディタを利用してプログラムを作成します. Visual Studio Code for macOS を推奨します. 上記の演習環境のインストール方法で,仮想環境を選択した場合,利用するエディタは Visual Studio Code for macOS に限定されますので,ご注意ください. 他のエディタを使いたい場合は,Java環境をローカル環境にインストールしてください.

ただ,自分の好みのエディタを利用してもらっても構いません. ただし,次の機能の使い方を知っておくと,これ以降の演習の手間が大きく省けるでしょう.

  • ソースコード全体の一括インデント
  • インクリメンタル検索
  • カーソルの行頭,行末,先頭行,末尾移動
  • Javaのシンタックスハイライト

推奨

その他のエディタ

非推奨のエディタ

各授業の開始前に行っておくこと

各授業の開始前に行っておくこと

本講義は,履修者それぞれが行う各講義前の準備が非常に重要である. 特に,毎回の授業の予習,復習が重要であるため,各自,しっかりと行うこと.

以下の図は,毎回の授業前後で行うべき予習・復習の大まかな流れである(画像のクリックで行うべきことが現れる).

事前に講義資料をよく読み,内容の理解に務めること. また,内容理解の助けとなる例題も掲載されている. その例題についても取り組んでおくこと.

講義資料を読む

授業開始前に,講義資料を読み,内容の理解に務めること. わからない箇所があれば,どの部分がわからないのかをメモしておくこと. 授業中に質問することで,個別に詳細を説明する.

例題に取り組む

講義資料には,例題が含まれていることもある. その例題に取り組み,理解を深めること. その周辺の資料を読めば回答できるように書いている. この場合も,わからない箇所はメモしておくこと. 授業中に質問することで,個別に詳細を説明する.

練習問題に取り組む

解けるところまで練習問題に取り組むこと. わからない箇所はどのようにわからないのかをメモしておくこと. 授業中に質問することで,個別にヒントや解き方を説明する.

わからなかったところをまとめる

各自練習問題に取り組む中で,わからなかった箇所が出てくると思います. そのわからなかった箇所はしっかりとメモしておきましょう. メモの書き方として,次のようなことを記しておくと良いでしょう.

  • わからなかった箇所.
  • 理想的な動作はどのようなものか.
  • 理想とは違って,どのような動作になったのか.
  • 考えられる原因.

授業の流れ

授業の流れ

本講義では,毎回開始直後に小テストが与えられます. 各自,与えられた小テストに取り組み,Moodleの所定の場所に提出してください. 続いて,小テストの簡単な解説のあと,練習問題に取り組む時間とします. 練習問題の受付時間は,次回講義の1時限目までです.

全体的な授業の時間割り当ては次の通りです.

授業の時間割当

続いて,それぞれの詳しい内容を記します.

小テスト

授業開始時に与えられる小課題のことを指します. 小テストは前回学習した内容です(その日の1時限目までに提出されるべき課題のもの).

例えば,第0回目として,Java言語の基礎として,コンパイル方法や基礎文法について学習していただきます. そのため,第1回目の小テストは,Java言語の繰り返し,条件分岐を含んだ問題となります. そして,第1回目の内容は,クラスの定義,配列についてです. この内容は第2回目の講義の小テストとして出題されます.

毎回,2種類の小テストが課されます. プログラムの読解とプログラムの作成です. 2種類の小テストの両方を講義時間内にMoodleの所定の場所に提出してください.

なお,小テストという名前ではありますが,教員,TAへの質問や友人同士の相談を行なっても構いません. ただし,友人のプログラムをそのままコピーは不可です. あくまでも作成方針について相談し,作成方法がわからないからといって,他人のソースファイルをそのまま書き写さないようにしてください. なお,コピーチェックを行う場合もあります.このチェックは予告なしで行い,コピーが見つかれば, コピーした方,された方に関係なく両者を0点とします.

完成しなくても,提出すると完成度に応じて部分点を与えます. 提出しなければ,未提出となり,0点より悪い評価になりますので,必ず提出するようにしてください.

なお,小テストを時間内に終えられたら,適宜練習問題に取り組んでください.

小テストの時間配分について

小テストはプログラム作成と,プログラム読解の2種類で構成されます. プログラム作成,プログラム読解の2つを授業時間内に取り組んでください. 解けない場合は,10:00に,教員よりプログラム作成の解説が行われます.それを聞いて挑戦してください. そして,続く練習問題に取り組むことにより,理解を深めるようにしてください.

小テスト解説

11:00 から プログラム作成の小テストの解説が教員により行われます. 基本的に,プログラム作成の問題に対して,考えるべき方針と,講義資料のどの部分を読めば良いかが説明されます.

小テストの解説が終われば,続けて前回の課題の説明が行われます. その解説を聞き,練習問題が提出できるようにしてください.

練習問題

小テストを提出し終えたら,練習問題に取り組んでください. 各単元には,3〜8題の練習問題が出されています. 各自,提出期限までに練習問題に取り組み,Moodle の所定の場所へ提出してください. 提出期限は次の回の授業終了までです. どのように解けば良いかがわからない場合は,教員,TAを呼び,アドバイスを受けることも可能です.

その回の小テストが解けないことによるペナルティは特にありません. ただし,最終課題に向けて,自分の力で解けるようになっておかなければ,最終課題が解けるようになりません. そのため,期限が過ぎたとしても,自力で解けるようになるよう,練習問題に取り組むことをお勧めします. わからない場合は,教員,TA,補助員,ChatGPTなどを活用してください.

授業終了後に行うこと

内容を振り返り,学習したポイントがどこかを自分なりにまとめてください. それにより,その単元で学習した内容が自身に定着することになります.

また,課題に取り組み,完成できれば提出してください. 小テストが解けなかった場合,その振り返りを行い,自身の力で解けるようになっておくことが望まれます. わからない箇所はMicrosoft Teamsや,次回授業時,オフィスアワー,寺子屋などで質問するようにしてください.

成績評価

成績評価

本講義は,講義期間の最後の方に行われる最終課題と試験により評価されます. また,次のものは成績に加算される可能性があります.

  • 小テスト
    • 提出状況.
    • 完成度.
  • 各練習問題の提出状況.

小テストを1回提出しないことで,即,単位取得が危ぶまれることはないものの,毎回提出することをお勧めします. なお,毎回提出したとしても,内容が伴っていなければ当然低い点数となります.

例えば,コンパイルできないソースファイルが提出された場合, 1,2講目は点数がある程度与えられるものの,3回目以降は0点もしくはそれに近い点数となります. 当然回が進むにつれ,評価のポイントも変わってくるためです.

一方で,提出点を稼ぐために,他人のプログラムをコピーするのはお勧めしません. 全ての小テスト,練習課題を提出したとしても,最終成績に影響するのは,多くて10点程度です(シラバスの評価方法を参照のこと). 場合によっては全く影響しない場合もあります. そのため,自身の理解のための行動を心がけるようにしてください.

まとめ

まとめ

第0講 Java言語の基礎1

この講で利用するプログラム

第0講 Java言語の基礎1のサブセクション

基礎の基礎

Hello World

Java言語で Hello World を書いてみましょう. 以下のプログラムを書き,HelloWorld.java に保存してください(//の後ろは書かなくても構いません). ファイル名とclassの後ろにある名前(クラス名)は必ず一致させていなければいけません. 一致していない場合は,コンパイルエラーが発生します.

public class HelloWorld { // <= クラス宣言の開始
    public static void main(String[] args){ // <= mainメソッドの開始
        System.out.println("Hello World");
    } // <= mainメソッドの終了
} // <= クラス宣言の終了

コンパイル方法

コンパイルには,javac コマンドを利用します.ターミナルで以下のコマンドを入力してください($はプロンプトを表していますので,入力不要です).

$ javac HelloWorld.java # Javaプログラムをコンパイルする

上記のように javacHelloWorld.java を渡して実行してください. コンパイルが行われます.コンパイルに成功すると,HelloWorld.class というクラスファイルが出力されます. a.out のようなファイルは出力されないことに注意してください.

コンパイルのイメージ コンパイルのイメージ

コンパイルに失敗すると,コンパイルエラーがコンパイラにより示されます. 代表的なコンパイルエラーは次の通りです.

  • シンボルが見つかりません.
    • 宣言されていない変数を使おうとしたときに指摘されます.
      • 変数は参照できる場所に宣言されていますか.
      • 綴りは間違っていないか確認しましょう.
  • クラスXxxxはpublicであり,ファイルXxxx.javaで宣言しなければなりません
    • Javaではクラス名とファイル名は必ず一致させておかなければいけません.
      • クラス名とファイル名が一致しているか確認しましょう.
  • <identifier>がありません.
    • publicstaticclassなどの綴り間違いが疑われます.
      • public static void main(String[] args)は間違いなく書かれていますか.
      • 一番外側に,public class Xxxxのように囲んでいますか.
      • カッコの対応は合っていますか.
  • \12288は不正な文字です.
    • 全角スペースが入っていないか確認してください.全角スペースはC言語と同じく文字列以外での利用が許されていません.

実行方法

実行するには,java コマンドを利用します. 先ほどコンパイルして得られた HelloWorld.class を実行するには,以下のようなコマンドを入力しましょう.

$ java HelloWorld # Javaプログラムを実行する
Hello World

実行するときには,拡張子(.class)はつけないことに注意してください.

プログラムの解説

一番外側

Javaプログラムは必ず一番外側が public class Xxxx で囲まれている必要があります.この一番外側の囲みをクラス宣言と呼びます. 変数も関数も必ずこのクラス宣言に囲まれた部分でしか定義できません.

また,Xxxx の部分をクラス名と呼び,ファイル名とクラス名は一致させる必要があります. 上の例の場合では,public class HelloWorld{ ... } ですから,HelloWorld.java に保存しなければいけません. それ以外のファイル名に保存すると,コンパイルできません.

mainメソッド

C言語における関数は,Javaでは,メソッド(method)と呼びます. main メソッドは,Javaプログラムを実行した時に一番最初に呼び出されるメソッドです.

main メソッドは,必ず public static void main(String[] args){ ... } であり, なんらかのクラス宣言に含まれています. 大文字小文字も区別されますので,一言一句間違えずに入力してください. なお,mainメソッドは必ずクラス宣言の括弧の内側に書かれる必要があります

public class ClassName {
    // mainメソッドはこの部分に書く.
    public static void main(String[] args) {
        // メソッドの中にメソッドは定義できない.コンパイルエラーが起こる.
        // public static void main(String[] args) {
        // }
    }
}
// ここにmainメソッドを書くとコンパイルエラーが起こる.
// public static void main(String[] args) {
// }

画面に出力する方法(printf)

Java言語で標準出力(ターミナルの実行画面)に文字を出力するには,System.out.printlnを利用します. 改行を伴って出力されます.改行したくない場合は,System.out.print を用いてください.

ただし,上記のメソッド(printlnもしくはprint)では,%dのような書式付きでは出力できません. C言語のように(printfのように)書式付きで出力したい場合,System.out.printf を利用してください. ただし,改行は \n ではなく,%n を利用してください. 改行はプラットフォーム依存であるためです. 詳しくは FAQ なぜ\nではなく,%nを利用するのでしょうかを参照してください.

なお,%dなどのフォーマット記述子には対応する型が存在します.対応しない型を指定すると実行時エラーが発生します. 対応については,FAQ フォーマット記述子とは何ですかを参照してください.

文字列

C言語と同じく,文字列は,"mojiretu"のように,""(ダブルクォート)で囲んで表します. Hello Worldのサンプルプログラムで言えば,3行目の"Hello World"部分が文字列です. ダブルクォートの内側は自由に変更しても,コンパイルエラーにはならず,実行結果が変わります.

基礎文法

条件分岐

概要

条件分岐は,if もしくは switch を用います. 文法は,C言語と同じです.典型的な例を次に示します.

if(条件式1){
    // 条件式1が成り立ったときに実行される文.
}
else if(条件式2){
    // 条件式2が成り立ったときに実行される文.
}
else{
    // 条件式1も条件式2も成り立たなかったときに実行される文.
}

C言語の場合,条件式は int 型で表現しましたが,Java言語の場合, Boolean 型となります. Boolean 型は,true もしくは false の2値で表される型です. C言語のようにプログラムを書くと,Boolean型になりますので,あまり気をつけなくとも良いようになっています.

Boolean型による条件分岐

if(value = 10){
    // この場合,if文の条件が Integer 型になるため,コンパイルエラーになる.
}
if(value == 10){
    // こう書くと,value == 10 の結果は Boolean になるため,Javaのif文として成り立つ.
    // 厳密に書けば,(value == 10) == true という条件であるが,
    // 冗長であるため,value == 10 と書くほうが望ましい.
}

例題

整数型である Integer 型の値が正負のどちらかであるかを if 文を使って判定しましょう. 以下のプログラムの.....部分を埋めて完成させてください.

public class PositiveChecker{
    public static void main(String[] args){
        Integer value = 5;
        if(.....){
            System.out.printf("与えられた数値は正の値です: %d%n", value);
        }
        else if(.....){
            System.out.printf("与えられた数値は0です: %d%n", value);
        }
        else{
            System.out.printf("与えられた数値は負の値です: %d%n", value);
        }
    }
}

完成すれば,value の値をいろいろと変更して,実行してみましょう. その際,全ての分岐が網羅するには,valueをどのような値にして,何度変更すれば良いかを考えてみましょう.

出力例

$ java PositiveChecker
与えられた数値は正の値です: 5
$ java PositiveChecker
与えられた数値は負の値です: -9
$ java PositiveChecker
与えられた数値は0です: 0

3回変更すれば,条件分岐を網羅できます.

  1. valueを0にする.
  2. valueを正の値(例:1, 2, 3, …, 2,147,483,647 (Integerの最大値))のいずれかの値にする.
  3. valueを負の値(例:-1, -2, -3, …, -2,147,483,648 (Integerの最小値))のいずれかの値にする.
        ....
        if(value > 0){
            System.out.printf("与えられた数値は正の値です: %d%n", value);
        }
        else if(value == 0){
            System.out.printf("与えられた数値は0です: %d%n", value);
        }
        ....

繰り返し

概要

繰り返しは,for もしくは while を利用します.文法はC言語と同じです. 典型的な例を次に示します.

// 初期化式,継続条件式,反復式をセミコロン(;)で区切る.
for(Integer i = 0; i < 10; i++){
    // iが0から9までこの文を繰り返す.
}
// for(初期化式; 継続条件式; 反復式)

Integer loop = 0;
// 継続条件式を while に指定する.
while(loop < 10){
    // loopが0から9までこの文を繰り返す.
    loop++;
}
// while(継続条件式)

ループ制御には,Integer型を利用することが多いです.if 文と同じく,繰り返し文の継続条件はBoolean型で表現されます.

また,for文の初期化式で変数宣言が行えるようになっています. ループを途中で抜けるには,break,そして, ループを途中で中断し,次の繰り返しに移行するには continueを使うのも C言語と同じです.

例題

for 文を使って,1以上,100未満の奇数の値を出力しましょう.

public class OddPrinter {
    public static void main(String[] args) {
        // for文を使って繰り返し.
    }
}

出力例

$ java OddPrinter
1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51 53 55 57 59 61 63 65 67 69 71 73 75 77 79 81 83 85 87 89 91 93 95 97 99

完成すれば,1以上,100未満の偶数値を出力する EvenPrinter を作成してください.

public class OddPrinter{
    public static void main(String[] args){
        // 正攻法
        for(Integer i = 1; i < 100; i++){
            // iを割った余りが1であれば,奇数.
            if(i % 2 == 1){ // i % 2 != 0 の条件でも可.
                System.out.print(i);
                System.out.print(" ");
            }
        }
        System.out.println();

        // 解2: 2づつ増加させる方法.
        for(Integer i = 1; i < 100; i += 2){
            System.out.print(i);
            System.out.print(" ");
        }
        System.out.println();
    }
}

例題 改1

OddPrinterEvenPrinter それぞれで,20個の数値を出力するごとに改行してください.

出力例

$ java EvenPrinter
2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
42 44 46 48 50 52 54 56 58 60 62 64 66 68 70 72 74 76 78 80
82 84 86 88 90 92 94 96 98
public class EvenPrinter {
    public static void main(String[] args) {
        // 正攻法:出力した回数を数え,20個出力したら改行する.
        Integer printedCount = 0;
        for(Integer i = 1; i < 100; i++){
            // iを割った余りが1であれば,偶数.
            if(i % 2 == 0){
                System.out.printf("%2d ", i);
                printedCount++;
                if(printedCount == 20){
                    System.out.println();
                    printedCount = 0;
                }
            }
        }
        System.out.println();

        // 解2: 改行する位置は決まっているので,3つのループで実現する.
        for(Integer i = 2; i <= 40; i += 2){
            System.out.printf("%2d ", i);
        }
        System.out.println();
        for(Integer i = 42; i <= 80; i += 2){
            System.out.printf("%2d ", i);
        }
        System.out.println();
        for(Integer i = 82; i < 100; i += 2){
            System.out.printf("%2d ", i);
        }
        System.out.println();
    }
}

Java言語は,C言語に比べて,非常に多彩な型が存在します(標準で約4,000の型が用意されている). 型がわからなければ,プログラムが読めなくなりますので,C言語のとき以上に型に気をつけてください.

この講義では,とりあえずは,次の型さえ区別できていれば良いです. 講義が進むと,さらに扱う型が増え,最終的に標準で用意されている型では,20 程度の型を使うことになります.

変数の宣言方法

C言語と同じく,型名の後ろに変数名を書いて変数を宣言します. 先にも述べたように,C言語と比べて非常に多彩な型が出てきます. 変数がどんな型であるのかを把握しなければプログラムがわからなくなりますので, 変数の型に気をつけてプログラムを読むようにしましょう.

Integer intValue; // Integer型の変数 intValue を宣言した.
intValue = 10;    // intValue に10を代入した.

// Double型の変数 pi を宣言し,初期値として3.14 を代入した.
Double pi = 3.14;

// String型の変数 name を宣言し,初期値として"Tamada"を代入した.
String name = "Tamada";

// Boolean型の変数 flag を宣言し,初期値として,true を代入した.
Boolean flag = true;

// Character型の変数 character を宣言し,
// 'a' という値を代入した.文字は''で囲む.
Character character = 'a';

代表的な型

  • 整数型
    • Integer 型,(Long型,Short 型,Byte型)
    • ほとんどの場合,Integer型を利用する.
  • 浮動小数点型
    • Double 型,(Float 型)
    • ほとんどの場合,Double型を利用する.
  • 真偽値型
    • Boolean 型(true もしくは false
  • 文字型
    • Character 型.
  • 文字列型
    • String 型.

各型の取りうる値

  • Boolean
    • trueもしくは false のどちらか.
  • Character
    • 16ビット Unicode 文字(\u0000\uffff
  • Byte
    • 8ビット整数(-128127$-2^7$〜$2^7-1$))
  • Short
    • 16ビット整数(-32,76832,767$-2^{15}$〜$2^{15}-1$))
  • Integer
    • 32ビット整数(-2,147,483,6482,147,483,647$-2^{31}$〜$2^{31}-1$))
    • 最大値は,Integer.MAX_VALUE
  • Long
    • 64ビット整数(-9,223,372,036,854,775,8089,223,372,036,854,775,807$-2^{63}$〜$2^{63}-1$))
  • Float
    • 32ビット単精度浮動小数点数
  • Double
    • 64ビット倍精度浮動小数点数

各型の相互変換

String型への変換

次の2通りの方法があります.

  • 実体に対して,toStringメソッドを呼び出す方法.
  • 他のString 型と連結する方法.

処理速度は,toStringメソッドを呼び出す方が早いです. 以下の例も参考にしてください.

Integer intValue = 10;
Double doubleValue = 3.141592;
String string1 = intValue.toString(); // もちろん,"" + intValue でも可.
String string2 = "" + doubleValue;    // doubleValue.toString() でも可.

// 次のような代入方法では,コンパイルエラーが起こる.
String string3 = intValue;  // 型が違う.
String string4 = (String) doubleValue; // 暗黙的な型変換が行えない

String型から各種数値型への変換

必要になった時に改めて説明しますが,次の原則を覚えておくと役に立つでしょう. 数値型(Integer型やDouble型など)にvalueOfというメソッドが定義されています.

練習問題

本日の残りの時間で,次に挙げる練習問題を順に作成してください.

1. Big & Small

0から1までの Double 型の乱数を発生させ,その値が0.5より小さければ Small,0.5以上であれば Big と出力するプログラムを作成してください. 0以上1未満の Double 型の乱数は,次のコードで得られます.

public class BigAndSmall{
    public static void main(String[] args){
        Double value = Math.random();
        // valueには0から1の乱数が代入されている.
        System.out.printf("value: %f: ", value);

        // ここに判定のプログラムを書いていく.
    }
}

実行すると,そのときの value の値と Big もしくは Small の文字列が出力されます.

乱数発生方法

0以上1未満の Double 型の乱数は,次の1行で得られます.

Double value = Math.random();

出力例

以下の出力例と同じ結果は出ませんが,条件に示したように,value の値が0.5より小さいか,以上かで判定できているかを確認してください.

$ java BigAndSmall
value: 0.327039: Small
$ java BigAndSmall
value: 0.582704: Big
$ java BigAndSmall
value: 0.239037: Small
$ java BigAndSmall
value: 0.304460: Small

2. うるう年の判定

与えられた年がうるう年であるか否かを判定しましょう. うるう年は,次のように判定します.

  • 西暦で表される年が 4 で割り切れたらうるう年である.
  • ただし,そのうち,100で割り切れる年はうるう年ではない.
  • ただし,さらに,400で割り切れる年はうるう年である.

次のようなフローチャートになります.

graph LR
    id1{4で割り切れる}
    id2{100で割り切れる}
    id3{400で割り切れる}
    leap(うるう年)
    ordinal(平年)
    id1 -->|true|id2
    id1 -->|false|ordinal
    id2 -->|true|id3
    id3 -->|false|ordinal
    id3 -->|true|leap
    id2 -->|false|leap
public class LeapYear{
    public static void main(String[] args){
        Integer year = 2016;
        Boolean leapYear = false;
        // ここに判定処理を書いていく.
 
        if(leapYear){ // leapYearがtrueの場合.
            System.out.printf("%d年はうるう年です.%n", year);
        }
        else{
            System.out.printf("%d年はうるう年ではありません.%n", year);
        }
    }
}

完成すれば,year の値を変更して実行結果を確認してください. なお,2000, 2004, 2016年はうるう年,2100,2015, 1900年はうるう年ではありません.

条件文の AND, OR, NOT

  • 2つの条件の両方を満たしたときに処理を行いたい場合は,次のように,AND(&&)で2つの条件式を結ぶ.
if(条件式1 && 条件式2){
    // 条件式1, 条件式2の両方を満たした時に実行される.
}
  • 2つの条件のどちらか一方を満たしたときに処理を行いたい場合は,次のように,OR(||)で2つの条件式を結ぶ.
if(条件式1 || 条件式2){
    // 条件式1,条件式2のどちらかを満たすときに実行される.
}
  • ある条件を満たさないときに処理を行いたい場合は,次のように,条件式の前に NOT(!)をつける.
if(!条件式1){
    // 条件式1を満たさないとき(falseの場合)に実行される.
}

出力例

以下のように,判定する年を変更して実行結果を確認してみましょう. 以下の例では省略していますが, year に代入する値を変更するたびにコンパイルしてください.

$ java LeapYear
2016年はうるう年です.
$ java LeapYear
2000年はうるう年です.
$ java LeapYear
2004年はうるう年です.
$ java LeapYear
2100年はうるう年ではありません.
$ java LeapYear
2015年はうるう年ではありません.
$ java LeapYear
1900年はうるう年ではありません.

3. 総和を求める.

ループを用いて,1から10までの総和(10を含む)を求めましょう.次の手順で作成していきましょう.

  1. クラス名をGrandTotal とする.
  2. main メソッドを用意する.
  3. main メソッド内で Integer 型の変数 result を宣言する.
    • result は初期値として0を代入しておく.
  4. main メソッド内で Integer 型のループ制御変数 i を宣言する.
  5. ループ制御変数 i を用いて1から10までループを作成する.
  6. 繰り返しごとに,resulti の値を加算していく.
  7. ループ終了後,result の値を出力する.

ループはC言語と同じように書けます. forwhile のどちらも利用できますので,試してみましょう.

完成すれば,範囲を変更して,総和を求めましょう.

出力例

$ java GrandTotal
1から10までの総和は55です.
$ java GrandTotal
1から100までの総和は5050です.
$ java GrandTotal
1から1000までの総和は500500です.

4. 九九を表示する.

出力例のように,九九を表示するプログラムを作成しましょう. クラス名を Multiplication とします.

出力するとき,System.out.printf のフォーマット記述子に%d ではなく,%2dとすると,1桁の数字でも2桁として出力してくれます. なお,%02dとすると,足りない桁は0で埋めてくれます.

出力例

$ java Multiplication
   1  2  3  4  5  6  7  8  9
1  1  2  3  4  5  6  7  8  9 
2  2  4  6  8 10 12 14 16 18 
3  3  6  9 12 15 18 21 24 27 
4  4  8 12 16 20 24 28 32 36 
5  5 10 15 20 25 30 35 40 45 
6  6 12 18 24 30 36 42 48 54 
7  7 14 21 28 35 42 49 56 63 
8  8 16 24 32 40 48 56 64 72 
9  9 18 27 36 45 54 63 72 81 

5. Xの描画

繰り返しを利用し,出力例の模様を出力してください. クラス名は XPrinter としてください.

出力例

$ java XPrinter
X........X
.X......X.
..X....X..
...X..X...
....XX....
....XX....
...X..X...
..X....X..
.X......X.
X........X

まとめ

まとめ

if(条件式1 && 条件式2){
    // 条件式1,条件式2の両方を満たすときに実行される.
}
if(条件式1 || 条件式2){
    // 条件式1,条件式2のどちらかを満たすときに実行される.
}
if(!条件式1){
    // 条件式1を満たさないときに実行される.
}

第1講 Java言語の基礎2

この講で利用するプログラム

第1講 Java言語の基礎2のサブセクション

コメント

Javaのコメントは3種類の書き方ができます.

一行コメント

// 以降改行までがコメントとして扱われます.

Integer answer = 1 + 1; // この部分がコメント.
// 改行すると別のコメントとして扱われるため再度コメント記号が必要

通常コメント

/**/ で囲まれた部分がコメントとして扱われます. C言語と同じスタイルです.

このコメントを入れ子にすることはできません.

/* この部分がコメントです. */
/* この部分がコメントです.
   複数行に分かれていてもOK! */
Integer answer = 1 + 1;
/* この部分がコメントです.
   Integer answer = 1 + 1; // 途中に1行コメントが含まれていてもOK
*/

Javadoc コメント

/***/ で囲まれた部分がコメントとして扱われます. 通常コメントと同じような書き方ですが,このコメントが書ける場所は次の3つに限られています. 次の3つ以外の場所にJavadocコメントが書かれると通常コメントとして扱われます.

  • クラス宣言の前,
  • メソッド宣言の前.
  • フィールド宣言の前.
    • フィールド宣言とは,クラス宣言の内側,かつ,メソッド宣言の外側で定義された変数を指します.

javadoc コマンドにより,ソースファイルからドキュメントを生成できますが,その時にドキュメントに掲載する文章をこのコメントで記します. この授業では扱いません.

/**
 * クラスに対するJavadocコメント.
 * このクラスの役割などを書く.
 */
public class SampleClass{
    /**
     * フィールドに対するJavadocコメント.
     * このフィールドの役割などを書く.
     */
    String stringField = "string"; // フィールドの宣言

    /**
     * メソッドに対するJavadocコメント.
     * このメソッドの役割などを書く.
     */
    public static void main(String[] args){
        // 何らかの処理.
    }
}

クラス定義

クラス定義の基本形

まずは,以下のクラスファイルの基本形を覚えておきましょう. GivenClassName は与えられたクラス名に置き換えてください. run メソッド内に指定されたプログラムを書いてください. main メソッドは返り値の型の前にpublic staticというキーワードがついていますが,これらのキーワードは今のところは mainのみにつけるものと思ってください.

public class GivenClassName{
  void run(){
    // ここに課題内容のプログラムを書く.
  }
  public static void main(String[] args){
    GivenClassName application = new GivenClassName();
    application.run();
  }
}

ここで注目してもらいたいのは,6行目,GivenClassName application = new GivenClassName();と, 7行目のapplication.run(); です.

6行目のnew GivenClassName()GivenClassName という型の実体を作成し, GivenClassName 型の変数application に代入しています. そして,7行目のapplication.run() は,変数application に対して, run メソッドを呼び出しています.

C言語とは異なり,Java言語では,メソッドはどこかの型の実体に必ず所属しています. そのため,メソッドを呼び出すときは,どの実体に対して呼び出すのかを明示しなくてはいけません.

なお,publicについては,FAQ可視性とは何ですかを参照してください.

new演算子

先ほど,new で実体を作成したと言いました. 次のプログラムは今まで書いてきたプログラムの例です.

Integer value1 = 3;
Integer value2 = 5;
Integer result = value1 + value2;
System.out.println(result);

実は,このプログラムは,様々なものが省略されています.省略せずに書くと,次のようなプログラムになります.

Integer value1 = Integer.valueOf(3);
Integer value2 = Integer.valueOf(5);
Integer result = Integer.valueOf(value1 + value2);
System.out.println(result);

Integer.valueOfnew Integerと同じような処理と思ってください. このように,実は,実体を作成して変数に代入しています. Java言語では,ほぼ全てこのように実体を作成しなければ変数に代入できないことを覚えておいてください.

ヒント

実体を作成するには,基本的には new演算子を利用し,new 型名とします. しかし,Integer型は,Java 9 からはnew演算子での実体の作成が非推奨となりました(コンパイル時に警告が出るようになります). 代わりに Integer.valueOfメソッドを利用します. 上記のプログラムは,以下のように書き換えることで,将来的にも利用できるプログラムとなります.

Integer value1 = Integer.valueOf(3);
Integer value2 = Integer.valueOf(5);
Integer result = Integer.valueOf(value1 + value2);
System.out.println(result);

変数

宣言場所

変数は宣言する場所によって2種類の呼び方があり,特徴も少し異なります. 一つは,メソッド内で宣言する変数,ローカル変数(local variable), もう一つは,メソッドの外で宣言する変数,フィールド(field)です. メソッドもフィールドもどちらも,クラス宣言の直下になければいけません(クラス宣言の括弧({ })のすぐ内側).

  • フィールド(field)
    • 初期値は null(何も参照していないことを表す予約語).
    • 変数名.フィールド名でアクセスできる.
    • 有効範囲は,そのフィールドが宣言されているクラスの実体が有効な範囲.
      • クラスの実体の有効範囲と同じ.
      • クラスの実体の有効範囲は,そのクラスがフィールド,ローカル変数のどちらで宣言されたかによって異なる.
  • ローカル変数(local variable)
    • 初期化されなければコンパイルエラーとなる.
    • 有効な時間は,そのメソッド内のスコープ内のみ.
      • その変数が宣言された箇所以降かつ,宣言された場所の一番内側の括弧内({ })のみ.

プログラムを書くとき,変数の有効範囲はできるだけ狭く,というのが定石です. そのため,基本的には,ローカル変数を使い,ローカル変数で対応できない場合に,フィールドを利用しましょう.

実体

実体を確認するために,次のプログラムを考えてみましょう.

Date は日付を表す型です.new Date(), もしくは,new Date(year, month, day, hour, minute) という命令で実体を作成します.与える引数の型は全て Integer です. ただし,年月日,時間を指定する場合,年は1900年からの経過年数, 月は0から始まる(0が1月を表す)点に注意してください. 古い時代のもので,互換性のために残されているもので,変な仕様ではありますが,これで確認してみましょう.

また,コンパイル時に「非推奨のAPIを使用または...」という注意が出ますが,無視してください. これはコンパイルエラーではなく,注意(Warning)ですので,コンパイルはできています. お勧めできない古い時代のライブラリを使っている時に出ます.基本的には使わない方が良いのですが, 今回は説明のために用います.

なお,Date型を利用するときは,import java.util.Dateクラス宣言の前に必要になります.import文が必要な理由は, FAQ import文とは何かを参照してください.

実際に,入力して,どのような実行結果になるかを考えてから,動かしてみましょう. そして,予想との違いを考えてください.

import java.util.Date;

public class DateExample{
  void run(){
    // year は 1900年からの経過年数.
    // month は 0 から始まる.
    Date date1 = new Date(115, 8, 29, 9, 0);
    Date date2 = date1;
    System.out.printf("date1: %s%n", date1);
    System.out.printf("date2: %s%n", date2);

    // Year を +1 している.
    date1.setYear(date1.getYear() + 1);
    System.out.printf("date1: %s%n", date1);
    System.out.printf("date2: %s%n", date2);
  }
  public static void main(String[] args){
    DateExample example = new DateExample();
    example.run();
  }
}

Javaの変数はほぼ全てがポインタとなっています. ポインタとは,メモリ領域の特定の場所を指し示すだけの変数です. 上のプログラムの date2 = date1 という命令が実行されると, date2date1 の参照先が代入されたことを表します. その結果,date1date2 は同じ参照先の実体を指し示すようになり, 一方を変更すると(date1.setYear(...);), もう一方も変更されることになります. 下の図をクリックすると図が更新されます.何度かクリックして見て動作を確認しましょう.

値の一致性

基礎プログラミング演習で学習したものとは変数の扱いが異なる場合がありますので,注意してください. Java言語では,全ての変数は,メモリ上のどこかにある実体を参照しているものと思ってください.

参照の一致性

先のプログラムrun メソッドに次のコードを追加して, 実行結果を確認してみましょう.

void run(){
    Date date1 = new Date(115, 8, 29, 9, 0);
    Date date2 = date1;
    date1.setYear(date1.getYear() + 1);
    // 以下のプログラムを追加する.
    Date date3 = new Date(116, 8, 29, 9, 0);

    System.out.println(date1 == date2);
    System.out.println(date1 == date3);
    System.out.println(date2 == date3);
}

各変数が指し示す Date 型変数の値は全て,2016-09-29 9:00 になっているはずですが, なぜ truefalse という違いが現れるのでしょうか. 実は,== は参照先が同じであるかを判定する演算子であり,値が一致するか否かは判定されません. 以下の画像をクリックし,動作を確認してみましょう.

参照の一致性

値の一致性

では,2つの変数の値が一致するかどうかを判定するにはどうすれば良いのでしょうか. それには,Objects型のequalsメソッドを利用します. 先ほどと同じく,runメソッドに次の処理を追加し,実行結果を確認しましょう.

import java.util.Date;
import java.util.Objects;

public class DateExample{
    void run(){
        // ...
        // 次の処理を追加する.
        System.out.println(Objects.equals(date1, date2));
        System.out.println(Objects.equals(date1, date3));
        System.out.println(Objects.equals(date2, date3));
    }
    // ...
}

このようにすることで,値の一致性を確認できます. Javaで比較する時は,参照の一致か,値の一致か,どちらを判定したいのかを区別して比較しましょう.

なお,Objects型を利用するときは,クラス宣言の前に import 文が必要です. import java.util.Objects と書いてください.

import java.util.Date;
import java.util.Objects;

public class DateExample{
    void run(){
        Date date1 = new Date(115, 8, 29, 9, 0);
        Date date2 = date1;
        date1.setYear(date1.getYear() + 1);
        System.out.printf("date1: %s%n", date1);
        System.out.printf("date2: %s%n", date2);

        Date date3 = new Date(116, 8, 29, 9, 0);
        System.out.printf("date3: %s%n", date3);

        System.out.println("参照の一致性");
        System.out.println(date1 == date2);
        System.out.println(date1 == date3);
        System.out.println(date2 == date3);

        System.out.println("値の一致性");
        System.out.println(Objects.equals(date1, date2));
        System.out.println(Objects.equals(date1, date3));
        System.out.println(Objects.equals(date2, date3));
    }

    public static void main(String[] args){
        DateExample app = new DateExample();
        app.run();
    }
}

何も参照していない値null

Java言語における変数は基本的に全て参照であり,メモリ上のどこを指し示しているかを表しています. では,何も指し示していない場合,すなわち,何も代入されていない場合,どのように扱われるのでしょうか. この場合,nullという値が代入されていることになります.

nullは何も参照していない値を表しています. そして,nullが代入されている変数に対して,メソッド呼び出しやフィールドを参照しようとすると NullPointerExceptionという実行時エラーが発生します.

例題

例えば,次のNullPointerExceptionDemoで確認してみましょう.

public class NullPointerExceptionDemo {
    void run() {
        String emptyString  = "";
        String stringString = "string";
        String nullString   = null;

        System.out.println(emptyString.toString());  // => 空文字が出力される.
        System.out.println(stringString.toString()); // => stringという文字列が出力される.
        System.out.println(nullString.toString());   // => NullPointerException
    }

    public static void main(String[] args) {
        NullPointerExceptionDemo demo = new NullPointerExceptionDemo();
        demo.run();
    }
}

実行結果(実行時のエラーメッセージの読み方)

このプログラムを実行すると,次のような出力が得られます(行頭の番号は説明のために追加したものであり,出力されません).

  1: 
  2: string
  3: Exception in thread "main" java.lang.NullPointerException
  4: 	at NullPointerExceptionDemo.run(NullPointerExceptionDemo.java:9)
  5: 	at NullPointerExceptionDemo.main(NullPointerExceptionDemo.java:14)

出力結果の1行目がプログラムの7行目により出力された空文字列(長さ0の文字列)です. そして,出力結果の2行目が,"string"という文字列が出力されています. 出力結果の3行目以降が NullPointerException という実行時エラーの情報です. 3行目が,どのようなエラーが発生したかを表しています(エラーの種類). 続く4行目以降で,どのメソッドを実行している時に,そのエラーが起きたのかを示しています(エラーの発生場所). ここでは,NullPointerExceptionDemo.javaの9行目(runメソッド内)で発生しました(実行結果の4行目). そして,そのrunメソッドはNullPointerExceptionDemo.javaの14行目(mainメソッド内)で呼び出されました,のように読みます.

なお,Java でクラス名やメソッド名などはキャメルケースで書かれます. つまりNullPointerExceptionは null pointer exception(ヌル・ポインタ・エクスセプション)と読み,null参照の例外という意味です.

配列

宣言方法

Java言語にもC言語と同じように,配列が存在します. ただし,宣言の方法はC言語とは異なります.

// C言語は以下のように,変数名に配列を表す記号をつけていた.
int array[];
// Java言語では,型名の方に配列を表す記号をつける.
// Integerの配列型である array を宣言する,と考えてください.
Integer[] array;

配列の長さ

Java言語で配列の長さを取得するには,配列型の変数に .length をつけることで取得できます.

Integer[] array = // 配列の初期化は省略
System.out.println(array.length); // => arrayの長さが出力される

配列の各要素

配列の各要素にアクセスするには,C言語と同じく,array[0]のように書きます. この array[0] の 0 をインデックスと呼びます. 配列の要素は,0から始まり,array.length - 1 のインデックス(配列の長さ - 1)までが配列の有効な範囲です.

配列の範囲外の要素にアクセスしようとすると,ArrayIndexOutOfBoundsException というエラーが発生します.

配列の例

では,次のように,String型の配列であるarray の各要素から図のように文字列が参照されている例を考えましょう.

配列の例

この時,以下の処理を行うと,どのような結果になるか考えましょう.

  1. System.out.println(array.length);
  2. System.out.println(array[1]);
  3. System.out.println(array[5]);
  4. System.out.println(array[6]);

配列の作成方法

この授業では扱いません.基本的に自分で配列を作成することはなく, ライブラリの返り値など,与えられたものの利用のみとします. 配列が必要な場合は,4回目で扱う Listを利用するようにしましょう.

なぜなら,配列は近年のプログラミングからすると機能が乏しく,使いにくいためです. それよりは,配列と同じように扱えながらも,より高機能な機能が提供されている List の使い方を学ぶ方が優意義であるためです.

コマンドライン引数

今まで,main メソッドの引数に String型の配列がついていました. この変数を使うと,コマンドライン引数で受け取った値をプログラムから利用できます.

つまり,ターミナル上でプログラムを実行するとき,後ろに指定した文字列をプログラムで受け取れます.

$ java HogeHoge command line arguments
    # => 上記の command, line, arguments がコマンドライン引数

例題1 ArgsPrinter

次のプログラムで,コマンドライン引数で与えられた値を全て出力してみましょう.

public class ArgsPrinter{
    void run(String[] args){
        // args のインデックスの0番目から最後までを繰り返して,
        // args の各要素を出力してみましょう.
        for(...) {
            // 何番目の要素として,何が配列の要素として格納されているかを確認しましょう.
            System.out.printf("%d: %s%n", ...);
        }
    }
    public static void main(String[] args){
        ArgsPrinter printer = new ArgsPrinter();
        printer.run(args);
    }
}

実行例

入力できれば,上記のプログラムを実行して実行結果を確認してください. 実行する時,コマンドライン引数をいくつか与えてみてください.

$ java ArgsPrinter
$ java ArgsPrinter abc def ghi
$ java ArgsPrinter abcdefg
$ java ArgsPrinter 0 1 2 3 4 5 6 7 8 9

このように,run メソッドが引数を受け取るようにすることもできます. 必要に応じてコマンドライン引数を run メソッドでアクセスできるようにしましょう.

public class ArgsPrinter{
    void run(String[] args){
        // args のインデックスの0番目から最後までを繰り返して,
        // args の各要素を出力してみましょう.
        for(Integer i = 0; i < args.length; i++){
            // 何番目の要素として,何が配列の要素として格納されているかを確認しましょう.
            System.out.printf("%d: %s%n", i, args[i]);
        }
    }
    public static void main(String[] args){
        ArgsPrinter printer = new ArgsPrinter();
        printer.run(args);
    }
}

例題2 コマンドライン引数とデフォルト値

コマンドライン引数に何か値が与えられた場合はその値を出力し,そうでない場合は"no arguments"を出力しましょう.

public class FindValueInArgs {
    void run(String[] args) {
        String value = // デフォルト値を代入しておく.
        // もしコマンドライン引数に値が存在すれば
        if(...) {
            // コマンドライン引数で与えられた値を value に代入する.
        }
        System.out.printf("args[0]: %s%n", value); 
    }

    public static void main(String[] args) {
        FindValueInArgs app = new FindValueInArgs();
        app.run(args);
    }
}

デフォルト値を args[0] = "no arguments" とできない点に注意してください. 配列は長さを変更できません. つまり,コマンドライン引数が与えられなかった場合,args の長さは 0 ですので,args[0] は範囲を超えたアクセスになります. そのため,別の変数(value)を用意し,そこにデフォルト値もしくは,指定された値を代入しましょう.

実行例

入力できれば,上記のプログラムを実行して実行結果を確認してください. 実行する時,コマンドライン引数をいくつか与えてみてください.

$ java FindValueInArgs
$ java FindValueInArgs hello
$ java FindValueInArgs world
$ java FindValueInArgs hello world

このようにすることで,コマンドライン引数で指定された場合はその値を利用し, そうでない場合はデフォルト値を利用することが可能になります.

public class FindValueInArgs {
    void run(String[] args) {
        String value = "no arguments"; // デフォルト値を代入しておく.
        // もしコマンドライン引数に値が存在すれば
        if(args.length > 0) {
            // コマンドライン引数で与えられた値を value に代入する.
            value = args[0];
        }
        System.out.printf("args[0]: %s%n", value);
    }

    public static void main(String[] args) {
        FindValueInArgs app = new FindValueInArgs();
        app.run(args);
    }
}

練習問題

1. HelloWorld 改

コマンドライン引数で与えられた人に,挨拶しましょう. もし,誰も指定されない場合は,"Hello, World"と出力してください. クラス名は,HelloWorld2 とし,クラス定義の基本形に従ってプログラムを書いてください. コマンドライン引数も参考になるでしょう.

出力例

$ java HelloWorld2 Tamada
Hello, Tamada
$ java HelloWorld2 Sagisaka
Hello, Sagisaka
$ java HelloWorld2
Hello, World

2. HelloWorld 改2

コマンドライン引数で与えられた人に,挨拶しましょう. 基本的には,HelloWorld 改と同じですが,もし, "World"が指定されたら,"Hi, World"と気さくに挨拶してください. クラス名は,HelloWorld3 とし,クラス定義の基本形 に従ってプログラムを書いてください. コマンドライン引数も参考になるでしょう.

出力例

$ java HelloWorld3 Miyamori
Hello, Miyamori
$ java HelloWorld3 Tamada
Hello, Tamada
$ java HelloWorld3 World
Hi, World
$ java HelloWorld3
Hello, World

3. 階乗

コマンドライン引数であたえられた数値の階乗を計算するプログラムを作成してください. $n$の階乗は,$n! = n \times (n - 1) \times (n - 2) … 2 \times 1$ で求められます.

public class Factorial{
    void run(String[] args){
        Integer number = Integer.valueOf(args[0]);
        Integer factorial;
        // number の階乗を計算する.
        System.out.printf("%d! = %d%n", number, factorial);
    }
    // mainメソッドは省略
}

String型からInteger型への変換

なお,String型の変数からInteger型に変換するには,以下のように行います. Factorial の3行目で同じことを行っています.

// Integer max = "15"; // コンパイルエラー.
    // "15"という文字列はそのまま Integer 型に代入できない.
String numberString = "15";
Integer max = Integer.valueOf(numberString);
    // => max には 15 という数値が代入される

従来,String型からInteger型に変換する方法は2つありました.

  • Integer.valueOfメソッドを使う方法.
  • new Integer で実体を作成する方法.

しかし,Java 9 から後者の方法(new Integerで実体を作成する方法)が非推奨のAPIとして指定されました. そのため,Integer.valueOfで行う方法に書き換えました(2018年4月18日).

出力例

$ java Factorial 3
3! = 6
$ java Factorial 4
4! = 24
$ java Factorial 5
5! = 120

4. FizzBuzz

FizzBuzz は1から特定の値までの数値を順に出力します. ただし,3の倍数の時は,数値の代わりに Fizz という文字列を, 5の倍数の時は,数値の代わりに Buzz という文字列を, 3と5の公倍数の時は,数値の代わりに FizzBuzz という文字列を出力します. クラス名は,FizzBuzzとしてください.

コマンドラインから受け取った値まで,上記のルールに従って値を出力してみましょう. もし,コマンドラインで値が指定されなかったときは,15が指定されたものとして処理を進めてください.

出力例

$ java FizzBuzz 3
1
2
Fizz
$ java FizzBuzz
1
2
Fizz
4
Buzz
... 途中省略
13
14
FizzBuzz

5. Fibonacci数列

Fibonacci数列とは,次の漸化式で表される数列です. このFibonacci数列を初項からコマンドライン引数で指定された項まで出力してみましょう. コマンドライン引数が指定されなかった場合は,20項目(20項も出力結果に含む)までを出力するようにしてください. クラス名は,Fibonacciとします.

$$ F_i = \begin{cases} 1 & (i = 1) \\\\\ 1 & (i = 2) \\\\\ F_{i-2}+F_{i-1} & (i \geq 3)\ \end{cases} $$

出力例

$ java Fibonacci 3
1 1 2
$ java Fibonacci 8
1 1 2 3 5 8 13 21
$ java Fibonacci 15
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610
$ java Fibonacci
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765

まとめ

まとめ

第2講 Java言語の基礎3

この講で利用するプログラム

第2講 Java言語の基礎3のサブセクション

メソッド(関数)

基本的な呼び出し方.

C言語で言う関数をJava言語ではメソッド(method)と呼びます. メソッドは必ず何らかの実体に含まれています. そのため,メソッドを呼び出すときは,どの実体に対して呼び出すのかを指定しなければいけません. 例えば,クラス定義の基本形 に示した次のコードを見てみます.

public static void main(String[] args){
  GivenClassName application = new GivenClassName();
  application.run();
}

このコードの3行目は,application.run() とメソッドを呼び出しています. これは,application という実体に対して,run というメソッドを呼び出していることになります. このように,必ず,何らかの実体を経由しなければメソッドは呼び出せません.

例題1 定義済みメソッドの呼び出し

コマンドライン引数で与えられた文字列を全て大文字に変換するプログラム,ToUpper を書いてみましょう. 以下のように出力されます.

$ java ToUpper tamada
TAMADA
$ java ToUpper tamada Java KSU_cse_AP
tamada -> TAMADA
Java -> JAVA
KSU_cse_AP -> KSU_CSE_AP

String 型の値を大文字に変更するには,toUpperCase メソッドを String 型変数に対して呼び出します. すると,toUpperCaseの返り値として,全てが大文字に変更された文字列が返ってきます. なお,String型の実体は不変(immutable)です.内容の変更はできませんので,元の変数の内容は変わりません.

参考コード

以下のコード片と ArgsPrinterが参考になるでしょう.

void run() {
    String original;
    original = "tamada";
    String upper;
    upper = original.toUpperCase();
    // => original に代入されている文字が全て大文字になって,upperに代入される.
}

このプログラムは,String型変数であるoriginal に対して,toUpper を呼び出し, その返り値をupper というString型変数に代入しています. Javaのあらゆるメソッドはこのように,何らかのものに対して呼び出します.

動作イメージを次の画像で確認してください.クリックするたびに画像が更新されます.

public class ToUpper{
    void run(String[] args){
        for(Integer i = 0; i < args.length; i++){
            String value = args[i].toUpperCase();
            System.out.printf("%s -> %s%n", args[i], value);
        }
    }
    public static void main(String[] args){
        ToUpper upper = new ToUpper();
        upper.run(args);
    }
}

メソッドの定義方法

メソッドを定義する方法は,基本的にはC言語と同じです.異なる点は,Java言語の場合, メソッド宣言は必ずクラス宣言の内側でなければならない,という点です.

メソッドの典型的な宣言は次の通りです.

// クラス宣言
public class Xxxxx{
    // ReturnType, ArgumentType は適切な型に読み替えてください.
    ReturnType methodName(ArgumentType argumentName, ...){
    }
}

methodName はメソッド名を表しています.この部分はある程度好きな名前を付けられます. 処理の内容が後から見てわかるように付けましょう.ローマ字でも構いません.

また,ReturnTypeは返り値の型を表しており,適切な型に読み替えてください. 例えば,そのメソッドが返り値を持つ場合は,StringInteger など適切な型を宣言しましょう. そのメソッドが何も値を返さない場合は,voidとしてください.省略はできません.

ArgumentType も適切な型に読み替えてください. C言語の場合,引数に何も受け取らない場合,voidと書けましたが,Java言語では書けません. 引数に何も受け取らない場合は,methodName() のように,括弧内には何も書かないようにしましょう.

なお,この ReturnType methodName(ArgumentType argumentName, ...) の部分をメソッドのシグネチャと呼びます. このメソッドの{から} の間をメソッドのボディと呼びます. そして,メソッドのシグネチャとメソッドのボディを書くことをメソッドを定義する,もしくはメソッドを宣言すると言います.

メソッドは細かく分けましょう. 目安として,メソッド内の処理が5行以上になれば,別のメソッドに分けることを考えましょう.

引数, 返り値のないメソッド

引数も帰り値もないメソッドは,フィールドの値を利用したり, フィールドに値を作成したり,常に同じ値を返すような場合に用いられます.

例えば,次のコードは呼び出された時点の日付,時刻で,フィールドのcurrentDateの値を更新します.

import java.util.Date;
public class DateSample{
    Date currentDate = new Date();
    // ... today 以外のメソッドは省略.
    void updateCurrentDate(){
        this.currentDate = new Date();
    }
}

引数あり, 返り値のないメソッド

引数の値を用いて,何らかの処理を行うメソッドの場合に用いられます. 処理の結果は,System.out.printで出力されたり,フィールドに代入することが多いです.

引数なし, 返り値のあるメソッド

String型の文字列を全て大文字にする toUpperCase や全て小文字にする toLowerCase などのように, フィールドの値を外に返すために用いられることが多いです.

例えば,次のメソッドは常に,呼び出し時点の時刻を返します.

import java.util.Date;
public class DateSample{
    // ... today 以外のメソッドは省略.
    Date today(){
        return new Date();
    }
}

引数, 返り値のあるメソッド

値を引数として受け取り,何らかの加工を行った結果を返す場合が多いです.

例題2

前回の練習問題1. HelloWorld改をメソッドを使って書き換えてください. クラス名は,HelloWorld2r としてください.

コマンドライン引数で与えられた人に,挨拶しましょう. もし,コマンドライン引数に何も指定されない場合は, "Hello, World" と出力してください.

以下のプログラムのコメント部分に何を書けば良いかを考えましょう.

public class HelloWorld2r{
  void run(String[] args){
    if(/* 条件は前回の練習問題と同じ */){
      this.greet(/* 引数に何を渡すか考えてください. */);
    }
    else{
      this.greet(/* 引数に何を渡すか考えてください. */);
    }
  }
  void greet(/* 引数となる変数を宣言してください */){
    System.out.printf("Hello, %s%n",
        /* ここに何が来るでしょうか. */);
  }
}

なお,runメソッドが実行している実体と同じ実体に対してメソッドを呼び出すときは, 上記のように,thisというキーワードに対してメソッドを呼び出します.詳細は暗黙の定数thisで説明しています.

public class HelloWorld2r{
    void run(String[] args){
        if(args.length == 0){
            this.greet("World");
        }
        else{
            this.greet(args[0]);
        }
    }
    void greet(String name){
        System.out.printf("Hello, %s%n", name);
    }
    public static void main(String[] args){
        HelloWorld2r hello = new HelloWorld2r();
        hello.run(args);
    }
}

例題3

前回の練習問題3. 階乗をメソッドを使って書き換えてください.

public class Factorial2{
  void run(String[] args){
    Integer number = Integer.valueOf(args[0]);
    Integer factorial =
        this.factorial(/* 引数に何を渡すか考えてください */);
    // number の階乗を計算する.
    System.out.printf("%d! = %d%n", number, factorial);
  }
  // ここに factorialメソッドを定義してください.
}
public class Factorial2{
    void run(String[] args){
        Integer number = Integer.valueOf(args[0]);
        Integer factorial = this.factorial(number);
        // number の階乗を計算する.
        System.out.printf("%d! = %d%n", number, factorial);
    }
    Integer factorial(Integer max){
        Integer factorial = 1;
        for(Integer i = 1; i <= max; i++){
            factorial = factorial * i;
        }
        return factorial;
    }
    public static void main(String[] args){
        Factorial2 factorial = new Factorial2();
        factorial.run(args);
    }
}

メソッドの仮引数と実引数

メソッドの引数を理解するために,仮引数と実引数の2つの概念があることを区別しましょう. 仮引数(parameter)とは,メソッド定義時に定義される引数のことを指し, 実引数(argument)は,メソッド呼び出し時に用いる引数のことである.

次の例で考えてみましょう.

public class LeapYear{
  void run(String[] argsForRun){
    for(Integer i = 0; i < argsForRun.length; i++){
      Integer year = Integer.valueOf(argsForRun[i]);
      Boolean leapYearFlag = this.isLeapYear(year);
      this.printLeapYear(year, leapYearFlag);
    }
  }
  Boolean isLeapYear(Integer y){
    return y % 400 == 0 && (y % 100 != 0 || y % 400 == 0);
  }
  void printLeapYear(Integer y2, Boolean lyf){
    if(lyf)
      System.out.printf("%d年はうるう年です.%n", y2);
    else
      System.out.printf("%d年はうるう年ではありません.%n", y2);
  }
}

このプログラムには,実引数,仮引数がそれぞれ存在します. 仮引数は,2行目 runメソッド宣言時の String型配列のargsForRunと 9行目のisLeapYearメソッドのInteger型のy,そして,12行目の printLeapYearメソッドの y2lyfです. ここは変数を宣言している部分です.

一方の実引数は,5行目の this.isLeapYearメソッドに渡している変数year, そして,6行目のthis.printLeapYearに渡している yearと,leapYearFlagです. これらは既に宣言された変数を利用する箇所です.

仮引数は変数を定義する場所,実引数は,既に宣言された変数を利用する場所であると区別しましょう. ですから,実引数の場所で変数を宣言することはできません.

メソッド内での値の更新

次のプログラムを考えてみましょう.updateDate1updateDate2updateDate3の3つのメソッドそれぞれで 年に1を追加しているように見えます. 動作結果を考えて実行してみましょう. また,なぜこのような結果になるのかを考えてみましょう.

public class DateConfusion{
    void run(){
        Date date;
        date = new Date();
        this.updateDate1(date);
        System.out.println(date);

        date = new Date();
        this.updateDate2(date);
        System.out.println(date);

        date = new Date();
        Date date2 = this.updateDate3(date);
        System.out.println(date);
        System.out.println(date2);
    }
    void updateDate1(Date d){
        d.setYear(d.getYear() + 1);
    }
    void updateDate2(Date d){
        d = new Date(d.getYear() + 1, d.getMonth(),
                     d.getDate());
    }
    Date updateDate3(Date d){
        // dの時刻と同じ時刻の Date 型の実体が作成される.
        Date d2 = new Date(d.getYear() + 1, d.getMonth(),
                           d.getDate());
        return d2;
    }
    // mainメソッドは省略.
}

メソッドの中で参照先が変わったとしてもメソッドの呼び出し元では全く影響を受けません. 一方で,メソッドの中で参照先の実体の状態を変更すると,呼び出し元にも影響を受けます.= による代入は参照先の変更であることをしっかりと理解しましょう.

変数のスコープ

変数はスコープ(scope)を持っています.スコープとは変数の有効範囲のことです. 有効範囲の外からその変数の参照,代入は行えません. プログラム中の {} で囲まれた範囲をブロックと呼びます. スコープは変数が宣言されて以降,宣言されたブロック内でのみ有効です.

以下のプログラム(メソッドの仮引数と実引数の再掲)の各メソッド内の変数のスコープを確認してみましょう.

まず,runメソッドの仮引数である argsForRun のスコープを確認しましょう. 仮引数はそのメソッド内で有効で,メソッドの外側では例え同じ名前であっても違う変数として扱われます. 次に,ループ制御変数であるi,ループ内で宣言された Integer型の year, 最後に,Boolean型のleapYearFlagのスコープを確認してください. 引き続き,isLeapYearメソッドの仮引数であるyprintLearpYearメソッドの仮引数y2lyfのスコープを確認してください. 図をクリックすると,それぞれの有効範囲が出てきます.

このように,変数の有効範囲は基本的には,{から}の範囲内で,宣言されて以降ということがわかると思います. 変数の有効範囲はできるだけ短く,というのが定石です. 有効範囲が広いと,どこからでもアクセスできて,どこで更新されているのかがわからなくなるので,避けるべき,ということです.Q&A変数のスコープが短い方が良いのはなぜですか?も参考にしてください.

暗黙の定数this

各メソッドでは,暗黙的に宣言されたthisという変数が利用できます. thisは,その型の実体が代入されています. 例えば,次の例で考えてみましょう(メソッドの仮引数と実引数の再掲).

public class LeapYear{
  void run(String[] argsForRun){
    for(Integer i = 0; i < argsForRun.length; i++){
      Integer year = Integer.valueOf(argsForRun[i]);
      Boolean leapYearFlag = this.isLeapYear(year);
      this.printLeapYear(year, leapYearFlag);
    }
  }
  Boolean isLeapYear(Integer y){
    return y % 400 == 0 && (y % 100 != 0 || y % 400 == 0);
  }
  void printLeapYear(Integer y2, Boolean lyf){
    if(lyf)
      System.out.printf("%d年はうるう年です.%n", y2);
    else
      System.out.printf("%d年はうるう年ではありません.%n", y2);
  }
  public static void main(String[] args){
    LeapYear ly = new LeapYear();
    ly.run(args);
  }
}

mainメソッドでLearpYearをnewして実体を作成し,lyという変数に代入しています. そして,lyrunメソッドを20行目で呼び出しています(ly.run(args);の行). この呼び出しにより,runメソッド内に処理が移ります. このrunの中でLeapYearの実体を参照する変数がthisです.

5行目と6行目で this が使われており,this.isLearpYear(year)でからisLeapYearメソッドを, this.printLeapYear(year, leapYearFlag)printLeapYearメソッドをそれぞれ呼び出しています.

なお,thisに値を代入しようとするとコンパイルエラーが発生します.

ヒント
  void run(String[] argsForRun){
    LeapYear ly = new LeapYear();
    for(Integer i = 0; i < argsForRun.length; i++){
      Integer year = Integer.valueOf(argsForRun[i]);
      Boolean leapYearFlag = ly.isLeapYear(year);
      ly.printLeapYear(year, leapYearFlag);
    }
  }

例えば,runを上のようにし,thisの代わりにly経由でisLeapYearなどを呼び出したとしても,同じ結果を得られます. ただし,lythisは異なる実体です(ly != this). このようなとき,lyを改めて作成し直す必要はありません.

mainメソッドのlyrunメソッド内のthisは同じ実体ですが, mainメソッドとrunメソッドのlyは異なる実体であることに注意しましょう.

練習問題

1. 与えられた文字列のソート

以下の条件を満たすように,コマンドライン引数 で与えられた複数の文字列をソートして出力するプログラムを作成してください (ソートアルゴリズムを自分で書く必要はありません).

  • 配列を画面に出力するためのprintArrayを実装してください.
    • printArrayで接頭辞を出力できるようにしてください.
  • ソートの前後で,printArrayメソッドを使って配列の要素を出力してください.
  • クラス名は,ArgsSorterとしてください.

ソートは Arraysに対して,sortメソッドを呼び出すと渡した配列の要素がソートされます. ただし,Arraysを利用する場合は,import java.util.Arrays;という一文が クラス宣言の前に必要です.

public class ArgsSorter{
  void run(String[] args){
    // ここで,printArray を呼び出し,"before"の一行を出力する.
    // argsの内容をソートするため,Arrays.sortメソッドを呼び出す.
    Arrays.sort(args); // <= args がソート済みになる.
    // ここで,printArray を呼び出し,"after"の一行を出力する.
  }
  // printArrayメソッドをここに書く.
  // mainメソッドは省略.
}

実行例

$ java ArgsSorter one two three four five six
before: one, two, three, four, five, six,
after: five, four, one, six, three, two,
$ java ArgsSorter time flies like an arrow
before: time, flies, like, an, arrow,
after: an, arrow, flies, like, time,
$ java ArgsSorter 2016 10 6
before: 2016, 10, 6,
after: 10, 2016, 6,

このように,アルファベット順にソートされていることが確認できます. 最後の例は,間違いではありません."2016""10""6"という文字列でソートしている,つまり, 1桁目の文字("2", "1", "6")でソートしているので,この順で正しいです.

2. 学生証番号の正当性を検証するプログラム

本学の学生証番号(6桁)は,各桁を足し合わせると,10の倍数となるようになっています. コマンドライン引数で与えられた学生証番号が正しいものであるかを判定するプログラムStudentIdValidatorを作成してください. 正しい学生証番号であれば,与えられた学生証番号と共に,valid,正しくない学生証番号であれば,invalid, 与えられた文字列が6桁の学生証番号でなければ,not student idと出力してください.

  • String型のIdを受け取るvalidateメソッドを定義してください.
  • Integer型のIdを受け取るvalidateIdメソッドを定義してください.

それぞれのメソッドの処理は次の通りです。 この処理通りにしなければいけないわけではありませんが、 このような処理にすると各メソッドの役割がわかりやすいと思います。

  • runメソッド
    • 引数で受け取った String型配列の各要素を順にvalidateメソッドに渡す。
  • validateメソッド
    • 引数で受け取ったString型の値の長さを確認する。
      • 6桁の学生証番号でなければ、not student idと出力して終了する。
    • 学生証番号であれば、String型をInteger型に変換する。
    • 変換後のInteger型の変数を validateIdメソッドに渡す。
  • validateIdメソッド
    • 引数で受け取ったInteger型変数の正当性を確認する。

ヒント:文字列の長さを取得する.

文字列の長さを取得するには,String型の変数に対して,length()メソッドを呼び出してください.

String name = "Haruaki Tamada";
Integer length1 = name.length(); // 14が代入される.
Integer length2 = "Haruaki Tamada".length(); // length1 と同じく 14 が代入される

上記のように,String型の変数に対して length メソッドを呼び出すとその文字列の長さが取得できる. 同様に String型の値("Haruaki Tamada")に対してもlengthメソッドが呼び出せる.

実行例

$ java StudentIdValidator 024509
024509: valid
$ java StudentIdValidator 123456
123456: invalid
$ java StudentIdValidator 123455 123456
123455: valid
123456: invalid
$ java StudentIdValidator 123509 244409 1024509
123509: valid
244409: invalid
1024509: not student id

3. 二次方程式の解

2次方程式の解を解の公式を用いて求めるプログラム作成してください. その際,実数解,重解,虚数解を区別して出力するようにしましょう. ただし,以下の条件を満たしてください.

  • クラス名は,QuadraticEquationとすること.
  • $ax^2+bx+c$の$a$,$b$,$c$コマンドライン引数から受け取ること.
  • $a$, $b$, $c$Double型の値が与えられるものとする.
  • runメソッドは以下の通りとし,以下の内容に合うようメソッドを定義すること.
    void run(String[] args) {
        Double a = convertToDouble(args[0]);
        Double b = convertToDouble(args[1]);
        Double c = convertToDouble(args[2]);
        Double d = discriminant(a, b, c); // 判別式によりDの値を求める.
        solve(a, b, c, d); // 判別式から実数解,重解,虚数解に場合分けして結果を出力する.
    }
なお,解の公式は$\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$,判別式$D=b^2 -4ac$が正の場合は実数解,$0$の場合は重解,負の場合は虚数解です.

String型からDouble型への変換

文字列からDouble型に変換するには,Double.valueOf(stringValue) を利用する.

// Double value = "3.141592";
    // => 文字列は Double 型にそのまま代入できない.
Double value = Double.valueOf("3.141592");
    // => 文字列の "3.141592"  Double型に変換して代入する

平方根

平方根は,次のコードで求められます.

Integer two = 2;
Double sqrtOfTwo = Math.sqrt(two); // => 1.41421356...

ヒント

  • Double型の数値を printf で出力するときは,%lfではなく,%fもしくは%gで出力しましょう.
    • %f は実数型の数値(DoubleFloat)が出力できます.
    • %g も実数型の数値ですが,最適表示となります.
  • NaNが出力された場合,0除算,もしくは Math.sqrt に負の値で計算しようとしています.
    • NaN は Not a Number の略です.
  • 虚数解の場合,実部と虚数部は別々に計算しましょう.
    • 実部は, $-\frac{b}{2a}$,虚数部は $\pm\frac{\sqrt{-D}}{2a} i$ のように計算しましょう.
      • 判別式$D$に $-1$ を掛けているのは,平方根を求めるメソッド(Math.sqrt)に負数を渡すと結果がNaNになるためです.

実行例

$ java QuadraticEquation 1 -4 4
answer = 2.000000
$ java QuadraticEquation 1 -4 8
answer = 2.000000 + 2.000000 i, 2.000000 - 2.000000 i
$ java QuadraticEquation 1 0 -4
answer = 2.00000, -2.00000

4. モンテカルロ法による $\pi$の計算

モンテカルロ法により,$\pi$を計算しましょう. モンテカルロ法とは,乱数を用いて統計計算を行う方法です. ここでは,$\pi$を求めます.次のアルゴリズムに従って実装してください.
  1. 0.0〜1.0の乱数を2つ取得し,$x, y$とします.
  2. 原点と$(x, y)$の距離を計算します.距離は,$\sqrt{x^2+y^2}$で求められます.
  3. 計算した距離が1よりも小さい場合,ヒットしたとみなします
    • 距離が1以下の場合,点は円の内側に存在します(ヒットした).
    • 距離が1よりも大きな場合,点は円の外側に存在します.
  4. 1〜3を規定回数繰り返します.
  5. ヒットした回数の割合を計算します.
  6. この割合が$\frac{\pi}{4}$になるので,4を掛け$\pi$を計算します.
下図のように半径1の円の第1象限に注目します. 0.0から1.0の乱数を2つ取得して,$x, y$とします. 下図の点が得られた乱数値から求められた座標とします. 求められた座標と原点との距離を計算します.距離は,$\sqrt{x^2+y^2}$で求められます. この距離が1よりも小さい場合,円の内側に存在するため,ヒットしたものとします(1回目のクリックの時の画像). 一方,円の外側にある場合は,ヒットしないものとします(次のクリックの時の画像). これを規定回数(デフォルトは1000回とします.)繰り返してください. このとき,ヒットした確率は $\frac{\pi}{4}$に近付いていくため, ヒットした確率に 4をかけると$\pi$の近似値が得られます.

クラス名は,MonteCarloPiとします. 引数に値が指定された場合,その値だけ繰り返してください. 値が何も指定されない場合は,1,000回の繰り返しとします.

実行例

計算結果は必ずしもこの通りではありません.

$ java MonteCarloPi
pi = 3.10800
$ java MonteCarloPi
pi = 3.17200
$ java MonteCarloPi 10000
pi = 3.13916
$ java MonteCarloPi 100000
pi = 3.14776
$ java MonteCarloPi 1000000
pi = 3.14127
$ java MonteCarloPi 10000000
pi = 3.14229

ヒント

5. 台形公式による積分計算を利用した $\pi$の計算

台形公式を用いて$\pi$を計算してください. まず,半径1の円を表す式$x^2+y^2=1$を考えましょう. 式を変形すると,$y=\sqrt{1−x^2}$となります. これの$0\leq x \leq 1$の範囲を考えます. 下図のように,この式で描かれるグラフに内接するいくつかの台形を考えます. 下図を1度クリックすると,2つの台形(1つは三角形のように見えますが,上底(右側の高さ)が0に近い台形と考えてください)があり, 横幅は$x_i−x_{i−1}$,高さは$h_{i−1}, h_i$です($h_{i−1}\ge h_i$). このとき,一つの台形の面積は,$\frac{(x_i−x_{i−1})\times(h_{i−1}+h_i)}{2}$です. 高さを求めるには,$y=\sqrt{1−x^2}$にその時の$x_i$の値を代入することで求められます.
これをすべての台形について求めます.そして,求めたすべての台形の面積を足し合わせると, $\frac{\pi}{4}$の近似値が得られます. 得られた値に4を掛けると$\pi$が得られます. なお,すべての$i$において,横幅の間隔($x_i − x_{i−1}$)は同じ値(等間隔)です. さらに,横幅により狭い値を指定することで,値を$\pi$に近づけられるようになります. さて,この方法で,$\pi$を求めるプログラムを作成しましょう.

クラス名は, TrapezoidalRulePiとします. 作成する際に,コマンドライン引数で台形の横幅を指定できるようにしてください. もし,コマンドライン引数で値が指定されなければ,0.0001が指定されたものとしてください.

実行例

計算結果は必ずしもこの通りでなくても構いません (多少の計算誤差があったとしても構いません).

$ java TrapezoidalRulePi 0.01
pi = 3.1375956845831103
$ java TrapezoidalRulePi 0.001
pi = 3.1414660465553967
$ java TrapezoidalRulePi
pi = 3.141591477698228
$ java TrapezoidalRulePi 0.0000001
pi = 3.141592654152668

ヒント

まとめ

まとめ

第3講 Java言語の基礎4

この講で利用するプログラム

第3講 Java言語の基礎4のサブセクション

配列とList

Listとは

第1講で,Javaでは,配列ではなく List を使うように述べました. ここでは,Listの使い方を学びます.

Java言語に限らずListとはデータ構造の一つで,順序を持つ複数のデータを扱います. 要するに配列を置き換えるものです.Listの特徴は次の通りです.

  • 格納された要素をインデックスにより参照できる.
  • 要素を追加・削除できる.
  • 生成時にサイズを決める必要はない.
  • サイズは必要に応じて自動的に増加,縮小する.
  • 途中の要素を削除しても,間が詰められる.

基本的な特徴は配列と同じです. しかし,配列にはない特徴もあります.例えば,サイズを生成時に決める必要がない, やサイズが自動的に増減するなどです.Listは配列よりも優れた特徴を持つ型ですから, 配列ではなく,Listを積極的に使っていきましょう.

Listの種類

Java言語では順序を持つデータの集合をListと呼びます. その実現方法は配列を用いる方法,リンクリストを用いる方法の2種類があります. それぞれの実現方法には向いている点,向いていない点が存在します. そのため,Javaにはそれぞれの実現方法でListを実現する型が 存在します.ArrayListLinkedListの2つの型です.どちらを使っても処理の違いはありませんが, 処理内容によっては実行速度が違ってくる可能性があります. とはいえ,その違いが効いてくるのは,もっと大規模で,実行速度が重視される場合ですから,今は気にしなくても良いでしょう.

リンクリストの詳細については,FAQリンクリストとは何ですかを参照してください.

基本的にArrayListLinkedListも使い方に違いはありません. 以下の例では,ArrayListで説明していますが,ArrayListLinkedListに置き換えても 同じ説明が成り立ちますので,適宜読み替えてください.

なお,ArrayListを使うときには,import文が必要です.import java.util.ArrayList;クラス宣言の前に書きましょう.

Listの宣言方法

Java言語でListを使うには,今までとは少し異なる宣言方法が必要です. データ構造にどのような型の変数を格納するかを型宣言に含める必要があります. 例えば,ArrayListString型や,Integer型を格納しようとすると,次のような宣言が必要になります.

// String型を格納する ArrayList.
ArrayList<String> listForStrings =
    new ArrayList<String>();
// Integer型を格納する ArrayList.
ArrayList<Integer> listForIntegers =
    new ArrayList<Integer>();

上記のように,ArrayList<格納する型> という型として宣言しなければならず,また, 実体を作成するときも,new ArrayList<格納する型>() のように作成しなければいけません. 格納する型が異なるとコンパイルエラーが発生します.

なお,実体を作成するときの格納する型は,下のように省略できます.以下のコードは上に挙げたコードと全く同じ結果になります.

ArrayList<String> listForStrings = new ArrayList<>();
ArrayList<Integer> listForIntegers = new ArrayList<>();

Listの操作

一般的に Listに対して行える操作は4種類です. その操作は,CRUDと呼ばれます.作成(Create),読み取り(Read),更新(Update),削除(Delete)の4種類です. 以降の説明は,次に示すArrayList型の変数に対してメソッドを呼び出すものとして読み進めてください.

ArrayList<String> list = new ArrayList<>();
// ArrayList<String> list = new ArrayList<String>();

Listにデータを追加する (add)

String value1 = // ...
list.add(value1);
list.add("Haruaki Tamada");
// list.add(9); // => コンパイルエラーが発生する.
    // 9 は String型ではないため.
list.add("9"); // => OK"9"は文字列

データを追加するには,上のサンプルのようにaddメソッドを用います. データ集合の最後に追加されていきます.用意されている長さを超えて追加しようとすると,自動的に長さが伸びていくため, 理論上は,無制限にデータを追加できます.

Listにあるデータを取得する (get)

String item1 = list.get(0);
    // => listのインデックスも配列と同じように0から始まる.
String item2 = list.get(1);
String item100 = list.get(100);
    // => 範囲を超えてアクセスしようとすると,実行時に
    // IndexOutOfBoundsException というエラーが発生する

上のサンプルのように,getメソッドを呼び出すことで,List内の特定の要素を取得できます. 返り値の型は,ArrayList型の変数の宣言時に指定した型でなければいけません.

Listにあるデータを更新する (set)

list.set(1, "TAMADA, Haruaki");

特定の要素を置き換える場合は,setメソッドが利用できます. インデックスと置き換え後のデータを指定すると,データの更新が可能です. 指定したインデックスに要素が存在しない場合は,IndexOutOfBoundsExceptionというエラーが発生します.

Listにあるデータを削除する (remove)

list.remove(1);

指定したインデックスの要素を削除したい場合は,removeメソッドを 利用します.removeを使って削除したあと,後ろの要素は詰められます. すなわち,次のコードで全ての要素を順番に削除できます.

while(!list.isEmpty()){ // listが空じゃない間繰り返す.
    list.remove(0);     // 一番最初の要素を削除する.
}

Listのサイズを取得する (size)

List の現在のサイズ(長さ)を取得する場合は,sizeメソッドを利用しましょう. 文字列の長さ,配列の長さと混同しやすいため,注意してください.

  • 文字列(String型)の長さ
    • string.length()(メソッド呼び出しで取得するため括弧が必要).
  • 配列の長さ
    • array.length(変数アクセスのため,括弧は不要).

Listの要素の繰り返し (Iterator)

Java言語での Listの繰り返しは,次の3種類が利用できます.

典型的な方法

一番典型的な方法です.ただし,ループの途中で listの要素数を変化させることは 混乱の元になるので,ループ内での Listへの値の追加・削除は行わない方が良いでしょう.

for(Integer i = 0; i < list.size(); i++){
    String item = list.get(i);
    // ここに繰り返しの処理を書く.
}

Iterator

Iterator 型を利用する方法.Javaらしい書き方.

for(Iterator<String> iterator = list.iterator();
        iterator.hasNext(); ){
    String item = iterator.next();
    // ここに繰り返しの処理を書く.
}

拡張for文

拡張for文と呼ばれる書き方.実質的には,Iteartor型を利用する方法と同じ. コンパイラがIterator型を利用する方法に変換してコンパイルする.最近はこの書き方が多い.

for(String item: list){
    // ここに繰り返しの処理を書く.
}

例題1 argsArrayListに変換する

コマンドライン引数に受け取ったString型の値を全て ArrayListに入れ,ArrayListから順に取り出し,出力するプログラムを書きましょう.

import java.util.ArrayList;
public class ArgsPrinter2{
  void run(String[] args){
    ArrayList<String> list = this.buildList(args);
    this.printList(list);
  }
  ArrayList<String> buildList(String[] array){
    ArrayList<String> arrayList = new ArrayList<>();
    for(Integer i = 0; i < array.length; i++){
      arrayList.add(array[i]);
    }
    return arrayList;
  }
  void printList(ArrayList<String> arrayList){
    for(String item: arrayList){
      System.out.println(item);
    }
  }
  public static void main(String[] args){
    ArgsPrinter2 printer = new ArgsPrinter2();
    printer.run(args);
  }
}

このプログラムを書き,コンパイル,実行してみましょう. 実行時にコマンドライン引数に値を指定して実行してみましょう.

例題2 乱数の生成

50個のDouble型の 0〜1の乱数をArrayListに入れて,出力してみましょう. クラス名は,DoubleValuePrinterとしてください. 乱数の発生方法は,Big & Smallを参照してください. なお,リストを生成する部分,出力する部分を別のメソッドにしてみましょう. クラス名はDoulbeValuePrinterとしましょう. 

完成すれば,コマンドライン引数で発生させる乱数の個数を指定できるようにしてください. 指定されなければ,50個としてください.

出力例

$ java DoubleValuePrinter
  1: 0.24279591112755294
  2: 0.7216985840426494
  3: 0.5978665614812361
... 途中省略
 48: 0.15288776496056167
 49: 0.8335019950136539
 50: 0.8114170360899468
$ java DoubleValuePrinter 3
  1: 0.20585052641970603
  2: 0.578743233682112
  3: 0.107553196759134
import java.util.ArrayList;

public class DoubleValuePrinter{
    void run(String[] args){
        ArrayList<Double> list = this.buildList(args); // リスト作って
        this.printList(list);                          // 出力する.
    }
    ArrayList<Double> buildList(String[] args){
        Integer count = this.parseCount(args); // 生成する個数を決める.
        ArrayList<Double> arrayList = new ArrayList<>(); // 格納するリストを生成する.
        for(Integer i = 0; i < count; i++){
            arrayList.add(Math.random()); // 実際に格納する.
        }
        return arrayList; // リストを返す.
    }
    Integer parseCount(String[] args){
        if(args.length == 0){ // コマンドライン引数で何も与えられなかった場合
            return 50;
        }
        return Integer.valueOf(args[0]); // コマンドライン引数で数が与えられた場合
    }
    void printList(ArrayList<Double> arrayList){
        for(Integer i = 0; i < arrayList.size(); i++){
            System.out.printf("%2d: %f%n", i, arrayList.get(i));
        }
    }
    public static void main(String[] args){
        DoubleValuePrinter printer = new DoubleValuePrinter();
        printer.run(args);
    }
}

ユーザ定義型

ユーザ定義の型を定義する

Javaでは,自分で型を作成することもできます. まずは,人の氏名(姓,名)を持つ型 Person を作成してみましょう.

Person型

public class Person{
    String givenName;
    String familyName;
}

これで,Person型の作成は完了です.Person 型は2つの String 型のフィールド変数 を持っています.givenName(名)とfamilyName(姓)です.

このプログラムだけでは何もできませんので,Person型を利用するプログラムPersonManagerを作成しましょう.

PersonManager型

public class PersonManager{
  void run(){
    Person person1 =
        this.createPerson("Haruaki", "Tamada");
    this.printPerson(person1);
  }
  Person createPerson(String name1, String name2){
    Person person = new Person();
    person.givenName = name1;
    person.familyName = name2;
    return person;
  }
  void printPerson(Person person){
    System.out.printf("%s, %s%n",
        person.familyName, person.givenName);
  }
  public static void main(String[] args){
    PersonManager app = new PersonManager();
    app.run();
  }
}

以上のプログラムを作成し,実行してみましょう. なお,Person.javaPersonManager.javaは同じディレクトリに置いておく必要があります.

例題1 独自型の実体を作成する.

上で作成した PersonManager 型を以下の指示に従って拡張しましょう.PersonManager.javarunメソッドに以下の処理を追加してください. その際,メモリの状態を想像しながらプログラムを書きましょう.

フィールドの参照・代入もメソッド呼び出しと同じように,どの実体のフィールドなのかを 意識して指定してください.Person型のフィールドgivenNameには,上記のperson.givenName = name1 のように,Person型の変数を介してしかアクセスできません.

  1. Person 型の変数 person2 を宣言してください.
  2. person2 に自分の名前を持つ Person 型の実体を作成して,代入してください.
  3. person2 の情報を画面に出力してください.
public class PersonManager{
  void run(){
    Person person1 =
        this.createPerson("Haruaki", "Tamada");
    this.printPerson(person1);

    Person person2 = createPerson("名前", "自分の");
    this.printPerson(person2);
  }
  Person createPerson(String name1, String name2){
    Person person = new Person();
    person.givenName = name1;
    person.familyName = name2;
    return person;
  }
  void printPerson(Person person){
    System.out.printf("%s, %s%n",
        person.familyName, person.givenName);
  }
  public static void main(String[] args){
    PersonManager app = new PersonManager();
    app.run();
  }
}

PersonManager.javaのコンパイル,実行には,同じディレクトリにPerson.javaを置いておく必要があります.

例題2 実体の代入

次の処理を PersonManagerrun メソッドに追加していきましょう.

  1. Person 型の変数 person3 を宣言してください.
  2. person3person1 を代入してください.
  3. person3familyName を全て大文字にしてください.
    • person3.familyName = person3.familyName.toUpperCase();
  4. person1 の情報を画面に出力してください.
メモリ状態
public class PersonManager{
  void run(){
    Person person1 =
        this.createPerson("Haruaki", "Tamada");
    this.printPerson(person1);

    Person person2 = createPerson("名前", "自分の");
    this.printPerson(person2);

    Person person3 = person1;
    person3.familyName = person3.familyName.toUpperCase(); // <= person3 の値を更新する.
    this.printPerson(person1); // <= person1 を出力する.
  }
  Person createPerson(String name1, String name2){
    Person person = new Person();
    person.givenName = name1;
    person.familyName = name2;
    return person;
  }
  void printPerson(Person person){
    System.out.printf("%s, %s%n",
        person.familyName, person.givenName);
  }
  public static void main(String[] args){
    PersonManager app = new PersonManager();
    app.run();
  }
}

例題3 独自の型をListに入れる

上で作成したPerson型の実体 person1person3ArrayListに入れて出力部分をまとめてみましょう.

ArrayListには,Integer 型以外でもどのような型でも入れられますので, 上記のように,ArrayList にどのような型を入れるのか,入っている型が何かをしっかりと認識しましょう.

ヒント

    void run(){
        // 今までの処理内容.
        // ただし,printPersonの行は全て削除しておく.
        ArrayList<何を格納するのか> list = new ArrayList<>();
        // listに person1 〜 person3 を追加する.
        this.printPersons(list);
    }
    void printPersons(...){ // <= printPerson を変更する.
        // 引数で与えられた list から要素を一つずつ取り出し,出力する.
    }
public class PersonManager{
  void run(){
    Person person1 =
        this.createPerson("Haruaki", "Tamada");

    Person person2 = createPerson("名前", "自分の");

    Person person3 = person1;
    person3.familyName = person3.familyName.toUpperCase(); // <= person3 の値を更新する.

    ArrayList<Person> list = new ArrayList<>();
    printPersons(list);
  }
  Person createPerson(String name1, String name2){
    Person person = new Person();
    person.givenName = name1;
    person.familyName = name2;
    return person;
  }
  void printPersons(ArrayList<Person> persons){
    for(Person person: persons){
      System.out.printf("%s, %s%n",
          person.familyName, person.givenName);
    }
  }
  public static void main(String[] args){
    PersonManager app = new PersonManager();
    app.run();
  }
}

練習問題

ここに挙げている問題では配列の使用は mainメソッドの引数として与えられた変数 argsのみとし, その他は,List (ArrayListもしくはLinkedList)を使用してください.argsを 受け渡す分には配列を使って構いませんが,新たに配列を作成しないようにしてください.

1. 乱数値100個の統計

0以上,1000未満の乱数を100個取得してください. それらの合計値,最大値,最小値,平均を求めてください. 出力は,まず,合計,最大値,最小値,平均を1行で出力してください. その次に,得られた乱数値を出力してください.ただし,10個出力するごとに改行を入れてください. クラス名は,StatsValues としてください.

平均をDouble型として求めるには,Integer型の合計値sumDouble型に変換する必要があります. Double.valueOf(sum)Double型に変換でき,それ以降Double型として扱えるようになります.

整数乱数を生成する

0以上,1000未満の乱数値を取得するには,Random型を利用します.Random型の 利用には,import java.util.Randomがクラス宣言の前に必要です. 以下のコードで1000未満の正数乱数が得られます.

なお,Random型の初期化は最初に1度だけ行うようにしてください. 乱数を得るたびにRandom型を初期化すると乱数にならない場合があります.

Random random = new Random();
Integer randomValue = random.nextInt(1000);
    // => 0以上1000未満の正の乱数値が得られる

出力例

乱数ですので,必ずしもこの通りの結果にはなりません.

$ java StatsValues
合計: 45262, 最大値: 995, 最小値: 11, 平均値: 452.620000
252  37 553 448 504 144 969 928 177 262 
836  15 198 496 650 977 102 630 348 351 
820  59 288 435 622 677 103 588 576 683 
916 138 154 528 179 411 578 740 715 372 
351 105  41 203 596 746 195 153 469 328 
166 189 754 862 541  84 165 428 567  76 
848 730 947 439 376 258 330 365 896 144 
688  27 380 573 377 752  19  70 965 605 
551 355 103 860 629 660 528 465 995 134 
154 574  11 763 268 443 723 872 233 674

2. 素数の一覧

コマンドライン引数で与えられた値までの素数を出力してください. なお,素数を10個出力するごとに改行を入れてください. クラス名は Primes とします. コマンドライン引数で値が指定されなかった場合は,200までの素数を求めましょう.

素数を求めるには,エラトステネスのふるいを使うと良いでしょう. 次の画像は,0以上100未満の素数を求める時のエラトステネスのふるいのアルゴリズムを表したものです. 順に最後まで繰り返して残った値が素数です. 画像をクリックすると順に素数ではない数値が消えてきます.参考にしてください.

エラトステネスのふるい

まず ArrayListに求めたい値だけ,素数であるフラグ(Boolean型を使えば良いでしょう)をあらかじめ追加しましょう. そして,そこから,素数でないものを順に除外して来ましょう. その後,素数のみを納める別のArrayListを作成し,素数フラグのリストを順に見て素数のリストに格納していきましょう. その素数リストを返すArrayList<Integer> generatePrimes(Integer max) メソッドを作成しましょう.

出力例

$ java Primes 
    2     3     5     7    11    13    17    19    23    29
   31    37    41    43    47    53    59    61    67    71
   73    79    83    89    97   101   103   107   109   113
  127   131   137   139   149   151   157   163   167   173
  179   181   191   193   197   199 
$ java Primes 59
    2     3     5     7    11    13    17    19    23    29
   31    37    41    43    47    53    59

ヒント

ArrayList<Integer> generatePrimes(Integer max){
  ArrayList<Boolean> primes = new ArrayList<>();
  for(Integer i = 0; i <= max; i++){
    primes.add(true); // 仮に全てのiが素数であるとする.
  }
  primes.set(0, false); // 0は素数ではない.
  primes.set(1, false); // 1は素数ではない.

  for(Integer i = 2; i < primes.size(); i++){ 
        // 最小の値である2から始める.
    if(!primes.get(i)){ // iが素数ではなかったら何も行わない.
      continue;
    }
    // j = i * 2 から始めて j += i のインデックスを false にする.
  }
  return primesList(primes);
}
ArrayList<Integer> primesList(ArrayList<Boolean> primes){
  ArrayList<Integer> returnList = new ArrayList<>();
  for(Integer i = 2; i < primes.size(); i++){
    if(primes.get(i)){ // 素数なら returnList に追加する.
      returnList.add(i);
    }
  }
  return returnList;
}

3. 素因数分解

コマンドライン引数で与えられた整数値の素因数分解を行ってください. クラス名は,Factorizerとしてください.

素因数分解を行うには,素数で割り切れなくなるまで,その素数を素因数として記録します. そして,次の小さな素数で同じことを繰り返します.

  • 例えば,12 を素因数分解するとき,一番小さな素数である2で割り切れるかを確認します.
  • 1回目は割り切れ,2を素因数に追加し,割った後の数は6です.
  • 次に,62で割り切れます.再度,素因数に2 を追加します.割った後の数は3です.
  • 32では割り切れませんので,次の小さな素数で割り切れるかを確認します.
  • 33で割り切れますので,素因数として3を追加します.割った後の数は1ですので,これで終了です.

素数のリストを得るには,素数の一覧で作成した PrimesgeneratePrimes メソッドを 利用すれば良いでしょう.Primes.javaFactorizer.javaを同じディレクトリに置いてください. 先ほどの練習問題で適切にプログラムが書けていれば,次のプログラムで100までの素数の一覧が取得できます. なお,この場合,100以上の数を素因数に持つ数値は素因数分解できません. どのような値を generatePrimes メソッドに渡せば良いかに注意してください(定数を利用するのはやめましょう).

Primes primes = new Primes();
ArrayList<Integer> list = primes.generatePrimes(100);

出力例

$ java Factorizer 12
   12: 2 x 2 x 3
$ java Factorizer 24 50
   24: 2 x 2 x 2 x 3
   50: 2 x 5 x 5
$ java Factorizer 123 127
  123: 3 x 41
  127: 127

ヒント

4. 試験成績の分析

import java.util.Random;
public class ExamAnalyzer{
  ExamScore createRandomScore(String name){
    Random random = new Random();
    Integer math = random.nextInt(101);
    Integer physics = random.nextInt(101);
    Integer english = random.nextInt(101);
    return this.createExamScore(math, physics, english, name);
  }
}

上記のプログラムを踏まえて,以下のプログラムを作成してください(各クラスを別のファイルに分けて,両方のソースファイルを提出してください).

  1. ExamScore型を作成してください.
    • Integer型のmath
    • Integer型のphysics
    • Integer型のenglish
    • String型のnameをフィールドに持ちます.
  2. ExamAnalyzerに次のメソッド,処理を追加してください.
    • runメソッドを作成してください.引数,返り値はなしで構いません.
    • 3つのInteger型,1つのString型を受け取り,ExamScoreを返す createExamScoreメソッドを定義してください.
      • メソッドのボディで,ExamScoreの実体を作成し,引数の値を作成したExamScoreの実体のフィールドに代入してください.
      • 代入が終了した ExamScoreの実体を返してください.
    • runメソッド内で次の処理を行ってください.
      • ExamScore型の実体を格納するArrayList実体を作成してください.
      • createRandomScoreメソッドを用いて,ランダムな成績を10個作成してください.
        • 名前は数値の連番("0""9")にしましょう.
        • Integer型変数intValueString型に変換するには,"" + intValueもしくは,intValue.toString()としてください.
      • 作成したArrayListに上記で作成したランダムな成績を追加してください.
      • 全員のmathphysicsenglishごとに平均値,最大値,最小値を求めてください.
      • それぞれExamScoreの実体に対して,mathphysicsenglishの平均値を求めてください.
      • 求めた値を出力してください.

出力例

$ java ExamAnalyzer
       math   phys   eng    ave
ave   51.200 53.100 52.500
max       81     75     99
min       26      0      3
  0       72     53     11 45.333
  1       36     71     65 57.333
  2       33     26      3 20.667
  3       81      0     99 60.000
  4       42     75     36 51.000
  5       26     40     13 26.333
  6       64     57     55 58.667
  7       43     66     90 66.333
  8       50     72     84 68.667
  9       65     71     69 68.333

上記のように Double型の小数点を整形して出力するには,System.out.printf の フォーマット指定を%6.3fとしてください.%6.3f6が 全ての桁数の指定,.3が小数点以下の桁数を指定しています.

まとめ

まとめ

Random random = new Random();
Integer integerRandomValue1 = random.nextInt(100); // 0以上100未満の乱数を返す.
Integer integerRandomValue2 = random.nextInt();    // Integer型の乱数を返す

第4講 数値計算(1/2)

この講で利用するプログラム

第4講 数値計算(1/2)のサブセクション

ニュートン法による平方根の計算

アルゴリズム

平方根は Math.sqrtメソッドの呼び出しで求められると述べました. ここでは,Math.sqrtを使わず,ニュートン法を用いて平方根を計算してみましょう.

ニュートン法は,関数$f(x)$が与えられた時,その導関数$f'(x)$を用いて,$f(x) = 0$となる解$x$を数値的に求める方法です. $f(x)=x^2 - 2$とすると,$f(x)=0$の解は$x=\sqrt{2}$ ($x > 0$のとき)です. このように,$f(x)=x^2 - n$の解$x=\sqrt{n} (x > 0)$をニュートン法で求めることで,平方根を計算します.

ニュートン法のアルゴリズムは次の通りです.

  • 適当な出発点 $x_0$から始まります.
  • $f(x_0)$の時の$y$座標の値を計算し,$(x_0, y_0)$を求めます.
  • $y_0$の絶対値が閾値(threshold; $0.00001$程度)より小さければ,$x_0$が平方根の値となります.
  • $(x_0, y_0)$における$f(x)$の接線の式$l_0$を求めます.
  • $l_0$と$x$軸との交点座標を求めます.この交点を$x_1$とし,手順の最初に戻ります(今までの$x_0$を$x_1$に置き換えて処理を進めてください).

絶対値

絶対値は,Math.absメソッドを利用して求められます.

Double value = -10.5;
Double positiveValue = Math.abs(value);
// => 10.5 が代入される
ニュートン法

例題

実際にニュートン法のプログラムを書いてみましょう.

import java.util.ArrayList;
public class SquareRoot{
    void run(String[] args){
        ArrayList<Double> targets = findTargets(args);
        for(Double value: targets) {
            Double result = calculate(value);
            System.out.printf("sqrt(%f) = %f (%f)%n",
                    value, result, Math.sqrt(value));
        }
    }
    ArrayList<Double> findTargets(String[] args) {
        ArrayList<Double> targets = new ArrayList<>();
        for(Integer i = 0; i < args.length; i++){
            targets.add(Double.valueOf(args[i]));
        }
        return targets;
    }
    Double calculate(Double n){
        Double threshold = 0.00001;

        Double xValue = 10.0; // 初期値 x0
        Double yValue = function(n, xValue);
        // ここにニュートン法のプログラムを書きましょう.

        // |yValue| < threshold ならばループを抜ける.
        // (yValue の絶対値が閾値(threshold)よりも小さい)
        while(...){
            // xValue における放物線f(x)傾きを求める.
            // 傾き(slant)は 2 * xValue で求められる.
            // f'(x)=2x であるため.

            // 次は,接線が y 軸と交わる切片 b を求める(y = a x + b).
            // (xValue, yValue) を通り 傾き a は先ほど求めた.
            // そのため,b = yValue - (slant * xValue) で求める.

            // 次に,接線が x 軸と交わるときの xValue の値を求める.

            // yValue に 放物線の y の値(xValueを元に求める)を代入する.
        }
        return xValue;
    }
    // x^2 - n を計算するメソッド.
    Double function(Double n, Double x){
        return x * x - n;
    }
}

出力例

$ java SquareRoot 2 3 4 5 6
sqrt(2.000000) = 1.414214 (1.414214)
sqrt(3.000000) = 1.732051 (1.732051)
sqrt(4.000000) = 2.000000 (2.000000)
sqrt(5.000000) = 2.236070 (2.236068)
sqrt(6.000000) = 2.449490 (2.449490)
public class SquareRoot{
    void run(String[] args){
        ArrayList<Double> targets = findTargets(args);
        for(Double value: targets) {
            Double result = calculate(value);
            System.out.printf("sqrt(%f) = %f (%f)%n",
                    value, result, Math.sqrt(value));
        }
    }
    ArrayList<Double> findTargets(String[] args) {
        ArrayList<Double> targets = new ArrayList<>();
        for(Integer i = 0; i < args.length; i++){
            targets.add(Double.valueOf(args[i]));
        }
        return targets;
    }
    Double calculate(Double n){
        Double threshold = 0.00001;

        Double x = 10.0; // 初期値 x0
        Double y = function(n, x);
        // ここにニュートン法のプログラムを書きましょう.

        while(Math.abs(y) > threshold){
            Double slant = 2 * x;
            // y = a x + b が接線の式.傾きaは2xとなる.
            // bを求める.
            Double b = y - slant * x;
            // y = 0 の時のx座標を求める.
            x = -1 * b / slant;
            y = function(n, x);
        }

        return x;
    }
    // x^2 - n を計算するメソッド.
    Double function(Double n, Double x){
        return x * x - n;
    }

    public static void main(String[] args){
        SquareRoot sqrt = new SquareRoot();
        sqrt.run(args);
    }
}

任意桁の計算

BigInteger

Java言語の IntegerLong型は表せる桁数が決まっています.Integerは 32ビット,Longでも64ビットであるため,それぞれ, $-2^{31}〜2^{31} - 1$,$-2^{63}〜2^{63} - 1$ までの値までしか扱えません.

Javaでは任意桁の計算が行える型が存在します. 任意桁の整数を表すBigIntegerと任意桁の実数を表すBigDecimalです. これらを扱ってみましょう.

BigInteger の初期化

任意桁の整数を表す型です. BigIntegerを利用するときは,import java.math.BigInteger;というimport 文が必要です. BigInteger型の実体を作成するときは,new BigInteger("表したい数")のように数値を文字列で指定してください. この型を扱うとき,通常の四則演算記号が使えない点に注意してください.

BigInteger value1 = new BigInteger("10");
BigInteger value2 = new BigInteger("20");
BigInteger value3 = value1.add(value2); // => OK
BigInteger value4 = value1 + value2;    // => コンパイルエラー

Integer型,BigInteger型の相互変換

プログラム中でInteger型の値をBigIntegerとして扱いたい場合や,逆にBigInteger型の値をInteger型として扱いたい場合があります. 以下のプログラムのような操作により,相互変換が可能です.

Integer intValue = 10;
// Integer型からBigInteger型へ変換する.
BigInteger bigValue = BigInteger.valueOf(intValue);
// BigInteger型からInteger型へ変換する.
// もし,bigValue が大きすぎて 32ビットに収まらない場合は,下位32ビットのみが返される.
Integer intValue2 = bigValue.intValue();

BigInteger の四則演算

上記のように,足し算は addというメソッド呼び出しで実現します. 以下に対応を掲載します.どれも BigInteger型の変数b1b2を使って計算しているものとします.

  • 足し算(b1 + b2
    • b1.add(b2)
  • 引き算(b1 - b2
    • b1.subtract(b2)
  • 掛け算(b1 * b2
    • b1.multiply(b2)
  • 割り算(b1 / b2
    • b1.divide(b2)
  • 割った余り(b1 % b2
    • b1.remainder(b2)
  • 符号反転(-b1
    • b1.negate()
  • 比較
    • b1.compareTo(b2)
      • b1の方が小さければ,負の整数.
        • b1 < b2 ならば b1.compareTo(b2) < 0
      • b1の方が大きければ,正整数.
        • b1 > b2ならば b1.compareTo(b2) > 0
      • 同じ値であれば,0
        • b1 == b2ならば b1.compareTo(b2) == 0

なお,BigDecimalも基本的にはBigIntegerと同じです. メソッド呼び出しで演算を行い,実体を作成するときは,実数を表す文字列を渡せば良いです.

サンプルプログラム

先ほど示した計算方法を実際にプログラムに書いてみましょう. 2つのBigInteger型の変数を作成し,四則演算,あまり,符号逆転の操作を行い,結果を表示します.

import java.math.BigInteger;
public class BigOperator{
    void run(){
        BigInteger b1 = new BigInteger("10");  // BigInteger型の実体を作成する.
        BigInteger b2 = BigInteger.valueOf(3); // Integer型からBigInteger型へ変換する.

        BigInteger result1 = b1.add(b2);       // b1 + b2
        BigInteger result2 = b1.subtract(b2);  // b1 - b2
        BigInteger result3 = b1.multiply(b2);  // b1 * b2
        BigInteger result4 = b1.divide(b2);    // b1 / b2
        BigInteger result5 = b1.remainder(b2); // b1 % b2
        BigInteger result6 = b1.negate();      // -b1

        System.out.printf("%s + %s = %s%n", b1, b2, result1);  // <= 13
        System.out.printf("%s - %s = %s%n", b1, b2, result2);  // <= 7
        System.out.printf("%s * %s = %s%n", b1, b2, result3);  // <= 30
        System.out.printf("%s / %s = %s%n", b1, b2, result4);  // <= 3
        System.out.printf("%s %% %s = %s%n", b1, b2, result5); // <= 1
        System.out.printf("-%s = %s%n", b1, result6);          // <= -10

        if(b1.compareTo(b2) > 0){ // b1 > b2
            System.out.println("b1 の方が b2 より大きい.");
        }
    }
}

例題 階乗改

第1講 練習問題 3. 階乗 を改良し, 桁あふれを起こさないようにしましょう. 前回作成した階乗のプログラムは,13!を正確に計算できません. これを BigInteger を使って,どんな数値が与えられたとしても計算できるようにしてください. クラス名をBigFactorialとしてください. コマンドライン引数から値を受け取れるようにしましょう. また,コマンドライン引数で受け取る値はInteger型として扱って良いです.

出力例

$ java Factorial 12 # => 前回のプログラム.正しい結果の限界.
12! = 479001600
$ java Factorial 13 # => 前回のプログラム.正しくない結果.
13! = 1932053504
$ java BigFactorial 11
11! = 39916800
$ java BigFactorial 12 13 14 15
12! = 479001600
13! = 6227020800
14! = 87178291200
15! = 1307674368000
import java.math.BigInteger;

public class BigFactorial{
    void run(String[] args){
        for(String arg: args){
            this.printFactorial(Integer.valueOf(arg));
        }
    }
    void printFactorial(Integer to){
        BigInteger answer = BigInteger.ONE;
        for(Integer i = 1; i <= to; i++){
            // Integer 型を BigInteger 型に変換するには,
            //     BigInteger.valueOf(value);
            BigInteger value = BigInteger.valueOf(i);
            // BigIntegerのかけ算は * ではダメ.
            // multiply メソッドを呼び出さないといけない.
            answer = answer.multiply(value);
        }
        System.out.printf("%d! = %s%n", to, answer);
    }
    public static void main(String[] args) {
        BigFactorial app = new BigFactorial();
        app.run(args);
    }
}

ユーザ定義の型2(複素数型)

複素数型の定義

ユーザ定義の型である複素数型Complexを作成しましょう. フィールドには,Double型のrealimagを定義してください. また,printメソッドでComplexを出力するようにしましょう.

public class Complex{
    Double real;
    Double imag;
    void print(){
        System.out.printf("%5.2f + %5.2f i",
            this.real, this.imag);
    }
    void println(){
        this.print();
        System.out.println();
    }
    public String toString(){
        return String.format("%5.2f + %5.2f i", this.real, this.imag);
    }
}

上記のように,独自の型にはフィールドとメソッドを同時に定義できます.

なお,toStringメソッドはその実体を文字列に変換するときに自動的に呼び出されます. 標準的な Java の実体を拡張しているため,メソッドのシグネチャは必ず public String toString() である必要があります. もし,publicをつけ忘れるとコンパイルエラーになります.toString内で 利用しているString.formatメソッドは C 言語の sprintf と同じ機能を持つものです. 書式付きフォーマッタで文字列を作成します.

Complex complex = // ... 何か値を作成する.
System.out.printf("%s%n", complex); // このようなとき,自動的に toString
System.out.println(complex);        // が呼び出されcomplex の文字列表現が出力される

複素数型を利用するプログラムの作成

次に,Complexを利用するプログラム ComplexCalculator を作成しましょう.

public class ComplexCalculator{
    void run(){
        Complex c1 = this.createComplex(3.0, 4.0);
        c1.println();
    }
    Complex createComplex(Double realValue,
              Double imagValue){
        Complex c = new Complex();
        c.real = realValue;
        c.imag = imagValue;
        return c;
    }
    // mainメソッドは省略.
}

上記のrunメソッド内で,Complex型の printメソッドを呼び出しています. このように,メソッドを呼び出すにも,変数を経由して呼び出す必要があります. 一方,変数(フィールド)も変数を経由してアクセスします.createComplexメソッドをみてください. このように,Complex型の変数 c を経由してフィールドにアクセスしています. このように,メソッドもフィールドも,必ず変数を経由してアクセスする必要があります.

例題 absolute

さて,Complex型に Double型を返す absoluteメソッドを追加してください.引数はありません. 複素数の絶対値を返すメソッドです. $z=a+bi$の絶対値$r$は,$r = |z| = \sqrt{a^2 + b^2}$で求められます.

上記が完成すれば,ComplexCalculatorabsolute を呼び出し,結果を出力してください.

public class Complex{
    Double real;
    Double imag;

    void print(){
        System.out.printf("%5.2f + %5.2f i",
            this.real, this.imag);
    }
    void println(){
        this.print();
        System.out.println();
    }
    public String toString(){
        return String.format("%5.2f + %5.2f i", this.real, this.imag);
    }
    Double absolute(){
        return Math.sqrt(this.real * this.real + this.imag * this.imag);
    }
}
public class ComplexCalculator{
    void run(){
        Complex c1 = this.createComplex(5.0, -6.0);
        Complex c2 = this.createComplex(3.0, 2.0);

        System.out.printf("absoluate(%s) = %f%n", c1, c1.absolute());
    }
    // createComplex は省略.
    // mainメソッドは省略.
}

例題 conjugate

次に,Complex型に Complex型の実体を返す conjugateメソッドを追加してください. 引数はありません.conjugateは共役複素数を意味します. 共役複素数とは,虚部の符号を逆転させたものであり,$z=a+bi$ の共役複素数は,$\bar{z}=a-bi$です. conjugateメソッド内で新たにComplex型の実体を作成し, 呼び出されたメソッドが持つ複素数の共役複素数として変数に値を代入し,返してください (thisの値を元に,新たな Complex型の変数を作成し,thisの共役複素数となるような値を代入して返してください).

上記が完成すれば,ComplexCalculatorconjugate を呼び出し,結果を出力してください.

public class Complex{
    Double real;
    Double imag;

    void print(){
        System.out.printf("%5.2f + %5.2f i",
            this.real, this.imag);
    }
    void println(){
        this.print();
        System.out.println();
    }
    public String toString(){
        return String.format("%5.2f + %5.2f i", this.real, this.imag);
    }
    Double absolute(){
        return Math.sqrt(this.real * this.real + this.imag * this.imag);
    }
    Complex conjugate(){
        Complex complex = new Complex();
        complex.real = this.real;
        complex.imag = this.imag * -1;
        return complex;
    }
}
public class ComplexCalculator{
    void run(){
        Complex c1 = this.createComplex(5.0, -6.0);
        Complex c2 = this.createComplex(3.0, 2.0);

        System.out.printf("absoluate(%s) = %f%n", c1, c1.absolute());
        System.out.printf("conjugate(%s) = %s%n", c1, c1.conjugate());
    }
    // createComplex は省略.
    // mainメソッドは省略.
}

練習問題

1. 線形合同法による擬似乱数列

線形合同法(Linear Congruential Generators)を用いて,0〜1の範囲の乱数をコマンドライン引数で指定された数だけ求めてください. コマンドライン引数で何も指定されなかった場合は,10が指定されたものとしてください. クラス名は LinearCongruentialGenerator としてください.

このプログラムも,素数の一覧 と同じように ArrayList<Double>を返すメソッドを作成し, 返されたArrayList<Double>の実体をそのままSystem.out.printlnに渡してください.

線形合同法は,擬似乱数を発生させるアルゴリズムです.以下の漸化式で求めます.

$$X_{n+1}=(A\times X_n + B) \mod M$$

$A$,$B$,$M$は定数です. $A$は自分の誕生日(月日.3桁もしくは4桁),$B$は1, $M$は65535,$X_0$は自分の年齢としてください. 完成すれば,$A$,$B$,$M$,$X_0$の値を変更して結果がどのように変わるかを確認しましょう. ただし,$A

この問題は,必ずしも再帰呼び出しで作成する必要はありません.

なお,C言語の rand 関数は,この線形合同法を用いて計算されています.

また,ここで作成した乱数は安全ではありません(未来の出力が容易に予測できます). そのため,自分自身で作成するプログラムに,このようなアルゴリズムで作成した乱数は使わないようにしてください. 少なくともライブラリとして用意されている乱数を利用してください.

出力例

各自の出力結果は,以下のものと異なる値になります.

$ java LinearCongruentialGenerator
[0.019073486328125, 0.308502197265625, 0.329376220703125, 0.855133056640625, 0.471710205078125, 0.077545166015625, 0.383575439453125, 0.413238525390625, 0.002471923828125, 0.299713134765625]
$ java LinearCongruentialGenerator 2
[0.019073486328125, 0.308502197265625]
$ java LinearCongruentialGenerator 5
[0.019073486328125, 0.308502197265625, 0.329376220703125, 0.855133056640625, 0.471710205078125]

ヒント

乱数値はDouble型で求めますが, $X_{n+1}$ の計算式はInteger型で計算する必要があります. つまり,結果を保存するとき, $X_{n+1}$ を $M$で割り,Integer型からDouble型に変換する必要があります.

ArrayList<Double> random(Integer max){
  ArrayList<Double> results = // 結果を格納するリストを作成する.
  Integer a, xn, b, m;
  xn = 20; // X0(自分の年齢)
  // a, b, m にも初期値を代入する.
  // 以下の2行を指定回数繰り返す.
      xn = // 線形合同法の計算式に従い,X_n+1 を求める.
      results.add(1.0 * xn / m);
      // xnを0.0〜1.0の範囲に変換してリストに追加する.
  return results;
}

2. ニュートン法による立方根の計算

ニュートン法による平方根の計算を参考に立方根を求めてください. クラス名は,CubicRootとします.

出力例

$ java CubicRoot 2 3 4 5 6 8 27
cubic_root(2.000000) = 1.259921
cubic_root(3.000000) = 1.442250
cubic_root(4.000000) = 1.587401
cubic_root(5.000000) = 1.709976
cubic_root(6.000000) = 1.817121
cubic_root(8.000000) = 2.000000
cubic_root(27.000000) = 3.000000

3. Fibonacci数列(任意桁)

第1講の練習問題 5. Fibonacci数列を改良し,桁あふれを起こさないようにしてください. 再帰呼び出しではなく,単純な繰り返しでFibonacci数列の$n$項目を求めてください(単純な再帰呼び出しにすると,非常に遅くなるため). クラス名はBigFibonacciとしてください. Fibonacci数列を Integer型で扱うと,第47項目の計算で桁あふれを起こします.

コマンドライン引数に値が指定されない場合は,10項目が指定されたものとしてください. コマンドライン引数に複数個の数値が与えられた場合,全ての数値に対して結果を出力してください.

出力例

$ java BigFibonacci
fibonacci(10) = 55
$ java BigFibonacci 10 30 46 47 48 49 50
fibonacci(10) = 55
fibonacci(30) = 832040
fibonacci(46) = 1836311903
fibonacci(47) = 2971215073
fibonacci(48) = 4807526976
fibonacci(49) = 7778742049
fibonacci(50) = 12586269025

4. Complexの四則演算

Complex 型に四則演算を行うメソッドを追加してください.

  • Complex同士の足し算(add
    • $(a + bi) + (c + di) = (a + c) + (b + d)i$
  • Complex同士の引き算(subtract
    • $(a + bi) - (c + di) = (a - c) + (b - d)i$
  • Complex同士の掛け算(multiply
    • $(a + bi)(c + di) = (ac - bd) + (ad + bc)i$
  • Complex同士の割り算(divide
    • $(a + bi) / (c + di) = \frac{(a + bi)(c - di)}{(c + di)(c - di)} = \frac{(ac + bd) + (bc - ad)i}{c^2 + d^2}$
  • 上記4つのメソッドは1つのComplex型の値を受け取り,新たな Complex 型の実体を返します.
    • 引数で受け取った Complex型の値は変更せずに,新たな Complex型の実体を作成してください.
  • ComplexCalculatorrunメソッドに次の処理を追加してください.
    • 2つのComplex型の実体を作成してください.
      • 値はプログラム中で適当に指定してください.
    • 上記の4つのメソッドを呼び出し,四則演算の結果を出力してください.

ヒント

public class Complex{
    // ...
    Complex add(Complex value){
        // this + value の結果を返す.
    }
    Complex subtract(Complex value){
        // this - value の結果を返す.
    }
    Complex multiply(Complex value){
        // this * value の結果を返す.
    }
    Complex divide(Complex value){
        // this / value の結果を返す.
    }
}

それぞれのメソッドの中では,thisvalue も値を変更せず,新たな実体を作成し,その実体に適切な値を設定して返して(return)ください. 例題 conjugate が参考になるでしょう.

出力例

$ java ComplexCalculator
absoluate( 5.00 + -6.00 i) = 7.810250
conjugate( 5.00 + -6.00 i) =  5.00 +  6.00 i
 5.00 + -6.00 i +  3.00 +  2.00 i =  8.00 + -4.00 i
 5.00 + -6.00 i -  3.00 +  2.00 i =  2.00 + -8.00 i
 5.00 + -6.00 i *  3.00 +  2.00 i = 27.00 + -8.00 i
 5.00 + -6.00 i /  3.00 +  2.00 i =  0.23 + -2.15 i

まとめ

まとめ

第5講 数値計算(2/2)のサブセクション

再帰呼び出し(Recursive call)

再帰呼び出しとは

再帰呼び出しとは,あるメソッド中でそのメソッド自身を呼び出しているメソッドを,再帰呼び出しメソッドと呼びます. 再帰呼び出しを使うと問題が簡単に解ける場合もあります.

例えば,次のようなメソッドのことを再帰呼び出しメソッドと呼びます.

Integer recursive(Integer value){
    if(value == 1){
        return 1;
    }
    return recursive(value - 1) + value;
}
このプログラムは,${\rm sum}(n)=\sum_{i=1}^{n} i$を計算するメソッドです. 漸化式と馴染みの深いプログラミングテクニックです. なお,この式は漸化式では次の通りになります. \[ {\rm sum}(n) = \begin{cases} 1 & n=1\\\\ n + {\rm sum}(n-1) & n> 1 \end{cases} \]

$n$value)が 1 の時は 1 を返し, それ以外の場合は, $n + {\rm sum}(n - 1)$recursive(value - 1) + value)を返します.

例題1 階乗改2

第1回目の練習問題 3. 階乗を別の書き方をしてみましょう. $n$の階乗は,$n!=n×(n−1)×(n−2)…2×1$ で求められます. この式を漸化式で表すと次の通りです.

\[ n! = \begin{cases} 1 & n=1\\ n \times (n-1)! & n> 1 \end{cases} \]

Javaプログラムでこの漸化式を表してみましょう.

public class Factorial3 {
    void run(String[] args) {
        for(Integer i = 0; i < args.length; i++) {
            Integer number = Integer.valueOf(args[i]);
            this.print(number, this.factorial(number));
        }
    }
    Integer factorial(Integer number) {
        if(number == 1) {
            return 1;
        }
        return number * this.factorial(number - 1);
    }
    void print(Integer number, Integer result) {
        System.out.printf("%d! = %d%n", number, result);
    }
    // mainメソッドは省略.
}

上記プログラムのfactorialメソッドに注目してください. 引数のnumberの値が1の場合は,返り値として1を返します. そして,numberの値が1以外の場合は,引数として与える数を変更して,factorialメソッドを再度呼び出しています. あるメソッドから,自分自身を呼び出すことを再帰呼び出しと呼びます. 上に示した漸化式で考えると理解が容易でしょう.

再帰呼び出しの実行例

また,動作を確認するために,上記プログラムのfactorial メソッドの先頭にSystem.out.printf("factorial(%d)%n", number); という一文を入れて実行してみても良いでしょう.

実行例

$ java Factorial2 8
8! = 40320
$ java Factorial2 2 5 6 
2! = 2
5! = 120
6! = 720

例題2 最大公約数

次に,2つの整数値の最大公約数(Greatest Common Divisor)をユークリッド互除法(Euclidean Algorithm)により求めてみましょう. ユークリッド互除法は次の漸化式で求められます. ただし, $x \ {\rm mod} \ y$は $x$を$y$で割った余りx % y)を表します.

\\[ {\rm gcd}(x, y) = \begin{cases} x & y = 0のとき \\\\ {\rm gcd}(y, x\ {\rm mod}\ y) & y \neq 0 のとき \\end{cases} \\]
public class Gcd {
    void run(String[] args) {
        if(args.length <= 1) {
            System.out.println("java Gcd <number> <numbers...>");
            return;
        }
        ArrayList<Integer> values = convert(args);
        Integer gcdValue = values.get(0);
        for(Integer i = 1; i < values.size(); i++){
            gcdValue = // gcd を呼び出してください.
        }
        System.out.printf("gcd(%s) = %d%n", String.join(", ", args), gcdValue);
    }
    Integer gcd(Integer x, Integer y) {
        if(/* y が 0のとき */) {
            return x;
        }
        // gcd(y, x mod y) を呼び出す
    }
    ArrayList<Integer> convert(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        for(Integer i = 0; i < args.length; i++){
            list.add(Integer.valueOf(args[i]));
        }
        return list;
    }
    // mainメソッドは省略
}
import java.util.ArrayList;

public class Gcd {
    void run(String[] args) {
        if(args.length <= 1) {
            System.out.println("java Gcd <number> <numbers...>");
            return;
        }
        ArrayList<Integer> values = convert(args);
        Integer gcdValue = values.get(0);
        for(Integer i = 1; i < values.size(); i++){
            gcdValue = gcd(gcdValue, values.get(i));
        }
        System.out.printf("gcd(%s) = %d%n", String.join(", ", args), gcdValue);
    }
    Integer gcd(Integer x, Integer y) {
        if(y == 0) {
            return x;
        }
        return gcd(y, x % y);
    }
    ArrayList<Integer> convert(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        for(Integer i = 0; i < args.length; i++){
            list.add(Integer.valueOf(args[i]));
        }
        return list;
    }
    public static void main(String[] args){
        Gcd gcd = new Gcd();
        gcd.run(args);
    }
}

実行例

$ java Gcd 9 18
gcd(9, 18) = 9
$ java Gcd 9 18 12 27
gcd(9, 18, 12, 27) = 3

再帰呼び出しの失敗

再帰呼び出しには,必ず再帰からの脱出口を設ける必要があります. 上の例で言えば,if(number == 1){ return 1; } が脱出口になっています. もし,脱出口が設けられていない,もしくは,設けられていても,適切な条件でなければ再帰から抜け出せず,StackOverflowExceptionが発生します.

練習問題

1. GrandTotal改

第0講 練習問題 3. 総和を求める再帰呼び出しを使って計算するプログラムを作成してください. クラス名は,GrandTotal2としてください. また,コマンドライン引数で最大値を与えられるようにしてください.

出力例

$ java GrandTotal2
1から10までの総和は55です.
$ java GrandTotal2 10
1から10までの総和は55です.
$ java GrandTotal2 100
1から100までの総和は5050です.
$ java GrandTotal2 90
1から90までの総和は4095です.

2. Fibonacci数列 改

第1講 練習問題 5. Fibonacci数列再帰呼び出しを使って計算してみましょう. Fibonacci数列の $n$項目の値を出力してください. クラス名はFibonacci2としてください.

コマンドライン引数に値が指定されない場合は,10項目が指定されたものとしてください. コマンドライン引数に複数個の数値が与えられた場合,全ての数値に対して結果を出力してください.

出力例

$ java Fibonacci2
fobonacci(10) = 55
$ java Fibonacci2 1 2
fibonacci(1) = 1
fibonacci(2) = 1
$ java Fibonacci2 4 10 20
fibonacci(4) = 3
fibonacci(10) = 55
fibonacci(20) = 6765
$ java Fibonacci2 40
fibonacci(40) = 102334155

3. 最小公倍数

コマンドライン引数で与えられた全ての数値の最小公倍数(Least Common Multiple)を求めるプログラムを作成してください. クラス名は Lcm としてください. なお,2つの数の最小公倍数は次の式で求められます.最大公約数(gcd)は例題の通りです.

\[ {\rm lcm}(a, b)=|ab|/{\rm gcd}(a, b) \]

出力例

$ java Lcm 3 6
lcm(3, 6) = 6
$ java Lcm 30 42
lcm(30, 42) = 210
$ java Lcm 3 6 9 18
lcm(3, 6, 9, 18)=18
$ java Lcm 1 2 3 4 5 6 7 
lcm(1, 2, 3, 4, 5, 6, 7)=420

ヒント

処理の流れ

与えられた全ての数($a_1, a_2, ..., a_n$)の最小公倍数を求めるには,次のように計算すると良いでしょう. つまり,最初の2つの数($a_1$と$a_2$)の最小公倍数を求め,$l_1$ とする. 次に,$l_1$と$a_3$の最小公倍数を求め,$l_2$とする. 続けて,順番に最小公倍数を求めていくと良いでしょう. \[ {\rm lcm}({\rm lcm}({\rm lcm}(a_1, a_2), a_3), ...) \]

文字列の結合

文字列を特定の文字で区切って結合するには,String.joinメソッドを使うと良いでしょう.

String[] array = // ... "1", "2", "3", "4"
String joined = String.join(", ", array); // "1, 2, 3, 4"という文字列が得られる

その他

4. 回文チェッカー

コマンドライン引数で与えられた文字列(複数指定可)が回文(palindrome)であるかどうかを確認するプログラムを作成してください. 回文とは,始めから読んだ場合と終わりから読んだ場合とで,文字の出現する順番が同じである文字列のことを指します (本来は意味の通じるように,という条件もありますが,プログラムでは判断するのは難しいので,意味については関知しないものとします).

クラス名は PalindromeChecker としてください.

出力例

$ java PalindromeChecker akasaka
akasaka: true
$ java PalindromeChecker ABBA madamimadam
ABBA: true
madamimadam: true # Madam, I'm Adam
$ java PalindromeChecker あかさか わにのにわ つつみがみっつ
あかさか: false
わにのにわ: true
つつみがみっつ: false

一般的な回文では,濁音,半濁音,促音,拗音は清音と同一視されます (つつみがみっつは回文として扱われる). しかし,処理がややこしくなりますので,上記の条件は無視して良いです (つつみがみっつは回文として扱わなくて良いです).

ヒント1: 文字列のn文字目を取得する.

文字列(String型)のn文字目を取得するには,charAtメソッドを用います. charAt メソッドは1つのInteger型の値を受け取り,Character型の変数を返します.

String string = "abracadabra";
Character c1 = string.charAt(0);
    // => 'a'(1文字目の'a')
Character c1 = string.charAt(string.length() - 1);
    // => 'a'最後の文字の'a'

ヒント2: 部分文字列を取得する

文字列(String型)の部分文字列を取得するには,substringメソッドを用います. substring メソッドは1つ,もしくは2つのInteger型を受け取ります.

String string = "abracadabra";
String sub1 = string.substring(2);
    // => "racadabra"
String sub2 = string.substring(2, string.length() - 2);
    // => "racadab"
String sub3 = string.substring(1, string.length() - 1);
    // => "bracadabr"

ヒント3: 考え方

次の考え方で回文であるかを判定できます.

まず,isPalindrome メソッドを定義しましょう. 引数は1つのString型を受け取り,Boolean型を返します.

  1. メソッドの最初で,文字列の長さが1文字以下ならば回文であると判定します.
  2. そうでないなら,受け取った文字列から最初の文字(first)と最後の文字(last)を取得します(文字列のn文字目を取得する).
  3. firstlast が一致しなければ,回文ではないと判定します.
  4. 最初と最後の文字を除いた部分文字列を取得します(部分文字列を取得する).
  5. 取得した部分文字列を引数にして,isPalindromeを呼び出し,その結果をメソッドの返り値とします.

以下も参照してください.

まとめ

まとめ

第6講 ファイル

この講で利用するプログラム

第6講 ファイルのサブセクション

ファイルを扱う型

File 型

Java でファイルを扱うときは File 型を利用します. 名前はFile型ですが,ファイルもディレクトリも同じ型として扱います.

なお,File型を利用するときは,java.io.File のインポートが必要です.File型の 実体を作成するにはファイル名,もしくはディレクトリ名を文字列(String型)で渡しましょう.

File dir1  = new File(".");
File dir2  = new File("../path/to/some/dir");
File file1 = new File("ファイル名");
File file2 = new File("../lesson01/BigAndSmall.java");

上記のように,カレントディレクトリを元に,パスの指定も可能です. 指定したファイルが存在しない場合でも問題なく実体を作成できます. ファイルが存在しているかは,ls コマンドに必要な File 型のメソッド にも載せているように File型変数に対して,existsメソッドを呼び出すことで確認できます.

例題 1. ls コマンドの作成

ここでは,lsコマンドに相当するプログラムを作成しましょう.lsコマンドは コマンドライン引数に渡された情報によって次のように挙動が変わります.

  • ファイル名が渡されると,そのファイルの名前が出力されます.
  • ディレクトリ名が渡されると,そのディレクトリ内部のファイル,ディレクトリの一覧が出力されます.
  • 何も渡されなかった場合,カレントディレクトリのファイル,ディレクトリの一覧が出力されます.
  • 指定されたファイル,もしくはディレクトリが存在しない場合,その旨を出力する.

ls コマンドの出力例

$ java ListFiles # 何も指定されない場合,現在ディレクトリを表示する.
Complex.java            Factorial2.java
Fibonacci4.java         SquareRoot.java
ComplexCalculator.java  Fibonacci2.java
GrandTotal2.java        CubicRoot.java
Fibonacci3.java         Matrix.java
$ ls Complex.java
Complex.java
$ java ListFiles .. # ディレクトリを指定した場合,中身を表示する.
lesson01/     lesson02/     lesson03/     lesson04/
lesson05/     lesson06/     lesson07/
$ java ListFiles ../lesson01
Adder.java             EvenPrinter.java
LeapYear.java          OddPrinter2.java
BackSlashPrinter.java  EvenPrinter2.java
Multiplication.java    PositiveChecker.java
BigAndSmall.java       GrandTotal.java
OddPrinter.java        XPrinter.java
$ java ListFiles ../lesson00 # 存在しない場合
ls: ../lesson00: No such file or directory

表示の順序,改行の位置は必ずしも同じでなくても構いません.

ls コマンドに必要な File 型のメソッド

では,lsコマンドを作成していきましょう.lsコマンドを 作成するために,File型の次の3つ(4つ)メソッドが利用できます.File型の 変数 file に対してメソッドを呼び出すと思ってください.

  • ファイル,ディレクトリの名前を取得する.
    • file.getName()
      • ファイル,もしくはディレクトリ名がString型として返る.
  • ファイルとディレクトリを区別する.
    • file.isDirectory()
      • ディレクトリならば,Boolean型の trueが返る.
      • ディレクトリでないなら(ファイルであれば),Boolean型の falseが返る.
    • file.isFile()
      • 一般ファイルならば,Boolean型の trueが返る.
      • 一般ファイルでないなら(ディレクトリであれば),Boolean型の falseが返る.
      • isDirectoryの判定と,isFileの判定のどちらかを利用すれば良いでしょう.
  • ファイルとディレクトリの存在を確認する.
    • file.exists()
      • ファイル,もしくはディレクトリが存在すれば,Boolean型のtrueが返る.存在しなければfalseが返る.
  • ディレクトリ内のファイル,ディレクトリの一覧を取得する.
    • file.listFiles()
      • File型の配列(File[])が返る.
      • file.isDirectory()trueを返さない場合,listFilesメソッドはnullを返します.

ls コマンドの作成

さて,ls コマンドに必要な File 型のメソッドにある 4つのメソッドを利用して,lsコマンドを作成しましょう. 次のコードを元に作成してください.以下のコードの....の部分に適切な命令を書きましょう.

// import文を書く.
....
public class ListFiles{
  void run(String[] args){
    for(String arg: args){
      File thisFile = // ....
          // argを元に,File型の変数を作成する.
      this.listFile(thisFile);
    }
    if(args.length == 0){
      File currentDirectory = // ...
        // カレントディレクトリ(".")を表す File 型の実体を作成する.
      this.listFile(currentDirectory);
    }
  }
  void listFile(File target){
    if(this.isExist(target)){
      if(....){ // 引数に与えられた target がディレクトリの場合
        this.listFilesInDirectory(target);
      }
      else{
        System.out.printf("%s%n", ....);
            // 引数のファイルの名前を出力する.
      }
    }
  }
  void listFilesInDirectory(File dir){
    // 引数に受け取ったディレクトリの中身一覧を取得する.
    File[] files = ....
    // for文で files を繰り返す.
    for(....){
      // 配列の各要素であるファイルの名前を出力する.
    }
  }
  Boolean isExist(File target){
    Boolean exists = // .....
        // target が指し示すファイルが存在するかを確認する.
        // File型の existsメソッドを利用する.
    if(!exists){ // ファイルが存在しない場合
      // 指定されたファイル名は存在しない旨を出力する.
      System.out.printf("ListFiles: %s: No such file or directory%n",
          target.getName());
    }
    return exists;
  }
  // mainメソッドは省略
}
// import文を書く.
import java.io.File;

public class ListFiles{
    void run(String[] args){
        for(String arg: args){
            File thisFile = new File(arg); // argを元に,File型の変数を作成する.
            this.listFile(thisFile);
        }
        if(args.length == 0){
            this.listFile(new File("."));
        }
    }
    void listFile(File target){
        if(this.isExist(target)){
            if(target.isDirectory()){ // 引数のファイルがディレクトリの場合
                this.listFilesInDirectory(target);
            }
            else{
                System.out.printf("%s%n", target.getName()); // 引数のファイルの名前を出力する.
            }
        }
    }
    void listFilesInDirectory(File dir){
        // 引数に受け取ったディレクトリが持つファイル,ディレクトリの一覧を取得する.
        File[] files = dir.listFiles();
        // for文で files を繰り返す.
        for(File file: files){
            System.out.printf("%s%n", file.getName());
            // 配列の各要素であるファイルの名前を出力する.
        }
    }
    Boolean isExist(File target){
        if(!target.exists()){ // ファイルが存在しない場合
            // 指定されたファイル名は存在しない旨を出力する.
            System.out.printf("ListFiles: %s: No such file or directory%n", target.getName());
        }
        return target.exists(); // ファイルが存在するかを返す(if文の条件の反転).
    }
    public static void main(String[] args){
        ListFiles ls = new ListFiles();
        ls.run(args);
    }
}

例題 2. File の情報を出力するプログラム.

概要

ファイルの情報を出力するプログラムを作成しましょう. コマンドライン引数で指定されたファイルの次の情報を取得して出力してください.

  • ファイルの権限
    • ls -lで表示される rwx のこと.
      • rwxはそれぞれ,読み込み権限,書き込み権限,実行権限を表します.権限があれば文字が表示され,権限がなければ-で表されます.
      • r--であれば,内容は読めるが,書き込み,実行が不可能であることを表します.
  • 絶対パス
  • 相対パス(コマンドラインで指定されたパス)
  • ファイル名(ディレクトリ名を含まない名前)
  • 隠しファイルか否か.
  • 最終更新日
  • ファイルの長さ.

情報取得のために必要なメソッド.

以下のメソッドで必要な情報が取得できます.これらのメソッドも全て,File型が持ちます. 以下の説明では,ls コマンドに必要な File 型のメソッド と同じく,File型の変数 file に対してメソッドを呼び出すと思ってください.

  • ファイルの権限を取得する.
    • file.canRead(): ファイルが存在し,読み込み権限があればBoolean型のtrueを返します.
    • file.canWrite(): ファイルが存在し,書き込み権限があればBoolean型のtrueを返します.
    • file.canExecute(): ファイルが存在し,実行権限があればBoolean型のtrueを返します.
  • 絶対パス
    • file.getAbsolutePath(): この実体が表すファイルの絶対パスをString型で返します.
  • 相対パス
    • file.getPath(): この実態が表すファイルのパスをString型で返します.実体を作成するときに渡した文字列がそのまま返されます.
  • ファイル名
  • 隠しファイルか否か
    • file.isHidden(): ファイルが隠しファイルであれば,Boolean型の trueを返します. macOS であれば,.から始まるファイルは隠しファイルです.
  • 最終更新日
    • file.lastModified(): 最終更新日をLong型で返します. 日付(Date)型に変換するには,Date date = new Date(file.lastModified())としてください.
  • ファイルの長さ
    • file.length(): ファイルの長さをLong型で返します.

File の情報を出力するプログラムの作成.

// import文を書く.
....
public class FileInfo{
  void run(String[] args){
    for(String arg: args){
      File thisFile = .... // argを元に,File型の変数を作成する.
      this.showFileInfo(thisFile);
    }
  }
  void showFileInfo(File target){
    if(....){ // ファイルが存在する場合.
      this.showInfo(target);
    }
    else{
      // 指定されたファイル名は存在しない旨を出力する.
      System.out.printf("FileInfo: %s: No such file or directory%n", ....);
    }
  }
  void showInfo(File target){
    System.out.printf(
      "%s %6d %s %s (%s) %s%n",
      getMode(target),
      ...., // ファイルの長さを指定する.
      ...., // ファイルの最終更新日を Date 型で指定する.
      ...., // ファイルの相対パスを指定する.
      ...., // ファイルの絶対パスを指定する.
      getHidden(target)
    );
  }
  String getHidden(File file){
    if(....){
      return "隠しファイル";
    }
    return "";
  }
  String getMode(File file){
    String rwx = "";
    // 読み込み権限があるか確認する.
    if(....) rwx = rwx + "r";
    else     rwx = rwx + "-";

    // 書き込み権限があるか確認する.
    if(....) rwx = rwx + "w";
    else     rwx = rwx + "-";

    // 実行権限があるか確認する.
    if(....) rwx = rwx + "x";
    else     rwx = rwx + "-";

    return rwx;
  }
  public static void main(String[] args){
    FileInfo info = new FileInfo();
    info.run(args);
  }
}

実行例

$ java FileInfo FileInfo.java
rw-   1617 Fri May 12 15:43:20 JST 2017 FileInfo.java (/Users/tamada/ksuap/lesson07/FileInfo.java)
$ java FileInfo ListFiles.java ~/.bashrc /usr/bin/java
rw-   1621 Fri May 12 15:32:17 JST 2017 ListFiles.java (/Users/tamada/ksuap/lesson07/ListFiles.java)
rw-    578 Fri May 12 13:27:36 JST 2017 /Users/tamada/.bashrc (/Users/tamada/.bashrc) 隠しファイル
r-x  58560 Wed Sep 14 09:55:30 JST 2017 /usr/bin/java (/usr/bin/java)
// import文を書く.
import java.io.File;
import java.util.Date;

public class FileInfo{
    void run(String[] args){
        for(String arg: args){
            File thisFile = new File(arg); // argを元に,File型の変数を作成する.
            this.showFileInfo(thisFile);
        }
    }
    void showFileInfo(File target){
        if(target.exists()){ // ファイルが存在する場合.
            this.showInfo(target);
        }
        else{
            // 指定されたファイル名は存在しない旨を出力する.
            System.out.printf("FileInfo: %s: No such file or directory%n", target.getName());
        }
    }
    void showInfo(File target){
        System.out.printf(
            "%s %d %s %s (%s) %s%n",
            getMode(target),
            target.length(), // ファイルの長さを指定する.
            new Date(target.lastModified()), // ファイルの最終更新日を Date 型で指定する.
            target.getPath(), // ファイルの相対パスを指定する.
            target.getAbsolutePath(), // ファイルの絶対パスを指定する.
            getHidden(target)
        );
    }
    String getHidden(File file){
        if(file.isHidden()){
            return "隠しファイル";
        }
        return "";
    }
    String getMode(File file){
        String rwx = "";
        // 読み込み権限があるか確認する.
        if(file.canRead())    rwx = rwx + "r";
        else                  rwx = rwx + "-";

        // 書き込み権限があるか確認する.
        if(file.canWrite())   rwx = rwx + "w";
        else                  rwx = rwx + "-";

        // 実行権限があるか確認する.
        if(file.canExecute()) rwx = rwx + "x";
        else                  rwx = rwx + "-";

        return rwx;
    }
    public static void main(String[] args){
        FileInfo info = new FileInfo();
        info.run(args);
    }
}

例題 3. tree コマンド

概要

次に,treeコマンドのように,ディレクトリのツリー構造を表示しましょう. クラス名は,TreeViewer としてください.

ヒント

  • ディレクトリの数とファイルの数を数える必要がありますので,フィールドにディレクトリ数,ファイル数を格納するInteger型の変数を宣言しましょう.
  • traverseメソッドを宣言しましょう.
    • File型の変数とインデントレベルを表す数値(Integer型)を引数として受け取ってください.
    • 返り値はありません(voidです).
    • インデントレベルに応じた空白を出力し,引数のファイルの名前(パスは含みません)を出力しましょう.
    • 引数で受け取ったFile型変数がファイルであった場合,ファイル数をインクリメントしてメソッドを終了してください (returnしてください).
    • 引数で受け取ったFile型変数がディレクトリであった場合,次の処理を行なってください.
      • ディレクトリ数をインクリメントしてください.
      • ディレクトリの中身を取得してください(listFiles).
      • 受け取ったFile型の配列の各要素に対して,traverseメソッドを呼び出してください.
        • その際,引数で受け取ったインデントレベルに +1 して呼び出してください.
        • 再帰呼び出しを行なっています.
      • ループが終了すれば,traverseメソッドも終了です.
  • runメソッドは次の処理を行なってください.
    • File型の変数を宣言してください.
    • コマンドライン引数の 0 番目の要素をFile型に変換してください.
    • 変換したFile型の実体を先ほど宣言したFile型変数に代入してください.
    • traverseメソッドを呼び出してください.
      • 第1引数は先ほど作成した File型の変数,インデントレベルは 0 としてください.
    • traverseメソッドの呼び出し後,ディレクトリ数,ファイル数を出力してください.

実行例

$ java TreeViewer prog
prog/
    lesson01
        Adder.java
        ... 途中省略
        XPrinter.java
    lesson02
        DateExample.class
        ... 途中省略
        StringBuilderExample.java
    lesson03
        ArgsSorter.class
        ... 途中省略
        TrapezoidalRulePi.java
    lesson04
        ArgsPrinter2.class
        ... 途中省略
        StatsValues.java
    lesson05
        Complex.java
        ... 途中省略
        SquareRoot.java
    lesson06
        Bound.class
        ... 途中省略
        ThrowingExercise2.java
    lesson07
        FileInfo.class
        ... 途中省略
        ListFiles.java

8 directories, 98 files

このようにディレクトリが存在しなくなるまでディレクトリを掘り進んでください. コマンドライン引数で渡す文字列は1つで良いです(複数の引数に対応する必要はありません).

import java.io.File;

public class TreeViewer{
    Integer directoryCount = 0;
    Integer fileCount = 0;
    void run(String[] args){
        traverse(new File(args[0]), 0);
        System.out.printf("%d directories, and %d files%n", directoryCount, fileCount);
    }
    void traverse(File file, Integer indent){
        for(Integer i = 0; i < indent; i++) System.out.print("    ");
        System.out.printf("%s%n", file.getName());
        if(file.isDirectory()){ // ディレクトリであれば,中身を確認する.
            File[] files = file.listFiles();
            Integer newIndent = indent + 1;
            for(File f: files){
                this.traverse(f, newIndent);
            }
            directoryCount++;
        }
        else{
            fileCount++;
        }
    }
    public static void main(String[] args){
        TreeViewer tree = new TreeViewer();
        tree.run(args);
    }
}

練習問題

1. ファイルを探すコマンド FileFinder

指定されたディレクトリ以下の特定の名前をもつファイルが存在するかを探索するプログラムを作成してください. 以下のように指定してください.

java FileFinder ファイル名 探索ディレクトリ

例題 3が参考になるでしょう.探索には,TreeViewertraverseと同じように 再帰呼び出しを行いましょう. ファイル名の一致を確認するには,値の一致性を確認しましょう

見つかった場合に,全てのパスを出力し,見つからなかったら,その旨を出力するようにしましょう. 見つかったら,とりあえず,結果を入れるListにパス(File型変数)を追加しましょう. そして,最後に Listの大きさを確認し,見つかったか,見つからなかったかを判断しましょう.

実行例

$ java FileFinder TreeViewer.java prog
prog/lesson07/TreeViewer.java
$ java FileFinder TreeViewer.java ../
../prog/lesson07/TreeViewer.java
../2019autumn/prog/07/TreeViewer.java
$ java FileFinder TreeViewer.notfound prog
TreeViewer.notfound: Not found.

2. ディレクトリを作成するコマンド mkdir

ディレクトリを作成するコマンド MakeDirectory を作成しましょう. 作成したいディレクトリのパスを持つFile型変数を作成し,その変数に対して,mkdir メソッドを呼び出すとディレクトリを作成できます.

実行例

$ java ListFiles ..
lesson01 lesson02 lesson03 lesson04 lesson05 lesson06 lesson07
$ java MakeDirectory ../lesson00
$ java ListFiles ..
lesson00 lesson01 lesson02 lesson03 lesson04 lesson05 lesson06 lesson07
$ java ListFiles ../lesson00
$ java MakeDirectory ../lesson00/not/exist/parent/dir
../lesson00/not/exist/parent/dir: could not make directory.

3. ディレクトリを作成するコマンド mkdir の改良

先ほどの MakeDirectory は途中のディレクトリが存在しないとき, ディレクトリの作成に失敗しました.そのような場合でもディレクトリが作成できるようにしましょう.

ディレクトリを作成するコマンド mkdirでは,mkdirメソッドを利用しました. ここでは,mkdirメソッドの代わりに,mkdirsメソッドを利用してください. これで,途中のディレクトリが存在しない場合でも作成してくれます.

クラス名を MakeDirectories としてください. なお,コマンドライン引数で複数の値を受け取れるようにしてください.

実行例

$ java MakeDirectories a/b/c/d/e/f
$ java ListFiles a
b

4. ファイル,ディレクトリを削除するコマンド remove

ファイル,ディレクトリを削除するコマンド Remover を作成してください. コマンドライン引数で受け取った複数のパスのファイルを削除するコマンドです. ディレクトリ内にファイルがあったとしても,それらも全て削除してください.

ただし,必要なファイルを削除しないように気をつけてください.

削除は,File型の変数に対して,deleteメソッドを呼び出してください. ただし,ディレクトリ内が空でなければ,deleteメソッドは失敗します. ディレクトリが空でない場合は,そのディレクトリ内部のファイル,ディレクトリを削除してから削除するようにしましょう. 再帰呼び出しを用いると実現できるでしょう.

実行例

$ java ListFiles a/b/c/d/e
f
$ java Remover a/b/c/d/e/f
$ java ListFiles a/b/c/d/e
$ java Remover a
$ java ListFiles a
ls: a: No such file or directory.

まとめ

まとめ

  • ファイルを扱うには,File 型を利用する
    • File 型を利用するには,import java.io.Fileが必要
    • ファイル,ディレクトリを同じ型で扱う.
    • File型が持つ各種メソッド
      • ls コマンドのために必要なメソッド情報取得のためのメソッド練習問題 2, 練習問題 3, 練習問題 4からのまとめ.
      • 全て,File型の変数 fileに対して呼び出すものとする.
      • ファイル,ディレクトリの名前を取得する: file.getName()
      • ファイル,ディレクトリを区別する: file.isDirectory()file.isFile()
      • ファイル,ディレクトリの存在を確認する: file.exists()
      • ディレクトリ内のファイル,ディレクトリ一覧を取得する: file.listFiles()
      • ファイルの権限を確認する: file.canRad()file.canWrite()file.canExecute()
      • ファイルの絶対パスを取得する: file.getAbsolutePath()
      • ファイルの相対パスを取得する; file.getPath()
      • 隠しファイルか否かを判定する: file.isHidden()
      • ファイルの長さを取得する: file.length()
      • ファイルの最終更新日時を取得する: file.lastModified()
        • Long型で返される.Date型として扱うには,new Date(file.lastModified())とする.
      • ディレクトリを作成する: file.mkdir()
        • file変数が表すパスのディレクトリを作成します.作成に成功すれば,trueを返し,そうでなければ falseを返します.
      • ディレクトリを作成する 2: file.mkdirs()
        • file変数が表すパスのディレクトリを作成します.存在していないものの,必要なディレクトリがあれば,一緒に作成されます.必要な全てのディレクトリが作成された場合は,trueを返し,そうでなければfalseを返します.
      • ファイル,ディレクトリを削除する: file.delete()
        • file変数が表すパスがディレクトリで,かつディレクトリの中身が空でなければ falseを返し,削除に失敗します.

第7講 画像操作

この講で利用するプログラム

第7講 画像操作のサブセクション

画像の書き込み

画像生成

例題1 グラデーション画像

次のプログラムを作成して実行してみましょう.

import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;

import java.io.File;
import java.io.IOException;;

public class Gradiation {
    void run() throws IOException {
        // 横幅 255,高さ 255 の画像を生成する.
        BufferedImage image = new BufferedImage(255, 255, BufferedImage.TYPE_INT_RGB);
        for(Integer x = 0; x < 255; x++) {
            for(Integer y = 0; y < 255; y++) {
                image.setRGB(x, y, x);
            }
        }
        ImageIO.write(image, "png", new File("dest.png"));
    }
    public static void main(String[] args) throws IOException {
        Gradiation g = new Gradiation();
        g.run();
    }
}

グラデーション画像 グラデーション画像

実行結果は dest.png というファイルに png フォーマットで出力されます. 具体的な内容は上の折り畳みを開いて確認してください.

Java で画像を扱うには,BufferedImage という型を用います. この画像は,上記の10行目のように,BufferedImage の実体を作成しています. この結果,横幅255,高さ255の画像が生成されます. また,この画像の各画素(1つのピクセル)は int 型で表され,その値は RGB(Red, Green, Blue)を表すことを示しています.

13 行目の image.setRGB で指定されたピクセルの値を設定しています. setRGB の引数は前から順に x座標,y座標,ピクセルの値を表しています. 13 行目を image.setRGB(i, j, j)image.setRGB(i, j, i << 8 | j) のように変更して結果がどのように変わるか確認してみましょう.

画像の書き出し

runメソッドの最後の行(16行目)では,画像ファイルを dest.png というファイルに PNG フォーマットで出力しています. ImageIO(イメージアイオーと呼ぶ) は画像の入出力を扱うクラスです. 第2引数の "png""jpeg""gif" などに変えると指定されたフォーマットで出力されるようになります.

以上で画像の生成から出力まで行えるようになりましたが,入出力エラーに対応する必要があります. それが,8行目,17行目の runメソッド,mainメソッドの定義の後ろについている throws IOException というものです. もし,この throws IOException がなければコンパイラは次のメッセージを表示し,コンパイルに失敗します.

Gradiation.java:15: エラー: 例外IOExceptionは報告されません。スローするには、捕捉または宣言する必要があります
        ImageIO.write(image, "png", new File("dest.png"));
                     ^
エラー1個

サポートされている画像フォーマット

ImageIOのAPIドキュメント に記載されている通り,以下のフォーマットの読み書きがサポートされています.

  • BMP
  • GIF
  • JPEG
  • PNG
  • TIFF
  • WBMP

例外機構(Exception Architecture)

例外(Exception)は,近年のプログラミング言語で採用されている実行時エラーの通知機構です.

従来のプログラミング言語,例えば,C 言語の場合,fopenで開くファイルが見つからなかった場合, 返り値をNULL にすることでエラーを通知していました. この場合,プログラマが責任を持って, 返り値の値を確認して,正常か異常かを判断しなければいけませんでした.

一方の例外機構は,異常処理を行うための別の処理経路を作るものです. もし,プログラムの実行途中で何らかの異常が発生した時,それまでに行なっていた処理を中断し, 別の処理を行うようにする機構です.

graph LR;
A[ファイルを開く] --> B{存在確認}
B -->|存在する| C[正常処理]
B -->|存在しない| D[異常処理]

C 言語のようなエラー処理は上記のフローチャートのように, プログラマ自身がfopenの返り値を元に分岐処理によって,正常処理,異常処理を振り分ける必要があります.

graph LR;
A[ファイルを開く]
A --> C[正常処理]
A -->|存在しない| D[異常処理]

対して,例外機構がサポートされている言語の場合,プログラマが異常,正常の分岐を明示的に書く必要はありません. 異常が起こった場合の処理の経路が決まっており,その経路に処理を書いておくことが異常処理を行うことになります. 正常処理の場合は,それまでの処理の続きにそのまま処理を書いていきます. これにより正常処理の見通しがよくなります.

そして,例外が投げられれば,何らかの異常が発生したと判断できるようになります. 逆に,例外が投げられなければ,データが変であろうが,正常な処理であると判断できます (もちろん,開発途中で変な場合はバグの可能性はありますが).

プログラマが明示的に投げる例外も存在しますが,多くの場合,システムが異常を検知し例外を投げます. 例えば,ファイルが見つからない場合,スリープ中に割り込みが入った場合などです. そのような例外が発生した時に,どのような対応をするのかをあらかじめ決めておく必要があります.

次のプログラムが,例外処理のイメージです.

void exceptionalMethod() throws Exception {
  // 例外が発生する可能性のある処理
  someMethodCall();

  // 例外が起こらなかった場合の処理
  process();
}

someMethod の実行中に何らかの例外が起こった場合,まずsomeMethodの呼び出し元であるexceptionalMethodにどのように処理するかが問い合わされます. そして,exceptionalMethodは例外は処理せず,呼び出し元に処理を任せるよう設定している(メソッドにthrows Exceptionと宣言しているため)ため,exceptionalMethod の呼び出し元に通知されます. このthrows節は後ほど説明します.

検査例外と非検査例外

Java の例外は,検査例外と非検査例外の2つに分類できます. 違いは次に挙げる通りです.

検査例外

例外が発生した時の処理をプログラム中に明示的に書いておかなければコンパイルエラーになる例外. 完全に防ぐことが不可能な例外(実行時の状態やユーザの操作によって発生しうる例外).

例えば,実行時エラーは,プログラムでどれほど厳密にチェックしたとしても,完全に回避できません.

  • 代表的な検査例外
    • IOException
      • 入出力時にエラーが発生した時.
    • InterruptedException
      • 割り込みが発生した時に発生する例外.

非検査例外

一方,非検査例外はプログラム中で十分にチェックすることで避けることが可能です. そのため,例外が発生した時の処理は書かなくてもコンパイルが通るようになっています.

  • 代表的な非検査例外
    • NullPointerException
      • 初期化されていない変数に対する処理を行なった場合に発生する例外.
    • ArrayIndexOutOfBoundsException
      • 配列の範囲を超えてアクセスしようとした時に発生する例外.
    • NumberFormatException
      • 数値の変換に失敗した時に投げられる例外.

非検査例外は,事前にチェックすることで,例外の発生を抑えられます. 逆に言えば,実行時に非検査例外が投げられた場合は,事前のチェックが不十分であるとも言えます. 例えば,配列の範囲を超えてアクセスすることは,事前に配列の範囲を超えないようにプログラム中で確認することで避けられます.

例外の責任転嫁

さて,例外が発生した時の対処法をプログラム中に書いておく必要があります. 取れる対処法は2つです.

  • 例外が投げられたら,その場で例外に対応する.
  • 例外が発生する可能性のあるメソッドを呼び出している元に対応を任せる.

どちらがふさわしいかは場合により異なります. ここでは,呼び出し元に対応を任せましょう. そのために,呼び出し元に責任を丸投げするようにプログラム中に明示しておきましょう.

こうすることで,例外が発生した時はそのメソッドの呼び出し元に責任を転嫁し,正常処理のみに集中して処理を書くことができます.

情報

本講義では,例外に対応する処理については省略します. 詳細を知りたい場合は,try-catch について調べてみてください.

IOException の責任転嫁

さて,コンパイルエラーで,IOExceptionが投げられる可能性があると述べられています. この例外は,ファイル書き込みに何らかの問題があったときに発生する例外です. ここでは,呼び出し元に責任を転嫁しましょう. 以下のような対応になります.

  • ImageIO.write が例外を発生させた時,ImageIO.writeの呼び出し元であるrunに対応が任されます. これが例外が投げられた,ということです.
  • そこで,runの呼び出し元であるmainに対応を任せましょう.
  • さらに,mainも呼び出し元に対応を任せましょう.
  • すると,実行環境が対応されなかった例外をスタックトレースという形で出力し,プログラムが終了するようになります.

throws 節

呼び出し元に対応を任せるには,メソッドのシグネチャthrows節を追加します.

throws 節は以下のように指定します. 以下のように書くことで,例外クラスが発生した時,methodNameの呼び出し元に責任を転嫁することができるようになります.

public class ClassName{
  void methodName() throws 例外クラス {
    // 検査例外が発生する可能性のある処理
  }
}

なお,複数の例外が発生する可能性のある場合,throws節に例外クラスの名前をコンマ区切りで指定できます.

例題2. 画像の読み込み/書き込み

ImageIOは画像の書き込みだけではなく,readメソッドで読み込みも可能です. 以下のようなプログラムで画像のフォーマット変換が可能になります. args[0]から画像を読み出し,出力先(args[1])の拡張子からフォーマットを取り出し,画像を出力します.

import java.io.IOException;
import java.io.File;
import java.util.Objects;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;

public class ToJpegConverter {
    void run(String[] args) throws IOException {
        // 画像ファイルを読み込む.
        BufferedImage image = ImageIO.read(new File(args[0])); 
        String destName = findDestName(args[1])
        ImageIO.write(image, "jpg", new File(destName)); // 画像を書き出す.
    }
    String findDestName(String fileName) {
        // ファイル名から最後のドット(.)の位置を取得する.
        Integer index = fileName.lastIndexOf(".");
        // 取得した位置から後ろの文字列を取得する.拡張子に相当する.
        String extension = fileName.substring(index + 1).toLowerCase();
        // 拡張子が jpg,もしくは jpeg ならそのまま返す.
        if(Objects.equals(extension, "jpg") || Objects.equals(extension, "jpeg"))
            return fileName;
        // そうでなければ拡張子の前の部分を取り出して,".jpg" を追加して返す.
        return fileName.substring(0, index) + ".jpg";
    }
    public static void main(String[] args) throws IOException {
        ToJpegConverter tjc = new ToJpegConverter();
        tjc.run(args);
    }
}

実行結果

$ ls
Gradiation.class   Gradiation.java  ToJpegConverter.class  ToJpegConverter.java  dest.png
$ file dest.png  # dest.png のファイルフォーマットを調べる.
dest.png: PNG image data, 255 x 255, 8-bit/color RGB, non-interlaced
$ java ToJpegConverter dest.png dest.jpg # フォーマットを変換し,jpegファイルを出力する.
$ file dest.jpg  # 出力された dest.jpg のフォーマットを調べる.
dest.jpg: JPEG image data, JFIF standard 1.02, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 255x255, components 3
$ java ToJpegConverter dest.png dest2.png # 拡張子が何であっても,jpegファイルを出力する.
$ file dest2.jpg  # 出力された dest.jpg のフォーマットを調べる.
dest2.jpg: JPEG image data, JFIF standard 1.02, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 255x255, components 3

画像の変換

例題3. アフィン変換

画像に対して,平面上の変換を適用します. この変換は,平行移動,拡大・縮小,反転,回転,変形により構成されます. この変換のことをアフィン変換(Affine Transform)と呼びます. Java では,AffineTransform 型が変換の内容を表し, AffineTransformOp型がアフィン変換を適用する変換器を表します.

次の例題プログラムをコンパイル,実行してみましょう. コマンドライン引数で与えられた画像ファイルを90度回転させた画像が transformed.png に出力されます.

import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;

import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class ImageTransformer {
    void run(String[] args) throws IOException {
        BufferedImage image = ImageIO.read(new File(args[0]));
        BufferedImage result = doFilter(image);
        ImageIO.write(result, "png", new File(args[1])));
    }
    BufferedImage doFilter(BufferedImage source) {
        // 画像の中心座標を中心に π/2 (90 度)回転させるアフィン変換を作成する.
        AffineTransform affine = AffineTransform.getRotateInstance(Math.PI / 2,
                source.getWidth() / 2, source.getHeight() / 2);
        AffineTransformOp transformer = new AffineTransformOp(affine, AffineTransformOp.TYPE_BICUBIC);
        return transformer.filter(source, null);
    }
    // main 省略
}

元画像(DALL・E2で生成) 元画像(DALL・E2で生成)

変換後の画像) 変換後の画像)

doFilter メソッドの最初の getRotateInstance が回転を表すアフィン変換の実体を取得しています. 第1引数が回転角度(ラジアン指定),第2引数,第3引数が回転の軸です. 上の例題では,元画像(source)の中心座標を軸として$\frac{\pi}{2}$(90度)回転させています. 第2,第3引数を省略すると左上の原点を軸として回転します.

21行目の transformer.filter メソッドの第2引数は出力先の BufferedImage の実体です. 出力先が null であれば適当な大きさのBufferedImageの実体が作成されますので,ここでは nullにしています.

アフィン変換の詳細については,API ドキュメント や 後述の参考資料,また,様々なドキュメントがWeb上にありますので,そちらを参照してください.

度数法と弧度法の相互変換

度数法と弧度法の変換には,Math.toRadianssMath.toDegrees が利用できます.

  • Math.toRadians(90)$\frac{\pi}{2}$ に変換されます.
  • Math.toDegrees(Math.PI / 2)90.0 という値に変換されます(実際には90.0ではなくそれに近い値になります).

参考資料

例題4. 画像の上下反転

アフィン変換を用いて,画像の上下や左右を反転させることもできます. 以下のサンプルプログラムを動かしてみましょう.

AffineTransformgetScaleInstance メソッドは,画像をx方向y方向それぞれの倍率を設定できます. そこで,x方向に1倍,y方向に-1倍する変換を行います. そのままでは,描画領域外に描画されることになりますので,y軸方向に source.getHeight() だけマイナス方向に移動させます. このように,アフィン変換の実体に対して,変換を繰り返すことが可能です. 以下の画像をクリックして,どのように変化するかを確認してください.

Affine変換による画像変換
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;

import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class ImageFlipper {
    void run(String[] args) throws IOException {
        BufferedImage image = ImageIO.read(new File(args[0]));
        BufferedImage result = doFilter(image);
        ImageIO.write(result, "png", new File(args[1]));
    }
    BufferedImage doFilter(BufferedImage source) {
        // x方向に1倍,y方向に-1倍する.
        AffineTransform affine = AffineTransform.getScaleInstance(1, -1);
        // 上の変換に加えて,x方向に0,y方向に source の高さ分だけ移動させる.
        affine.translate(0, -source.getHeight());
        AffineTransformOp transformer = new AffineTransformOp(affine, AffineTransformOp.TYPE_BICUBIC);
        return transformer.filter(source, null);
    }
    // main 省略
}

元画像(DALL・E2で生成) 元画像(DALL・E2で生成)

変換後の画像) 変換後の画像)

import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;

import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class ImageFlipper {
    void run(String[] args) throws IOException {
        BufferedImage image = ImageIO.read(new File(args[0]));
        BufferedImage result = doFilter(image);
        ImageIO.write(result, "png", new File(args[1]));
    }
    BufferedImage doFilter(BufferedImage source) {
        // x方向に1倍,y方向に-1倍する.
        AffineTransform affine = AffineTransform.getScaleInstance(1, -1);
        // 上の変換に加えて,x方向に0,y方向に source の高さ分だけ移動させる.
        affine.translate(0, -source.getHeight());
        AffineTransformOp transformer = new AffineTransformOp(affine, AffineTransformOp.TYPE_BICUBIC);
        return transformer.filter(source, null);
    }
    public static void main(String[] args) throws IOException {
        ImageFlipper flipper = new ImageFlipper();
        flipper.run(args);
    }
}

例題5. 画像への書き込み

画像に様々な図形を描画することも可能です. 画像に図形を描画するには,BufferedImage の実体から createGraphicsGraphics2D 型の実体を取得する必要があります. Graphics2D とは描画器であり,この実体を用いることで,四角形や楕円,文字などを自由に描くことができるようになります. 以下のサンプルプログラムを実行して実行結果を確認してみてください.

import java.awt.Graphics2D;
import java.awt.BasicStroke;
import java.awt.Color;
// その他の import は省略

public class ImageDrawer {
    void run(String[] args) throws IOException {
        BufferedImage image = ImageIO.read(new File(args[0]));
        BufferedImage result = doFilter(image);
        ImageIO.write(result, "png", new File(args[1]));
    }
    BufferedImage doFilter(BufferedImage image) {
        Graphics2D g = image.createGraphics();   // 描画器を取得する.
        g.setStroke(new BasicStroke(5f)); // 線の太さを設定する.
        g.setColor(Color.BLUE); // 色を設定する.
        g.drawRect(10, 10, image.getWidth() - 20, image.getHeight() - 20); // 四角形を描く
        g.setColor(Color.ORANGE);
        g.drawOval(20, 20, image.getWidth() - 40, image.getHeight() - 40); // 楕円を描く
        return image;
    }
    // main 省略
}

元画像(DALL・E2で生成) 元画像(DALL・E2で生成)

変換後の画像) 変換後の画像)

Graphics2D には線の太さや種類(波線や点線など)を変更できる setStroke や,色を設定する setColor のほか,次のような図形を描くことができます.

  • 直線
    • drawLine(x1, y1, x2, y2)
      • x1, y1Integer型)からx2, y2Integer型)まで直線を描画する.
  • 四角形
    • drawRect(x, y, width, height)
      • x, yInteger型)を起点(左上)として,width, heightInteger型)の大きさの四角形を描画する.
  • 楕円
    • drawOval(x, y, width, height)
      • x, yInteger型)を起点(左上)として,width, heightInteger型)の大きさの楕円を描画する.
  • 文字列
    • drawString(string, x, y)
      • x, yInteger型)の位置に string の文字列を描画する.
  • 画像を描く
    • drawImage(image, x, y, ImageObserver)
      • x, yInteger型) の位置に image を描画する.ImageOverserimageの状態の通知を受けるための実体であるが,nullを渡しておけば良い.
    • drawImage(image, x, y, width, height, ImageObserver)
      • x, yInteger型)の位置に width, heightInteger型)の大きさで imageを描画する. 最後の引数のImageOverserimageの状態の通知を受けるための実体であるが,nullを渡しておけば良い.
    • drawImage(image, AffineTransform, ImageOverser)
      • x, yInteger型)の位置に AffineTransformの実体が表す変換を施した上で,imageを描画する. 最後の引数のImageOverserimageの状態の通知を受けるための実体であるが,nullを渡しておけば良い.
import java.awt.Graphics2D;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class ImageDrawer {
    void run(String[] args) throws IOException {
        BufferedImage image = ImageIO.read(new File(args[0]));
        BufferedImage result = doFilter(image);
        ImageIO.write(result, "png", new File(args[1]));
    }
    BufferedImage doFilter(BufferedImage image) {
        Graphics2D g = image.createGraphics();   // 描画器を取得する.
        g.setStroke(new BasicStroke(5f)); // 線の太さを設定する.
        g.setColor(Color.BLUE); // 色を設定する.
        g.drawRect(10, 10, image.getWidth() - 20, image.getHeight() - 20); // 四角形を描く
        g.setColor(Color.ORANGE);
        g.drawOval(20, 20, image.getWidth() - 40, image.getHeight() - 40); // 楕円を描く
        return image;
    }
    public static void main(String[] args) throws IOException {
        ImageDrawer drawer = new ImageDrawer();
        drawer.run(args);
    }
}

練習問題

1. グラデーション2

実行例に示す画像のような255×255の画像を作成してください. 画像ファイル名は gradiation2.png とし,クラス名は Gradiation2 としてください.

実行例

ヒント

  • 例題1を元に作成しましょう.
  • setRGB に指定する色を以下のルールで指定すれば良いです.
    • 右上が青であるため,xが大きくなるにつれ,青成分を増加させるようにしましょう.
    • 左下が赤であるため,yが大きくなるにつれ,赤成分(y << 16)を増加させましょう.
    • 2つの成分を論理和で合成すると良いでしょう(x | y << 16).

2. ファイルフォーマット変換

2つのコマンドライン引数を受け取り,第1引数で与えられた画像を読み込み,第2引数で与えられたファイルに画像を出力してください. ただし,第2引数のファイル名の拡張子で指定されたフォーマットで画像を出力してください. クラス名は ImageFormatConverter としてください.

実行例

$ java ImageFormatConverter sample1.png a2-1.jpg
$ file a2-1.jpg  # 出力された a2-1.jpg のフォーマットを調べる.
a2-1.jpg: JPEG image data, JFIF standard 1.02, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 255x255, components 3
$ java ImageFormatConverter sample1.png a2-2.bmp
$ file a2-2.bmp  # 出力された a2-1.bmp のフォーマットを調べる.
a2-2.bmp: data

ヒント

  • 拡張子は,例題2findDestNameextensionで取得できています. この extensionをそのまま出力フォーマットの指定で用いると良いです.

3. グレースケール画像への変換

コマンドライン引数で与えられた画像をグレースケール画像に変換してgray.pngに出力してください. クラス名は GrayScaleFilter としてください. グレースケールへの変換は, BufferedReader の実体を作成する時に, BufferedImage.TYPE_BYTE_GRAY を指定するとグレースケール画像に変換できます.

実行例

グレースケール画像 グレースケール画像

ヒント

  • 読み込んだ画像(source)とは別の BufferedImage の実体を作成します(grayImage).
    • 作成の際に BufferedImage.TYPE_BYTE_GRAY を指定しましょう.
    • 大きさは元の画像の大きさと同じにしましょう.
      • BufferedImage grayImage = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_BYTE_GRAY)
  • grayImage から createGraphics メソッドを用いて Graphics2D の実体(g2)を取得します.
    • Graphics2D g2 = grayImage.createGraphics();
  • g2.drawImage で元の画像をgrayImageに描画しましょう.
    • g2.drawImage(source, 0, 0, null);
  • grayImage をファイルに出力して終了です.

4. 画像の回転

コマンドライン引数で与えられた画像を,同じくコマンドライン引数で与えられた角度(度数法)だけ回転させて出力してください. ただし,画像が切れないように大きさを調整してください. 角度は 0〜90 の範囲とします. クラス名は ImageRotatorとしてください. グレースケール画像と同様に,元画像とは別の BufferedImageの実体を作成し,そこに回転した画像を描画しましょう.

実行例

$ java ImageRotator sample1.png 60 dset.png

回転後の画像 回転後の画像

ヒント

  • 以下の説明では,元画像(source)の横幅を $w$,高さを$h$とし,回転角度を $\theta$ とします.

変換後の画像の大きさ

  • 以下の画像では$\theta=60$の例を示しています(クリックして確認してください).
  • このように,回転後の画像の横幅は,$w\sin\theta + w\cos\theta$ です.
    • 同様に,回転後の画像の高さは $h\sin\theta + h\cos\theta$ です.
画像の回転の様子

処理手順

  • AffineTransformの実体を作成し, $\theta$だけ回転させた後,$x$軸方向に $w\sin\theta$ だけ移動させます.
    • 変換を指定した順序とは逆の順序で変換が適用される点に注意してください.
      • つまり,プログラムでは,平行移動(translate)した後,回転(rotate)させてください. 以下のようなプログラムで実現できるでしょう.
Dobule angle = Math.toRadians(degree);
AffineTransform affine = new AffineTransform();
affine.translate(width * Math.sin(angle), 0d);
affine.rotate(angle);
// ... 出力先画像の Graphics2D の実体を取得し,g2 に代入する.
g2.drawImage(source, affine, null);

5. 自由課題

与えられた画像に対して何らかの変換を行いファイルに出力してください. クラス名は,ImageEditor としてください.

まとめ

まとめ

第8講 ファイル入出力

この講で利用するプログラム

第8講 ファイル入出力のサブセクション

ファイルの入出力

ファイルの入出力

ストリーム(Stream)

Java で入出力を扱うには,ストリームという概念の理解が必要です. ストリームとは,データを流れとして扱う仕組みです. データがデータソース(Data Source; データの源,入力元)から流れてくる入力ストリームと, データをデータの出力先(Data Destination; 出力先)に流し込む出力ストリームの2つがあります.

下の画像が入力ストリームを表しています. データソースはファイルやネットワークから,もしくは,別のプログラムであるなど, 入力ストリームを扱う側は知らなくても良いようになっています. とにかく,ストリームを扱う側は,必要なデータが流れてくることさえ理解していれば良いわけです. このような性質のため,Java では,あらゆる入出力にストリームを利用します.

プログラムへの情報の読取り

入力元がファイルであろうと,ネットワークの先であろうと,受け取る方法は同じです. 入力元がどこであろうと,同じように扱うために,ラッピング という作業が必要になります.

下の画像が出力ストリームを表しています.出力も入力ストリームと同じく, ラッピングを利用して,出力先を気にすることなくデータを書き込めます.

プログラムからの情報の書き込み

ファイル,ネットワーク,また,メモリに書き込むときにもストリームを利用します.

ラッピング(Wrapping)

先ほど,ファイルやネットワーク,さては,メモリの入出力までストリームを利用できると述べました. 異なるものを同じように扱うために,ラッピング(wrapping)と言う概念を利用します. ラッピングは,アダプタパターン(Adapter Pattern)と言われる場合もあります.

ラッピングとは,包むや包装するという意味です. その意味のとおり,ラッピングを行うには,とある実体を別の型で覆います. 右図のように,別の型で覆うことで,内側の型(Inner)を外側の型(Outer)として扱えるようになります.

// Inner.java
class Inner {
  void method1() {
    // some operation...
  }
}
// Outer.java
class Outer {
  Inner inner;
  void method2() {
    inner.method1();
  }
}
// Main.java
public class Wrapping {
  void run() {
    Inner inner = new Inner();
    Outer outer = this.wrap(inner);
    outer.method2();
  }
  Outer wrap(Inner inner) {
    Outer outer = new Outer();
    outer.inner = inner;
    return outer;
  }
}

Reader/Writer, InputStream/OutputStream

Java のストリームを扱う型は下の図のように,ReaderWriterInputStreamOutputStreamの4種類に大別できます.

上図を縦のグループで見ると,Reader/Writerがテキストデータを 扱う型,InputStream/OutputStreamがバイナリデータを扱う型です. バイナリデータとは,画像ファイルや音声ファイルなどです. 文字を扱う型では,HTML やプログラムのソースファイルなど,テキストデータを扱います.

一方,上図を横のグループで見ると,ReaderInputStreamが入力ストリームを 表しており,WriterOutputStreamが出力ストリームを表しています. そして,Readerと呼ばれる型にも複数の型が存在します.Writerも同じく, 複数のWriter型が存在します.

import 文

4種類のストリームを扱う場合は,import 文が必要です. それぞれの型の先頭にjava.io.をつけたものを import してください. 例えば,ReaderWriterを使いたい場合は,次の 2 行がプログラムの先頭に必要です.

import java.io.Reader;
import java.io.Writer;

Reader型の例えば,FileReaderBufferedReader を利用するときには,次の2行が必要です.

import java.io.BufferedReader;
import java.io.FileReader;

複数の import 文をまとめるために,import java.io.*;としても構いません. 先ほどのReaderWriterの import の 2 行を 1 行で書けるようになります.

Reader型

Reader 型の利用方法

まずは,入力を扱うReaderを利用する方法を確認します.Readerと一口に言っても, 以下のように多くの型がReader型には存在します.

  • FileReader
    • ファイル(File型)からテキストデータを読み込むためのReader型.
  • BufferedReader
    • バッファリングを行うためのReader型.
    • データソースから行単位で文字列を読み込める.
      • 他の Readerは文字単位でしかデータを読み込めない.
  • StringReader
    • 文字列(String型)からテキストデータを読み込むためのReader型.
  • LineNumberReader
    • 読み込んだ行数を数えるReader型.
  • InputStreamReader
    • InputStream型の入力ストリームをReader型に変換するReader型.

ここに挙げた以外にもいくつかのReader型が存在しますが,この講義では,FileReaderBufferedReader,そして,InputStreamReaderの3つの型を扱います.

また,それぞれの型を利用するには,それぞれの型のインポートが必要です. 例えば,BufferedReaderを利用するときには,import java.io.BufferedReader;という一文が クラス宣言の前に必要です.

典型的なファイルからのデータの読み込み方法.

void readMethod(File file) throws IOException{
    BufferedReader in = new BufferedReader(new FileReader(file)); // (1)
    // 上記の(1)の処理を区別して書くと,次のような処理になる.
    //     FileReader freader = new FileReader(file);
    //     BufferedReader in = new BufferedReader(freader);
    String line;
    while((line = in.readLine()) != null){
        // 1行ずつ処理を行う.
    }
    in.close();
}

ファイルからデータを 1 行ずつ読み込む典型的な方法は,上記の通りです. まず FileReaderを利用してファイルから読み込むための Readerを構築(new)します. 次に,構築した FileReaderBufferedReaderに渡し, 行単位で読み込めるBufferedReaderを構築します.

そして,1 行ずつ読み込むには,BufferedReader型が持つreadLineメソッドを 呼び出します.readLineメソッドは,1 行読み込み,その結果をString型で返します. もう読み込めるデータがない場合は,nullを返します. そのため,while((line = in.readLine()) != null) という繰り返しで最後まで順に読み込めるようになります.

最後にこれ以上ストリームを利用しないため,closeメソッドを呼び出します.close メソッドは一番外側のReaderに対してだけ呼び出せば良いです. ラップされているReaderは外側のReaderが閉じられると,自動的に閉じられます.

入出力を扱うには,必ず IOExceptionという検査例外に対応しなければいけません. どこかで入出力エラーが起こる可能性が排除できないためです. そのため,メソッドのシグネチャに,throws 節を追加しています. もちろん,readMethodメソッドを呼び出しているメソッドにも throws 節で,IOExceptionの責任を転嫁してください.

この点に不安がある人は,[例外機構](https://ksuap.github.io/2024spring//lesson07/write/#例外機構 exception-architecture)を復習してください.

ただし,FileReaderに渡すファイルが存在しない場合 (existsメソッドがfalseを返す場合),FileNotFoundExceptionという例外が投げられます. なお,FileNotFoundExceptionIOExceptionとしても扱えますので,throws 節はIOExceptionのみで問題ありません.

1文字単位の読み込み

1 行単位で読み込むには,典型的なファイルからのデータの読み込み方法のようなプログラムで可能です. 一方で,1文字単位で読み込みたい場合もあります. その場合の典型例を以下に示します.

void readMethod(String targetFile) throws IOException{
  FileReader in = new FileReader(targetFile); // File型でもString型でもOK
  Integer read;
  while((read = in.read()) != -1){
    // 1文字単位で読み込む.この場合,ラッピングはしてもしなくてもOK.
  }
  in.close();
}

Java の Reader 型には,1文字単位で読み込むメソッドreadが用意されています. 読み込んだ文字を Integer 型で返します. 読み込むデータがもう存在しない場合は,-1が返されますので,その間繰り返す,というプログラムになっています.

例題 1. Cat コマンドの作成

コマンドラインで指定されたテキストファイルの内容を表示するコマンド Catを作成してください. この例題では,コマンドライン引数が与えられなかった場合は考える必要はありません.

実行例

$ cat Cat.java
import java.io.*;
    ...途中省略
}
$ java Cat Cat.java
import java.io.*;
    ...途中省略
}
import java.io.*;

public class Cat{
    void run(String[] args) throws IOException{
        cat(new File(args[0]));
    }
    void cat(File file) throws IOException{
        BufferedReader in = new BufferedReader(new FileReader(file));
        String line;
        while((line = in.readLine()) != null){
            System.out.println(line);
        }
        in.close();
    }
    public static void main(String[] args) throws IOException{
        Cat cat = new Cat();
        cat.run(args);
    }
}

Writer型

Writer 型の利用方法

出力を扱うWriterを利用する方法を確認しましょう.Writer型もReader型と同じく, 多くの型が存在します.典型的な型を次に紹介します.

  • FileWriter
    • ファイル(File型)に書き込むためのWriter型.
  • PrintWriter
    • printfprintlnprintが利用できるWriter型.
      • 他のWriter型は文字単位でしか出力できない.
  • StringWriter
    • 文字列(String型)に書き込むためのWriter型.
  • OutputStreamWriter
    • OutputStream型をWriter型に変換するためのWriter型.

ここに挙げた以外にもいくつかのWriter型が存在しますが,この講義では,FileWriterPrintWriter の2つの型のみを利用します.

典型的なファイルへのデータの書き込み方法.

void writeWithWriter(File file, String message) throws IOException{
    PrintWriter out = new PrintWriter(new FileWriter(file)); // (2)
    // 上記の(2)の処理を区別して書くと次のような処理になる.
    //     FileWriter fwriter = new FileWriter(file);
    //     PrintWriter out = new PrintWriter(fwriter);
    out.print(message);
    out.close();
}

ファイルに文字列(String型の変数message)を書き込む方法は上記の通りです. まず,FileWriterを利用してファイルに書き込むためのWriter型を構築します. 次に,構築したFileWriterPrintWriterに渡し,文字列の出力が 可能なPrintWriterを構築します.

そして,文字列をファイルに書き込むには,PrintWriter型の変数outに対してprintメソッドを呼び出して,文字列を書き込んでいます. 他に,printlnprintfメソッドも用意されています.

最後に,これ以上ストリームを利用しないため,closeメソッドを呼び出しています.closeメソッドを 呼び出すと,ラップされているFileWriterも閉じられます.

読み込み時と同じように,IOExceptionの例外に対応しなければいけません. ここでも同じように,writeWithWriterを呼び出しているメソッドに責任を転嫁するため,throws節 でIOException を投げると宣言しましょう.

1文字単位で書き込む場合は,Writer型のwriteメソッドを利用してください.write メソッドにInteger型の値を渡せば適切な出力先に書き込まれます. 1文字単位の読み込みで取得した Integer型の値を渡すのが典型的な使い方でしょう.

例題 2. 指定された行数を出力するコマンド

ここでは,ファイルに値を書き出してみましょう. クラス名をOutputNとし, コマンドライン引数で数字とファイル名を受け取ってください. コマンドライン引数で受け取った数字を1から順にカウントアップして指定行数を指定されたファイルに出力してください.

実行例

$ ls             # 出力ファイルがないことを確認する.
OutputN.java      OutputN.class
$ java OutputN 3 file3.txt # file3.txt に書き込む.
$ ls             # file3.txt が作成されたことを確認する.
OutputN.java      OutputN.class        file3.txt
$ cat file3.txt  # file3.txt の内容を確認する.
1
n2
3
$ java OutputN 10 file10.txt # file10.txt に書き込む.
$ ls             # 新たに file10.txt が作成されたことを確認する.
OutputN.java      OutputN.class        file3.txt      file10.txt
$ cat file10.txt # file10.txt の内容を確認する.
1
2
...途中省略
9
10
  • 最初は何もファイルがありません.
  • java OutputN 3 file3.txt を実行して,file3.txt に値を書き込んでいます.
    • OutputNの実行時に3が渡されていますので,1, 2, 3 とカウントアップした値が各行に出力されています.
    • cat file3.txtで内容の確認をしています.
  • java OutputN 10 file10.txt を実行して,file10.txt に値を書き込んでいます.
    • cat file10.txtで内容の確認をしています.
import java.io.*;

public class OutputN{
    void run(String[] args) throws IOException{
        Integer max = Integer.valueOf(args[0]);
        PrintWriter out = new PrintWriter(new FileWriter(new File(args[1])));
        for(Integer i = 1; i <= max; i++){
            out.println(i);
        }
        out.close();
    }
    public static void main(String[] args) throws IOException{
        OutputN output = new OutputN();
        output.run(args);
    }
}

例題 3. 単純なファイルコピー

ここでは,引数に与えられたファイルの内容を output というファイルにコピーして見ましょう. クラス名は SimpleCopier とし,コマンドライン引数で 1 つのファイル名を受け取ってください.

コマンドライン引数で受け取ったファイルをFileReaderを用いて開き, 出力先の output ファイルもFileWriterで開きましょう. 次に,読み込み元から1文字読み込み,出力先に1文字書き出す処理を読み込み元からデータが読み込めなくなるまで繰り返しましょう.

実行例

$ ls
SimpleCopier.java     SimpleCopier.class
$ java SimpleCopier SimpleCopier.java # SimpleCopier.java を output にコピーする.
$ ls
output                SimpleCopier.java     SimpleCopier.class
$ diff output SimpleCopier.java # 2つのファイルの違いを確認するコマンド
$ # 何も違いがないので,何も出力されない.
import java.io.*;

public class SimpleCopier{
    void run(String[] args) throws IOException {
        FileReader in = new FileReader(args[0]);
        FileWriter out = new FileWriter("output");
        this.copy(in, out);
        in.close();
        out.close();
    }
    void copy(FileReader in, FileWriter out) throws IOException {
        int data;
        while((data = in.read()) != -1){ // 1文字ずつ読み込み,
                                         // データが読み込めなくなるまで繰り返す.
            out.write(data); // データを1文字書き出す.
        }
    }
    // mainメソッドは省略.
}

InputStream/OutputStream型

OutputStream 型

データを書き出すための Stream であり,使い方は Writer とほぼ同じです. 例えば,以下のサンプルプログラムの writeWithWriterWriter 型で示した典型的なWriterの使い方です. このwriteWithWriterと対比して,writeWithOutputStreamを確認してください.

void writeWithWriter(File file, String message) throws IOException{
    PrintWriter out = new PrintWriter(new FileWriter(file)); // (2)
    // 上記の(2)の処理を区別して書くと次のような処理になる.
    //     FileWriter fwriter = new FileWriter(file);
    //     PrintWriter out = new PrintWriter(fwriter);
    out.print(message);
    out.close();
}

void writeWithOutputStream(File file, String message) throws IOException{
    BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file)); // (3)
    // 上記の(3)の処理を区別して書くと次のような処理になる.
    //     FileOutputStream fout = new FileOutputStream(file);
    //     BufferedOutputStream out = new BufferedOutputStream(fout);
    out.write(message.getBytes());
    out.close();
}

writeWithWriterwriteWithOutputStreamの違いは,メソッド内で利用している型のみで,構造がほぼ同じであることに注意してください. writeWithOutputStream では,ファイルを開き,その型(ここではFileOutputStream)を別の型(BufferedOutputStream)でラップしています. そして,BufferedOutputStreammessage.getBytes() の返り値をwriteメソッドで書き出しています. message.getBytes()は名前から想像できるように,文字列(String型)のバイト配列を返します. このことから,OutputStreamは文字データ(Character型)ではなく,バイナリデータ(Byte型)を扱うことに注意しましょう.

加えて,利用している型は異なりますが,名前が似ている型があることにも注意しましょう. 例えば,writeWithWriterメソッドでは,中で FileWriterを利用していますが, writeWithOutputStreamメソッドでは,FileWriterではなく,FileOutputStreamを利用しています.

OutputStream 型色々

OutputStream型もWriter型と同様に多くの型が存在します. ファイルに書き出すときは,FileOutputStreamを使いますが,典型的には OutputStream の機能のみ(writeメソッドにInetger型を渡す)を使うことが多いでしょう.

  • FileOutputStream
    • ファイル(File型)にデータを書き込むためのOutputStream型.
  • ByteArrayOutputStream
    • メモリ上にデータを書き出し,バイト配列で取得できるOutputStream型.
  • GZIPOutputStream
    • このOutputStreamにデータを書き出すと zip 圧縮されるようになる.

InputStream 型

InputStreamはデータを読み込むための Stream であり,バイナリデータを読み込みます. 使い方は Reader とほぼ同じで,Stream を開き,そこからデータを読み取ります. readメソッドで1バイトのデータを読み込みます.この時,Integer型でデータが返されます. 返り値となるデータが-1 の場合,これ以上,読み取るデータがないことを表します.

InputStream 型色々

InputStream型もReader型と同様に多くの型が存在します. ファイルからデータを読み出すときは,FileInputStreamを使います. また,読み込むときにバッファリングしたい場合は,BufferedInputStreamを使うことが多いでしょう. それでも,本講義で扱うプログラムでは,引数のないreadメソッドを呼び出し,1 バイト単位で読み込む方法で十分でしょう.

  • FileInputStream
    • ファイル(File型)からバイナリデータを読み込むためのInputStream型.
  • BufferedInputStream
    • データをバッファリングして読み込むためのInputStream型.
  • ByteArrayInputStream
    • バイト配列から1バイトずつデータを読み込むためのInputStream型.
  • GZIPInputStream
    • gzip 圧縮されたファイルをFileInputStreamで開き,このGZIPInputStreamでラップして読み込むと,gzip 圧縮された内容を読み込むことが可能です.

例題 4 InputStream を利用した Cat

InputStream はバイナリデータですので,行の概念がありません.改行コードも文字も同列に扱われるためです. それでも,例題 1のようなプログラムを作成可能です. 1バイトずつ読みこみ,1バイトずつ書き出せば良いのです. 以下の例をコンパイル,実行してください.

import java.io.*;
public class CatByStream{
    void cat(String file) throws IOException{
        System.out.printf("========== %s ==========%n", file);
        FileInputStream in = new FileInputStream(file);
        this.printFileContent(in);
        in.close();
    }
    void printFileContent(FileInputStream in) throws IOException{
        Integer data;
        while((data = in.read()) != -1){
            System.out.write(data);
        }
    }
    // run や main メソッドは省略.
}

このプログラムのprintFileContentメソッドを見てみましょう. FileInputStream型の変数inに対してreadメソッドを呼び出し,1バイトのデータを受け取っています. 読み取れるデータがない場合は -1 が返されます.

そして,受け取ったデータを変数dataに代入し,System.out.writeメソッドが標準出力に書き出しています. このように1バイト単位で読み出し,書き出しすることでも,Cat コマンドを実現可能です.

import java.io.*;
public class CatByStream{
    void run(String[] args) throws IOException {
        for(Integer i = 0; i < args.length; i++){
            this.cat(args[i]);
        }
    }
    void cat(String file) throws IOException{
        System.out.printf("========== %s ==========%n", file);
        FileInputStream in = new FileInputStream(file);
        this.printFileContent(in);
        in.close();
    }
    void printFileContent(FileInputStream in) throws IOException{
        Integer data;
        while((data = in.read()) != -1){
            System.out.write(data);
        }
    }
    public static void main(String[] args) throws IOException {
        CatByStream cat = new CatByStream();
        cat.run(args);
    }
}

例題 5 単純なファイルコピー 2

それでは,例題 3 単純なファイルコピーInputStream/OutputStreamを使って書き直して見ましょう. クラス名は SimpleCopier2 としてください.

import java.io.*;

public class SimpleCopier2{
    void run(String[] args) throws IOException {
        FileInputStream in = new FileInputStream(args[0]);
        FileOutputStream out = new FileOutputStream("output");
        this.copy(in, out);
        in.close();
        out.close();
    }

    void copy(FileInputStream in, FileOutputStream out) throws IOException {
        Integer data;
        while((data = in.read()) != -1){ // 1文字ずつ読み込み,
                                         // データが読み込めなくなるまで繰り返す.
            out.write(data); // データを1文字書き出す.
        }
    }

    public static void main(String[] args) throws IOException {
        SimpleCopier2 copier = new SimpleCopier2();
        copier.run(args);
    }
}

例題 6 pnm 画像の生成

pnm 画像とは,pbm, pgm, ppm の総称で非常に単純な画像フォーマットです. それぞれ,portable binary map, portable gray map, portable pix map の略です. この画像フォーマットは ImageIO では出力できません. しかし,非常に単純なフォーマットで,簡単に出力できますので,実践してみましょう. ここでは,ppm 画像(カラー画像)を作成します. ppm画像は 3 バイトで1画像を表します.画素とは画像の1ピクセルのことです. 1画素は R(赤成分),G(緑成分),B(青成分)の3つの成分で構成されます.

ppm 画像は,次のようなフォーマットです.各行は1バイトの改行(\n)で区切られます. このうち,WIDTHは横幅を表す整数値,HEIGHTは高さを表す整数値で,画像データはバイナリデータで画素の左上から始まり左に向かって出力されます. 一番右端のデータが書かれれば,一段下の行の左端からのデータが書き出されます. そして,1画素あたり,R(赤成分),G(緑成分),B(青成分)の順で書き出され,これがWIDTH*HEIGHTだけ続きます.

P6
WIDTH HEIGHT
255
画像データ

このような ppm 画像で,下に示すようなグラデーション表示をしてみましょう. 256×256 の大きさで,左上が黒(RGB がそれぞれ 0),左下が赤,右上が青,右下がマゼンタ(RGB で R と B 成分が 255,G 成分が 0)になるように画像を作成してみましょう. なお,ppm 画像は,macOS のプレビューで確認できます.

では,GradiationGeneratorというクラス名で作成してみましょう. 生成した画像を gradiation.ppm というファイルに出力しましょう.

生成するグラデーション画像

// import文は省略
public class GradiationGenerator {
    void run() throws IOException{
        OutputStream out = // gradiation.ppm に出力する OutputStream を構築する.
        this.writeHeader(out);
        this.writeBody(out);
        out.close();
    }

    void writeBody(OutputStream out) throws IOException{
        // 各画素をR, G, B それぞれ1バイトで出力する.
    }

    void writeHeader(OutputStream out) throws IOException{
        out.write('P');  // ヘッダ部分は文字で表される.
        out.write('6');
        out.write('\n'); // 環境が変わっても \n を出力する必要がある.
        out.write('2');  // 横幅
        out.write('5');
        out.write('6');
        out.write(' ');
        out.write('2');  // 高さ
        out.write('5');
        out.write('6');
        out.write('\n');
        out.write('2');  // 1画素の1色成分の最大値
        out.write('5');
        out.write('5');
        out.write('\n');
    }
    // mainメソッドは省略.
}
import java.io.*;

public class GradiationGenerator{
    void run() throws IOException{
        OutputStream out = new FileOutputStream("gradiation.ppm");
        this.writeHeader(out);
        this.writeBody(out);
        out.close();
    }

    void writeBody(OutputStream out) throws IOException{
        for(Integer i = 0; i < 256; i++){ // 画像の縦方向
            for(Integer j = 0; j < 256; j++){ // 横方向
                out.write(i); // R成分
                out.write(0); // G成分
                out.write(j); // B成分
            }
        }
    }

    void writeHeader(OutputStream out) throws IOException{
        out.write('P');  // ヘッダ部分は文字で表される.
        out.write('6');
        out.write('\n'); // 環境が変わっても \n を出力する必要がある.
        out.write('2');  // 横幅
        out.write('5');
        out.write('6');
        out.write(' ');
        out.write('2');  // 高さ
        out.write('5');
        out.write('6');
        out.write('\n');
        out.write('2');  // 1画素の1色成分の最大値
        out.write('5');
        out.write('5');
        out.write('\n');
    }

    public static void main(String[] args) throws IOException {
        GradiationGenerator generator = new GradiationGenerator();
        generator.run();
    }
}

練習問題

1. 行番号付きの Cat コマンドの作成

例題 1を改良し,行番号付きで出力する cat コマンドを作成してください. クラス名はCat2としてください. ただし,例題 1と異なり,複数のファイルを指定できるようにしましょう. この例題でも,引数は必ず与えられるものとしてください.

出力例

$ cat -n Cat2.java
     1  import java.io.*;
             ...途中省略.
    23  }
$ java Cat2 Cat2.java Cat.java
     1  import java.io.*;
             ...途中省略.
    23  }
     1  import java.io.*;
             ...途中省略.
    19  }

2. Grep コマンドの作成

ここでは,grepコマンドを作成しましょう.grepコマンドとは, キーワードと1つ以上のファイル名が与えられます. ファイルの行にキーワードが含まれていれば,その行を出力するコマンドです.

クラス名はGrepとしてください. 結果出力には,以下の出力例のようにファイル名も含めてください. 複数のファイルが与えられたとしても,検索できるようにしましょう. なお,キーワードは省略されることはなく,ファイルは少なくとも1つは指定されるとして構いません.

文字列にある文字列が含まれているかを確認する

ある文字列(stringA)に,別の文字列(stringB)が含まれているかを 確認するには,containsメソッドを利用してください.

String stringA = "this is a pen";
String stringB = "is a";
String stringC = "are";
if(stringA.contains(stringB)){
  System.out.println("このメッセージは表示される.");
}
if(stringA.contains(stringC)){
  System.out.println("このメッセージは表示されない.");
}

出力例

$ grep line Cat.java
        String line;
        while((line = in.readLine()) != null){
            System.out.println(line);
$ java Grep line Cat.java
Cat.java:         String line;
Cat.java:         while((line = in.readLine()) != null){
Cat.java:             System.out.println(line);
$ java Grep class Cat.java Cat2.java
Cat.java: public class Cat{
Cat2.java: public class Cat2{

3. Head コマンドの作成

指定された行数だけファイルの先頭から出力するコマンドheadを作成しましょう. コマンドライン引数では,行数とファイル名を受け取ってください. ただし,ファイル名は省略可能です.ファイル名が省略された場合,標準入力から読み込むようにしてください. クラス名は Headとしてください.

コマンドライン引数が 1 つしか与えられなかった時に,標準入力(System.in)から受け取るようにするには, 次のようなコードで BufferedReaderを構築してください. 標準入力,標準出力については,基礎プログラミング演習 II の講義資料を確認してください.

BufferedReader in;
// コマンドライン引数が1つしか与えられなかった場合.
if(args.length == 1){
  in = new BufferedReader(new InputStreamReader(System.in));
}
else{
  in = new BufferedReader(new FileReader(args[1]));
}

出力例

$ head -3 Cat.java     # Cat.java の先頭3行を出力する.
import java.io.*;

public class Cat{
$ java Head 3 Cat.java # Cat.java の先頭3行を出力する.
import java.io.*;

public class Cat{
$ cat Cat.java | java Head 4 # 入力を標準入力から受け取る
import java.io.*;

public class Cat{
    void run(String[] args) throws IOException{

cat Cat.java | java Head 4 とコマンドを実行した時に,標準入力から入力を受け取ることになります. そうでない場合(ファイルをコマンドライン引数で指定した場合)は, java Head 3 Cat.java のようなコマンドの入力になります.

4. Tee コマンドの作成

ここでは,teeコマンドを作成しましょう.teeコマンドは 標準入力で受け取った文字列を標準出力と,指定されたファイルに出力するコマンドです. 以下の図のように T の形に入力を分配するところから名付けられています.クラス名をTeeとしてください.

tee

コマンドライン引数にファイル名を受け取ってください. また,標準入力から値を受け取るようにしましょう.上記のようにBufferedReaderを 構築した後は,Catの時と同じように入力を受け取れば良いです. 入力が終わればnullが返ってきますので,自動的にループを抜けるようになっています.

出力例

$ cat hoge # hoge というファイルがないことを確認する.
cat: hoge: No such file or directory
$ cat Cat.java | java Tee hoge # 標準出力と hoge に出力する.
import java.io.*;
    ...途中省略
}
$ cat hoge # hoge の内容を確認する.
import java.io.*;
    ...途中省略
}

5. カエサル暗号

カエサル暗号(Caesar 暗号; シーザー暗号)はアルファベットを$n$文字ずらす暗号化方式です. この$n (0 \leq n \leq 256)$が鍵となります.

例えば, $n=1$の時,"abracadabra" という文字列はそれぞれ1文字ずれて 'bcsbdbebcsb' になります ('a''b''b''c''r''s',...のようにずらしていきます).

クラス名は CaesarCipherとし,コマンドラインで3つの引数を受け取ってください. 引数は最初から鍵,入力ファイル,出力ファイルとしてください.

ヒント

文字データを扱うように見えますが,Reader/Writer を使うより,InputStream/OutputStream を使う方が良いでしょう.

InputStream/OutputStream を利用する場合,文字は単なる数値として扱われますので,1 バイト読み込み,暗号化処理を行い,1 バイト書き出してください. 暗号化処理は単純に読み込んだデータに鍵を足せば良いです. ただし,暗号化の計算結果が 0 以上,256 未満になるようにしてください. 暗号化の計算結果が負数であった場合は 256 を足し,256 以上であれば,計算結果から 256 を減算してください (計算した結果の値が 1 バイトに収まっている必要があります).

出力例

$ java CaesarCipher 10 CaesarCipher.java encrypted # 鍵を10にして暗号化する.
$ cat encrypted
swzy|~*tk?k8sy84Ezlvsm*mvk}}*Mko}k|Mszro|?***# ..... 以降省略
$ java CaesarCipher -10 encrypted plain # 復号(暗号文を元の文に戻す)する.
$ diff CaesarCipher.java plain # 全く同じはずなので,何も出力されない.
$

まとめ

まとめ

第9講 Map

この講で利用するプログラム

第9講 Mapのサブセクション

Map(連想配列)

Map とは

住所録を作成することを考えます.iPhone や Android の連絡先アプリを想像してください.

住所録のイメージ

住所録では,上記のように住所や電話番号など複数の情報が名前に紐付いています. このように,人の名前をキーとし,その人に関する情報をバリューとして紐付ける方法として,Mapというデータ構造 が利用できます.Mapでは,キーとバリューのペアの集合を扱います.

Java 以外の他の言語では,Mapを連想配列やディクショナリなどと呼びます. いずれもある実体に別の実体を関連づけるためのデータ構造です.

別の考え方をすると,Listはインデックスという数値に実体が対応付いていました.Mapは 実体に紐付けるインデックスに相当する部分が,数値以外の自由な値が指定できるものとも考えられできます.

Map の種類

先ほども述べたようにMapはキーとバリューのペアの集合を扱うデータ構造です. その実現方法に,ハッシュ値を利用する方法,木構造を利用する方法などがあります. それぞれの実現方法で,Mapを実現する型が存在します.HashMapTreeMapの2つの型です. どちらを利用しても,利用方法や,処理結果に違いはありません. 以下の例では,HashMapを利用して説明していますが,HashMapTreeMapに置き換えても 同じ説明が成り立ちますので,適宜読み替えてください.

なお,HashMapを使うときには,import文が 必要です.import java.util.HashMap;クラス宣言の前に書きましょう.

Map の宣言方法

Map を使うには,Listと同じように,データ構造にどのような型の変数を格納するかを 宣言しなければいけません. 例えば,HashMapString型をキーに Integer型をバリューとする場合,次のような宣言が必要です.

HashMap<String, Integer> map =
    new HashMap<String, Integer>();

上記のように,HashMap<キーの型, バリューの型> という型として宣言しなければいけません. また,実体を作成するときも,new HashMap<キーの型, バリューの型>()のように作成しなければいけません. キーの型,バリューの型が異なれば,同じHashMap型であっても,異なる型であると判断されます.

// Complexをキーに,String型をバリューとした Map.
HashMap<Complex, String> map1 =
    new HashMap<Complex, String>();
// String型をキーに,Complex型をバリューとした Map.
HashMap<String, Complex> map2 =
    new HashMap<String, Complex>();

つまり,上記のように Complex型をキーに,String型を バリューとしたHashMap型変数map1と キーとバリューの型を逆にしたHashMap型変数map2は キーとバリューの型が異なるため,異なる型であると判断され,互いに代入できません. なお,Listと同じく,newの後ろの格納する型は省略可能です. 以下のコードは,上記のComplexStringをそれぞれキー,バリューに指定したコードと同じプログラムとしてコンパイルされます.

// Complexをキーに,String型をバリューとした Map.
HashMap<Complex, String> map1 = new HashMap<>();
// String型をキーに,Complex型をバリューとした Map.
HashMap<String, Complex> map2 = new HashMap<>();

なお,上記のように,独自に作成した型(第 5 講で作成したComplex)もMapに格納可能です.

Map の操作

Map に対する操作はListと同じく,CRUD (Create, Read, Update, Delete)が可能です. 以降の説明は,次に示すHashMap型の変数に対してメソッドを呼び出すものとして読み進めてください.

HashMap<String, Complex> map = new HashMap<>();
// HashMap<String, Complex> map =
//     new HashMap<String, Complex>();
// 上記のように書いても良い

Map にデータを追加する (put)

Complex c1 = // Complexの実体を作成する.
Complex c2 = // Complexの実体を作成する.
map.put("1+i", c1);
map.put("この場合文字列ならなんでも良い", c2);
map.put("バリューは同じものでも良い", c1);
map.put("1+i", c2); // "1+i"に紐付いているバリューを更新する

データを追加するには,上のサンプルプログラムのようにputメソッドを用います.put メソッドで追加するときのキーの型とバリューの型は,Mapの生成時に指定した型と合わせる必要があります. 理論上はデータ数に上限はなく,幾つでも追加できます.

また,異なるキーに同一のバリューを割り当てられます.一方で,同じキーに異なるバリューは割り当てられません. すでにキーとバリューのペアが割り当てられているところに,別のバリューを割り当て用とすると, 今までのバリューが上書きされます.

Map にあるデータを取得する (get)

// putで追加した c1 が返される.
Complex complex1 = map.get("バリューは同じものでも良い");
// putで追加した c2 が返される.
Complex complex2 = map.get("この場合文字列ならなんでも良い");
// 対応付けがないため,null が返される.
Complex complex3 = map.get("対応付けがない場合はnull");

上のサンプルプログラムのように,getメソッドを呼び出すことで,Map内の キーに対応付けられたバリューを取得できます. 返り値の型は,HashMap型の変数宣言時に指定した型(この例ではComplex型)でなければいけません.

キーに対応付けられたバリューが存在しない場合,nullが返されます. 上のサンプルプログラムでは,3 行目のキーはバリューとの対応付けがないため,nullが返されます. なお,返り値が nullの時にデフォルト値を返す getOrDefault メソッドも用意されています.

// c1 が返される.
Complex complex4 =
    map.getOrDefault("対応付けがない場合", c1);

// 上記のプログラムは以下の4行のプログラムと同等.
Complex complex5 = map.get("対応付けがない場合");
if(complex5 == null){
  // キーに対応する値がある場合,このif文は実行されない.
  complex5 = c1;
}

Map にあるデータを削除する (remove)

map.remove("1+i");
// 紐付けが解消されたので,nullが返される.
Complex removedValue1 = map.remove("1+i");

キーを指定して,バリューとの対応付けを削除します. 削除すると,対応付けられていたバリューが返り値として返されます. 上のサンプルプログラムの 1 行目のように,返り値を無視しても構いません.

指定したキーに対応付けられたバリューが存在しなかった場合,nullが返されます.

Map のサイズを取得する (size)

Integer size = map.size();

Map の現在のサイズを取得したい場合は,sizeメソッドを利用しましょう. キーとバリューのペアがいくつ格納されているかが得られます.

Map の要素の繰り返し

Java 言語では,Mapの繰り返しは,次の2種類が利用できます. 注意すべき点として,Mapには順番が存在しないため,従来の Integer型を用いる繰り返しは行えません. そのため,Iterator型を利用して繰り返しを行う必要があります. このとき,java.util.Iteratorだけでなく,java.util.Mapもインポートしておく必要があります (import java.util.Iterator;import java.util.Map;).Map.Entry型をHashMapの要素として扱うためです.

典型的な方法(Iterator)

一番典型的なIterator型を利用する方法です.Java らしい書き方です.

for(Iterator<Map.Entry<String, Complex>> iterator =
      map.entrySet().iterator();
      iterator.hasNext(); ){
  Map.Entry<String, Complex> entry = iterator.next();
  String key = entry.getKey();
  Complex value = entry.getValue();
  // ここに繰り返しの処理を書く.
}

ただし,mapのループの途中で mapの要素数を変化させると例外(ConcurrentModificationException)が発生する場合があります. ループを回している対象の Mapへの値の追加(get)・削除(remove)は行わないようにしましょう.

for(Iterator<Map.Entry<String, Complex>> iterator =
      map.entrySet().iterator();
      iterator.hasNext(); ){
  Map.Entry<String, Complex> entry = iterator.next();
  String key = entry.getKey();
  Complex value = entry.getValue();
  // mapを対象にループを回しているため,
  // ここで,mapの要素数を変化させる(get, removeなど)と例外が投げられる.
}

拡張 for 文を使った方法

拡張 for 文と呼ばれる書き方.実質的には,Iterator型を利用する方法と同じです. コンパイラがIterator型を利用する方法に変換してコンパイルしてくれます. 最近の Java の書き方はこの方法です.

for(Map.Entry<String, Complex> entry: map.entrySet()){
  String key = entry.getKey();
  Complex value = entry.getValue();
  // ここに繰り返しの処理を書く.
}

上記のように,Map.Entry型を順番に取り出して処理するようなプログラムに なっています.Map.Entry型はキーとバリューのペアを扱う型です. すなわち,Mapに格納されている各ペアを扱う型です.Map.Entry型の 利用には,import java.util.Map;というimport文が必要です.

サンプルプログラム

コマンドライン引数の文字列をコンマ(,)で区切り,前方の値をキーに, 後ろの値をバリューにしてMapに格納するプログラム. 最後に,順番に取り出し,出力しています.

import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
  void run(String[] args) {
    HashMap<String, Integer> map = new HashMap<>();
    for(String arg: args) {
      this.putValueToMap(map, arg);
    }
    this.printMap(map);
  }
  void putValueToMap(HashMap<String, Integer> map, String string) {
    // , で文字列を区切っている.
    String[] items = string.split(",");
    Integer value = Integer.valueOf(items[1]);
    map.put(items[0], value);
  }
  void printMap(HashMap<String, Integer> map) {
    for(Map.Entry<String, Integer> entry: map.entrySet()) {
      System.out.printf("%s: %d%n",
          entry.getKey(), entry.getValue());
    }
  }
  // mainメソッドは省略.
}

出力例

$ java HashMapSample one,1 two,2 three,3 four,4
four: 4
one: 1
two: 2
three: 3

先にも述べたようにMapは順序を保持しないため,追加順と出力順が異なっている点に注意しましょう. 出力の順序には意味はありません.実際には,キーのハッシュ値の順番になっていますが, その値を意識する必要はありません. もし,追加順に出力したい場合は,HashMapの代わりにLinkedHashMapを利用してください. また,HashMapの代わりに TreeMapを用いればキーでソートされた結果が得られます. サンプルプログラムを変更して確認してみましょう.

文字列を分割する

文字列を特定の文字で分割するには,String型のsplitメソッドを利用します. split の引数には分割したい区切り文字を指定します. 返り値は引数で渡された文字で分割された結果が,String型の配列で返されます.

文字列分割のサンプルプログラム

String string = "abr,aca,dab,ra";
String[] array1 = string.split(",");
System.out.println(Arrays.toString(array1);
// => [ "abr", "aca", "dab", "ra" ]
String[] array2 = string.split("a");
System.out.println(Arrays.toString(array2);
// => ["", "br,", "c", ",d", "b,r"]

上記のように,"abr,aca,dab,ra""," で分割した場合は,長さ 4 の配列が返されます. この時のそれぞれの要素は上記のサンプルプログラムの通りです.

同様に,"abr,aca,dab,ra""a"で分割した場合は,長さ 5 の配列が返されます. コンマがややこしいですが,上記の実行結果をよく確認してください.

このように,指定された文字列で分割した結果を得るには split メソッドを利用します.

例題 1. お弁当の料金表

以下のプログラムの欠けている部分を補って,指定したお弁当の料金を出力するプログラムを作成してください.

import java.util.HashMap;
public class LunchBoxPrices{
  HashMap<String, Integer> prices;

  void run(String[] args){
    this.initialize();
    for(String arg: args){
      this.searchAndPrint(arg);
    }
  }
  void searchAndPrint(String lunchBoxName){
    Integer price = // お弁当の料金を prices から取得(get)する.

    if(price == null){ // お弁当が見つからなかった.
      // ここに見つからなかった旨を出力する処理を書く.
    }
    else{
      // お弁当の料金を出力する.
      System.out.printf("%s: %d円%n", lunchBoxName, price);
    }
  }
  void initialize(){ // お弁当の料金表を作成するメソッド.
    this.prices = new HashMap<>();
    prices.put("日の丸弁当", 200);
    // 他のお弁当の料金も追加する(10個程度).
  }
  // mainメソッドは省略.
}

お弁当の料金,名前は適当に決めてください.

出力例

$ java LunchBoxPrices のり弁当 上幕ノ内弁当  横綱弁当
のり弁当: 350円
上幕ノ内弁当: 800円
横綱弁当: 見つかりませんでした
import java.util.HashMap;

public class LunchBoxPrices{
    HashMap<String, Integer> prices;

    void run(String[] args){
        this.initialize();
        for(String arg: args){
            this.searchAndPrint(arg);
        }
    }
    void searchAndPrint(String lunchBoxName){
        // お弁当の料金を検索する.
        Integer price = this.prices.get(lunchBoxName);
        if(price == null){ // お弁当が見つからなかった.
            // ここに見つからなかった旨を出力する処理を書く.
            System.out.printf("%s: 見つかりませんでした%n", lunchBoxName);
        }
        else{
            // お弁当の料金を出力する.
            System.out.printf("%s: %d円%n", lunchBoxName, price);
        }
    }
    void initialize(){ // お弁当の料金表を作成するメソッド.
        this.prices = new HashMap<>();
        prices.put("日の丸弁当", 200);
        // 他のお弁当の料金も追加する(10個程度).
        prices.put("のり弁当", 350);
        prices.put("幕ノ内弁当", 400);
        prices.put("ステーキ弁当", 800);
        prices.put("ハンバーグ弁当", 550);
        prices.put("唐揚げ弁当", 450);
        prices.put("サービス弁当", 350);
        prices.put("おにぎり弁当", 250);
        prices.put("上幕ノ内弁当", 800);
        prices.put("サンドウィッチ弁当", 400);
    }
    public static void main(String[] args){
        LunchBoxPrices lbp = new LunchBoxPrices();
        lbp.run(args);
    }
}

練習問題

1. CSV データの格納

ここでは,郵便番号から住所の検索を行うプログラムを作成します.

  1. 郵便番号データを用意してください.
    • 郵便番号データダウンロードページから自分の出身地の郵便番号データをダウンロードしてください.
    • ダウンロードした zip ファイルを解凍してください.
    • 解凍してできたファイルを zipcode.csv にファイル名を変更してください.
    • ダウンロードしたファイルの文字コードは ShiftJIS になっています.utf-8 でなければ Java では文字化けを起こしますので,変換しておいてください.
  2. 郵便番号を読み込み,検索を行うプログラムを作成してください.
    • クラス名は ZipCodeとしてください.
    • コマンドライン引数で与えられた郵便番号に対応する住所を出力してください.

ヒント

ベースとなるプログラム

例題 1. お弁当の料金表が参考になるでしょう. 初期化処理(initializeメソッド)でzipcode.csvを読み込み,HashMap型の変数に 郵便番号と対応した住所を追加(put)しましょう.

その後,検索処理を行います.検索処理はsearchAndPrintメソッドとほぼ同じで良いでしょう. キーとバリューそれぞれの型は何が良いかを考えて作成しましょう.

ダブルクォートの削除

ダウンロードした csv ファイルは,各カラムがダブルクォートで囲まれています. これを削除するには,以下のメソッド(stripQuote)を使うと良いでしょう.sampleCodeメソッド はstripQuoteの使い方の例です.sampleCodeメソッドにあるように,splitで 得られた配列の各要素を順にstripQuoteに渡すことで, ダブルクォートを除いた部分を取り出しています.

    String stripQuote(String item){
        if(item.matches("\".*\"")){
            return item.substring(1, item.length() - 1);
        }
        return item;
    }
    void sampleCode(){
        String original = "\"クォートで囲まれた文字列\"";
        String value = this.stripQuote(original);
        // originalには「"クォートで囲まれた文字列"」が代入されており,
        // valueには「クォートで囲まれた文字列」が代入される.
    }

なお,matches メソッドは与えられた正規表現(Regular Expression)にマッチするかを判定するメソッドです.

出力例

$ java ZipCode 6038035 6038047 1000001
6038035: 京都市北区上賀茂朝露ケ原町
6038047: 京都市北区上賀茂本山
1000001: 見つかりませんでした

100-0001 は東京都の郵便番号ですので,京都府の郵便番号表では見つかりません. そのため,上記のように「見つかりませんでした」と出力されています.

2. 電話帳の作成

以下の仕様に従った名前と電話番号のペアを管理する電話帳を作成しましょう.

  • クラス名はPhoneBookとしてください.
  • 起動するとコマンド入力待ちとなります.
    • 以下のコマンドを入力することによって,データの更新が行われる.
    • add 名前 電話番号
      • 電話帳に名前と対応する電話番号を追加する.
    • list
      • 登録された名前と電話番号の一覧を表示する.
    • find 名前
      • 電話帳に名前が存在すれば名前と電話番号を表示する.
      • 電話帳に名前が存在しなければ何も表示しない.
    • remove 名前
      • 電話帳から名前のデータを削除する.
      • 電話帳に名前のデータが存在しなければ何も行わない.
    • exit
      • 電話帳を終了する.
  • 入力において,文法エラーは起きないものとします.
  • 標準入力から 1 行読み込むには,SimpleConsole.javaを利用してください.
    • SimpleConsole.javaをここからダウンロードし,プログラムと同じディレクトリに置いてください.
    • 入力した文字列をスペースで区切るには,String型のsplitメソッドを利用しましょう.
      • String[] items = line.split(" ");
    • SimpleConsole.javaは一切変更してはいけません.
      • 同様に,PhoneBook.javaには,SimpleConsole.java のコードを貼り付けてはいけません.

ヒント

SimpleConsole の使い方.

SimpleConsole console = new SimpleConsole();
String line = console.readLine();

利用する前に,new で実体を作成してください. その後,得られた実体(上記の例では console)に対して readLine メソッドを呼び出すと 1 行読み込むことができます.

出力例

> list
> add tamada 090-1111-1111
> find tamada
tamada 090-1111-1111
> find akiyama
> add akiyama 090-2222-2222
> list
tamada 090-1111-1111
akiyama 090-2222-2222
> remove akiyama
> find akiyama
> add tamada 090-1111-2222
> list
tamada 090-1111-2222
> exit

> で始まる行が入力を表しています. 最初は電話帳に何も入力されていないため,listコマンドを入力しても何も出力されません. データをaddコマンドで追加していくことで,結果が変わってきます.

まとめ

まとめ

第10, 11講 応用1

2023-07-20 (Thu) 10:30 までに以下の12個のプログラムを提出してください.

この講で利用するプログラム

  1. コマンドラインで受け取った文字列が特定の文字列かを確認する
  2. 文字列の逆順表示
  3. yesコマンド:特定文字列を繰り返す.
  4. sortコマンド:ファイルの内容をソートする.
  5. tacコマンド:ファイルの内容を逆順に表示する.
  6. freqコマンド:与えられたテキストファイルに現れる単語の頻度を求める.
  7. tailコマンド:与えられたテキストファイルの最後の数行を出力する.
  8. シェルピンスキーのギャスケット
  9. Gzipによる圧縮
  10. 逆ポーランド記法の計算機
  11. 2つの文字列間の類似度・距離

第10, 11講 応用1のサブセクション

練習問題

  1. コマンドラインで受け取った文字列が特定の文字列かを確認する
  2. 文字列の逆順表示
  3. yesコマンド:特定文字列を繰り返す.
  4. sortコマンド:ファイルの内容をソートする.
  5. tacコマンド:ファイルの内容を逆順に表示する.
  6. freqコマンド:与えられたテキストファイルに現れる単語の頻度を求める.
  7. tailコマンド:与えられたテキストファイルの最後の数行を出力する.
  8. シェルピンスキーのギャスケット
  9. Gzipによる圧縮
  10. 逆ポーランド記法の計算機
  11. 2つの文字列間の類似度・距離

1. コマンドラインで受け取った文字列が特定の文字列かを確認する

コマンドラインで渡された文字列が特定の文字列("KSU_AP")であれば, 「渡された文字列は"KSU_AP"です」と出力してください. 特定の文字列でなければ,「渡された文字列は"KSU_AP"ではなく,"(実際に渡された文字列)“です.」 と出力するプログラムを作成してください.

コマンドライン引数で複数の文字列が渡された場合,全ての文字列に対して判定を行ってください.

クラス名は,ComparingStringとしてください.

実行例

$ java ComparingString hoge KSU KSU_AP KSU_AP2
渡された文字列は "KSU_AP"ではなく"hoge"です.
渡された文字列は "KSU_AP"ではなく"KSU"です.
渡された文字列は "KSU_AP" です.
渡された文字列は "KSU_AP"ではなく"KSU_AP2"です.

参考

2. 文字列の逆順表示

コマンドライン引数で渡された文字を出力した後,その文字列を逆順に表示してください. クラス名は,ArgsReverserとしてください.

実行例

$ java ArgsReverser abcdefg
abcdefg, gfedcba
$ java ArgsReverser abcdefg foobar
abcdefg, gfedcba
foobar, raboof
$ java ArgsReverser Haruaki Tamada
Haruaki, ikauraH
Tamada, adamaT

ヒント

文字列からn番目の文字を取得する.

  • valueというString型の変数で考えます.
  • valuen番目の文字はvalue.charAt(n)で取得できます.
    • 返り値はCharacter型の変数です.
    • nInteger型です.
    • n0からvalue.length() - 1以下の範囲で value.charAt(n)は有効な値を返します.
  • System.out.printfCharacter型を出力するときのフォーマット記述子は%cです.
  • また,Character型の変数をSystem.out.printに渡しても改行なしで出力されます.

参考

3. yesコマンド:特定文字列を繰り返す.

無限にyを出力するコマンドyesを作成してください. もし,コマンドライン引数に何か文字が指定された場合,その文字を無限回出力してください. 終了するときは,Ctrl-C で終了してください. クラス名は,Yesにしましょう.

実行例

$ yes
y
y
y
... 途中省略
^C # Ctrl-C を入力し,強制終了
$ yes no
no
no
... 途中省略
^C # Ctrl-C を入力し,強制終了
$ java Yes
y
y
... 途中省略
^C # Ctrl-C を入力し,強制終了
$ java Yes yes
yes
yes
... 途中省略
^C # Ctrl-C を入力し,強制終了

参考

4. sortコマンド:ファイルの内容をソートする.

コマンドライン引数で与えられたファイルの内容を行単位でソートして出力するコマンド Sorterを作成してください.

実行例

$ cat string.txt
String class represents character strings.
All string literals in Java programs, such as "abc", are implemented as instances of this class.
Strings are constant; their values cannot be changed after they are created.
String buffers support mutable strings.
Because String objects are immutable they can be shared.
$ sort string.txt
All string literals in Java programs, such as "abc", are implemented as instances of this class.
Because String objects are immutable they can be shared.
String buffers support mutable strings.
String class represents character strings.
Strings are constant; their values cannot be changed after they are created.
$ java Sorter string.txt
All string literals in Java programs, such as "abc", are implemented as instances of this class.
Because String objects are immutable they can be shared.
String buffers support mutable strings.
String class represents character strings.
Strings are constant; their values cannot be changed after they are created.
$ cat string2.txt
abra
cada
bra
abc
def
$ java Sorter string2.txt
abc
abra
bra
cada
def

string.txtが必要であればこの文章をクリックし,ダウンロードしてください.

ヒント

  • まず,ファイルから1行ずつ文字列を読み込みましょう.
  • 読み込んだ文字列を ArrayList に追加しましょう.
  • Collections.sortArrayListの実体を渡し,ソートしましょう.
  • ArrayListの要素を順に出力しましょう.

ArrayListのソート

配列のソートはArrays.sortメソッドで行いました. 同じように,ArrayListをソートするメソッドも標準的に用意されています.Collections.sort メソッドを利用すると,ArrayListの内容を辞書順に並び替えられます. なお,Collectionsを利用するときは,java.util.Collectionsのインポートが必要です.

ArrayList<String> list = // ArrayListの実体を作成する.
Collections.sort(list); // => list の内容が要素を並び替えたものになっている

参考

5. tacコマンド:ファイルの内容を逆順に表示する.

ファイルを逆順に表示するコマンドtacを作成してください. 複数のファイルが指定された場合,各ファイルの内容を逆順に表示してください.

クラス名は Taccatの逆)とします.

実行例

$ cat sample2.txt
ABCDEFG

1234567890
$ java Tac sample2.txt
1234567890

ABCDEFG
$ cat sample3.txt
abracadabra
$ java Tac sample3.txt
abracadabra

参考

6. freqコマンド:与えられたテキストファイルに現れる単語の頻度を求める.

与えられたテキストファイル(英文)の中に現れる単語の頻度を求めるプログラムを作成してください. 単語の頻度とは,その単語が文書中に何回現れたかを示す値です.

実行例

$ cat string.txt
String class represents character strings.
All string literals in Java programs, such as "abc", are implemented as instances of this class.
Strings are constant; their values cannot be changed after they are created.
String buffers support mutable strings.
Because String objects are immutable they can be shared.
$ java Frequencies string.txt
All: 1
"abc",: 1
constant;: 1
be: 2
string: 1
instances: 1
.... 以下略

string.txtが必要であればこの文章をクリックし,ダウンロードしてください.

参考

7. tailコマンド:与えられたテキストファイルの最後の数行を出力する.

与えられたテキストファイルの最後の数行を出力するプログラムを作成してください. 最後の数行もコマンドラインで与えられるものとします. また,数値は必ず正の値が与えられるものと考えて構いません.

実行例

$ cat string.txt
String class represents character strings.
All string literals in Java programs, such as "abc", are implemented as instances of this class.
Strings are constant; their values cannot be changed after they are created.
String buffers support mutable strings.
Because String objects are immutable they can be shared.
$ tail -1 string.txt
Because String objects are immutable they can be shared.
$ java Tail 2 string.txt
String buffers support mutable strings.
Because String objects are immutable they can be shared.
$ java Tail 1000 string.txt
String class represents character strings.
All string literals in Java programs, such as "abc", are implemented as instances of this class.
Strings are constant; their values cannot be changed after they are created.
String buffers support mutable strings.
Because String objects are immutable they can be shared.

string.txtが必要であればこの文章をクリックし,ダウンロードしてください. 総行数よりも大きな値が指定された場合でもエラーが発生しないようにしてください.

なお,tailコマンドのオプションで指定した-1は負の値を表しているのではなく、 1という数値を表しています。1に付けられている-はオプションを表す記号であり, 負の値を表す記号ではないことに注意してください.

ヒント

最後の数行がどの行から開始すれば良いのかは最初はわかりません. そのため,まずは,全ての行を ArrayListに保存しましょう.

全ての行を保存すると,必要な行を出力するためのインデックスは(全行数-指定行数)から始めれば良いとわかります. そこから最終行までを出力してください.

参考

8. シェルピンスキーのギャスケット

実行例のような図形を描きましょう. このような図形をシェルピンスキーのギャスケットと呼びます. デフォルトでは,$n=3$とし,コマンドライン引数により,$n$を指定できるようにしてください. クラス名は SierpinskiGasket としてください.

実行例

上の画像をクリックすると$n=0$から$n=5$まで変化します.

ヒント

全体の処理内容

  1. まず,一番外側の三角形を描きましょう.
    • $(x_1, y_1)=(10.0, 380.0), (x_2, y_2)=(390.0, 380.0), (x_3, y_3)=(200.0, 131.3)$
    • この三角形の高さは$\frac{|x_2-x_1|}{2}\sqrt{3}$で求められます.そのため,$y_3$400 - (390-10)/2 * Math.sqrt(3) で求められます.
  2. 次に,drawGasketメソッドに先ほどの頂点3点と $n$を渡します.
  3. drawGasketの処理は次の通りです.
    1. $n$が0の場合は何もせずに処理を終わらせます.
    2. 各頂点の中点を求めます.
      • 中点は $(\frac{x_1 + x_2}{2}, \frac{y_1 + y_2}{2})$で求められます.
    3. 求めた中点を結ぶ三角形を描きます.
    4. $n$の値を1だけ減らします.
    5. 左下の三角形に対して,drawGasketを呼び出します.
    6. 右下の三角形に対して,drawGasketを呼び出します.
    7. 上の三角形に対して,drawGasketを呼び出します.

次のような型,メソッドを用意すると良いでしょう.

  • 座標を表す型 Point型.
  • 2つのPoint型変数を受け取り,中点となるPoint型変数を返す midpointメソッド.
  • 2つのDouble型を受け取り,Point型変数を返すpointメソッド.
  • Graphics2D で直線を引くには,g.draw(new Line2D.Double(x1, y1, x2, y2)) のように,Line2D.Double の実体をdrawメソッドに渡します.
    • Line2D.Double を利用するには java.awt.geom.Line2D をインポートする必要があります.
  • Graphics2Dで文字列を描画するには,g.drawString("some string", x, y) を呼び出します.
  • Graphics2Dで色を変更するには,drawなどを呼び出す前に g.setColor(Color.RED) などで色を設定しましょう.

参考

9. Gzipによる圧縮.

コマンドラインで与えられたファイル(複数可)をgzipを用いて圧縮してください. クラス名は GZip としてください.

gzipの圧縮には,GZIPOutputStreamが利用できます.GZIPOutputStream を利用するには,java.util.zip.GZIPOutputStreamのインポートが必要です.

実行結果

$ ls
GZip.java       GZip.class
$ java GZip GZip.java # GZip.java を圧縮する.
GZip.java: 1152 bytes -> 426 bytes (36.98%)
$ ls
GZip.java       GZip.java.gz    GZip.class # Gzip.java.gz が作成されている
$ mv GZip.java GZip.java.back # Gzip.java の名前を変更する.
$ gunzip GZip.java.gz         # Gzip.java.gz を解凍し,Gzip.javaを得る.
$ ls
GZip.java       GZip.java.back  GZip.java.gz    GZip.class
$ java GZip GZip.java GZip.class
GZip.java: 1152 bytes -> 426 bytes (36.98%)
GZip.class: 1708 bytes -> 1010 bytes (59.13%)

ヒント

  • FileOutputStreamGZIPOutputStreamでラップすれば圧縮が行えます.
    • FileWriterPrintWriterでラップするのと同じ要領でプログラムを書いてください.
    • あとは入力ストリーム(InputStream)から1バイトずつ読み込み,出力ストリームに書き出してください.
      • 読み込みは,InputStream型のreadメソッド(引数なし)を用いてください.1バイト単位で読み込みます.
      • また,書き出しは GZIPOutputStreamwriteメソッドを用いてください.読み込んだ1バイトを書き出せば良いです.
  • 出力するのはバイナリファイルですので,Reader/Writerではなく InputStream/OutputStreamを利用してください.
  • ファイルに書き出せれば,圧縮前後のファイルサイズを調べて,圧縮率を算出してください.
    • ファイルサイズは,File型変数のlength()メソッドで調べられます.

参考

10. 逆ポーランド記法の計算機

逆ポーランド記法での計算機を作成しましょう. 逆ポーランド記法とは,後置記法とも呼ばれます. 中置記法と呼ばれる通常の記法は,1 + 2 ですが,これを逆ポーランド記法で書くと,1 2 +となります. コマンドライン引数で,このような計算式を受け取り,計算結果を出力するプログラムを書いてください.

数値は全てDoubleで扱ってください. クラス名は,ReversePolishNotationCalculatorとしましょう.

実行例

$ java ReversePolishNotationCalculator "1 2 +"
3.000000 (1 2 +)
$ java ReversePolishNotationCalculator "1 2 +" "3 4 +"
3.000000 (1 2 +)
7.000000 (3 4 +)
$ java ReversePolishNotationCalculator "1 2 + 9 6 - *"
9.000000 (1 2 + 9 6 - *)

引数で受け取った文字列を逆ポーランド記法で書かれた数式と捉えて,計算してください. 計算結果を出力後,同じ行に計算の元となった数式を1行で出力してください. なお,演算子と数値の間は必ずスペースで区切られているという前提で計算してください.

複数の計算式が指定された場合,一つの数式を1行で出力するようにしてください.

なお,逆ポーランド記法で与えた計算式を中置記法で表すと,次の通りです.

  • 1 2 +1 + 2
  • 3 4 +3 + 4
  • 1 2 + 9 6 - *(1 + 2) * (9 - 6)

ヒント

与えられた文字列をスペースで区切って,最初から順に処理していきましょう. 今,見ている文字列(スペースを含まない)が数値の場合は,ArrayListに追加しましょう. 数値でなく演算子の場合は,ArrayListの後ろから2つの数値を取り出し(ArrayListから削除し), 演算子に従った処理を行って,計算結果をArrayListに追加しましょう.

上記の繰り返しで逆ポーランド記法の計算機は実現できます. 上記の処理内容は,実は,スタック(Stack)そのものです.

参考

11. 2つの文字列間の類似度・距離

コマンドライン引数で与えらえた2つの文字列の類似度を次の5つの方法で求めてください.

  • Simpson係数
  • Jaccard係数
  • Dice係数
  • コサイン類似度
  • 編集距離(Levenshtein距離)

クラス名は,StringSimilaritiesとしてください.

Simpson係数(Simpson Index)

2つの集合があったとき,下図のように,両方の集合の要素数のうち短い方で積集合を割ったものが Simpson係数です.

Simpson Index Simpson Index

文字列(String型)は,Character型の集合と考えられます. そのため,2つの文字列に含まれる文字種別の数,積集合を計算できれば,Simpson係数が求められます.

なお,それぞれの要素数は文字の長さではなく,文字列から重複を取り除いた長さを用いる点に注意してください. つまり,androidという文字列の長さは,7ですが,重複を取り除いた時の長さは6になります (dが2つ含まれています).この6で計算してください.

Jaccard係数(Jaccard Index)

2つの集合があったとき,右図のように積集合を和集合で割った値がJaccard係数です.

Jaccard Index Jaccard Index

文字列(String型)は,Character型の集合と考えられます. そのため,2つの文字列の積集合,和集合を計算できれば,Jaccard係数が求められます.

Simpson係数と同じく,文字列の長さではなく,重複を取り除いた長さを用いてください.

Dice係数(Dice Index)

2つの集合があったとき,下図のように積集合の2倍の数を,集合の要素数の和で割ったものが Dice係数です.

Dice Index Dice Index

文字列(String型)は,Character型の集合と考えられます. そのため,それぞれの文字列に含まれる文字種別の数,積集合を計算できれば,Dice係数が求められます.

Simpson係数と同じく,文字列の長さではなく,重複を取り除いた長さを用いてください.

コサイン類似度(Cosine similarity)

2つのベクトルを考え,そのベクトルの成す角のコサインがコサイン類似度です. 文字列を文字を要素とするベクトルと考え,2つの文字列から2つのベクトルを構築します.

2つのベクトル $\vec v_1$,$\vec v_2$があるとき,2つのベクトルのコサインは, $\cos \theta =(\vec v_1 \cdot \vec v_2)/(|\vec v_1||\vec v_2|)$で求められます. 文字列から,文字の頻度を計算し,それをベクトルとして扱えば,コサイン類似度が求められます.

さて,"value1""value2" の2つの文字列を考えてみましょう. 2つの文字列からそれぞれベクトルを作成して,$\vec{v_1}= \{ 1,1,1,1,1,1,0 \}$, $\vec{v_2} = \{ 1,1,1,1,1,0,1 \}$とします. ベクトルの各要素は, {'v', 'a', 'l', 'u', 'e', '1', '2'}としています. $\cos\theta$を求める式に当てはめると,$\frac{5}{\sqrt{6}\sqrt{6}}$となり,"value1""value2" のコサイン類似度は,0.83333…と計算できます.

また,別の例として,"clock""watch" の2つの文字列を考えてみましょう. これらから,ベクトルの要素を数えると,$\vec{v_1}= \{ 0, 2, 0, 1, 1, 1, 0, 0 \}$, $\vec{v_2} = \{ 1, 1, 1, 0, 0, 0, 1, 1 \}$です. ベクトルの各要素は, {'a', 'c', 'h', 'k', 'l', 'o', 't', 'w'}としています. $\cos\theta$を求める式に当てはめると,$\frac{2}{\sqrt{7}\sqrt{5}}$ となり,"clock""watch" のコサイン類似度は,0.338062…と計算できます.

編集距離(Edit Distance; Levenshtein Distance)

編集距離(Levenshtein距離)とは,2つの文字列がどの程度異なっているかを表す距離のことを指します. 一方の文字列から,もう一方の文字列に変更するまでに必要な,1文字の挿入,削除,置換の最小手順で距離を定義しています.

例えば,次の手順を行えば,"distance"から "similarity"に変更できます. この場合,8回の手順で変更できたため,編集距離は 8 となります.

  1. "distance"
  2. "sistance".(1文字目のdsに置換)
  3. "simtance".(3文字目のsmに置換)
  4. "simiance".(4文字目のtiに置換)
  5. "similance".(5文字目にlを追加)
  6. "similarce".(7文字目のnrに置換)
  7. "similarie".(8文字目のciに置換)
  8. "similarit".(9文字目のetに置換)
  9. "similarity".(10文字目にyを追加)

実際に長さ$n$と$m$の文字列$S_n, S_m$間の編集距離を計算するには, $(n+1)(m+1)$の表が必要になります. この表のことを編集グラフ(Edit graph)と呼びます.

  • 表の初期化処理は次の通りです.
    • $n=0$の$i$列目に$i$を代入します.
    • $m=0$の$j$行目に$j$を代入します.
    • 結果として,一番上の行,一番左の列のみに数値が入れられた表が得られます. 値は,右(下)になるにつれ1ずつ増えていきます.
  • 表の空いている場所に次のルールに従って値を埋めていきます.
    • 注目しているセルの上,左のセルの値に1を足し,$d_1$, $d_2$とします.
    • $S_n$の$i$番目の文字と$S_m$の$j$番目の文字を比較します. 異なっていればコストを1,同じであれば,コストを0とします.
    • 注目しているセルの左上のセルに,先ほど求めたコストを足します($d_3$とします)
    • $d_1$,$d_2$,$d_3$を比べ,一番小さな値を注目しているセルの値として更新します.
    • 上記をすべてのセルに対して行います.
  • すべての値が埋められた表が得られたとき,一番右下の値が編集距離です.

表の作成イメージは以下の通りです. まず,0行目,0列目を埋めます. その後,空いているセルを左上から順に埋めていきます. 当該セルの値は,上,左,左上の3つのセルの値から求められます. 基本的には,上,左,左上のセルの値に1を加算した値を求めます. ただし,それぞれの文字列の対応する文字が一致した場合,左上のセルの値をそのまま利用します(1の加算は行いません). その上で,3つの数値を比較し,一番小さな値がそのセルの値として表が更新されます.

実行例

$ java StringSimilarities distance similarity
simpson(distance, similarity)=0.500000
jaccard(distance, similarity)=0.333333
dice(distance, similarity)=0.500000
cosine(distance, similarity)=0.530330
edit_distance(distance, similarity)=8
$ java StringSimilarities android ipodtouch
simpson(android, ipodtouch)=0.500000
jaccard(android, ipodtouch)=0.272727
dice(android, ipodtouch)=0.428571
cosine(android, ipodtouch)=0.502519
edit_distance(android, ipodtouch)=7

ヒント

文字列から重複なしの文字の集合を取得する.

以下のプログラムで,文字列から重複なしの文字の集合を取得できます.

ArrayList<Character> getList(String item){
    ArrayList<Character> list = new ArrayList<Character>();
    for(Integer i = 0; i < item.length(); i++){
        Character c = item.charAt(i);
        if(!list.contains(c)){
            list.add(c);
        }
    }
    return list;
}

表の作成

表の作成には,Table.javaを利用してくださいTable.java は左の1行目のように,どんな型を入れるTableにするか, 何行×何桁のTableにするかを決めます. そして,値を設定するには,setメソッドに値とx, yのインデックスを指定します. 値を取得するときは,x, yのインデックスを指定して取得します.

Table<String> table = new Table<String>(41, 21);
table.set("X", 0, 1);
String item = table.get(0, 1);

参考

第12, 13講 応用2

2023-07-20 (Thu) 10:30 までにcpコマンドのプログラムを作成し,提出してください. 以下のステップが完成するごとに提出してください.

ただし,ステップ2のプログラムは,ステップ1を含んでいなければいけません. 同じように,ステップ6はステップ1〜5の全てが実行できなければいけません.

この講で利用するプログラム

  1. ファイルをコピーする
  2. 複数ファイルをディレクトリへコピーする
  3. オプション解析を行う
  4. ファイルの上書きを確認する
  5. 出力先のファイルが新しい場合はコピーしない
  6. ディレクトリを再帰的にコピーする

第12, 13講 応用2のサブセクション

cpコマンドの実装

  1. ファイルをコピーする
  2. 複数ファイルをディレクトリへコピーする
  3. オプション解析を行う
  4. ファイルの上書きを確認する
  5. 出力先のファイルが新しい場合はコピーしない
  6. ディレクトリを再帰的にコピーする

1. ファイルをコピーする.

UNIXのcpコマンドのように,java Copy1 from_file to_file を 実行すると,from_fileの内容がto_fileにコピーされるようなコマンドCopy1を 作成してください.to_fileが存在している場合は上書きしてください (単純にFileWriterで書き込むと上書きすることになります).

コマンドライン引数に必要な数のファイルが指定されない場合,エラーメッセージを出力しましょう. コマンドライン引数で指定されたものは,ファイルであるとしても構いません.

ヒント

// 必要な import 文を書いてください.
public class Copy1{
    void run(String[] args) throws IOException{
        // コマンドライン引数に必要な分のファイルが指定されているか確認する.
        //     args.lengthが2より小さい場合,必要な引数が指定されていない旨を出力して終了する.
        // argsの0番目,1番目の要素からそれぞれFile型の実体を作成する.
        // copyメソッドを呼び出す.
    }
    void copy(File from, File to) throws IOException{
        // fromをBufferedReader(FileReader)で開く.
        // toをPrintWriter(FileWriter)で開く.
        //     fromから読み込んだ内容をtoに書き出す.
        //     ファイルの終わりまでこの処理を繰り返す.
        // from, to から開いたストリームを閉じる.
    }
    // mainメソッドは省略.
}

実行例

$ echo 'abcdef' > file1  # file1 を作成する.
$ ls file2               # file2 が存在しないことを確認する.
ls: file2: No such file or directory
$ java Copy1 file1 file2 # file1 を file2 にコピーする.
$ cat file2              # file2 と file1 の内容が同じであることを確認する.
abcdef
$ java Copy1             # コマンドライン引数がない場合にエラーを出して終了する.
cp: コマンドライン引数には,少なくとも,コピー元,コピー先を指定する必要があります.
$ java Copy1 file2       # コマンドライン引数が足りない場合にエラーを出して終了する.
cp: コマンドライン引数には,少なくとも,コピー元,コピー先を指定する必要があります.

参考

2. 複数ファイルをディレクトリへコピーする

1. ファイルをコピーするでは,コマンドライン引数は2つ,かつ両方がファイルであることが前提でした. ここでは,複数ファイルを一つのディレクトリにコピーするプログラムCopy2を作成します.

java Copy2 from_file1 from_file2 from_file3 directory

を実行すると,from_file1from_file2from_file3がディレクトリにコピーされます. すなわち,directory/from_file1directory/from_file2directory/from_file3が作成されます. このような挙動を行うプログラムを作成してください.

まず,directory/from_file1に書き込むためには,directory/from_file1を表すFile型の実体を作成しなければいけません. そのためにまず,File型の変数toが出力先ディレクトリであるdirectoryを表し, 出力先のファイル名であるfrom_file1File型の変数fromが表していると仮定します. このとき,new File(to, from.getName()) という新たなFileの実体を 作成することで,directory/from_file1を表すFile型の実体を作成できます.

コマンドライン引数の一番最後の要素がディレクトリであるか否かを判定してください. 一番最後の要素が存在し,ディレクトリである場合のみ,その前のコマンドライン引数が示すファイルの内容をディレクトリ以下にコピーしてください.

コマンドライン引数の一番最後の要素が存在しない場合,もしくは,ファイルであった場合は, 複数ファイルのコピーは行えませんので,エラーメッセージを出力し,終了してください.

ヒント

    void run(String[] args){
        // コマンドライン引数に必要な分のファイルが指定されているか確認する.
        //     args.lengthが2より小さい場合,必要な引数が指定されていない旨を出力して終了する.
        // 出力先は,コマンドライン引数の一番最後の要素である.
        File to = new File(args[args.length - 1]);
        if(.....){ // toが存在しない,もしくはファイルの場合.
            // args.length が 2 より大きい場合,
            //     複数ファイルを1つのファイルにコピーできない旨を出力し,終了する.
            // そうでない場合,copyメソッドを呼び出す.
        }
        else if(....){ // toがディレクトリの場合.
            for(....){ // argsを必要なだけ繰り返す.一番後ろの要素は省くことを忘れない.
                File from = new File(arg[i]);
                File toFile = new File(to, from.getName());
                // fromをtoFileにコピーするよう copy メソッドを呼び出す.
            }
        }
    }

実行例

$ for i in 1 2 3; do echo "file$i" > "file$i.txt" ; done # ファイルを作成する.
$ ls file?.txt
file1.txt    file2.txt    file3.txt
$ mkdir dir
$ java Copy2 file1.txt file2.txt dir
$ ls dir                         # dir の内容を確認する.
file1.txt    file2.txt
$ java Copy2 file3.txt file4.txt # file3.txt を file4.txt にコピーする.
$ cat file4.txt
file3
$ java Copy2 file2.txt file3.txt file5.txt
cp: 複数ファイルを一つのファイルにコピーできません.

参考

3. オプション解析を行う

オプション解析を行うようにプログラムを改良しCopy3を作成してください. オプションとは,コマンドに渡す -h などの-から始まる文字(文字列)です. オプションによってプログラムの挙動が少し変わります.

最終的に 6. ディレクトリを再帰的にコピーするを終えると, 次のようなオプションを含んだプログラムが完成することになります.

$ java Copy3 -h
Usage: java Copy3 [OPTIONS] from_file to_file
       java Copy3 [OPTIONS] from_file ... to_directory
OPTIONS
     -h: このメッセージを表示して終了する(help).
     -i: コピー先のファイルが存在していた時,ユーザに上書き確認を求める(interactive).
     -r: ディレクトリを再帰的にコピーする(recursive).
     -u: コピー先のファイルが存在しない場合,もしくはコピー元のファイルの方が新しい場合のみコピーする(update).
     -v: 実行内容を表示する(verbose).

ヒント1. オプション解析

オプション解析を行うために,Arguments.javaを作成してください.Arguments には,Boolean型の5つの変数,helpinteractiverecursiveupdate, そして,verboseと,ArrayList<String>型のlistの6つのフィールドを 持たせてください.listは宣言時に初期化を行うようにしてください(newを使いArrayList<String>の 実体を作成し,代入しておいてください).Boolean型の変数も 全て falseで初期化しておいてください.

次の parseメソッドを Argumentsに定義してください.

    void parse(String[] args){
        for(String arg: args){
            if(Objects.equals(arg, "-h")){
                this.help = true;
            }
            else if(Objects.equals(arg, "-i")){
                this.interactive = true;
            }
            else if(Objects.equals(arg, "-r")){
                this.recursive = true;
            }
            else if(Objects.equals(arg, "-u")){
                this.update = true;
            }
            else if(Objects.equals(arg, "-v")){
                this.verbose = true;
            }
            else{
                this.list.add(arg);
            }
        }
    }

ヒント2. オプション解析とヘルプの表示

次に,Copy3クラスのrunメソッドを次のように定義しましょう.

    void run(String[] args){
        Arguments arguments = new Arguments();
        arguments.parse(args);
        if(arguments.help){
            this.printHelp();
        }
        else{
            this.performCopy(arguments);
        }
    }

このメソッドのでは,-hオプションが指定された時,printHelpメソッドを呼び出すようになっています. 上記に示したヘルプメッセージを表示するようprintHelpメソッドの処理を書いてください. 単純に上に示したメッセージをSystem.out.printlnで出力すれば良いでしょう.

また,performCopyステップ2までの処理(run)を基本として 書き直してください.String型の配列であるargsではなく,ArrayList<String>から 要素を取り出す必要があります.

ヒント3. verboseオプションが指定された場合の処理

また,-vオプション(verbose)が指定された場合,どのファイルがどこにコピーされたのかを表示するようにしてください. 次のような処理になるでしょう.「コピー処理」部分はfromからtoへのコピーの処理に置き換えてください.

    void copy(File from, File to, Arguments args) throws IOException{
        // コピー処理
        // ...
        if(args.verbose){
            System.out.printf("%s -> %s%n", from.getPath(), to.getPath());
        }
    }

実行例

$ for i in 1 2 3 ; do echo "file$i" > "file$i.txt" ; done
$ java Copy3 -v file1.txt hoge.txt                 # 実行状況を確認してコピーする.
file1.txt -> hoge.txt
$ mkdir dir2
$ java Copy3 -v file1.txt file2.txt file3.txt dir2 # 実行状況を確認してコピーする.
file1.txt -> dir2/file1.txt
file2.txt -> dir2/file2.txt
file3.txt -> dir2/file3.txt

参考

4. ファイルの上書きを確認する.

ファイルの上書き確認を行うようプログラムを改良し,Copy4を作成しましょう. コピー先のファイルが存在し,ディレクトリでなく通常のファイルの場合(to.exists() && to.isFile()) ユーザに上書きして良いかの確認を取りましょう.

ヒント

このプログラムを実現するには,copyメソッドの最初に,isOverwriteメソッドを呼び出し, 上書きするかを判定します.isOverwriteメソッドでは,まずチェックすべきかどうか,すなわち, 出力先が存在しており,かつ,ファイルであるかを確認しています. その上で,チェックすべきである場合は,interactiveオプションが指定されており, ユーザが上書きしない場合に,isOverwritefalseを返すようにしています. なお,ユーザへの確認処理はisOverwriteAskToUserメソッドで行っています.

なお,SimpleConsole.java第9回目 練習問題2. 電話帳の作成と同じものを利用してください. SimpleConsole.javaはこの文章をクリックすることでもダウンロードできます

また,実際にコピーを行う処理をcopyメソッド内からdoCopyメソッドに移しています.

    Boolean isOverwriteAskToUser(File to) throws IOException{
        SimpleConsole console = new SimpleConsole();
        while(true){
            System.out.printf("%sを上書きしますか? (y/n [n]) ", to.getName());
            String line = console.readLine();
            line = line.trim();  // 入力された前後の空白を取り除く.
            if(Objects.equals(line, "y")){
                return true;
            }
            else if(Objects.equals(line, "") || Objects.equals(line, "n")){
                // nもしくは単に改行された場合
                System.out.println("上書きしません.");
                return false;
            }
            else{ // その他の値が入力された場合.
                System.out.println("yかnを入力してください.");
            }
        }
    }
    Boolean isOverwrite(File to, Arguments args) throws IOException{
        if(to.exists() && to.isFile()){
            if(args.interactive && !isOverwriteAskToUser(to)){
                return false;
            }
        }
        return true;
    }
    void copy(File from, File to, Arguments args) throws IOException{
        if(isOverwrite(to, args)){
            this.doCopy(from, to);
            // verboseの処理
        }
    }
    void doCopy(File from, File to){
        // コピー処理
        // ...
    }

実行例

$ for i in 1 2 3 ; do echo "file$i" > "file$i.txt" ; done
$ java Copy4 file1.txt file2.txt
$ cat file2.txt
file1
$ java Copy4 -i file1.txt file3.txt    # file3.txt が存在すれば上書き確認を行う.
file3.txtを上書きしますか? (y/n [n]) aaaa # 再入力可能かを確認するため,aaaa を入力する.
yかnを入力してください.
file3.txtを上書きしますか? (y/n [n]) n    # nを入力して,上書きしない.
上書きしません.
$ cat file3.txt                        # 内容が書き換わっていないことを確認する.
file3
$ java Copy4 -i -v file1.txt file3.txt
file3.txtを上書きしますか? (y/n [n]) y    # yを入力した.上書きコピーを実行する.
file1.txt -> file3.txt
$ cat file3.txt                        # 内容が書き換わったことを確認する.
file1

参考

5. 出力先のファイルが新しい場合はコピーしない

ファイルの更新日時を確認し,出力先のファイルの方が新しければコピーしないようにしましょう.Copy4を コピーしてCopy5を作成し,処理を追加しましょう. 先ほど作成したisOverwriteメソッドを修正して,この確認を行うようにしましょう.

ヒント

    Boolean isOverwrite(File from, File to, Arguments args) throws IOException{
        if(to.exists() && to.isFile()){
            Boolean overwriteFlag = true; // デフォルトは上書きする.
            if(args.update){              // updateオプションが指定された場合
                // toの方が新しい場合,上書きしない.
                overwriteFlag = ...       // 上書きしない.
            }
            // 上書き,かつ,インタラクティブであれば,ユーザに上書きの可否を問い合わせる.
            // ユーザが上書きを指示しなければ上書きしない.
            if(overwriteFlag && args.interactive && !isOverwriteAskToUser(to)){
                overwriteFlag = false;
            }
            return overwriteFlag;
        }
        return true;
    }

なお,Copy4で作成したisOverwriteメソッドに新しい条件を追加しています. 条件が複数になったため,overwriteFlagというBoolean型の変数で上書き確認を行なっています.

実行例

$ for i in 1 2 3 ; do echo "file$i" > "file$i.txt" ; done
$ touch file1.txt                         # file1.txt の最終更新日時を更新した.
$ java Copy5 -u -v file1.txt file3.txt    # file3.txt の方が古いため,コピーする.
file1.txt -> file3.txt
$ java Copy5 -u -v file2.txt file1.txt    # file1.txt の方が新しいため,コピーしない.
$ touch file2.txt
$ cat file2.txt
file2
$ java Copy5 -u -v -i file2.txt file1.txt # file1.txt の方が古いため,コピーする.
file1.txtを上書きしますか? (y/n [n]) n
上書きしません.
$ java Copy5 -u -v -i file2.txt file1.txt # file1.txt の方が古いため,コピーする.
file1.txtを上書きしますか? (y/n [n]) y
file2.txt -> file1.txt
$ cat file1.txt
file2

参考

6. ディレクトリを再帰的にコピーする

今までは,ファイルを出力先のファイル,もしくはディレクトリにコピーするコマンドを作成しました. このステップでは,ディレクトリをコピーする処理を追加します.Copy5をコピーし,Copy6を作成してください.

ステップ2のヒントで作成したrunメソッドでは, 出力先(to)が,存在しない,もしくはファイルの場合と,出力先がディレクトリに場合に場合分けを行い, それぞれで処理を実行していました.

ヒント

ディレクトリを再帰的にコピーするには,copyRecursiveメソッドを用意します. そこで,ディレクトリ内のファイル,ディレクトリを調べ,ファイルであれば, すでに作成済みであるcopyメソッドを呼び出します. ディレクトリであれば,copyRecursiveの再帰呼び出しを行います.

ここで,copyメソッドを呼び出すとき,出力先となるFile型の変数に気をつける必要があります. 例えば,fromaaaが指定され,その中のaaa/bbb/file.txtdest にコピーするとき,単純に new File(to, file) とすると,dest/aaa/bbb/file.txt に出力されることになります. 本来の出力は,dest/bbb/file.txt であるはずです.そのファイル名を取得しているのが,createToFileです.

    void performCopy(Arguments args){
        // コマンドライン引数に必要な文のファイルが指定されているか確認する.
        // 出力先は,コマンドライン引数の一番最後の要素である.
        File to = new File(args.list.get(args.list.size() - 1));
        if(.....){ // toが存在しない,もしくはファイルの場合.
            this.copyToFile(args, to);
        }
        else if(....){ // toがディレクトリの場合.
            this.copyToDirectory(args, to);
        }
    }
    void copyToDirectory(Arguments args, File to){
        for(....){ // argsを必要なだけ繰り返す.一番後ろの要素は省くことを忘れない.
            File from = new File(args.list.get(i));
            // fromがファイルの場合
                File toFile = new File(to, from.getName());
                // copyメソッドを呼び出し,コピーする.
            // コマンドライン引数に recursive が指定された場合.
                // copyRecursiveメソッドを呼び出す.
                this.copyRecursive(from, from, to, args);
            else{  // その他の場合
                System.out.printf("cp: %sはディレクトリです(コピーしません)%n", from.getName());
            }
        }
    }
    void copyToFile(Arguments args, File to){
        // args.list.size() が 2 より大きい場合,
        //     複数ファイルを1つのファイルにコピーできない旨を出力し,終了する.
        // そうでない場合,fromがディレクトリか否かを調べる.
        else{
            File from = new File(args.list.get(0));
            if(from.isDirectory()){  // from がディレクトリの場合.
                // toがファイルの場合
                    System.out.println("cp: ディレクトリをファイルにコピーできません.");
                // コマンドライン引数に recursive が指定された場合.
                    // copyRecursiveメソッドを呼び出す.
                    this.copyRecursive(from, from, to, args);
                else{  // その他の場合
                    System.out.printf("cp: %sはディレクトリです(コピーしません)%n", from.getName());
                }
            }
            else{  // fromがファイルの場合.
                this.copy(from, to, args);
            }
        }
    }
    ....
    void copyRecursive(File base, File from, File to, Arguments args) throws IOException{
        for(File file: from.listFiles()){
            if(file.isDirectory()){
                // copyRecursiveメソッドを再帰呼び出し.
            }
            else{
                File toFile = this.createToFile(base, file, to);
                // toFile の親ディレクトリ(File型)を取得する(toFile.getParentFile()).
                // toFile の親ディレクトリが存在しない場合作成する(parent.mkdirs()).
                this.copy(file, toFile, args);
            }
        }
    }
    File createToFile(File base, File from, File to){
        String basePath = base.getPath();
        String fromPath = from.getPath();
        String newPath = fromPath.substring(basePath.length() + 1);
        return new File(to, newPath);
    }

実行例

$ mkdir dir1
$ for i in 1 2 3 ; do echo "file $i in dir1" > dir1/file$i.txt ; done
$ ls dir1
file1.txt  file2.txt  file3.txt
$ ls dir2
ls: dir2: No such file or directory
$ java Copy6 dir1 dir2
cp: dir1はディレクトリです(コピーしません)
$ java Copy6 -r -v dir1 dir2
dir1/file1.txt -> dir2/file1.txt
dir1/file2.txt -> dir2/file2.txt
dir1/file3.txt -> dir2/file3.txt

参考

第14講 最終課題

第14講 最終課題のサブセクション

概要

目標

あるデータが与えられますので,そのデータを分析し,結果を出力するプログラムを作成します. いくつかのステップが与えられるので,各ステップで指定された分析を行ってください.

参考資料

課題の進め方

この課題は大きく6つのステップに分けられます. それぞれのステップで ScoreAnalyzer1.javaScoreAnalyzer6.java を作成します. 以下のことを念頭に課題を進めてください.

  • ステップ1からステップ2ステップ3と順番に課題を進めてください. 途中のステップのスキップはできません.
  • ステップ1で作成した内容を元に, ステップ2を作成してください.
    • ステップ3以降も同様に,前のステップで作成した内容を全て含めて当該ステップに取り組んでください.
    • プログラムを作成するとき,一つ前のステップのプログラムをコピーして始めると良いでしょう.
      • ただし,mainメソッドの内容を修正することを忘れないようにしましょう.
      • mainメソッドで異なる型をnewするバグはなかなか気付きません.
  • ステップ3までが必須で,ステップ4ステップ5ステップ6がチャレンジ問題です.
    • ただし,必須問題しか完成させていない場合,最高でも60点にしかならないため,試験で失敗すると単位取得が厳しくなります.
    • ステップ4までを完成させていれば,たとえ試験で失敗したとしても単位取得が現実的になります.
      • もちろん,失敗の度合いもありますし,ステップ4まで完成させたつもりが完成できていないこともありますので,確実に単位取得を保証する訳ではありません.

課題の提出方法

この課題は6つのステップに分けられています. 各ステップでScoreAnalyzer1.javaScoreAnalyzer6.javaを作成します. 具体的に指示はしていませんが,独自の型を作成する必要もあるでしょう. それら全てのソースファイルを Moodle に提出してください.

  • 提出期限は 2024-08-05 (Mon) 9:00です.
  • 提出先は Moodle の【2024-08-05 (Mon) 9:00〆切】最終課題提出場所 です.
  • 次のチェックリストを提出前に確認してください.
    • 関係するソースファイルをすべて提出しているか.
    • 提出したソースファイルのみでコンパイルに成功するか.
    • 全てのソースファイルにコメントとして,自分の学生証番号,名前が記載されているか.

課題のデータ

課題には次のデータを利用してください. なお,評価には,このページからダウンロードできるデータとは異なるものを利用します. データの形式は同じですが,記載されているデータやデータ量は異なります. しっかりとデータを読み,適切な分析を行うようにして下さい. なお,多少の計算誤差は許容されます. また,与えられるデータはソートされているとは限りません.

  • この課題で利用するデータです
  • これらのデータはとある講義の小テストの結果を示したものです.
    • ランダムに並び替えた上でIDを付け直していますので,個人を特定できないようにしています.

評価項目

以下の点を満たしていれば,加点されます. また,それぞれのステップで確認事項があります.それぞれを満たすことで加点されていきます.

  • インデントがずれている部分がないこと.
    • 少しでもインデントがずれているとNG.
    • Visual Studio Code の場合,Option + Shift + F でインデントすること.
  • ループ制御変数以外で1文字の変数名を利用していないこと.
  • 1つのメソッドが20行以内であること.
    • メソッド開始の{と終了の}は含まない.
  • 3つ以上のネストが存在しないこと.
    • 2重ループ内の条件分岐はアウト.別のメソッドに切り出しましょう.
  • フィールド,ローカル変数の数がそれぞれ5つ以下であること.
    • ローカル変数の数は,ここでは,メソッド内に定義されている全ての変数の数とします.
    • ただし,メソッドの引数は含みません.
  • 配列を使っていないこと.ただし,以下の部分は除きます.
    • mainメソッドの引数,および,その変数を他のメソッドに渡したときの引数,
    • splitメソッドの返り値および,その変数を他のメソッドに渡したときの引数.
  • クラス定義の基本形に従ってプログラムを書いていること.

最終課題

ステップ0

以下のステップを実行する前に,概要課題のデータから必要なデータをダウンロードしておいてください.

reading.csvwriting.csvは同じフォーマットのデータで,左カラムから順に以下のデータが書かれています.

  • 小テストが行われた日付
  • reading/writingの区別
  • 課題番号
  • 学生ID
  • 点数
  • 開始時刻
  • 提出時刻

ステップ1

1-A. 問題説明

コマンドライン引数で問題番号とデータファイルが指定されます. 指定されたデータファイルを読み,指定された問題番号のスコアの頻度(%)を出力してください. コマンドライン引数で与えられるデータは必ず1つであり,正しいフォーマットのファイルが渡されると仮定して構いません.

1-B. 実行例

$ java ScoreAnalyzer1 1 reading.csv # 問題番号1のスコアの頻度を出力する.
  :  2.308 ( 3/130) # 問題番号1を時間内に提出できなかった学生が3名(2.308%)いる.
 6:  7.692 (10/130) # 問題番号1で6点をとった人が130名中10名(7.692%)いる.
 8: 20.000 (26/130)
10: 70.000 (91/130)
$ java ScoreAnalyzer1 2 reading.csv
 0:  4.615 ( 6/130)
  :  0.769 ( 1/130)
 2: 10.000 (13/130)
 4: 24.615 (32/130)
 6: 31.538 (41/130)
 8:  6.923 ( 9/130)
10: 21.538 (28/130)
$ java ScoreAnalyzer1 4 reading.csv
  :  0.800 ( 1/125)
 0:  2.400 ( 3/125)
 2:  4.800 ( 6/125)
 4:  5.600 ( 7/125)
 6: 24.800 (31/125)
 8: 42.400 (53/125)
10: 19.200 (24/125)
$ java ScoreAnalyzer1 5 writing.csv
 1: 77.419 (72/93)
 3: 11.828 (11/93)
 5: 10.753 (10/93)
  • ファイルが何も指定されなかった場合は考える必要はありません.
  • 違うフォーマットのファイルが指定されることは考慮する必要はありません.
  • 出力の順番は実行例の通りでなくても構いません.

1-C. ヒント

処理の流れ

次の流れで処理すれば良いでしょう.

  1. HashMap<String, Integer>型の変数mapを宣言し,初期化する.
  2. コマンドライン引数で渡された文字列をファイルとして順に処理する.
    1. 1行をコンマ(,)で区切る.
    2. 3番目の要素(インデックスが2)が問題番号であるため,指定された問題番号であるかを確認する.
    3. 指定された問題番号の場合,当該スコアの人数を調べる.つまり,スコア(5番目の要素; インデックスが4)をキーとして人数(バリュー)をmapからgetする.
    4. 当該スコアの人数が0であれば(nullが返されれば),0で初期化する.
    5. 人数を +1 し,再度 mapに登録し直す.
  3. 受験者数を調べる.
    1. mapのバリューの全てを足し合わせることで,受験者数(examineeCount)が数えられます.
  4. HashMap<String, Integer> 型の変数の要素(キーとバリューのペア)を順に繰り返す.
    1. キーがスコア,バリューがそのスコアの人数を表します.
    2. バリューの数値に100.0 / examineeCountを掛けるとそのスコアの割合になります.
      • Integer型のままだと小数点以下が計算されない点に注意してください.

1-D. 評価項目

  • 概要に示した評価項目
  • データを変更しても,例外なく実行結果を出力できるか
    • 人数やスコアの有効値が変更されても例外なく出力されるか.

ステップ2

2-A. 問題説明

問題番号ごとに,スコアの割合を算出してcsv形式で出力してください. 各桁(column)にスコアを並べ,各行(row)には問題番号を並べてください.

2-B. 実行例

次のような出力になっています.

,スコア1,スコア2,....,スコアn
問題1,割合1,割合2,....割合n

上記のようにヘッダ(1行目)には有効なスコアを並べてください. 続いて,問題ごとに,各スコアの割合を出力してください.

$ java ScoreAnalyzer2 writing.csv
,1,3,5
1,,,100.000
2,70.536,14.286,15.179
3,25.893,25.893,48.214
4,41.818,13.636,44.545
5,77.419,11.828,10.753

2-C. ヒント

表形式の値を管理するために,多次元配列を用いるのではなく, HashMap<String, HashMap<String, Integer>> を用いるようにしてください. 次のような構造にすると良いでしょう.

HashMap<String, HashMap<String, Integer>> map = new HashMap<>();
// ... map に値をputしていく.
HashMap<String, Integer> scores = map.get("1");   // 問題番号1のスコア一覧を取得する.
Integer numberOfAInAssignment1 = scores.get("A"); // 問題番号1でAのスコアを取得した人数を取得する

2-D. 評価項目

  • 概要に示した評価項目
  • データを変更しても,例外なく実行結果を出力できるか
    • 人数やスコアの有効値,問題番号の範囲が変更されても例外なく出力されるか.
  • 成績のデータの管理に配列を用いていないこと.

ステップ3

3-A. 問題説明

学生ごとにスコアをまとめて csv として出力してください. また,学生ごと,問題ごとにスコアの最大,最小,平均も出力してください.

3-B. 実行例

次のような出力になっています.

学生ID,課題1の点数,課題2の点数,...., 最大値,最小値,平均点
....
,課題1の最大値,課題2の最大値,...
,課題1の最小値,課題2の最小値,...
,課題1の平均点,課題2の平均点,...
$ java ScoreAnalyzer3 reading.csv # プログラム読解のスコア
88,10,2,4,6,8,10,2,6.000000
89,10,,,,,10,10,10.000000
110,10,4,10,8,10,10,4,8.400000
111,10,4,8,10,10,10,4,8.400000
112,10,8,8,10,10,10,8,9.200000
# 途中省略
83,10,8,0,8,10,10,0,7.200000
84,,6,,,,6,6,6.000000
85,10,6,10,8,0,10,0,6.800000
86,,,,0,,0,0,0.000000
87,10,10,8,8,8,10,8,8.800000
,10,10,10,10,10
,6,0,0,0,0
,9.275591,5.829457,6.047244,7.177419,8.474576
$ java ScoreAnalyzer3 writing.csv # プログラム作成のスコア
88,5,1,1,1,1,5,1,1.800000
89,5,1,1,,,5,1,2.333333
110,5,5,5,5,,5,5,5.000000
111,5,1,5,5,1,5,1,3.400000
112,5,1,5,5,3,5,1,3.800000
# 途中省略
83,5,1,1,1,1,5,1,1.800000
84,5,,,,,5,5,5.000000
85,5,3,1,1,1,5,1,2.200000
86,,,3,5,,5,3,4.000000
87,5,3,5,3,3,5,3,3.800000
,5,5,5,5,5
,5,1,1,1,1
,5.000000,1.892857,3.446429,3.054545,1.666667

3-C. ヒント

条件を満たすために

  • 平均値,最大値,最小値をまとめる型Statsを用意しましょう.
    • フィールドに最大値を表すmax,最小値を表すmin,値の合計値を表すsum,値の数を表すcountを用意しましょう.
    • メソッドに,最大値を返すmax,最小値を返すmin,平均値を返すaverageを定義しましょう.
    • そして,値を追加するputメソッドを用意してください.
      • putメソッドでは,追加された値が最小値,最大値に相当する場合は,それぞれに代入してください.
      • 加えて,sumに値を追加し,countをインクリメントしてください.これらの2つの変数はaverageメソッドで利用します.
  • 次に,1人の学生の成績をまとめる型 StudentScore を用意しましょう.
    • StudentScoreidと課題番号とその評価をキー,バリューとするHashMap型の変数をフィールドとして持つと良いでしょう.
    • また,上記で定義したStats型の変数をフィールドに定義しましょう.
    • そして,評価の最大値を返すmax,最小値を返すmin,平均値を返すaverageメソッドを用意しましょう.
      • それぞれ,Stats型の変数から値を取得します.
    • 最後に,課題番号とその評価を追加するput(String, Integer)を用意し,putで追加された値をStats型の変数にputしましょう.

3-D. 評価項目

ステップ4

4-A. 問題説明

ステップ3の結果に加えて,ステップ3の結果をヒートマップとして出力してください. ヒートマップとは,二次元データの値の大小を色や濃度で表したグラフの一つです. 点数の範囲を調べ,最小値が0,最大値が255となるように点数をスケールさせてください. その計算結果を RGB のどれかに当てはめると良いでしょう(画素の計算方法).

ヒートマップの作成には,グラデーション画像の生成を参照してください. 必要な大きさの BufferedImage を作成し,各画素に対して,setRGB メソッドで色を設定してください. で作成してください. 出力するファイル名は heatmap.pngとしてください.

4-B. 実行例

画像をクリックするごとに赤,緑,青,黄,マゼンタ,シアン,グレー,HSVでヒートマップを作成したものに切り替わります. 色が黒に近いほど点数が低く,白(実際は透明色)は未受験,色が濃くなるほど良い点数であることを表しています. なお,色の種類は自由に決めてもらって構いません(赤,緑,青,黄,マゼンタ,シアン,グレー,HSVのいずれか1つ).

横軸が学生,縦軸が課題を表しています. また,1つの課題の点数に3×3のピクセルを使っています. このように拡大しない場合は,以下の画像の1/3程度の大きさになります.

reading.csv

拡大しない場合の大きさ(赤画素)

writing.csv

拡大しない場合の大きさ(緑画素)

4-C. ヒント

画素の計算方法

Integer maxScore = 10;
Color calculatePixelColor(Integer score){
    if(score == null){
        return new Color(0xff, 0xff, 0xff, 0xff); // 白の透明色
    }
    Double color = Double.valueOf(255.0 * score / maxScore);
    return new Color(value.intValue(), 0, 0); // 赤の場合
}

Color型を利用するには,java.awt.Colorimportする必要があります.

HSV

HSVとは色相(Hue),彩度(Saturation),明度(Value)の3つで1つの色を表す色モデルです. HSB(Hue, Saturation, Brightness)と表現されることもあります. 以下の画像は色相を30度ずつ変更した場合の色の変化です. 色相が0と360は同じ色です. クリックで確認してください.

ヒートマップを作るときに,スコアの点数を色相の0(赤)〜240(青)に割り当てることで,より自然なヒートマップになります. 以下の処理を参考にすると良いでしょう(HSBtoRGBは0.0〜1.0の範囲の数値を受け取ります).

Color calculatePixelColor(Integer score){
    if(score == null){
        return new Color(0xff, 0xff, 0xff, 0xff); // 白の透明色
    }
    Float hue = Float.valueOf(
        (1.0f - 1.0f * score / maxScore) * (240.0f / 360.0f)
    );
    return new Color(Color.HSBtoRGB(hue, 1.0f, 1.0f)); // 赤の場合
}

画素を大きなサイズにする.

最初から画素を大きなものにするのはのではなく,まずは1ピクセルに1課題の点数を割り当てたヒートマップ画像(original_heatmap (BufferedImage型))を作成してください.そして,次の手順のようにして,画素を拡大しましょう. ここでは,1課題を $n \times n$ピクセルに拡大したい場合を考えます.

  1. original_heatmap の大きさの $n$倍のBufferedImageresult_image)を作成する.
  2. result_image から Graphics2D を取得する. Graphics2D g = result_image.createGraphics();
  3. result_imageoriginal_heatmapを拡大して描画する. g.drawImage(original_heatmap, 0, 0, original_heatmap.getWidth() * n, original_heatmap.getHeight() * n, null);

4-D. 評価項目

  • 概要に示した評価項目
  • データを変更しても,例外なく実行結果を出力できるか
    • 人数やスコアの有効値が変更されても例外なく出力されるか.

ステップ5

5-A. 問題説明

各学生が各課題の提出までに要した時間を計算してください. 入力データの6,7カラム目が開始時刻,提出時刻です. この2つから所要時間を計算し,ステップ4の出力に,その時間を追加してください. 各行を次のように出力してください.

学生ID,課題1の点数,課題1の所要時間,課題2の点数,課題2の所要時間,...,点数の最大値,点数の最小値,点数の平均値

上記に加えて,次の内容をオプションで指定できるようにしてください. ただし,提出時間のヒートマップの作成はステップ6で対応するため, ここではオプションで受け取るだけで構いません(ヒートマップの出力部分はステップ4と同じで構いません).

  • ヒートマップの出力先
    • ただし,拡張子と出力フォーマットは一致させてください.
  • ヒートマップの種類
    • 点数のヒートマップか(score),提出時間のヒートマップ(time
      • 提出時間のヒートマップの作成はステップ6で対応するため,オプションで指定可能なようにするだけで構いません.
  • 学生のソート方法
    • ID順(id),成績の平均点順(score),所要時間の平均順(time)のいずれかでソートする(昇順).
  • ヘルプメッセージの表示

なお,ステップ3で作成した独自型(StudentScore)を修正すると,ステップ3が動作しなくなるためStudentScore5に変更しておきましょう.

5-B. 実行例

$ java ScoreAnalyzer5 --help # <= ヘルプメッセージを表示する.
java ScoreAnalyzer5 [OPTIONS] <FILENAME.CSV>
OPTIONS
    --help           このメッセージを表示して終了する.
    --dest <DEST>    ヒートマップの出力先を指定する.
    --sort <ITEM>    指定された項目の昇順でソートする.
    --heatmap <TYPE> ヒートマップの種類を指定する.scoreもしくはtime.
$ java ScoreAnalyzer5 --sort id --dest heatmap_score.png reading.csv
# heatmap_score.png.ppm にヒートマップを出力する.
1,,,,,,,,,10,3,10,10,10.000000
2,10,8,0,9,8,8,6,5,8,5,10,0,6.400000
3,10,5,6,5,4,12,8,5,,,10,4,7.000000
4,6,2,10,7,8,9,8,5,10,8,10,6,8.400000
5,10,4,2,13,10,6,10,8,10,7,10,2,8.400000
# 途中省略
140,6,4,0,1,2,6,6,9,8,7,8,0,4.400000
141,10,2,10,5,6,3,8,3,10,3,10,6,8.800000
142,10,3,10,5,,,8,3,10,1,10,8,9.500000
143,10,6,10,8,8,9,10,11,10,10,10,8,9.600000
144,10,7,6,9,6,8,2,9,10,8,10,2,6.800000
,10,10,10,10,10
,6,0,0,0,0
,9.275591,5.829457,6.047244,7.177419,8.474576
$ java ScoreAnalyzer5 reading.csv --sort score
86,,,,,,,0,0,,,0,0,0.000000
139,8,6,4,11,0,4,2,1,,,8,0,3.500000
100,,,0,10,4,7,6,7,5,12,6,0,3.750000
113,,,2,4,2,6,8,3,,,8,2,4.000000
116,10,5,,,2,0,,,0,10,10,0,4.000000
16,10,15,4,8,0,10,6,3,0,12,10,0,4.000000
# 途中省略
143,10,6,10,8,8,9,10,11,10,10,10,8,9.600000
50,10,4,10,3,10,3,8,8,10,2,10,8,9.600000
58,10,3,10,4,10,6,8,7,10,9,10,8,9.600000
103,10,4,10,5,8,4,10,6,10,2,10,8,9.600000
89,10,6,,,,,,,,,10,10,10.000000
1,,,,,,,,,10,3,10,10,10.000000
136,10,7,10,10,,,,,,,10,10,10.000000
51,10,5,10,7,,,10,9,,,10,10,10.000000
,10,10,10,10,10
,6,0,0,0,0
,9.275591,5.829457,6.047244,7.177419,8.474576

5-C. ヒント

オプションの解析

オプションの解析では,オプションの指定方法が間違っているケースは考える必要はありません. オプションの内容を記録するためにArguments型を用意しましょう. Arguments型にはフィールドとして,オプションの内容を保持する変数を宣言してください. そして,Arguments型に次のようなparseを用意してください.

void parse(String[] args){
    for(Integer i = 0; i < args.length; i++){
        if(!args[i].startsWith("--")){
            arguments.add(args[i]);
        }
        else {
            i = parseOption(args, i);
        }
    }
}
Integer parseOption(String[] args, Integer i) {
    if(Objects.equals(args[i], "--dest")){
        i++;
        this.dest = args[i];
    }
    ...
    return i;
}

拡張子と出力フォーマットを合わせる.

拡張子が望むものかどうかを確認し,望むものでなければ拡張子を追加しましょう.

String updateExtension(String fileName, String wontExtention){
    if(fileName.endsWith(wontExtention)){
        return fileName;
    }
    return fileName + wontExtention;
    // 拡張子を置き換えたい場合は次の処理.
    // Integer index = fileName.lastIndexOf(".");
    // if(index < 0)
    //     return fileName + wontExtention;
    // return fileName.substring(0, index) + wontExtention;
}

上記のメソッドを定義しておくと,次のような結果が得られます.

String name1 = updateExtension("heatmap.png", ".png"); // => "heatmap.png"
String name2 = updateExtension("heatmap.png", ".ppm"); // => "heatmap.png.ppm"
String name3 = updateExtension("heatmap", ".ppm");     // => "heatmap.ppm"

指定の方法でソートする.

次の3つの比較器を用意し,それぞれStudentIdComparator.javaStudentScoreComparator.javaStudentTakenTimeComparator.javaに保存し,同じディレクトリに置いてください.

StudentIdComparator.java
import java.util.Comparator;
public class StudentIdComparator implements Comparator<StudentScore5> {
    public int compare(StudentScore5 ss1, StudentScore5 ss2){
        return ss1.id.compareTo(ss2.id);
    }
}
StudentScoreComparator.java
import java.util.Comparator;
public class StudentScoreComparator implements Comparator<StudentScore5> {
    public int compare(StudentScore5 ss1, StudentScore5 ss2){
        return Double.compare(ss1.average(), ss2.average());
    }
}
StudentTakenTimeComparator.java

averageOfTakenTimeメソッドをStudentScore5に定義し,所要時間の平均を返すようにしてください.

import java.util.Comparator;
public class StudentTakenTimeComparator implements Comparator<StudentScore5> {
    public int compare(StudentScore5 ss1, StudentScore5 ss2){
        return Double.compare(ss1.averageOfTakenTime(), ss2.averageOfTakenTime());
    }
}
ソートされた学生一覧を取得する.

学生一覧がHashMap<String, StudentScore5>に保存されている場合,次のようなプログラムを書くことで, StudentScore5の一覧がソートされた状態で取得できます.

ArrayList<StudentScore5> sortedStudentList(HashMap<String, StudentScore5> map, String sortKey){
    Arraylist<StudentScore5> list = new ArrayList<>(map.values());
    if(Objects.equals(sortKey, "id")){
        Collections.sort(list, new StudentIdComparator());
    }
    else if(Objects.equals(sortKey, "score")){
        Collections.sort(list, new StudentScoreComparator());
    }
    else if(Objects.equals(sortKey, "time")){
        Collections.sort(list, new StudentTakenTimeComparator());
    }
    return list;
}

5-D. 評価項目

  • 概要に示した評価項目
  • オプションの解析を適切に行えているか.
  • オプションが指定された場合,指定されなかった場合の両方で,適切に処理できているか.
  • データを変更しても,例外なく実行結果を出力できるか
    • 人数やスコアの有効値が変更されても例外なく出力されるか.

ステップ6

6-A. 問題説明

ステップ5で指定したヒートマップを出力するようプログラムを変更してください. つまり,--heatmap scoreが指定された場合(もしくは--heatmapオプションが指定されなかった場合)は点数のヒートマップ, --heatmap timeが指定された場合は提出までに要した時間のヒートマップを出力してください. 加えて,ヒートマップもステップ5で指定したソート項目でソートされるようにしてください.

6-B. 実行例

  • reading.csvのスコアのヒートマップ

    ソートなし

    IDでソート

    スコアでソート

    時間でソート

  • reading.csvの時間のヒートマップ

    ソートなし

    IDでソート

    スコアでソート

    時間でソート

6-C. ヒント

6-D. 評価項目

  • 概要に示した評価項目
  • データを変更しても,例外なく実行結果を出力できるか
    • 人数やスコアの有効値が変更されても例外なく出力されるか.

最終課題に向けて

ここに示した内容はこれまでに提出された課題を見て,説明が必要であろうと思われる部分を抜粋しました.

Visual Studio Code で表示されるラベルについて

Visual Studio Codeなどの昨今の IDE (Integrated Development Environment; 統合開発環境) では, EoD (Ease of Development) のため実引数に仮引数の名前を表示する機能があります. 以下に例を図示します.

Visual Studio Codeスクリーンショット

この図中の5, 7行目と12行目にそれぞれ,prefixformat という文字列が見えます. これは IDE の機能により表示されているラベルです. 実際にプログラム中にこのラベルを書くとコンパイルエラーになるため,注意してください.

インデントを揃える.

インデントをしっかりと揃える必要がありますが,手作業でインデントを揃えないようにしましょう. 手作業でインデントすると漏れや間違いが起こる可能性があるためです. 利用しているエディタの一括インデントを行ってください.

Visual Studio Code の場合,Option+Shift+F で一括インデントが行えます.

コンパイル&実行結果を確認する.

提出された練習問題を見てみるとごく単純なコンパイルエラーが残っているケースが見られます. どんなに面倒でも一度コンパイルしてください. そして,コンパイルできたら実行して,結果を確認してください. その際,どのような入力により,どのような結果が期待されるのかを確認してから実行すると良いでしょう.

以下のような場合は,速やかにTA,教員に相談してください. 授業中に挙手する他に,Teams でのチャットでも質問対応しております. 相談により成績が下がることはありませんが,相談せずに未完成のまま提出することは 結果的に成績の低下に繋がります.

  • コンパイル方法がわからない.
  • 実行方法がわからない.
  • コンパイルエラーが修正できない.
  • 期待する結果がわからない.
  • 期待する結果と実行結果の差がわからない.

スコープについて

変数には有効範囲があります.この有効範囲のことを スコープ(scope) と呼びます. プログラム中の {} で囲まれた範囲を ブロック(block) と呼びます. スコープは変数が宣言された後,宣言されたブロックの中でのみ有効です.

メソッドの呼び出しについて

メソッドを呼び出すには,何らかの実体に対して呼び出す必要があります. この実体のことを レシーバ(receiver) と呼びます.

例えば,以下のフィボナッチ数列の $n$番目の値を求めるプログラムFibonacciで考えてみましょう.

public class Fibonacci {
    void run(String[] args) {
        Integer index = 10;
        if(args.length != 0) 
            index = Integer.parseInt(args[0]);
        Integer result = fibonacci(index);
    }
    Integer fibonacci(Integer index) {
        if(index < 2)
            return 1;
        return fibonacci(index - 1) + fibonacci(index - 2);
    }
    public static void main(String[] args) {
        HelloWorld app = new HelloWorld();
        app.run(args);
    }
}

mainメソッド内で,app.run(args) というメソッド呼び出しを行なっています. この呼び出しのappがレシーバ,argsが実引数(arguments),runがメソッド名です.

メソッド呼び出し部分の各名称

runメソッド内やfibonacciメソッド内の fibonacciメソッドの呼び出しには レシーバがないように見えます. これは実はthisというレシーバが隠されており,自分自身を表しています.

次に,8行目のfibonacciメソッドの宣言に注目してください. この行の最初の Integer は返り値の型であり,このメソッドの最後にこの型の値を return する必要があります. fibonacciはメソッド名,括弧内のindexは仮引数(parameter)と呼びます. 波括弧で囲まれた部分はメソッドボディやメソッドの中身と呼ばれ,そのメソッドが行う処理が書かれています.

メソッド定義部分の各名称

メソッド分割について

メソッドは細かく分割しましょう. メソッドを分割するのは,処理に名前をつけるために行います. 処理に名前が適切に付けられていると,処理内容を理解しなくても何が行われるのかを理解できます. つまり,読みやすくなるのです.

適切な名前のためには,命名規則に従うことも重要でしょう. Javaの場合,メソッド名は動詞から始まり,キャメルケースで命名することが推奨されています. このことを意識して適切なメソッド名をつけてみましょう.

最初は適切な名前をつけるのは難しいかもしれません. そのような場合,日本語(ローマ字)で付けるのも良いでしょう.

一方でメソッドを分けるときに,次のようなメソッドの中で他のメソッドを呼び出しているだけのメソッドには分割の意味はありません.

public class SomeClass {
    void run(String[] args) {
        perform(args);
    }
    void perform(String[] args) {
        // 何らかの処理
    }
    // mainメソッドは省略.
}

エラーについて

コンパイルエラーや実行時エラーの場合,エラーメッセージと発生した場所をしっかりと確認しましょう.

コンパイルエラーについて

コンパイルエラーは次のようにエラーの内容とエラーの場所を示してくれます. この例の場合,Fibonacci.java の 18 行目で,「シンボルを見つけられない」というコンパイルエラーです. シンボルを見つけられないというエラーは,典型的には綴りを間違えています. その変数の名前をしっかりと確認しましょう.

Fibonacci.java:18: エラー: シンボルを見つけられません
                prev2 = reslut; // result を代入して,次の i に備える.
                        ^
  シンボル:   変数 reslut
  場所: クラス Fibonacci
エラー1個

また,典型的なコンパイルエラーとその原因は次の通りです. 指摘された箇所を失火路と確認して修正していきましょう.

  • シンボルが見つかりません.
    • 綴りは合っていますか?
    • import は忘れていませんか.
    • 変数の有効範囲(スコープ)は合っていますか?
  • クラスXxxxpublicであり,ファイルXxxx.javaで宣言しなければなりません
    • クラス名とファイル名が一致していますか? 大文字小文字も区別してください.
  • \12288は不正な文字です.
    • 全角スペースが入っていないか確認してください.全角スペースはC言語と同じく文字列以外での利用が許されていません.

以下の内容も併せてご覧ください.

実行時エラーについて

実行時に何らかのエラーが起こった場合,次のようなメッセージが出力されます. このようなメッセージのことをスタックトレース(stack trace)と呼びます.

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
	at FizzBuzz.run(FizzBuzz.java:5)
	at FizzBuzz.main(FizzBuzz.java:25)

1行目にどのようなエラーであるか,2行目以降にプログラムのどこで発生したかが書かれています. どのようなエラーなのかは,thread "main" の後ろを見てみましょう. ここには,エラーの原因となった例外のクラス名が書かれています. 上の例の場合,ArrayIndexOutOfBoundsException,すなわち,array index out of bounds exception です. これは,配列の範囲を超えて要素にアクセスしようとした場合に発生する例外です. 英語の意味がわからない場合,Google 翻訳や DeepL 翻訳にかけてみましょう.

2行目以降は,クラス名.メソッド名(ソースファイル名:行番号) のように出力されています. 表示された場所と例外クラスの名前を手掛かりに実行時エラーが起こらないよう修正しましょう.

ヒント

スタックトレースとは,実行時に例外が発生したときに表示されるエラーメッセージです. 上で示したように,どこでどのような例外が発生したのかを把握する手掛かりとなる重要なメッセージです.

そして,例外が発生してスタックトレースを出力するときになると, 例外が発生した箇所はどのような経緯で呼び出されたのかを辿ってmainメソッドまで到達しようとします. メソッドの呼び出しはスタックで管理されていますので,スタックを辿っていくわけです. このことからスタックトレースと呼ばれています.

スタックトレースは Java に限ったものではなく,例外機構を導入しているプログラム言語であれば 似たような出力が行われます.

例外の責任転嫁について

ファイルの入出力時に発生する可能性のある IOException などの検査例外 と呼ばれる例外は, 例外が発生したときの対応をプログラム中に明示しておかなければコンパイルできないようになっています. 対応方法は以下の2種類です.

  • 例外に対してその場で対応し,プログラム実行を復帰させる.
  • メソッドの呼び出し元に対応を任せる(責任を転嫁する).

この講義では,呼び出し元に責任を転嫁します. 責任を転嫁するには,例外が発生する可能性のあるメソッドのシグネチャに throws 節を追加します.

public class Cat {
    void run(String[] args) throws IOException {
        for(String arg: args) {
            performEach(arg);
        }
    }
    void performEach(String arg) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader(arg));
        String line;
        while((line = in.readLine()) != null)
            System.out.println(line);
        in.close();
    }
    public static void main(String[] args) throws IOException {
        SomeClass app = new SomeClass();
        app.run(args);
    }
}

こうすることで,performEach 内で IOExceptionが発生した場合,呼び出し元である run メソッドに対応を任せます. しかし,run メソッドも同様に throws 節が宣言されており,呼び出し元である main メソッドに対応を任せています. 同様に main メソッドも throws 節があるため,呼び出し元に責任転嫁しています. mainメソッドの呼び出し元とは,javaコマンドです. もし,例外が発生した場合,javaコマンドまで例外が伝播し,そこでスタックトレースが出力されてプログラムが終了することになります.

補講A モダンな書き方

ここに記した内容は,応用であり,実践しなければならないものではありません. 特に,この授業内では,ここに記す書き方をしたからといって加点するわけではありませんし, 実践しなかったからといって減点することもありません.

ただし,近年のプログラム(Javaだけでなく,PythonやJavaScriptなど)には,似たような書き方や考え方が導入されています. モダンなライブラリなどを利用する場合や,ソースコードを読む場合には,これらの書き方に習熟しておく必要があります. そのため,プログラムが得意な人は以下のような書き方に挑戦してみると良いでしょう.

この講で利用するプログラム

補講A モダンな書き方のサブセクション

Optional: nullを使わない

背景

クイックソートの考案者であるアントニー・ホーア(Antony Hoare)が 2009年の QCon London で null の発明は10億ドルにも相当する誤りであった と発言しています.

nullがあると,NullPointerException が発生する可能性があります. そもそも null を使わなければ,NullPointerException が発生することはありません.

近年のプログラミング言語では,nullを許容しない変数を定義することも可能です. 例えば,Kotlin では,次のプログラムの1行目のように,デフォルトでは null を代入するとコンパイルエラーとなります. null を代入するには,型名に ? を付け,null許容型として扱う必要があります.

var str1: String = null;  // コンパイルエラー
var str2: String? = null; // OK

Javaではnullを許容しない変数は宣言できません. その代わりに Optional という型を利用します.

Optional

概要

Optionalとは,nullかもしれない値を扱うためのラッパ型です. mapifPresentメソッドなどで値が存在するときのみに処理を実行できます.

作成

String someString = // 文字列を代入する.
Optional<String> optional = Optional.of(someString);

ただし,Optional.ofnullを渡すと NullPointerException が投げられます. nullかもしれない変数をもとに Optional型の変数を作成するには,ofNullable を用います.

String nullString = null;
Optional<String> optional = Optional.ofNullable(nullString);

利用方法

あるディレクトリが保持するファイル・ディレクトリ一覧を取得する.

Optional を利用しない場合
File fileOrDirectory = // ファイルもしくはディレクトリ
File[] entries = fileOrDirectory.listFiles();
if(entries != null) {
    for(Integer i = 0; i < entries.length; i++) {
        System.out.printf("%d: %s%n", i, entries[i].getName());
    }
}
Optional を利用する場合
File fileOrDirectory = // ファイルもしくはディレクトリ
Optional<File[]> entries = Optional.ofNullable(fileOrDirectory.listFiles());
    // fileOrDirectory がファイルを指す場合,null が返される.
    // https://docs.oracle.com/javase/jp/8/docs/api/java/io/File.html#listFiles--
entries.ifPresent(entries -> { // entries の中身が null でない場合にのみ,下の for 文が実行される.
    for(Integer i = 0; i < entries.length; i++) { // files は entries の中身である File[] 型.
        System.out.printf("%d: %s%n", i, entries[i].getName());
    }
})

以上のものをもっと簡略化して書くと次のようになる.

File fileOrDirectory = // ファイルもしくはディレクトリ
Optional<File[]> entries = Optional.ofNullable(fileOrDirectory.listFiles());
entries.ifPresent(files -> IntStream.range(0, files.length)
    .forEach(i -> System.out.printf("%d: %s%n", i, files[i])));

参考資料

Stream: ループを使わない

背景

不吉な匂い(Code Smell)という,バグではないものの,バグの温床となり得る場所を指す言葉があります. リファクタリング 第二版 という本で,不吉な匂いにループが新たに追加されています. forwhileなどのループはバグの温床になり得るので避けるべきと言われているのです. なぜなら,1行に書くべきことは1つと言われながらも,for文は1行に初期化式,継続条件,反復式の3つの式が存在しています. また,配列などの長さを超えてのアクセスは,インデックスで要素にアクセスしているために起こります.

Java 8 から Stream API と呼ばれるデータ処理を行うためのAPIが導入されており,これにより for などのループを置き換えることが可能になります. この Stream API とメソッド定義を簡略化して書けるラムダ式を用いることにより,より簡潔に分かりやすく書けるようになります. そして,この Stream API は Java だけでなく,昨今のプログラム言語で数多くサポートされています.

Stream API

第0講 基礎文法 例題で示した 1以上100未満の奇数一覧を出力するプログラムを例に挙げます.

元のプログラムのは次の通りです.

for(Integer i = 1; i < 100; i++){
  // iを割った余りが1であれば,奇数.
  if(i % 2 == 1){ // i % 2 != 0 の条件でも可.
    System.out.print(i);
    System.out.print(" ");
  }
}

上のプログラムを Stream API で書き直したものが下のものです. メソッドの返り値を変数に代入せず,そのままメソッド呼び出しを続けている点に注意して読んでください(メソッドチェイニング (Method chaining)) と呼びます). Terminal で jshell を実行し,以下のプログラムをコピー&ペーストして実行してみましょう.

System.out.println(                     // 得られた文字列を出力する.
  IntStream.range(1, 100)               // 1以上,100未満の各値に対して,続く処理を適用する.
    .filter(i -> i % 2 == 1)            // iを2で割った余りが1のもの(奇数)のみ残す.
    .mapToObj(i -> Integer.toString(i)) // Integer 型を String 型に変換する.
    .collect(Collectors.joining(" "))); // 全ての文字列を空白区切りで連結させる

Stream APIは上記のように,繰り返しの中での様々な処理を関数を渡すことにより,副作用の少ないプログラムを書くことを目指します. 元のプログラムに比べての下のプログラムのメリットは次の通りです.

  • プログラムの副作用が少ない.
    • 副作用とは元のプログラムで i に何度も値が再代入されている点です.
      • 再代入は,現在の i の値が何であるかを把握するために労力を要するので,ない方が望ましいわけです.
  • forの継続条件である i < 100<<= であるべきか,i の初期値が 1 で良いのかと悩む必要はありません.
    • 問題文に記されている通り,以上,未満を表すのが IntStream.range メソッドです.
      • 以上,以下であれば,IntStream.rangeClosed を使いましょう.
  • System.out.print は最後に一度だけ実行される(文字列の出力は一般的にオーバーヘッドが大きい).
  • 各行で何を行っているのかはメソッドを見ると大まかにわかります.
    • filterは不要な値を削除する,mapmapToObj)は値を変換している,collectは集めている,とおおまかな処理内容が推測できます.
    • 元のプログラムは if の条件で何が起こるかを把握しなければプログラムの内容を読み解けません.

Streamの利用方法

Stream の生成法

  • ArrayList<K>から Stream<K> を入手する.
    • list.stream()
  • 配列(array)から Stream を入手する.
    • Arrays.stream(array)
  • その他
    • Stream.iterate(seed, prev -> prev + step)
    • 上のように,一番最初の値(seed)と seed から prev + step を順に適用した数値を返します.

Stream でできること

filter

Stream の要素を削除します. 引数(Predicate)は要素を受け取り Boolean を返します. 返り値がfalseのものはこのStreamから削除されます(元のListや配列から削除されるわけではありません).

map

Streamの各要素に対して,値の変換を適用します. 引数(Function)は,値を受け取り,変換後の値を返します(元のListや配列の要素が変換されるわけではありません).

forEach

Streamの各要素に対して,処理を行います. 引数(Consumer)は,値を受け取り,何も返しません.

collect

Map 型の Stream

Map はキーと対応するバリューのペアを格納します. そのため,そのまま Stream の実体を取得できません. 次のように,3つの方法で取得します(Map<K, V>型の変数mapから取得します.KVStringIntegerなどと読み替えてください)

  • map.keySet().stream()
    • Stream<K> が返されます.
  • map.values().stream()
    • Stream<V> が返されます.
  • map.entrySet().stream()
    • Stream<Map.Entry<K, V>> が返されます.

forEach のみが必要であれば,mapに対して直接 forEach を呼び出すことも可能です. この場合,map.forEach((k, v) -> ...) のように,キーとバリューの両方を受け取ります.

Streamの適用例

奇数の一覧の別解

第0講 基礎文法 例題で示した 1以上100未満の 奇数一覧の別解.

System.out.println(
    IntStream.iterate(1, prev -> prev + 2) // 無限 stream を取得する.
        .takeWhile(value -> value <= 100)  // takeWhile は Java 11 から利用できる.
        // .limit(50)                      // 最初から50個の要素のみ取得する.Java 8だと takeWhile が使えないため.
        .mapToObj(value -> Integer.toString(value))
        .collect(Collectors.joining(" ")));

総和を求める.

第0講 練習問題3を Stream で解くと次のようなプログラムになります.

Integer sum(Integer from, Integer max) {
    return IntStream.rangeClosed(from, max) // from以上,max以下の範囲の各値に対して
      .sum();                               // 全てを合計する.
}

ArgsPrinter

第1講 例題 ArgsPrinter を Stream で解くと次のようなプログラムになります. argsのインデックス番号とその値を出力したいが,Arrays.stream を用いるとインデックス番号が取得できない. そのため,IntStream.range を用いる.

IntStream.rangeClosed(0, args.length)
    .forEach(i -> System.out.printf("%d: %s%n", i, args[i]));

モンテカルロ法による $\pi$の計算

第2講 練習問題4

Double pi(Integer loopCount) {
    return 4d * IntStream.range(0, loopCount)  // 0以上,loopCount以下繰り返す.
        .mapToDouble(i -> calculateDistance()) // 乱数で得た座標と原点の距離を計算する.
        .filter(length -> length < 1d)         // 得られた距離が 1 より小さいもののみにする.
        .count();                              // 数を数える.
}

Double calculateDistance() {
    Double x = Math.random();
    Double y = Math.random();
    return Math.sqrt(x * x + y * y);
}

参考資料

ラムダ式

概要

ラムダ式は,特定のメソッドを簡略化して書けるような文法で,Java 8から導入されました. 例えば,Strema API を用いて指定された数までの FizzBuzz を出力するプログラムを例に挙げて説明します. 以下のプログラムの runメソッドの filterメソッド,map メソッドに渡している v -> v % 2 == 1i -> Integer.toString() がラムダ式と呼ばれる書き方です.

public class OddNumbersStream {
    void run(String[] args) {
        return IntStream.rangeClosed(1, 50)
            .filter(v -> v % 2 == 1)
            .mapToObj(number -> Integer.toString(number))
            .collect(Collectors.joining(" "));
    }
    public static void main(String[] args) {
        new OddNumbersStream().run(args);
    }
}

これは,以下のようなメソッドが定義されており,その定義を省略して書いています.

    void run(String[] args) {
        return IntStream.
            .filter(int test(int v) {
                return v % 2 == 1;
            })
            .mapToObj(String apply(int number) {
                return Integer.toString(number);
            })
            .collect(Collectors.joining(" "));
    }

ラムダ式では,-> の前に引数を,後ろにメソッドの中身を書きます. メソッド名,引数の型,返り値の型が省略されています. また,-> の後ろのメソッドの中身が1つの式の場合はメソッド定義の波括弧({})とreturnが省略可能です.

書き方

( 変数1,  変数2, ...) -> { メソッドの中身 }

ただし,以下のものが省略可能です.

  • ->の前の型,
  • ->の前の括弧(()),ただし,メソッド引数が1つだけの場合.
    • 引数が0個,2つ以上の場合は,省略不可.
  • 波括弧({})とreturn.ただし,メソッドの中身が1つの式の場合のみ.

ラムダ式の書き方いろいろ

省略しない場合

Function<Integer, String> fizzbuzzer = (Integer number) -> {
    if(number % 15 == 0)     return "FizzBuzz";
    else if(number % 3 == 0) return "Fizz";
    else if(number % 5 == 0) return "Buzz";
    return Integer.toString(number);
};

引数の型を省略

Function<Integer, String> fizzbuzzer = (number) -> {
    if(number % 15 == 0)     return "FizzBuzz";
    else if(number % 3 == 0) return "Fizz";
    else if(number % 5 == 0) return "Buzz";
    return Integer.toString(number);
};

引数の括弧を省略

Function<Integer, String> fizzbuzzer = number -> {
    if(number % 15 == 0)     return "FizzBuzz";
    else if(number % 3 == 0) return "Fizz";
    else if(number % 5 == 0) return "Buzz";
    return Integer.toString(number);
};

メソッドの切り出し

Function<Integer, String> fizzbuzzer = number -> {
    return fizzbuzz(number);
};

String fizzbuzz(Integer number) {
    if(number % 15 == 0)     return "FizzBuzz";
    else if(number % 3 == 0) return "Fizz";
    else if(number % 5 == 0) return "Buzz";
    return Integer.toString(number);
}

波括弧とreturnの省略

Function<Integer, String> fizzbuzzer = number -> fizzbuzz(number);

String fizzbuzz(Integer number) {
    if(number % 15 == 0)     return "FizzBuzz";
    else if(number % 3 == 0) return "Fizz";
    else if(number % 5 == 0) return "Buzz";
    return Integer.toString(number);
}

このような書き方は,これまでの Java の文法とは大きく異なるので,初見だと面食らうかもしれません. しかし,書き方が違うだけで行っていることは従来からと同じです. 慣れるまでは何を意味しているのかを確認しながら読み解いてください.

ラムダ式は,実際にはクラスの定義を行い,その定義したクラスの実体を作成しています. そのため,オブジェクト指向を学んだ後に復習することをお勧めします.

参考資料

練習問題

1. nullチェック

与えられた String 型変数が null ならば空文字を返し, そうでなければその値そのものを返すメソッドpurifyNullを作成してください. ただし,purifyNullメソッド内ではif文やswitch文を使ってはいけません. クラス名は PurifyNullDemo としてください.

public class PurifyNullDemo {
  public void run() {
    demo("string"); // "string" が出力される.
    demo("null");   // "null" が出力される.
    demo(null);     // "" が出力される.
  }
  void demo(String givenString) {
    String purifiedString = purifyNull(givenString);
    System.out.printf("\"%s\" => \"%s\"%n", givenString, purifiedString);
  }
  String purifyNull(String givenString) {
    // if文を使わず givenString が null のときに空文字を返す.
  }
}

2. 乱数値100個の統計(Stream)

第03講 練習問題 1 乱数値100個の統計を Stream を用いて書き直してください. クラス名は StatsValuesStream としてください.

補講B グラフィックス

シラバスでは第6講で実施する予定でしたが,以下の理由により実施しないことにしました. 講義資料はそのまま置いておきます.

この講で利用するプログラム

補講B グラフィックスのサブセクション

グラフィックス

EZ.java

本日のプログラムを実行するには EZ.javaが必要です. これから書くプログラムと,同じディレクトリにこのEZ.javaを置いてください.

EZ.java は正式には,EZ Graphics と呼びます. このプログラムは,ハワイ大学マノア校の Advanced Visualization and Applications 研究室 Dylan Kobayashi によって開発されたソフトウェアです. 1 つのファイルを同じディレクトリに置くことで図形の描画が容易に扱えるようになります. 公式サイトからもダウンロードできますが, この授業向けに少し修正していますので,ここからEZ.javaをダウンロードして利用してください.

楕円の描画

ダウンロードしたら,早速プログラムを書いていきましょう. 書き終えたら,コンパイル,実行してみましょう. 実行結果を確認できれば,値を変更して再度,コンパイル,実行してみましょう. どこを変更すると,どう変わるのかを確認してください.

import java.awt.Color;

public class DrawOval{
  void run(){
    EZ.initialize(400, 400); // 画面の大きさを決める.
    // 円を描く.(中心座標x, y, 幅,高さ,色,塗りつぶし)
    EZCircle circle1 = EZ.addCircle(
        100, 100, 200, 200, Color.BLUE, true);
    EZCircle circle2 = EZ.addCircle(
        200, 200, 200, 200, Color.RED,  false);
  }
  // mainメソッドは省略.
}

楕円の描画

なお,Colorは色を表す型です.この型を利用するときには,import java.awt.Color;の一文がクラス宣言の前に必要です. そして,BLACK, BLUE, CYAN, DARK_GRAY, GRAY, GREEN, LIGHT_GRAY , MAGENTA, ORANGE, PINK, RED, WHITE, YELLOW の 13 色が定義済みです.

座標系

座標は左右方向が x 軸,上下方向が y 軸になっており,右側が x 軸のプラス方向,左端が x 軸の 0 です. また,上下方向では,一番上が y 方向の 0,下方向がプラスになっていることに注意してください.

例題 1. 楕円の描画の変更

  • 円の位置をずらしてください.
  • 円の色を変更してください.
  • 円の大きさを変更してください.

その他の図形の描画

円以外の図形も描いてみましょう. どのようなメソッドを呼び出せば良いかは,EZ Graphics のドキュメント を読んでみましょう.

https://www2.hawaii.edu/~dylank/ics111/doc/

EZ Graphics ドキュメント1

EZ Graphics ドキュメント2

各ページの左のフレームは,EZ Graphics が持つ型を表しています. 右のフレームは,その型が持つ情報を表しています.右のフレームを下にスクロールしていくと, メソッドの一覧が閲覧できます(上の右側の画像).

この形式のドキュメントを一般に API ドキュメント と呼びます. Javadoc コメントで生成されるドキュメントです.

例題 2. 楕円以外の図形の描画

では,EZ Graphics の API ドキュメントを参照し, 楕円以外の図形も描いてみましょう.

まず,DrawOval.javaをコピーしてDrawShapes.javaを作成してください. そして,DrawShapes.javaに追加・変更していってください. なお,DrawShapes.javamainメソッドの中身も忘れずに変更しましょう.

アニメーション

アニメーションの基礎

アニメーションは基本的にパラパラ漫画と同じ原理で行います. EZ Graphics では,EZ.addCircleEZ.addLineなどで 追加して返される実体の位置を変更し,EZ.refreshScreen()を呼び出すと画面を更新します.

また,Java で一定時間スリープするのは,Thread.sleepメソッドを利用します. sleepメソッドに スリープする時間(ミリ秒)を渡します. 次の例では,100 ミリ秒(0.1 秒)スリープしています.

次の例で確認してみましょう.

import java.awt.Color;
public class RoundTrip{
    void run(){
        EZ.initialize(400, 400);
        EZCircle circle = EZ.addCircle(
            100, 100, 5, 5, Color.BLUE, true);
        this.roundTrip(circle);
    }
    void roundTrip(EZCircle circle){
        Integer deltaX = 10;
        while(true){ // 無限ループ
            Integer newX = circle.getXCenter() + deltaX;
            circle.translateTo(newX, circle.getYCenter());
            if(newX >= 400 || newX <= 0){
                deltaX = deltaX * -1;
            }
            EZ.refreshScreen();
            Thread.sleep(100);
        }
    }
}

これでコンパイルすると,次のようなコンパイルエラーが発生します.

RoundTrip.java:17: エラー: 例外InterruptedExceptionは報告されません。スローするには、捕捉または宣言する必要があります
       Thread.sleep(100);
                    ^
エラー1個

例外機構(Exception Architecture)

例外(Exception)は,近年のプログラミング言語で採用されている実行時エラーの通知機構です.

従来のプログラミング言語,例えば,C 言語の場合,fopenで開くファイルが見つからなかった場合, 返り値をNULL にすることでエラーを通知していました. この場合,プログラマが責任を持って, 返り値の値を確認して,正常か異常かを判断しなければいけませんでした.

一方の例外機構は,異常処理を行うための別の処理経路を作るものです. もし,プログラムの実行途中で何らかの異常が発生した時,それまでに行なっていた処理を中断し, 別の処理を行うようにする機構です.

graph LR;
A[ファイルを開く] --> B{存在確認}
B -->|存在する| C[正常処理]
B -->|存在しない| D[異常処理]

C 言語のようなエラー処理は上記のフローチャートのように, プログラマ自身がfopenの返り値を元に分岐処理によって,正常処理,異常処理を振り分ける必要があります.

graph LR;
A[ファイルを開く]
A --> C[正常処理]
A -->|存在しない| D[異常処理]

対して,例外機構がサポートされている言語の場合,プログラマが異常,正常の分岐を明示的に書く必要はありません. 異常が起こった場合の処理の経路が決まっており,その経路に処理を書いておくことが異常処理を行うことになります. 正常処理の場合は,それまでの処理の続きにそのまま処理を書いていきます. これにより正常処理の見通しがよくなります.

そして,例外が投げられれば,何らかの異常が発生したと判断できるようになります. 逆に,例外が投げられなければ,データが変であろうが,正常な処理であると判断できます (もちろん,開発途中で変な場合はバグの可能性はありますが).

プログラマが明示的に投げる例外も存在しますが,多くの場合,システムが異常を検知し例外を投げます. 例えば,ファイルが見つからない場合,スリープ中に割り込みが入った場合などです. そのような例外が発生した時に,どのような対応をするのかをあらかじめ決めておく必要があります.

次のプログラムが,例外処理のイメージです.

void exceptionalMethod() throws Exception {
  // 例外が発生する可能性のある処理
  someMethodCall();

  // 例外が起こらなかった場合の処理
  process();
}

someMethod の実行中に何らかの例外が起こった場合,まずsomeMethodの呼び出し元であるexceptionalMethodにどのように処理するかが問い合わされます. そして,exceptionalMethodは例外は処理せず,呼び出し元に処理を任せるよう設定している(メソッドにthrows Exceptionと宣言しているため)ため,exceptionalMethod の呼び出し元に通知されます. このthrows節は後ほど説明します.

検査例外と非検査例外

Java の例外は,検査例外と非検査例外の2つに分類できます. 違いは次に挙げる通りです.

検査例外

例外が発生した時の処理をプログラム中に明示的に書いておかなければコンパイルエラーになる例外. 完全に防ぐことが不可能な例外(実行時の状態によって発生しうる例外).

例えば,実行時エラーは,プログラムでどれほど厳密にチェックしたとしても,完全に回避できません.

  • 代表的な検査例外
    • IOException
      • 入出力時にエラーが発生した時.
    • InterruptedException
      • 割り込みが発生した時に発生する例外.

非検査例外

一方,非検査例外はプログラム中で十分にチェックすることで避けることが可能です. そのため,例外が発生した時の処理は書かなくてもコンパイルが通るようになっています.

  • 代表的な非検査例外
    • NullPointerException
      • 初期化されていない変数に対する処理を行なった場合に発生する例外.
    • ArrayIndexOutOfBoundsException
      • 配列の範囲を超えてアクセスしようとした時に発生する例外.
    • NumberFormatException
      • 数値の変換に失敗した時に投げられる例外.

非検査例外は,事前にチェックすることで,例外の発生を抑えられます. 逆に言えば,実行時に非検査例外が投げられた場合は,事前のチェックが不十分であるとも言えます. 例えば,配列の範囲を超えてアクセスすることは,事前に配列の範囲を超えないようにプログラム中で確認することで避けられます.

例外の責任転嫁

さて,例外が発生した時の対処法をプログラム中に書いておく必要があります. 取れる対処法は2つです.

  • 例外が投げられたら,その場で例外に対応する.
  • 例外が発生する可能性のあるメソッドを呼び出している元に対応を任せる.

どちらがふさわしいかは場合により異なります. ここでは,呼び出し元に対応を任せましょう. そのために,呼び出し元に責任を丸投げするようにプログラム中に明示しておきましょう.

こうすることで,例外が発生した時はそのメソッドの呼び出し元に責任を転嫁し,正常処理のみに集中して処理を書くことができます.

情報

本講義では,例外に対応する処理については省略します. 詳細を知りたい場合は,try-catch について調べてみてください.

InterruptedException の責任転嫁

さて,コンパイルエラーで,InterruptedExceptionが投げられる可能性があると述べられています. この例外は,スリープ中に割り込みが発生した時に発生する例外です. ここでは,呼び出し元に責任を転嫁しましょう. 以下のような対応になります.

  • Thread.sleep が例外を発生させた時,Thread.sleep の呼び出し元であるroundTripに対応が任されます.これが例外が投げられた,ということです.
  • そこで,roundTripの呼び出し元であるrunに対応を任せましょう.
  • さらに,runでも,呼び出し元であるmainに処理を任せる事にします.
  • 同じく,mainも呼び出し元に対応を任せましょう.
  • すると,実行環境が対応されなかった例外をスタックトレースという形で出力し,プログラムが終了するようになります.

throws 節

呼び出し元に対応を任せるには,メソッドのシグネチャthrows節を追加します.

throws 節は以下のように指定します. 以下のように書くことで,例外クラスが発生した時,methodNameの呼び出し元に責任を転嫁することができるようになります.

public class ClassName{
  void methodName() throws 例外クラス {
    // 検査例外が発生する可能性のある処理
  }
}

なお,複数の例外が発生する可能性のある場合,throws節に例外クラスの名前をコンマ区切りで指定できます.

例題 3. アニメーション

すなわち,メソッドの宣言部分を以下のように変更してください.

public class RoundTrip{
    void run() throws InterruptedException{
        // ... 省略
        // roundTripで IntrruptedException が発生する可能性がある.
        this.roundTrip(circle);
    }
    void roundTrip(EZCircle circle)
            throws InterruptedException{
        // .... 省略
        // Thead.sleepの呼び出しで
        // IntrruptedExceptionが発生する可能性がある.
        Thread.sleep(100);
    }
    public static void main(String[] args) throws InterruptedException{
        RoundTrip trip = new RoundTrip();
        // runでIntrruptedExceptionが発生する可能性がある.
        trip.run();
    }
}

このようにプログラムを変更し,コンパイルしてみましょう.今度はコンパイルできたはずです. 次のような実行結果となるはずです.

アニメーションの例

このように,アニメーションを行うには,スリープが必要です. 一方,スリープを行うには,例外への対応が必要になります. 今後,ファイルの入出力を扱う時にも例外機構は必要になってきますので, どのような機構であるのか,しっかりと押さえておいてください.

スリープを行わないと,環境によっては目にも留まらぬ速さでアニメーションが繰り広げられます.

import java.awt.Color;

public class RoundTrip{
    void run() throws InterruptedException{
        EZ.initialize(400, 400);
        EZCircle circle = EZ.addCircle(100, 100, 5, 5, Color.BLUE, true);
        roundTrip(circle);
    }
    void roundTrip(EZCircle circle) throws InterruptedException{
        Integer vx = 10;
        while(true){ // 無限ループ
            Integer x = circle.getXCenter() + vx;
            circle.translateTo(x, circle.getYCenter());
            if(x >= 400 || x<= 0){
                vx = vx * -1;
            }
            EZ.refreshScreen();
            Thread.sleep(100);
        }
    }
    public static void main(String[] args) throws InterruptedException{
        RoundTrip trip = new RoundTrip();
        trip.run();
    }
}

例題 4. 鉛直投げ上げ運動のアニメーション

以下の実行結果になるよう,鉛直投げ上げ運動のアニメーションを作成してみましょう. (アニメーションが途中で終わっているので変な挙動のように見えますが,バウンドし続ける動きになっています) クラス名は Bound としてください.

$y$方向の 0 が一番上,下方向がプラスになっていますので,通常の投げ上げとは方向が逆になっています(上方向に重力がかかっていると思ってください). 必要な式は次の通りです.

  • 時間$t$は 0 から始まり,0.1 ずつ増加するものとする.
    • 投げ上げ運動,自由落下運動それぞれに切り替わるとき,$t=0$となる.
  • 初期値(投げ上げ運動)
    • 初速 $v_0 = 85.0$
    • 初期位置 $y_0 = 10.0$
    • 時間 $t=0.0$
    • 重力加速度 $g=9.8$
  • 投げ上げ運動と自由落下運動の切り替え条件
    • 投げ上げ運動の時
      • $v<0$となったとき,自由落下運動に切り替わる.
    • 自由落下運動の時
      • $y < 10$となったとき,投げ上げ運動に切り替わる.
        • 本来の物理世界であれば, $v_{i -1}$に跳ね返り係数をかけ, $v_i$とするが,今回は跳ね返り係数を 1 としている.
  • 投げ上げ運動と自由落下運動の切り替え時の値の更新
    • 時間 $t$ を初期化する( $t=0$).
    • 初速を更新する( $v_0=-v$).
    • 初期位置を更新する( $y_0 = y$).
  • その他
    • 時刻 $t$における速度を求める.
      • $v = v_0 - gt$
    • 時刻 $t$における現在位置を求める.
      • $y = y0 + (v_0 t - \frac{1}{2}gt^2)$

跳ね返り

import java.awt.Color;

public class Bound{
    void run() throws InterruptedException{
        Integer x = 100;
        Double y0 = 10.0;
        Double y = y0;
        EZ.initialize(400, 400);
        EZCircle circle = EZ.addCircle(x, y.intValue(), 5, 5, Color.RED, true);

        Double v = 85.0;
        Double v0 = v;
        Double t = 0.0;
        Double g = 9.8;
        Boolean nageage = true;

        while(true){
            v = v0 - g * t;
            y = y0 + (v0 * t - (g / 2) * t * t);

            if(isSwitch(nageage, v, y)){ // 切り替え条件を確認する.
                t = 0.0;
                v0 = -1 * v;
                y0 = y;
                nageage = !nageage;
            }
            t += 0.1;

            // デバッグ用.
            // System.out.printf("(x, y) = (%3d, %+4d, %+4.2f), t = %5.2f, v = %+5.2f, g = %+5.2f, v0 = %+5.2f, nageage: %s%n",
            //                   x, y.intValue(), y0, t, v, g, v0, nageage);
            circle.translateTo(x, y); // 位置を更新する.
            EZ.refreshScreen();       // 画面を更新する.

            Thread.sleep(100);        // 0.1秒間スリープする.
        }
    }

    Boolean isSwitch(boolean nageage, Double v, Double y){
        if(nageage && v < 0){ // 投げ上げ時,最高点に達した.
            return true;
        }
        else if(!nageage && y < 10.0){ // 自由落下時,地面に達した.
            return true;
        }
        return false;
    }

    public static void main(String[] args) throws InterruptedException{
        Bound bound = new Bound();
        bound.run();
    }
}

練習問題

1. 図形の描画

次の図になるようにプログラムを作成してみましょう. 色は必ずしもこの通りでなくて構いません. プログラム名は,DrawShapes2としてください.

図形の描画の完成形

2. サイン波の描画

$0 \leq x \leq 2\pi$ の範囲でサイン波を描画してみましょう.クラス名は SineCurve としてください.

サイン波

ヒント

プログラム全体の構成

上の図は,399 本の直線で描画されています. x=0から400未満まで繰り返し,v = Math.sin(i * delta) * s$(x_i, y_i)=(i, v)$を得ます. $((x_{i-1}, y_{i-1}), (x_i, y_i))$に線を追加することで上記のサイン波が描画できます.

なお,delta$\delta$)は横幅,s$s$)は高さを表しています. EZ.initialize(400, 400) で初期化した時, delta$\delta = \frac{2\pi}{400}$sは 150としてください( $-1 \leq \sin\theta \leq 1$であり,この値を -150〜150に割り当てるため). また,中央に寄せるため,y 軸方向に +200 としてください.

Java での $sin$,$cos$

Java で, $\sin$, $\cos$を計算するには,Math.sinMath.cosメソッドを利用しましょう. ただし,以下の点に注意してください.

  • 引数にはラジアンの値を渡してください.
  • $\pi$を利用するには,Math.PIという変数を利用してください.
  • すなわち,$\sin \frac{\pi}{3}, \cos \frac{\pi}{3}$を Java で求めるには,次のようなコードを用いてください.
Double sinValue = Math.sin(Math.PI / 3.0);
Double cosValue = Math.cos(Math.PI / 3.0);

Double 型の Integer 型への変換

Double型をInteger型として扱うには,Double型の変数に対して,intValueメソッドを呼び出しましょう. 例えば,Double型のdValueという値を Integer型のiValueに代入するには,次のようなプログラムになります.

Double dValue = // Double型の値を代入.
Integer iValue = dValue.intValue();

3. コッホ曲線(Koch curve)の描画

コッホ曲線は,線分を三等分し,分割点を頂点とした正三角形を描く線です. この作図を無限に繰り返すことで,線分の長さが$\infty$になります. コマンドライン引数でコッホ曲線の $n$ を指定できるようにしましょう.

クラス名は,KochCurveとしてください. コッホ曲線の例を以下に示します(画像のクリックで画像が更新されます).

コッホ曲線
上記のように,$(x_1, y_1)$と$(x_5, y_5)$が指定された時,$(x_2, y_2)$〜$(x_4, y_4)$を求めましょう.
  • $l=(\sqrt{(x_5 - x_1)^2 + (y_5 - y_1)^2})/3$
  • $(x_2, y_2) = (x_1 + l, y_1)$
  • $(x_3, y_3) = (x_2 + l\cos{\frac{\pi}{3}}, y_2 + l\sin{\frac{\pi}{3}})$
  • $(x_4, y_4) = (x_1 + 2l, y_1)$
$n=1$までは上記のように計算できますが,$n>2$の場合はこの計算では求められません. 次の例を元に考えてみましょう(画像のクリックで画像が更新されます).
上記のように,$(x_1, y_1)$と$(x_5, y_5)$が指定された時,$(x_2, y_2)$〜$(x_4, y_4)$を求めましょう. 元々の傾きを$\theta$として示します.
  • $l=(\sqrt{(x_5 - x_1)^2 + (y_5 - y_1)^2})/3$
  • $(x_2, y_2) = (x_1 + l\cos{\theta}, y_1 + l\sin{\theta})$
  • $(x_3, y_3) = (x_2 + l\cos{(\theta + \frac{\pi}{3})}, y_2 + l\sin{(\theta + \frac{\pi}{3})})$
  • $(x_4, y_4) = (x_3 + l\cos{(\theta + \frac{\pi}{3} - \frac{2\pi}{3})}, y_3 + l\sin{(\theta + \frac{\pi}{3} - \frac{2\pi}{3})}) = (x_3 + l\cos{(\theta - \frac{\pi}{3})}, y_3 + l\sin{(\theta - \frac{\pi}{3})})$

この計算式で,コッホ曲線を描いてみましょう. 再帰呼び出しを利用すると良いでしょう. $(x_2, y_2),(x_3, y_3)$間の直線を引く時,また,$(x_3, y_3),(x_4, y_4)$間の直線を引く時に それぞれを$(x_1, y_1),(x_5, y_5)$として再帰呼び出しを行えばコッホ曲線を描けるでしょう.

ヒント

再帰呼び出し

次のようなメソッドを用意しましょう.このメソッドを呼び出すことで,2点の間にコッホ曲線を描けるようになります.

void drawKoch(Integer x1, Integer y1, Integer x5, Integer y5,
      Integer dimension, Double angle){

  if(dimension == 0){
    // (x1, y1)から(x5, y5)まで線を引く.
  }
  else{
    // (x1, y1), (x5, y5) 間の長さの 1/3.これが l となる.
    Double length = // 長さlを求める.
    Double delta = Math.PI / 3.0;

    // (x2, y2) を求める.
    // (x1, y1)から(x2, y2)まで線を引く.

    // (x3, y3) を求める(θ は angle + delta).
    // (x2, y2)から(x3, y3)まで線を引く.
    this.drawKoch(x2.intValue(), y2.intValue(),
                  x3.intValue(), y3.intValue(),
                  dimension - 1, angle + delta);

    // (x4, y4) を求める(θ は angle - delta).
    // (x3, y3)から(x4, y4)まで線を引く.
    this.drawKoch(x3.intValue(), y3.intValue(),
                  x4.intValue(), y4.intValue(),
                  dimension - 1, angle - delta);

    // (x4, y4)から(x5, y5)まで線を引く.
  }
}

実行例

コッホ曲線

画像のクリックで画像が更新されます.

4. コッホ曲線(Koch curve)のアニメーション描画

コッホ曲線を $n=0$から$n=5$までを 1 秒程度で更新して描いてみましょう. クラス名は,KochCurveAnimationとしてください.

今まで描画した内容を消したい場合は,EZ.removeAllEZElements() メソッドを呼び出してください.

5. 斜方投射

例題 3例題 4を合わせた動きをするボールを描きましょう. $x$軸方向には,例題3を,$y$軸方向には,例題4の動きを設定すれば良いでしょう. クラス名を ThrowingExercise にしてください.

実行例

斜方投射の実行例

6. アニメーション

EZ Graphics を利用して,自由にアニメーションを描いてください. クラス名は Animation としてください. 内容は自由です.

7. アナログ時計

EZ.javaを利用して,アナログ時計を描画してください. クラス名は Clockとします.

実行例

アナログ時計 アナログ時計

この時の時刻は,19時22分25秒.

ヒント

基本的な描画方法

  1. 現在時刻を取得する.
  2. 背景を描画する.
  3. 短針,長針,秒針の両端座標を計算で求める.
  4. 短針,長針,秒針をそれぞれ描画する.
  5. 適当な時間(1,000ミリ秒(1秒))スリープする.
  • ただし,何秒かに一回程度,描画されない秒が出てくる.
  • 針の両端座標の計算処理の積み重ねのため.
  • これを防ぐためには,スリープの秒数を少なくする.
  • ただし,そのぶん処理が重くなり,チラツキの原因となる.
  • これらの問題は,ここでは解決する必要はない.
  1. 全ての描画をクリアする.
  2. 1に戻る.
  1. スリープ時間を100ミリ秒程度に短くする.
  2. 時,分,秒を保持しておく.
  3. 時,分,秒が更新されていた場合のみ描画する.

度数法から弧度法(ラジアン)に変更する

  • Math.toRadians に度数法の値(0〜360度)を渡せばラジアン値が得られる.
Double degree = // 度数法による角度
Double radian = Math.toRadians(degree); // 弧度法による角度

針の両端座標の計算

  • 中心座標は常に同じ.もう一方の端を毎秒,計算により求める.
  • 秒針は degreeOfSeconds = date.getSeconds() * 6.0 - 90 で得られる角度を元に計算する.
  • 長針も秒針と同様.date.getSeconds()の代わりに date.getMinutes() を利用する.
  • 短針は,degreeOfHours = (date.getHours() * 5 + date.getMinutes() / 12.0) * 6.0 - 90 で値を得る.
    • date.getHours() * 30 - 90で計算すると,例えば,6:30 の時でも短針は一番下を指したままである. そうではなく,長針が進むに従って,短針も少しずつ移動して欲しいため,上記の処理としている.

Javaの座標では,0度はx軸のプラス方向である. 一方で,アナログ時計の0はy軸のマイナス方向(上方向)である. そのため,角度を-90度にすることで,座標の角度と時間による角度を揃えられる.

なお,角度のプラス方向は共に時計回りである.

再描画

EZ.refreshScreen() を呼び出すと,今まで追加した要素を再描画します. つまり,これまでに addLineaddCircleなどで追加した図形を再描画します. ここでは,1秒ごとに新たな線を引きたいため,今まで追加した図形を削除する必要があります. そのために,EZ.refreshScreen()ではなく,EZ.removeAllEZElements()を呼び出す必要があります.

もちろん,追加した EZLine の実体に対して,適切に座標を変更した上で,EZ.refreshScreen()を呼び出せば期待通りの動作となります.

参考

まとめ

まとめ

Q & A

フォーマット記述子とは何ですか

System.out.printf に渡す%sなどの表示形式を指定するための文字列です. Java言語の場合,C言語とほぼ同じですが,Javaの場合は型がより多彩ですので,どの記述子にすべきかを注意する必要があります. 記述子が対応しない型の値を対応づけると実行時エラー(IllegalFormatConversionException)が発生します. フォーマット記述子と対応する型を以下に示します.

  • %c
    • 1文字(characterの頭文字)を出力する.
    • 対応する型:Character
  • %s
    • 与えられた値の文字列表現(stringの頭文字)を出力する.
    • 対応する型:何でもOK.
  • %d
    • 整数を10進数で出力する(decimalの頭文字).
    • 対応する型:Integer型,Long型,Short
  • %o
    • 整数を8進数で出力する(octalの頭文字).
    • 対応する型:Integer型,Long型,Short
  • %x
    • 整数を16進数で出力する(hexadecimalのx).
    • 対応する型:Integer型,Long型,Short
  • %f
    • 浮動小数点変数を出力する(floating point numberの頭文字)
    • 対応する型:Float型,Double
  • %e
    • 浮動小数点変数を指数表現で出力する(exponentialの頭文字).
    • 対応する型:Float型,Double
  • %n
    • 改行を出力する.実行環境によって改行コードが異なり,その違いを吸収するため.

なぜ\nではなく,%nを利用するのでしょうか

C言語では,\nで改行を表していましたが,Javaでは明示的に \n を使うことはありません. 改行のみを出力するときは,System.out.println(); とし, 改行付きで出力するときは,printlnメソッドの引数に出力したい内容を渡します. また,System.out.printf でも,\nは使わず%n を利用する方が良いとされています.

なぜなら,改行コードはプラットフォーム(WindowsやmacOSなど)で異なるためです. Windows では,改行は\r\nの2バイトで表されており,macOSやLinuxは\nで改行を表しています. 古いmacOS(MacOS 9以前)は\rで改行を表していました.

一方で,Java は,一度書けばどこでも動く(Write Once, Run Anywhere)ことを重要視しています. そのため,環境ごとの違いをどこかで吸収する必要があります. macOSでは改行されるのに,Windowsでは改行されない,のようなプログラムがあっては困るわけです. そのため,改行コードを直に書くことは避け,改行コードを表す記号を利用する方が良いとされているわけです.

改行が必要な場合は,printlnprintf%nを渡すようにしましょう.

import 文とは何ですか

Javaの型は必ずパッケージに所属しています. パッケージとは,ディレクトリのようなもので,階層構造が存在します. パッケージには,サブパッケージと型が属します. 型が約4,000個存在するため,パッケージを導入して分類しなければ混乱するためです. 標準的には,java.langパッケージに所属する型が利用できます. しかし,java.langパッケージに所属する型以外を利用する場合は,どのパッケージの型を利用するのかを指定しなければいけません. その指定を行うのが,import文です.

Java言語の型一覧は次のURLから確認できます. https://docs.oracle.com/javase/jp/8/docs/api/

なお,java.langパッケージには,String型やInteger型,Double型, System 型などが所属しています.

可視性とは何ですか

Javaでは実は,publicprotectedprivate というキーワードをクラス,メソッド,フィールドに付けられます. このキーワードの付け方により,どこからアクセスできるのかを制御できるようになります.

可視性のデフォルトはなしprivateより弱く,protectedよりも強い制限です. この可視性を使うときは,publicは最低限にする方が良いとされています.

情報

この授業では扱いません.

リンクリストとは何ですか

順序を持つデータ集合を実現する方法の一つ. 各要素が次の要素へのリンクを持つことで順序を持つデータ構造を実現しています.

下のようにNode型が次の要素へのリンクであるNode型のフィールドと, 要素である valueフィールドを持ちます. 最初の要素さえ持っていれば,最後まで順番に辿れるようになります.

リンクリスト

Wikipediaの連結リストも参照すると良いでしょう.

インデントが面倒です

1行ずつ手作業でインデントしていくのは面倒な作業です. そのような作業はPCに任せましょう. ほとんどのエディタには,対象のプログラムを一括でインデントしてくれる機能が揃っています. そのような方法を調べて,利用してください.

変数のスコープが短い方が良いのはなぜですか?

変数のスコープとは,変数の有効範囲のことであると学びました. プログラムを書く時には,一般的にこの有効範囲は狭い方が良いとされています.

スコープが広い場合,一度にいろんな変数のことを把握しておかなければプログラムの挙動がわかりません. 一方スコープが狭いと,ある場所(関数,メソッドやブロック)に関係する変数自体が少なくなります. すなわち,その場所に書かれた内容の理解がより容易であると言えます.

メソッド冒頭の変数宣言を止めよう

同様に,変数宣言も必要な箇所で行うようにしましょう. 関数,メソッドの冒頭に必要な変数をまとめて宣言するのは,宣言する箇所が決められていた時代の名残です. 今現在のC言語ですら,変数は関数の冒頭以外でも宣言できます. 必要な時に必要な変数を宣言するようにしましょう.

なぜmainメソッドに処理を書かない方が良いのでしょうか

Java言語は全てのプログラムが型(クラス)として作成されます. staticキーワードが付けられていないメソッド,フィールドは,実体に対して1つ存在します(newすると異なる実体が作成され,実体ごとに同じ定義ながらも異なる値を持つ).

対するstatic キーワードが付けられたメソッド,フィールドは,型に対して1つ存在することになります. Java言語では実体を作成してプログラムをするのに,型に対して1つしか存在できないstaticが付けられたメソッドに処理を書くのはJavaの書き方としてはあまり適当とは言えません.

プログラムを課題として捉えて,一度書けばあとは捨てると言う考えではある程度以上は上達しません. 後からどのように利用されるか,どのように拡張されるのか,と言う視点も持って取り組むようにしましょう. そのためには,使い捨てになりがちな悪習を止める必要があります. mainの処理を最小限にし,ifforなどがないようなプログラムを書いていきましょう. その指針として,クラス定義の基本形を挙げていますので,この指針に従ってプログラムを書いていきましょう.

キャメルケースって何ですか?

キャメルケース(CamelCase)とは,プログラム中で名前を書くための方法の1つです. クラス名や変数名,メソッド名は,内容を表すために,複数の単語から構成されます. その各単語の頭文字のみを大文字にし,そのまま繋げたものです. ラクダのコブのような形になることから付けられています. 一番最初の単語の頭文字が大文字の場合をアッパーキャメルケース(UpperCamelCase),小文字のものをローワーキャメルケース(lowerCamelCase)と呼びます.

一方,各単語をアンダーバー(_)で繋げる方法をスネークケース(sanke_case),ハイフン(-)で繋げる方法をケバブケース(kebab-case)と呼びます. スネークケースはC言語などで,ケバブケースはHTMLなどで用いられることが多いです. 変数名の付け方は各言語で推奨される方法がありますので,その方法に従うことをお勧めします.

Javaでは一般的に次のような規則で命名されます.

  • クラス名: アッパーキャメルケース(UpperCamelCase
  • 定数:全て大文字のスネークケース(SNAKE_CASE
    • フィールドの型の前にstatic finalをつけると定数として扱われます.
  • その他の名前: ローワーキャメルケース(lowerCamelCase
    • メソッド名,フィールド名,ローカル変数

各命名方法で書いてみると次のようになります.

  • null pointer exception
    • アッパーキャメルケース:NullPointerException
    • ローワーキャメルケース:nullPointerException
    • スネークケース:null_pointer_exception
    • ケバブケース:null-pointer-exception
  • Haruaki Tamada
    • アッパーキャメルケース:HaruakiTamada
    • ローワーキャメルケース:haruakiTamada
    • スネークケース:haruaki_tamada
    • ケバブケース:haruaki-tamada

System.exit は使わない方が良い?

Javaのプログラムを終了するための手段として,System.exit(0)が用意されています. System.exitに渡す数値がステータスコードとなり,プログラムが終了します. これは,他の如何なる処理が動作中であっても強制的に終了させます.

なぜmainメソッドに処理を書かない方が良いのでしょうか」にも示した通り,Javaは型を作成します. これは,作成したプログラムは他のプログラムからも呼び出される可能性がある,と言うことを指します. そのようなJavaプログラムで闇雲に System.exit を呼び出して,プログラムを終了させていると,予想外の動作となることでしょう. 特に,WebアプリケーションをJavaで作っている時に,System.exitを呼び出すと,Webアプリケーション全体を終了させることになります. そのようなことがないよう,System.exitの代わりに,returnで値を返すなどし,System.exitmainメソッド内のみにするなどしましょう.

ヨーダ記法は避けるべき?

ヨーダ記法は,if分などの条件に通常とは異なる順序で記す記法です. スターウォーズのキャラクタであるヨーダが通常の英語の語順とは異なる順序で話すことから名付けられたようです(Wikipedia).

例えば,value30 という数値との比較は,通常 value == 30 と書きますが,ヨーダ記法では,30 == value と書きます. 文字列同士の比較でも,string"Yoda" という文字列の比較を string.equals("Yoda")ではなく,"Yoda".equals(string) と書きます.

if(30 == value) { // value == 30 をヨーダ記法で記述
    ...
}
if("Yoda".equals(string)) { // string.equals("Yoda") をヨーダ記法で記述
    ...
}

このヨーダ記法により,== を間違って = と書くことに起因するエラーや stringnullであった場合に,NullPointerExceptionを避けられるなどのメリットがあるとされます.

しかし今日では, === の間違いは,コンパイラレベルで指摘されるべき内容でありますし, 通常とは異なる順序で書くことによる可読性の低下を許容できないという意見もあります. さらに,NullPointerExceptionを避けられるというのも,nullをそもそも使わないプログラミングスタイルや, Objects.equals を用いることで避けられるため,わざわざ読みにくいヨーダ記法を採用する理由は薄まっていると言えます.

なお,通常の順序とは,通常の文章の読み方に合わせて,変数を左辺,定数を右辺に書きます. 「value は 30 です」という文章と「30 は value です」という文章のどちらが自然に感じるでしょうか. この自然言語の語順と同じであることが読みやすさに繋がり,読みやすさに繋がらないプログラムはできるだけ避けた方が良いです.

参考

なぜメソッドに分けないといけないのでしょうか

プログラムコードをメソッドに分割することで,次のようなメリットが生まれます.

  • 可読性が向上する.
  • メンテナンス性が向上する.
  • 再利用性が向上する.

加えて,メソッドの名前が適切に付けられているとメソッドの中を読まなくても,処理内容がわかるようになります. 例えば,以下のようなプログラムを考えてみましょう.

    void run(String[] args) {
        Integer maxIndex = findMaxIndex(args);
        ArrayList<Integer> results = findPrimes(maxIndex);
        printResults(results);
    }

このように,各メソッドの中身を読まなくても findPrimesmaxIndex という名前から, maxIndex までの素数の一覧を出力するプログラムなのだろうと予想できます. 一方,このようにメソッドに分けられておらず, 全ての処理を run メソッドに書いていると処理内容を全て理解しなければ,何のプログラムかが理解できません.

プログラムは書く時間よりも読む時間の方が圧倒的に長くなります. 書いた瞬間から読まれますし,時間が経ったあとに振り返って読む場合,誰かに見せる場合など,色々な機会で読まれます. そのため,書きやすさよりも読みやすさを重視した方が,費やす時間の合計は少なくなります. そのために,注意深く読んで理解するよりも,パッとみてどのようなプログラムかが想像した後に詳細を読み進めていく方が読むコストは低くなります. これらのことから,読みやすさを重視するためにも,メソッドに分けていきましょう.

どのようにメソッドに分ければ良いのでしょうか.

メソッドに分けるための目安として,行数が使われる場合が多いです. $n$ 行以上になればメソッドに分割しよう,ということですが,この基準となる $n$ は人によってマチマチです. Thought Works アンソロジー という本では, メソッドは3行までと述べています. が,流石に $n=3$ は制限が強過ぎます.

良いコ-ドへの道―普通のプログラマのためのステップアップガイド では $n=30$ 程度と述べています. また,メソッド 分割で検索してみると様々な$n$が提案されています. では,具体的な $n$ の値をどのように決めれば良いかというと,基本的には与えられた規則に従うのが良いでしょう. この授業では,$n$は 5〜20 程度が良いと考えています.

一方で,行数ではなく,処理の意味で分けるべきという意見も多くあります. 例えば,プログラムを書いていて,空行を設けている箇所はないでしょうか. 空行を入れるということは,そこで意味が変わっているケースが多いでしょう. その空行がメソッド分割の鍵になります. 空行の前後を別のメソッドに分割するのが良いでしょう.

インデントを揃えているのにインデントがズレていると指摘を受けました

Emacsを使ってインデントを揃えていると,タブとスペースが混在した状態でインデントされます. タブとスペースを混在させてのインデントがデフォルトになっているためです(ファイルサイズの削減のためという歴史的経緯でこのようになっているようです). そのような状態でターミナルや別のエディタでファイルをみるとインデントがズレた状態になってしまいます.

このようなことにならないよう,Emacsでのインデントもタブとスペースが混在しないように設定する必要があります. Emacsの設定ファイルは,~/.emacs, ~/.emacs.el, ~/.emacs.d/init.el です. この順で検索され,見つかればそれ以降のファイルは読み込まれません. どの設定ファイルを推奨するかは,公式には一切書かれていないようです. ただし,どのファイルも作成されていない場合は,~/.emacs.d/init.el にしておくと良いでしょう.

Emacsのインデントにタブとインデントが混在しないようにするには,Emacsの設定ファイルに次の1行を追加して,Emacsを再起動しましょう.

(setq-default indent-tabs-mode nil)

Javaのソースコードはどこで見られますか

Javaをインストールするとソースファイルもインストールされます. macOSの場合Javaは /Library/Java/JavaVirtualMachines 以下にインストールされています. どのようなものがインストールされているのかを確認してみましょう.

以下のコマンドを入力してください.なお,以下のzulu-17.32.13zulu-8.62.0.19の部分は自分の環境に合わせて読み替えてください. インストール方法や,Javaのバージョンによって微妙に異なる結果になっていますが, おおよそ以下のような構造になっているはずです.

$ ls /Library/Java/JavaVirtualMachines/zulu-8.62.0.19/Contents/Home/
ASSEMBLY_EXCEPTION  demo/       include/  lib/     man/        release  src.zip             Welcome.html
bin/                DISCLAIMER  jre/      LICENSE  readme.txt  sample/  THIRD_PARTY_README
$ ls /Library/Java/JavaVirtualMachines/zulu-17.32.13/Contents/Home/
bin/   demo/        include/  legal/  man/         release
conf/  DISCLAIMER/  jmods/    lib/    readme.txt/  Welcome.html

Java 8 の場合は Home 以下に src.zip があり, Java 11 以降は lib 以下に src.zip が置かれているはずです. この src.zip が Java API のソースコードです. 展開するといくつかのディレクトリが作られますので, unzip -d src path/to/src.zip のように -d src をつけて展開すると良いでしょう.

devcontainer を利用している場合は,ワーキングディレクトリ(/workspaces/lesson??) の兄弟ディレクトリとして/workspaces/javaディレクトリが存在します. そのディレクトリ内(/workspaces/java/src)以下に Java の標準APIのソースコードを置いています.

講義資料に間違いを見つけました.

講義資料に間違いを見つけた場合は,間違いの報告をお願いします. バグ報告は https://github.com/ksuap/bugreport/issues からお願いします. 具体的な報告方法はこちらを参照してください. なお,バグ報告には,GitHub のアカウントが必要となります.

連絡事項・更新履歴