[雑談] 試験の役に立たないJava講座 (5)

ちょっとした間違いで5回目を消してしまったので、再度書き直します。

ということで引き続きインプレス社の「徹底攻略Java SE 11 Silver問題集」の章立てに沿って、少し雑談していきたいと思います。

以前よりお話ししているとおり問題集の内容に沿ったものではなく抜けているところも多々ありますので、認定資格を取得しようというのであれば問題集を購入して、そちらをしっかり勉強してください。

こちらは章立てに沿って適当に書き散らかしているものですので、試験の役には立ちません。

ということで今回は第3章、演算子を扱います。

式と文

まず、式と文について確認していきたいと思います。

以下のプログラム例を見てください。

x = 3

上の例のプログラム片はプログラム言語によって式とみなされるものと、文とみなされるものがあります。

どういうことかというと、上のx = 3xという変数に3という値を代入するという「操作」を指示しています。

プログラム言語によっては「操作」を扱う記述は文であり、式として扱わないという考え方を持っています。

JavaはC言語などと同様にx = 3を代入式という式の一つとして扱い、代入式は代入された値を式の値として持つ仕様となっています。

ここではあまり関係のない細かいことかもしれませんが、後々式と文の違いが必要になってくる事例が出てきます。

代入式

=」は左辺の変数 (フィールドとも呼ばれるクラス変数、インスタンス変数もここでは一括して変数と呼ぶことにします) に右辺の値を設定する代入演算子で、式の値は変数に代入された値となります。

代入が式となっている利点は、代入した結果をそのまま比較できるので、記述量が減る点にあります。

例えば、何かのメソッドから得た値が0以上の場合だけ繰り返したい場合、代入文が式の値を持たない場合は以下のように書かなければなりません。

while (true) {
  x = foo();
  if (x < 0)

    break;

  // 何らかの処理

}

しかし、代入文が式であれば値を持つので以下のように書けます。

while ((x = foo()) >= 0) {
  // 何らかの処理
}

行数が減るだけでなく、ループの脱出条件がわかりやすくなるという利点もあります。

本当は変数の初期化も式だとありがたいのですが、それは残念ながらできません。

// このプログラム例はエラーになります。
while ((int x = foo()) >= 0) {
  // 何らかの処理

}

これができるとforループのように一時変数の通用範囲を制限できるのでいいと思うのですが、できないのでとても残念に思います。

代入演算子のバリエーション

プログラムを書いていると「更新」という操作が割と多く発生します。

例えば「x = x + 2」のように「x」に「2」を足した値を「x」に再設定するといった操作です。

こういった場合には演算子と「=」を組み合わせた代入演算子を使用して、「x += 2」と書くことができます。

残念ながら「x = x * 2 + 3」のような式は一つにまとめられません (というか、どう書けばいいかも私には思いつかないので、合理的な書き方が思いついたらぜひ仕様提案してください)。

インクリメント演算子とデクリメント演算子

さらによく使われるものということで、変数を1増やす・減らすという操作の専用演算子が用意されています。

変数の値を1増やしたい場合は「++x」のように変数名の前に「++」を書き、1減らしたい場合は「--x」のように変数名の前に「--」を書きます。

さらにインクリメント演算子、デクリメント演算子にはバリエーションとして、「++」、「--」を変数の後に置く書き方、「a++」や「a--」があります。

このように書いた場合、式の値は変数値に1を加える、あるいは1を引く前の値となります。

例えば「a」に「3」が設定されている場合、「a++」実行すると「a」の値は「4」となりますが、「a++」という式の値は「3」になるということです。

この2種類、「a++」と「++a」の使われ方について少し考えてみたいと思います。

プログラミングで広く使われるデータ構造の一つにスタックがあります。

スタックは整形ポテトチップの筒のように容器に入れた順序とは逆順にしか取り出せない、最初に入れたものは最後まで取り出せないFirst In Last Outのデータ構造で、例えばアプリケーションのアンドゥー機能などいろいろなところで使われています。

スタックは一般的なデータ構造なのでJavaの標準ライブラリにも用意されていますが、とりあえず簡単なものを作ってみたいと思います。

まずデータを保存するための領域を用意します。保存するデータ型は簡単のためにint型として、データを保存する領域には配列を用意しておきます。

public class Stack {
  private int[] buffer = new int[10];
  private int index = 0;

}

そして、データを保存する位置を記憶しておくためにindexという変数も用意し、最初にデータを記録する位置として0で初期化しておきます。

それではデータを記録するpushメソッドを書いてみましょう。

public class Stack {
  private int[] buffer = new int[10];
  private int index = 0;

  public void push(int value) {

    buffer[index] = value;

    index = index + 1;

  }

}

データを記録するための配列bufferindexで指定された位置に値を書き込み、次回のためにindexを1増やしておきます。

