LoginSignup
34

More than 1 year has passed since last update.

~今さらながらJavaで関数型プログラミングを学習してみた~

Last updated at Posted at 2018-09-11

2019.11.03 初投稿以降1年を経過しましたので内容の本質を変えず、提示ソースの説明追加を中心にレビュー・更新しました。
2021.01.21 「実際Javaシステム開発においてどの程度関数型プログラミングを取り込むべきなのか?」を中心テーマとして第2部を投稿しました。
 第2部へのリンク
2022.07.14 「分かりにくいmap,flatMap,collectの追加紹介と知っておくと便利なCollector,Collections,Arrays」をテーマとして第3部を投稿しました。
 第3部へのリンク

はじめに

Java8が2014年3月にリリースされてから4年以上がたちJava9やJava10以降を含め多くの方々に利用されています。Java8リリース当時話題となったのが関数型プログラミングです。現在では関数型プログラミングという言葉はシステム開発関係者の間では一般的に認知され浸透しています。
しかし、現状関数型プログラミング主体のシステム開発実績を頻繁に目にするという状況ではないことから実際の浸透はまだまだ先の事だと思われます。そして、Java8もそうですが既存言語も関数型プログラミングスタイルを取り入れて機能強化を進めています。
今後どういう流れになっていくかは予測できませんが、個人的には既存言語の中で関数型プログラミングスタイルを使っていくというのが主流になるのではと推測しています。純粋な関数型言語は特化した使い方になり、Java系で言えばScalaがJava言語にとって代わるという状況にはならないだろうと考えています。
このような状況にあると思える関数型プログラミングですが、今後何らかの形で浸透いくと予想されるので、今さらながらですが、全体調査を行い、Java10環境で簡単なプログラミングを実践学習してみました。
また、興味ある方の何らかの参考になるのではと思い備忘録も兼ねて学習結果を投稿することにしました。なお、内容には主観にもとづく部分や、個人的指向が入ったプログラムスタイルになっている場合もありますので了承お願い致します。
関数プログラミング特有の語句については後尾部分に理解した範囲での簡単な説明を記述していますので参考にして下さい。(*番号が付記された語句)

追加されたJava8新機能

Java8では関数プログラミングに関して以下の新機能が追加されています。

  • 関数型インターフェイス(*1)
  • ラムダ式(*2)
  • StreamAPI(*5)
  • Optional(*6)
  • Map/List/Setの一部メソッド

これらの紹介に入る前にそもそも関数型プログラミングとはどういうものなのかについて学習した範囲で簡単に説明を試みたいと思います。

関数型プログラミングとは

関数型プログラミングとは狭義では「参照透過性(*9)を持った副作用(*8)のない関数によってプログラムを組み立てていく手法」となっており、この定義では関数型言語がベースとなるのですが、現在では既存言語でも関数型プログラミングスタイルでの実装が可能となっており、ここでは関数型プログラミングを広域的な視野でとらえることとし、その特徴を3点挙げてみました。

  1. 関数の引数に関数(ラムダ式など)を渡せる機能を活用してプログラミングの簡素化 を行う。
  2. 言語・APIにより提供されている関数型プログラミング用各種メソッド(関数)や文法等を活用してプログラミングの簡素化や品質の向上を行う。
  3. 参照透明性(*9)のある副作用の無い関数を作成・活用し、その関数を組み合わせてシステムの大部分を開発し、安全で高品質なシステムを目指す。なお、問題対象を数学的に表現できればほぼ全てを関数で開発することができると一般的には言われています。

上述のような方法・目的でプログラムを作成していくことが広い視野での関数型プログラミングと認識できると思います。また、関数型プログラミングはオブジェクト指向のように要求分析設計工程といった開発手法と連携したものでなく、それ自体はプログラム実装工程が対象となっています。
関数型プログラミングを行うために、HaskellやScalaをはじめとした言語レベルで対応したものや、Java8のように既存言語に関数型プログラミング用APIを付加したもの等があります。
Java8は1.~2.までの範囲を、HaskellやScalaは1.~3.までの範囲をカバーできていると言われています。副作用をほぼ避けることができるプログラミング言語は純粋関数型プログラミング言語とも呼ばれており、この点から言えばScalaの3.は純粋な3.というより準3.というレベルだと思われます。ただ、システムを「副作用をほぼ避けた関数」で構成する必要があるかはそのプロジェクト次第である事は言うまでもありません。
関数型プログラミングは比較的小さな関数を作りこれを適用あるいは組み合わせてシステムを開発していくというような手法をとっており、宣言型プログラミング(*3)形式ベースとなっています。従って、3.のレベルを実装するためには従来のオブジェクト指向プログラミング手法ではなく関数型言語および関数型プログラミングの考え方を十分理解しておく必要があります。
また、対象問題がどの程度関数型プログラミングにマッチしているのか、連携するシステム、基盤ミドルウェア、基盤フレームワーク、関連蓄積資産、開発・管理・運用環境といった面にも事前に十分把握しておく必要があります。
関数型プログラミングのメリットとして以下の項目が一般的には挙げられています。

  • プログラミング量が減る。
  • 品質が向上する。
  • テストをしやすい。
  • 高速化に寄与する。
  • 並列化がしやすい。

4番目及び5番目の項目については多少ケースバイケースという状況があるように思えますが、適用部分についてプログラミング量が減少するというのは確かだと思われます。
一方欠点としては、開発システムが全て数学的に表現できるのであれば理論上は純粋な関数プログラミングで開発可能なのですが、現実的にはそのような環境は少なく、特にビジネス系システムでCRUD処理がメインとなるようなアプリケーションでは余りメリットが無いのではと言われています。
Java8では、関数型プログラミングに対応するために関数型インターフェース、ラムダ式、Stream、Optionalなどの仕組みが新たに導入されました。これにより従来の命令型プログラミングの一部が関数型プログラミング(宣言型プログラミング)スタイルで記述することが可能になりました。
そしてこのJava8以降で関数型プログラミングを行う時には、

  1. オブジェクト指向プログラミングをベースにして可能な範囲で新規APIを使っていく。(主としてコーディング量の減少)
  2. もう少し進んで、参照透明性のある(できれば副作用の無い)関数を可能な範囲で作成・活用してメソッドを実装していく。(コーディング量減少と品質の向上)

