デトフィア

プログラミング

vscodeのdevcontainerを利用する

開発環境を構築する際に利用する場合があります。私が以前いた場所では使っていました。OracleのDBサーバーを開発環境として構築する際にdevcontainerを使っていました
devcontainerを用意しておけばdockerのコマンドを使わなくてもコンテナの利用ができます

導入

拡張機能を入れておきます

  • Dev Containers

利用する

試しにRustの開発環境を作成します 何もファイルなどがない空のフォルダをvscodeで開きます ターミナルにてcargo -Vと入力します
もしもRustの環境がマシンにない人はコマンドがないというエラーになると思います

次に画面左下の><マークをクリックして、Reopen in Containerをクリックします

そのまま検索欄にrustを入力して、Rust devcontainerをクリックします

そのまま流れでデフォルトを選択して、最後は0個を選択のままOKボタンをクリックします

するとイメージのビルドなどが行われますのでそのまま待ちます

エクスプローラーに.devcontainerフォルダが作成されます

ターミナルにてcargo -Vを入力するとコマンドが利用できることがわかります 私の場合は自身の端末より新しいバージョンであることが確認できています

つまりコンテナの環境でコマンドが実行できているのです

そのままcargo initしてcargo runするとRustの実行までできます

.devcontainerフォルダにはdevcontainer.jsonができています

{
    "name": "Rust",
    "image": "mcr.microsoft.com/devcontainers/rust:1-buster",

    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    // "forwardPorts": [],

    // Use 'postCreateCommand' to run commands after the container is created.
    // "postCreateCommand": "rustc --version",

    // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
    "remoteUser": "vscode"
}

このようにjsonファイルを元にコンテナを作成してくれるのです

自分で.devcontainerを作成する

また空フォルダを開いたら、Dockerfileを作成します

簡単にnodeを利用するだけです

FROM node:18
D:.
    Dockerfile
    
サブフォルダーは存在しません 

ただDockerfileを作っただけですね

ここまで出来たら、画面左下の><ボタンを押して今度はOpen folder in Contaierをクリックします

そして現在のフォルダを選択したら、fron Dockerfileを選択します

すると自動的に.devcontainerというフォルダとdevcontainer.jsonが作成され、コンテナが起動します
ターミナルからnode -vを実行します

Dockerfileに基づいたコンテナが作成されています

Dockerコンテナに作成したJenkinsからDockerコンテナのTomcatにwarファイルをデプロイする

以前書いた記事

DockerとJenkinsでJavaプロジェクトのコンパイル等してwarファイルのデプロイをする - Qiita

この記事ではTomcat環境をDockerコンテナで作成して、そこに同じくDockerコンテナで作成したJenkins環境からデプロイを行うということをやりました。 流れとしてはこうです

  1. Githubにソースをpushする
  2. Jenkinsのジョブを実行する(手動)
  3. テストやビルドを通してwarファイルがtomcatコンテナにデプロイされる

今回はMavenでやっていたのをgradleにして、Jenkinsの環境も新しく2.376で実施します。

また以前の記事では色々と省略していたので詳しく記事にしていこうと思います。

記事の流れ

  • コンテナの準備
    • tomcatとjenkinsのコンテナを作成します
  • tomcatの設定
    • ユーザーとロールの設定
    • Webアプリケーションマネージャの設定
  • Jenkinsの設定
    • ジョブの作成
    • 認証情報の作成
    • パイプラインスクリプトの記載

予め対象となるソースはGitHubにアップロードしておきます。

コンテナの準備

以下のフォルダ構成でファイルを準備します。

D:.
│  docker-compose.yaml
│
├─jenkins
│  └─data
└─tomcat
        Dockerfile

Dockerfile

FROM tomcat:9.0
RUN apt-get update && apt-get install -y wget && apt-get install -y vim

Docker-compose.yaml

version: '3'
services: 
  tomcat:
    build:
      context: .
      dockerfile: ./tomcat/Dockerfile
    container_name: tomcat
    privileged: true
    ports:
      - "8012:8080"
    volumes:
      - "./tomcat/data:/var"
    tty: true
  jenkins:
    container_name: jenkins
    image: jenkins/jenkins:alpine
    ports:
      - "8888:8080"
    volumes: 
      - "./jenkins/data:/var/jenkins_home"
    tty: true

コンテナを立ち上げます

docker-compose up -d

D:\docker\tomcatjenkins>docker-compose up -d
Docker Compose is now in the Docker CLI, try `docker compose up`