このようにすることでpushで与えられたデータはbuffer配列に順に記録されます。

次にデータを取り出すpopメソッドを見てみましょう。

public class Stack {
  private int[] buffer = new int[10];
  private int index = 0;

  public void push(int value) {

    buffer[index] = value;

    index = index + 1;

  }

  public int pop() {

    index = index - 1;

    return buffer[index];

  }

}

メソッドpopが呼ばれるとき、indexは最後にデータを記録した配列要素の次の要素を指しています。このため、popではindexから1を減らして、最後にデータを記録した要素位置に移動してからデータを読み取っています。

さて、これをインクリメント演算子とデクリメント演算子を使って書き直してみましょう。

public class Stack {
  private int[] buffer = new int[10];
  private int index = 0;

  public void push(int value) {

    buffer[index++] = value;

  }

  public int pop() {

    return buffer[--index];

  }

}

pushメソッドではデータ配列のアクセスで「index1を加える前の値」が必要になりますのでbuffer[index++]というように後置のインクリメント演算子を使用し、popメソッドではデータ配列のアクセスで「indexから1を引いた後の値」が必要となるのでbuffer[--index]を使用しています。

ここからは余談ですが、for文の書き方について。

for文を書く場合、for (int i = 0; i < 10; i++) { ... }のように書くことが多いと思います。

私が昔々言われたのはfor (int i = 0; i < 10; ++i) { ... }のように前置演算子を使うべきということでした。

上でも説明したように「i++」は「i」の値を1増やした後、増やす前の値を式の値にするものですから、前に書いたpushメソッドの例で言えば、以下のように処理がされるということになります。

public void push(int value) {
  // buffer[index++] = value;
  // 上の式はコンパイラで以下のように展開されます。

  // 式の計算順序としてindex++が最初に実行されなければいけません。

  // indexに1を足す前にindexの値を保存しておきます。

  int __index = index;

  // 次にindexに1を足します。

  index = index + 1;

  // 最後に保存しておいた値を使って配列にアクセスします

  buffer[__index] = value;

}

ところで、for文で使用される場合のように式の結果を使用しない場合、値を保存しておくという操作は無駄なので、「i++」は使ってはいけないという趣旨でした。

実際にはコンパイラが無駄な操作を省いてくれるので、for文で「i++」を使っても「++i」を使っても結果は変わらないのですが、「for文では++iを使う」と意識すると「i++」と「++i」の違いを気遣うようになるので有用なのではないかと思っています。

式の評価順序

まず、x = x + 3という式を考えます。

これまで説明抜きにx = x + 3は「x3を加えたものをxに代入する」として話を進めてきました。

実際にはx = x + 3が「x3を加えたものをxに代入する」となる理由があり、それが式の評価順序と演算子の優先順位です。

式の仕様はJava言語仕様の第15章にあります。

式の仕様について説明していくと長くなるので、そのうち稿を改めて話をしたいと思っていますが、簡単に言うとそれぞれの演算子には優先順位と評価順序があって、それらに従って式が評価されるということになります。

代入演算子「=」は右辺を評価した結果を左辺の変数などに代入すると決められています。そして加算演算子「+」は左辺を評価し、続いて右辺を評価して、それぞれの値を足し合わせた値を式の値とします。

その結果として右辺のx + 3が先に評価されその結果が左辺の「x」に代入されるという仕組みになっています。

さて、それでは以下のような式を考えてみましょう。

int x = 2;
x = x + (x += 3) + x;

こんな式は絶対に書いてはいけないのですが、頭の体操と思ってやってみてください。

まず、最初に代入式から評価していきます。代入演算子の左辺は変数で特に評価する必要はないので、右辺にあるx + (x += 3) + xが評価されます。

次に加算演算子「+」を評価していくのですが、加算演算子「+」と減算演算子「-」が同じレベル (かっこでくくられていないか、同じかっこのペアでくくられている範囲) に複数ある場合、最も右端の演算子で左辺と右辺に分け、左辺の式から評価していきます。

今回の例で言えば、まず右端の「+」でx + (x += 3)xに分け、左辺のx + (x += 3)を評価します。

この式にも「+」があるので、左辺のxと右辺の(x += 3)に分けて左辺から評価します。

その結果、この式はx + (x += 3)を先に、[x + (x += 3)の結果] + xを次に評価するという順序になるので、左から順番に加算を行っている形となります。

このように左から順に実行されるものを「左結合性」を持つと言い、代入演算子のように右から順に実行されるものを「右結合性」を持つと言います。

さて、x + (x += 3)の式の評価ですが、左辺xが評価されて変数xの値「2」が左辺の値となります。

右辺はカッコ内のx += 3が評価され、xの値「2」に3を加えた「5」が右辺の値となるとともに変数xにも設定されます。

