タグ「Java」が付けられているもの

Why to say I never use Lombok

Many Java developers use or watch Project Lombok. It aims to reduce boilerplate code in Java (If you want to know the detail, you have to read the document.) But I never use Lombok because of following reason.

In the past days, I was also a user of Lombok and many boilerplate code was reduced my project. But some day I think, "Is the code called the boilerplate really unnecessaly?" And I believe that accessor methods, some constructors, equals/hashCode, toString, etc. was born with necessaly to some reasons. If not, there are no reason of existents.

After the reject of Lombok, I must code more and more. But I don't regret my decision. Now when I create a class, I contemplate it's mean, role, access level, extention points for future, etc. If I've used Lombok, I may not contemplate such things.

I say to be clear, Lombok is a so powerful and useful tool for development Java applications. But I never use on my own reason.

Puzzle : variable arity parameter

この記事は Java Puzzlers Advent Calendar 2016 の 22 日目です。

問題

以下の文の実行結果はどのようになるでしょうか?

System.out.println(Objects.hash());

(1) コンソールに 0 が表示される

(2) コンソールに 1 が表示される

(3) NullPointerException がスローされる

(4) コンパイルエラーとなる

解答

(2) コンソールに 1 が表示される

解説

Objects.hash( ) メソッドは、hashCode を計算してくれる便利なメソッドです。そのシグネチャは以下の通りとなっています。

public static int hash(Object... values)

仮引数は Object クラスの可変長引数だけのため、引数が 0 個であっても呼び出しは可能です。従って選択肢 (4) はあり得ないとわかります。それ以外の選択肢の可能性については、Objects.hash( ) メソッドのソースコードを見てみないと判断できません。直感的に選択肢 (3) はなさそうな気もしますが...

Objects.hash( ) の実装は以下の通りとなっています。

public static int hash(Object... values) {
  return Arrays.hashCode(values);
}

何だこれ!ただのラッパーじゃないか!

仕方がないので、Arrays.hashCode(Object[]) の実装も見てみましょう。

public static int hashCode(Object a[]) {
  if (a == null)
    return 0;
  
  int result = 1;
  
  for (Object element : a)
    result = 31 * result + (element == null ? 0 : element.hashCode());
  
  return result;
}

これで答えの半分は出たようなものです。残りは、仮引数の可変長引数が 0 個だった場合の挙動です。

可変長引数はいわゆる Syntax Sugar であり、実引数では配列として扱われます。例えば、仮引数の可変長引数 Object... values は、実引数では Object[] values となります。ここで、可変長引数の数は配列の要素数と一致するため、可変長引数で引数がない場合には実引数は要素数ゼロの配列となります。

要素数ゼロの配列を受け取った Arrays.hashCode(Object[]) は、まず最初の if 文で 0 を返すことはありません (要素数ゼロの配列は null にはなりません)。その後、result が 1 で初期化されますが、要素数が 0 のため hashCode の計算を行う for 文を一度も実行しません。従って、戻り値は result が初期値のまま 1 となります。この値がそのまま Objects.hash( ) の戻り値になりますので、正解は選択肢 (2) となります。

なお、Arrays.hashCode(Object[]) が採用している計算式は、hashCode の計算式としてよく知られているものですので、覚えておいて損はありません。参考まで、Stream API で書き換えると以下のようになります。

if (a == null)
return 0;

return Arrays.stream(a) .filter(e -> e != null) .mapToInt(e -> e.hashCode()) .reduce(1, (left, right) -> left * 31 + right);

この問題は、本来であれば Objects.hash( ) の内部実装を知らないと解けない問題です。ただし、可変長引数の実体が配列であることを知っており、hashCode 計算に関する一般的な知識があれば消去法で解くことができます。

  1. 可変長引数 Object... values は仮引数 0 個でも問題ないため、(4) は除外
  2. hashCode の計算において null の場合は値を 0 として計算するのが基本であるため、NullPointerException をスローする必然性はなく、従って (3) は除外
  3. 逆に hashCode が 0 となる場合は原則として対象が null のため (*)、対象が null でない場合は通常 0 以外の値と見なすことが可能で、従って (1) を除外
  4. 最後に (2) が残り、かつ、これが正解である

(*) 例外として、Byte、CharacterShort、IntegerLongnull でなくても <code<hashCode の戻り値が 0 になる場合がある (保持する値が 0 の場合)