の2通りのケースがあると思いますが、個人的には(1)→(2)とステップを踏んで習得していくのが良いかと思います。
なお、気を付けなければいけないことは関数型プログラミングはJava8以降でしか稼働しないということです。Java6やJava7等の旧Javaバージョンでも稼働可とするには関数型プログラミングではなく、これまで通りの命令型プログラミングで開発する必要があります。

Java8新機能の概説

Java8において関数型プログラミングに関連して新規・更新された主なメソッドは以下の通りです。これらのメソッドを使用して関数型プログラミングの基本的な機能が利用できるようになっています。オブジェクト指向言語に関数型プログラミングを取り入れたという表現ができるのではないかと思います。
これら各メソッドの詳細はネットや書籍で簡単に調べることができます。
ここでは各メソッドの概説と代表的なメソッドについてサンプルを提示していきます。

・関数型インターフェイス
関数型インターフェイスを使用することによりラムダ式やメソッド参照(*7)が使用できるようになります。自作も可能ですが、標準的な関数型インターフェースが何種類か用意されています。

  • Supplier:引数なしで値を返すインターフェイス。(get, ()→T)
  • Consumer:引数ありで処理だけを行い値を戻さないインターフェイス。(accept, T→void)
  • Predicate:引数ありで条件判定処理を行い結果をboolean型で戻すインターフェイス。(test, T→boolean)
  • Function:引数ありで処理を行い任意の型のオブジェクトを戻すインターフェイス。(apply, T→R)
  • BiFunction:引数(2個)ありで処理を行い任意の型のオブジェクトを戻すインターフェイス。(apply, T,U→R)
  • ToIntFunction:引数ありで処理を行いint型の値を戻すインターフェイス。(applyAsInt, T→int)
  • ToLongFunction:引数ありで処理を行いlong型の値を戻すインターフェイス。(applyAsLong, T→Long)
  • ToDoubleFunction:引数ありで処理を行いDouble型の値を戻すインターフェイス。(applyAsDouble, T→Double)

():主なメソッド名, 引数の型→戻り値の型
以下は自作関数型インターフェイスの単純な定義・使用サンプルです。

[InterfaceExample関連コード説明]
・趣味名を指定し、「my hobby : 趣味名 」を出力するプログラムです。
・009~012で抽象メソッドgetHobby(戻り値:文字列)を持つ関数型インターフェイスIHobby定義しています。
・003~005でIHobbyの処理実装をラムダ式を用いて定義しています。
・006でIHobbyのgetHobbyメソッドを実行しています。

InterfaceExample
001 public class InterfaceExample{
002     public static void main(String[] args){
003     IHobby hb = (String hobby) -> {
004       return "my hobby : " + hobby;
005     };
006     System.out.println(hb.getHobby("cycling"));
007   }
008 }

009 @FunctionalInterface
010 interface IHobby{
011   public String getHobby(String hobby);
012 }

以下はFunction標準関数型インターフェイスを使用した単純なサンプルです。

[LambdaParameter関連コード説明]
・指定した値を5倍して出力するプログラムです。
・016~018で値を5倍するメソッドgetFiveTimes(戻り値:Integer型)を定義しています。
・007~009でexecuteメソッドを実行して5倍値を取得しています。
・012で007~009で定義した第2引数のラムダ式を関数型インターフェイスで受けています。
・013で関数型インターフェイスのapplyメソッドを実行しています。

LambdaParameter
001 package functiontest;

002 import java.io.IOException;
003 import java.util.function.Function;

004 public final class LambdaParameter {

005   public static void main(String[] args) throws IOException {
006     Integer value = 1;
007     Integer res = execute(value, t -> {
008       return getFiveTimes(t);
009     });
010     System.out.println("結果:" + res);
011   }

      /**
      * 引数の関数インターフェイス(ラムダ式)を実行するメソッド
      */
012   public static <R> R execute(Integer value, Function<Integer, R> fnc) {
013     R rtnval = fnc.apply(value);
014     return rtnval;
015   }

      /**
      * 具体的な処理を実行するメソッド(値を5倍する)
      */
016   public static Integer getFiveTimes(Integer value) {
017     return value * 5;
018   }

019 }

・Map
既存のMapクラスにも新しいメソッドが追加されています。
追加された主なメソッドは、forEach、replaceAll、computeIfAbsent、computeIfPresent、compute、mergeとなっています。

  • forEach:Map全要素について順次指定した処理を行います。
  • replaceAll:Map全要素の値を指定関数の値に全て置き換えます。
  • computeIfAbsent:指定キーが無い場合に、指定関数の値を持つMap要素を追加します。
  • computeIfPresent:指定キーが有る場合に、Map要素の値を指定関数の値に設定します。
  • compute:指定キーが無い場合にはMap要素(key,指定関数値)が追加され、有る場合は指定関数の値に変更します。
  • merge:指定キーが無い場合にはMap要素(key,引数値)が追加され、有る場合は指定関数の値に変更します。

以下は単純なサンプルです。(k:Mapキー v:Map値 p:引数値 key:指定キー)

[Map関連コード説明]
mapの型はString,Stringとしています。
・forEach(001):
 a. mapの全要素について[キー:値]の形式で出力します。
・replaceAll(002~):
 a. map全要素の値をヌル→[キー]に、ヌル以外→[キー+値の先頭2文字]に置き換えます。
・computeIfAbsent(008~):
 a. keyがmapのキーに存在する場合
   値は更新されず、値が戻り値となります。
 b. キーに存在して値がヌルの場合
   値が[キー+"-addition"]に更新され、その値が戻り値となります。
 c. キーに存在しない場合
   キーがkeyで、値が[key+"-addition"]の値をmapに追加し、その値が戻り値となります。
・computeIfPresent(011~):
 a. keyがmapのキーに存在する場合
   値が[キー+値+"-addition"]に更新され、その値が戻り値となります。
 b. キーに存在して値がヌルの場合
   値は更新されず、ヌルが戻り値となります。
 c. キーに存在しない場合
   値は追加・更新されず、ヌルが戻り値となります。