そして式x + (x += 3)の値は左辺値と右辺値を加えた「7」となります。

元に戻って右端の「+」を実行しますが、左辺値は先ほど見た通り「7」となり、右辺値は変数xの値、これは先に更新されたので「5」となりますので、式の結果はこれらを足し合わせた「12」となります。

非常に面倒ですが、基本的には算術式と同じように考えればいいので、演算子の優先順位と結合性、そして式の評価は原則として左辺から行われるということだけ気にしていればいいかと思います。

論理演算子

Javaでは算術演算子のほかに整数値をビット列としてあつかう論理演算子が用意されています。

論理演算子には論理和「|」、論理積「&」、排他的論理和「^」、論理否定「~」があります。

また、上記の論理演算子はboolean型にも適用できますが、boolean型での論理演算子の利用は条件演算子のところであわせて考えたいと思います。

Javaには整数値をビット列として扱う演算子としてシフト演算子も用意されています。

シフト演算子は整数値をビット列とみなして左 (上位桁方向) もしくは右 (下位桁方向) にビットを移動する操作です。

左シフト「<<」は左辺の整数値をビット列とみなしたときの各ビットを右辺で指定された数だけ上位に移動する操作になり、移動操作で空きになる下位ビットは0で埋められます。

右シフトには2種類あり、「>>」は左辺の整数値をビット列とみなしたときの各ビットを右辺で指定された数だけ下位方向に移動する操作ですが、移動操作で秋になる上位ビットにはもともとの最上位ビットの値がコピーされます。

例えばbyte値「11001011」を2ビット右シフトする(byte)0b11001011 >> 2は上位ビットが1で埋められるので「11110010」となります。

Javaでは正の値の最上位ビットは0、負の値の最上位ビットは1と決められていて、かつ右シフトは2で割る演算と同等ですので、右シフトで割り算を行った際に正負の符号が保存されるようこのような取り決めになっています。

右シフト演算子「>>>」を使用した場合には上位ビットは0で埋められるので、正負の符号に関係ない処理ではこちらも使えます。

条件演算子、「&&」と「||」

論理演算子に似た機能を持つ演算子として条件演算子があります。

条件演算子は両辺にboolean値をとり、結果が確定したところで評価を中断します。

条件積演算子「&&」は左辺値を評価した結果がfalseの場合、右辺は評価せずにfalseを値とし、条件和演算子「||」は左辺値を評価した結果がtrueの場合、右辺を評価せずにtrueを値とします。

これに対して論理積演算子「&」と論理和演算子「|」は左辺の評価結果にかかわらず右辺も評価します。

条件演算子が有用な場合として、以下のような例が挙げられます。

int[] array = foo();
if (array != null && array.length >= 5) {
  // fooメソッドから取得した配列の大きさが

  // 5以上の場合の処理

}

上の例でfooメソッドがnullを返した場合、array.lengthを実行するとエラーとなってしまいますが、array != null && array.length >= 5の条件式はarraynullだと「&&」の右辺は評価されないのでエラーにならずに処理を続けることができます。

同値演算子「==」と「!=」

2個の値が同じかどうかを判定する演算子として同値演算子「==」が用意されています。

文字列型のところでも触れましたが、「==」と「!=」はプリミティブ型に対しては思った通りの結果を返しますが、参照型 (オブジェクト型) は同じオブジェクトを参照しているかどうかで判定されるため、同じ値を持ったオブジェクトでも同一とは判定されません。

このためオブジェクトは値として同一であるかを判定するためのメソッドequalsを実装することが必須となっています。

もちろんequalsを実装する必要がないオブジェクトもありうるので、その場合はObject型で定義されているequalsメソッドを継承するという形で実装しないという選択肢もあります。

ただ、本当にequalsメソッドを使いたくないという場合には無条件に非チェック例外を発生するequalsメソッドを定義しておくというのもありかと思います。

ただし、テストをきちんと行わないと運用に入ってから例外が発生してびっくりするということがありますので、気を付けてください。

三項演算子、"?:"

演算子の最後として三項演算子を取り上げます。

三項演算子は[条件式]? [条件が成り立つ場合の式]: [条件不成立の場合の式]という形の式を作る演算子で、if文を簡略化したようなものです。

例えば配列arraynullの場合は0null以外の場合は配列の長さを変数lengthに代入するといった場合、if文を使用すると以下のような書き方になります。

int length;
if (array != null) {
  length = array.length;

} else {

  length = 0;

}

これを三項演算子を使用して書くと以下のようになります。

int length = array != null? array.length: 0;

このように一気に簡単になり、コードとしてもわかりやすくなると思っています。

以上、演算子について特徴的なところをだらだらと書いてみました。

演算子の優先順位と結合性についてはいろいろなところで触れられていますので、ぜひ確認しておいてください。

0コメント

  • 1000 / 1000