一般的な可変長引数を取るメソッドの挙動としては、選択肢 (1) または (2) が自然でしょう。選択肢 (3) のように振る舞う実装は可能ですが、null ではない引数に対して NullPointerException をスローするのは相応しくありません。どうしても例外をスローしたい場合は IllegalArgumentException などの方が適切でしょう。選択肢 (4) は問題外、可変長引数のことを全く分かっていない証拠です。

余談 #1 -- C/C++ における可変長引数の扱い

C/C++ の可変長引数は、Java のような Syntax Sugar ではなく、本当に実引数の数が変動します。stdarg.h (C++ の場合は cstdarg を推奨) で宣言されている関数 (というかマクロ) を使用して実引数の可変部を取得するのですが、タイプセーフではないし個数自体も実引数の固定部から読み取れる情報で判断しなければなりません。

C/C++ の場合は引数の型または個数 (あるいはその両方) の判定に誤りがあると容易にスタックのオーバーフローやアンダーフローが発生し、しかもその種のバグは気づきにくいです (22 年前の教訓)。

先人達からプロダクション・コードで scanf 関数は絶対に使うなと言われる理由は、scanf 関数の可変長引数はユーザー入力に依存するため、固定部に埋め込んだ引数の型と個数に関する情報とは矛盾した入力がなされる可能性があり、かつそれをプログラム側で阻止する手立てがないためです。

Java の可変長引数は妥協の産物のような仕様ですが、C/C++ のそれと比較すると タイプセーフで個数も確定した、安心して使用できるものに仕上がってはいるのです。

余談 #2 -- 可変長引数の内部実装の概要

C/C++ の関数、Pascal のプロシージャと関数は、引数をスタックに積んで、呼び出し先でそれをスタックから取り出しています。

Pascal と Modula では最初の引数からスタックに積むため取り出しは最後の引数から行われますが、可変長引数に関する情報を保持する最初の引数が最後に取り出されるため、Pascal は可変長引数に必要な情報を得られず、従って可変値用を引数を処理できません。。

C/C++ の規定値 (処理系依存ですが基本的には __stdcall または __cdecl 呼び出し) では最後の引数からスタックに積むことで最初の引数から取り出しを行えるようなトリックを仕掛けています。これにより最初の引数を取り出した後、可変長引数の処理に必要な残りの引数のデータ型と個数を判定できる仕組みになっているわけです (前述の scanf 関数のようにアテにならない場合もあります)。

C/C++ ではオプションとして Pascal 等と同じ振る舞いをする __fastcall (旧: __pascal) 宣言もあり、こちらを使うと可変長引数が封印される代わりに関数呼び出しが若干高速化されるメリットがあります。Win32 API では処理効率優先のため原則として __pascal 宣言を行っています (つまり Win32 API では可変長引数は使用できません)。

JavaScipt の引数は、実は数が決まっていません。通常は宣言された引数に対する処理を記述するのですが、未宣言の追加引数 (可変長引数) を取得手段は用意させていなすり

余談 #3 -- まとめ

  • Java の可変長引数は Syntx Suga である
  • C/C++ の可変長引数は、実引数の数が執変動する
  • JavaScipt でも可変長引は扱うこと可能はある

Puzzle : String concatination

この記事は Java Puzzlers Advent Calendar 2016 の 15 日目です。

問題

以下の式文において、評価後の s の値はどれになるでしょうか?

String s = "1" + 1 + 1;

(1) "111"

(2) "12"

(3) "1"

解答

(1) "111"

解説

文字列結合に用いられる + 演算子は、加算演算子 + をオーバーロードしたものです。Java では C++ と異なりプログラマが演算子をオーバーロードすることはできませんが、言語仕様でのオーバーロードは存在します。

C++ における演算子のオーバーロードでは、オペランドの数と式の評価順位については変更することができません。これは Java においても同じです。

文字列結合 s1 + s2 は、s1.concat(s2) に置き換えることができます。問題の式が評価される過程を、括弧を用いて表現すると、以下のようになります。

  1. "1" + 1 + 1
  2. ("1" + 1) + 1
  3. ("1" + "1") + 1
  4. ("1".concat("1")) + 1
  5. ("11") + 1
  6. "11" + 1
  7. "11" + "1"
  8. "11".concat("1")
  9. "111"

繰り返しになりますが、C++ の演算子オーバーロードでは評価順位まで変更することはできません。Java の文字列結合 + は加算演算子 + のオーバーロードであり、C++ と同じルールが適用されます。

Puzzle : Exception wrapper

