Java – if then else

if-then文

基本形

if-then文は以下のようにifキーワード、(boolean式)、1つの文が並べられた文で、boolean式がtrueの場合に文が実行される。

if (boolean式) 文;

キーワードから見るとif文となるが、Oracleの仕様書を尊重してif-then文としておく。

改行した場合

実行文の前で改行しても同じように実行される。

エラープローン

if-then文で改行して複数の文を書いた場合、条件式直後の1文しか条件の判定結果に含まれない。以下の例ではfalseで1つ目の分は無視されるが、その直後の文に制御が移って実行される(インデントは効果がない)。

当初簡易化のために改行形で書いていて、後から条件の対象となる文をそのまま付け加えた時、意図したロジックと異なる動作をしてしまう可能性がある。

一般形

上のようなトラブルを回避するため、一般にはif命令の後の文を{}で囲んでブロックとする。

if (...) { ..... }

if-then-else文

基本形

if-then-else文は以下のようにifキーワード、(boolean式)、文1、elseキーワード、文2が並べられた文で、boolean式がtrueの場合に文1が、falseの場合には文2が実行される。

if (boolean式) 文1 else 文2;

改行した場合

文1の前、elseの前、文2の前で改行しても、文2の終わりまでが1つのif-then-else文として扱われる。

エラー

以下のコードはコンパイルエラーになる。

これは、以下の理由による

  • if-then文に属するのは"1st line"の文だけ
  • その後、if-then文の外側として"2nd line"の文が実行される
  • これに続くelseには、それが属するべきif文がなく、if-else文ではないのにelse単独で存在
  • よって構文エラー

一般形

上記のようなエラーを避け、またif文であったようなトラブルを避けるためにも、if-else文においても{}によるブロック表記が一般的。

if (...) { ..... } else { ..... }

else if

Javaの仕様上、Pythonのelif、Rubyのelsif、PHPのelseifに相当するキーワードはない。

これらと同等のロジックを組むために一般的には”else if“という表現がされているが、これを適切に扱うにはブロックとして扱うべきである。これについて”Java仕様における”Java – if文をブロック化すべき理由とelse if“に示した。

 

Java – if文をブロック化すべき理由とelse if

概要

  • Javaにはif-then構文と、if-then-else構文の2つがある
  • それぞれのtrue/falseに応じた実行文は、1つの文やブロックなどでなければならない
  • else ifという構文はなく、else句の実行文にif-thenやif-then-else文を連ねたもの
  • if-then文やif-then-elseには原則として{}ブロックを使うべき

Java仕様におけるif/if-else/else ifの扱い

Oracleの仕様書では、if文の種類としてif-then文とif-then-else文が定義されている。しかし、”else if”という定義はされていない。

if-then文

if-then文の書式は以下のように定義されている。

IfThenStatement: if (Expression) Statement

Oracleの仕様によれば、Statementとして書けるのは以下の6つとされている。

  • StatementWithoutTrailingSubstatement
  • LabeledStatement
  • IfThenStatement
  • IfThenElseStatement
  • WhileStatement
  • ForStatement

いずれも単一の文であり、複数の文を書くことはできない。

if-then-else文

一方、if-then-else文は以下のように定義されている。

IfThenElseStatement: if (Expression) StatementNoShortIf else Statement

そしてStatementNoShortIfとして書けるのは次のいずれかとされている。

  • StatementWithoutTrailingSubstatement
  • LabeledStatementNoShortIf
  • IfThenElseStatementNoShortIf
  • WhileStatementNoShortIf
  • ForStatementNoShortIf

こちらも複数の文を書くことはできない。

ブロックは書ける

上記のように、if-then文やif-then-else文には、独立した複数の文を書くことはできない。

ただし双方で記述可能なIfThenStatementを見ると、Blockが記述可能となっている。

  • Block
  • EmptyStatement
  • ExpressionStatement
  • ・・・・・

したがって{}ブロックを使えば、他言語のif…else…endif構文のように複数の文を並べることができる(逆にそうしなければ書けない)。

else if

ブロックで書く

ところで、Javaの仕様ではelseifに相当するキーワードも構文も定義されていない。ただし、else句の後にif-then文やif-then-else文を書いて、同様のロジックとすることはできる。ただし、このときに原則として{}ブロックで書くべきである。

