javaはやっぱり長いので、ruby風にしてみる例

最もタメになる「初心者用言語」は Javaという記事の中で、

長いですね。
読むのもイヤになります。
Rubyなら三行で書けるのに。

f = open( "hoge.txt" )
f.each {|line| print line}
f.close

最もタメになる「初心者用言語」は Java

とあったので、上記ruby例に近い形で同じ動作をするように、改造してみた。大部分を流用させて頂いてます!ありがとうございます!!

下記のFRunnerクラスとFクラスをコピペして、hoge.txtを用意してFRunner.mainを実行してみてください。環境はjava5以降、他に必要なものはないはず。

まずはmainメソッド

for文などの言語方言は別として、出来るだけ近い形で記述してみた。

public class FRunner {
    public static void main(String[] args) {
        F f = F.open("hoge.txt");
        for (String line : f.lines) System.out.println(line);
        f.close();
    }
}

mainメソッドに書いてあること

記述方針を1行ずつ。

  • F f = F.open("hoge.txt");
    • 冒頭「F f」の宣言部は、そういう言語仕様が利点でもあるのでよしとする。
    • F.open("hoge.txt")は、new F("hoge.txt")としてもいいんだけど、「Fを新たにする」よりは「Fを開く」という方が意味的に自然なので、こういう記述にした。いわゆるFactoryパターン(でいいですよね…?)。
  • for (String line : f.lines) System.out.println(line);
    • javaにはeachなんてないので、for文。for-each文と呼ばれたりする。java5以降で使える表現。
    • {}ブロックを使わずにとりあえず1行で書く。
    • System.out.printlnを呼ぶ部分は「print line」にあわせてprintメソッドを用意してもいいんだけど、とりあえずシンプルに。
  • f.close();
    • 括弧は省けない。メソッドには()を付ける言語仕様なので。そんなに変でもないはず。

実装するFクラス

mainメソッドの形で呼ばれて動くように、Fクラスを作ってみた。長い。

import java.io.*;
import java.util.*;
import java.nio.charset.Charset;

public class F {
    public final Iterable<String> lines;
    protected final FileInputStream fileInputStream;
    
    protected F(FileInputStream fileInputStream) {
        this.fileInputStream = fileInputStream;
        this.lines  = new Iterable<String>(){
            protected final LineIteretor lineIteretor = new LineIteretor();
            public Iterator<String> iterator() {
                return lineIteretor;
            }
        };
    }
    
    public static F open(String fileName) {
        try {
            // Fileオブジェクトを作って、
            // Fileオブジェクトからファイル入力ストリームを作って、
            // Fオブジェクトを生成し、返却する。
            return new F(new FileInputStream(new File(fileName)));
        } catch (FileNotFoundException e) {
            System.err.println("ファイルがないです><");
            throw new RuntimeException(e);
        }
    }
    
    public void close() {
        try{
            // ファイル入力ストリームを閉じる⇒関連リソースは閉じられる。
            fileInputStream.close();
        }catch(IOException e){
            throw new RuntimeException(e);
        }
    }
    
    protected class LineIteretor implements Iterator<String> {
        // 実行環境で標準な文字セットを取得して、
        // 文字列リーダーを作って、
        // バッファリング機能を備えた文字列リーダーを作る。
        protected final BufferedReader bufferedReader =
            new BufferedReader(new InputStreamReader(fileInputStream, Charset.defaultCharset()));
        
        protected String nextLine;
        protected boolean notReadYet = true;
        
        protected void readNextLine() {
            try {
                nextLine = bufferedReader.readLine();
                notReadYet = false;
            } catch (IOException e) {
                System.err.println("ファイル入力エラーです><" + e.getMessage());
                // エラーの時は勝手に閉じてあげることにする。
                close();
                throw new RuntimeException(e);
            }
        }

        public boolean hasNext() {
            if (notReadYet) readNextLine();
            return nextLine != null;
        }
        
        public String next() {
            if (hasNext()) {
                String currentLine = nextLine;
                readNextLine();
                return currentLine;
            } else {
                throw new IllegalStateException();
            }
        }
        
