SCX Projects

SCX Node

SCX Node 是一套中立的、平台无关的通用数据模型。

它提供一组简单的 Node 类型,用来表达对象、数组、字符串、数字、布尔值和 null。

SCX Node 本身不负责 JSON 解析,不负责 XML 解析,也不负责 Java 对象绑定。它只提供一个格式无关的数据树模型,供其它模块复用。

例如:

JSON       -> Node
XML        -> Node
Java Bean  -> Node
Node       -> JSON
Node       -> XML
Node       -> Java Bean

当前版本为 0.1.0

GitHub

安装

Maven

<dependency>
    <groupId>dev.scx</groupId>
    <artifactId>scx-node</artifactId>
    <version>0.1.0</version>
</dependency>

基本概念

SCX Node 中最核心的概念包括:

Node             所有节点的顶层接口
ValueNode        值节点
NumberNode       数字节点
ContainerNode    容器节点
ObjectNode       对象节点
ArrayNode        数组节点
StringNode       字符串节点
BooleanNode      布尔节点
NullNode         null 节点

类型层级可以简单理解为:

Node
├── ValueNode
│   ├── NumberNode
│   │   ├── IntNode
│   │   ├── LongNode
│   │   ├── FloatNode
│   │   ├── DoubleNode
│   │   ├── BigIntegerNode
│   │   └── BigDecimalNode
│   ├── StringNode
│   └── BooleanNode
├── ContainerNode
│   ├── ObjectNode
│   └── ArrayNode
└── NullNode

也就是说:

ObjectNode / ArrayNode 是可变容器
ValueNode / NullNode 是不可变值

快速开始

创建一个对象节点:

import dev.scx.node.ArrayNode;
import dev.scx.node.ObjectNode;

import static dev.scx.node.NullNode.NULL;

var user = new ObjectNode();

user.put("id", 1);
user.put("name", "Tom");
user.put("age", 18);
user.put("active", true);
user.put("parent", NULL);

var tags = new ArrayNode();

tags.add("java");
tags.add("scx");

user.put("tags", tags);

System.out.println(user);

输出大致是:

{
 "id": 1,
 "name": "Tom",
 "age": 18,
 "active": true,
 "parent": null,
 "tags": [
  "java",
  "scx"
 ]
}

需要注意,toString() 采用的是类 JSON 格式,主要用于调试和展示。

如果需要标准 JSON 序列化,应使用 scx-format-json

Node

Node 是所有节点的顶层接口。

它是一个 sealed interface,只允许下面几类节点实现:

ValueNode
ContainerNode
NullNode

接口定义可以理解为:

public sealed interface Node permits ValueNode, ContainerNode, NullNode {

    Node deepCopy();

}

deepCopy() 用于深拷贝。

不同节点的行为不同:

ContainerNode    返回结构完全独立的副本
ValueNode        返回自身
NullNode         返回自身

原因是:

ObjectNode / ArrayNode 是可变的
ValueNode / NullNode 是不可变的

ContainerNode

ContainerNode 表示容器节点。

它有两个实现:

ObjectNode
ArrayNode

接口可以理解为:

public sealed interface ContainerNode extends Node permits ArrayNode, ObjectNode {

    int size();

    boolean isEmpty();

    void clear();

    @Override
    ContainerNode deepCopy();

}

容器节点是可变的。

因此 deepCopy() 会递归复制内部结构。

ObjectNode

ObjectNode 表示对象节点。

它内部使用 LinkedHashMap<String, Node> 保存字段。

因此字段顺序会按插入顺序保留。

创建对象节点:

var objectNode = new ObjectNode();

设置字段:

objectNode.put("name", new StringNode("Tom"));
objectNode.put("age", new IntNode(18));

也可以使用便捷方法:

objectNode.put("name", "Tom");
objectNode.put("age", 18);
objectNode.put("active", true);

获取字段:

Node name = objectNode.get("name");

删除字段:

Node old = objectNode.remove("name");

遍历字段:

for (var field : objectNode) {
    System.out.println(field.getKey());
    System.out.println(field.getValue());
}

ObjectNode 的 null 规则

ObjectNode 不允许字段名为 null

objectNode.put(null, "Tom");

会抛出:

NullPointerException

ObjectNode 也不允许字段值为 Java null