ブロック化すべき理由

先の仕様で、if-then文のStatementにはifTthenStatementifThenElseStatementがかけるが、if-then-else文のif-then句ではifThenElseStatementが書けないことになっている。これが何を意味するか試してみよう。

まずif-then文の実行文としてifThenStatementあるいはifThenElseStatementを連ねた場合。想定通り真偽条件に応じた結果が出力されている。

次にif-then-elseの第1ステートメントにifThenElseStatementを書いた場合。この場合も想定通りの結果になる。

ここで、仕様で想定されていない構成にしてみる。if-then-else文の第1ステートメントには、ifThenElseStatementが想定されているが、ifThenStatementは想定されていない。これを無視してifThenStatementを書いたのが以下。

これは インデントで表現すると以下のようなロジックを想定している。ところがその想定と異なる動作になってしまう。

このことはOracleの仕様書14.5. Statementsに書かれていて、”else句は(そのelseより前の)最も内側のifに属する”こととされている。つまり上の文を解釈通りのインデントにすると以下のようになる。

もしこの文を当初のロジックにしたければ、以下のようにブロック化しなければならない。

これで想定通り何も表示されず、想定ロジック・インデント通りの動作となる。

このようなelse句の解釈や、if-then文のStatementを追加する場合を考慮すると、たとえ1行の文でもブロック化すべきということになる。

余談

StatementNoShortIfとしてforやwhileの単文も書くことができる。

しかしながら、こういう表現も思わぬトラブル防止のためにブロック内に書いておいた方が安全だろう。

 

 

Java – キーボード入力

InputStream

標準入力(System.in)を引数に渡して入力ストリームを生成する。

実行するとプロンプト(>)が表示されて入力待ちになり、文字列を入力してEnterを押すとその内容がオウム返しに表示される。

Scanner

java.util.Scannerの引数に標準入力(System.in)を渡してインスタンスを生成し、next()などのメソッドでキーボードからの入力を得る。

next()

以下のコードは、文字列”end”が入力されるまで、入力された文字列をオウム返しに表示し続ける。

実行例。

nextInt()

実行例。

全角数字も扱える。

3桁ごとのカンマ区切りは通る。

仕様ではInteger正規表現にマッチし、かつ範囲内である必要がある。数値と認識されない文字列に対しては、例外が投げられる。

以下は数値として扱えない。

  • 3桁区切りでないカンマ区切り
  • 全角の漢数字

closeするとSystem.inが使えなくなる

先のコードで、new Scanner(System.in)でインスタンスを生成している行に警告が出る(リソース・リーク: 'scanner' が閉じられることはありません)。

そこで、Scannerインスタンス使用後にいったんclose()してみると、再度インスタンスを生成しようとしたときに例外が投げられる。scannerをクローズしたときに標準入力System.inが閉じられてしまうため。

実行結果。

 

Java – ブロックスコープ

ブロック内でのみ有効

ブロック内で宣言された変数はそのブロック内でのみ有効で、ブロックを出るときに破棄される。

ブロック内で宣言された変数は、ブロック外では存在していない。

ブロック外で宣言可能

ブロック内スコープの変数はブロック外では存在しないので、ブロック外で同じ名前で宣言できる。

ブロック外宣言はブロック内で有効

ブロックの前で宣言された変数は、その後のブロック内で使用可能。以下の例では、ブロックの前で宣言された変数sがその後のブロックの中で書き換えられている。

したがって、ブロックの前で宣言された変数をブロック内ローカル変数として定義することはできない。定義しようとすると重複定義エラーになる。

 

Java – 初期化ブロック

staticブロックはクラスの読み込み時に実行されるが、初期化ブロックはクラスのインスタンス生成時に実行される。

上記のコードの実行結果は以下のとおり。

実行時の流れは以下のとおり。

  1. MainClassが読み込まれる
  2. MainClassmain()メソッド(static)実行
  3. main()メソッド内で
    1. MainClassと同じソース内のClass1のインスタンス生成
      Class1のインスタンスの初期化ブロック実行
    2. 別のソースのOuterClassのインスタンス生成
      OuterClassのインスタンスの初期化ブロック実行
    3. MainClassのインスタンス生成
      MainClassのインスタンスの初期化ブロック実行
    4. MainClassのインスタンスのmethod()メソッド実行
    5. method()メソッド内で
      1. MainClassのインナークラスInnerClassのインスタンス生成
        InnerClassのインスタンスの初期化ブロック実行
      2. MainClassと同じソース内のClass2のインスタンス生成
        Class2のインスタンスの初期化ブロック実行

 

