背景
上記の書籍を読んで、Javaによる実践的なソフトウェア開発を
するために必要な知識を得られたので、その中から特に印象に
残ったものをメモしておく。
関連記事
これの前段階である入門編を読んだ際のまとめ記事はこちら。
www.eureka-moments-blog.com
目次
- 背景
- 関連記事
- 目次
- メモ
メモ
2つのインスタンスが等価かを判定するequals()
equals()は、Objectクラスから継承して備える。
2つの変数に入っているインスタンスを比較して等価であるかを
判定できるが、その判定方法はオーバーライドして実装する。
下記のコードはその実装例。
public class Hoge { String fuga; // 後で比較する文字列のインスタンス変数 public boolean equals(Object o) { // 引数はObject型 if (o == this) return true; if (o == null) return false; if (!(o instanceof Hoge)) return false; Hoge h = (Hoge)o; // 先頭や末尾に空白がある場合に取り除くためにtrim()を使う if (!this.fuga.trim().equals(h.fuga.trim())) { return false; } return true; } }
Objectクラスのこういうメソッドは便利だけど、
習慣化するまではオーバーライドをし忘れそうなので
注意したい。
オブジェクト自身のハッシュ値を返すhashCode()
HashSetコレクションの要素であるオブジェクトをremove()で削除する際は、
下記の2段階の処理が行われる。
- 高速だが曖昧な方法で、「大体同じか?」を判定する
- 「大体同じ」な要素にだけ、equals()で「厳密に同じか?」を判定する
1における「大体同じか?」の判定には、インスタンスに対するハッシュ値を
利用するが、それを計算するためにObjectクラスが持つhashCode()を継承して
オーバーライドする必要がある。ハッシュ値とは、インスタンスの内容を数値
として要約したもの。
下記は、オーバーライド時のハッシュ値計算の実装例。
import java.util.Objects; public class Hoge { String fuga; int var; // オーバーライド public int hashCode() { // Objectsクラスのhashメソッド // 任意の個数の引数を受け取り、それに基づいたハッシュ値を生成するAPI // ハッシュ値の比較は単なる整数値の比較なのでequals()より高速 return Objects.hash(this.fuga, this.var); } }
インスタンスのコレクションを作りたくなる時もよくあるので、
このオーバーライドも習慣化しないといけない。
インスタンスの自然順序付け
例えばArrayListによるインスタンスのコレクションがあるとして、
それをCollections.sort()で並び替えるには、そのクラスについて
一般的に想定される並べ順を規定しておく必要がある。
その手段としては、java.lang.Comparableインターフェースを実装
する方法がある。
public class Hoge implements Comparable<Hoge> { int var; // このインスタンス変数の大小で順序付けする // Comparableインターフェースを実装する際は、 // compareTo()のオーバーライドが強制される public int compareTo(Hoge obj) { if (this.var < obj.var) { return -1; } if (this.var > obj.var) { return 1; } return 0; } }
実装するのは面倒だけど、逆に言えば実装次第で自由に
並べ替えのルールが作れるということ。後工程の処理を
効率化するためにコレクションをソートしておくことは
よくやるので、使いこなせるようにしないといけない。
null安全に配慮したOptionalクラス
java.util.Optionalクラスは、下記4つの特徴を持つ、1つのインスタンスを
格納するだけクラス。
- newできず、静的メソッドofNullable()で生成する
- isPresent()を用いて中身がnullか検証できる
- get()で内容を取得できるが、nullなら例外が発生する
- orElse()でnullを置換して内容を取得できる
Optionalクラスは特に、メソッドから戻り値を返す場面で利用され、
nullが格納されている可能性を考慮した処理の記述を呼び出し元に
強制できる。
import java.util.*; public class Hoge { // 文字列sを文字cで挟んで装飾するメソッド // 戻り値にnullの可能性があると呼び出す側に認識させる書き方 public static Optional<String> decorate(String s, char c) { String r; if (s == null || s.length() == 0) { // sがnull or 長さ0ならnullを返す r = null; } else { r = c + s + c; } return Optional.ofNullable(r); } } public static void main(String[] args) { Optional<String> s = decorate("", '*'); // 必然的にnullを考慮した処理を書くことになる System.out.println(s.orElse("nullのため処理できません"); }
関数オブジェクトの代入
Javaでは関数も第1級オブジェクトとして扱うようになっている。
よって、プログラム内で関数を変数に代入できる。
import java.util.function.*; public class Hoge { // 引数の文字列が何文字かカウントする関数 public static Integer len(String s) { return s.length(); } public static void main(String[] args) { // lenメソッドのロジックを変数funcに代入 // ここで代入されているのはメソッドへの参照 Function<String, Integer> func = Hoge::len; // 変数funcに格納されているロジックを引数"Fuga"で実行 int a = func.apply("Fuga"); // 呼び出し System.out.println("文字列Fugaは" + a + "文字"です); } }
標準関数インターフェース
上記で利用したFunctionクラス以外にも、関数オブジェクトを格納する
ための下記のようなインターフェースが用意されている。これらは
標準関数インターフェースと呼ばれる。
戻り値がない関数を格納するConsumerインターフェース
Consumer<String> func = System.out::println;
func.accept("Hoge hoge");
引数がない関数を格納するSupplierインターフェース
Supplier<String> func = System::lineSeparator;
System.out.println("改行します" + func.get());
複数の引数を受け取る関数を格納するBiFunctionインターフェース
// 左から2つ目までのStringは引数の型 // 右端のStringは戻り値の型 BiFunction<String, String, String> func = System::getProperty; System.out.println(func.apply("java.version", "不明"));
ラムダ式の省略記法
下記のようなラムダ式があるとする。
IntToFloatFunction func = (int x) -> { return x * x * 3.14; }
こういったラムダ式は、様々な省略した書き方をすることができ、
それには次に紹介するようなルールがある。
ルール①: 代入時はラムダ式の引数宣言における型を省略可能
// この場合、ラムダ式の代入先である変数の型が持つ唯一の抽象メソッドを // 特定し、その引数宣言に使われる型を自動的に利用する IntToFloatFunction func = (x) -> { // intの型表記を省略 return x * x * 3.14; }
ルール②: ラムダ式の引数が1つのみなら丸カッコを省略可能
IntToFloatFunction func = x -> { // xを囲む丸カッコを省略 return x * x * 3.14; }
ルール③: ラムダ式が単一のreturn文の場合、波カッコとreturnを省略可能
IntToFloatFunction func = x -> x * x * 3.14
個人的にはラムダ式はあまり使いたくないけど、
こうやって簡単に書けるやり方は知っておいて
損はなさそう。
高階関数による宣言的な記述
import java.util.*; public class Hoge { public static void main(String args[]) { List<Fuga> fugas = new ArrayList<>(); // リストの中からvarが0のFugaを探す // 条件だけを指示して探索させる boolean flag = fugas.stream().anyMatch(f -> f.var == 0); } }
宣言的な記述とは、目的だけを表現してその実現方法までは
踏み込まない書き方をいう。
こういうヒトの思考に近い直感的な書き方は可読性の向上に繋がりそう。
タイムゾーンの操作
タイムゾーンは、java.util.TimeZoneクラスを用いて操作する。
例えば、現在のタイムゾーンを取得して表示するコードは下記のようになる。
import java.util.*; public class Huga { public static void main(String[] args) { TimeZone tz = TimeZone.getDefault(); System.out.print("Current Time Zone:"); System.out.println(tz.getDisplayName()); if (tz.useDaylightTime()) { System.out.println("Using Summer Time"); } else { System.out.println("Not Using Summer Time"); } System.out.print("Time Difference from UTC"); System.out.println(tz.getRawOffset() / 3600000 + "Hours"); // convert from millisec to hour } }
ファイル操作時の例外処理
ファイルを開いた後にはclose()を実行して最後に閉じる必要があるが、
状況によってはclose()が実行されずにメソッドが終了してしまう場合がある。
public class Main { public static void main(String[] args) throws IOException { FileWriter fw = new FileWriter("hoge.dat", true); fw.write("fuga"); fw.flush(); // ここで強制終了 fw.close(); // ここのclose()は実行されない } }
こういったことを防ぐために、必ずclose()されることを保証できる
ようなコードを書かないといけない。
以上を踏まえると、正しい例外処理は下記のようなコードになる。
public class Main { public static void main(String[] args) { // tryブロックの外で宣言しnullで初期化しないと、 // finallyブロックでclose()を実行できない FileWriter fw = null; try { fw = new FileWriter("hoge.dat", true); fw.write("fuga"); fw.flush(); } catch (IOException e) { System.out.println("ファイル書き込みエラー"); } finally { // ファイルを閉じるためのfinallyブロック if (fw != null) { try { // close()がIOExceptionを出す可能性があるため再度try-catchで囲む fw.close(); } catch (IOException e2) { } // 失敗しても何もできないためcatchブロックは空にする } } } }
XMLファイルを操作するためのJAXP
例えば、下記のようなXMLファイルがあるとすると、
<person> <name>Mike</name> <age>17</age> <job> <role>Engineer</role> <year>10</year> </job> </person>
JAXPによってファイル中の特定のデータを読み取る
コードは下記のようになる。
import javax.xml.parsers.*; import org.w3c.dom.*; import java.io.*; public class Hoge { public static void main(String[] args) throws Exception { InputStream is = new FileInputStream("sample.xml"); // 文書全体を取得 Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is); // 一番外側のpersonタグ(Element)を取得 Element person = doc.getDocumentElement(); // その中のjobタグ(Element)を取得 Element job = findChildByTag(person, "job"); // その中のroleタグ(Element)を取得 Element role = findChildByTag(job, "role"); // その中の文字列情報(Text)を取得 String str = role.getTextContent(); } // 指定された名前を持つタグの最初の子タグを返す関数 static Element findChildByTag(Element self, String name) throws Exception { NodeList children = self.getChildNodes(); // 全ての子を取得 for (int i = 0; i < children.getLength(); i++) { if (children.item(i) instanceof Element) { Element e = (Element)children.item(i); if (e.getTagName().equals(name)) { return e; } // タグ名を照合 } } return null; } }
OSSライブラリJacksonを使ったJSONファイルの操作
CSVだと力不足、XMLだと大げさ過ぎるというニーズにフィットする
手軽なフォーマットとしてJSON(JavaScript Object Notation)がある。
もともとはJavaScriptで利用されていた独自の形式だったが、今では
広く利用されている。
{ "person": { "name": "Mike", "age": 17, "job": { "role": "Engineer", "year": 10 } } }
Javaの標準APIとしては、基本機能であるJSON-Pと、より発展的な
操作を実現するためのJSON-Bが用意されている。
ただし、歴史的経緯より、OSSライブラリであるJacksonやgsonが
使われる場合が多いらしい。
Jacksonを使うには、まずこちらのサイトを参考にセットアップを
行う必要がある。
sukkiri.jp
JSONファイル中のデータにアクセスする方法は2つあり、一つは下記のように、
内部構造を逐次指示してアクセスする方法である。
こちらのコードでは、Jacksonに準備されているObjectMapperというクラスを
使ってJSONファイルの内容をJsonNodeインスタンスとして読み取り、その
内容を取り出すようにしている。
import com.fasterxml.jackson.databind.*; import java.io.*; public class Hoge { public static void main(String[] args) throws Exception { ObjectMapper mapper = new ObjectMapper(); JsonNode root = mapper.readTree(new File("sample.json"); JsonNode person = root.get("person"); JsonNode job = person.get("job"); System.out.println("name:" + person.get("name").textValue()); System.out.println("role:" + job.get("role").textValue()); } }
そしてもう一つの方法は、「JSONのデータ構造と同じクラスを作っておき、
そこにデータを保存する」というものである。
まずは全体のクラス。
public class JsonData { public PersonData person; }
次に全体クラスのメンバであるPersonDataクラス。
public class PersonData { public String name; public int age; public JobData job; }
次に、PersonDataクラスのメンバであるJobDataクラス。
public class JobData { public String role; public int year; }
そして最後に、上記の3つのクラスを用いてJSONファイルを読み取るクラス。
import com.fasterxml.jackson.databind.*; import java.io.*; public class Hoge { public static void main(String[] args) throws Exception { ObjectMapper mapper = new ObjectMapper(); JsonFileData file = mapper.readValue(new File("sample.json"), JsonData.class); System.out.println("name:" + file.person.name); System.out.println("job:" + file.job.role); } }
javadocによる仕様書の自動生成
JDKに付属しているJavadocを使うことで、作成済みのコードから
プログラムの仕様書を自動生成できる。
このとき、出力される仕様書に解説文を含める場合は、コード内に
その解説文を/* /で囲った特殊なコメントとして記述する。
/** * 口座クラス * このクラスは、1つの銀行口座を表現する */ public class Account { /** 残高 */ private int zandaka; // フィールド直前に書いたコメントは、そのフィールドの解説になる /** 口座名義人 */ private String owner; /** * 送金を行うメソッド * このメソッドを呼び出すと、他の口座に送金する */ public void transfer(Account dest, int amount) { ... } }
また、メソッドのパラメータや戻り値などの記述を含めるには、
Javadocコメント内に@で始まるタグを記述する必要がある。
/** * 口座クラス * @author Shisato * @deprecated 代わりにNewAccountクラスを利用してください * @see NewAccount */ public class Account implements java.io.Serializable { // フィールドの宣言部分は省略 /** * 他口座への振り込みを行うメソッド * @param bank 送金先銀行 * @param dest 送金先口座 * @param amount 送金する金額 * @return 送金手数料 * @exception java.lang.IllegalArgumentException 残高不足のとき */ public void transfer(Bank bank, Account dest, int amount) { ... } }
チーム開発
チーム開発に必要なもの
- 共通の目標
- 共通の言葉
- 共通の手順
アジャイル開発の進め方
2~4週間の短期間で工程を反復するスパイラル型開発プロセスを実践する。
動くソフトウェアを頻繁に顧客に提示し、すぐにフィードバックを得られ、
素早く顧客の要求に対応できるようになる。
1回の工程反復期間のことはイテレーションあるいはスプリントと呼ぶ。
スクラム
世界で最も多く活用されているとされるアジャイル開発方法論。
プログラミングに関する言及はほとんどなく、製品開発チームのメンバーの
役割や連携をどう実現するかを重視している。
スクラムでは、各メンバーが次の3つの役割を担うとしている。
①プロダクトオーナー
チームが開発すべき製品に関する最終責任者。
作るべき機能の優先順位を決めるなど、開発を方向付ける。
②開発メンバー
実際に開発をするメンバー。適宜お互いの作業を手助けしながら、
チーム全体の成果に全員が責任を持つ。
③スクラムマスター
スクラムに関する専門家。チームが正しくスクラムをできるよう指導や
助言を行う。また、円滑なチーム活動とチームの成長を手助けする。
プロダクトバックログ
製品が実現すべき要件や機能を、優先度が高い順にリストアップしたもの。
プロダクトオーナーが管理する。
スプリントバックログ
現在のスプリントで実現すべき機能と、その達成に必要な作業項目のリスト。
各機能の進捗状況などが一目でわかるように、タスクボードに整理して提示
することが多い。開発メンバー全員で管理する。
障害リスト
チームの活動を阻害している事柄を、懸念度が高い順にリストアップしたもの。
スクラムマスターが管理する。
スプリント計画ミーティング
プロダクトバックログの中から、今回のスプリントで取り組む機能を選び出し、
具体的な開発方法や作業量などを見積もり、スプリントバックログに取り込む。
スプリントの最初に1回だけ実施する。
デイリースクラム
開発チームが全員で1か所に集まり、進捗状況や問題点などを共有するミニ会議。
スプリント期間中に毎日行う。
スプリント・レビュー
実際に製品を利用するユーザーや依頼主に、現時点での完成品を触ってもらい、
評価とフィードバックを得る。スプリント期間中の最後に1回だけ実施する。
スプリント・レトロスペクティブ
自分達チーム自体の活動や成長について振り返りを行う。
次回のスプリントでより効率的な開発を行うためのヒントを得る。
スプリント・レビューの後に1回だけ実施する。
デイリースクラムの実践ポイント
基本的には各自が下記について発表する。
- 昨日やったこと
- 今日やること
- その他チームに影響がありそうなこと
また、実践の際は下記を心がける。
- 長くても15分以内に終わらせる
- 割り込まない、議論は始めない
- 全員に対して共有する
Threadクラスによる並列処理
Threadクラスを利用して、例えば下記2つの処理を同時に実行する
プログラムを考える。
- 9から0までのカウントダウンを表示する
- カウントダウン中に「STOP」と入力すると、カウントダウンを停止する
スレッドの実現方法としては、まずThreadクラスを継承してrun()メソッドを
オーバーライドする。このとき、run()には別スレッドで処理したい内容を
書き込む。
そして、別スレッドの実行を開始したい場所で上記のクラスをインスタンス化し、
start()を呼び出す。
以上を踏まえると、作りたいプログラムのコードは下記のようになる。
まずは、表示を行う別スレッドを定義するクラスを作る。
import java.util.concurrent.*; public class PrintThread extends Thread { public void run() { for (int i = 9; i >= 0; i++) { System.out.print(i + ".."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { ; } } } }
そして、そのクラスをインスタンス化して別スレッドを開始するクラスを作る。
import java.util.*; public class Hoge { public static void main(String[] args) { System.out.println("止めるには「STOP」と入力してください"); System.out.println("カウントダウンを開始します"); Thread t = new PrintThread(); // 別スレッドクラスのインスタンス化 t.start(); // 別スレッドの開始 String input = new Scanner(System.in).nextLine(); // 入力処理 System.out.println("入力文字列:" + input); System.out.println("停止処理は未作成です"); } }
synchronizedによる排他制御
マルチスレッドプログラミングにおいて注意したいのが、複数のスレッドが
同時に同じデータにアクセスして壊してしまうこと。こういった問題を避ける
ために、1つのスレッドがあるデータにアクセスしている間、他のスレッドは
待機するようにコントロールする。これをスレッドの同期や調停という。
Javaには、スレッドを調停するための仕組みが準備されていて、それらを利用し
「複数のスレッドから同時に利用しても安全なクラスやメソッド」は、
スレッドセーフな設計であるという。
スレッドの調停を行うには、次のようなsynchronizedブロックを利用する。
synchronized (対象インスタンス) { // スレッドの競合から保護したい処理 }