objectNode.put("name", (Node) null);

会抛出:

NullPointerException

如果要表示数据层面的 null,应使用:

import static dev.scx.node.NullNode.NULL;

objectNode.put("name", NULL);

也就是说:

Java null       表示没有对象
NullNode.NULL   表示数据中的 null

ObjectNode 便捷 put 方法

ObjectNode 提供了一组便捷方法。

objectNode.put("i", 1);
objectNode.put("l", 1L);
objectNode.put("f", 1.0F);
objectNode.put("d", 1.0D);
objectNode.put("bigInt", new BigInteger("123456789"));
objectNode.put("bigDecimal", new BigDecimal("123.45"));
objectNode.put("str", "hello");
objectNode.put("bool", true);

它们会自动包装成对应的节点:

int         -> IntNode
long        -> LongNode
float       -> FloatNode
double      -> DoubleNode
BigInteger  -> BigIntegerNode
BigDecimal  -> BigDecimalNode
String      -> StringNode
boolean     -> BooleanNode

如果要放入 null,不要传 Java null,而是使用:

objectNode.put("x", NullNode.NULL);

ArrayNode

ArrayNode 表示数组节点。

它内部使用 ArrayList<Node> 保存元素。

创建数组节点:

var arrayNode = new ArrayNode();

添加元素:

arrayNode.add(new StringNode("Tom"));
arrayNode.add(new IntNode(18));

也可以使用便捷方法:

arrayNode.add("Tom");
arrayNode.add(18);
arrayNode.add(true);

按索引插入:

arrayNode.add(0, new StringNode("first"));

按索引替换:

arrayNode.set(0, new StringNode("new value"));

按索引读取:

Node first = arrayNode.get(0);

按索引删除:

Node old = arrayNode.remove(0);

遍历数组:

for (var item : arrayNode) {
    System.out.println(item);
}

ArrayNode 的 null 规则

ArrayNode 不允许添加 Java null

arrayNode.add((Node) null);

会抛出:

NullPointerException

如果要表示数据层面的 null,应使用:

arrayNode.add(NullNode.NULL);

也就是说:

Java null       不允许作为节点元素
NullNode.NULL   表示数组中的 null

ArrayNode 便捷 add 方法

ArrayNode 提供了一组便捷方法。

arrayNode.add(1);
arrayNode.add(1L);
arrayNode.add(1.0F);
arrayNode.add(1.0D);
arrayNode.add(new BigInteger("123456789"));
arrayNode.add(new BigDecimal("123.45"));
arrayNode.add("hello");
arrayNode.add(true);

它们会自动包装成对应节点:

int         -> IntNode
long        -> LongNode
float       -> FloatNode
double      -> DoubleNode
BigInteger  -> BigIntegerNode
BigDecimal  -> BigDecimalNode
String      -> StringNode
boolean     -> BooleanNode

ValueNode

ValueNode 表示值节点。

它有三类实现:

NumberNode
StringNode
BooleanNode

ValueNode 提供一组类型转换方法:

int asInt();

int asIntExact();

long asLong();

long asLongExact();

float asFloat();

float asFloatExact();

double asDouble();

double asDoubleExact();

BigInteger asBigInteger();

BigInteger asBigIntegerExact();

BigDecimal asBigDecimal();

String asString();

boolean asBoolean();

其中:

asXxx()        允许按 Java / BigDecimal 规则转换,可能丢失精度
asXxxExact()   不允许丢失精度,精度丢失时抛 ArithmeticException

对于 StringNode,如果字符串不能解析为数字,对应数字转换会抛出 NumberFormatException

NumberNode

NumberNode 表示数字节点。

它有六种实现:

IntNode
LongNode
FloatNode
DoubleNode
BigIntegerNode
BigDecimalNode

数字节点转换为其它数字类型时,不会出现 NumberFormatException

但是精确转换仍然可能抛出:

ArithmeticException

例如:

var node = new BigDecimalNode(new BigDecimal("1.5"));

node.asIntExact();

会因为精度丢失而抛出异常。

IntNode

IntNodeint 节点。

var node = new IntNode(123);

常见转换:

int i = node.asInt();

long l = node.asLong();

String s = node.asString();

boolean b = node.asBoolean();

其中:

asBoolean() = value != 0