Java – staticブロック

staticブロックは、クラスが呼ばれるタイミング、コードが実行される前に実行される。インスタンス生成時に実行される初期化ブロックよりも早いタイミングで、クラススコープの変数の初期化などに用いることができる。

上記のコードの実行結果は以下のとおり。

実行時の流れは以下のとおり。

  1. MainClassが読み込まれる
    MainClassstaticブロック実行
  2. MainClassmain()メソッド(static)実行
  3. main()メソッド内で
    1. MainClassと同じソース内のClass1のインスタンス生成
      Class1staticブロック実行
    2. 別のソースのOuterClassのインスタンス生成
      OuterClassstaticブロック実行
    3. MainClassのインスタンス生成
    4. MainClassのインスタンスのmethod()メソッド(public)実行
    5. method()メソッド内で
      1. MainClassと同じソース内のClass2のインスタンス生成
        Class2staticブロック実行

MainClassの動作を見ると、staticブロックが実行されるのはインスタンス生成時ではなく、クラスが読み込まれるタイミングとなっている。

その他のクラスはインスタンス生成時にクラスが呼ばれ、staticブロックが実行されている(宣言のみではstaticブロックは実行されない)。

インナークラスではstaticブロックは書けない(コンパイルエラー)。

 

Java – assert

概要

  • assert文にはコードを書く際に想定している前提条件を指定する
  • コード実行時、VMの引数に-enableassertionsを指定する(指定しないと無視される)
  • 前提条件に合わない場合、AssertionErrorが発生する
  • エラー時のメッセージを追加することができる
  • privateメソッドで想定している引数チェックなど、開発時のデバッグに用いる
  • publicメソッドの引数チェックのような、運用時の動作保証に対しては用いるべきではない

基本形

assertの後に想定条件を書く。実行時引数に-enableassertionsが設定されていて想定条件がfalseの場合はAsertionErrorが発生する。

assert 想定条件;

このコードのquotient()メソッドは引数dividendがゼロでないことを想定しているが、これを呼び出すメソッド側でゼロを指定している。

まずこのコードを実行時引数になにも指定しないで通常通り実行すると、以下の様に表示される。

次にこのコードの実行時引数に-enableassertionを指定して実行すると、以下のAssertionErrorが投げられる。

メッセージ指定形式

以下の形式で、assertが実行されたときのAssertionErrorにメッセージが付加される。

assert 想定条件 : メッセージ;

たとえば先のコードのassert文を以下の様に変更する。

そして実行時引数に-enableassertionsを指定して実行すると、エラー表示が以下の様になる。

実行時引数の指定

Eclipseで実行する場合は、実行→実行構成のダイアログ→引数タブ→VM引数に-enableassertionsを指定する。

コマンドラインから実行する場合は、コンパイル後のクラスファイルを実行する際に-enableassertionsを指定する。

活用シーン

assertは分散開発時に担当している処理の前提条件を明確化(通常はコメントにより明示するなど)。

デバッグ時に-enableassertionsを指定してエラー補足・対応、運用時にはオプションを外して実行。

publicメソッドの引数チェックなど運用時の仕様に関する条件は、assertではなく条件分岐や例外処理を用いるべき。

 

Java – 文字列リテラル

基本形

文字列を扱うStringjava.lang.Objectを継承したクラスだが、リテラルの書き方が用意されている。

