Java Stream で Max , Min を求めた時の優先順位

Stream から 最大値、最小値を求めた時の結果は、null のStream のケースが
有りうるので、Optional です。

Stream<T>.max(Comparator<? super T> comparator)
Stream<T>.min(Comparator<? super T> comparator)

の返却値は、

Optional<T>

最大値、最小値が複数ある時、結果として返ってくる要素は、
以下の様に単純(一般に良くある例)な場合、
Stream走査で最初に見つかる最大値の要素あるいは、最小値の要素が返ってくる。

例) Item というクラスが抱える getValueメソッドが数値を返すとする。
List<Item> list が、Stream 対象

Optional<Item> maxItem =
list.stream()
.max((a, b)->Double.valueOf(a.getValue()).compareTo(Double.valueOf(b.getValue()))));

あるいは、

Optional<Item> maxItem = list.stream().max(Comparator.comparing(Item::getValue));

最後に見つかる最大値の要素を取得するには、、
List を stream する限定の方法になるが、List の indexOf を thenComparing で追加条件にすると

Item lastMax = list.stream()
.max(Comparator.comparing(Item::getValue).thenComparing(list::indexOf))
.orElse(null);

List stream とは限らない Set などではこれは使えないので、そういう場合は、
終端処理の reduce を使うことになる。

.stream().reduce((a, b)->a.getValue() > b.getValue() ? a : b).orElse(null);

Javaの起動コマンドスクリプト

 -jar オプションによる JAR MANIFEST でメインクラスを指定した jar ファイル指定で
起動するケースがほとんどのせいで、
Java を学び始めた遠い昔で実行した起動コマンドスクリプトの作法を
忘れるまではいかなくても、これで良いのだっけと不安になってしまう。
 java [ options ] class [ argument ... ]

package org.abc; AppMain というmainメソッドクラスがあり、Javaコンパイル結果、
  /home/guest/classes/org/abc/AppMain.class
と配置されていたら、

java -classpath /home/guest/classes org/abc/AppMain

もしくは、

java -cp /home/guest/classes org/abc/AppMain

パッケージ区切りは実際コンパイルで配置されたOSのファイルPATH区切り文字である
パッケージ区切り文字の "." ではない。

クラスが展開された形ではなくjar配布であれば、起動クラスを探索するclasspath の指定は、
jar ファイルのPATH である。

java -classpath /home/guest/lib/hoge.jar org/abc/AppMain

依存するJARは全て ";" で連結指定しなければなりません。
依存するJARファイルのバージョン管理を考慮すると
やはり -jar JARファイルパス に全てを
梱包してビルド⇒ 実行する全ての依存するクラスを -jar で指定する
する方が、メンテナンスが楽です。

改行を含まないデータTSVファイルの読込み

改行を含まないデータCSVファイルの読込み - Oboe吹きプログラマの黙示録
を書いたのなら、
TSV の場合も同様に書ける。
まず、Csvtolist はカンマ区切りなので、これをタブ区切りで処理する Tsvtolist を用意する。
Csvtolist から変更箇所は1箇所だけである

Tsvtolist

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;

public interface Tsvtolist extends Serializable{

    public static Function<String, List<String>> of(){
        return Tsvtolist::create;
    }
    
    public static List<String> create(String str){
        char DELIMITER = '\t';
        char CARRIAGE_RETURN = '\r';
        char NEWLINE = '\n';
        char DOUBLE_QUOTE = '"';
        if (str==null || str.length()==0){
            return Collections.emptyList();
        }
        List<String> tokens = new ArrayList<String>();
        StringBuilder tokenBuf = new StringBuilder();
        boolean insideDoubleQuote = false;
        boolean isDoubleQuoteEscapeActive = false;
        StringBuilder wspBuf = new StringBuilder();
        for(int ii=0; ii < str.length(); ii++){
            final char ch = str.charAt(ii);
            if (ch==CARRIAGE_RETURN || ch==NEWLINE){
                if (insideDoubleQuote){
                    tokenBuf.append(ch);
                }else{
                    throw new RuntimeException("unquoted " 
                                               + (ch=='\n' ? "newline" : "carriage return") 
                                               + " found at position #" + (ii+1));
                }
            }else if(ch==DOUBLE_QUOTE){
                if (insideDoubleQuote){
                    if (isDoubleQuoteEscapeActive){
                        tokenBuf.append(ch);
                        isDoubleQuoteEscapeActive = false;
                    }else if(((ii+1) < str.length()) && str.charAt(ii+1)==DOUBLE_QUOTE){
                        isDoubleQuoteEscapeActive = true;
                    }else{
                        insideDoubleQuote = false;
                    }
                }else{
                    insideDoubleQuote = true;
                    if (wspBuf.length() != 0){
                        if (tokenBuf.length() != 0){
                            tokenBuf.append(wspBuf);
                        }
                        wspBuf.delete(0, wspBuf.length());
                    }
                }
            }else{
                if (insideDoubleQuote){
                    tokenBuf.append(ch);
                }else{
                    if (ch==DELIMITER){
                        tokens.add(tokenBuf.toString());
                        tokenBuf.delete(0, tokenBuf.length());
                        wspBuf.delete(0, wspBuf.length());
                    }else if(Character.isWhitespace(ch)){
                         wspBuf.append(ch);
                    }else{
                        if (wspBuf.length() != 0){
                            if (tokenBuf.length() != 0){
                                tokenBuf.append(wspBuf);
                            }
                            wspBuf.delete(0, wspBuf.length());
                        }
                        tokenBuf.append(ch);
                    }
                }
            }
        }
        if (insideDoubleQuote){
            throw new RuntimeException("terminating double quote not found");
        }
        tokens.add(tokenBuf.toString());
        return tokens;
    }
}

