【问题标题】:Serialize a Double to 2 decimal places using Jackson使用 Jackson 将 Double 序列化到小数点后 2 位
【发布时间】:2023-04-05 00:06:01
【问题描述】:

我正在使用JacksonSpring MVC,将一些简单的对象写成JSON。其中一个对象具有amount 属性,类型为Double。 (我知道Double 不应该用作货币金额。但是,这不是我的代码。)

JSON 输出中,我想将金额限制为小数点后两位。目前显示为:

"amount":459.99999999999994

我尝试过使用 Spring 3 的 @NumberFormat 注释,但在这个方向上没有成功。看起来其他人也有问题:MappingJacksonHttpMessageConverter's ObjectMapper does not use ConversionService when binding JSON to JavaBean propertiesenter link description here

另外,我尝试使用带有自定义序列化程序的 @JsonSerialize 注释。
在模型中:

@JsonSerialize(using = CustomDoubleSerializer.class)
public Double getAmount()

和序列化器实现:

public class CustomDoubleSerializer extends JsonSerializer<Double> {
    @Override
    public void serialize(Double value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
        if (null == value) {
            //write the word 'null' if there's no value available
            jgen.writeNull();
        } else {
            final String pattern = ".##";
            //final String pattern = "###,###,##0.00";
            final DecimalFormat myFormatter = new DecimalFormat(pattern);
            final String output = myFormatter.format(value);
            jgen.writeNumber(output);
        }
    }
}

CustomDoubleSerializer“似乎”可以工作。但是,任何人都可以提出任何其他更简单(或更标准)的方法吗?

【问题讨论】:

  • 一种方法是在 setter 方法中格式化金额,然后将值设置为字段。因此getAmount() 将返回 2 个十进制值。不确定它是否可以满足您的要求。如果其他人期望该字段的精度,此实现可能会产生副作用。
  • 舍入序列化程序对我来说似乎是正确的方法。或者,创建两个 getter,即 getAmountPrecise()getAmountRounded(),并且只序列化后者。
  • 您能否使用此属性向 POJO 添加任何新方法?你能改变这个类还是它属于外部库?
  • 也许您可以通过使用private static final DecimalFormat formatter = new DecimalFormat(".##"); 并在之后引用字段formatter 来提高性能,因为无论如何它一直都是一样的?
  • 你试过@JsonFormat(shape = JsonFormat.Shape.NUMBER_FLOAT, pattern=...)吗?但我认为你的方式是标准的。

标签: java jackson


【解决方案1】:

请注意,459.99999999999994 实际上是 460,预计会以这种方式序列化。所以,你的逻辑应该比仅仅丢数字更棘手。 我可能会提出类似的建议:

Math.round(value*10)/10.0

您可能希望将其放入 setter,并摆脱自定义序列化。

【讨论】:

  • 像这样更改值仍然会导致序列化时带有重要尾随数字的数字。舍入严格来说是一个演示问题。在 Chrome 控制台中尝试此操作,例如 - (Math.round(Math.PI * 10) / 10.0).toFixed(20)。输出不是3.10000000000000000000,而是3.10000000000000008882
  • 答案适用于 java,而不是 javascript。
  • 两种语言对 IEEE-754 浮点数学的处理是相同的。
【解决方案2】:

我的项目中有类似的情况。我已将格式化代码添加到 POJO 的 setter 方法中。 DecimalFormatter、Math 和其他类最终将值四舍五入,但是,我的要求不是四舍五入,而只是将显示限制为 2 位小数。

我重新创建了这个场景。 产品是一个 POJO,它有一个成员 Double amount。 JavaToJSON 是一个将创建 Product 实例并将其转换为 JSON 的类。 Product 中的 setter setAmount 将负责格式化到小数点后两位。

这是完整的代码。

Product.java

package com;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Product {

    private String name;
    private Double amount;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Double getAmount() {
        return amount;
    }
    public void setAmount(Double amount) {
        BigDecimal bd = new BigDecimal(amount).setScale(2, RoundingMode.FLOOR);
        this.amount = bd.doubleValue();
    }

    @Override
    public String toString() {
        return "Product [name=" + name + ", amount=" + amount + "]";
    }
}

JavaToJSON.java

package com;

import java.io.File;
import java.io.IOException;

import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

public class JavaToJSON {

    public static void main(String[] args){

        ObjectMapper mapper = new ObjectMapper();

        try {
            Product product = new Product();
            product.setName("TestProduct");
            product.setAmount(Double.valueOf("459.99999999999994"));

            // Convert product to JSON and write to file
            mapper.writeValue(new File("d:\\user.json"), product);

            // display to console
            System.out.println(product);

        } catch (JsonGenerationException e) {

            e.printStackTrace();

        } catch (JsonMappingException e) {

            e.printStackTrace();

        } catch (IOException e) {

            e.printStackTrace();

        }
    }

}

我没有积累足够的积分,所以我无法上传截图给你看输出。

希望这会有所帮助。

【讨论】:

  • 你不需要截图。如果您想显示结果,只需在开头添加几行空格(就像使用代码一样)
【解决方案3】:

我知道 Double 不应该用作货币金额。然而, 这不是我的代码。

确实不应该。 BigDecimal 是存储货币金额的更好选择,因为它无损 并且提供了对小数位的更多控制。

所以对于对代码有控制权的人,可以这样使用:

double amount = 111.222;
setAmount(new BigDecimal(amount).setScale(2, BigDecimal.ROUND_HALF_UP));

这将序列化为111.22。无需自定义序列化程序。

【讨论】:

  • 确实不应该 : tru dat ;)
【解决方案4】:

关于上面所说的,我只是想解决一点问题,这样人们就不会像我一样在这上面浪费时间。应该实际使用

BigDecimal.valueOf(amount).xxx

而不是

new BigDecimal(amount).xxx

这实际上有点关键。因为如果你不这样做,你的十进制金额就会被搞砸。这是浮点表示的限制,如 here 所述。

【讨论】:

    【解决方案5】:

    到目前为止我见过的最好的方法是创建一个自定义的序列化程序和@JsonSerializer(using=NewClass.class)。想尝试@JsonFormat(pattern=".##")左右,但根据OP的一条评论可能无法正常工作(我认为格式化程序不尊重这一点)

    请看这里:https://github.com/FasterXML/jackson-databind/issues/632

    public class MoneyDeserializer extends JsonDeserializer<BigDecimal> {
    
        private NumberDeserializers.BigDecimalDeserializer delegate = NumberDeserializers.BigDecimalDeserializer.instance;
    
        @Override
        public BigDecimal deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            BigDecimal bd = delegate.deserialize(jp, ctxt);
            bd = bd.setScale(2, RoundingMode.HALF_UP);
            return bd;
        }    
    }
    

    但是,虽然更方便且编写的代码更少,但通常,确定字段的规模是业务逻辑的关注点,而不是(反)序列化的一部分。要清楚这一点。杰克逊应该能够按原样传递数据。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-02-18
      • 2018-01-17
      • 1970-01-01
      • 2011-09-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多