・compute(014~):
 a. keyがmapのキーに存在する場合
   値が[キー+値+"-addition"]に更新され、その値が戻り値となります。
 b. キーに存在して値がヌルの場合
   値が[キー+値(ヌル)+"-addition"]に更新され、その値が戻り値となります。
 c. キーに存在しない場合
   (key, [key+値(ヌル)+"-addition"])の要素をmapに追加、その値が戻り値となります。
・merge(017~):
 a. keyがmapのキーに存在する場合
   値が[値+"-add"]に更新され、その値が戻り値となります。
 b. キーに存在して値がヌルの場合
   値が["-add"]に更新され(ヌル値は無視)、その値が戻り値となります。
 c. キーに存在しない場合
   (key, ["-add"])の要素(ヌル値は無視)をmapに追加、その値が戻り値となります。

MapMethod
    forEach
001   map.forEach((k, v) -> System.out.println(k + ":" + v));
    replaceAll
002   map.replaceAll((k, v) -> {
003     if (null == v) {
004       return k;
005     }
006     return k + v.substring(0, 2);
007   });
    computeIfAbsent
008   map.computeIfAbsent(key, k -> {
009     return k + "-addition";
010   });
    computeIfPresent
011   map.computeIfPresent(key, (k, v) -> {
012     return k + v + "-addition";
013   });
    compute
014   map.compute(key, (k, v) -> {
015     return k + v + "-addition";
016   });
    merge
017   map.merge(key, "-add", (v, p) -> v + p);

・List
既存のListクラスにも新しいメソッドが追加されています。
追加された主なメソッドは、forEach、removeIf、replaceAll、stream、parallelStreamとなっています。

  • forEach:全要素について順次指定した処理を行います。
  • removeIf:指定した関数の条件に合致した要素を削除します。
  • replaceAll:指定要素の値を指定関数の値に全て置き換えます。
  • sort:指定要素の値をソートします。
  • stream:Stream(直列ストリーム)を返します。
  • parallelStream:Stream(並列ストリーム:並列に処理可能なStream)を返します。

以下は単純なサンプルです。(v:リスト値 value:外部値)

[List関連コード説明]
listの型はStringとしています。
・forEach(001):
 a. listの全要素について値を出力します。
・removeIf(002~):
 a. list内に指定値が有る:該当要素は削除され、trueが戻ります。
 b. 指定値がヌル:該当要素は削除されずfalseが戻ります。
 c. 指定値が無い:要素は削除されずfalseが戻ります。
・replaceAll(008~):
 a. 全要素について値がヌル以外:値の先頭2文字に更新されます。
 b. 全要素について値がヌル:値は更新されません。
・sort(014):
 a. 全要素に関して自然順・null最大値ソートを行います。
・stream(015):
 a. listのStreamを取得します。
・parallelStream(016):
 a. listの並列Streamを取得します。

ListMethod
    forEach
001   list.forEach(v -> System.out.println(v));
    removeIf
002   list.removeIf(v -> {
003     if (null == v) {
004       return false;
005     }
006     return v.equals(value);
007   });
    replaceAll
008   list.replaceAll(v -> {
009     if (null == v) {
010       return v;
011     }
012     return v.substring(0, 2);
013   });
    sort
014   list.sort(Comparator.nullsLast(Comparator.naturalOrder()));
    stream
015   Stream sm = list.stream();
    parallelStream
016   Stream sm = list.parallelStream();

・StreamAPI
配列やCollectionなどを扱うためのAPIで、値の集計などのデータ処理に利用できます。Streamのメソッドは、動作内容で中間処理用と終端処理用の2種類に分類されます。
0個以上の中間処理用メソッドを経由して終端処理用メソッドを実行します。
プリミティブなStreamAPIも用意されています。
中間処理用メソッド
asDoubleStream、asLongStream、boxed、distinct、filter、flatMap、flatMapToDouble、flatMapToInt、flatMapToLong、limit、map、mapToDouble、mapToInt、mapToLong、mapToObj、onClose、parallel、peek、sequential、skip、sorted、unordered
以下は代表的メソッドの簡易説明です。

  • filter:条件に一致したオブジェクトを抽出します。
  • map:入力値を型変換した値にしてStream型で返します。
  • flatMap:入力値を複数の型変換した値にして処理しStream型で戻します。
  • distinct:重複した値を排除します。
  • sorted:ソートします。
  • limit:指定した個数に限定します。

以下は単純なサンプルです。
(stream:Streamインスタンス v:ストリーム値 list:外部List numlimit:外部値)

[Stream関連コード(中間処理)説明]
streamの型はIntegerとしています。
・filter(001~):
 a. stream全要素について3の倍数を抽出し、Streamで戻しています。
・map(004):
 a. listに文字列ファイル名が格納されており、これをlist→streamに変換します。
 b. その後更にStreamに変換後、この値を戻しています。
・flatMap(005~):
 処理内容:list各要素について数字分だけ先頭に"abc"をつけたコピーを作成し戻しています。
 a. listに半角1文字+数字1桁が格納されており、数字の個数分["abc"+値]の配列を作成します。
 b. その後この配列をStream型に変換します。更にこの値をListに変換しています。
・distinct(010):
 a. stream全要素について重複要素を排除して同型で戻しています。
・sorted(011):
 a. streamの全要素数を昇順でソートし同型で戻しています。
・limit(012):
 a. streamの全要素数をnumlimitに制限し同型で戻しています。

StreamMethod1
    filter
001   stream.filter(v -> {
002     return (v%3) == 0;
003   });
    map
004   list.stream().map(Paths::get);
    flatMap
005   list.stream().flatMap(v -> {
006     String[] array = new String[Integer.parseInt(v.substring(1))];
007     Arrays.fill(array, "abc" + v);
008     return Stream.of(array);
009   }).collect(Collectors.toList());
    distinct
010   stream.distinct();
    sorted
011   stream.sorted();
    limit
012   stream.limit(numlimit);

終端処理用メソッド
allMatch、anyMatch、average、collect、count、findAny、findFirst、forEach、forEachOrdered、iterator、max、min、noneMatch、reduce、spliterator、sum、summaryStatistics、toArray
以下代表的メソッドの簡易説明です。

  • allMatch:指定関数の条件に全て合致すればtrueを返します。
  • anyMatch:指定関数の条件に合致する値があればtrueを返します。
  • collect:要素の値を使用して結果を作成します。
  • count:要素の数を返します。
  • reduce:要素の値を集約します。
  • max:最小値(Optional型)を返します。
  • min:最小値(Optional型)を返します。
  • toArray:配列に変換します。