IntNode 是 record,因此值通过 value() 获取:

int value = node.value();

LongNode

LongNodelong 节点。

var node = new LongNode(123L);

常见转换:

long l = node.asLong();

int i = node.asInt();

int exact = node.asIntExact();

如果 long 值无法无损转换为 intasIntExact() 会抛出 ArithmeticException

FloatNode

FloatNodefloat 节点。

var node = new FloatNode(1.25F);

它可以转换为其它数字类型。

但是精确转换到整数时,如果存在小数部分,会抛出 ArithmeticException

DoubleNode

DoubleNodedouble 节点。

var node = new DoubleNode(1.25D);

FloatNode 类似,普通转换允许精度变化,精确转换会检查精度丢失。

BigIntegerNode

BigIntegerNodeBigInteger 节点。

var node = new BigIntegerNode(new BigInteger("12345678901234567890"));

BigIntegerNode 不允许值为 null

new BigIntegerNode(null);

会抛出 NullPointerException

BigDecimalNode

BigDecimalNodeBigDecimal 节点。

var node = new BigDecimalNode(new BigDecimal("123.45"));

BigDecimalNode 不允许值为 null

new BigDecimalNode(null);

会抛出 NullPointerException

StringNode

StringNode 表示字符串节点。

var node = new StringNode("123");

StringNode 不允许值为 null

new StringNode(null);

会抛出 NullPointerException

字符串可以转换为数字:

int i = new StringNode("123").asInt();

long l = new StringNode("123").asLong();

BigDecimal d = new StringNode("123.45").asBigDecimal();

如果字符串不是合法数字,会抛出:

NumberFormatException

字符串转布尔使用:

Boolean.parseBoolean(value)

因此:

new StringNode("true").asBoolean();

结果是:

true

而:

new StringNode("yes").asBoolean();

结果是:

false

BooleanNode

BooleanNode 表示布尔节点。

它不是 record,而是两个单例:

BooleanNode.TRUE

BooleanNode.FALSE

通常使用:

var node = BooleanNode.of(true);

布尔转数字规则:

true   -> 1
false  -> 0

示例:

var node = BooleanNode.TRUE;

int i = node.asInt();

String s = node.asString();

boolean b = node.asBoolean();

NullNode

NullNode 表示数据层面的 null。

它是单例:

NullNode.NULL

构造方法是私有的,因此不能手动 new。

var node = NullNode.NULL;

NullNode#toString() 返回:

null

NullNode#deepCopy() 返回自身。

deepCopy

deepCopy() 用于复制节点。

对于不可变节点:

StringNode
IntNode
LongNode
FloatNode
DoubleNode
BigIntegerNode
BigDecimalNode
BooleanNode
NullNode

deepCopy() 返回自身。

例如:

var a = new StringNode("hello");
var b = a.deepCopy();

System.out.println(a == b);

结果是:

true

对于容器节点:

ObjectNode
ArrayNode

deepCopy() 会递归复制结构。

示例:

var user = new ObjectNode();
user.put("name", "Tom");

var copy = user.deepCopy();

copy.put("name", "Jerry");

System.out.println(user.get("name"));
System.out.println(copy.get("name"));

原对象不会被修改。

需要注意,当前实现假设 ObjectNode / ArrayNode 不存在自引用。

不要构造递归引用结构。

equals 和 hashCode

ObjectNode 的相等性由内部字段 map 决定。

ArrayNode 的相等性由内部元素 list 决定。

数字节点、字符串节点等 record 类型按 record 默认规则比较。

BooleanNode 按布尔值比较。

示例:

var a = new ObjectNode();
a.put("name", "Tom");

var b = new ObjectNode();
b.put("name", "Tom");

System.out.println(a.equals(b));

结果是:

true

toString

SCX Node 的 toString() 采用类 JSON 格式。

例如:

var node = new ObjectNode();

node.put("name", "Tom");
node.put("age", 18);

System.out.println(node);

输出大致是:

{
 "name": "Tom",
 "age": 18
}

需要注意:

  1. toString() 主要用于调试。
  2. StringNode#toString() 只做简单引号包裹,不处理复杂 JSON 转义。
  3. 如果需要严格 JSON 输出,应使用 scx-format-json

与 JSON 的关系

