JavaFX の String 変数のデフォルト値は空文字列だけど null と等価扱いされる件(に物申す)

プロローグ
タイトルはちょっと語弊があるかもしれませんので、詳細は bluepapa32 さんのブログエントリから

後者は、多くの NPE によるバグを救ってくれるので、非常にありがたいように思いますが...
実は この仕様のために思わぬバグを生むこともあるのです。
今回、自分も思い切りハマったのですが、次の典型的なコードは意図しない動きをします。
このコードでファイルなどを読み込んだ場合、空行が含まれているとそれ以降の行が読み込まれないのです。


事件はここで起きました

    var r = new java.io.BufferedReader(in);

    var s: String;
    while ((s = r.readLine()) != null) {
        println(s);
    }


容疑は偽証罪

実は JavaFX では "" == null は 真 になります。

bluepapa32 さん、これはナイスな指摘です!ありがとうございます!
僕もうっかりしてました。僕が以前書いたエントリで、僕もこの書き方をしていました。(訂正済み)

上記は Java では良く使われるイデオムです。ではこれはどう対処すれば良いのでしょうか。僕が思いついた案は四つ。でもベストなソリューションは他にあるかもしれません。


案1
public int read() throws IOException を使う。

    var r = new BufferedReader(...);
    var b: Integer;
    var buff: StringBuilder = StringBuilder {};
    while ((b = r.read()) != -1) {
        buff.append(b as Character);
    }
    println(buff.toString());

でもこれは1文字ずつの処理なのでパフォーマンスが問題視される可能性はあります。


案2
read(char[] cbuf, int off, int len) throws IOException を使う。

    var r = new BufferedReader(...);
    var s = "0000000000";
    var cbuf: nativearray of Character = s.toCharArray() as nativearray of Character;
    var buff: StringBuilder = StringBuilder {};
    var num: Integer;
    while ((num = r.read(cbuf, 0, 10)) != -1) {
        buff.append(cbuf, 0, num);
    }
    println(buff.toString());

nativearray を使います。但しこの nativearray は実験的に追加され今後のバージョンでは消える可能性もあるという話もあり注意が必要です。(でも JavaFXDuke 本(これは勝手な呼称で実際はJavaFX: Developing Rich Internet Applications (Java Series))の Chapter 3 では普通に解説されているんですけどね...)
それから、char[] 型の変数を作る為に、物凄く奇怪なコードになっています。注意しなければならないのは Character[] 型では駄目ということです。
Character[] 型 を第一引数に指定しても read(char[], int, int) のシグネチャにマッチせずにコンパイルできません。
あれこれ試してみたのですがリテラル式で char 配列を作れませんでした。それで一旦適当な長さの文字列を作り、toCharArray() で要素が10個の char[] を作っているわけです。
ただこれは JavaFX 版イデオムとして受け入れられないですよね。あまりにも奇怪で変態すぎます。(完全に趣味で書きました)


案3
Java で書く。
もう Java に InputStream を渡して処理させる。それ以上でもそれ以下でもないです。はい。


案4
それでもやっぱり public String readLine() throws IOException を使う。
は?それじゃ振り出しに...。いえ、readLine() の戻り値を Object 型の変数で受け取ります。

    var r = new java.io.BufferedReader(in);

    var s: Object;
    while ((s = r.readLine()) != null) {
        println(s);
    }

Object 型の変数は String 型のスーパークラスなので当然、文字列を代入できます。
そして Object 型の変数は空文字列が代入されても条件式の中で null と等しいと評価されることはありませんし、null を代入されたからといって空文字列に置き換えられることはありません。
いろいろと意見もあるかもしれないですが、僕はこの案4が良いかな(ましかな)と思っています。

id:bluepapa32 さんはどう対処したんでしょう。
どなたかご意見、もっと良い解決法をお持ちの方がいたら、是非ともコメントをお願いします。


事件は解決?真相は根本にあるのでは?
やはりちょっと頂けないですよね。bluepapa32 さんが言われているとおりこれにハマってしまう人はきっと多そうです。
(僕が)案4が良いと言ってもやはり Java と同じ(それよりもっと良いのはすごく簡単な)イデオムを使いたいですし。それにちょっと時間が経過してしまえばまたうっかり String 型の変数で宣言してバグを出してしまいそうです。
やはり一番は言語的にサポートしてもらいたいです。
例えば、null の代入・保持を許容して条件式では null と評価され、文字列を評価するコンテキストやメンバへアクセスするときは、空文字列として評価するみたいな。ヌルポを防ぐ機構は維持したままで良い。

    //これは言語仕様の改善案
    str:String = null;
    if (str == null) {
        println("str is null.");
    } else {
        println("str is not null.");
    }
    println("size: {str.length()}");
    println("content: {str}");
    str = '';
    if (str == null) {
        println("str is null.");
    } else {
        println("str is not null.");
    }

実行結果
str is null.
size: 0
content: 
str is not null.

みたいな。
Groovy の ?. を真似るのも良いかも - var?.hoge() - 知れないけど、? を付け忘れるとヌルポになってしまうしね。


エピローグ
というより、JavaFX Script の中で java.io.InputStream を使わせること自体どうなんだろうと思う。
javafx.io.http.HttpRequest を前に初めて使ったときも「え?なんでここで InputStream なの?」って、ちょっと退きました。
本当に少ないコード量で簡単にリッチなアプリを作ることを可能にしてくれて「どうもありがとう」と感じる部分はもちろんあるんだけど、なんかアナログだなあ感じさせられる部分も実は若干ながらある。

どうなんでしょうね。


追記:
言語仕様の改善案に以下のコード断片とその結果を追加しました。これがないと意味を成さないので。
  str = ''; if {...} else {...}