java8:関数型インターフェースの背景にある考え方

【前置き】

Java8から関数型インターフェースが使用可能になりました。
具体的に「ラムダ式」「Stream」「Optional」「Files」と言った方がわかりやすいでしょうか。

関数型インターフェースの使用を半ば強制されるフレームワークが登場していたり(例:Apache Spark)、関数型インターフェースでJavaを書く開発者も増えてきたので、目にすることも多くなってきたかと思います。

関数型インターフェースは関数型プログラミングをサポートするものであるため、従来からJavaでサポートされていたオブジェクト指向プログラミングとは発想が異なります。
そのため、従来のJavaを学習してきた方にとっては抵抗感を感じるものであると思います。

今回の記事では、抵抗感を少しでも減らすために、関数型プログラミングの考え方を簡単に紹介したいと思います。

【サンプルコード】

言葉で説明するよりも先にサンプルコードを見た方がわかりやすいと思うので、サンプルコードを先に紹介します。
年齢のリストから30代の人数を数える、というプログラムです。
ごく短いプログラムですので、お付き合いください。

・FunctionTest.java

・実行結果

【関数型プログラミングの考え方】

関数型プログラミングでは、以下のことを実現しようとしています。
色々難しい用語(例えば「副作用」等)はあるのですが、今回は用語を使わずに簡潔にまとめます。

・内部状態(State)を排除する

最も本質的な考え方です。

関数型プログラミングでは、内部状態を排除することを目的としています。
「内部状態」とは、上記のコードで言うと「count」や「i」を指します。

内部状態が入りこんでしまうと、内部状態により関数の結果が変わってしまうため、内部状態を把握する必要が出てきてしまい、可読性が悪化します。
(把握のために「count」や「i」をトレースする必要が出てきてしまう)
把握しきれずに意図しないバグを出してしまうことも珍しくありません。
内部状態を排除して、品質を上げよう、という発想です。

また、コンピュータにとっては内部状態は重要ですが、人間にとってはやりたいことを実現できれば良く、内部状態は重要ではありません。
重要ではない記述を削減することでコードを完結にしたい、という発想もあります。

Java8のラムダ式では、ラムダ式の外部で定義された変数の値をラムダ式の内部で変更することを禁止されています(コンパイルエラーになる)。
その背景には、内部状態の排除があると思っています。

・自然言語に近い形で処理を記述する

これは、コードが簡潔になった結果生じた副次的な考え方かもしれません。

関数型プログラミングでは、関数を組み合わせることにより処理を実現します。
関数を次々とつなぎ合わせるように記述することで、ソースコードが自然言語に近い形になります。
わかりやすく言えば、ソースコード自体がコメントのようになります。

例えば、サンプルコードでは「年齢のリストから30代の人数を数える」という処理を行おうとしています。
従来のプログラミングでは、これを実現するためにforループとかカウント用の変数を使用しており、何をしているのか把握するためには、内部状態をトレースして意図を汲み取る必要があります。
しかし、関数型プログラミングでは、
「list.stream().filter(x -> x >= 30 && x <= 39).count()」→
「listを30<=x<=39でfilterしてcountする」
と読めるため、
「年齢のリストから30代の人数を数える」
という処理であることを自然に把握することができます。


いかがでしたでしょうか。

IT業界、特にSIer業界だと、情報処理技術者試験を軸にして知識を身に付けることが多いかと思います。
しかし、情報処理技術者試験では手続き型プログラミングやオブジェクト指向プログラミングを中心とした出題で、関数型プログラミングが扱われることは全く言って良いほどありません。
そのためとっつきにくさは拭えないと思いますが、先進的な企業を中心に関数型プログラミングを取り入れる企業も出てきています。
これからのことを考えると、せめて、関数型プログラミングに対する抵抗感は払拭するべきではないかと思っています。

今回はこれで締めくくりたいと思います。
では、また来週!

保守性の高いコードを作成するために心がけるべきこと

ソースコードは作って終わりではなく、その後何年、何十年にもわたって保守開発が行われます。
また、保守開発を行う開発者もその間に入れ替わります。
ソースコードを作る際は、このことを踏まえて高品質・低コストで保守開発ができるようにする必要があります。

難しい話をするとデザインパターンやフレームワークの話になるのですが、今回は新人も含めて最低限心がける必要があることを挙げていきます。

1.適切な変数名やメソッド名を与える

変数名・メソッド名を与える際は、その変数やメソッドが何をするのかわかるような名前にする必要があります。
例えば、「a」や「hoge」といった変数名は不可で、「loopEndFlag」や「commodityCode」といった意味のある変数名にする必要があります。

