对 class 建模以表示 JSON 数据的原因是什么,我需要这样做吗?

What is the reasoning on modelling a class to represent JSON data and do I need to?

我在 Whosebug 上遇到了这个问题,它询问 converting JSON to Java. 答案显示另一个 class 被建模来表示 JSON 数据以及正在创建的对象和我不明白为什么。

那个对象现在是包含Gson读取内容后的所有信息还是只有一对key/value?如果它只包含 1 个 key/value 对,我假设我需要为下面的 JSON 创建多个对象,我可以使用循环迭代并将值添加到下拉菜单?

{
    "1": "Annie",
    "2": "Olaf",
    "3": "Galio",
    "4": "TwistedFate",
    "5": "XinZhao",
    "6": "Urgot",
    "7": "Leblanc",
    "8": "Vladimir",
    "9": "FiddleSticks",
    "10": "Kayle",
    "11": "MasterYi",
    "12": "Alistar",
    "13": "Ryze",
    "14": "Sion",
    "15": "Sivir",
    "16": "Soraka",
    "17": "Teemo",
    "18": "Tristana",
    "19": "Warwick",
    "20": "Nunu"
}

基本上我的目标是:

1) 创建一个包含值的名称列表。

2) 按字母顺序对名称列表进行排序(未排序时)

3) 遍历列表并将每个名称添加到下拉菜单中

4) 选择下拉菜单中的名称后,与该值关联的键将传递给另一个 url,后者接收更多数据。

抱歉,如果不清楚。我花了几个小时试图了解如何从 JSON 获取元素并显示它,以及尝试创建一个列表,我可以在其中使用键来显示名称信息但没有运气,除了使用 for-each 循环。

Gson Bean映射方案

好的,对于 JSON 对象,您所拥有的有点不寻常;键(在你的例子中是数字)本质上代表了它们包含的对象的属性。这是可行的,但你必须明白,例如,在 JSON 对象中查找 "Annie" 时,如果使用 Gson 映射到 "bean" class,这我们将调用 Data(如链接示例中所示),然后您必须创建一个数据对象,如下所示:

class Data {
    private String _1;
    // ...
    private String _20;

    public String get1() { return _1; }
    public void set1(String _1) { this._1 = _1; }
    // ...
    public String get20() { return _20; }
    public void set20(String _20) { this._20 = _20; }
}

并且通过在给定字符串上使用 Data data = new Gson().fromJson(myJsonString, Data.class);,您可以通过调用...找到 "Annie"...呃...data.get1()?

显然,这不是好的解决方案。

更好的解决方案

由于您的数据不遵循 JSON 对象的典型格式,您有两个选择:

  1. 如果可以,请将您的 JSON 表示重构为更详细但更好的解析表示。
  2. 使用不同的方法来解析现有的 JSON。

解决方案 1:更改 JSON 表示

重构 JSON 会产生一个(最好)如下所示的对象:

{
    "champions" : [
        {
            "index" : 1,
            "name" : "Annie"
        },
        {
            "index" : 2,
            "name" : "Olaf"
        },
        // ...
    ]
}

这可以很容易地映射到几个如下所示的 bean:

class Data {
    private List<Champion> champions;
    // TODO getters and setters
}
class Champion {
    private int index;
    private String name;
    // TODO getters and setters
}

然而,这给 JSON 对象增加了很多不必要的混乱,并且对于每个冠军只有两个字段(名称和索引)来说并不是真正必要的。

您可以像这样进一步简化它:

{
    "champions" : [
        "Annie",
        "Olaf",
        // ...
    ]
}

那个 bean class 将是:

class Data {
    private List<String> champions;
    // TODO getters and setters
}

更简单,但仍然需要对您得到的 JSON 进行更改,这在某些情况下是不可能的。但是,如果你使用它,你也可以通过以下方式完全摆脱 "bean" class:

List<String> champions = (List<String>) new Gson().fromJson(myJsonString, new TypeToken<List<String>>(){}.getType());

解决方案 2:更改 JSON 的解析方式

可以说更好更简洁的解决方案就是改变 JSON 的解析方式。

这里的目标(如果我理解正确的话)是解析 JSON 并吐出代表每个冠军名字的字符串集合,可通过 [=102= 中冠军的数字索引访问] 代表。

因此,由于 JSON 对象的布局方式是字符串到字符串的简单映射,我们可以使用 Gson 直接通过管道传输到 Map<String, Object>,如下所示:

Map<String, String> mappedValues = new Gson().fromJson(myJsonString, Map.class);
String anniesName = mappedValues.get("1");  // "Annie"
String olafsName = mappedValues.get("2");   // "Olaf"
boolean hasTwentyOneElements = mappedValues.containsKey("21");   // false

这个更短,不需要 "bean" class,并保持原始的 JSON 表示。缺点是您不能轻易判断每个条目的索引是否正确和一致; IE。如果有人输入了错误的号码,或删除了其中一项。

要获取所有键的容器,您只需使用 mappedValues.keySet(),而要获取所有键值对的容器,您可以使用 mappedValues.entrySet(),这会给您一个 Set<Map.Entry<String, String>>.这两个都可以迭代,并且可能是随机顺序(我不确定底层 Map 实现是否保留插入顺序)。

要获取给定名称的索引(即 champ),您将使用类似于以下内容的内容:

String index = null;
for (Map.Entry<String, String> entry : mappedValues.entrySet()) {
    if (champ.equals(entry.getValue())) {
        index = entry.getKey();
        break;
    }
}

当然,您必须在此之后检查 index 是否为空,并适当地处理它,但这很容易做到。

编辑: provides a cleaner, more efficient lookup strategy by means of inverting the map (although the answer is written for Jackson, instead of Gson); an adaptation of this for Gson is as follows (and yes, there is a vastly superior version in ,为了可用性而省略):

public Map<String, String> invertMap(Map<String, String> input) {
    Map<String, String> newMap = new LinkedTreeMap<String, String>(); // TODO Pick optimal storage class
    for (Map.Entry<String, String> entry : input.entrySet()) {
        newMap.put(entry.getValue(), entry.getKey());
    }
    return newMap;
}

// ...

Map<String, String> mappedValues = invertMap(new Gson().fromJson(myJsonString, Map.class));
String annieIndex = mappedValues.get("Annie");   // "1"
String olafIndex = mappedValues.get("Olaf");     // "2"

值得注意的是,这通过有效地构建地图两次(一次由 Gson 和一次反转)牺牲了构建地图的效率,但它使值查找效率更高。

让我们使用 Jackson 的功能,它允许您将任何 属性 映射到单个方法(我相信您在这里并不真的需要 getter)。只需交换这个通用 setter 中的键和值,然后添加到一个已经按键(名称)排序的 TreeMap 中。然后你可以按字母顺序输出键(名称)并轻松通过名称获得ID。

public static void main(String[] args) throws IOException {

    String json = "....."; // your JSON string here

    com.fasterxml.jackson.databind.ObjectMapper mapper = 
        new com.fasterxml.jackson.databind.ObjectMapper();
    ReverseMap pairs = mapper.readValue(json, ReverseMap.class);
    for (Map.Entry<Object, String> entry : pairs.getValues().entrySet()) {
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
}

public class ReverseMap {

    private TreeMap<Object, String> mapping = new TreeMap<>();

    @com.fasterxml.jackson.annotation.JsonAnySetter
    public void add(String name, Object value) {
        mapping.put(value, name);
    }

    public Map<Object, String> getValues() {
        return mapping;
    }
}