データに改行を含まない前提なので、ファイル読込みの処理を組み込んだ interface class は、
TsvReadProcess

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;

public interface TsvReadProcess{
    public void inspectLine(int lineno, List<String> list) throws FingersplitException;
    public Charset getCharset();
    
    public static TsvReadProcess of(Charset charset, BiFunction<Integer, List<String>, String> inspector) {
        return new TsvReadProcess() {
            @Override
            public void inspectLine(int lineno, List<String> list) throws FingersplitException {
                String res = inspector.apply(lineno, list);
                if (res != null && !res.isBlank()){
                    throw new FingersplitException(lineno, res);
                }
            }
            @Override
            public Charset getCharset(){
                return charset;
            }
        };
    }

    public default void execute(ThrowableSupplier<InputStream> getStreamer, 
                                BiConsumer<Integer, Map<String, String>> bifunc) throws Exception {
        try(InputStream inst = getStreamer.get();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inst, getCharset()))){
            String line = BOMfunction.chop(reader.readLine());
            AtomicInteger ix = new AtomicInteger(0);
            List<String> hlist = Tsvtolist.create(line);
            inspectLine(1, hlist);
            int rownum = hlist.size();
            Map<Integer, String> hmap = hlist.stream()
            .collect(()->new HashMap<Integer, String>(),(r, t)->{
                r.put(ix.incrementAndGet(), t);
            },(r, t)->{});
            ix.set(1);
            int lc = 1;
            while((line = reader.readLine()) != null){
                Map<String, String> map = new HashMap<>();
                List<String> list = Tsvtolist.create(line);
                if (rownum != list.size()) {
                    throw new FingersplitException(lc, "header rows No match!");
                }
                inspectLine(ix.incrementAndGet(), list);
                hmap.entrySet().stream().forEach(e->{
                    map.put(e.getValue(), list.get(e.getKey()-1));
                });
                bifunc.accept(lc, map);
                lc++;
            }
        }catch(Exception e){
            throw e;
        }
    }
}

使用している BOMfunction や、FingersplitException は、
改行を含まないデータCSVファイルの読込み - Oboe吹きプログラマの黙示録
を参照

使用サンプル

try{
    TsvReadProcess process = TsvReadProcess.of(Charset.forName("MS932"), (i,list)->{
        // TODO  異常があれば、エラーメッセージを返す、正常なら、null またはブランク、空白を返す。
        return  null;
    });
    process.execute(()->getMyInputStream(), (i, map)->{
        System.out.println("i="+i+"  "+ map);
        
    });
}catch(FingersplitException e){
    System.err.println("catch! FingersplitException");
    System.err.println("catch! "+e.getMessage());
    e.printStackTrace();
}catch(Exception e){
    e.printStackTrace();
}

UTF-8 の TSV ファイルだったら、
Charset.forName("MS932") を指定しないで、
StandardCharsets.UTF_8 を与える

改行を含まないデータCSVファイルの読込み

実践で良く処理するCSVは、改行をデータに含まないケースがほとんどである。
改行をデータに含むケースを考慮したものとして、
GitHub - yipuran/yipuran-csv: Java CSV read and write
を作ったが、改行をデータに含まないケースはこれを使わずにもっと少ないコードで
機能を果たせる。
先日投稿した、CSV形式の1行分の文字列からList<String>への変換をinterface method にする。 - Oboe吹きプログラマの黙示録
この Csvtolist を使えば以下のように普遍的に書くこともできる。