また、現場毎で命名規則が決められていることも多いので、それに倣った命名をする必要があります。
命名規則を無視すると、他のソースコードとの統一性が失われて読みにくいソースコードになったり、影響分析等のためにキーワードで検索する時に引っかからなくなったりします。

2.適切にコメントを記述する

コメントを入れることで、その箇所で何をしているのかがわかりやすくなります。
しかし、コメントを入れれば良いというものではなく、意味のあるコメントである必要があります。

例えば、

といったソースをそのまま日本語にしただけのようなコメントは不可で、

といった業務的な意味を書く必要があります。

また、ソースコードの先頭には「ソース名」「処理概要」「変更日」「変更概要」「変更者」といった情報を記述するのが一般的です。
メソッドの先頭には「メソッド名」「処理概要」「引数」「戻り値」「出力され得る例外」といった情報を記述するのが一般的です。

コメントの書き方も、現場毎で決まっていることが多いです。

3.分かりやすいロジックを心がける

保守開発の際にソースコードのロジックを読み解くことも多いので、if文のネスト(if文の中のif文)が多すぎる、goto文で制御があちこちに飛ぶ、といったロジックが分かりにくくなるような書き方も避けた方が良いです。
また、if文やfor文等を使う際は、インデント(左側のスペース)を適切に入れて、構造が分かりやすくなるようにした方が良いです。
(インデントの入れ方は各言語の入門書を真似れば良いです)

なお、新人の内はあまり気にする必要はありませんが、使用する文法も入門書に載っているものを中心にした方が無難です。
自分の現場で広まっているなら良いのですが、そうでないのに新しい文法やマイナーな文法を使うと、他の開発者(特に新人)から見て理解しにくくなることがあります。

4.ハードコーディングは原則禁止

ハードコーディングとは、マスタデータをソースコードの中に持たせることです。

例えば、

のような書き方は不可で、商品コードの一覧を持たせたいなら、データベースやファイルに持たせてそこから取得するべきです。

マスタデータは、追加や修正や削除が行われることがあります。
マスタデータをデータベースやファイルに持たせていない場合、追加・修正・削除が行われる度に、ソースコードを修正しコンパイルする必要が出てきて保守工数が増加します。

更に言うと、マスタコード読み込みのような色々なソースコードで使われる処理は、共通処理として別のソースコードに切り出すべきです。
これをしないと、修正する際に修正漏れが発生する可能性が高くなります。
大抵の場合、使うべき共通処理は現場毎やプロジェクト毎で決められているので、それに従う必要があります。

5.仕様書とソースコードを合わせる

仕様書には、ソースコードがどのような意図で作成されているのか、他のソースコードとどのような連携をしているのか、等の設計思想が記載されています。
しかし、仕様書がソースコードと乖離していた場合、仕様書から調査する時に実際の実装を誤って理解してしまいます。 そのことにより、保守開発でバグが生まれる原因になります。
それを防ぐために、ソースコードが仕様書から乖離した時は、仕様書もソースコードに合わせて修正するべきです。


いかがでしたでしょうか。

学生時代からコーディングしていたという人は少なからずいらっしゃると思うのですが、今回述べたことは学生時代のコーディングでは習慣として身に付きにくい所だと思います。
(私が新人だった頃もコーディング経験者の同期がいたのですが、その同期が研修で書いたコードを見ると、変数名が「a」とか「b」とかの適当な名前で、ソースコードが読み辛かったのが記憶に残っています)
同じソースコードを担当者を変えながら何年も何十年も保守開発していくのは社会人ならではだと思いますので、保守しやすいソースコードを書く習慣が身に付いていない方は、保守のしやすさを是非意識していただければと思っています。

では、また来週!

実務で良く見かけるループ処理

今回の記事では、実務で良く使われるループ処理のパターンを挙げていきます。

1.二重ループ

ループの内側にループがあるというのは良くあるパターンです。
javaでサンプルを書くと以下のようなパターンです。

実行結果は

となります。

何をしているのかと言うと、
・2次元配列に格納した値を順番に取り出している
・ただし、5を取り出したらその時点で処理を終了する
ということをしています。
(実務では、1次元目が親項目、2次元目が子項目のことが多いです)

このパターンのループの場合、原則として、内側のループの終了条件は外側のループの終了条件を内包しています。
今回の例で言うと、
・内側のループの終了条件…!endFlag && j < arraySizeY
・外側のループの終了条件…!endFlag
となっています。