Creating network "tomcatjenkins_default" with the default driver
Building tomcat
[+] Building 9.6s (6/6) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                                    0.1s
 => => transferring dockerfile: 125B                                                                                                                                                    0.0s
 => [internal] load .dockerignore                                                                                                                                                       0.0s
 => => transferring context: 2B                                                                                                                                                         0.0s
 => [internal] load metadata for docker.io/library/tomcat:9.0                                                                                                                           0.0s
 => [1/2] FROM docker.io/library/tomcat:9.0                                                                                                                                             0.1s
 => [2/2] RUN apt-get update && apt-get install -y wget && apt-get install -y vim                                                                                                       9.1s
 => exporting to image                                                                                                                                                                  0.4s
 => => exporting layers                                                                                                                                                                 0.4s
 => => writing image sha256:56eab31fdeaae33f66468f60f0970fec539bb430f7ed6e2d478e3252bb8e9331                                                                                            0.0s
 => => naming to docker.io/library/tomcatjenkins_tomcat                                                                                                                                 0.0s
WARNING: Image for service tomcat was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating tomcat  ... done
Creating jenkins ... done

D:\docker\tomcatjenkins>

Tomcatの設定

ユーザーとロールを追加しておきます tomcat-users.xmlというファイルを修正します。Tomcatコンテナに入って直接修正します。 vimは事前にインストール済ですので利用可能です。

D:\docker\tomcatjenkins>docker exec -it tomcat bash
root@03442bc6f07e:/usr/local/tomcat#
root@03442bc6f07e:/usr/local/tomcat# find / -name tomcat-users.xml
/usr/local/tomcat/conf/tomcat-users.xml
root@03442bc6f07e:/usr/local/tomcat# vim /usr/local/tomcat/conf/tomcat-users.xml
root@03442bc6f07e:/usr/local/tomcat#

ファイルの最後の方に以下を追加しておきます

          <role rolename="manager-gui"/>
          <role rolename="admin-gui"/>
          <role rolename="manager-script"/>
          <user username="misaka" password="password" roles="manager-gui,admin-gui,manager-script"/>
</tomcat-users>

現状のwebappsフォルダには何もありません。

root@03442bc6f07e:/usr/local/tomcat# ls
BUILDING.txt  CONTRIBUTING.md  LICENSE  NOTICE  README.md  RELEASE-NOTES  RUNNING.txt  bin  conf  lib  logs  native-jni-lib  temp  webapps  webapps.dist  work
root@03442bc6f07e:/usr/local/tomcat# cd webapps
root@03442bc6f07e:/usr/local/tomcat/webapps# ls
root@03442bc6f07e:/usr/local/tomcat/webapps#

webapps.distフォルダには色々入っているので必要であればコピーすることで利用可能です。

root@03442bc6f07e:/usr/local/tomcat# cd webapps.dist
root@03442bc6f07e:/usr/local/tomcat/webapps.dist# ls
ROOT  docs  examples  host-manager  manager

webapps.distのmanagerをwebappsにコピーします

root@03442bc6f07e:/usr/local/tomcat# cp -r webapps.dist/manager webapps
root@03442bc6f07e:/usr/local/tomcat#
root@03442bc6f07e:/usr/local/tomcat# ls webapps
manager

そして許可するIPアドレスを修正します。(全てのIPアドレスを許可するように修正)

root@03442bc6f07e:/usr/local/tomcat# find ./ -name context.xml
./conf/context.xml
./webapps/manager/META-INF/context.xml
./webapps.dist/manager/META-INF/context.xml
./webapps.dist/examples/META-INF/context.xml
./webapps.dist/host-manager/META-INF/context.xml
root@03442bc6f07e:/usr/local/tomcat#
root@03442bc6f07e:/usr/local/tomcat# vim ./webapps/manager/META-INF/context.xml

以下のようにallowとなっている部分のValveをコメントアウトします

          <!--
  <Valve className="org.apache.catalina.valves.RemoteAddrValve"
                  allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />
          -->
  <Manager sessionAttributeValueClassNameFilter="java\.lang\.(?:Boolean|Integer|Long|Number|String)|org\.apache\.catalina\.filters\.CsrfPreventionFilter\$LruCache(?:\$1)?|java\.util\.(?:Linked)?HashMap"/>
</Context>

この2つをやらないと/managerにアクセスした時に403エラーが返ってきます

  • tomcat-users.xmlにアカウントを追加
  • context.xmlの許可アドレスを修正

Jenkinsの設定

次にJenkinsの設定を行います。 コンテナは立ち上がっている状態なので、以下にアクセスします

http://localhost:8888/

初期設定が必要になります

パスワードを/var/jenkins_home/secrets/initialAdminPasswordから取得して入力する必要があります。

コンテナに入ってcatコマンドを実行してもいいですが、コンテナに入らずに実行します。 execコマンドにコンテナ名とコマンドを渡してあげます

D:\docker\tomcatjenkins>docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
df8b97707cea464face45e640f0abfe7

ここで取得したキーを入力します

