2017-06-01 第8回目 ファイル入出力(2/2)
本日のテーマ
ファイルの入出力
ストリーム(Stream)
Javaで入出力を扱うには,ストリームという概念の理解が必要です. ストリームとは,データを流れとして扱う仕組みです. データがデータソース(Data Source; データの源,入力元)から流れてくる入力ストリームと, データをデータの出力先(Data Destination; 出力先)に流し込む出力ストリームの2つがあります.
下の画像が入力ストリームを表しています. データソースはファイルやネットワークから,もしくは,別のプログラムであるなど, 入力ストリームを扱う側は知らなくても良いようになっています. とにかく,ストリームを扱う側は,必要なデータが流れてくることさえ理解していれば良いわけです. このような性質のため,Javaでは,あらゆる入出力にストリームを利用します.
入力元がファイルであろうと,ネットワークの先であろうと,受け取る方法は同じです. 入力元がどこであろうと,同じように扱うために,ラッピング という作業が必要になります.
下の画像が出力ストリームを表しています.出力も入力ストリームと同じく, ラッピングを利用して,出力先を気にすることなくデータを書き込めます.
ファイル,ネットワーク,また,メモリに書き込むときにもストリームを利用します.
ラッピング(Wrapping)
先ほど,ファイルやネットワーク,さては,メモリの入出力までストリームを利用できると述べました. 異なるものを同じように扱うために,ラッピング(wrapping)と言う概念を利用します. ラッピングは,アダプタパターン(Adapter Pattern)と言われる場合もあります.
ラッピングとは,包むや包装するという意味です.
その意味のとおり,ラッピングを行うには,とある実体を別の型で覆います.
右図のように,別の型で覆うことで,内側の型(Inner
)を外側の型(Outer
)として扱えるようになります.
// Inner.java
public class Inner{
void method1(){
// some operation...
}
}
// Outer.java
public class Outer{
Inner inner
void method2(){
inner.method1();
}
}
// Main.java
public class Main{
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のストリームを扱う型は下の図のように,Reader
,Writer
,InputStream
,OutputStream
の4種類に大別できます.
上図を縦のグループで見ると,Reader
/Writer
がテキストデータを
扱う型,InputStream
/OutputStream
がバイナリデータを扱う型です.
バイナリデータとは,画像ファイルや音声ファイルなどです.
文字を扱う型では,HTMLやプログラムのソースファイルなど,テキストデータを扱います.
一方,上図を横のグループで見ると,Reader
とInputStream
が入力ストリームを
表しており,Writer
とOutputStream
が出力ストリームを表しています.
そして,Reader
と呼ばれる型にも複数の型が存在します.Writer
も同じく, 複数のWriter
型が存在します.
この講義では,テキストデータを扱う型(Reader
/Writer
)のみを扱います.
ただし,バイナリデータを扱う型であってもテキストデータを扱う型と使い方はほぼ同じです.
import文
4種類のストリームを扱う場合は,import文が必要です.
それぞれの型の先頭にjava.io.
をつけたものを import してください.
例えば,Reader
と Writer
を使いたい場合は,次の2行がプログラムの先頭に必要です.
import java.io.Reader;
import java.io.Writer;
Reader
型の例えば,FileReader
,BufferedReader
を利用するときには,次の2行が必要です.
import java.io.BufferedReader;
import java.io.FileReader;
複数の import
文をまとめるために,import java.io.*;
としても構いません.
先ほどのReader
とWriter
のimportの2行を1行で書けるようになります.
Reader型
まずは,入力を扱うReader
を利用する方法を確認します.Reader
と一口に言っても,
以下のように多くの型がReader
型には存在します.
FileReader
- ファイル(
File
型)からテキストデータを読み込むためのReader
型.
- ファイル(
BufferedReader
- バッファリングを行うための
Reader
型. - データソースから行単位で文字列を読み込める.
- 他の
Reader
は文字単位でしかデータを読み込めない.
- 他の
- バッファリングを行うための
StringReader
- 文字列(
String
型)からテキストデータを読み込むためのReader
型.
- 文字列(
LineNumberReader
- 読み込んだ行数を数える
Reader
型.
- 読み込んだ行数を数える
InputStreamReader
InputStream
型の入力ストリームをReader
型に変換するReader
型.
ここに挙げた以外にもいくつかのReader
型が存在しますが,この講義では,FileReader
とBufferedReader
,そして,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
)します.
次に,構築した FileReader
をBufferedReader
に渡し,
行単位で読み込めるBufferedReader
を構築します.
そして,1行ずつ読み込むには,BufferedReader
型が持つreadLine
メソッドを
呼び出します.readLine
メソッドは,1行読み込み,その結果をString
型で返します.
もう読み込めるデータがない場合は,null
を返します.
そのため,while((line = in.readLine()) != null)
という繰り返しで最後まで順に読み込めるようになります.
最後にこれ以上ストリームを利用しないため,close
メソッドを呼び出します.close
メソッドは一番外側のReader
に対してだけ呼び出せば良いです.
ラップされているReader
は外側のReader
が閉じられると,自動的に閉じられます.
入出力を扱うには,必ず IOException
という検査例外に対応しなければいけません.
どこかで入出力エラーが起こる可能性が排除できないためです.
そのため,メソッドのシグネチャに,throws節を追加しています.
もちろん,readMethod
メソッドを呼び出しているメソッドにも throws
節で,IOException
の責任を転嫁してください.
この点に不安がある人は,例外機構を復習してください.
ただし,FileReader
に渡すファイルが存在しない場合
(exists
メソッドがfalse
を返す場合),FileNotFoundException
という例外が投げられます.
なお,FileNotFoundException
はIOException
としても扱えますので,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.*;
...途中省略
}
Writer型
出力を扱うWriter
を利用する方法を確認しましょう.Writer
型もReader
型と同じく,
多くの型が存在します.典型的な型を次に紹介します.
FileWriter
- ファイル(
File
型)に書き込むためのWriter
型.
- ファイル(
PrintWriter
printf
やprintln
,print
が利用できるWriter
型.- 他の
Writer
型は文字単位でしか出力できない.
- 他の
StringWriter
- 文字列(
String
型)に書き込むためのWriter
型.
- 文字列(
OutputStreamWriter
OutputStream
型をWriter
型に変換するためのWriter
型.
ここに挙げた以外にもいくつかのWriter
型が存在しますが,この講義では,FileWriter
,
PrintWriter
の2つの型のみを利用します.
典型的なファイルへのデータの書き込み方法.
void writeMethod(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
型を構築します.
次に,構築したFileWriter
をPrintWriter
に渡し,文字列の出力が
可能なPrintWriter
を構築します.
そして,文字列をファイルに書き込むには,PrintWriter
型の変数out
に対してprint
メソッドを呼び出して,文字列を書き込んでいます.
他に,println
やprintf
メソッドも用意されています.
最後に,これ以上ストリームを利用しないため,close
メソッドを呼び出しています.close
メソッドを
呼び出すと,ラップされているFileWriter
も閉じられます.
読み込み時と同じように,IOException
の例外に対応しなければいけません.
ここでも同じように,writeMethod
を呼び出しているメソッドに責任を転嫁するため,throws
節
でIOException
を投げると宣言しましょう.
1文字単位で書き込む場合は,Writer
型のwrite
メソッドを利用してください.write
メソッドにInteger
型の値を渡せば適切な出力先に書き込まれます.
1文字単位の読み込みで取得した Integer
型の値を渡すのが典型的な使い方でしょう.
例題2. 指定された行数を出力するコマンド
ここでは,ファイルに値を書き出してみましょう.クラス名をOutputN
とし,
コマンドライン引数で数字とファイル名を受け取ってください.
コマンドライン引数で受け取った数字を1から順にカウントアップして指定行数を
指定されたファイルに出力してください.
実行例
$ ls
OutputN.java OutputN.class
$ java OutputN 3 file3.txt
$ ls
OutputN.java OutputN.class file3.txt
$ cat file3.txt
1
2
3
$ java OutputN 10 file10.txt
$ ls
OutputN.java OutputN.class file3.txt file10.txt
$ cat 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
で内容の確認をしています.
練習問題
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
import java.io.*;
public class Cat{
$ java Head 3 Cat.java
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
としてください.
コマンドライン引数にファイル名を受け取ってください.
また,標準入力から値を受け取るようにしましょう.上記のようにBufferedReader
を
構築した後は,Cat
の時と同じように入力を受け取れば良いです.
入力が終わればnull
が返ってきますので,自動的にループを抜けるようになっています.
出力例
$ cat hoge
cat: hoge: No such file or directory
$ cat Cat.java | java Tee hoge
import java.io.*;
...途中省略
}
$ cat hoge
import java.io.*;
...途中省略
}
まとめ
- Javaでの入出力はストリームという流れで表される.
- 入力はデータソース(Data Source)からデータが流れてくる.
- 出力は出力先(Data Destination)にデータを流し込む.
- 利用するときは,ラッピング(Wrapping)を行う.
- Javaでは,4種類のストリームが用意されている.
- 文字データの入力は
Reader
,- 各種
Reader
型- ファイルからテキストデータを読み込む
FileReader
型. - バッファリングしてテキストデータを読み込む
BufferedReader
型. - 文字列からテキストデータを読み込む
StringReader
型. - 読み込んだ行数を数える
LineNumberReader
型. InputStream
型をReader
に変換するInputStreamReader
型.
- ファイルからテキストデータを読み込む
Reader
の典型的な使い方.
- 各種
- 文字データの出力は
Writer
,- 各種
Writer
型- ファイルにテキストデータを書き込む
FileWriter
型. printf
,println
,print
を利用できるPrintWriter
型.- 文字列にテキストデータ書き込む
StringWriter
型. OutputStream
型をWriter
に変換するOutputStreamWriter
型.
- ファイルにテキストデータを書き込む
Writer
の典型的な使い方.
- 各種
- バイナリデータの入力は
InputStream
, - バイナリデータの出力は
OutputStream
. - 使用するときには,
import
が必要.
- 文字データの入力は
- 文字列にある文字列が含まれているかを確認する
stringA.contains(stringB)
stringA
にstringB
が含まれていればtrue
,含まれていなければfalse
.