文字列リテラルは文字列をダブルクォート(")で囲む。

文字列は1文字でもよく、1文字も含まない空文字列(長さゼロで内容がない文字列)でもよい。

エスケープ

文字列リテラル中でバックスラッシュ(\、¥記号)は特別な意味を持ち、その直後の文字でエスケープ文字の種類があらわされる。

たとえば\tはタブ、\nは改行。

\uhhhhはユニコードエスケープ。

シングルクォート(')は文字列リテラル中でそのまま使えるが、ダブルクォート(")はエスケープする必要がある。

エスケープ記号自体を使いたいときは\でエスケープする(2つ並べる)。

変数展開・値の埋め込み

Javaには多言語のように文字列中で変数展開する機能は備えられていない。

変数の値を文字列に入れるには、文字列結合の演算子(+)を使うか、String.format()メソッドを使う。

 

Java – ミュータブル/イミュータブル

概要

“immutable”の説明として、「オブジェクト生成後にその内容を変えることができないオブジェクトを”イミュータブルである”」というらしい。いま一つわかり辛かったので、以下の様に考えてみた。

オブジェクトへの参照値が変わらないのにその内容が変わり得るのがミュータブル、オブジェクトの参照値が変わらなければその内容が変わり得ないのがイミュータブル、というのはどうだろうかと考えた。

これをJavaのint型、配列、String型で確認してみる。

プリミティブ型はイミュータブル

以下のコードで考える。

  • i2i1をそのまま代入しているだけなので、==比較はtrue
    • そのi2インクリメントしてもi1は影響を受けず、i1i2==比較はfalseになる
  • i3i1と同じ値をリテラルで設定していて、i1との==比較はtrue
  • i4i1と異なる値を代入していて、i1との==比較はfalse
  • i5i4と同じ値を計算した上で代入しており、i4 == i5

int型などのプリミティブ型は、変数ごとにスタック上に内容が保存されるため、オブジェクトのような参照をしない。その挙動から「プリミティブ型はイミュータブルである」ともいえる。

これをラッパークラスのIntegerで試しても同じ結果になる。

配列はミュータブル

配列はミュータブルなオブジェクトで、参照している内容を直接変更することができる。

以下の様に、2つの変数が同じインスタンスを参照している場合、一方でその内容を変更すると他方でもそれが反映される。

ただし同じ内容でもリテラルで初期化した場合は別のインスタンスとして保持され、一方への変更が他方へは影響しない。

Stringはイミュータブル

String型はイミュータブルなオブジェクトで、詳細はこちらにまとめた

String型の文字列インスタンスはヒープ上に保存され参照される。リテラルの内容が同じ場合は1つのインスタンスを共有してこれを参照する。

参照されている実体の内容が直接操作され変更されることはなく、変数同士で参照値を代入した場合に一方への変更が他方に影響することはない。

 

 

Java – String – イミュータブル

概要

JavaのStringの挙動について整理してみた。

同一性と同値性

String型のオブジェクトの同一性と同値性(等価性)について、以下のコードで確認した。

  • s2s1の参照値を代入しているので、s1s2は同一であり同値
  • その後にs2の内容を変更すると、s1とは内容も参照値も異なるようになる
  • s3には別のリテラルでs1と同じ内容を代入しているが、s1s3は同一であり同値

すなわち同じ内容のStringリテラルを用いると、それらリテラルは1つのインスタンスとして共有される。

同値なオブジェクトと同一性

以下のコードで、同値なオブジェクトが常に同一になるか(共有されるか)確認した。

  • s5は2つのStringリテラルからs4と同じ値のリテラルを作り出していて、s4s5は同一であり同値
  • s6は内容が"ABC"の変数s1Stringリテラルからs4と同じ値のリテラルを作り出しているが、s4s6は同値ではあるが同一ではない

すなわち以下の様に整理される。

  • リテラルから生成された内容が同じ場合は1つのインスタンスとして共有される
  • 変数を含めた操作で生成されたインスタンスは、同じ内容のものがあっても別のインスタンスとして扱われる

Stringのイミュータビリティー

以下の様にStringはイミュータブルと言える。

  • 異なる内容のStringリテラルは異なるインスタンスとして生成され、参照される
  • Stringクラスで変更を伴うメソッドはStringを戻り値としており、インスタンスの内容を直接は変更できない
  • Stringクラスはfinal宣言されており継承はできない(クラスを継承して自身を変更することはない)

なおStringに関しては、同値であっても同一でない(インスタンスが異なる)場合がある。

  • リテラル同士の結合結果が他のリテラルに等しい場合、その内容は1つのインスタンスとして共有される(同一であり同値)
  • String変数を含む結合結果が他のリテラルに等しい場合でも、それらは別のインスタンスとして扱われる(同値だが同一ではない)

リテラルと変数での挙動の違いは、コンパイルレベルで値を同じかどうかを判断しているためか。

これらの場合があるとしても、一度設定されたオブジェクトの内容を変更することはできないため、String型はイミュータブルであると言える。