[小西ホームページ]   [目次・索引]   [前の授業]   [次の授業]

情報処理IIIA(Javaプログラミング入門)第12回

目次 索引
12.1 ファイル入出力
12.1.1 バイトと文字
12.1.2 ファイルとストリーム
12.1.3 バイト単位の入出力
12.1.4 データ単位の入出力
12.2 演習12
12.3 レポート課題

12.1 ファイル入出力

12.1.1 バイトと文字

コンピュータのデータの単位は ビットbit )です。 ただし、これは理論上の話です。 現実のコンピュータ・システムは、 バイトbyte )を単位としています。 1バイトは8ビットであることを思い出してください。

1バイトで表現できるデータには次のようなものがあります(2 8 = 256)。

また、2バイトで表現できるデータは次の通りです(2 16 = 65,536)。

4バイトで表現できるデータは、

などです。

Javaは、文字( char 型)を1バイトではなく2バイトで表現します。 つまり、漢字なども文字として取り扱えるように設計されています。 このため、ちょうど1バイトで表現されるデータ型として、バイト( byte )が用意されています。 なお、特に断らない限り、整数は int 型になります。 int 型は4バイトで表現されます。

文字は、内部では整数として処理されています。 この、文字から整数への対応を 文字符号character code )と呼びます。 Javaの採用している文字符号は、 Unicode と呼ばれる体系です。 Unicodeは ASCII という体系の拡張になっています。 主な文字のASCII符号は次の通りです。

