GSON踩坑:对象转换成Map时,Long型变为科学计数法及时间格式转换异常的解决方案

当使用Gson将对象转换为Map时,遇到Long类型的timestamp字段被转换为科学计数法的问题,导致解析错误。解决方法包括修改Gson配置为LongSerializationPolicy.STRING或者改用FastJson进行解析。对于时间格式转换异常,可以设置Gson的DateFormat。测试显示,两种方法都能有效避免数据格式问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

GSON踩坑:对象转换成Map时,Long型变为科学计数法及时间格式转换异常的解决方案

问题场景

在我需要将一个对象转为Map类型时,调用Gson.fromJson发现,原来对象中的long类型的timestamp字段,被解析成了科学计数法,导致后续的解析出错。

下面给出测试代码进行演示

首先定义一个pojo类,包含一个long类型的timestamp字段

import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class ResultItem {

    private long timestamp;
}
复制代码

Gson代码

public class JSONUtils {
    private static final Gson GSON = new GsonBuilder()
            .setLongSerializationPolicy(LongSerializationPolicy.DEFAULT)
            .serializeNulls().create();

    public static Map toMap(String s) {

        if (StringUtils.isEmpty(s)) {
            return null;
        }
        return GSON.fromJson(s, Map.class);
    }

    public static Map<String, Object> convert(Object o) {
        Type type = new TypeToken<Map<String, Object>>() {
        }.getType();
        return GSON.fromJson(JSONUtils.toString(o), type);
    }
}
复制代码

测试代码

    @Test
    public void testGson(){
       long timestamp = 1668394335647l;
        ResultItem item = new ResultItem();
        item.setTimestamp(timestamp);

        Map<String, Object> convert = JSONUtils.convert(item);
        System.out.println(convert);

    }
复制代码

结果如下:

{timestamp=1.668394335647E12}
复制代码

可以发现,ResultItem类对象在调用Gson.fromJson后,Map中的timestamp字段被转成了科学计数法来表示。

解决办法

一、修改GSON的配置

修改GSON的配置,将setLongSerializationPolicy(LongSerializationPolicy.DEFAULT)改为setLongSerializationPolicy(LongSerializationPolicy.STRING)。如下所示:

public class JSONUtils {
    private static final Gson GSON = new GsonBuilder()
            .setLongSerializationPolicy(LongSerializationPolicy.STRING)	# 将DEFAULT改为STRING
            .serializeNulls().create();

    public static Map toMap(String s) {

        if (StringUtils.isEmpty(s)) {
            return null;
        }
        return GSON.fromJson(s, Map.class);
    }

    public static Map<String, Object> convert(Object o) {
        Type type = new TypeToken<Map<String, Object>>() {
        }.getType();
        return GSON.fromJson(JSONUtils.toString(o), type);
    }
}
复制代码

再次测试结果:

{timestamp=1668394335647}
复制代码

可以看到,得到的Map中的timestamp字段保留了原来long的格式,不再是被解析成科学计数法,解决了问题。

二、换成FastJson进行解析

第二种方法是,换一种第三方解析工具,将Google的Gson换成Alibaba的FastJson工具进行解析,代码如下:

修改convert方法为FastJson工具进行解析

public class JSONUtils {
    private static final Gson GSON = new GsonBuilder()
            .setLongSerializationPolicy(LongSerializationPolicy.DEFAULT)
            .serializeNulls().create();

    public static Map toMap(String s) {

        if (StringUtils.isEmpty(s)) {
            return null;
        }
        return GSON.fromJson(s, Map.class);
    }

    public static Map<String, Object> convert(Object o) {
        Type type = new TypeToken<Map<String, Object>>() {
        }.getType();
        return JSONObject.parseObject(JSON.toJSONString(o), type);
    }
}
复制代码

再次测试结果:

{timestamp=1668394335647}
复制代码

可以看到结果仍然保留了原先long格式的数据,同样解决了问题。

时间格式转换异常

