ファイルを読み込むメソッドの引数はInputStreamが良い、と思う。

## はじめに
ファイルを読み込むメソッドを作成しようとする。引数として考えるのが、
・ファイル名のString型
・ファイルのFile型
が考えられるが、InputStreamが良いのではないか、と結論付けた。
理由としては「テストしやすい」からである。
StringやFileだと、どうしてもファイルの場所を指定するので、ファイル構造を変更すると、ファイルの場所をすべて書き換えないといけない。そこでテストに使用するファイルはgetResourceAsStream()で指定し、テストクラスと同じ場所に置いてしまうのが、一番柔軟性が高いと判断した。

以下、テスト方法や実際の実装方法をまとめた。

##実際のソース
ということで、テスト対象のメソッド。引数はInputStreamにしてます。

```java:InputStreamMethod.java
/**
 * 一行で「グループ名:メンバー1,メンバー2,メンバー3」という形式のファイルを読み込む。<br/>
 * メンバーをキーにして、どのグループに所属しているか確認できるMapを返す。
 * @param inputStream 読み込みファイル
 * @return メンバーをキーにして、どのグループに所属しているか確認できるMap
 */
public Map<String, String> read(InputStream inputStream){
	try(BufferedReader reader = new BufferedReader(
			new InputStreamReader(inputStream, Charset.forName("UTF-8")))){

		Map<String, String> result = new HashMap<>();

		String line;
		while((line = reader.readLine()) != null){

			// グループ名
			String group = line.substring(0, line.indexOf(":"));

			// メンバー名
			for (String member: line.substring(group.length() + 1).split(",")){
				result.put(member, group);
			}
		}

		return result;

	}catch (IOException | IllegalArgumentException | StringIndexOutOfBoundsException e) {
		// 複数例外catchを使用。初めて使うw

		System.err.println("グループの設定ファイルがおかしい");
		return null;
	}
}
```

さて、「いざファイルを読み込ませよう！」と思う前に、不安だから一行だけの読み込ませてみるか。でも、一行のファイルを作るのはめんどくさいから、ByteArrayInputStreamで引数に渡すInputStreamをテストソース内で作成しよう。

```java:InputStreamMethodTest.java
@Test
public void read1行(){

	// 一行お試し。メモリー上にストリームを作成して、テストを実施する
	ByteArrayInputStream stream = new ByteArrayInputStream("東北:青森県,岩手県,秋田県,宮城県,山形県,福島県".getBytes());

	InputStreamMethod target = new InputStreamMethod();
	Map<String,String> result = target.read(stream);

	assertThat(result.get("青森県"), is("東北"));
	assertThat(result.get("秋田県"), is("東北"));
	assertThat(result.get("福島県"), is("東北"));
}
```

複数行だと、どうかな？

```java:InputStreamMethodTest.java
@Test
public void read3行(){
	StringBuffer sb = new StringBuffer();
	sb.append("北海道:北海道").append(System.lineSeparator());
	sb.append("東北:青森県,岩手県,秋田県,宮城県,山形県,福島県").append(System.lineSeparator());
	sb.append("関東:茨城県,栃木県,群馬県,埼玉県,千葉県,東京都,神奈川県").append(System.lineSeparator());

	ByteArrayInputStream stream = new ByteArrayInputStream(sb.toString().getBytes());

	InputStreamMethod target = new InputStreamMethod();
	Map<String,String> result = target.read(stream);

	assertThat(result.get("北海道"), is("北海道"));
	assertThat(result.get("青森県"), is("東北"));
	assertThat(result.get("神奈川県"), is("関東"));
}
```

よし、大丈夫そうだ。じゃあ、実際のファイルを読ませるか。

```java:InputStreamMethodTest.java
@Test
public void read() {

	InputStreamMethod target = new InputStreamMethod();
	Map<String,String> result = target.read(getClass().getResourceAsStream("InputStreamMethodTest.txt"));

	assertThat(result.get("北海道"), is("北海道"));
	assertThat(result.get("青森県"), is("東北"));
	assertThat(result.get("秋田県"), is("東北"));
	assertThat(result.get("福島県"), is("東北"));
	assertThat(result.get("神奈川県"), is("関東"));
	assertThat(result.get("鳥取県"), is("中国"));
	assertThat(result.get("徳島県"), is("四国"));
	assertThat(result.get("沖縄県"), is("九州"));

	assertThat(result.get("うどん県"), is(nullValue()));
}
```

テストで読み込むテキストファイル。InputStreamMethodTest.javaと同じ場所に配置するのがミソです。

```text:InputStreamMethodTest.txt
北海道:北海道
東北:青森県,岩手県,秋田県,宮城県,山形県,福島県
関東:茨城県,栃木県,群馬県,埼玉県,千葉県,東京都,神奈川県
中部:山梨県,長野県,新潟県,富山県,石川県,福井県,静岡県,愛知県,岐阜県
近畿:三重県,滋賀県,京都府,大阪府,兵庫県,奈良県,和歌山県
中国:鳥取県,島根県,岡山県,広島県,山口県
四国:香川県,愛媛県,徳島県,高知県
九州:福岡県,佐賀県,長崎県,熊本県,大分県,宮崎県,鹿児島県,沖縄県
```

ファイルのフォーマットがおかしかったときのテストもしておこう。

```java:InputStreamMethodTest.java
@Test
public void readファイルエラー(){

	ByteArrayInputStream stream = new ByteArrayInputStream("東北青森県,岩手県,秋田県,宮城県,山形県,福島県".getBytes());

	InputStreamMethod target = new InputStreamMethod();
	assertThat(target.read(stream), is(nullValue()));
}
```

