Reactの状態管理ライブラリRecoilを使ってみる

フロントエンドは、よくVueを使っているプロジェクトにいることが多いのですが、ここ最近は個人の時間でReactを勉強していました。状態管理ライブラリを探していたところRecoilというものがあることを知り、少し触れてみました。

この記事ではAtomの使い方について、簡単に書いておこうかと思います。

https://recoiljs.org/docs/introduction/core-concepts#atoms

環境

viteを使いプロジェクトを作成します。React + TypeScript のテンプレートを指定しています。

npm create vite@latest recoil-my-playground -- --template react-ts

ディレクトリに移動して、Recoilをインストールします。

npm install recoil

package.json の中身は次のとおりです。これを書いている時点でのRecoilのバージョンは、0.7.7 でした。

{
  "name": "recoil-my-playground",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "recoil": "^0.7.7"
  },
  "devDependencies": {
    "@eslint/js": "^9.8.0",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.1",
    "eslint": "^9.8.0",
    "eslint-plugin-react-hooks": "^5.1.0-rc.0",
    "eslint-plugin-react-refresh": "^0.4.9",
    "globals": "^15.9.0",
    "typescript": "^5.5.3",
    "typescript-eslint": "^8.0.0",
    "vite": "^5.4.0"
  }
}

atom を定義する

atomは状態の最小限単位です(名前からも最小単位であることが想像しやすいですね)。Recoilではatomを定義し、Reactコンポーネントatomの値を読み込み/書き込みすることができるようになっています。

atomは次のように定義できます。

import { atom } from "recoil";

export const helloTypographyState = atom<string>({
  key: "helloTypographyState",
  default: "this is read from recoil atom",
});

ここでは、helloTypographyState を定義しました。このatomはstring型の値を保持し、デフォルトが this is read from recoil atomとしています。Reactコンポーネントはこの値にアクセスすることができます。

atom を読み込む

atomを読み込むにはuseRecoilValueを使います。useStateと同じ感じです。

上記のatomを読み込んで画面に表示するコンポーネントは次のように実装できます。

import { useRecoilValue } from "recoil";
import { helloTypographyState } from "../store/HelloTypographyState";

export function HelloTypography() {
  const helloTypography = useRecoilValue(helloTypographyState);
  return <h1>Hello, {helloTypography}</h1>;
}

atomを使うことでコンポーネントが表示する文字列を、コンポーネントから切り離して管理することができました。

atom を書き込む

atomを書き込むにはuseSetRecoilStateを使います。

次のButtonChangeHelloTypographyコンポーネントでは、先ほど定義した helloTypographyStateをChangeに変更します。

import { useSetRecoilState } from "recoil";
import { helloTypographyState } from "../store/HelloTypographyState";

export function ButtonChangeHelloTypography() {
  const setHelloTypography = useSetRecoilState(helloTypographyState);
  return <button onClick={() => setHelloTypography("Change")}>Change</button>;
}

これで、同じatomを共有する2つのReactコンポーネントを定義することができました。atomの値は変化が起きると値を読み込んでいるコンポーネントは最新の値で再レンダリングされる仕組みになっています。そのため、atomを書き込むコンポーネントatomの値をどのように変更すればよいかだけに注力すればよく、再レンダリングをしなければならないReactコンポーネントや、再レンダリングのタイミングはRecoilが管理してくれます。

「具体と抽象」を読んで思ったこと

先日、具体と抽象を買って読み始めたのだが、ふと技術本も抽象度が高い内容を扱っているものと具体的な内容を扱っているものに分けることができそうだと気がついた。

例えば、特定の言語にの使い方やフレームワークの解説は具体的。一方で、プログラミングスタイルやリファクタリングなどを扱うものは抽象的。

仮にこのように分類してみると、抽象度が高い内容を扱ったものであればいろいろな現場、プロジェクト、会社で役に立つし、具体的な内容を扱っているものは扱っている内容と同じ仕事をする時において特に効果を発揮すると言える。

Scala 3 でfor内包表記を使ったコードのメモ

scala 3 を使っていて、いい感じにコードを書くことができたので、ここにメモ。

CSVから値を読み込んで直積型に変換する処理を実装していた。

登場する型はRecipeとRecipeRecord。

CSVから読み込んだデータはRecipeRecordで表現されているので、それをRecipeに変換する。

case class RecipeRecord(name: String, description: String)