このような場合、内側のループにしかない終了条件に着目すると、それぞれのループで何をしているのか把握しやすくなります。
今回の例で言うと、内側のループにしかない終了条件として「j < arraySizeY」があります。
この終了条件に着目することで、内側のループでは2次元目の配列を順番に読んでいる、ということを把握することができます。

2.リトライ処理

タイミングによって失敗する可能性がある処理を行う場合、失敗してもすぐに異常終了しないように、その処理が失敗した際にリトライをかける制御を入れることがあります。
例えば、通信やデータベースアクセス等で良く使われる制御です。

フローチャートで言うと以下のようになります。

「処理が失敗した時だけループを継続する」というロジックになっていたら、このパターンである可能性が高いです。

なお、今回のフローチャートでは省略していますが、無限ループで負荷がかからないように、ループ時にスリープを入れたり、ループ回数を設定したりすることも多いです。

3.先読みRead

レコードの中身を見てループ継続条件を判断する場合は、1レコード目のみループの前に先読みして、2レコード目以降はループの中で読み込む、ということをします。

フローチャートで言うと以下のようになります。
(EOFの場合の処理は省略しています)

派生パターンとしては、コントロールブレイクやマッチング処理があります。
詳しくは以下のページを参照してください。
コントロールブレイク
マッチング処理

なお、ファイルの場合はReadですが、SQLのカーソルの場合はFetchです。どちらにしても制御としては同じです。


いかがでしたでしょうか。

上記の3つのパターンが出てくるソースコードを分析する機会があったのですが、若手の方が分析に苦労していたので、今回の記事を書こうと思いました。
今回記事にしたことは半ば暗黙知化していることなので、それを文章にすることで少しでも他の技術者の助けになれれば幸いです。

これからも、参考になる情報を記事にしていきたいと思います!

プログラムによる小数点以下の計算で誤差が生じる原因と対処法2選

プログラムで小数点以下の計算を行う際、誤差が生じることがあります。
金額計算を行う時はこの誤差が即障害に繋がるので、誤差が生じないように実装する必要があります。

今回の記事では、誤差が生じる原因とその対処法を2つ挙げていきたいと思います。

1.浮動小数点型の丸め誤差

丸め誤差とは、小数点以下の数を2進数で表現できない(近似値を使わざるを得ない)ことにより発生する誤差です。
浮動小数点型(javaで言うとfloat型やdouble型)の変数を使用する際に、この問題が発生することがあります。
浮動小数点型を使用すると、例えば「0.3」が「0.29999999…」になったりするので、小数点以下の誤差が許されない場合には浮動小数点型を使用するべきではありません。

最も簡単な対処法は、整数型(javaで言うとint型等)で計算できるように、ファイルやテーブルの数値項目の単位を変えるという対処法です。
例えば、「金額項目は0.01円単位とする」という設計とすれば、「1.01円」を「101」と表すことが可能となり、整数型で計算できるようになります。

また、プログラム言語が任意精度型(javaで言うとBigDecimal型)の変数を用意している場合は、その型を使用することで丸め誤差の発生を防ぐことができます。
なお、COBOLの場合は丸め誤差が発生しないので、COBOLで小数点以下の項目を定義する場合は任意精度型であると考えて良いです。

2.中間結果を格納する領域の桁数不足による切り捨て発生

複数の四則演算を行って最終的な結果を得る際、中間結果を格納するための領域が必要になります。
もちろん、その領域が自分で定義した整数型の変数だったりすると、その時点で切り捨てが発生し、誤差が発生してしまいます。

このようなケースはレビューをすれば一目瞭然なのであまり心配はいらないのですが、問題なのはその中間結果がコンパイラにより暗黙的に用意される場合です。
具体的に言えば、COBOLでCOMPUTE文を使うような場合に問題になります。
基本的には、乗算を除算よりも先に行う、というのが誤差を防ぐための方法になりますが、中間結果の桁数の仕様を把握した上でそのような対処法を採用するのが望ましいです。
中間結果の桁数はコンパイラを用意しているベンダー毎で異なるので、詳しくはベンダーが用意しているマニュアル等で調べる必要があります。

例えば、COMPUTE文の中間領域が小数点以下0桁(コンパイラが暗黙的に設定)、最終結果を格納する領域が小数点以下0桁(プログラマが明示的に定義)である場合、以下のような計算結果になります。

・正しい計算

  123456円の消費税8%
 →123456円 * 1.08
 →133332.48円
 →133332円
  (小数点以下切り捨て)

