对 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 对象的典型格式,您有两个选择:
- 如果可以,请将您的 JSON 表示重构为更详细但更好的解析表示。
- 使用不同的方法来解析现有的 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 java-8,为了可用性而省略):
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;
}
}
我在 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 对象的典型格式,您有两个选择:
- 如果可以,请将您的 JSON 表示重构为更详细但更好的解析表示。
- 使用不同的方法来解析现有的 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
是否为空,并适当地处理它,但这很容易做到。
编辑:
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;
}
}