この記事は Java Puzzlers Advent Calendar 2016 の 9 日目です。

問題

以下の式文のうち、コンパイルできないものはどれでしょうか? (複数回答可)

(1) new java.lang.reflect.InvocationTargetException();

(2) new java.lang.reflect.InvocationTargetException(new Exception());

(3) new java.lang.reflect.InvocationTargetException(new IOException());

(4) new java.util.concurrent.ExecutionException();

(5) new java.util.concurrent.ExecutionException(new Exception());

(6) new java.util.concurrent.ExecutionException(new IOException());

(7) new java.io.UncheckedIOException();

(8) new java.io.UncheckedIOException(new Exception());

(9) new java.io.UncheckedIOException(new IOException());

解答

(1) new java.lang.reflect.InvocationTargetException();

(4) new java.util.concurrent.ExecutionException();

(7) new java.io.UncheckedIOException();

(8) new java.io.UncheckedIOException(new Exception());

解説

この問題では、3 種類の例外クラスについて取り上げました。その役割を簡単にまとめます。

java.lang.reflect.InvocationTargetException

リフレクション API でメソッドまたはコンストラクタを呼び出したとき、呼び出し先がスローした例外をラップするために用いられます。実際に呼び出し元で何がスローされたのかは、原因例外を取得して分析する必要があります。なお、呼び出し元が例外をスローしない場合には、InvocationTargetException がスローされることはありません (ただし throws 句には含まれるので、対応する catch ブロックは用意する必要があります)。

InvocationTargetException は必ず原因となる例外を引数にとってインスタンスを生成します (引数を持たないコンストラクタは protected 宣言のため、通常使用できません)。

参考まで、私は、InvocationTargetException の原因例外を以下のように仕分けしています。

catch (InvocationTargetException e) {
    // InvocationTargetException は必ず原因例外を持つ
    Throwable t = e.getCause();
    
    if (t instanceof Error) {
        // 原因が Error → キャストして再スローする
        // そもそも Error をプログラムで処理仕様とすることに無理がある
        throw (Error) t;
    } else if (t instanceof RuntimeException) {
        // 原因が RuntimeExeption → キャストして再スローする
        // RuntimeException を処理してもあまり意味がない
        throw (RuntimeException) t;
    } else if (t instanceof Exception) {
        // 原因が Exception → 被チェック例外にラップしてスローする
        // チェック例外にラップしてもいいけど、使い勝手が...
        ...
    } else {
        // 原因はそれ以外 = ほほ間違いなく Throwable そのもの
        // これが扱いに一番困る (でも投げてくるメソッドは存在する)
        ...
    }
}

java.concurrent.ExecutionException

Cuncurrency Utilities で使用される例外クラスで、タスクとして実行した Callable インタフェースの実装がスローした例外をラップします。

実際に呼び出し元で何がスローされたのかは、原因例外を取得して分析する必要があります。なお、呼び出し元が例外をスローしない場合には、ExecutionException がスローされることはありません (ただし throws 句には含まれるので、対応する catch ブロックは用意する必要があります)。

ExecutionException は必ず原因となる例外を引数にとってインスタンスを生成します (引数を持たないコンストラクタは protected 宣言のため、通常使用できません)。

java.io.UncheckedIOException

IOException をラップするランタイム例外です。チェック例外である IOException を、意味合いを変えずに非チェック例外に変換することを目的としています。Stream API がチェック例外とあまり相性が良くないことと関連していると思われます。

UncheckedIOException は必ず原因となる IOException を引数にとってインスタンスを生成します。引数を持たないコンストラクタは用意されていません。選択肢 (8) がコンパイルエラーとなるのは、IOException のコンストラクタが IOException しかラップできないためです。

Puzzle : Binary operator expression

この記事は Java Puzzlers Advent Calendar 2016 の 7 日目です。誰も書かないので、代わりに書きます。

問題

double d = 3 / 2;

変数 d の値は次のどちらですか?

(1) 1.5

(2) 1.0

解答

(2) 1.0

解説

もし、結果として 1.5 を期待しているのなら、以下のように書き換える必要があります。1.0 を期待しているのであれば書き換える必要はありません。

double d = (double) 3 / (double) 2;

もしくは、浮動小数点リテラルを用いた以下の書き換えでも構いません。

double d = 3.0 / 2.0

この書き換えが持つ意味を、きちんと理解できていますか?