以下は単純なサンプルです。
(stream:Streamインスタンス v:ストリーム値 array:外部配列 ac:アキュムレータ)

[Stream関連コード(終端処理)説明]
・allMatch(001~):
 a. streamを全一致条件(0<値>10)でチェックしています。全要素が一致するとtrueが戻ります。
・anyMatch(007~):
 a. streamをany条件(=5)でチェックしています。:どれか1つ以上一致するとtrueが戻ります。
・collect(010~):
 a. 例1:arrayをstreamに変換し、CollectorsクラスのtoListメソッドを使用してList型を戻しています。Collectorsクラスにはstream→他クラスへ変換するためのメソッドが複数用意されています。
 b. 例2:listをstreamに変換し、stream値をStringBuilderに格納して同型を戻しています。(第3引数の実質的な意味合いはこのケースの場合ありません。通常並列処理の場合意味合いを持ちます。)
   collectメソッドの引数説明は以下の通りです。
   * 第1引数:結果を格納するオブジェクト作成関数。
   * 第2引数:Stream値をオブジェクトに入れる関数。
   * 第3引数:結果オブジェクト同士を結合する関数。
・count(012):
 a. streamの要素数を取得しています。long型で戻ります。
・reduce(013):
 a. streamの要素を集約しています。Optional型で戻ります。streamがa,b,cという3要素の場合abcが戻ります。
・max(014):
 a. streamの最大値を自然順で取得しています。
・min(015):
 a. streamの最小値を自然順で取得しています。
・toArray(016):
 a. streamを配列型に変換しarrayに格納しています。
・filter+forEach(017~):
 a. stream全要素について3の倍数を抽出し標準出力しています。

StreamMethod2
    allMatch
001   stream.allMatch(v -> {
002     if ((0 < v) && (10 > v)) {
003       return true;
004     }
005     return false;
006   });
    anyMatch
007   stream.anyMatch(v -> {
008     return v.equals(5);
009   });
    collect(複数タイプ存在)
010   //例1:Arrays.stream(array).collect(Collectors.toList());
011   //例2:list.stream().collect(StringBuilder::new, (b, v) -> b.append(v), (b1, b2) -> b1.append(b2));
      //第1引数:結果収納オブジェクト生成、第2引数:ストリーム値を収納 第3引数:オブジェクト結合
    count
012   list.stream().count();
    reduce(複数タイプ存在)
013   list.stream().reduce((ac, v) -> ac + v);
    max
014   list.stream().max(Comparator.naturalOrder());
    min
015   list.stream().min(Comparator.naturalOrder());
    toArray
016   Object[] array = list.stream().toArray();
    filter+forEach
017   stream.filter(v -> {
018     return v%3 == 0;
019   }).forEach(System.out::println);

・Optional
Optionalは値を1つラップするクラスです。
ヌルや値の有無をチェックすることができます。
Optional.ofNullable、Optional.empty()、opt.isPresent()、・・

Java8での関数型プログラミングの例

ネットや書籍では数行のサンプルが多いのですが、ここでは一定の機能を持ったレベルの例を提示することにしました。
ただし、関数プログラミングはこうあるべきといった内容ではなく、単に初歩的理解のための参考コーディングとしての視点で作成しています。
機能は商品の在庫管理でメインクラスとFunctionStockクラスの2クラスから構成されています。
FunctionStockクラスは形式的に出来るだけ多くのAPI使用例を提示するため必要としない箇所でも関数型プログラミングを行っていますので、予め了承お願いいたします。
クラス概要
 StockMain:在庫管理のメインクラス。
  関数型プログラミングは行っていません。
 FunctionStock:実際に在庫処理を行うクラス。
  複数のメソッドで関数型プログラミングを行っています。
 IStock:在庫処理クラスのインターフェイス。
 IStockExecuter:在庫処理の自作関数インターフェイス。
詳細な機能はメインクラスのコメントに、メソッドの概要もコメントを付けて記述していますので参考にして下さい。
☆印で使用している関数型プログラミングのタイプを記述しています。

[StockMain関連コード説明]
013~014で在庫数及び付属品構成を格納するMapを定義しています。
025~036で6個の品目及び初期在庫数の設定をしています。cdとmagazineには付属品があります。
016~022で具体的な処理を設定しています。
 bookは2つの仕入れ処理、1つの販売処理を定義しています。
 magazineは1つの販売処理を定義しています。
 最後で在庫一覧出力処理を定義しています。
037~046はstockメソッドで仕入れ処理を実行しています。
047~056はsaleメソッドで販売処理を実行しています。数量は負値としています。
057~066はgetメソッドで指定商品の在庫数を出力しています。
067~076はgetListメソッドで商品の在庫一覧を出力しています。
仕入れ処理、販売処理、在庫数出力、在庫一覧出力はいずれもFunctionStockのexecuteメソッドで実行しています。

StockMain.java
001 package stocksample;

002 import java.util.Arrays;
003 import java.util.List;
004 import java.util.Map;
005 import java.util.TreeMap;

    /**
     * 在庫管理のメインクラス
     * 機能:仕入れ処理(stock)、販売処理(sale)、在庫確認処理(get)、在庫一覧出力処理(getList)
     * ():メソッド名
     * 在庫データ処理形式:メソッド名(商品名, 数量)
     * 各商品には付属品(複数可能ただし各数量は1個限定)が付けられるようになっていてその付属品の在庫管理も行っている。
     * 仕入れ処理:指定商品の在庫数を仕入れ数だけ増やす。指定商品が登録されていない場合は商品登録も行う。
     * 販売処理:指定商品の在庫数を販売数だけ減らす。ただし付属品も含めて在庫がない場合は処理を中止する。
     * 在庫確認処理:指定商品の在庫数を出力する。
     * 在庫一覧出力処理:全商品の在庫数を出力する。付属品リストも合わせて出力する。
     * エラーの扱い:発生時点でエラーメッセージを出力し以降の処理を中止する。
     */