/**
 * CSVファイル読込をBiConsumer で処理する
 * @param bifunc 行番号、CSVヘッダで定義するKeyのMap<String, String> の BiConsumer
 */
public static void executeCsvline(BiConsumer<Integer, Map<String, String>> bifunc) throws Exception{
    // getInputStream() は、CSVファイル入力 stream を取得する手段、 
    // AWS S3 などは、代わりに S3ObjectInputStream を返すようなメソッドを用意するなど
    // 的確な入力streamを使用する。
    try(InputStream inst = getMyInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(inst))){
        String line = BOMfunction.chop(reader.readLine());
        AtomicInteger ix = new AtomicInteger(0);
        List<String> hlist = Csvtolist.create(line);
        int rownum = hlist.size();
        Map<Integer, String> hmap =    hlist.stream()
        .collect(()->new HashMap<Integer, String>(),(r, t)->{
            r.put(ix.incrementAndGet(), t);
        },(r, t)->{});
        // TODO ヘッダ行のチェック仕様のチェックはここで⇒ hmap の妥当性、異常あれば RuntimeException で throw
        int lc = 1;
        while((line = reader.readLine()) != null){
            Map<String, String> map = new HashMap<>();
            List<String> list = Csvtolist.create(line);
            if (rownum != list.size()) {
                // ヘッダ行の列数とデータ行の列数が合わない時
                throw new RuntimeException("header rows No match!");
            }
            hmap.entrySet().stream().forEach(e->{
                map.put(e.getValue(), list.get(e.getKey()-1));
            });
            // TODO データ行のチェック仕様のチェックはここで⇒ 異常あれば RuntimeException で throw
            bifunc.accept(lc, map);
            lc++;
        }
    }catch(IOException e){
        throw e;
    }
}

BOM付きなら除去する BOMfunction.chopBOMfunction は、以下である。
yipuran-csv の中にあるが、同じものをここに書いておく)

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
 * BOM操作ユーティリティクラス.
 */
public final class BOMfunction{
    /** private constructor. */
    private BOMfunction(){}

    /**
     * BOMを出力する.
     * @param out OutputStream
     * @throws IOException
     */
    public static void push(OutputStream out) throws IOException{
        out.write(new byte[]{ (byte)0xef,(byte)0xbb, (byte)0xbf });
    }

    /**
     * BOMが先頭に付いた文字列のBOMを除去する.
     * @param str 除去前の文字列
     * @return 除去後の文字列
     */
    public static String chop(String str){
        byte[] b = str.getBytes();
        if (b.length < 3) return str;
        if (b[0] == -17 && b[1] == -69 && b[2] == -65) {
            byte[] n = new byte[b.length-3];
            for(int i=0,k=3; i < n.length;i++, k++){
                n[i] = b[k];
            }
            return new String(n);
        }else if(b[0] == -2 && b[1] == -1){
            byte[] n = new byte[b.length-2];
            for(int i=0,k=2; i < n.length;i++, k++){
                n[i] = b[k];
            }
            return new String(n);
        }else if(b[0] == -1 && b[1] == -2){
            byte[] n = new byte[b.length-2];
            for(int i=0,k=2; i < n.length;i++, k++){
                n[i] = b[k];
            }
            return new String(n);
        }
        return str;
    }
    /**
     * 文字列がBOM付き文字であるか返す
     * @param str 文字列
     * @return true=BOM付き
     */
    public static boolean match(String str){
        if (str==null) return false;
        byte[] b = str.getBytes();
        if (b.length < 3) return false;
        if (b[0] == -17 && b[1] == -69 && b[2] == -65) return true;
        // UTF_16BE BOM
        if (b[0] == -2 && b[1] == -1) return true;
        // UTF_16LE BOM
        if (b[0] == -1 && b[1] == -2) return true;
        return false;
    }
    /**
     * BOM付き 状況から Charset を返す。
     * @param str 文字列
     * @return java.nio.charset.Charset
     */
    public static Charset getCharset(String str){
        if (str==null) return StandardCharsets.UTF_8;
        byte[] b = str.getBytes();
        if (b.length < 3) return StandardCharsets.UTF_8;
        if (b[0] == -17 && b[1] == -69 && b[2] == -65) return StandardCharsets.UTF_8;
        // UTF_16BE BOM
        if (b[0] == -2 && b[1] == -1) return StandardCharsets.UTF_16BE;
        // UTF_16LE BOM
        if (b[0] == -1 && b[1] == -2) return StandardCharsets.UTF_16LE;
        return StandardCharsets.UTF_8;
    }
}

使用サンプルは、、