・COMPUTE文で問題のある計算順

  123456 / 100 * 108
 →1234 * 108
  (下2桁が意図せず失われる)
 →133272

・COMPUTE文で問題のない計算順

  123456 * 108 / 100
 →13333248 / 100
 →133332
  (下2ケタを切り捨てる)


金額計算を行う場合、誤差が生じると重大な障害を生み出しかねません。
小数点以下の計算は特に誤差が生じやすく、上記で述べたことも実際のシステム開発で障害になりやすいポイントです。
多くの場合は、開発者のプログラミングの知識不足により小数点以下の計算の誤差が発生するので、そのような障害を少しでも減らしたいという思いで今回の記事を書きました。

これからも、開発者の役に立てるような記事を書いていきたいと思います!

java:ミュータブルな参照型変数の初期化の注意点

ミュータブルな参照型変数を初期化する場合、初期化の方法を間違えると他の変数も一緒に初期化してしまいます。
この記事では、ミュータブルな参照型変数の初期化方法を説明します。

【基本データ型変数と参照型変数】

変数は大きく分けて、基本データ型変数(プリミティブ型変数、値型変数)と参照型変数(オブジェクト型変数、クラス型変数)の2つに分けることができます。

基本データ型変数とは、以下の8つの内の何れかの型で定義された変数であり、オブジェクトを持ちません。
・byte
・short
・int
・long
・float
・double
・boolean
・char

参照型変数は以上の8つの型以外の型で定義された変数であり、オブジェクトを持ちます。
実際には、メモリ領域の番地を指し示すポインタのようなものが格納され、ポインタを辿って値を参照します。
C言語のポインタの概念を理解しているとこの概念も理解できます。C言語のポインタについては以下のページをご参照ください。

C言語:ポインタの概念の図解

【イミュータブルとミュータブル】

参照型変数は、更にイミュータブルとミュータブルの2つに大別することができます。

イミュータブルは、日本語で言うと「不変」という意味であり、オブジェクトの中の値を変更することができない変数を指します。
setter等でオブジェクトの中の値を変更できないようにする、メンバ変数をprivateで定義してオブジェクトの中の値を変更できないようにする、等の条件を満たすと、イミュータブルな型となります。
イミュータブルな型となる条件はかなり例外的であり、自分でクラスを作成する場合は意識的に条件を満たそうとしなければイミュータブルな型にはならないはずです。
標準で用意されている型の中では、String型がイミュータブルな型として有名です。
イミュータブルな参照型変数はオブジェクトの中の値を変更できないため、中の値を変更する場合は新たにメモリ領域を確保してオブジェクトを作り直すような動きになります。今回取り上げる記事上は、基本データ型変数と同じような動きになります。

ミュータブルはその逆で、オブジェクトの中の値を変更することができる変数のことであり、多くの参照型変数はこちらに該当します。
String型も、配列にした場合はミュータブルな型になります(配列がミュータブルなので)。
そして、注意が必要なのはこのミュータブルな参照型変数です。

【ミュータブルな参照型変数の初期化の注意】

参照型変数の中には、ポインタが格納されています。

例えば、

とした場合、fugaには「{“あ”,”い”,”う”}」が入るわけではなく、fugaのアドレスが格納されます。
つまり、hogeとfugaは同じ位置を指し示すことになります。

ここで、fugaのみ初期化(値を変更)しようとしたとします。
String型のようにイミュータブルな型であれば、fugaの値を変更された時点でメモリ領域が新たに確保されるので、hogeとfugaは別々の値となり、何の問題もありません。
しかし、String[]型のようにミュータブルな型の場合は、hogeとfugaが同じ位置を示しており、新たにメモリ領域が確保されることもないので、hogeが示す値も一緒に書き変わってしまいます。

例えば、

のようにfugaを全角スペースで初期化しようとすると、fugaだけでなくhogeも初期化されてしまいます。

これを防ぐためには、以下の2つの手順を踏む必要があります。
1.初期化したい変数にnullを代入してメモリ領域への参照を切る
2.newで初期化したい変数用のメモリ領域を新たに確保する

以下、サンプルコードで動きをまとめたいと思います。

【サンプルコード】

【実行結果】


「参照型」や「イミュータブル」といった概念は、プログラム経験の浅い人には難しいと思います
私の場合はC言語でポインタの概念を理解してからこれらの概念を学んだのですが、それでも初めの内はjava独特の仕様に戸惑いました。
しかし、概念を理解すればコーディングする上で便利だと感じることもあるので、この記事を通して理解を深めていただければ幸いです。

今後も、つまずきやすいポイントを記事にしていきたいと思います!