006 public class StockMain {
007   private static Map<String, Integer> stockMap;
008   private static Map<String, List<String>> subStockMap;
009   private static IStock stockObject;
010   private static final boolean funcOption = true; //JDK8版
      //private static final boolean funcOption = false; //JDK6,JDK7版

011   public static void main(String[] args) {
012     System.out.println("**start**");
        //
        //初期マップ値をセット
013     stockMap = new TreeMap<String, Integer>();
014     subStockMap = new TreeMap<String, List<String>>();
015     setInitialMap();
        //
        //在庫データ処理
016     stock("book", 1);
017     stock("book", 2);
018     sale("book", 2);
019     get("book");
020     sale("magazine", 2);
021     get("magazine");
022     getList();
        //
023     System.out.println("**end**");
024   }

      /**
      * 初期マップ値をセットするメソッド
      */
025   private static void setInitialMap() {
026     List<String> cdlist = Arrays.asList("posterA", "posterB", "posterC");
027     subStockMap.put("cd", cdlist);
028     List<String> mglist = Arrays.asList("bagA");
029     subStockMap.put("magazine", mglist);
030     stockMap.put("cd", 3);
031     stockMap.put("magazine", 3);
032     stockMap.put("posterA", 3);
033     stockMap.put("posterB", 3);
034     stockMap.put("posterC", 3);
035     stockMap.put("bagA", 3);
036   }

      /**
      * 仕入れ処理を行うメソッド
      */
037   private static void stock(String productName, int quantity) {
038     if (funcOption) {
039       stockObject = new FunctionStock(productName, quantity, "add");
040     } else {
041       stockObject = new Stock(productName, quantity, "add");
042     }
043     setMap();
044     int result = stockObject.execute();
045     if (0 > result) System.exit(result);
046   }

      /**
      * 販売処理を行うメソッド
      */
047   private static void sale(String productName, int quantity) {
048     if (funcOption) {
049       stockObject = new FunctionStock(productName, -quantity, "add");
050     } else {
051       stockObject = new Stock(productName, -quantity, "add");
052     }
053     setMap();
054     int result = stockObject.execute();
055     if (0 > result) System.exit(result);
056   }

      /**
      * 指定商品の在庫数を出力するメソッド
      */
057   private static void get(String productName) {
058     if (funcOption) {
059       stockObject = new FunctionStock(productName, "get");
060     } else {
061       stockObject = new Stock(productName, "get");
062     }
063     setMap();
064     int result = stockObject.execute();
065     if (0 > result) System.exit(result);
066   }

      /**
      * 在庫一覧を出力するメソッド
      */
067   private static void getList() {
068     if (funcOption) {
069       stockObject = new FunctionStock("getlist");
070     } else {
071       stockObject = new Stock("getlist");
072     }
073     setMap();
074     int result = stockObject.execute();
075     if (0 > result) System.exit(result);
076   }

      /**
      * ストックオブジェクトにマップをセットするメソッド
      */
077   private static void setMap() {
078     stockObject.setStockMap(stockMap);
079     stockObject.setSubStockMap(subStockMap);
080   }
081 }

[FunctionStock関連コード説明]
028~042はコンストラクタで013~014でproductName, quantity, typeを設定しています。
typeは処理の種類を表しadd、delname、get、getlistを指定できます。
・executeメソッド
044で指定在庫データを確認用出力をしています。
045~048で指定在庫データをチェックしています。getDataCheckFunction().get()メソッドで在庫データのチェックを行い、結果をStringを取得後、Optional.ofNullableでOptionalに変換し、ifPresentでOptionalのnull判定を行いエラーがあればoutputErrorMessageでエラー表示しています。
052で在庫データ処理をexecuteStock().execute()メソッド(IStockExecuter自作関数インターフェイス)で実行し、処理結果をOptional 総称型:Integerに格納しています。
053で在庫データ処理でのエラーメッセージ生成をgetErrorKeyFunction().apply(result)メソッドで行い、outputErrorMessageで出力しています。
・getDataCheckFunctionメソッド
066~078で在庫データをチェック(ヌルチェック等)する関数型インターフェイス(Supplier 総称型:String)の実装を定義しています。
・executeStockメソッド
079~096で在庫データを処理(処理タイプ:add,delname,get,getlist)する関数型インターフェイス(IStockExecuterの)実装を定義しています。
updateStock().get()、 deleteStockName().get()、getStock().get()、outputStockList(getStockList().get())、outputSubStockList()を呼び出しています。
・updateStockメソッド
097~126で在庫数を更新する関数型インターフェイス(Supplier 総称型Optional)の実装を定義しています。
addToStockMap().apply(・・)メソッドを使用して在庫数を更新処理しています。
・addToStockMapメソッド
127~138で在庫数を具体的に更新する関数型インターフェイス(BiFunction 総称型:String, Integer, Optional)の実装を定義しています。
Mapのcomputeメソッドを使用して在庫数の増減算術を行っています。
・deleteStockNameメソッド
139~147で在庫対象品目(在庫データ)を削除する関数型インターフェイス(Supplier 総称型:Optional)の実装を定義しています。
Mapのremoveメソッドを使用して在庫対象品目の削除処理を行っています。
・getStockメソッド
148~154で特定品目の在庫数を取得する関数型インターフェイス(Supplier 総称型:Optional)の実装を定義しています。
MapのgetOrDefaultメソッドを使用して在庫数取得を行っています。
・getStockListメソッド
155~166で在庫数一覧を取得する関数型インターフェイス(Supplier 総称型:String)の実装を定義しています。
MapのforEachメソッドを使用して在庫一覧を生成しています。
・getErrorKeyFunctionメソッド
167~175で在庫処理結果をチェックする関数型インターフェイス(Function 総称型:Optional, String)の実装を定義しています。
Optional(errindex)のmapメソッドでエラーの場合のメッセージ生成を定義しています。
・outputSubStockListメソッド
191~204で付属品一覧を出力しています。特定品目に対する付属品出力情報はlist→streamに変換後collectメソッドで生成しています。
・outputErrorMessageメソッド
205~220でmessageKeyに対するエラーメッセージ出力をしています。messageKeyをOptionalに変換後mapメソッドでエラーメッセージを生成しています。

FunctionStock.java
001 package stocksample;

002 import java.util.Arrays;
003 import java.util.List;
004 import java.util.Map;
005 import java.util.Optional;
006 import java.util.function.BiFunction;
007 import java.util.function.Function;
008 import java.util.function.Supplier;

    /**
     * 在庫管理を行うクラス
     */