SCX Node 的结构和 JSON 数据模型很接近。

可以这样理解:

ObjectNode       JSON object
ArrayNode        JSON array
StringNode       JSON string
NumberNode       JSON number
BooleanNode      JSON boolean
NullNode.NULL    JSON null

但是 SCX Node 本身不是 JSON 库。

它不负责:

解析 JSON 字符串
输出标准 JSON
处理 JSON 转义
处理 JSON 语法选项

这些能力应由 scx-format-json 提供。

与 XML 的关系

XML 和 Node 的结构不完全一致。

scx-format-xml 会把 XML 规范化映射到 Node。

例如 XML 属性、子元素、重复标签等都会按 XML 模块自己的规则转换成 ObjectNode / ArrayNode / StringNode / NullNode

SCX Node 本身不关心这些规则。

与 SCX Object 的关系

scx-object 定义了:

Object <-> Node

的抽象接口。

scx-object-x 提供默认实现。

也就是说,SCX Node 可以作为 Java 对象和外部格式之间的中间模型。

Java Object
    ↓
Node
    ↓
JSON / XML

以及:

JSON / XML
    ↓
Node
    ↓
Java Object

完整示例

import dev.scx.node.ArrayNode;
import dev.scx.node.ObjectNode;

import static dev.scx.node.NullNode.NULL;

public class NodeDemo {

    public static void main(String[] args) {
        var user = new ObjectNode();

        user.put("id", 1);
        user.put("name", "Tom");
        user.put("age", 18);
        user.put("active", true);
        user.put("parent", NULL);

        var tags = new ArrayNode();

        tags.add("java");
        tags.add("scx");
        tags.add("node");

        user.put("tags", tags);

        var copy = user.deepCopy();

        ((ArrayNode) copy.get("tags")).add("copy");

        System.out.println(user);
        System.out.println(copy);
    }

}

设计说明

1. SCX Node 是中间数据模型

SCX Node 的目标不是替代 JSON/XML 库。

它是中间数据模型。

它让不同模块之间可以使用同一种数据树沟通。

2. 容器可变,值不可变

ObjectNodeArrayNode 是可变的。

值节点和 NullNode 是不可变的。

这也是为什么:

容器 deepCopy 返回新结构
值 deepCopy 返回自身

3. Java null 和 NullNode 分开

Java null 表示没有对象引用。

NullNode.NULL 表示数据中的 null。

SCX Node 不允许把 Java null 放进 ObjectNodeArrayNode

4. ObjectNode 保持字段顺序

ObjectNode 内部使用 LinkedHashMap

因此字段顺序会按插入顺序保留。

这对调试输出、格式化输出和稳定测试比较有用。

5. toString 不是严格序列化器

toString() 只是调试友好输出。

它不是完整 JSON 序列化器。

严格格式输出应交给 scx-format-json

常见问题

SCX Node 是 JSON 库吗?

不是。

它只是通用数据模型。

可以把 Java null 放入 ObjectNode 吗?

不可以。

请使用 NullNode.NULL

可以把 Java null 放入 ArrayNode 吗?

不可以。

请使用 NullNode.NULL

NullNode 可以 new 吗?

不可以。

使用:

NullNode.NULL

BooleanNode 可以 new 吗?

不可以。

使用:

BooleanNode.of(true)

或者:

BooleanNode.TRUE
BooleanNode.FALSE

deepCopy 会复制所有节点吗?

容器会复制。

值节点和 null 节点会返回自身。

ObjectNode 的字段有顺序吗?

有。

它按插入顺序保存字段。

StringNode 的 toString 是严格 JSON 字符串吗?

不是。

它只是简单用引号包裹。

如果需要严格 JSON 转义,请使用 scx-format-json

数字转换什么时候会抛异常?

精确转换时如果发生精度丢失,会抛 ArithmeticException

字符串转数字时如果格式不正确,会抛 NumberFormatException

StringNode 的 asBoolean 怎么判断?

使用 Java 的 Boolean.parseBoolean(...)

只有忽略大小写等于 "true" 时才是 true

NumberNode 的 asBoolean 怎么判断?

非零为 true

零为 false

ObjectNode 和 ArrayNode 可以自引用吗?

不推荐。

当前 deepCopy()toString() 都假设不存在自引用。