では、肝心のメイン文を、と。

```java:InputStreamMethod.java
public static void main(String[] args) throws FileNotFoundException {

	File file = new File("src/streamtest/InputStreamMethodTest.txt");
	if (! file.exists()){
		System.err.println("ファイルがありません:" + file.getAbsolutePath());
		System.exit(-1);
	}

	InputStreamMethod target = new InputStreamMethod();

	Map<String, String> map = target.read(new FileInputStream(file));
	map.keySet().stream()
	.map(key -> key +":" + map.get(key))
	.forEach(System.out::println);
}
```

ということで、完成。

## ひとまず、まとめ
以前書いたソースの引数をStringにしていたが、Maven導入でフォルダ構成がちょっと変わったので、格好悪い感じになっています。どうやったら、うまくいくか、と考えたとき、InputStreamを引数にするのが柔軟性があるかなと思いました。実際業務でも書いてみましたが、いい感じに収まった気がします。

## closeって入れ子でも呼ばれるのか
こう書いてみて、引数にとったInputStreamってちゃんとcloseしているのか、不安になりました。InputStreamMethod.readの内部でByteArrayInputStream streamを使用したInputStreamReaderをtryで囲んでいるが、この場合closeが呼ばれているか気になったのでテスト。呼ばれているようだ。

```java:InputStreamMethodTest.java
@Test
public void close(){

	final StringBuilder sb = new StringBuilder();

	ByteArrayInputStream stream =
			new ByteArrayInputStream("東北:青森県,岩手県,秋田県,宮城県,山形県,福島県".getBytes())
			{
				@Override
				public void close() throws IOException{
					sb.append("stream was closed");
					super.close();
				}
			};

	InputStreamMethod target = new InputStreamMethod();
	Map<String,String> result = target.read(stream);

	assertThat(sb.toString(), is("stream was closed"));
}


@Test(expected = IOException.class)
public void closeFile() throws IOException {

	InputStream stream = getClass().getResourceAsStream("InputStreamMethodTest.txt");
	InputStreamMethod target = new InputStreamMethod();
	Map<String,String> result = target.read(stream);

	stream.read();
	org.junit.Assert.fail("ファイルをcloseした状態でreadしているので、IOExceptionが発生するはず");
}
```

## ファイル出力もOutputStreamを引数に？
ついでにファイル出力も引数をOutputStreamにした方が良いかとやってみた。

```java:OutputStreamMethodTest.java
@Test
public void write_1データ() throws IOException {

	OutputStreamMethod target = new OutputStreamMethod();
	target.put("北海道", "北海道");

	ByteArrayOutputStream out = new ByteArrayOutputStream();
	target.write(out);

	assertThat(out.toString(), is("北海道:北海道"));
}

@Test
public void write_1行() throws IOException {

	OutputStreamMethod target = new OutputStreamMethod();
	target.put("青森県", "東北");
	target.put("岩手県", "東北");
	target.put("秋田県", "東北");

	ByteArrayOutputStream out = new ByteArrayOutputStream();
	target.write(out);

	assertThat(out.toString(), is("東北:岩手県,秋田県,青森県"));
}

@Test
public void write_3行() throws IOException {

	OutputStreamMethod target = new OutputStreamMethod();
	target.put("北海道", "北海道");
	target.put("青森県", "東北");
	target.put("岩手県", "東北");
	target.put("秋田県", "東北");
	target.put("茨城県", "関東");
	target.put("栃木県", "関東");
	target.put("群馬県", "関東");
	ByteArrayOutputStream out = new ByteArrayOutputStream();
	target.write(out);

	String[] expected = {
			"北海道:北海道"
			,"東北:岩手県,秋田県,青森県"
			,"関東:栃木県,群馬県,茨城県"
			};

	assertThat(out.toString(), is(String.join(System.lineSeparator(), expected)));
}
```

```java:OutputStreamMethod.java
Map<String, String> map = new HashMap<>();

public String put(String key, String value ){
	return map.put(key, value);
}

public void write(OutputStream outputtStream) throws IOException{

	BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputtStream));

	String[] groups =
			map.values().stream()
			.distinct()
			.sorted()
			.toArray(count->new String[count]);


	for (int i = 0; i < groups.length; i++){

		final String group = groups[i];

		String[] members =
				map.keySet().stream()
				.filter(p-> group.equals(map.get(p)))  // 同じグループのみでフィルタリングしてる
				.sorted()
				.toArray(count -> new String[count]);

		writer.write(group  + ":" + String.join(",", members));

		// 最終グループでないときは、改行を入れる
		if (i != groups.length -1){
			writer.newLine();
		}
	}

	writer.flush();
}

public static void main(String[] args) throws FileNotFoundException, IOException {
	File file = new File("src/streamtest/OutputStreamMethodTest.txt");
	if (file.exists()){
		System.err.println("ファイルがあります:" + file.getAbsolutePath());
		System.exit(-1);
	}

	OutputStreamMethod target = new OutputStreamMethod();
	target.put("北海道", "北海道");
	target.put("青森県", "東北");
	target.put("岩手県", "東北");
	target.put("秋田県", "東北");
	target.put("茨城県", "関東");
	target.put("栃木県", "関東");
	target.put("群馬県", "関東");

	target.write(new FileOutputStream(file));
}
```

よし。いけてる。

ソースは
https://github.com/xaatw0/quiita/tree/master/src/streamtest
に落ちてます。