009 public final class FunctionStock implements IStock {
010   private String productName;
011   private int quantity;
012   private Map<String, Integer> stockMap;
013   private Map<String, List<String>> subStockMap;
014   private String type;
015   private String errorKey;
016   private String errorProductName;
017   private final List<String> typeList = Arrays.asList("add", "delname", "get");
018   private final List<String> errorKeyList = Arrays.asList("zerostock,subzerostock", "noname", "noname");
019   private final List<String> errorMessageKeyList= Arrays.asList(
020   "nullname", "noname", "number", "zerostock", "subzerostock","keyerror");
021   private final List<String> errorMessageList= Arrays.asList(
022   "★製品名が指定されていません。",
023   "★在庫リストに指定された製品名が存在しません。",
024   "★数量が指定されていません。",
025   "★在庫量がゼロ未満になってしまいます。 <%p1%> <%p2%>個",
026   "★付属品の在庫量がゼロ未満になってしまいます。 <%p1%> <%p2%>個",
027   "★キーが異常です。");

      /**
      * コンストラクタ
      */
028   public FunctionStock(String productName, int quantity, String type) {
029     this.productName = productName;
030     this.quantity = quantity;
031     this.type = type;
032   };

      /**
      * コンストラクタ
      */
033   public FunctionStock(String productName, String type) {
034     this.productName = productName;
035     this.quantity = 0;
036     this.type = type;
037   };

      /**
      * コンストラクタ
      */
038   public FunctionStock(String type) {
039     this.productName = "";
040     this.quantity = 0;
041     this.type = type;
042   };

      /**
      * 在庫データを処理するメソッド
      * ☆OptionalのofNullable、ifPresent及びorElseメソッドを使用
      * ☆Functionインターフェイスのapplyメソッドを使用
      */
043   public int execute() {
        //在庫データ出力
044     outputData();
        //在庫データチェック
045     Optional.ofNullable(getDataCheckFunction().get()).ifPresent(ekey -> {
046       outputErrorMessage(ekey);
047       errorKey = ekey;
048     });
049     if (null != errorKey) {
050       return -1;
051     }
        //在庫データ処理
052     Optional<Integer> result = executeStock().execute();
        //エラー出力
053     outputErrorMessage(getErrorKeyFunction().apply(result));
054     return result.orElse(-1);
055   }

      /**
      * 在庫データを出力するメソッド
      */
056   private void outputData() {
057     StringBuilder sb = new StringBuilder();
058     sb.append("処理データ:");
059     sb.append(productName);
060     sb.append(",");
061     sb.append(quantity);
062     sb.append(",");
063     sb.append(type);
064     System.out.println(sb.toString());
065   }

      /**
      * 在庫データをチェックするメソッド
      * ☆関数型インターフェースSupplierを使用
      */
066   private Supplier<String> getDataCheckFunction() {
067     return () -> {
068       if (null == productName || (!"getlist".equals(type) && "".equals(productName))) {
069         return "nullname";
070       }
071       if ("add".equals(type)) {
072         if (0 == quantity) {
073           return "number";
074         }
075       }
076       return null;
077     };
078   }

      /**
      * 在庫データを処理するメソッド
      * ☆自作関数型インターフェースIStockExecuterを使用
      * ☆Optionalのemptyメソッドを使用
      */
079   private IStockExecuter executeStock() {
080     return () -> {
081       Optional<Integer> result = Optional.empty();
082       if ("add".equals(type)) {
083         result = updateStock().get();
084       } else if ("delname".equals(type)) {
085         result = deleteStockName().get();
086       } else if ("get".equals(type)) {
087         result = getStock().get();
088       } else if ("getlist".equals(type)) {
089         outputStockList(getStockList().get());
090         outputSubStockList();
091       } else {
092         errorKey = "keyerror";
093       }
094       return result;
095     };
096   }

      /**
      * 在庫数を更新するメソッド(増減可)
      * ☆関数型インターフェースSupplierを使用
      * ☆関数型インターフェースBiFunctionのapplyメソッドを使用
      * ☆Optionalのofメソッドを使用
      * ☆ListのforEachメソッドを使用
      * ☆MapのgetOrDefaultメソッドを使用
      */
097   private Supplier<Optional<Integer>> updateStock() {
098     return  () -> {
099       if (0 > addToStockMap().apply(productName, quantity).get()) {
100         addToStockMap().apply(productName, -quantity);
101         return Optional.of(-1);
102       }
103       if (0 > quantity) {
104         List<String> slist = subStockMap.get(productName);
105         if (null != slist) {
106           slist.forEach(v  -> {
107             if (null != errorProductName) return;
108             int substock = stockMap.getOrDefault(v, -1);
109             if (-1 == substock || 0 > (substock + quantity)) {
110               errorProductName = v;
111             }
112           });
113           if (null == errorProductName) {
114             slist.forEach(v  -> {
115               addToStockMap().apply(v, quantity);
116             });
117           }
118         }
119         if (null != errorProductName) {
120           addToStockMap().apply(productName, -quantity);
121           return Optional.of(-2);
122         }
123       }
124       return Optional.of(0);
125     };
126   }

      /**
      * 商品の在庫数値を更新するメソッド
      * ☆関数型インターフェースBiFunctionを使用
      * ☆Mapのcomputeメソッドを使用
      * ☆Optionalのofメソッドを使用
      */
127   private BiFunction<String, Integer, Optional<Integer>> addToStockMap() {
128     return (pname, qty) -> {
129       int addedValue = stockMap.compute(pname, (k, v) -> {
130         if (null == v) v = 0;
131         return v + qty;
132       });
133       if (0 > addedValue) {
134         return Optional.of(-1);
135       }
136       return Optional.of(addedValue);
137     };
138   }

      /**
      * 商品の在庫データを削除するメソッド
      * ☆関数型インターフェースSupplierを使用
      * ☆OptionalのofNullable、isPresent及びofメソッドを使用
      */
139   private Supplier<Optional<Integer>> deleteStockName() {
140     return () -> {
141       int result = -1;
142       if (Optional.ofNullable(stockMap.remove(productName)).isPresent()) {
143         result = 0;
144       }
145       return Optional.of(result);
146     };
147   }

      /**
      * 在庫数を取得するメソッド
      * ☆関数型インターフェースSupplierを使用
      * ☆MapのgetOrDefaultメソッドを使用
      * ☆Optionalのofメソッドを使用
      */
148   private Supplier<Optional<Integer>> getStock() {
149     return () -> {
150       int result = stockMap.getOrDefault(productName, -1);
151       outputNumberStock(result);
152       return Optional.of(result);
153     };
154   }

      /**
      * 在庫リストを生成するメソッド
      * ☆関数型インターフェースSupplierを使用
      * ☆MapのforEachメソッドを使用
      */
155   private Supplier<String> getStockList() {
156     return () -> {
157       StringBuilder sb = new StringBuilder();
158       stockMap.forEach((k, v) -> {
159         sb.append(k);
160         sb.append(":");
161         sb.append(v);
162         sb.append("\n");
163       });
164       return sb.toString().substring(0, sb.toString().length()-1);
165     };
166   }

      /**
      * 在庫処理結果をチェックするメソッド
      * ☆関数型インターフェースFunctionを使用
      * ☆Optionalのmap及びorElseメソッドを使用
      */
167   private Function<Optional<Integer>, String> getErrorKeyFunction() {
168     return errindex -> {
169       Optional<String> opkey = errindex.map(eindex -> {
170         if (0 <= eindex) return "";
171         return errorKeyList.get(typeList.indexOf(type)).split(",")[Math.abs(eindex)-1];
172       });
173       return opkey.orElse("");
174     };
175   }

      /**
      * 在庫数を出力するメソッド
      */
176   private void outputNumberStock(int result) {
177     if (-1 < result) {
178       StringBuilder sb = new StringBuilder();
179       sb.append("☆指定された在庫名の在庫数量:");
180       sb.append(productName);
181       sb.append(" ");
182       sb.append(result);
183       sb.append("個");
184       System.out.println(sb.toString());
185     }
186   }

      /**
      * 在庫リストを出力するメソッド
      */
187   private void outputStockList(String list) {
188     System.out.println("☆在庫リスト");
189     System.out.println(list);
190   }

      /**
      * 付属品リストを出力するメソッド
      * ☆MapのforEach、getOrDefaultメソッドを使用
      * ☆Listのstreamメソッドを使用
      * ☆Streamのcollectメソッドを使用
      */
191   private void outputSubStockList() {
192     System.out.println("☆付属品リスト");
193     stockMap.forEach((k, v) -> {
194       List<String> list = subStockMap.getOrDefault(k, null);
195       if (null != list) {
196         StringBuilder sb = list.stream().collect(StringBuilder::new, (ssb, adname) -> {
197           ssb.append(adname);
198           ssb.append(", ");
199         }, (ba, bb) -> {ba.append(bb);});
200         String str = k + " : " + sb.toString();
201         System.out.println(str.substring(0, str.length()-2));
202       }
203     });
204   }

      /**
      * エラーメッセージを出力するメソッド
      * ☆OptionalのofNullable及びmapメソッドを使用
      */
205   private void outputErrorMessage(String messageKey) {
206     if ("".equals(messageKey)) return;
207     Optional<String> mes = Optional.ofNullable(messageKey).map(m -> {
208       String messtr = errorMessageList.get(errorMessageKeyList.indexOf(m));
209       if (-1 < messtr.indexOf("<%p")) {
210         String pname = productName;
211         if (null != errorProductName) {
212           pname = errorProductName;
213         }
214         messtr = messtr.replace("<%p1%>", pname).replace("<%p2%>", String.valueOf(stockMap.get(pname)));
215       }
216       return messtr;
217     });
218     System.out.println(mes.get());
219     System.out.println("★処理は中止されました。");
220   }

221   public void setStockMap(Map<String, Integer> stockMap) {
222     this.stockMap = stockMap;
223   }

224   public void setSubStockMap(Map<String, List<String>> subStockMap) {
225     this.subStockMap = subStockMap;
226   }
227 }
IStock.java
001 package stocksample;