オススメのプラグインをインストールするをクリック(左)して、初期設定を行います。

ここで古いJenkinsを利用しているとプラグインのインストールに失敗するようです。 私は2.303でエラーになりました。 と思ってlatestを利用しても同じようなエラー。迷っていると以下の記事に遭遇しました

Jenkinsのdockerイメージは、Docker公式とJenkins公式の2つあるのか… - 新しいことにはウェルカム なるほど。。

任意のユーザーを作成してJenkinsの開始を行います。

Deploy to container プラグインのインストール

Deploy to containerというプラグインをインストールします

Jenkinsの管理 > プラグインマネージャー > 利用可能

検索ワードに「Deploy to container」と入力

installにチェックを入れてinstall without restartを押します

パイプラインジョブの作成

パイプラインジョブを任意の名前で作成したら、GitHub projectにチェックを入れてURLを指定します。 一旦これだけの設定で保存しておきます

次のジョブの画面からPipeline Syntaxをクリックします。

ContainersにTomcat9.x Remoteを選択して、Credentialsの追加を押下します

認証情報の追加でDomainを「グローバルドメイン」を選択して、種類に「ユーザー名とパスワード」を選択します。

ここではtomcatで作成したユーザーとパスワードを指定します。IDは任意のものを入力して追加ボタンを押下します

その後、ContainersのCredentialsに追加した認証情報を指定します

このTomcatのURLはコンテナのIPアドレスと、そのコンテナで動いているTomcatのポート番号を指定します

今回の場合ホスト側でアクセスする時のアドレスではないことに気を付けてください

TomcatのURLを入力したらGenerate Pipeline Scriptボタンを押します。
このスクリプトはデプロイステージで利用しますのでメモしておきます。

deploy adapters: [tomcat9(credentialsId: 'tomcat_misaka', path: '', url: 'http://172.31.0.2:8080')], contextPath: null, war: '**/*.war'

ジョブの画面から設定を押してジョブの修正を行います。

設定画面の下にパイプラインという項目を修正します。

定義に「Pipeline script」を選択します

以下のコードを入力します

pipeline { 
    agent any 
    options {
        skipStagesAfterUnstable()
    }
    stages {
        stage('Build') { 
            steps {
                echo 'Start gradle Build..' 
                checkout([$class: 'GitSCM', branches: [[name: '*/main']],
                userRemoteConfigs: [[url: 'https://github.com/jirentaicho/jenkins-test']]])
                sh 'chmod +x gradlew'
                sh './gradlew build'
                echo 'Deploy..'
                deploy adapters: [tomcat9(credentialsId: 'tomcat_misaka', path: '', url: 'http://172.31.0.2:8080')], contextPath: null, war: '**/*.war'
            }
        }
    }
}

deploy adapters: の部分はgenerateで出力したコードの部分です。 どの認証情報で、どのパスに、どのwarファイルをデプロイするかを指定しています。

gradle buildを行うと一通り実行してくれるのでbuildだけを行っています

実行して正常に終了してくれるのを待ちます

成功すると緑色で、失敗すると赤色で表示されます。 もし失敗した場合はログを確認します。

ビルド履歴の#番号の個所をクリックします

Console Outputを押すと実行ログがを見ることができます

これは接続拒否されていました。 →原因はポート番号に誤りがありました。

他にもURLが間違っているとTimeoutになっていたりします。

成功してTomcatのWebアプリケーションマネージャを利用するとデプロイができていることがわかります

またアプリケーションも確認します

テストの失敗

失敗するテストケースをgithubにpushしてjenkinsを動かしてみます。 gradleのtestで失敗するはずです。

    @Test
    public void test_fail(){
        assertTrue(false);
    }

予想通り失敗しています

詳細のログを確認してみます

テストで失敗してることがわかります。

おまけ

docker-composeでコンテナの停止から削除

D:\docker\tomcatjenkins>docker-compose stop jenkins
Stopping jenkins ... done

D:\docker\tomcatjenkins>docker-compose rm jenkins
Going to remove jenkins
Are you sure? [yN] y
Removing jenkins ... done

Javaのイマジナリージェネリクス

ModelMapperにある、とある処理

private <D> D mapInternal(Object source, D destination, Type destinationType, String typeMapName) {
if (destination != null)
  destinationType = Types.<D>deProxy(destination.getClass());
return engine.<Object, D>map(source, Types.<Object>deProxy(source.getClass()), destination,
    TypeToken.<D>of(destinationType), typeMapName);
}

ライブラリなので知る必要は無い箇所だが、とても難しそうなジェネリクスを使った処理になっている

modelmapper/ModelMapper.java at 1e2fdc058ff930788a351f1fc2789fb33cc9face · modelmapper/modelmapper · GitHub