def convertToRecipe(recipeRecord: RecipeRecord): Either[String, Recipe] = for {
  _     <- validateRecipeName(recipeRecord.name)
  _     <- validateRecipeDescription(recipeRecord.description)
  recipe = Recipe(recipeRecord.name, recipeRecord.description)
} yield recipe

変換をする時に、バリデーションを行う。

バリデーションで失敗すると、EitherのLeft[String]が返るようにしている。一方で、バリデーションが成功すればRight[Unit]が返る。

これをfor内包表記で使うことで、一連の処理をつなげて書くことができた。for内包表記のおかげで短絡も実現できたので、バリデーションが失敗すると関数全体の戻り値はLeft[String]になる。バリデーションが成功すればRithg[Recipe]が返される。

クライアントコードは変換に成功すればRecipe型を、失敗すれば原因を表す文字列を受け取ることができる。

JavaScriptのthisについて

関数宣言とArrow Functionのthisの違いについてのメモです。

以下のサイトに詳しく書いてあります。

関数宣言の場合、thisは動的に決まるとのこと。

ベースのオブジェクトがあればそのオブジェクトをthisとし、なければundefinedとなる、と。

ここで言うベースのオブジェクトとはブラケット演算子、ドット演算子の左側にあるオブジェクトのことです。

では、実験してみましょう。

環境はこちら。

❯ node --version
v20.11.1

REPLを使っていきます。ちょっと試したいときはREPLが使いやすいと個人的には思います。

次のようにthisをコンソールに出力してみます。

関数宣言の場合、ただ呼び出すとundefinedになります。これはベースとなるオブジェクトがないからですね。

> function hoge() {
... console.log(this);
... }
> hoge();
undefined

このhoge関数をhogeObjオブジェクトにセットしてみます。

そうすると、thisはhogeObjになりました。thisが実行時のコンテキストで動的に変わっているのがわかりました。

> const hogeObj = {
... hoge
... }
> hogeObj.hoge();
{ hoge: [Function: hoge] }

callを使えばベースとなるオブジェクトを指定しつつ呼び出すことができます。

> hoge.call({});
{}

同じようにArrow Functionでも試してみましょう。

> const fuga = () => {
... console.log(this);
... }
> fuga();
<ref *1> Object [global] {
  global: [Circular *1],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  structuredClone: [Function: structuredClone],
  atob: [Getter/Setter],
  btoa: [Getter/Setter],
  performance: [Getter/Setter],
  fetch: [Function: fetch],
  crypto: [Getter],
  hoge: [Function: hoge]
}

Arrow Functionの場合はthisを外側のスコープに探しに行きます。

node.jsの場合はglobalオブジェクトがグローバルスコープのthisのようです。

https://nodejs.org/dist/latest-v20.x/docs/api/globals.html#global-objects

そういえばdenoはどうなんだろう?

❯ deno --version
deno 1.41.0 (release, x86_64-unknown-linux-gnu)
v8 12.1.285.27
typescript 5.3.3
> this
Window {}

Windowオブジェクトが取れました。実行環境で違いますね。

話を戻してArrow Functionのベースオブジェクトを変更してみます。

> const fugaObj = {
... fuga
... }
> fugaObj.fuga();
<ref *1> Object [global] {
  global: [Circular *1],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  structuredClone: [Function: structuredClone],
  atob: [Getter/Setter],
  btoa: [Getter/Setter],
  performance: [Getter/Setter],
  fetch: [Function: fetch],
  crypto: [Getter],
  hoge: [Function: hoge]
}

> fuga.call({});
<ref *1> Object [global] {
  global: [Circular *1],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  structuredClone: [Function: structuredClone],
  atob: [Getter/Setter],
  btoa: [Getter/Setter],
  performance: [Getter/Setter],
  fetch: [Function: fetch],
  crypto: [Getter],
  hoge: [Function: hoge]
}

オブジェクトにセットしても、callをつかっても、結果が変わらないことがわかります。

というわけで、Arrow Functionの場合はthisが静的に決定されあとから変更することができないことがわかりました。

一方で、Arrow Function自体を動的に生成する場合、外側のthisが動的に変わるのであればもちろんArrow Functionが参照するthisも生成されるごとにことなるthisを参照することになります。

例えば次のようなケースです。

> const hoge = {
... fuga() {
...   return () => { console.log(this) };
... }
... }
> hoge.fuga()();
{ fuga: [Function: fuga] }
> hoge.fuga.call({})();
{}

hogeオブジェクトのfugaメソッドはArrow Functionを作成して返します。