002 import java.util.List;
003 import java.util.Map;

004 public interface IStock {
005   public int execute();
006   public void setStockMap(Map<String, Integer> stockMap);
007   public void setSubStockMap(Map<String, List<String>> subStockMap);
008 }
IStockExecuter.java
001 package stocksample;

002 import java.util.Optional;

003 @FunctionalInterface
004 public interface IStockExecuter {
005   public abstract Optional<Integer> execute();
006 }

関数型プログラミング実施のまとめ

  • Java8の関数型プログラミング用API使用によりコレクション処理でのループ記述やインデックス記述を無くす事ができることを確認できました。
  • とはいえ、どちらかというと実質は表層的な省力化で、これによりJavaプログラミングが大きく変わるという程のものでもないとも思えます。
  • もともとJava8の場合オブジェクト指向がベースで関数型プログラミング用APIは付加的なものなので当然と言えば当然という事になります。
  • 関数プログラミングを採用する時に気を付けなければいけない点は、APIやメソッドの実装詳細を知っていなければいけないという事です。
    APIやメソッドを連結して使用するのが関数プログラミングの基本なので多連結する場合予期せぬ結果となってしまう事が予想されるためです。
    特にヌルや異常値入力の扱いは正確に把握しておく必要があります。
  • 関数型プログラミング主体にシステム構築を目指すのであればHaskell等の関数型言語での実装を目指すのが適切だと思われます。
  • しかし関数型プログラミング主体が向いているシステムが多いという情報は見かけないので個人的にはこれまで通りJavaを基軸にするつもりです。

関数プログラミングに関係する語句の簡易説明