呼び出し元

public <D> D map(Object source, Class<D> destinationType) {
    Assert.notNull(source, "source");
    Assert.notNull(destinationType, "destinationType");
    return mapInternal(source, null, destinationType, null);
}

利用例

Game game = new Game("スプラ3");
ModelMapper modelmapper = new ModelMapper();
GameDto gameDto = modelmapper.map(game,GameDto.class);

この場合はDというのは何なのか?を特定するのが理解の第一歩です

例えば、少しマジカルな印象だがClassクラスの定義はClass<T>なので何らかの型を指定できる

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement,
                              TypeDescriptor.OfField<Class<?>>,
                              Constable {

なのでClass<D>のDを返すというのは、D型の事を指している

ものすごく単純なClass<D>のD型のオブジェクトを返す処理を書きました

private <D> D get(Class<D> clazz) throws Exception {
    // コンストラクタの取得
    Constructor<D> constructor = clazz.getConstructor();
    D obj = constructor.newInstance();
    return obj;
}

このDというのはClass<D>のDなので、Dの実際のインスタンスを返すことになる。

GameDto gameDto = null;
try {
    gameDto = get(GameDto.class);
    gameDto.name = "リフレクションで取得したインスタンス";
} catch (Exception exception) {
    exception.printStackTrace();
}
assertEquals("リフレクションで取得したインスタンス",gameDto.name);

Classクラスを返すのでなく、その型のオブジェクトを返すというのが重要なポイントです

例えば次はわかりやすいです

private <T> T get(List<T> list){
        return list.get(0);
}

このメソッドが返すのはListでなくて、Listに紐づくT型のインスタンスです

List<GameDto> list = List.of(new GameDto());
GameDto dto = this.get(list);
dto.name = "スプラ";
assertEquals("スプラ",dto.name);

本題に戻りまして...

ModelMapperの以下の処理は、Class<D>、つまりD型のインスタンスを返すことになります

public <D> D map(Object source, Class<D> destinationType) {
    Assert.notNull(source, "source");
    Assert.notNull(destinationType, "destinationType");
    return mapInternal(source, null, destinationType, null);
}

その中で一番最初に掲載したコードに行き着くわけですが、引数のdestinationはnullが渡っています。 (今回の場合)

private <D> D mapInternal(Object source, D destination, Type destinationType, String typeMapName) {
if (destination != null)
  destinationType = Types.<D>deProxy(destination.getClass());
return engine.<Object, D>map(source, Types.<Object>deProxy(source.getClass()), destination,
    TypeToken.<D>of(destinationType), typeMapName);
}

なので注目すべきはreturn節です。
"" ここでTypeToken.<D>of(destinationType)というコードでmapメソッドの引数に渡しています。destinationTypeはクラスオブジェクトです。
このofは引数のType情報に基づいてTypeTokeという型のインスタンスを返します。TypeTokeは、rawType変数としてClassオブジェクトを管理しています。→ここにdestinationTypeが入ります。 ""

""で囲った内容は少し複雑です。重要な考えはmapInternalメソッドで受け取るD destinationがnullなのに、engine.<Object, D>mapとして返却できるということです。

この動きを簡単に確認してみます。

public class MyTypeToken<T> {

    private Class<T> clazz;

    public static <T> MyTypeToken<T> of(Type type) throws Exception {
        return new MyTypeToken<T>(type);
    }

    private MyTypeToken(Type t) throws Exception{
        // キャストしてあげる
        Class<T> clazz = (Class<T>) t;
        this.clazz = clazz;
    }

    public Class<T> getClazz(){
        return this.clazz;
    }
}

このクラスは受け取ったTをClass<T>として保持します そのためにTypeをキャストしています。 →未検査警告が出ると思います。

今作ったMyTypeTokenを利用してジェネリクスで指定されている型を受け取らずに結果を返せるメソッドを定義します

private <D> D getD(D clazz, Type type){
    // clazzがnullでも、戻り値を特定できる
    D result = null;
    try {
        MyTypeToken token = MyTypeToken.<D>of(type);
        Class<D> d = token.getClazz();
        Constructor con = d.getConstructor();
        result = (D) con.newInstance();

    } catch (Exception e){
    }
    return result;
}

このメソッドは<D>としてMyTypeTokenのオブジェクトを生成します。 そしてDのインスタンスはMyTypeTokenに格納されているClass<T> clazzから生成します

このメソッドをDの指定をせずに呼び出します

GameDto g = this.getD(null,GameDto.class);
g.name = "wow";
System.out.println(g.name);
assertEquals("wow",g.name);

このテストは成功します。つまりGameDtoのインスタンスがgetDメソッドを介して取得できています。 これは奇妙でイマジナリーなジェネリクスの動きを検証しました。

ここまでくると以下の処理も追いつけると思います

return engine.<Object, D>map(source, Types.<Object>deProxy(source.getClass()), destination,
    TypeToken.<D>of(destinationType), typeMapName);

コードリーディング FileUtils.readFileToString

コードを読んで勉強する目的

今回のターゲットはFileUtils.readFileToStringです

FileUtils.readFileToStringメソッドの引数を見ると

public static String readFileToString(final File file, final Charset charsetName) throws IOException {
    try (InputStream inputStream = openInputStream(file)) {
        return IOUtils.toString(inputStream, Charsets.toCharset(charsetName));
    }
}

Fileオブジェクトを受け取る必要がありますので、以下のように利用します

try {
    String result = FileUtils.readFileToString(Paths.get("D:\\sumomo\\sumomon.txt").toFile(), Charset.forName("UTF-8"));
    System.out.println(result);
} catch (IOException e) {
    e.printStackTrace();
}

または以下のようなFileクラスを使う方法です

try {
    String result = FileUtils.readFileToString(new File("D:\\sumomo\\sumomon.txt"), Charset.forName("UTF-8"));
    System.out.println(result);
} catch (IOException e) {
    e.printStackTrace();
}

おそらくこのFileクラスのオブジェクトを受け取るのが想定された方法なのでしょうが、java.io.Fileは使われることが少なくなりました。 しかし古くからあるためjava.nioとの相互変換も可能です。

このコードはテキストファイルの中身をStirngで受け取り、コンソールに出力しています。

ソースコード

実装は以下のようになっている

public static String readFileToString(final File file, final Charset charsetName) throws IOException {
    try (InputStream inputStream = openInputStream(file)) {
        return IOUtils.toString(inputStream, Charsets.toCharset(charsetName));
    }
}
  • try-with-resourcesを使って安全にcloseされます
  • 更に別のIOUtilsというクラスを利用しています

渡したFileオブジェクトはopenInputStreamメソッドに渡っています。

public static FileInputStream openInputStream(final File file) throws IOException {
    Objects.requireNonNull(file, "file");
    return new FileInputStream(file);
}
  • Fileのnullチェックを行う
  • FileInputStreamオブジェクトを返す

ちなみに

  InputStreamで文字列を出力する時は、readメソッドの引数に渡したバイト配列をSystem.out.writeなどで出力します。

  文字列ストリームを扱う場合はInputStreamをInputStreamReaderのコンストラクタに渡します

IOUtils.toStringメソッド

public static String toString(final InputStream input, final Charset charset) throws IOException {
    try (final StringBuilderWriter sw = new StringBuilderWriter()) {
        copy(input, sw, charset);
        return sw.toString();
    }
}

StringBuilderWriterというWriterのサブクラスを利用しています。 これはorg.apache.commons.io.outputパッケージにあります

先ほども書いたようにInputStreamReaderで文字IOストリームを扱えます。このクラスはReaderクラスのサブクラスで、Writerというのも同じ立ち位置です。 なのでWriterのサブクラスということは文字IOストリームを扱うためのサブクラスだと認識できます。

copyメソッド

public static void copy(final InputStream input, final Writer writer, final Charset inputCharset)
    throws IOException {
        final InputStreamReader reader = new InputStreamReader(input, Charsets.toCharset(inputCharset));
        copy(reader, writer);
    }

文字IOストリームを扱うためのInputStreamReaderを生成しています。 ここまでほぼ以下のコードと変わりません

FileInputStream fileInputStream = new FileInputStream("file.txt");
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF8");

このメソッドは戻り値の無いvoid型であることに注意します。 IOUtils.toStringメソッドを見て推測すると、Writerオブジェクトに対してファイルの内容を付与していると思われます。

そして引数の違う次のcopyメソッドです

public static int copy(final Reader reader, final Writer writer) throws IOException {
    final long count = copyLarge(reader, writer);
    if (count > Integer.MAX_VALUE) {
        return EOF;
    }
    return (int) count;
}

このメソッドのjavadocを見ると

Copies chars from a Reader to a Writer. This method buffers the input internally, so there is no need to use a BufferedReader. Large streams (over 2GB) will return a chars copied value of -1 after the copy has completed since the correct number of chars cannot be returned as an int. For large streams use the copyLarge(Reader, Writer) method.

とあります。 やはりReader→Writerへと文字列のコピーを行うようです。 大きな文字を扱う場合はcopyLargeメソッドを使ってくださいと注意書きがありますが、内部的にはcopyLargeメソッドを使っているようです??

public static long copyLarge(final Reader reader, final Writer writer, final char[] buffer) throws IOException {
    long count = 0;
    int n;
    while (EOF != (n = reader.read(buffer))) {
        writer.write(buffer, 0, n);
        count += n;
    }
    return count;
}

ここでreader.readメソッドを利用します。 これは配列に文字を読み込みます。そしてストリームの終端に達すると-1を返します。 読み込んだ配列をWriter.writeに渡して書き込んでいます

このwriteメソッドはStringBuilderWriterで実装が以下のようになっています

@Override
public void write(final char[] value, final int offset, final int length) {
    if (value != null) {
        builder.append(value, offset, length);
    }
}

StringBuilderに対して文字列を加えていっているというシンプルな構造です。 そして読み込んだテキストを最終的に文字列として返しているのがtoStringメソッドです

@Override
public String toString() {
    return builder.toString();
}

StringBuilderのtoStringを呼び出しているだけです。

ソースを読むと、上手に面倒な処理が隠蔽されていることがわかります。 もちろんこのクラスが利用しているのはjava.ioパッケージなので、nioを使ってもっとシンプルに扱うこともできます。

JavaのCollectionUtils.getCardinalityMapを少しだけ速くする

CollectionUtils.getCardinalityMapこのメソッドは面白いです。

イテレート可能なオブジェクトの要素を全てkeyにしたMapを作ります。 このMapのvalueは数値の1です。keyが重複した場合はvalueをインクリメントさせます

以下のテストで動きを確認します

@Test
public void test_get_countmap(){
    List<String> strs = List.of("abc","def","ghi","abc");
    Map<Object, Integer> count = CollectionUtils.getCardinalityMap(strs);
    assertEquals(count.get("abc"),2);
    assertEquals(count.get("ghi"),1);
    assertEquals(count.size(),3);
}

ちなみに実装は以下のコードです

public static <O> Map<O, Integer> getCardinalityMap(final Iterable<? extends O> coll) {
    final Map<O, Integer> count = new HashMap<>();
    for (final O obj : coll) {
        final Integer c = count.get(obj);
        if (c == null) {
            count.put(obj, Integer.valueOf(1));
        } else {
            count.put(obj, Integer.valueOf(c.intValue() + 1));
        }
    }
    return count;
}

果たしてこれを改造するとしたらどうでしょうか そもそもこのコードはそこまで改造する必要はありません あえて言うならば、最初のcount.get(obj)以降動きが、if文を読むまでは理解できないことくらいでしょうか。 この場合はメソッド抽出でもできればキレイになりますが、Utilクラスにそんなことまで求めるのはナンセンスかもしれません。

無理矢理コードを書きました

public static <T> Map<T,Integer> getCardinalityMap(final Iterable<? extends T> coll){
    final Map<T, Integer> count = new HashMap<>();
    coll.forEach(item -> {
        final Integer c = count.get(item);
        if (c == null) {
            count.put(item, Integer.valueOf(1));
        } else {
            count.put(item, Integer.valueOf(c.intValue() + 1));
        }
    });
    return count;
}

foreachを使っているのに元々のメソッドとほとんど何も変わっているように見えません。 ここでメソッド抽出をして少しでもわかるように修正します

public static <T> Map<T,Integer> getCardinalityMap(final Iterable<? extends T> coll){
    final Map<T, Integer> count = new HashMap<>();
    coll.forEach(item -> {
        count.put(item, getCardinalityValue(item,count));
    });
    return count;
}

private static <T> int getCardinalityValue(T item, Map<T,Integer> count){
    final Integer c = count.get(item);
    if (c == null) {
        return 1;
    } else {
        return Integer.valueOf(c.intValue() + 1);
    }
}

countにputしているのはCardinalityな値なのねというのは抽象的に理解できるようになりました

速度が少しだけ早くなります。 また大量のオブジェクトを突っ込む処理を行って単純な速度を比較します

@Test
public void test_count(){
    class Item{
        public final int id;
        public final String name;
        public Item(int id, String name){
            this.id = id;
            this.name = name;
        }
    }
    // 大量のオブジェクトを仕込む
    List<Item> lista = new ArrayList<>();
    for(int i = 0; i < 300000; i++){
        Item item = new Item(i,"商品" + String.valueOf(i));
        lista.add(item);
    }

    long startTime = System.currentTimeMillis();
    Map<Object, Integer> counta = CollectionUtils.getCardinalityMap(lista);
    long endTime = System.currentTimeMillis();
    System.out.println("処理時間:" + (endTime - startTime) + " ms");

    startTime = System.currentTimeMillis();
    Map<Object, Integer> countb = MyCollectionUtils.getCardinalityMap(lista);
    endTime = System.currentTimeMillis();
    System.out.println("処理時間:" + (endTime - startTime) + " ms");

}

◆1回目 処理時間:100 ms 処理時間:60 ms

◆2回目 処理時間:110 ms 処理時間:80 ms

◆3回目 処理時間:110 ms 処理時間:72 ms

◆4回目 処理時間:105 ms 処理時間:75 ms

◆5回目 処理時間:116 ms 処理時間:83 ms

◆6回目 処理時間:100 ms 処理時間:80 ms

◆7回目 処理時間:105 ms 処理時間:71 ms

何度か実施しましたが、どれも既存の処理よりも速度が遅くなるということはありませんでした。

JavaのCollectionUtils.containsAllをstreamと遅延処理で見直す

CollectionUtils.containsAllは一つでも要素が存在しない場合はfalseを返します

@Test
public void test_all_contains(){
    List<String> lista = List.of("abc","def","hij");
    List<String> listb = List.of("abc","def","ghi");
    boolean result = CollectionUtils.containsAll(lista,listb);
    assertFalse(result);
    List<String> listc = List.of("abc","def","ghi");
    result = CollectionUtils.containsAll(listb,listc);
    assertTrue(result);
}

CollectionUtils.containsAllの実装は以下のようになっている

public static boolean containsAll(final Collection<?> coll1, final Collection<?> coll2) {
    if (coll2.isEmpty()) {
        return true;
    }
    final Iterator<?> it = coll1.iterator();
    final Set<Object> elementsAlreadySeen = new HashSet<>();
    for (final Object nextElement : coll2) {
        if (elementsAlreadySeen.contains(nextElement)) {
            continue;
        }

        boolean foundCurrentElement = false;
        while (it.hasNext()) {
            final Object p = it.next();
            elementsAlreadySeen.add(p);
            if (nextElement == null ? p == null : nextElement.equals(p)) {
                foundCurrentElement = true;
                break;
            }
        }

        if (!foundCurrentElement) {
            return false;
        }
    }
    return true;
}

一度見つかった要素に関してはelementsAlreadySeenに入るので要素の数までは見ていません。 そのため以下のようなコードはtrueです

@Test
public void test_all_contains_(){
    List<String> lista = new ArrayList<>();
    lista.add(null);
    lista.add(null);
    lista.add(null);
    lista.add("abc");
    lista.add("abc");
    lista.add("abc");

    List<String> listb = new ArrayList<>();
    listb.add(null);
    listb.add("abc");

    boolean result = CollectionUtils.containsAll(lista,listb);
    assertTrue(result);
}

しかしこのコードはifもforもwhileも使っているので頭が痛くなりますね

以下のように修正してみましょう。

public static boolean containsAll(final Collection<?> cola ,final Collection<?> colb){
    long result = cola.stream().filter( a -> !colb.contains(a)).count();
    if(result >= 1){
        return false;
    }
    return true;
}

これでテストしても機能として通っています

しかし処理時間に関しては疑問が残りますよね? 以下のテストで簡易的に実行時間差を見てみます

@Test
public void test_perf(){

    class Item{
        public final int id;
        public final String name;
        public Item(int id, String name){
            this.id = id;
            this.name = name;
        }
    }

    List<Item> lista = new ArrayList<>();
    List<Item> listb = new ArrayList<>();
    // 超絶大量のインスタンス
    for(int i = 0; i < 30000; i++){
        Item item = new Item(i,"商品" + String.valueOf(i));
        lista.add(item);
        listb.add(item);
        // これは別のインスタンスなので調査不可能
        // lista.add(new Item(i,"商品" + String.valueOf(i)));
        // listb.add(new Item(i,"商品" + String.valueOf(i)));
    }

    long startTime = System.currentTimeMillis();
    boolean result = CollectionUtils.containsAll(lista,listb);
    assertTrue(result);
    long endTime = System.currentTimeMillis();
    System.out.println("処理時間:" + (endTime - startTime) + " ms");


    startTime = System.currentTimeMillis();
    boolean myresult = MyMapUtils.containsAll(lista,listb);
    endTime = System.currentTimeMillis();
    System.out.println("処理時間:" + (endTime - startTime) + " ms");
    assertTrue(myresult);
}

計測した時間は大差がついてます

処理時間:25 ms 処理時間:908 ms

これはfilterの中で他のコレクションにアクセスしているため遅くなっています(遅くなっていると思います) Setに詰め替えることでこれは解決できます

public static boolean containsAll(final Collection<?> cola ,final Collection<?> colb){
    Set<?> set = new HashSet<>(colb);
    long result = cola.stream().filter( a -> !set.contains(a)).count();
    if(result >= 1){
        return false;
    }
    return true;
}

処理時間:26 ms 処理時間:5 ms

圧倒的なパフォーマンスの差を出しました。

しかしまだです。まだ、パフォーマンスで負ける可能性があります これは全て走査するので無駄な処理が走ってしまいます 元々は1件でもあればその時点でfalseが返却されるのと、elementsAlreadySeenのようなtemp変数が用意されているため速いです

以下のコードでテストしてみます

@Test
public void test_perf(){
    class Item{
        public final int id;
        public final String name;
        public Item(int id, String name){
            this.id = id;
            this.name = name;
        }
    }

    List<Item> lista = new ArrayList<>();
    List<Item> listb = new ArrayList<>();
    // 件数を増やす30000→300000
    for(int i = 0; i < 300000; i++){
        Item item = new Item(i,"商品" + String.valueOf(i));
        lista.add(item);
        // listbにだけ違うデータを仕込む
        if(i == 3){
            listb.add(new Item(999999,"ダミー"));
            continue;
        }
        listb.add(item);
    }

    long startTime = System.currentTimeMillis();
    boolean result = CollectionUtils.containsAll(lista,listb);
    assertFalse(result);
    long endTime = System.currentTimeMillis();
    System.out.println("処理時間:" + (endTime - startTime) + " ms");

    startTime = System.currentTimeMillis();
    boolean myresult = MyMapUtils.containsAll(lista,listb);
    endTime = System.currentTimeMillis();
    System.out.println("処理時間:" + (endTime - startTime) + " ms");
    assertFalse(myresult);
}

処理時間:104 ms 処理時間:76 ms

まだ速度で負けてないのですが、その差が縮まっています。

というわけで更に改善します。 これはstreamの遅延処理を使っています

public static boolean containsAll(final Collection<?> cola ,final Collection<?> colb){
    Set<?> set = new HashSet<>(colb);
    return cola.stream().filter( a -> !set.contains(a)).findFirst().isEmpty();
}

処理時間:98 ms 処理時間:34 ms

圧倒的な速さです。

これはなぜでしょうか?

実はfilterメソッドは一度に全ての要素を検索するわけではありません。 まずfilterで要素が見つかった場合は、次のチェーンへと処理を渡します。 今回の場合はfindFirstメソッドです。これは最初の1件を返すメソッドです。

このfindFirstは終端処理であり、ここで終了条件を満たすとチェーンはそこで終了します。 そのため、もしもfilterで要素が見つかると、findFistで終了条件が満たされ処理終了します。 filterは最後まで処理を行うことをしません。これが遅延処理です。

最初はcount関数を使っていたため、終端処理で処理が終了しません(終了するのに必要な要素が取得できていない)ので、終端処理が次の要素を求めます。 そこで処理がfilterに戻ってきてノロノロと処理を続けていたのです。 streamを使うと読みやすさだけでなくパフォーマンスも改善されるということが分かったかと思います。

※計測は必ずしも正確ではありません。あくまで目安程度です。

おまけ

List.ofでリストを作る時にnullを渡すとぬるぽになるので注意

List<String> listc = List.of("aa,", null);

java.lang.NullPointerException

JavaのLinkedHashMapの要素順序を反転させる

Mapのkeyとvalueを反転させるにはMapUtilsのinvertMapを使います

Map<String,String> mapA = new HashMap<>();
mapA.put("keyA","valueA");
mapA.put("keyB","valueB");
mapA.put("keyC","valueC");
Map<String,String> invMap = MapUtils.invertMap(mapA);

では要素の並びを反転させるにはどうしたらいいでしょうか まずJavaではLinkedHashMapを使うことで要素を追加した順番を担保できます

というわけでMapの要素を逆順にするコードを書きました

public static <T,R> Map<T,R> reverse(Map<T,R> map){
    List<T> mapmapper = map.keySet().stream().collect(Collectors.toList());
    Collections.reverse(mapmapper);
    return mapmapper
            .stream()
            .collect(Collectors.toMap(
                    k -> k, k -> map.get(k), (k, v) -> v, LinkedHashMap::new));
}

やり方は色々あると思いますが、最初にkeyだけの逆順リストを作成します。 あとはそれを順番に回して新しいLinkedHashMapを作成するという単純な処理です。

HashMapだと勝手にソートされたりします。

これをテストしてみます

@Test
public void test_mycollectionutils_reversemap() {
    Map<String, String> mapA = new LinkedHashMap<>();
    mapA.put("keyA", "valueA");
    mapA.put("keyB", "valueB");
    mapA.put("keyC", "valueC");
    mapA.put("keyE", "valueE");
    mapA.put("keyD", "valueD");

    Map<String, String> reMap = MyMapUtils.reverse(mapA);

    List<String> keys = reMap.keySet().stream().collect(Collectors.toList());
    assertEquals("keyD",keys.get(0));
    assertEquals("keyE",keys.get(1));
    assertEquals("keyC",keys.get(2));
    assertEquals("keyB",keys.get(3));
    assertEquals("keyA",keys.get(4));
}

わかりやすくEとDは元々順番が揃っていないようにしています