Arrow Functionはthisを外側のスコープに探しに行きます。このときの外側のスコープにはfugaメソッドであり、fugaメソッドのthisはhogeオブジェクトです。

なので単純に hoge.fuga()() と呼び出すとArrow Functionのthisはhogeオブジェクトになりました。

ただし、fugaメソッドの呼び出しをcallで行いベースオブジェクトを変更するとArrow Functionのthisも変化します。

これはArrow Functionが作成されるとき、外側のスコープを探しに行くためです。

fugaメソッドのthisが変更されたのであれば変更されたthisをArrow Functionも探しに行くことになる、ということですね。

jacksonその2:TreeModel

jacksonについて調べたときのメモその2です。

環境は前回の記事と同じですので省略します。

https://baubaubau.hatenablog.com/entry/2024/02/04/145625

TreeModelというものがどうやらあるらしいので触ってみましょう。

https://github.com/FasterXML/jackson-databind?tab=readme-ov-file#tree-model

一応wikiにもページがありましたが中身が空っぽのようです。

https://github.com/FasterXML/jackson-databind/wiki/JacksonTreeModel

TreeModel

jacksonを使うとJavaのオブジェクトとJSONまたは、ListやMapなどのコレクションなどとJSONとの間で相互変換ができますが、JSONをトラバーサルするのには向いていません。ということでTreeModelというものがあるぞ、というみたいです。

ObjectMapperをインスタンス化し、

ObjectMapper objectMapper = new ObjectMapper();

readTreeでJSON文字列を読み込みます。するとJSONのルート要素を表現するJsonNodeが返ってくるようです。

JsonNode root;

root = objectMapper.readTree("""
    {
        "NB001": {
            "profile": {
                "firstName": "book",
                "lastName": "store",
                "age": 25
            },
            "type": [
                "software engineer"
            ]
        }
    }
    """);

トラバーサルして値を読み込むにはgetを使い目的のプロパティまで掘っていけば良さそう。

値の型に応じてasなんちゃらメソッドを呼び出せば対応するJavaの値で取得できます。

String firstName = root.get("NB001").get("profile").get("firstName").asText();
assertEquals("book", firstName);

int age = root.get("NB001").get("profile").get("age").asInt();
assertEquals(25, age);

値がない場合、結果はnullになりました。

JsonNode notExist = root.get("notExist");
assertNull(notExist);

値と型が適切ではない場合も試してみました。

どうやら変換できるものは頑張ってくれますが、無理なものは無意味な値で取得されるようです。てっきり例外が発生するかと思いましたがそうではないみたい。

int firstName = root.get("NB001").get("profile").get("firstName").asInt();
assertEquals(0, firstName); // 本当は "book"

String age = root.get("NB001").get("profile").get("age").asText();
assertEquals("25", age); // 数値の25が文字列に変換された

値を取得する前に対象がどんな型なのかを確認するには、isなんちゃらのメソッドを使えば良さそうです。

JsonNode firstNameNode = root.get("NB001").get("profile").get("firstName");
assertTrue(firstNameNode.isTextual());
assertFalse(firstNameNode.isInt());

JsonNode ageNode = root.get("NB001").get("profile").get("age");
assertTrue(ageNode.isInt());
assertFalse(ageNode.isTextual());

トラバーサるしつつ、JSONの一部分をオブジェクトへマッピングできるようです。

次のようなクラスを定義し、

public record Profile(String firstName, String lastName, int age) {
}

treeToValueで読み込みます。

Profile profile = objectMapper.treeToValue(root.get("NB001").get("profile"), Profile.class);
assertEquals(new Profile("book", "store", 25), profile);

そういえば、jacksonはネストされた構造のJSONを読み込むとどうなるのでしょうか?

こういうクラスを用意し、

record ProfileNestedJson(String firstName, String lastName, int age, ProfileNestedJsonAddress address) {
}
                                                                                                        
record ProfileNestedJsonAddress(String country, String countryCode) {
}

以下のように読み込めました。

JsonNode root = objectMapper.readTree("""
        {
            "NB001": {
                "profile": {
                    "firstName": "book",
                    "lastName": "store",
                    "age": 25,
                    "address": {
                        "country": "Japan",
                        "countryCode": "JPN"
                    }
                },
                "type": [
                    "software engineer"
                ]
            }
        }
        """);

var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJson.class);
                                                                                                                          
assertEquals(new ProfileNestedJson("book", "store", 25, new ProfileNestedJsonAddress("Japan", "JPN")), profileNestedJson);