ネットでよく出てくる関数プログラミングに関係する語句の簡易説明を以下に記述しました。
モナドについてはわかりにくいので追加説明を付加しました。

  1. 関数型インターフェイス:
    定義されている抽象メソッドが1つだけあるインターフェースのことでラムダ式と関連します。

    基本的には@FunctionalInterfaceアノテーションを付けて使用します。
  2. ラムダ式:
    関数型インターフェースを使用して処理内容を実装・記述するために使用します。

    基本構文は(引数) -> {処理}で、引数部分の個数による構文形式は、引数が1個:(str) -> {処理}、引数が2個:(str1, str2) -> {処理}となります。

    処理部分の基本形は、{文1;文2;…return 戻り値;}となり、式なので値を戻す事が可能となっています。

    Java8での新規メソッドなどではこのラムダ式を引数として渡すことが可能になりシンプルなプログラミングができるようになりました。副作用のない処理部分を記述すれば本来の関数型プログラミングがその部分では実現できることになります。

    以前の匿名クラスと類似しています。
  3. 宣言型プログラミング:
    出力を得るための処理手続き(データとアルゴリズム)を記述せずに、目的・性質・制約などを記述して出力を得るプログラミング手法のことです。

    純粋関数型言語等いくつかの言語の総称を指す場合もあります。

    更に簡単に言えば、何が必要かを設定して、それをどのように処理するかは言語・API・関数などに任せるプログラミング手法のことです。
  4. 高階関数:
    他の関数を引数あるいは結果として返すことができる関数のことです。

    Java8ではMap, FlatMap, Filter, Reduce, forEach, anyMatch, allMatchなどがあります。
  5. StreamAPI:
    配列やCollectionなどの集合体を扱い、値の集計やデータを使った処理などが出来るAPIです。

    Java8の目玉新機能となっています。
  6. Optional:
    Optionalはnullを含む値をラップし、nullを安全に処理するための関数です。
  7. メソッド参照:
    関数型インターフェースの変数にメソッドそのものを代入することをメソッド参照と言います。

    ただし、関数型インターフェースにおける抽象メソッドの引数の個数・型と、代入したいメソッドの引数の個数・型が一致している必要があります。

    定義済みのメソッドを引数なしで呼び出せるのがメソッド参照の特徴で、「クラス名::メソッド名」がその基本形となっています。

    定義済みのメソッドをラムダ式のように扱うことも可能となっています。

    (例)

     例1:list.forEach(System.out::print);

     例2:Consumer c = System.out::println;

        c.accept("abc");
  8. *副作用:
    入力として受けつけたデータ以外の物(変数値など)が変化し(状態変化)、以降における結果に影響を与えることを指します。

    代入、インクリメント、デクリメント、入出力などを指します。
  9. 参照透過性(参照透明性):
    同じ入力なら必ず同じ結果を返す性質のこと。
  10. 遅延評価:
    値が必要になるまで計算しないという計算方法で、繰り返し構造を容易に組み込むことが可能となっています。

    Java8ではStreamAPIにおいて遅延評価を局所的にサポートしており、StreamAPIでは生成処理指示・中間処理指示では具体的な計算は行われず、終端
    処理指示を行ったタイミングで行われるようになっています。また、ラムダ式を使用した遅延処理も可能となっています。
  11. *モナド:
    モナドはプログラム的に言えば関数を結合・合成して実行できるようにするための仕組み・構造・概念です。

    一般的に概念を含め細部を理解することは難しいと言われていますが、難しくないという意見もあります。どちらかというと説明書が難しいのではという意見が多いように思われます。


    *世の中での定義や説明が必ずしも明確・適格でない語句

[モナドの追加説明]
モナドについての追加説明をプログラムの視点から記述してみます。
「モナド」はいくつかある種類のモナド(モナドインスタンス)の総称で、Haskell(関数型言語)では型クラスになっています。
標準的なモナドとしては以下のものがあります。
 Maybeモナド、Listモナド、Identityモナド、Eitherモナド、Stateモナド、IOモナド、Writerモナド、Readerモナド、・・
 (HaskellではMaybe型、List型、・・・・とも呼びます。)
モナドとなるためには、以下の3つの条件を満足する必要があります。
この条件を満たせばモナドと呼ぶことができます。
  (1)1つの型引数を受け取ること。
  (2)returnとbind(>>=演算子)で操作できること。
returnはモナドに値を入れるための関数です。
bindはモナドの値を関数に渡すための演算子で、モナド >> 関数のように記述して戻り値はモナドに入れます。
  (3)定められているモナド則(ルール)を満たすこと。
条件を満たせばモナドとなるので以下の手順で自作することも可能です。
  (1)モナドとする型を定義します。
  (2)Monadのインスタンスを定義します。
   (returnやbindの実装が含まれます。)
このモナドの主目的は、
モナド値 >>= モナド型関数1 >>= モナド型関数2 >>= モナド型関数3 ---> モナド値を得る
のように連続して関数を実行できるようにすることです。
またモナドでは結合する時に何らかの処理を入れることも可能となっています。
  モナド値:モナドが持つ値のことです。
  モナド型関数:モナド値を受け取り処理結果をモナド値で戻す関数のことです。
モナドは具体的には以下のようにして使用できます。(単なる一例です。言語はHaskellです。)

  • let addOne x = Just(x + 1) --関数addOneの定義(JustはMaybeモナドの値である事を表現しています。)

  • let addTwo x = Just(x + 2) --関数addTwoの定義

  • return 2 >>= addOne >>= addTwo --Maybeモナドに2を入れて関数addOneとaddTwoを実行

結果:Just 5
これはMaybeモナドを使用して2つの関数を結合している例で、>>=は結合するための演算子です。
最後のreturn 2 >>= addOne >>= addTwoは以下のような形式でも指定することができます。
  do return 2
   addOne
   addTwo
モナドを更に理解するためには以下の手順がよいかと思います。

  • まずネットで簡単検索し全体を把握(深入りはしないで表層だけにする)。
  • Haskell言語の簡易習得(説明がHaskell言語を使用するものが多いため)。
  • Haskellインストール。
  • Haskellでモナドを使ってみる。
  • Haskellでモナドを作ってみる。
  • モナドの説明書を読んでみる。

ちなみに私はHaskellを使用した事はありません。

<Monad定義>
class Monad m where
 return :: a -> m a
 (>>=) :: m a -> (a -> m b) -> m b
 (>>) :: m a -> m b -> m b
 x >> y = x >>= \_ -> y
 fail :: String -> m a
 fail msg = error msg

最後に

以上です。最後までお読み頂き有難うございました。

「実際Javaシステム開発においてどの程度関数型プログラミングを取り込むべきなのか?」を中心テーマとして第2部を投稿しました。ぜひご覧ください。
 第2部へのリンク

「分かりにくいmap,flatMap,collectの追加説明と知っておくと便利なCollector,Collections,Arrays」をテーマとして第3部を投稿しました。
ぜひご覧下さい。
 第3部へのリンク

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34