まず、問題の式文において、変数 d の値がなぜ 1.0 になるのか? int 型同士の除算だからそうなるという解答では 50 点以下です。なぜかというと、演算子に注意が集中して、式というものに対して無頓着になっているから。

この問題の本質は、除算という二項演算式と代入演算式の評価順序の違いにあります。より具体的には、除算という二項演算式は代入演算式よりも先に評価されるということです。実際に問題の式がどのように評価されているかというと、

  1. 式文 d = 3 / 2; を代入演算式として評価する。
  2. 上記の代入演算式の中に除算式 (二項演算式) が含まれるため、そちらを優先して評価する。
  3. 除算式 3 / 2 を評価する。除数・被除数とも int 型のため、評価後の式の値は 1 となる。
  4. 代入演算式 d = 3 / 2 は上記の除算式の評価により d = 1 となる。
  5. 代入演算式 d = 1 において、左辺: double 型、右辺: int 型のため、右辺を double 型に拡大変換する。
  6. 代入演算式 d = 1.0D を評価する。

なお、Java における式の評価順は、概ね以下のように定められています。

  1. 一次式 (リテラル、変数、括弧、インスタンス生成式、フィールド・アクセス式、メソッド呼び出し式など)
  2. 後置式 (後置インクリメント・デクリメント式)
  3. 単項演算式 (前置インクリメント・デクリメント式、符号、キャストなど)
  4. 二項演算式 (四則演算、シフト演算、比較演算など) ...この中でさらに順序がある
  5. 三項演算式 (条件演算)
  6. ラムダ式
  7. 代入演算式

さらに、式文として単独で評価可能な式は、以下に限られます。

  • 代入演算式
  • インクリメント・デクリメント
  • インスタンス生成式
  • メソッド呼び出し式

ここで話を戻して、結果として 1.5 を得るにはどうすれば良いのかを考えます。左辺のデータ型 double に適合するよう右辺の各値を動的に double 型へと変換してくれれば良いのですが、Java はそのような構文解釈をしません (そのような構文解釈をするプログラミング言語へ逃げるというのもひとつの解決策ではあります)。従って、Java のルールの中で解決する必要があります。

最終的に得たい値は、代入演算式の左辺である double 型ですが、被除数 3 と除数 2 はともに int 型です。さらに代入演算式よりも除算式が先に評価されます。ここから、除算よりもさらに前の段階で被除数 3 と除数 2 を double 型に変換できれば良いことになります。方法としては 2 通りあります。

  • 被除数 3 と除数 2 を double 型にキャストする (キャスト = 単項演算式)
  • 浮動小数点リテラルを用いて、被除数 3.0D、除数 2.0D とする (リテラル = 一次式)

どちらも二項演算式よりも優先される単項演算式や一次式を用いるため、二項演算式である除算式の評価前に被除数と除数を double 型に変換することができます。

Puzzle : if-then-else statement and block

この記事は Java Puzzlers Advent Calendar 2016 の 2 日目です。昨日分は @orekyuu 氏の「Equals Method Overloading」です。

問題

ある人が、ある意図を持って以下のようなプログラムを書きました。

import java.time.*;

public class IfThenElseSample {
  public static void main(String... args) {
    if (Year.of(2015).isLeap())
      if (Year.of(2016).isLeap())
        System.out.println("2016 is a leap year");
    else
      System.out.println("2015 is not a leap year");
  }
}

このプログラムは標準出力に何を表示するでしょうか?

(1) 2016 is a leap year

(2) 2015 is not a leap year

(3) 何も出力されない

解答

(3) 何も表示されない

どこがいけないのか?

このプログラムの作者は、最初の if-else 文が「偽」となり "2015 is not a leap year" が表示されることを期待していたのでしょう。インデントからはそのような作者の意思が伝わってきます。しかし、Java のソースコードはフリーフォーマットのため、インデントも改行も構文解釈に影響しません。このプログラムのインデントをより構文解釈に近づけたら、例えば、次のようになります。

import java.time.*;

public class IfThenElseSample {
  public static void main(String... args) {
    if (Year.of(2015).isLeap())
      if (Year.of(2016).isLeap())
        System.out.println("2016 is a leap year");
      else
        System.out.println("2015 is not a leap year");
  }
}

これなら、最初の if 文が「偽」であれば何も実行されず、従って標準出力には何も表示されないことが、プログラムの作者にもお分かり頂けるのではないでしょうか。

JLS §14.9 より、if 文 (if-then および if-then-else) は以下のように定義されます。

IfThenStatement:
if ( Expression ) Statement