对于时间格式的转换异常,可以修改Gson的配置如下:

public class JSONUtils {
    private static final Gson GSON = new GsonBuilder()
        	.setDateFormat("yyyy-MM-dd HH:mm:ss")	# Gson添加DateFormat配置
            .setLongSerializationPolicy(LongSerializationPolicy.STRING)	# 将DEFAULT改为STRING
            .serializeNulls().create();

    public static Map toMap(String s) {

        if (StringUtils.isEmpty(s)) {
            return null;
        }
        return GSON.fromJson(s, Map.class);
    }

    public static Map<String, Object> convert(Object o) {
        Type type = new TypeToken<Map<String, Object>>() {
        }.getType();
        return GSON.fromJson(JSONUtils.toString(o), type);
    }
}

 

<think>由于文件过大不能一次性读取到内存中,且要求不能一次性读取整个文件,我们需要流式地处理JSON文件。但是,JSON本身是一种需要完整解析的数据格式,对于大文件处理通常采用流式JSON解析器(如Gson的JsonReader或Jackson的JsonParser)来逐部分读取。 然而,问题描述中文件结构是:每个键对应一个字典数组(即一个对象,其属性值都是数组)。例如: { "key1": [ {...}, {...} ], "key2": [ {...}, {...} ] } 现在要加入一个新的键值对,比如"key3": [ {...} ]。注意,这个操作是在已有的JSON对象中添加一个新的属性。 但问题在于,如果文件已经很大,我们无法一次性读取整个JSON对象,因为整个JSON是一个大对象。因此,我们需要考虑如何在不读取整个文件的情况下修改文件。 实际上,在文件末尾添加新的键值对是不符合JSON语法的,因为JSON要求整个对象用花括号包裹,属性之间用逗号分隔。所以,如果我们在文件末尾添加,会破坏JSON结构。 可行的思路: 1. 如果文件不存在,则直接创建一个新的JSON文件,写入整个对象(包含我们要添加的键值对)。 2. 如果文件存在,我们需要修改文件。但修改JSON文件中间部分会导致需要重写后面的所有内容(因为要在最后一个已有键值对后面添加逗号和新键值对,并确保最后的花括号位置正确)。 但是,由于文件过大,我们不想全部读入内存。我们可以采用以下方法: 方案: 我们可以将原文件(除了最后一个花括号)读入,然后在适当的位置插入新的键值对,并补上花括号。但是,这需要我们知道原文件最后一个花括号的位置。 具体步骤: 1. 如果文件不存在,直接创建并写入整个JSON对象(包含新的键值对)。 2. 如果文件存在: a. 使用RandomAccessFile或其他可以定位到文件末尾的方法,找到文件末尾的'}'字符(但注意,JSON文件可能包含格式化(换行、空格)或者压缩形式,所以不能简单定位到最后一个字符)。 b. 实际上,我们可以从文件末尾向前读取,直到找到非空白字符,如果是'}', 那么我们就可以在这个'}'之前插入新的键值对(注意需要添加逗号分隔)。 但是,如果原文件有多个键值对,那么最后一个键值对后面没有逗号,我们在添加新键值对,需要在原最后一个键值对后面添加逗号,然后写入新的键值对,最后再写入'}'。 然而,这种方法需要处理JSON的格式(比如换行、缩进等)。而且,如果原文件只有一个键值对,或者没有键值对(空对象{})等情况,都需要分别处理。 另一种思路:使用JSON流式解析器(如Gson的JsonReader)来读取整个文件,同使用JsonWriter来写入新的文件,在读取过程中,我们可以将原文件的内容原样写入(除了最后的'}'),然后在最后添加新的键值对,再写入'}'。这样我们不需要将整个文件读入内存,但是需要同打开两个文件(读文件和写文件),并且需要逐部分读取和写入。 步骤(使用流式解析和生成): 1. 使用JsonReader读取原文件(如果存在)。 2. 使用JsonWriter写入一个临文件。 3. 开始读取JSON对象: - 读取第一个token(应该是BEGIN_OBJECT) - 然后遍历所有键值对,将每个键值对写入临文件。 - 当读取到END_OBJECT,我们并不立即写入,而是先检查是否已经读取完所有内容。 4. 在写入END_OBJECT之前,我们插入新的键值对(注意:如果原对象不为空,则需要在前面添加逗号;如果为空,则直接写入新键值对)。 5. 最后写入END_OBJECT。 但是,这种方法需要处理JSON的精确结构,包括逗号、空格等。而且,流式解析器在读取并不保留原始格式(空白、换行等),所以写入的临文件格式可能与原文件不同(比如变成紧凑格式)。 如果我们希望保留原格式(缩进、换行等),那么这种方法就不合适了。 另一种折中:我们可以在写入使用相同的缩进格式,这样生成的文件虽然格式一致,但可能和原格式不同(比如原文件是紧凑的,我们也可以按紧凑写;原文件有缩进,我们也按缩进写)。但是,这样会丢失原格式的细节(比如空格数量等)。 考虑到问题要求不能一次性读取整个文件,我们采用流式处理(使用JsonReader和JsonWriter)并生成格式化的JSON(或者紧凑格式,根据原文件格式选择,但问题没有说明,我们可以选择紧凑格式,或者由用户指定)。 步骤详解(使用Gson库,因为Java标准库没有JSON流式解析): 1. 检查文件是否存在,不存在则直接创建并写入整个对象(包含新键值对)。 2. 存在则使用JsonReader读取原文件,同使用JsonWriter写入临文件。 3. 在读取原文件的过程中,我们记录下已经读取的键值对,并写入临文件。 4. 当读取到原文件结束(遇到END_OBJECT),我们暂停,然后写入新的键值对(注意:如果原文件有内容,则先写一个逗号,然后写新键值对;如果原文件为空,则直接写新键值对)。 5. 最后写入END_OBJECT。 注意:JsonReader和JsonWriter默认处理的是无格式的JSON(即紧凑格式)。如果我们希望保留原格式(比如缩进),那么我们需要知道原文件的格式,但流式解析器不会保留空白。所以,我们只能按紧凑格式写入,或者我们自己控制格式(例如,写入使用JsonWriter.setIndent(" ")来指定缩进,但这样整个文件都会按照这个缩进重写)。 因此,我们可以选择在写入使用缩进(比如两个空格)来格式化,这样生成的文件就是格式化的。如果原文件是紧凑的,那么重写后就是格式化的;如果原文件是格式化的,那么重写后也是格式化的(但可能缩进空格数不同)。如果要求保持原格式,则比较困难。 由于问题没有要求保留原格式,我们可以选择使用紧凑格式,或者允许格式化(比如两个空格缩进)。这里为了可读性,我们选择在写入使用缩进。 具体代码步骤(使用Gson): 1. 添加Gson依赖(如果项目没有,需要添加)。 2. 创建JsonReader读取原文件,JsonWriter写入临文件。 3. 读取原文件,并写入到临文件,直到遇到END_OBJECT之前。 4. 在END_OBJECT之前插入新的键值对。 5. 完成写入,删除原文件,将临文件重命名为原文件。 但是,注意:JsonReader在读取一个对象,我们如何知道已经读取到最后一个键值对了?实际上,JsonReader是按顺序读取的,我们无法预知后面是否还有键值对。所以,我们只能读取整个对象,然后在对象结束之前插入新键值对。 具体步骤: - 开始读取对象(BEGIN_OBJECT) - 然后循环读取键值对,直到遇到END_OBJECT。 - 在遇到END_OBJECT,我们并不结束,而是先写入新的键值对(注意逗号处理)。 伪代码: try (JsonReader reader = new JsonReader(new FileReader(existingFile)); JsonWriter writer = new JsonWriter(new FileWriter(tempFile))) { writer.setIndent(" "); // 设置缩进为两个空格,可选,如果不设置则为紧凑格式 reader.beginObject(); writer.beginObject(); boolean first = true; boolean hasElements = false; while (reader.hasNext()) { if (hasElements) { // 不是第一个元素,需要写逗号 // 但是JsonWriter会自动处理逗号,我们只需要按顺序写 } String key = reader.nextName(); // 写入键 writer.name(key); // 读取值(是一个数组) reader.beginArray(); writer.beginArray(); while (reader.hasNext()) { // 读取数组中的每个元素(对象) readAndWriteValue(reader, writer); // 递归处理?但这里数组元素是对象,我们需要按类读取 } reader.endArray(); writer.endArray(); hasElements = true; } // 现在原文件的所有键值对已经写入,接下来要写入新的键值对 // 如果原文件有键值对,那么我们需要在写入新键值对之前写一个逗号吗?不需要,JsonWriter会自动在键值对之间写逗号,但是我们已经退出了循环,所以我们需要自己处理逗号? // 实际上,JsonWriter要求我们按顺序写,它会在每个name之前自动写逗号(除了第一个)。所以,如果我们接着写一个新的name,它会自动写逗号。 // 写入新的键值对 writer.name(newKey); writer.beginArray(); // 将新的字典数组写入,这里假设新的值是一个数组,数组元素是对象 for (JsonElement element : newValueArray) { // 将element写入writer,但注意,我们这里没有用Gson的toJson,因为我们在用JsonWriter // 我们可以使用gson.toJson(element, writer); 或者自己写 // 但注意,我们这里为了通用,假设新值是一个List<Object>,但问题中要求的是一个字典数组,即数组元素是对象 // 这里为了简化,我们假设新值已经构造好,是一个JsonArray或者我们可以迭代写入的对象数组 // 具体写入方法:遍历数组,对每个元素(一个Map或自定义对象)写入 } writer.endArray(); // 结束对象 writer.endObject(); reader.endObject(); // 读取原文件的结束对象标记 } catch (Exception e) { // 处理异常 } 但是,上述方法有一个问题:在读取原文件,我们使用了递归方式读取数组中的每个元素(对象),但数组中的对象可能包含任意结构,我们需要完全复制。我们可以写一个工具方法,将JsonReader当前的值复制到JsonWriter。 幸运的是,Gson提供了这样的工具:JsonWriter.value(JsonReader) 并没有直接提供,但我们可以自己写一个递归复制的方法,或者使用Gson的TypeAdapter,但这样会复杂。 我们可以写一个复制整个Json元素的方法(递归): private void copyJsonElement(JsonReader reader, JsonWriter writer) throws IOException { switch (reader.peek()) { case BEGIN_ARRAY: reader.beginArray(); writer.beginArray(); while (reader.hasNext()) { copyJsonElement(reader, writer); } reader.endArray(); writer.endArray(); break; case BEGIN_OBJECT: reader.beginObject(); writer.beginObject(); while (reader.hasNext()) { writer.name(reader.nextName()); copyJsonElement(reader, writer); } reader.endObject(); writer.endObject(); break; case STRING: writer.value(reader.nextString()); break; case NUMBER: writer.value(reader.nextDouble()); // 或者根据实际类,但JsonReader有nextDouble, nextLong等,我们可以用nextString来保持精度 // 但这样可能丢失类,所以我们可以这样: String number = reader.nextString(); try { long l = Long.parseLong(number); writer.value(l); } catch (NumberFormatException e) { try { double d = Double.parseDouble(number); writer.value(d); } catch (NumberFormatException e2) { writer.value(number); // 作为字符串 } } // 或者更简单:使用JsonReader的peek()判断?但JsonReader的peek()只能知道是NUMBER,不知道是整数还是浮点数 // 所以,我们可以直接使用:writer.value(reader.nextDouble()); 会丢失精度吗?对于整数,可以读成double,但整数可能很大,所以用nextString然后写入字符串? // 但是JsonWriter.value(String)是写入字符串,不是数字。所以我们可以用: // writer.jsonValue(reader.nextString()); 但JsonWriter没有jsonValue方法。 // 所以,我们只能分别处理:整数和浮点数 // 实际上,JsonReader有nextLong, nextDouble, 但如果我们不确定,可以使用nextString然后写入字符串,但这样不符合数字的格式。 // 因此,我们选择使用: // writer.value(reader.nextString()); 这样写的是字符串,而不是数字。所以不行。 // 正确做法:使用JsonToken判断,但JsonReader的peek()返回的是NUMBER,我们无法区分整数和浮点数,所以我们可以尝试读取为double,然后判断是否是整数,再决定写入long还是double?这很麻烦。 // 替代方案:使用Gson的JsonParser解析当前值?但这样会读入整个值到内存,对于大值可能不好,但通常一个数字不会太大。 // 或者,我们可以使用:将数字作为字符串读取,然后尝试解析为整数,成功则写整数,否则写浮点数,如果都不行就写字符串(但这种情况不应该发生)。 // 但这样会影响性能。 // 考虑到性能,我们可以直接写字符串,但这样会改变类(数字变成字符串),所以不行。 // 因此,我们只能使用JsonReader的nextDouble()和nextLong()?但JsonReader没有提供方法判断当前数字是整数还是浮点数。 // 实际上,JsonReader的nextDouble()可以读取整数,并且不会丢失精度(整数在double的精确范围内),但整数可能很大(超出double的整数范围?double可以精确表示2^53以内的整数,所以对于一般整数是安全的)。 // 所以,我们这里选择:使用nextDouble()读取,然后写入double。但这样整数也会变成浮点数(带.0),这改变了原数据。 // 所以,我们使用:将数字作为字符串读取,然后直接写入原始字符串(JsonWriter.value(String)会添加引号,所以不行)。 // 在JsonWriter中,有一个value(Number)方法,我们可以这样: // writer.value(new LazilyParsedNumber(reader.nextString())); 但是LazilyParsedNumber是Gson内部的。 // 所以,我们可以自己实现一个Number子类?或者,我们使用Gson的JsonPrimitive,但这样需要解析整个值。 // 由于这个问题比较复杂,我们可以使用一个折中:使用Gson的JsonParser解析当前值(作为JsonElement),然后使用Gson.toJson(JsonElement, JsonWriter)写入。 // 但这样会解析整个值到内存,如果当前值是一个很大的对象(比如一个很大的数组或对象),那么就会占用很多内存。但我们的数组元素是字典,可能不大,所以可以接受。 // 但是,我们原本就是为了避免大内存,所以不能这样做。 // 因此,我们只能放弃完美复制数字类,使用double,并接受整数可能被写成浮点数的形式。 // 或者,我们可以使用以下方法:读取字符串,然后尝试解析为BigDecimal,然后使用writer.value(BigDecimal)?但JsonWriter有value(BigDecimal)方法。 String numStr = reader.nextString(); try { BigDecimal bd = new BigDecimal(numStr); writer.value(bd); } catch (NumberFormatException ex) { // 如果解析失败,则作为字符串写入?但这种情况不应该发生,因为JSON中数字是特定格式 writer.value(numStr); // 这样会写成字符串,但原意是数字,所以错误 } break; // 其他类类似 case BOOLEAN: writer.value(reader.nextBoolean()); break; case NULL: reader.nextNull(); writer.nullValue(); break; default: throw new IllegalStateException("Unknown token: " + reader.peek()); } } 但是,上述数字处理非常复杂。实际上,Gson的JsonReader和JsonWriter在复制数字可能会改变表示形式(比如整数写成浮点数)。为了避免这个问题,我们可以使用Gson的JsonParser来解析每个值?但这样会破坏流式处理的优势(如果值很大,比如一个非常大的数组,就会占用大量内存)。 考虑到我们处理的JSON文件中,每个键对应一个字典数组,而数组中的每个元素是一个字典(对象),这些字典的大小应该是可控的(不会特别大)。所以,我们可以将数组中的每个对象作为一个JsonElement读取,然后写入。 但是,如果数组中的某个字典非常大(比如有几十MB),那么读取它到内存仍然可能导致内存溢出。所以,我们需要避免。 因此,我们只能选择使用上述的递归复制方法,并接受数字可能被写成浮点数的形式(或者使用字符串表示数字,但这样会改变类)。 或者,我们可以使用另一种方法:不解析值,而是按字符串读取整个值(对于数组,我们读取整个数组字符串)。但这样需要我们自己解析数组的边界(括号匹配),实现起来复杂。 考虑到间,我们采用递归复制,对于数字,我们使用BigDecimal来保持精度和格式。 修改数字处理部分: case NUMBER: String numStr = reader.nextString(); // 使用BigDecimal解析,然后写入 try { BigDecimal bd = new BigDecimal(numStr); writer.value(bd); } catch (NumberFormatException e) { // 如果解析失败,可能是NaN, Infinity等,这些在JSON标准中不允许,但有些解析器支持 // 作为字符串写入?但这样类就变了 // 或者直接抛出异常 throw new IOException("Invalid number: " + numStr, e); } break; 但是,BigDecimal可能会损失科学计数法的表示形式(它会展开),所以写入的字符串可能很长。而且,有些数字可能很大,BigDecimal会占用较多内存。但考虑到单个数字不会太大,所以可以接受。 现在,我们回到主流程。 但是,还有一个问题:在写入新键值对,我们如何写入新的数组?新的数组也是一个字典数组,即数组元素是对象。我们可以用同样的递归写入方法,但新数组的数据来源是内存中的对象(比如List<Map>)。 我们可以这样写: writer.name(newKey); writer.beginArray(); for (Object obj : newValueArray) { // 这里,我们需要将obj写入JsonWriter // 我们可以使用Gson对象转换为JsonElement,然后用gson.toJson(obj, writer) // 但这样需要Gson实例 gson.toJson(obj, obj.getClass(), writer); } writer.endArray(); 其中,gsonGson实例。 综上所述,我们假设新值是一个List<Object>,每个Object是一个字典(Map或自定义对象)。 完整代码步骤: 1. 创建Gson实例(用于写入新值)。 2. 检查原文件是否存在,不存在则直接创建文件,并写入一个JSON对象,该对象包含新键值对(注意:如果新键值对是唯一的,那么整个对象只有这个键值对)。 3. 存在则: a. 创建临文件。 b. 用JsonReader读取原文件,JsonWriter写入临文件。 c. 设置writer的缩进(可选)。 d. 开始对象:reader.beginObject(), writer.beginObject()。 e. 循环读取键值对,对于每个键值对: - 读取键名,写入键名。 - 读取值(是一个数组),开始数组,然后循环读取数组中的每个元素(对象),使用递归复制方法将元素复制到writer,然后结束数组。 f. 循环结束后,原文件的所有键值对已经复制完成。 g. 写入新的键值对(键名和新数组),新数组的内容使用gson.toJson写入。 h. 结束对象:writer.endObject(),reader.endObject()。 i. 关闭reader和writer。 j. 删除原文件,将临文件重命名为原文件。 注意:在复制原文件键值对,我们是一个一个复制的,所以内存占用取决于数组中的单个元素大小,而不是整个文件。 但是,如果原文件的一个数组非常大(有几十万个子对象),那么复制这个数组,我们可能会在内存中积累很多数据吗?不会,因为我们是流式处理,每个对象复制完就写入文件,不会积累。但是,递归深度可能会很深(如果嵌套很深),但通常不会太深。 另外,如果原文件为空(即只有{}),那么我们在读取,循环不会执行,然后我们直接写入新键值对。 处理逗号:JsonWriter会自动在键值对之间添加逗号,所以如果原文件有键值对,那么新键值对会自动添加逗号分隔。 代码示例(简化版,假设我们使用Gson,并处理了数字): 由于代码较长,这里给出关键部分: 依赖:com.google.code.gson:gson:2.8.6 或更高版本 方法: public void addKeyValueToJsonFile(File jsonFile, String newKey, List<Object> newValue) throws IOException { Gson gson = new Gson(); if (!jsonFile.exists()) { // 创建文件并写入整个对象 try (JsonWriter writer = new JsonWriter(new FileWriter(jsonFile))) { writer.setIndent(" "); // 缩进两个空格 writer.beginObject(); writer.name(newKey); writer.beginArray(); for (Object obj : newValue) { gson.toJson(obj, obj.getClass(), writer); } writer.endArray(); writer.endObject(); } return; } // 创建临文件 File tempFile = File.createTempFile("jsonTemp", ".json", jsonFile.getParentFile()); try (JsonReader reader = new JsonReader(new FileReader(jsonFile)); JsonWriter writer = new JsonWriter(new FileWriter(tempFile))) { writer.setIndent(" "); // 设置缩进,与上面一致 reader.beginObject(); writer.beginObject(); // 复制原有键值对 while (reader.hasNext()) { String key = reader.nextName(); writer.name(key); copyJsonElement(reader, writer); // 复制值(数组) } // 写入新的键值对 writer.name(newKey); writer.beginArray(); for (Object obj : newValue) { gson.toJson(obj, obj.getClass(), writer); } writer.endArray(); writer.endObject(); reader.endObject(); } // 替换原文件 if (!jsonFile.delete()) { throw new IOException("Could not delete original file"); } if (!tempFile.renameTo(jsonFile)) { throw new IOException("Could not rename temp file"); } } private void copyJsonElement(JsonReader reader, JsonWriter writer) throws IOException { JsonToken token = reader.peek(); switch (token) { case BEGIN_ARRAY: reader.beginArray(); writer.beginArray(); while (reader.hasNext()) { copyJsonElement(reader, writer); } reader.endArray(); writer.endArray(); break; case BEGIN_OBJECT: reader.beginObject(); writer.beginObject(); while (reader.hasNext()) { writer.name(reader.nextName()); copyJsonElement(reader, writer); } reader.endObject(); writer.endObject(); break; case STRING: writer.value(reader.nextString()); break; case NUMBER: // 使用字符串读取,然后用BigDecimal写入 String numberStr = reader.nextString(); try { BigDecimal bd = new BigDecimal(numberStr); writer.value(bd); } catch (NumberFormatException e) { throw new IOException("Invalid number: " + numberStr, e); } break; case BOOLEAN: writer.value(reader.nextBoolean()); break; case NULL: reader.nextNull(); writer.nullValue(); break; default: throw new IOException("Unexpected token: " + token); } } 注意:上述代码中,copyJsonElement递归复制任何JSON元素,包括数组和对象。 但是,这里有一个假设:原文件中的每个键对应的值都是数组(根据问题描述)。所以,在复制值的候,我们预期会是一个数组。但为了通用,我们写成递归复制,可以处理任何结构。 另外,如果原文件中有多个键,我们都会复制。 新键值对的newValue是一个List<Object>,每个Object是一个字典(例如Map<String, Object>或自定义对象)。我们使用gson.toJson来写入每个对象。 但是,如果原文件格式是紧凑的,我们写入的临文件会变成缩进格式(因为我们设置了缩进)。如果希望保持紧凑,可以去掉writer.setIndent(" ");,这样JsonWriter会写成紧凑格式。 另外,如果原文件有缩进,而我们用缩进写入,那么整个文件会重新格式化(缩进空格数可能不同)。 如果要求保持原格式,则很困难,因为流式解析器会丢弃空白。所以,我们只能选择一种格式(紧凑或缩进)来写入。 由于问题没有要求,我们可以选择缩进格式(可读性好)。 最后,注意异常处理,以及文件操作(删除和重命名)的原子性。 以上是一种解决方案,它可以在不一次性加载整个文件到内存的情况下,完成添加新键值对的任务。 但是,这种方法在复制原文件,如果原文件非常大,那么复制过程可能会比较慢(需要逐元素复制),但内存占用是可控的(取决于嵌套深度,而不是文件大小)。 另外,如果添加键值对的过程中失败,应该删除临文件,避免残留。 由于代码较长,这里只提供思路和关键代码,实际使用需要根据具体需求调整。 注意:上述代码没有处理可能出现的异常(如文件读写权限等),在实际应用中需要完善。 由于问题要求使用中文回答,以上是详细的解答。</think>### 在 Java 中流式处理大 JSON 文件添加键值对 当 JSON 文件过大无法一次性加载到内存,可以使用 **流式 JSON 解析库(如 Jackson)** 逐部分读取文件,并在写入新键值对保持低内存占用。以下是完整解决方案: ```java import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.List; import java.util.Map; public class JsonStreamUpdater { public static void addKeyValuePair( File inputFile, String newKey, List<Map<String, Object>> newValue ) throws IOException { // 文件不存在直接创建并写入 if (!inputFile.exists()) { try (JsonGenerator gen = new JsonFactory().createGenerator(new FileWriter(inputFile))) { gen.writeStartObject(); gen.writeFieldName(newKey); writeValueArray(gen, newValue); gen.writeEndObject(); } return; } // 创建临文件处理流式读写 File tempFile = File.createTempFile("temp-json", ".json"); try (JsonParser parser = new JsonFactory().createParser(inputFile); JsonGenerator gen = new JsonFactory().createGenerator(new FileWriter(tempFile))) { gen.copyCurrentStructure(parser); // 复制初始结构 // 定位到对象结尾前的 token while (parser.nextToken() != JsonToken.END_OBJECT) { gen.copyCurrentEvent(parser); // 复制现有键值对 } // 添加逗号分隔符(如果原文件非空) if (parser.getCurrentLocation().getByteOffset() > 1) { gen.writeRaw(','); } // 写入新键值对 gen.writeFieldName(newKey); writeValueArray(gen, newValue); gen.writeEndObject(); // 关闭整个对象 } catch (Exception e) { Files.delete(tempFile.toPath()); throw e; } // 原子替换原文件 Files.move(tempFile.toPath(), inputFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } // 辅助方法:写入字典数组 private static void writeValueArray( JsonGenerator gen, List<Map<String, Object>> values ) throws IOException { gen.writeStartArray(); for (Map<String, Object> item : values) { gen.writeObject(item); // 自动处理嵌套结构 } gen.writeEndArray(); } } ``` ### 关键实现细节 1. **流式处理原理** - 使用 `JsonParser` 顺序读取 JSON 令牌(tokens) - 通过 `JsonGenerator` 实写入新文件 - 仅保持当前令牌在内存中(常量级内存占用) 2. **新键值对插入位置** - 定位到原 JSON 对象的结束符 `}` 前 - 添加逗号分隔符(非空文件) - 写入新键和对应的数组值 3. **文件操作安全** - 先写入临文件,完成后原子替换原文件 - 异常清理临文件 - 支持首次运行自动创建文件 4. **依赖要求** ```xml <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.0</version> </dependency> ``` ### 使用示例 ```java // 新增键值对示例 List<Map<String, Object>> newData = List.of( Map.of("id", 901, "status", "active"), Map.of("id", 902, "status", "pending") ); JsonStreamUpdater.addKeyValuePair( new File("data.json"), "transactions", newData ); ``` ### 处理效果 原文件内容: ```json {"users":[{"id":101},{"id":102}]} ``` 添加后: ```json {"users":[{"id":101},{"id":102}],"transactions":[{"id":901,"status":"active"},{"id":902,"status":"pending"}]} ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值