executeCsvline(n, m)->{
    // n = データ行番号
    // m = ヘッダ列が示すkeyによる分割結果のMap<String, String>
});

これは、ファイル入力ストリーム等、実装の状況に合わせるサンプルでしかない。
汎用的なものが欲しい。
ファイル入力を組み込んだ interface class として以下を用意する
CsvReadProcess

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;

public interface CsvReadProcess{
    public void inspectLine(int lineno, List<String> list) throws FingersplitException;
    public Charset getCharset();
    
    public static CsvReadProcess of(Charset charset, 
                                   BiFunction<Integer, List<String>, String> inspector) {
        return new CsvReadProcess() {
            @Override
            public void inspectLine(int lineno, List<String> list) throws FingersplitException {
                String res = inspector.apply(lineno, list);
                if (res != null && !res.isBlank()){
                    throw new FingersplitException(lineno, res);
                }
            }
            @Override
            public Charset getCharset(){
                return charset;
            }
        };
    }

    public default void execute(ThrowableSupplier<InputStream> getStreamer, 
                                BiConsumer<Integer, Map<String, String>> bifunc) throws Exception {
        try(InputStream inst = getStreamer.get();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inst, getCharset()))){
            String line = BOMfunction.chop(reader.readLine());
            AtomicInteger ix = new AtomicInteger(0);
            List<String> hlist = Csvtolist.create(line);
            inspectLine(1, hlist);
            int rownum = hlist.size();
            Map<Integer, String> hmap = hlist.stream()
            .collect(()->new HashMap<Integer, String>(),(r, t)->{
                r.put(ix.incrementAndGet(), t);
            },(r, t)->{});
            ix.set(1);
            int lc = 1;
            while((line = reader.readLine()) != null){
                Map<String, String> map = new HashMap<>();
                List<String> list = Csvtolist.create(line);
                if (rownum != list.size()) {
                    throw new FingersplitException(lc, "header rows No match!");
                }
                inspectLine(ix.incrementAndGet(), list);
                hmap.entrySet().stream().forEach(e->{
                    map.put(e.getValue(), list.get(e.getKey()-1));
                });
                bifunc.accept(lc, map);
                lc++;
            }
        }catch(Exception e){
            throw e;
        }
    }
}

この中で使用しているものは以下、

ThrowableSupplier は、入力ストリームを生成して提供することを
約束する Throwableな、Function の Supplier
yipuran-core に、これはあるが、同じものをここに書いておく)
java.util.function.Supplier で例外捕捉をすることで呼び出しの外に例外捕捉を書けるようになる。

import java.io.Serializable;
import java.util.function.Function;
import java.util.function.Supplier;
/**
 * Exception 捕捉 Supplier.
 */
@FunctionalInterface
public interface ThrowableSupplier<R> extends Serializable{
    R get() throws Exception;

    /**
     * ThrowableSupplier 生成.
     * @param supplier 例外スローする Supplier&lt;R&gt;処理
     * @param onCatch Exception捕捉処理 , 値を返さなければならない。
     * @return Supplier&lt;R&gt;
     */
    public static <R> Supplier<R> to(ThrowableSupplier<? extends R> supplier, 
                                    Function<Exception, R> onCatch){
        return ()->{
            try{
                return supplier.get();
            }catch(Exception e){
                return onCatch.apply(e);
            }
        };
    }
    /**
     * ThrowableSupplier 生成(外に例外スロー).
     * @param supplier 例外スローする Supplier処理
     * @return Supplier&lt;R&gt;
     */
    public static <R> Supplier<R> to(ThrowableSupplier<? extends R> supplier){
        return ()->{
            try{
                return supplier.get();
            }catch(Throwable ex){
                throw new RuntimeException(ex.getMessage(), ex);
            }
        };
    }
}

例外を定義:FingersplitException

public class FingersplitException extends RuntimeException{
    private final int lineno;
    private final String message;
    
    public FingersplitException(int lineno, String message) {
        this.lineno = lineno;
        this.message = message;
    }
    public int getLine() {
        return lineno;
    }
    public String getMessage() {
        return message;
    }
    @Override
    public String toString() {
        return "line " + lineno + ": " + message;
    }
}

BOMfunction.chop は、先に書いたとおり。

CsvReadProcess 使用例は、
以下、getCustomInputStream() というCSVファイル
の入力ストリームを取得するメソッドが存在するものとして、