        @Deprecated
        public void remove() {
            close();
            throw new UnsupportedOperationException();
        }
    }
}

Fクラスに(主に)書いてあること

publicなインタフェースのみ。他、興味があればコメントなどでどうぞ。

  • public static F open(String fileName)
    • ファイルを開いて、Fクラスを生成して、返却する。
  • public final Iterable lines
    • ファイルのそれぞれの行に対するイテレータを管理するオブジェクト…ってわかりづらい。Iterableなオブジェクトはfor-each文でぶん回せるので、このインタフェースを用意。
    • Iteratorとして、内部クラスであるLineIteretorを使うようにした。
  • public void close()
    • ファイルを閉じる。閉じると、関連するBufferedReaderやInputStreamReaderも同時に閉じられるので、敢えて記述しない。
  • 例外は全てRuntimeException扱いで投げ直してます。

長くて良いことは、全くないです!!

長いのは優しさだよ
Javaソースコードは長いですが、初心者がソースを追うときに手がかりがいっぱいあるのは良いことです。
最もタメになる「初心者用言語」は Java

何故こんな「回りくどい」方法を取ったかというと、長い記述をそのまま使うのは全く良くないことだから、に尽きます。ソースを追うための手がかりほしいといっても、処理をするソース(今回の場合はFクラスのソース)が公開されてればソースを追うことは可能です。そのベタベタじゃなくオブジェクト化・構造化されたソースを追うために、javaIDEが必須と言って良いくらいに相性がいいわけで。

というか上記の例も、rubyでも内部を追っていくと同じ様なことをしているはずです。違うのは、親切で直感に近いインタフェースが用意されていることくらいで、「じゃ、まねして作っちゃえ」が今回のエントリの主眼です。

釈迦に説法かも知れませんが、長いソースが1つならまだ問題ないですけど、2つ3つ…と量産されるようになると試験項目はn倍で増えてくし、バグも伝染するし、仕様変更の「水平展開」も相当面倒になるし、長いソースで良いなんてことは全くないです。それよりも短く直感的に記述でき、必要に応じて処理内容を追える状態の方が良いです。できれば必要に応じて変更できる状態なら越したことはありません。

そして、残念ながら、今まで私が見てきた「java使えるひと」は「長い記述を長いまま記述する」ことを良しとしてきた人が多いです。誰が決めたでもない「テンプレート」や「コーディングルール」に従って、次々とコピペで長いソースが出来上がり、「あ、バグだ、全部直さなきゃ」。こんな状況が続く限りjavaはdisられ続けるでしょうし、これをjavarubyしたところで状況は好転しないでしょう。

…というのは、かなりグチ混じりですね。id:syttruさん、八つ当たりっぽくてすいません><

あと…

  • 上の例示は大した検証もしてないので、バグがあるかもしれません><
  • ファイルの文字コードをMS932で決め打ちにしてるけど、MS932以外の文字コードのファイルでも受け付けるようにしてみてるのも面白いかも。⇒間違ってました!!これは実行環境に依存します!!*1
  • LineIteretor#remove()では例外を投げてるけど、F.close()をした方がいいのかどうなのか…。@Deprecatedをつけた方が良いのかもしれないです。⇒そうした。
  • import文の見た目が長いので、*を使ってみた。普段はeclipse使ってるから、長いとかあまり気にならないけど。
  • やっぱり、明示的にリソースをcloseする義務があるってのが、面倒だよなぁ。LineIteretor#readNextLine()で例外が発生しても、引き続きそのファイルにアクセスしたい場合とかもあるし。
    • この辺り、rubyってどうしてるんでしょうか?GCみたいに不要なリソースは勝手にcloseしてくれるんでしょうか?それとも例外処理の思想が違う??教えて、エロい人><
      • javaでもfinalize()でcloseする様子。すぐ終了するような小さいプログラムの場合、律儀にcloseする必要はないかも知れない。将来的に流用しないなら。
    • 「引き続きそのファイルにアクセスしたい場合」ってどんな場合だろ?