表 12.1  主な文字のASCII符号
制御文字 水平タブ 改行 復帰 スペース
表示 '\t' '\n' '\r' ' '
符号 9 10 13 32
文字 ! " # $ % & ' ( ) * + , - . /
符号 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
文字 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
符号 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
文字 @ A B C D E F G H I J K L M N O
符号 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
文字 P Q R S T U V W X Y Z [ \ ] ^ _
符号 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
文字 ` a b c d e f g h i j k l m n o
符号 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
文字 p q r s t u v w x y z { | } ~
符号 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126

文字をバイトにしたり、整数を文字にするには、型の変換を指定する必要があります。 型の変換は キャストcast )と呼ばれます。 式 exp の値を型 type に変換するには、

(type)exp

と書きます。

12.1.2 ファイルとストリーム

Javaを用いますと、ファイルの内容を読み込んだり、ファイルにデータを書き込んだりできます。 Javaは、ファイルをバイトの列として取り扱います。 この列は、具体的には ストリームstream )とよばれるデータ構造です。 データの列という点で、ストリームは配列に似ています。 ストリームと配列の違いは次の通りです。

ストリームがどうしてもイメージできない人は、工場のベルトコンベアを思い浮かべてください。 ベルトに載ったデータが次々と自分に近づいてくるのが入力ストリーム、ベルトにデータを載せてだんだん自分から離れていくのが出力ストリームです。

12.1.3 バイト単位の入出力

はじめに、ファイルの内容を読み込む方法を説明します。 ファイルの内容の読み込みは、次の手順で行ないます。

  1. ファイルの入力ストリームを生成する。
  2. そのストリームを開く。
  3. そのストリームから1バイトずつデータを読み込む。
  4. データが必要なくなったりデータが終わったりしたら、そのストリームを閉じる。

Javaでは、 FileInputStream クラスのインスタンスを生成しますと、入力ストリームの生成とストリームを開くことが同時に行なえます。 ファイル file に対して

FileInputStream stream = new FileInputStream(file);

としますと、 stream に入力ストリームが格納され、このストリームが開きます。

データの読み込みには、インスタンスメソッド read を用います。 式

stream.read()

の値は、0以上255以下の整数か、-1になります。 前者は、読み込んだ1バイトのデータを整数と見なしたものです。 後者は、そのストリームのデータが終わったことを意味します。 通常、読み込んだデータをいったん int 型の変数に格納し、それが-1でなければ必要に応じてキャストするといったことを行ないます。

stream.close();

としますと、ストリームが閉じます。

配列の要素をすべて取り出すには次のようにしました。

int[] a = {...};
int i;
...
for (i = 0; i < a.length; i++) {
    ... a[i] ...
}

これに対して、入力ストリームからすべてのデータを読み込むには次のようにします。

FileInputStream fis = new FileInputStream("data.txt");
int in;
...
for (in = fis.read(); in != -1; in = fis.read()) {
    ... in ...
}
...
fis.close();

以下のプログラムは、ファイルの内容を読み込んで、それを画面に出力するものです。

cities1.txt
Berlin
Moscow
Paris
FileInputTest.java
/* 1*/ import java.io.*;
/* 2*/
/* 3*/ class FileInputTest {
/* 4*/     public static void main (String[] args) throws IOException {
/* 5*/         FileInputStream fis = new FileInputStream(args[0]);
/* 6*/         int in;
/* 7*/         char c;
/* 8*/         for (in = fis.read(); in != -1; in = fis.read()) {
/* 9*/             c = (char) in;
/*10*/             System.out.print(c);
/*11*/         }
/*12*/         System.out.println();
/*13*/         fis.close();
/*14*/     }
/*15*/ }
b00a001@Ampere:~% cat cities1.txt
Berlin
Moscow
Paris
b00a001@Ampere:~% java FileInputTest cities1.txt
Berlin
Moscow
Paris

b00a001@Ampere:~%

1行目は、入出力関係の組み込みクラスを利用するということです。 4行目の throws IOException は、入出力関係のエラー(正確には例外)が起こるかもしれないが、その際に特別なことはしないという意味です。 これらを省略しますと、プログラムはコンパイルできません。 5行目で、コマンドライン引数をファイル名と見なして入力ストリームを生成します。 9行目で、読み込んだデータを文字に変換します。

次に、ファイルへデータを書き込む方法を説明します。 ファイルへのデータの書き込みは、次の手順で行ないます。

  1. ファイルの出力ストリームを生成する。
  2. そのストリームを開く。
  3. そのストリームに1バイトずつデータを書き込む。
  4. データが終わったら、そのストリームを閉じる。

入力ストリームと同様、 FileOutputStream クラスのインスタンスを生成しますと、出力ストリームの生成とストリームを開くことが同時に行なえます。 ファイル file に対して

FileOutputStream stream = new FileOutputStream(file);

としますと、 stream に出力ストリームが格納され、このストリームが開きます。

データの書き込みには、インスタンスメソッド write を用います。

stream.write(byte);

としますと、ストリームにバイト byte が書き込まれます。 通常、途中の計算は int 型で行ない、最後に byte 型に変換して write メソッドを呼び出すといったことを行ないます。

stream.close();

としますと、ストリームが閉じます。

以下のプログラムは、コマンドライン引数の内容を順にファイルに書き込むものです。 第1引数がファイル名、第2引数以降が書き込む内容になります。

/* 1*/ import java.io.*;
/* 2*/
/* 3*/ class FileOutputTest {
/* 4*/     public static void main (String[] args) throws IOException {
/* 5*/         FileOutputStream fos = new FileOutputStream(args[0]);
/* 6*/         int i, j;
/* 7*/         byte b;
/* 8*/         String s;
/* 9*/         for (i = 1; i < args.length; i++) {
/*10*/             s = args[i];
/*11*/             for (j = 0; j < s.length(); j++) {
/*12*/                 b = (byte) s.charAt(j);
/*13*/                 fos.write(b);
/*14*/             }
/*15*/             b = (byte) '\n';
/*16*/             fos.write(b);
/*17*/         }
/*18*/         fos.close();
/*19*/     }
/*20*/ }
b00a001@Ampere:~% java FileOutputTest cities2.txt London "New York" Tokyo
b00a001@Ampere:~% cat cities2.txt
London
New York
Tokyo
b00a001@Ampere:~%

10行目で第 i + 1 引数の文字列が s に格納されます。 12行目でその j 番目の文字が取り出され、これをバイトに変換します。 15行目では、制御文字の改行をバイトに変換しています。 なお、コマンドライン引数はスペースで区切られますが、ダブルクオート( " )で囲みますと、スペースも文字列の一部と見なされます。

以下のプログラムは、ファイルをコピーするものです。 入力ストリームと出力ストリームの両方を用いています。

cities2.txt
London
New York
Tokyo
FileCopyTest.java
/* 1*/ import java.io.*;
/* 2*/
/* 3*/ class FileCopyTest {
/* 4*/     public static void main (String[] args) throws IOException {
/* 5*/         FileInputStream fis = new FileInputStream(args[0]);
/* 6*/         FileOutputStream fos = new FileOutputStream(args[1]);
/* 7*/         int in;
/* 8*/         byte b;
/* 9*/         for (in = fis.read(); in != -1; in = fis.read()) {
/*10*/             b = (byte) in;
/*11*/             fos.write(b);
/*12*/         }
/*13*/         fis.close();
/*14*/         fos.close();
/*15*/     }
/*16*/ }
b00a001@Ampere:~% cat cities2.txt
London
New York
Tokyo
b00a001@Ampere:~% java FileCopyTest cities2.txt cities3.txt
b00a001@Ampere:~% cat cities3.txt
London
New York
Tokyo
b00a001@Ampere:~%

12.1.4 データ単位の入出力

Javaにとってファイルはバイトの列ですので、バイト単位でファイルの読み書きができれば、ファイルの処理は十分なはずです。 しかし、テキストファイルの内容を一行ずつ読み込みたいときに、改行かどうかを調べながら1バイトずつ読み込まなくてないけないのでは不便です。 また、数100をファイルに書き込みたいときに、 1 , 0 , 0 と分解しなくてはいけないも面倒です。

組み込みのクラスである DataInputStream クラスを用いますと、1行ずつ読み込むことが簡単にできます。 同様に、 PrintStream クラスを用いますと、数や文字列などのデータを自動的に分解して書き込むことができます。

この2つのクラスは、フィルタとしての機能を提供します。 ベルトコンベアのイメージですと、フィルタとは、ベルトにのっているデータを取り出して、加工して、またベルトに戻す人のことです。 フィルタによって、ファイルから読み込んだバイトの列が文字列にまとめられたり、データが分解されてファイルに書き込むバイトの列になったりするのです。

DataInputStream クラスのインスタンスメソッド readLine によって、入力ストリームから1行分の文字列が読み込めます。 ここで1行とは、 \n , \r\n , \r または入力ストリームの終わりまでの文字列です。 入力ストリームが終わってから読み込もうとしますと、特別な値 null が返ってきます。

printStream クラスのインスタンスメソッド print を用いますと、画面に出力する要領でデータを分解して出力ストリームに書き込めます。 println ですと、データの後に改行が追加されます。

次のプログラムは、ファイルの内容を読み込んで画面に出力するものです。 上記の FileInputTest.java と比較してください。

cities1.txt
Berlin
Moscow
Paris
FileInputTest2.java
/* 1*/ import java.io.*;
/* 2*/
/* 3*/ class FileInputTest2 {
/* 4*/     public static void main (String[] args) throws IOException {
/* 5*/         FileInputStream fis = new FileInputStream(args[0]);
/* 6*/         DataInputStream dis = new DataInputStream(fis);
/* 7*/         String s;
/* 8*/         for (s = dis.readLine(); s != null; s = dis.readLine()) {
/* 9*/             System.out.println(s);
/*10*/         }
/*11*/         fis.close();
/*12*/         dis.close();
/*13*/     }
/*14*/ }
b00a001@Ampere:~/java% cat cities1.txt
Berlin
Moscow
Paris
b00a001@Ampere:~/java% java FileInputTest2 cities1.txt
Berlin
Moscow
Paris
b00a001@Ampere:~/java%

6行目で、ファイルの入力ストリームからフィルタ付き入力ストリームを生成します。 8行目で、 null 、すなわち入力ストリームが終わるまで、入力ストリームから一行ずつ読み込みます。 12行目で、入力ストリームを閉じます。

次のプログラムは、コマンドライン引数の内容をファイルに書き込むものです。 同様に、FileOutputTest.java と比べてください。

/* 1*/ import java.io.*;
/* 2*/
/* 3*/ class FileOutputTest2 {
/* 4*/     public static void main (String[] args) throws IOException {
/* 5*/         FileOutputStream fos = new FileOutputStream(args[0]);
/* 6*/         PrintStream prs = new PrintStream(fos);
/* 7*/         int i;
/* 8*/         for (i = 1; i < args.length; i++) {
/* 9*/             prs.println(args[i]);
/*10*/         }
/*11*/         fos.close();
/*12*/         prs.close();
/*13*/     }
/*14*/ }
b00a001@Ampere:~/java% java FileOutputTest2 cities2.txt London "New York" Tokyo
b00a001@Ampere:~/java% cat cities2.txt
London
New York
Tokyo
b00a001@Ampere:~/java%

6行目で、ファイルの出力ストリームからフィルタ付き出力ストリームを生成します。 9行目で、コマンドライン引数の文字列を分解することなく出力ストリームに書き込みます。 12行目で、出力ストリームを閉じます。

次のプログラムは、ファイルをコピーするものです。 FileCopyTest.java と見比べてください。

cities2.txt
London
New York
Tokyo
FileCopyTest2.java
/* 1*/ import java.io.*;
/* 2*/
/* 3*/ class FileCopyTest2 {
/* 4*/     public static void main (String[] args) throws IOException {
/* 5*/         FileInputStream fis = new FileInputStream(args[0]);
/* 6*/         DataInputStream dis = new DataInputStream(fis);
/* 7*/         FileOutputStream fos = new FileOutputStream(args[1]);
/* 8*/         PrintStream prs = new PrintStream(fos);
/* 9*/         String s;
/*10*/         for (s = dis.readLine(); s != null; s = dis.readLine()) {
/*11*/             prs.println(s);
/*12*/         }
/*13*/         fis.close();
/*14*/         dis.close();
/*15*/         fos.close();
/*16*/         prs.close();
/*17*/     }
/*18*/ }
b00a001@Ampere:~/java% cat cities2.txt
London
New York
Tokyo
b00a001@Ampere:~/java% java FileCopyTest2 cities2.txt cities3.txt
b00a001@Ampere:~/java% cat cities3.txt
London
New York
Tokyo
b00a001@Ampere:~/java%

12.2 演習12

シーザー暗号に基づいて暗号化を行なうJavaアプリケーションを作成してください。 ここで、 シーザー暗号Caesar cipher )とはシーザー(カエサル)が用いたとされる暗号化の方法です。 また、 暗号化encryption )とは、秘密にしたいメッセージ( 平文plaintext )を特定の方法で第三者には読めない形式( 暗号文ciphertext )にすることです。

暗号を用いた通信は、一般的には次の通りです。 まず、メッセージの送信者は、暗号化の方法の他に、 key )とよばれるデータを決定します。 そして、メッセージと鍵から暗号文を構成し、受信者に送信します。 受信者は、暗号化の方法とその鍵を知っています。 暗号文を受けとりますと、鍵を使って暗号文を元のメッセージに戻します( 復号decryption )。 第三者は、暗号化の方法は知っていても鍵を知らないので、元のメッセージに戻すことが困難になります。 なお、鍵を使わずに元のメッセージに戻すことを 解読cryptanalysis )と言います。

ここでは簡単のため、平文はアルファベット大文字の並びとします。 また、スペースや各種記号は解読のヒントになりますので取り除いておきます。

シーザー暗号では、鍵 K は整数です。 平文のアルファベットを一文字ずつ K だけずらしたアルファベットに置き換えて、暗号化を行ないます。 例えば K = 3 でしたら、平文

MYNAMEISKYOKOAZUMA(私の名前は東京子です。)

は暗号文

PBQDPHLVNBRNRDCXPD

に暗号化されます。 アルファベットの置き換えは以下の表の通りです。 YB に置き換えられることに注意してください。

表 12.2  シーザー暗号でのアルファベットの置き換え(鍵 K = 3 の場合)
平文 A B C D E F G H I J K L M
暗号文 D E F G H I J K L M N O P
平文 N O P Q R S T U V W X Y Z
暗号文 Q R S T U V W X Y Z A B C

この表を逆にたどりますと復号ができます。 K = 26 - 3 = 23 としてもう一度暗号化することでも復号可能です。

なお、シーザー暗号はすぐに解読されます。 なぜならば、 K = 1 の場合、 K = 2 の場合、…、 K = 25 の場合をすべて確かめ、その中から意味の通るものを選べばよいからです。 ここでシーザー暗号を取り上げたのは、暗号の仕組みに慣れるためです。

プログラムの方針は特に指定しません。 方針が思い付かない人は、以下のプログラムを穴埋めしてください。 このプログラムは、第1引数が鍵、第2引数が平文のファイル名、第3引数が暗号文のファイル名となります。 穴埋めでは、アルファベット大文字のみを置き換え、他の改行などはそのままにすることと、ずらして文字 Z をすぎてしまったら26だけ戻すことがポイントです。

plain.txt
MYNAMEISKYOKOAZUMAIAMNINETEENYEA
RSOLDIAMASTUDENTATTOKYOWOMANSCHR
ISTIANUNIVERSITYILIVEINKOKUBUNJI
CaesarCipher.java
import java.io.*;

class CaesarCipher {
    public static void main (String[] args) throws IOException {
        int key = Integer.parseInt(args[0]);
        FileInputStream fis = new FileInputStream(args[1]);
        FileOutputStream fos = new FileOutputStream(args[2]);
        int in, out;
        byte b;
        char c;
        ??? // 入力ストリーム fis から 1 バイト読み込み、こ
        ??? // れを入力データ in に格納する。この格納と以下
        ??? // の手続きを in が -1 でない限り繰り返す。
        ??? // in を文字 c に変換する。c が大文字ならば出力
        ??? // データ out を in + key とする。ただし、out
        ??? // が文字 'Z' をすぎたら、out から 26 を引く。
        ??? // もともと大文字でなければ out を in とする。
        ??? // どちらにしても、out をバイト b に変換し、出
        ??? // 力ストリーム fos に b を書き込む。
        fis.close();
        fos.close();
    }
}
b00a001@Ampere:~% cat plain.txt
MYNAMEISKYOKOAZUMAIAMNINETEENYEA
RSOLDIAMASTUDENTATTOKYOWOMANSCHR
ISTIANUNIVERSITYILIVEINKOKUBUNJI
b00a001@Ampere:~% java CaesarCipher 3 plain.txt cipher.txt
b00a001@Ampere:~% cat cipher.txt
PBQDPHLVNBRNRDCXPDLDPQLQHWHHQBHD
UVROGLDPDVWXGHQWDWWRNBRZRPDQVFKU
LVWLDQXQLYHUVLWBLOLYHLQNRNXEXQML
b00a001@Ampere:~% java CaesarCipher 23 cipher.txt decrypt.txt
b00a001@Ampere:~% cat decrypt.txt
MYNAMEISKYOKOAZUMAIAMNINETEENYEA
RSOLDIAMASTUDENTATTOKYOWOMANSCHR
ISTIANUNIVERSITYILIVEINKOKUBUNJI
b00a001@Ampere:~%

12.3 レポート課題

今日の演習12に従ってJavaプログラムを作成し、そのプログラムをkonishi@twcu.ac.jpあてにメールで提出してください。 メールには、学生番号、氏名、科目名、授業日(12/6)を明記してください。


[小西ホームページ]   [目次・索引]   [前の授業]   [次の授業]

2002年1月8日更新
konishi@twcu.ac.jp
Copyright (C) 2002 Zenjiro Konishi. All rights reserved.