IfThenElseStatement:
if ( Expression ) StatementNoShortIf else Statement

IfThenElseStatementNoShortIf:
if ( Expression ) StatementNoShortIf else StatementNoShortIf

ただし、Statement および StatementNoShortIf は (JLS §14.5 より) 以下となります。

Statement:
StatementWithoutTrailingSubstatement
LabeledStatement
IfThenStatement
IfThenElseStatement
WhileStatement
ForStatement

StatementNoShortIf:
StatementWithoutTrailingSubstatement
LabeledStatementNoShortIf
IfThenElseStatementNoShortIf
WhileStatementNoShortIf
ForStatementNoShortIf

StatementWithoutTrailingSubstatement:
Block
EmptyStatement
ExpressionStatement
AssertStatement
SwitchStatement
DoStatement
BreakStatement
ContinueStatement
ReturnStatement
SynchronizedStatement
ThrowStatement
TryStatement

ここから、if 文 (if-then) の then 部分は Statement であるため if 文 (if-then) と if-else 文 (if-then-else) のいずれをネストさせても良いが、if-else 文 (if-then-else) の then 部分は StatementNoShortIf であるため if-else 文 (if-then-else) のみネストさせられることが明らかとなります。これが意味するところは、「ネストした if 文 (if-then) / if-else 文 (if-then-else) においては、else は直近の if と対応付けられる」ということです。

さらに if-else 文 (if-then-else) の then 部分は StatementNoShortIf に限定されるため、then 部分に if 文 (if-then) をネストさせることはできず、ずっと if-else 文 (if-then-else) をネストさせる形になります。つまり、一度でも else を付けたならばネストの末代まで else を付けなければなりません。IfThenElseStatementNoShortIf の then 部分だけでなく、LabeledStatementNoShortIfWhileStatementNoShortIfForStatementNoShortIf についても、StatementNoShortIf しかネストさせられないことが下記の定義より明らかであるためです (StatementNoShortIf には if 文 (if-then) は含まれません)。

ラベル付き文 (JLS §14.7 より)

LabeledStatement:
Identifier : Statement

LabeledStatementNoShortIf:
Identifier : StatementNoShortIf

while 文 (JLS §14.12 より)

WhileStatement:
while ( Expression ) Statement

WhileStatementNoShortIf:
while ( Expression ) StatementNoShortIf

for 文 (JLS §14.14 より)

ForStatement:
BasicForStatement
EnhancedForStatement

ForStatementNoShortIf:
BasicForStatementNoShortIf
EnhancedForStatementNoShortIf

基本 for 文 (JLS §14.14.1 より)

BasicForStatement:
for ( [ForInit] ; [Expression] ; [ForUpdate] ) Statement

BasicForStatementNoShortIf:
for ( [ForInit] ; [Expression] ; [ForUpdate] ) StatementNoShortIf

拡張 for 文 (JLS §14.14.2 より)

EnhancedForStatement:
for ( {VariableModifier} UnannType VariableDeclaratorId : Expression ) Statement

EnhancedForStatementNoShortIf:
for ( {VariableModifier} UnannType VariableDeclaratorId : Expression ) StatementNoShortIf

なお、else 部分については StatementStatementNoShortIf のどちらでも構いません。

どうすれば良いのか?

プログラムの作者の意図が、最初の if-else 文で「偽」の場合に "2015 is not a leap year" を出力するものだったと仮定するならば (おそらくそのつもりだったのでしょうが)、最初の if 文の then 部分をブロックにします。

import java.time.*;

public class IfThenElseSample {
  public static void main(String... args) {
    if (Year.of(2015).isLeap()) {
      if (Year.of(2016).isLeap())
        System.out.println("2016 is a leap year");
    } else
      System.out.println("2015 is not a leap year");
  }
}

より望ましい方法は、すべての then および else をブロックにしてしまうことです。

import java.time.*;

public class IfThenElseSample {
  public static void main(String... args) {
    if (Year.of(2015).isLeap()) {
      if (Year.of(2016).isLeap()) {
        System.out.println("2016 is a leap year");
      }
    } else {
      System.out.println("2015 is not a leap year");
    }
  }
}

はじめからこのようにしておけば、よかったのです。多少インデントがずれていようが、then と else の処理内容は明白ですから。

教訓

ソースコードのインデントや改行を信用してはいけない。Java はインデントや改行を単なる空白文字として構文解釈をする。

明日は、再び @orekyuu 氏の出番です。