業務備忘録

備忘録です

【Java】StreamAPIの練習をする(2.flatMapの利用)

2023-08-29 23:40:35 | 日記

今回はstreamAPIで使えるflatMapの練習をします。

練習台として前回の記事で作成しておいた書籍の貸出アプリを使用しますが、
まずは前回から追記した点を記載。

1.変更点

User.java

public String getBookName() {
        return bookName;
    }

    public String getUserName() {
        return userName;
    }

    public Date getDate() {
        return date;
    }

User.javaのフィールドにgetterを追加。

Book.java

    public List<User> getHistory() {
        return history;
    }

貸出履歴保持用のリストにgetterを追加。

Proc.java

    public void lendProc(Map<String, Book> bookMap) {
        System.out.println("借りる本のタイトルを入力してください");
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        //貸出対象の書籍名
        String targetBookName;
       
        try {
            targetBookName = br.readLine();
            System.out.println(targetBookName + "を貸出します...");
           
            //貸出可能図書を管理している連想配列から貸出対象の本を取得
            Book targetBook = bookMap.get(targetBookName);
            targetBook.incrementLendCount();
            targetBook.setLendDate(new Date());
            targetBook.addHistory(new User(targetBookName,"simulacre", targetBook.getLendDate()));
            bookMap.put(targetBookName, targetBook);//Bookオブジェクトに対する処理を行った後に,パラメータであるbookMapに戻す
           
            System.out.println(targetBookName + "は" + targetBook.getLendCount() + "回貸し出されました");
       
           
            System.out.println("続いて貸出を行う場合は1を入力してください");
            System.out.println("貸出回数で並べ替えを行う場合は2を入力してください");
            System.out.println("貸出履歴を一覧表示する場合は3を入力してください");
           
            String mode = br.readLine();
           
            if(mode.equals("1")) {
                //貸出処理を再帰的に呼び出す
                lendProc(bookMap);
            }else if(mode.equals("2")) {
                //貸出順に表示する
                sortByLendCount(bookMap);
                return;
            }else if(mode.equals("3")) {
                showAllHistory(bookMap);
            }else {
                return;
            }
           
        } catch (IOException e) {
            e.printStackTrace();
        }

貸出対象の書籍(targetBook)に対する操作(貸出回数のインクリメント、貸出日の更新、貸出履歴の追加)を行ったあと、貸出可能書籍を保持しているhashMap、つまり引数として渡されてきたbookMapにtargetBookを返しています。

今回は、Bookオブジェクト内で、貸出履歴を保持しているhistoryに対しStreamAPIで操作を行います。
historyは、Userクラスで型付けされているArrayListです。つまり、BookMap→Bookオブジェクト→historyリスト→Userオブジェクト→Dateなど貸出日情報
という階層になっていて、素朴に考えると貸出日情報を取り出すにはまあまあ手間がかかりそうです。

今回追加したshowAllHistoryメソッド内でStreamAPIを用いてとりあえず貸出日順に表示してみます。

 

2.実際にやってみる

showAllHistoryメソッド

private void showAllHistory(Map<String, Book> bookMap) {
        List<Book> allHistoryList = new ArrayList<>();
        allHistoryList.addAll(bookMap.values());
        allHistoryList.stream().flatMap(s->s.getHistory().stream())
            .sorted((r1,r2)-> r2.getDate().compareTo(r1.getDate()))
            .forEach(t->System.out.println(t.getDate()));
    }

とりあえずhashMapのままだとStreamAPIを使用できないので、bookMapから、保持されているBookオブジェクトを全部取り出す必要があります。

Mapインターフェースにはvalues()メソッドが用意されており、values()はhashMapのすべての値をCollection※として返します。
※Collection…ListやSetなどの要素の集合を表すオブジェクトを規定しているインターフェース

addAllメソッドは、引数に指定したCollectionを呼び出し元のCollectionに追加するメソッドなので、

allHistoryList.addAll(bookMap.values());

この時点で、allHistoryListにはbookMap内のBookオブジェクトがすべて追加されています。

例えば『こころ』を2回借りだして、『斜陽』を1回借りだした場合、

AllHisytoryListと各オブジェクト間の関係はこのようになっています。
リスト内の各Bookオブジェクトは貸出履歴としてUserオブジェクトを保持していますが、Userオブジェクトを操作するには各BookオブジェクトのHistoryからUserオブジェクトを逐一取得して…もしくは、別にListを用意して、Historyリスト内のUserオブジェクトを追加していくような処理を経なくてはなりません。

そんなかったるい処理を書かずに、リストの中に存在するリストをひとまとめにして処理したい場合、flatMapメソッドを使用します。

allHistoryList.stream().flatMap(s->s.getHistory().stream())

この記述だけで、Userクラスに型付けされたStream,Streamが作成されており、Userオブジェクトを順次処理できるようになります。あとはsorted()を利用して並び替えを行いforEach()で各要素に終末処理を行うだけ。ともかくネストしたCollectionに対して便利に操作できるメソッドのようです。

↓処理結果

Wed Aug 30 23:20:10 JST 2023

Wed Aug 30 23:20:07 JST 2023

Wed Aug 30 23:20:03 JST 2023

 

 


【Java】Stream APIの練習をする

2023-08-27 20:47:49 | 日記

1. Stream APIって?

コレクション(ArrayやArrayListなど)などに格納された大量のデータを効率的に操作できる手段。 for文などを利用して記述するより、スッキリとコレクションへの処理を記述することが可能。 C#で言うところのLINQ(firstOrDefaultとかwhereとかで操作するやつ)。

2. 実際にやってみる

Stream API については記事を複数回に分けて書こうと思います。 まずは準備として以下のパッケージを自作。図書館での貸し出し処理をモデルにします。
コード全体はgitLabに公開。
https://gitlab.com/simulacre1/JavaStudy/-/tree/main/LibraryTest

Book.java

書籍を表現したクラス。

gooBlogの字数の関係で委細省略。
bookName(書籍名)
author(著者名)
lendCount(貸出回数)
lendDate(最終貸出日)
の各フィールドに対するgetter/setterに加え、
貸出回数をインクリメントする素朴なメソッドがを実装しています。
あと貸出履歴を保持するリストとしてhistoryと、historyに履歴を追加するaddHistory()も。

User.java

利用者を表現したクラス。

委細省略。
bookName(書籍名)
userName(利用者名)
date(日付)
をとりあえずフィールドにして、コンストラクタだけ書いてあります。

BookList.java

gooBlogの字数制限の関係でコードは画像です。

DBは使わないのでとりあえず貸出可能書籍管理用のクラス。

Proc.java

処理を行うクラス。
貸出を行う場合はlendProc()を再帰的に呼び出す。
貸出処理の内実は、

①引数の書籍一覧を格納したbookMapから貸出対象の書籍のBookオブジェクトを取得
②貸出回数のインクリメント
③lendDate(最終貸出日)の更新
④addHistory()によって貸出履歴追加

貸出を止めて貸出回数順に書籍を一覧表示するメソッドがsortByLendCount()。後述します。

LendBook.java

mainメソッド。処理の振り分けのみ。

 

3. 実際にやってみる

コンソールから、『こころ』の貸出処理を2回、『斜陽』の貸出処理を1回行ったのち、"2"を入力して貸出回数によるソート処理を呼び出します。

    private void sortByLendCount(Map<String, Book> bookMap) {
        bookMap.entrySet().stream()
            .sorted((s1, s2)->s2.getValue().getLendCount() - s1.getValue().getLendCount())
            .forEach(r ->System.out.println(r.getValue().getBookName() + "は" + r.getValue().getLendCount() + "回貸し出されました"));
    }

StreamAPIを利用するためには、stream()によって大量の処理(=ストリーム処理)の始点を作らなくてはなりませんが、
HashMapのようなmapインターフェースを実装したオブジェクトにはstream()が準備されていません。
なので、HashMapからは、entrySet()※1を使用して、HashedMapをSetに変換したうえでstream()を呼び出し、ストリーム処理を開始します。

肝心の並べ替えは、sorted()によって行います。
sorted()の引数には、2つの要素から計算した結果※2を渡します。

.sorted((s1, s2)->s2.getValue().getLendCount() - s1.getValue().getLendCount())

上記のようにラムダ式の2つ目の引数で取得した要素から、1つ目の引数で取得した要素を引くような記述の場合、

===========================

こころは2回貸し出されました

斜陽は1回貸し出されました

想像の共同体は0回貸し出されました

近代文学論争は0回貸し出されました

ドストエフスキーの詩学は0回貸し出されました

近代天皇像の形成は0回貸し出されました

天皇の肖像は0回貸し出されました

ドストエフスキーの詩学は0回貸し出されました

=============================

と、降順で並べ替えられます。
一方

.sorted((s1, s2)->s1.getValue().getLendCount() - s2.getValue().getLendCount())

と、減算の順番を逆にした場合は、

===============================

想像の共同体は0回貸し出されました

近代文学論争は0回貸し出されました

ドストエフスキーの詩学は0回貸し出されました

近代天皇像の形成は0回貸し出されました

天皇の肖像は0回貸し出されました

ドストエフスキーの詩学は0回貸し出されました

斜陽は1回貸し出されました

こころは2回貸し出されました

==================================

と昇順で並べ替えられます。

この場合、貸し出されていない書籍が上位に表示されて邪魔ですね。そこで、貸出回数が0回の書籍を篩にかけます。

bookMap.entrySet().stream()
            .filter(i->i.getValue().getLendCount() > 0)
            .sorted((s1, s2)->s1.getValue().getLendCount() - s2.getValue().getLendCount())
            .forEach(r ->System.out.println(r.getValue().getBookName() + "は" + r.getValue().getLendCount() + "回貸し出されました"));

==================

斜陽は1回貸し出されました

こころは2回貸し出されました

==================

filter()によって、filter()の引数の条件に合った要素のみに対象を絞り込むことができます。

次はflatMapメソッドの使用などについて書きます。

※1 正確には、entrySet()は、mapのキーと値のセット(Entry)を格納したSet = Set<Map.Entry<K,V>>
を返すので、EntryのgetValueメソッド(mapの値を返す)によって値となるBookオブジェクトを取得しています。
stream()はCollectionインターフェースで定義されているので、SetインターフェースはCollectionを親インターフェースとするが、Mapは親インターフェースとしていない点に差があります。

※2 不正確な記述ですが、きちんと説明するために調査が必要なので説明は別日を期します。

 


【Java】Integerってなんなの

2023-08-11 18:42:50 | 日記

1.Integerって何?

標記の通り。

Java習いたての頃からInteger.parseInt()で文字列を数値に変換…といった操作は お手の物であるわけですが、
一方でinteger型の変数に平気で数値リテラルが代入されているのも見かけたりするわけで、じゃあIntegerってint型とは何が違うわけ?という疑問が浮かぶのも当然です。

プリミティブ型はオブジェクトではなく、単なる値であり、それ自身はメソッドを持ちません。
しかし、プログラムの中では、プリミティブ型の値に対する操作(文字列との相互変換など)が必要になる場面がたくさんあります。
そこでJavaは、プリミティブ型を内包し、そのプリミティブ型の値を操作する機能を備えた「ラッパークラス」を提供しています。

Java 本格入門, 2023, 技術評論社

要はコード内で値であるかオブジェクトであるかの違いなんですね〜

2.何ができるの?

メソッド名 機能 コメント
valueOf(int i) 指定されたint値を表すIntegerインスタンスを返す Integerオブジェクトをnewするよりこれを使うほうがいいらしい
compare(int x, int y) 2つのint値を数値的に比較する Mathクラスかと思ってた
comareTo(Integer anotherInteger) 2つのIntegerオブジェクトを数値的に比較する  
intValue() Integerの値をintとして返す  
parseInt(String s)    
toString() Integerの値を表すStringオブジェクトを返します  
フィールド 内容 コメント
MAX_VALUE int型の最大値231-1を表す 桁溢れのチェックに使えそう。
MIN_VALUE int型の最小値-231を表す  

 

3.実際に使ってみる

/new 演算子でIntegerオブジェクトを生成するのはdeprecated
Integer integerTest1 = new Integer(1);
Integer integerTest2 = Integer.valueOf(1);

拡張機能を入れたvsCode上だとnew Integer(1)の部分は線で見せ消ちにされます。deprecatedは非推奨という意味。
valueOfメソッドを使用する場合は、引数が-128〜127の範囲であれば、valueOfメソッドがあらかじめIntegerオブジェクトを予め保存しているため、新たにIntegerオブジェクトを生成せずに済むらしい。
一方new 演算子でコンストラクタを呼び出すと必ずIntegerオブジェクトが生成されるため、メモリを圧迫してしまうとのこと。

ただしこの仕様のせいで挙動に注意が必要らしい。

3.1valueOf()した場合とコンストラクタを呼び出した場合

boolean integerCompared1 = Integer.valueOf(1) == Integer.valueOf(1);
System.out.println("valueOf()の取得結果同士の比較=>" + Boolean.valueOf(integerCompared1));

boolean integerCompared2 = Integer.valueOf(1) == new Integer(1);
System.out.println("valueOf()とnewした場合の比較=>" + Boolean.valueOf(integerCompared2));

3.2結果

valueOf()の取得結果同士の比較=>true
valueOf()とnewした場合の比較=>false

valueOfはキャッシュしてある生成済みのオブジェクトを返すので、valueOfの引数に-128〜127を指定した結果同士を比較した1行目は同じオブジェクトのアドレスを指すことになる。したがって比較結果はtrueとなる。
一方、newした場合は異なるオブジェクトのアドレスを比較するのでfalseとなる。

3.3気をつけましょう

Integerはint型を代入することができる。これは内部的にはInteger.valueOf()してintからIntegerに暗黙的に変換している。
なので、「Integerってint型を代入できるんだ(*^○^*)!ついでに比較して処理をすすめよう!」と考えると痛い目にあうことになる。

Integer integerTest5 = 100;
Integer integerTest6 = 100;
System.out.println(Boolean.toString(integerTest5 == integerTest6));
//true
 
Integer integerTest7 = 999;
Integer integerTest8 = 999;
System.out.println(Boolean.toString(integerTest7 == integerTest8));
//false

こうなる理由は3.2参照

 

3.4比較したいときはどうするの

Integer integerTest7 = 999;
Integer integerTest8 = 999;
System.out.println(integerTest7.compareTo(integerTest8));
//0

compareTo()メソッドを使うことで数値的に比較できる。
2つのオブジェクトを数値的に比較した場合の差を返すので、同じ値の場合は0となる。

とにかくInteger同士を比較演算で比較するのは結構危険そう。

4.Integerの初期値

クラスのフィールドとして、プリミティブ型のintを宣言した場合、初期値は0です。
これに対して、ラッパー型のIntegerを宣言した場合、初期値はnullとなります。
[...]このため、0とデータ型がない状態(空)を区別したい場合は、 ラッパー型を用いる必要があることがわかります。
[...]プリミティブ型を使用していると、値が取得できない場合に0(初期値)となるため、値が指定されていなくて初期値の0となったのか、値として0が指定されていたのかが区別できません。

Java 本格入門, 2023, 技術評論社

なるほど。

 

参考:

https://meetup-jp.toast.com/1808

『Java 本格入門』(2023,技術評論社)


【Java】hashMapを触る

2023-08-09 23:12:23 | 日記

Javaに触る機会ができたので渋々勉強します。今回はHashMapクラスで、要は合言葉(key)で値を取り出す連想配列です。

 

1.とりあえず触ってみる

vsCodeの拡張のせいで連想配列のメソッドの引数にラベル(key,value)がついてしまっていますが本来は無い想定で。

Map<Integer, String> map = new HashMap<>()
と初期化したので、今回はキーはInteger型で値はString型です。

注)コールバック関数…引数に指定された別の関数。画像では使い捨てのラムダ式を使用。

 

removeメソッドは存在しないキーを指定しても例外などは発生しない(nullを返す)。

2.キーと値の型を指定しない場合はObject型になるらしい

2.1 なんでも入るぞ

標題の通り、型を指定しない場合はkeyもvalueもObject型となります。Object型はすべてのクラスの基底クラスなので、
つまり、この場合は連想配列にはなんでも追加できることになります。
画像でもvalueにBooleanやString,int,自作のクラスも入れることができます。

もちろんkeyにもあらゆる型を代入可能。

2.2 キャストしないとね

当然ですがObject型で連想配列を使用している場合は型独自のメソッドなどは参照できないのでキャストする必要があります。

 

結局、Object型のHashMapを利用すれば複数の型・戻り値でreturnできるというわけですが、
なんでもObject型で解決するなら静的型付け言語の意味なくないですかね?