try{
    CsvReadProcess process = CsvReadProcess.of(Charset.forName("MS932"), (i, list)->{
          // TODO  異常があれば、エラーメッセージを返す、正常なら、null またはブランク、空白を返す。
          return  null;
    });
    process.execute(()->getCustomInputStream(), (i, m)->{
        // i = データ行番号
        // m = ヘッダ列が示すkeyによる分割結果のMap<String, String>
    });
}catch(FingersplitException e){
    e.printStackTrace();
}catch(Exception e){
    e.printStackTrace();
}

CSVファイルがUTF-8 なら、Charset.forName("MS932") の代わりに、
StandardCharsets.UTF_8 を使う。

Java起動時のPATH

Javaを呼び出すプロセスの現在のディレクトリPATH
これを求める方法

Paths.get("") を使う方法

import java.nio.file.Paths;
String curpath = Paths.get("").toAbsolutePath().toString();

システムプロパティ環境変数user.dir)から求める方法

String userDir = System.getProperty("user.dir");

ついでに、、、
環境変数java.class.path)で求めるPATHは、

String classpath= System.getProperty("java.class.path");

は、Thread.currentThread().getContextClassLoader().getResource
リソースのPATHを取得するときのディレクトリに該当し
実行時のクラスをロードする起点のPATHである。
java -jar で起動したときこの System.getProperty("java.class.path") のPATH は、
起動対象のJARファイルのPATH になる。

FastAPI でダウンロードするサイトを作る

(1)動的にダウンロードするコンテンツを作成してダウンロードさせる場合

例)CSVを生成→ダウンロード

from fastapi import FastAPI, Response

app = FastAPI()

@app.get("/download")
def csvget():
    content = '"あ","い","う"\n"1","2","3"'
    filename = "data.csv"
    return Response(content=content.encode("utf-8-sig"),
                    headers={"Content-Disposition": f'attachement; filename={filename}'},
                    media_type="text/csv")

content.encode("utf-8-sig") とすることで、BOM付きのUTF-8 にしている。
SJIS エンコードにしたければ、content.encode("MS932") とする。

ダウンロードファイル名に2バイト文字を含む場合は、
urllib.parse quoteでURLエンコードが必要になる。

from urllib.parse import quote
return Response(content=content.encode("utf-8-sig"),
                headers={"Content-Disposition": f"attachement; filename*=UTF-8''{quote(filename)}"},
                media_type="text/csv")

(2)既に存在するファイルをダウンロードさせる場合
FileResponse を使用し、path とダウンロードファイル名を指定する。

from fastapi.responses import FileResponse
return FileResponse(path=file_path,
                    filename=f'{filename}'
                   )

1つのプロジェクトで複数の起動クラス毎の JAR をビルドする

昔、考えが及ばずあまりイケてない方法を書いた。
1プロジェクトで起動可能な2つのJARを作る - Oboe吹きプログラマの黙示録
これでは、2通りのJARしか対応できないので、ダメだ。
static mainメソッドのクラスをプロジェクトで複数持って、
各クラス毎の java -jar {JARファイルパス}で実行するJAR
を作成する方法が望ましい。

Maven コマンド package 実行時に、ーDオプションで対象の Main クラス名と、
対応するJARファイル名称を指定する単純な方法で良いはずだ。

pom.xml

<build>
   <plugins>
      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
         <configuration>
            <source>17</source>
            <target>17</target>
         </configuration>
      </plugin>
      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-shade-plugin</artifactId>
         <version>2.4.1</version>
         <executions>
            <execution>
               <phase>package</phase>
               <goals>
                  <goal>shade</goal>
               </goals>
               <configuration>
                  <finalName>${jarname}</finalName>
                  <transformers>
                     <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>${mainclass}</mainClass>
                     </transformer>
                  </transformers>
                  <filters>
                     <filter>
                        <artifact>*:*</artifact>
                        <excludes>
                           <exclude>META-INF/*.SF</exclude>
                           <exclude>META-INF/*.DSA</exclude>
                           <exclude>META-INF/*.RSA</exclude>
                        </excludes>
                     </filter>
                  </filters>
               </configuration>
            </execution>
         </executions>
      </plugin>
   </plugins>
</build>
起動クラス名 JARファイル名 
org.simple.SimpleMain1 simple1.jar
org.simple.SimpleMain2 simple2.jar
org.simple.SimpleMain3 simple3.jar

それぞれ、以下のように Maven build を実行すれば良い。
  mvn package -Dmainclass=org.simple.SimpleMain1 -Djarname=simple1
  mvn package -Dmainclass=org.simple.SimpleMain2 -Djarname=simple2
  mvn package -Dmainclass=org.simple.SimpleMain3 -Djarname=simple3

Eclipsse での mvn 実行に設定では、ーDオプションは、以下のように設定すればよい。