Javaのクラスにはないプロパティを持つJSONはどうなるのでしょうか?

次のようにpostalCodeを追加してJSONを読み込むと例外が発生しました。てっきり無視されるかと思いましたが。

JsonNode root = objectMapper.readTree("""
        {
            "NB001": {
                "profile": {
                    "firstName": "book",
                    "lastName": "store",
                    "age": 25,
                    "address": {
                        "country": "Japan",
                        "countryCode": "JPN",
                        "postalCode": "000-000"
                    }
                },
                "type": [
                    "software engineer"
                ]
            }
        }
        """);
var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJson.class);
                                                                                                                          
assertEquals(new ProfileNestedJson("book", "store", 25, new ProfileNestedJsonAddress("Japan", "JPN")), profileNestedJson);

例外は次の通り。丁寧にignorableじゃないぞと書かれています。

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "postalCode" (class org.example.TreeModelTest$TreeToValueTest$ProfileNestedJsonAddress), not marked as ignorable (2 known properties: "countryCode", "country"])
 at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: org.example.TreeModelTest$TreeToValueTest$ProfileNestedJson["address"]->org.example.TreeModelTest$TreeToValueTest$ProfileNestedJsonAddress["postalCode"])

    at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)
    at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:1153)
    at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:2224)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1719)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1697)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:279)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:464)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1419)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:545)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:571)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:440)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1419)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
    at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
    at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4875)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3033)
    at com.fasterxml.jackson.databind.ObjectMapper.treeToValue(ObjectMapper.java:3497)
    at org.example.TreeModelTest$TreeToValueTest.treeToValueNestedJsonAdditionalProperties(TreeModelTest.java:157)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

どうやら @JsonIgnoreProperties で無視するプロパティを指定する必要があるとのこと。

https://github.com/FasterXML/jackson-databind?tab=readme-ov-file#annotations-ignoring-properties

というわけで次のようにしたら動きました。

record ProfileNestedJsonAdditionalProperties(String firstName,
                                             String lastName,
                                             int age,
                                             ProfileNestedJsonAdditionalPropertiesAddress address) {
}
                                                                                                    
@JsonIgnoreProperties("postalCode")
record ProfileNestedJsonAdditionalPropertiesAddress(String country, String countryCode) {
}

JsonNode root = objectMapper.readTree("""
        {
            "NB001": {
                "profile": {
                    "firstName": "book",
                    "lastName": "store",
                    "age": 25,
                    "address": {
                        "country": "Japan",
                        "countryCode": "JPN",
                        "postalCode": "000-000"
                    }
                },
                "type": [
                    "software engineer"
                ]
            }
        }
        """);
var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJsonAdditionalProperties.class);
                                                                                                                                
assertEquals(new ProfileNestedJsonAdditionalProperties("book", "store", 25,
        new ProfileNestedJsonAdditionalPropertiesAddress("Japan", "JPN")), profileNestedJson);

jacksonその1

jacksonについて調べてみたときのメモです。

jacksonのリポジトリはこちら。

https://github.com/FasterXML/jackson

jacksonとは、JavaJSONライブラリ、というところまでは前提知識があります。

以前からそれとなく使っていて、Spring frameworkを使っていればフレームワークがデフォルトで使用しているはずです。

jacksonは3つの要素から構成されているようです。

他にもコミュニティによるモジュールがあるようですが、上記のモジュールはjackson開発チームで開発、メンテナンスがされているそう。

サードパーティのモジュール一覧はこちらに記載がありました。思っていたより多いです。

https://github.com/FasterXML/jackson?tab=readme-ov-file#third-party-datatype-modules

databindリポジトリチュートリアルがありました。やってみましょう。

https://github.com/FasterXML/jackson-databind

環境

java -version
openjdk version "17.0.8.1" 2023-08-24
OpenJDK Runtime Environment Temurin-17.0.8.1+1 (build 17.0.8.1+1)
OpenJDK 64-Bit Server VM Temurin-17.0.8.1+1 (build 17.0.8.1+1, mixed mode, sharing)

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>jackson-my-playground</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jackson.version>2.16.0</jackson.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.10.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

read and write

