業務備忘録

備忘録です

【Java】XMLからオブジェクトを生成する

2023-10-01 22:05:19 | 日記

書くことが思いつかず前の記事から2週間以上空いてしまいました。
前々回まで、StreamAPIの練習として適当に作成していた図書館の貸し出しをモデルにしたプロジェクトを書いていましたが、今回はStreamAPIとは別方向の改修を行いたいと思います。

1.前回までのコード

public class BookList {
        Map<String, Book> bookList = new HashMap<>();

    public void setBookList() {
        bookList.put("こころ", new Book("こころ", "夏目漱石", 200));
        bookList.put("斜陽",new Book("斜陽", "太宰治", 180));
        bookList.put("想像の共同体",new Book("想像の共同体", "アンダーソン", 350));
        bookList.put("近代文学論争",new Book("近代文学論争", "臼井吉見", 300));
        bookList.put("近代天皇像の形成",new Book("近代天皇像の形成", "安丸良夫", 400));
        bookList.put("天皇の肖像",new Book("天皇の肖像", "多木浩二", 190));
        bookList.put("ドストエフスキーの詩学",new Book("ドストエフスキーの詩学", "バフチン", 500));
        bookList.put("『キング』の時代",new Book("『キングの時代』", "佐藤卓己", 530));
       
    }

面倒なのでDBは使わない方針でしたが、そのため、書籍一覧を保持するBookListクラスのオブジェクトは以上のように初期化されていました。

上記のコードでbookListが保持しているBookクラスのオブジェクトには、貸出履歴や貸出回数を管理するメンバ変数が存在しますが、当然のことながら、JVM(Java Virtual Machine)が終了した時点でそれらの変数は解放されてしまいます。
そこで今回は、DBを使わずにXMLファイルを読み込むことでBookListの初期化を行いたいと思います。
XMLファイルを読み込む練習がしたいだけなので、DB使えよ…みたいな指摘はなしで。

2.XMLファイルとは

.xmlファイルとは、ファイル名の拡張子(末尾部分)が「.xml」のファイルで、XML(Extensible Markup Language)で記述されたテキストファイルのこと。ソフトウェアの設定ファイルなどによく見られる。

IT用語辞典 e-Words(https://e-words.jp/w/.xml%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.html)


xmlファイルでは、自分で任意のタグを用意することでデータを定義することができます。
例えば今回改修範囲であるBookクラス(書籍名・著者・ページ数などを保持するクラス)をXMLで表現すると以下のようになります。

<books>
    <book id="0001">
        <title>こころ</title>
        <author>夏目漱石</author>
        <year>1914</year>
        <lentCount>0</lentCount>
        <lastLent></lastLent>
        <pages>250</pages>
    </book>
    <book id="0002">
        <title></title>
        <author>夏目漱石</author>
        <year>1910</year>
        <lentCount>0</lentCount>
        <lastLent></lastLent>
        <pages>270</pages>
    </book>

3.JAXBの利用

さて、XML形式で外出しした書籍一覧情報ですが、これを実際のコード内で活用するには、

  1. XML形式の構文解析
  2. 毎にBookクラスのオブジェクトに変換

という手順を踏む必要があります。
こういった場合に使用できるAPIに、JAXB(Java Architecture XML Binding)があります。

JAXB(Java Architecture XML Binding)は、[…]XMLファイルを読み込むというよりも、「XMLファイルとJavaのオブジェクトを結び付ける」動作をします。JAXBを利用することで、XMLとJavaオブジェクトを相互変換できます。

Java本格入門p.262

JAXBはかつてはJavaの標準ライブラリ(Eclipseのパッケージエクスプローラ内のJRE システムライブラリーに格納されているパッケージ)であったが、現在は削除されているため、自力で導入する必要があります。導入については割愛。

【BookList.java】

   Books bookList = null;
 
   private Books makeBookList() {
       
        try(InputStream is = Files.newInputStream(Paths.get("./src/main/resources/xml/Book.xml"))){
            bookList = JAXB.unmarshal(is, Books.class);
           
        }catch(IOException e) {
            System.err.println(e);
        }
        return bookList;
    }

JAXBクラスのunmarshalメソッド(XMLからJavaオブジェクトへの変換を行うメソッド)を使用しています。引数にInputStreamを渡していますが、InputStreamの代わりにReaderクラスオブジェクトが指定されているオーバーロードもあるので、BufferedReaderでも大丈夫です。
第二引数にはBooksクラスのクラスリテラル(そのクラスを表すClassクラスのオブジェクト)が指定されます。
では、肝心のBooksクラスはどうなっているかというと、

【Books.java】

@XmlRootElement(name = "books")

public class Books {
    private List<Book> bookList;
   
    @XmlElement(name = "book")
    public List<Book> getBookList(){
        return bookList;
    }
   
    public void setBookList(List<Book> bookList) {
        this.bookList = bookList;
    }
   
}

@XmlRootElementは、オブジェクトをXML要素として表現するための注釈で、XMLのルートタグ(最上位の要素)と、ツリー構造で表現されるJavaのオブジェクトの、最上位のクラスを結び付けます。ここでは、Bookオブジェクトを格納するArrayListではなく、クラスに注釈を付与している点に注意しておきます。
@XmlElementも同様に、XML要素とオブジェクトを結び付ける注釈です。
このように、注釈型を用いるだけで、XML要素がJavaオブジェクトにマッピング(関連づけ・割り当て)されます。

4.実際に動かす

先掲のBookList.java内のbookListをwatch式で見てみます。
個人的には、@XmlRootElement注釈はBooksクラスに付与したのに、Booksクラスのメンバ変数であるbookListにバインドされているのが親切なような不気味なような…。

ともかく、XMLの各要素がBook型のリストに格納され、各BookオブジェクトはXMLタグで囲われた値を保持していることがわかります。ただし、

    <book id="0001">
        <title>こころ</title>
        <author>夏目漱石</author>
        <year>1914</year>
        <lentCount>0</lentCount>
        <lastLent></lastLent>
        <pages>250</pages>
    </book>

<book>タグの属性であるidは、Bookクラスのメンバ変数であるidにはバインドされていません。
タグ内の属性をバインドするには、@XmlAttribute注釈型を用います。

    @XmlAttribute(name = "id")
    public int getId() {
        return id;
    }

getterに@XmlAttributeを付与していることに注意します。メンバ変数に直接付与してもバインドされません。注意しましょう。

きちんとidもバインドされています。もっとも、XMLで"0001"だったidはint型の1として扱われ、0埋めではなくなっていますが…。

 


JavaのPathsクラスとURIクラス(Windows)

2023-09-14 01:13:55 | 日記

1.そもそもパスとURIってなんだっけ

パス…ファイルシステムのリソースの在処を示す。 標準のDOSパスの場合、パスは3つの部分から構成される。

  1. ボリュームまたはドライブ文字とそれに続くボリューム区切り記号 (:)。
    =>Cドライブ直下の場合はパスの先頭の"C:"
  2. ディレクトリ名。ディレクトリ区切り文字によって、入れ子になっているディレクトリ階層内でサブディレクトリが分割されます。
    =>ディレクトリ区切り文字はWindowsの場合はバックスラッシュ(\)。
  3. 任意のファイル名。
    =>readme.txtとか。

https://learn.microsoft.com/ja-jp/dotnet/standard/io/file-path-formats

URI

  1. スキーム
    =>http:、ftp:、data:、file:など通信手段を示す
  2. authority =>スキームのあと"//"で始まる部分。ユーザ情報、ホスト、ポートから構成される。 ホストは"www.google.com"、ポートはポート番号。
  3. パス
    =>指定したauthority内のリソースの在処を表す。 上で説明したパスに同じ。
  4. クエリ 指定したauthority内のリソースの在処を表す。 上で説明したパスに同じ。
  5. フラグメント ??

 

2.PathsクラスとURIクラスを利用する

JavaのURIクラスには、上で説明したスキームやクエリを取得するメソッドのほか、URIオブジェクトを生成するcreateメソッドが用意されています。

public static URI create(String str)

パラメータには、URIとして分析可能な文字列を渡します。

2.1. エラーになる表現もある

String pattern = "パスは{0}.\nURIは{1}";
 
URI uri2 = URI.create("file:/pleiades/workspace/JavaStudy/text/sample/test2.txt");
Path path2 = Paths.get(uri2);
System.out.println(MessageFormat.format(pattern,path2.toString() ,uri2.toString()));
 
URI uri3 = URI.create("C:/pleiades/workspace/JavaStudy/text/sample/test2.txt");
Path path3 = Paths.get(uri3);
System.out.println(MessageFormat.format(pattern,path3.toString(), uri3.toString()));
 
URI uri4 = URI.create("./text/test/txt");
Path path4 = Paths.get(uri4);
System.out.println(MessageFormat.format(pattern, uri4.toString(), path4.toString()));
 
URI uri5 = URI.create("file:\\pleiades\\workspace\\JavaStudy\\text\\sample\\test2.txt");
Path path5 = Paths.get(uri5);
System.out.println(MessageFormat.format(pattern, path5.toString(), uri5.toString()));
 

URIクラスのcreateメソッド※を利用して、URIクラスのオブジェクトを作成し、さらに、URIクラスのオブジェクトを利用してPathsクラスのgetメソッドからPathを指定します。
最後に、MessageFormatクラスのformatメソッド※でURIクラスオブジェクトとPathクラスオブジェクトの文字列表現を出力。
適当に4つ並べてみなしたが、この中で正しくPathクラスオブジェクトを取得できるのは1つだけです

※MessageFormat.formatメソッド…第一引数のpatternに変数を埋め込む書式パターン、第二引数以降に埋め込む変数を指定して、メッセージの指定部分のみを変更する。

2.2. 解答編

//パスは\pleiades\workspace\JavaStudy\text\sample\test2.txt.
//URIはfile:///pleiades/workspace/JavaStudy/text/sample/test2.txt
URI uri2 = URI.create("file:/pleiades/workspace/JavaStudy/text/sample/test2.txt");
Path path2 = Paths.get(uri2);
 
//FileSystemNotFoundException:Privider"C"not installed
URI uri3 = URI.create("C:/pleiades/workspace/JavaStudy/text/sample/test2.txt");
Path path3 = Paths.get(uri3);
 
//IllegalArgumentException:missing scheme
URI uri4 = URI.create("./text/test/txt");
Path path4 = Paths.get(uri4);
 
//IllegalArgumentException:Illegal character in opaque part at index 5
URI uri5 = URI.create("file:\\pleiades\\workspace\\JavaStudy\\text\\sample\\test2.txt");
Path path5 = Paths.get(uri5);
 

【失敗例1】

//FileSystemNotFoundException:Privider"C"not installed
URI uri3 = URI.create("C:/pleiades/workspace/JavaStudy/text/sample/test2.txt");
Path path3 = Paths.get(uri3);

ドライブの"C"をスキームとして分析してしまうためエラー。

【失敗例2】

//IllegalArgumentException:missing scheme
URI uri4 = URI.create("./text/test/txt");
Path path4 = Paths.get(uri4);

スキーム(https:など)を指定しない相対パスで生成したURIクラスオブジェクトを引数にしてgetを呼び出すとillegalArgumentExceptionがスローされます。
なお、

Path path4 = Paths.get("./text/test/txt");

のように、URIクラスオブジェクトを利用せず、Pathsクラスのgetメソッドに相対パスを文字列で指定した場合は、正しくリソースのパスを指定することができます。
相対パスで指定した場合、ルートはプロジェクトのフォルダになります(上掲の例だとJavaStudy)。

【失敗例3】

URI uri5 = URI.create("file:\\pleiades\\workspace\\JavaStudy\\text\\sample\\test2.txt");
Path path5 = Paths.get(uri5);

windowsのディレクトリ区切り文字であるバックスラッシュ(/)を利用しているパターン。
"Illegal character in opaque part at index 5"
=>"opaque"はURIのデータ格納部分。バックスラッシュで区切りを表現してはいけない。

【成功例】

//パスは\pleiades\workspace\JavaStudy\text\sample\test2.txt.
//URIはfile:/pleiades/workspace/JavaStudy/text/sample/test2.txt
URI uri2 = URI.create("file:/pleiades/workspace/JavaStudy/text/sample/test2.txt");
Path path2 = Paths.get(uri2);

パスはwindowsの区切り文字であるバックスラッシュで表現されていることがわかります。また、URIのスキーム部はパスとしては表現されないこともわかります。
なお、Ubuntuで実行した場合、

//パスは/pleiades/workspace/JavaStudy/text/sample/test2.txt.
//URIはfile:/pleiades/workspace/JavaStudy/text/sample/test2.txt

パスの区切り文字はスラッシュとなります。

 

 


【Java】StreamAPIの練習をする(mapで別のStreamを生成する)

2023-09-06 22:25:30 | 日記

今回は、貸出処理などにかかわらず、ページ数などの書籍の情報を表示するメソッドを作成します。

1.変えたところ

Proc.java

public void showLendInfoAndBookInfo(Map<String, Book> bookMap) {
    Stream<Book> stream = bookMap.values().stream();
           
        //最も貸し出された書籍を表示する。
        System.out.println("最も貸出された書籍のタイトルは『" +
                stream
                .filter(t->t.getLendCount() > 0)
                .sorted((r1,r2)->r2.getLendCount()-r1.getLendCount())
                .map(s->s.getBookName())
                .findFirst()
                .orElse("#Error!!1冊も貸し出されていません!") + "』です。");
       
        //一番ページ数が少ない書籍を表示する。
        System.out.println("図書館で一番ページが少ない本のページ数は" +
                stream.mapToInt(r->r.getPages())
                .sorted()
                .findFirst()
                .getAsInt() + "です。");
    }

 

2つSysoutしているなかでも、上のほうでは、最も貸し出された書籍を表示します。最初にfilter()で貸し出されたことのある書籍に絞り込んだうえで、貸し出し回数でソートします。
map()はStreamを別のStreamで置き換えるメソッドで、これによってStreamがStream(引数のラムダ式内で書籍名=String型を指定)となります。
findFirst()は、Streamの最初のOptionalを返します。Optionalは値がnullである場合の操作を備えたクラスで、orElseは値がnullである場合は、引数に指定した値を返します。
findFirst()で最も貸し出された書籍を取得し、orElse(値が空でない場合は要素を返し、空である場合は引数に指定した要素を返す)で一冊も書籍が貸し出されていない場合の出力を指定しているというわけです。

下の処理ほうの処理では、Bookオブジェクトからページ数で並べ替えてfindFirst()でページ数が最小の書籍のページ数を取得します。

 

『こころ』を2回、『想像の共同体』を1回貸し出しして、実際にshowLendInfoAndBookInfo()を呼び出しましょう。

最も貸出された書籍のタイトルは『こころ』です。

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

あれ?

2.そもそもさぁ

StreamはfindFirst()などの終端操作を行うと閉じられてしまい、使いまわしできないようです。一度閉じたStreamを呼び出すとillegalSateExceptionがスローされる。

Stream<Book> stream2 = bookMap.values().stream();
        //一番ページ数が少ない書籍を表示する。
        System.out.println("図書館で一番ページが少ない本のページ数は" +
                stream2
                .mapToInt(r->r.getPages())
                .min()
                .getAsInt()+ "です。");
    }

そもそも「ページ」はint型で管理しているので、わざわざsorted()とかfindFirst()する必要なし。

mapToInt()でIntStream(int型専用のメソッドを備えたクラス)に変換すれば、min()やmax()で最小値/最大値を取得できます。

最も貸出された書籍のタイトルは『こころ』です。

図書館で一番ページが少ない本のページ数は180です。

しかし最大値や最小値、平均などを算出するためにいちいちStreamを作成しなおすのは非常にめんどくさい。

 

IntSummaryStatistics statistics = stream3.map(r->r.getPages()).collect(Collectors.summarizingInt(s->s));

 

System.out.println("図書館で一番長い本のページ数は" + statistics.getMax() + "ページです");

System.out.println("図書館で一番短い本のページ数は" + statistics.getMin() + "ページです");

System.out.println("図書館の本の平均ページ数は" + statistics.getAverage() + "ページです");

System.out.println("図書館の本の冊数は" + statistics.getCount() + "冊です");

IntSummaryStatisticsにcollect(Streamで操作した結果を集約するメソッド)で集約した処理結果を渡すと、最大値や最小値などの情報を保存できます。

図書館で一番ページが少ない本のページ数は180です。

図書館で一番長い本のページ数は530ページです

図書館で一番短い本のページ数は180ページです

図書館の本の平均ページ数は331.25ページです

図書館の本の冊数は8冊です

 

https://gitlab.com/simulacre1/librarytest/-/tree/day1?ref_type=tags

 

 


【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 不正確な記述ですが、きちんと説明するために調査が必要なので説明は別日を期します。