第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 # 全く同じはずなので,何も出力されない.
$

まとめ

まとめ