package org.example;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class ReadWriteTest {

    @Test
    void readValue() throws JsonProcessingException {
        var objectMapper = new ObjectMapper();
        var person = objectMapper.readValue("""
                {
                 "firstName": "book",
                 "lastName": "store"
                }
                """, Person.class);
        assertEquals("firstName: book, lastName: store", person.toString());
    }

    @Test
    void writeValue() throws JsonProcessingException {
        var objectMapper = new ObjectMapper();
        var output = objectMapper.writeValueAsString(new Person("book", "store"));
        assertEquals("{\"firstName\":\"book\",\"lastName\":\"store\"}", output);
    }

    public static class Person {

        @SuppressWarnings("unused") // jackson use
        public Person() {

        }

        public Person(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public String firstName;

        public String lastName;

        @Override
        public String toString() {
            return String.format("firstName: %s, lastName: %s", firstName, lastName);
        }

    }

}

jacksonによるJSONの読み込み、書き込みはObjectMapperを使います。ObjectMapperクラスはインスタンス化することで使用できます。

JSONPOJOで読み込むにはreadValueを使います。反対に書き込むにはwriteValueを使います。

書き込んだ結果をStringで貰いたいのであれば、writeValueAsStringを使います。

読み込む先のPOJOはデフォルトコンストラクタ(引数なしコンストラクタ)が必要になりました。おそらくですが、読み込み元のJSONPOJOにどうやって変換するかどうかは変更できると思うので、上記のコードのようにデフォルトでは必要ということでしょう。

collection

JSONの読み込み、書き込みはPOJOだけではありません。以下のようにCollectionのMapやListを指定することも可能です。

package org.example;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class GenericCollectionTest {

    final ObjectMapper objectMapper = new ObjectMapper();

    @Test
    void readAsMap() throws JsonProcessingException {
        @SuppressWarnings("unchecked")
        Map<String, String> result = objectMapper.readValue("""
                {
                 "firstName": "book",
                 "lastName": "store"
                }
                """, Map.class);
        assertEquals(Map.of("firstName", "book", "lastName", "store"), result);
    }

    @Test
    void readAsList() throws JsonProcessingException {
        @SuppressWarnings("unchecked")
        List<Integer> result = objectMapper.readValue("""
                [1, 2, 3]
                """, List.class);
        assertEquals(List.of(1, 2, 3), result);
    }

    @Test
    void cantReadObjectToList() {
        assertThrows(MismatchedInputException.class, () -> objectMapper.readValue("""
                {
                 "firstName": "book",
                 "lastName": "store"
                }
                """, List.class));
    }

    @Test
    void readAsMapPersonRecord() throws JsonProcessingException {
        var people = objectMapper.readValue("""
                {
                 "NB001": {
                  "firstName": "book",
                  "lastName": "store"
                 }
                }
                """, new TypeReference<Map<Id, Person>>() {
        });
        assertEquals(Map.of(new Id("NB001"), new Person("book", "store")), people);
    }

    public record Id(String value) {
    }

    public record Person(String firstName, String lastName) {
    }

}

readAsMapPersonRecordではMapで読み込みをしつつ、valueにRecordクラスを指定しました。

このように、JSONをどのように変換するのか一つの型に収まらない場合はTypeReferenceを使えば良さそうです。

短絡評価:JavaScriptにおける値の評価順序と動作

  • 値が定まった時点で評価をそれ以上行わないことを短絡評価と呼ぶ。
  • 例えば[JavaScript]では && 演算子が該当する。
  • && は左から順に評価しtrueだった場合は右辺の評価を続ける。
    • 右辺が最後の評価対象の場合はその評価結果を返す。
    • true以外だった場合(false, undefined, 2)評価をそれ以上行わず、その時点の評価結果を返す。
  > true && console.log("hello");
  hello
  undefined
  > false && console.log("hello");
  false
  > console.log("hello") && "msg";
  hello
  undefined
  • && は暗黙的な型変換を行い、値を真偽値に変換する。
  • false に変換される値はfalsyな値と呼ばれる。

複数つなげた場合の動作

  • && を複数つなげて書くと短絡評価は左から右へと順番に処理される。
  • すべての値がtrueの場合の動作。
    • 1番目の値を評価し、trueなので真ん中の値を評価する。
    • 2番目の値を評価し、trueなので最後の値を評価する。
    • 3番目の値を評価し、結果として返す。
   > true && true && 'hello 1'
   'hello 1'
   > 'hello 1' && 'hello 2' && 'hello 3'
   'hello 3'
  • 途中の値がfalseの場合の動作。
    • 中央の値を評価し、falsyな値のためその値を結果として返す。
   > true && null && 'hello'
   null
  • 最後の値がfalseの場合の動作。
    • 最後の値を評価し、falsyな値のためその値を結果として返す。
   > true && true && null
   null