2016-11-17 第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のストリームを扱う型は下の図のように,ReaderWriterInputStreamOutputStreamの4種類に大別できます.

ストリームの種類

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

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

この講義では,テキストデータを扱う型(Reader/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型が存在しますが,この講義では,FileReaderBufferedReader,そして,InputStreamReaderの3つの型を扱います.

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

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

void readMethod(File file) throws IOException{
    BufferedReader in = new BufferedReader(new FileReader(file));
    // 上記の処理を区別して書くと,次のような処理になる.
    //     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の責任を転嫁してください.

この点に不安がある人は,例外機構を復習してください.

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

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

実行例

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

Writer型

出力を扱うWriterを利用する方法を確認しましょう.Writer型もReader型と同じく, 多くの型が存在します.典型的な型を次に紹介します.

ここに挙げた以外にもいくつかのWriter型が存在しますが,この講義では,FileWriterPrintWriter の2つの型のみを利用します.

典型的なファイルへのデータの書き込み方法.

void writeMethod(File file, String message) throws IOException{
    PrintWriter out = new PrintWriter(new FileWriter(file));
    // 上記の処理を区別して書くと次のような処理になる.
    //     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の例外に対応しなければいけません. ここでも同じように,writeMethodを呼び出しているメソッドに責任を転嫁するため,throws節 でIOException を投げると宣言しましょう.

例題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

ページのトップに戻る

練習問題

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としてください.

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.*;
    ...途中省略
}

ページのトップに戻る

まとめ