I am writing some code to serialize a neural network system I have developed. This system has a "database" that keeps track of the evolution of the neural networks, and it does so by storing the ID of each gene in a HashMap with a GeneKey, which is a record containing the ID of the gene before and the ID of the gene after the gene we're storing.
A HashMap with some data looks like this:
existingNodes = {
GeneKey[a=0, b=3] = 4,
GeneKey[a=1, b=4] = 5
}
Everything in the system serializes fine, except this HashMap, because Json can only have numbers and strings as its keys, and in my HashMap I'm using objects for the keys. Is there an easy way to serialize this to json using Gson?
Edit: This is how the HashMap is constructed:
HashMap<GeneKey, Integer> existingNodes = new HashMap<>();
existingNodes.put(new GeneKey(0, 3), 4);
existingNodes.put(new GeneKey(1, 4), 5);
System.out.println("existingNodes = "+registry);
//existingNodes = {
// GeneKey[a=0, b=3] = 4,
// GeneKey[a=1, b=4] = 5
//}
This is the GeneKey class:
public record GeneKey(int a, int b) {}
It would be helpful if you could give the JSON string which you got after serializing using Gson.
However, please check the following code if this solves your issue.
Define GenKey class with overriden hashCode and equals methods as:
public class GeneKey {
private int a;
private int b;
public GeneKey(int a, int b) {
this.a = a;
this.b = b;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public int getB() {
return b;
}
public void setB(int b) {
this.b = b;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + a;
result = prime * result + b;
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
GeneKey other = (GeneKey) obj;
if (a != other.a)
return false;
if (b != other.b)
return false;
return true;
}
#Override
public String toString() {
//return "GeneKey [a=" + a + ", b=" + b + "]";
// Updated for deserialization using Gson
Gson gson = new
GsonBuilder().serializeNulls().disableHtmlEscaping().create();
return gson.toJson(this);
}
}
Now try to convert the HashMap with GenKey as key into JSON String using Gson :
HashMap<GeneKey, Integer> existingNodes = new HashMap<>();
existingNodes.put(new GeneKey(0, 3), 4);
existingNodes.put(new GeneKey(1, 4), 5);
Gson gson = new
GsonBuilder().serializeNulls().disableHtmlEscaping().create();
System.out.println(gson.toJson(existingNodes));
This is the output which I received in my console:
{"{\"a\":0,\"b\":3}":4,"{\"a\":1,\"b\":4}":5}
Updating the answer to add deserialization logic if required:
//Say, we pass the serialized JSON as a payload to some REST API. We can deserialize the Key of the response as follows -
#PostMapping("/getJson")
public Map<GeneKey, Integer> getJson(#RequestBody Map<String, Integer> response) {
final Gson gson = new Gson();
Map<GeneKey, Integer> deserializedMap = new HashMap<GeneKey,
Integer>();
response.keySet().forEach( k -> {
GeneKey key = gson.fromJson(k, GeneKey.class);
deserializedMap.put(key, response.get(k));
});
System.out.println(deserializedMap);
return deserializedMap;
}
GsonBuilder#enableComplexMapKeySerialization
However, now the map is represented by an array of array of entries.
see Gson Serializing HashMap<Teacher, List<Student>>
Related
I have two JSON strings which I want to compare.
I want neither the order of the keys to matter or the order of elements in an array.
However I do want an extra field to be considered "not equal"
Non strict mode with JSONAssert seems like it fits the bill except for an extra field being considered equal "http://jsonassert.skyscreamer.org/cookbook.html"
If at all possible I would like to avoid pulling in extra dependancies. I already have jackson in my project
I have 2 ideas how to do it.
Is to write java objects and serialize it, and write own equals method.
Is to serialize it to Map<Object, Object> and compare 2 map.
String json1 = "{...}"
String json2= "{...}"
Object json1Object = objectMapper.readValue(json1, Object.class);
Object json2Object = objectMapper.readValue(json2, Object.class);
Assertions.assertEquals(json1Object, json2Object);
Assertions.assertTrue(json1Object.equals(json2Object));
So you probably have only one option. Write own comparator.
My quick solution:
#Test
public void comparingJsonTest4() throws JsonProcessingException {
String json1 = "{\"id\": 1, \"name\": \"test\", \"cars\": [\"Ford\", \"BMW\", \"Fiat\"]}";
String json2 = "{\"name\": \"test\", \"id\": 1, \"cars\": [\"BMW\", \"Ford\", \"Fiat\"]}";
ObjectMapper objectMapper = new ObjectMapper();
JsonNode json1Node = objectMapper.readTree(json1);
JsonNode json2Node = objectMapper.readTree(json2);
Assertions.assertEquals(0, new ComparatorWithoutOrder().compare(json1Node, json2Node));
}
class ComparatorWithoutOrder implements Comparator<JsonNode> {
#Override
public int compare(JsonNode o1, JsonNode o2) {
if(o1 == o2) {
return 0;
}
if(o1.getClass() != o2.getClass()) {
return -1;
}
if(o1.getClass() == ObjectNode.class) {
List<String> o1FieldNames = new ArrayList<>();
o1.fieldNames().forEachRemaining(o1FieldNames::add);
List<String> o2FieldNames = new ArrayList<>();
o2.fieldNames().forEachRemaining(o2FieldNames::add);
if(o1FieldNames.size() != o2FieldNames.size()) {
return -1;
}
if(!o2FieldNames.containsAll(o1FieldNames) || !o1FieldNames.containsAll(o2FieldNames)) {
return -1;
}
for (String o1FieldName : o1FieldNames) {
if (!(compare(o1.get(o1FieldName), o2.get(o1FieldName)) == 0)) {
return -1;
}
}
return 0;
}
if(o1.getClass() == ArrayNode.class) {
List<JsonNode> o1Children = new ArrayList<>();
o1.elements().forEachRemaining(o1Children::add);
List<JsonNode> o2Children = new ArrayList<>();
o2.elements().forEachRemaining(o2Children::add);
if(o1Children.size() != o2Children.size()) {
return -1;
}
for (JsonNode c1 : o1Children) {
boolean found = false;
for (JsonNode c2 : o2Children) {
if (compare(c1, c2) == 0) {
found = true;
break;
}
}
if (!found) {
return -1;
}
}
return 0;
}
return o1.equals(o2) ? 0 : -1;
}
}
At the beginning I wanted to use something like this:
json1Node.equals(new ComparatorWithoutOrder(), json2Node);
but thre was a problem to propoer handle ArrayNode inside ObjectNode. So if you want, you could skip implements Comparator<JsonNode>, because finally I don't use this functionality.
Say I have a list of objects. I want to group them into a list of lists where each inner list contains elements for which a boolean comparison function returns true:
public class VO {
public VO(int age, int val) {
this.age = age;
this.val = val;
}
public int age;
public int val;
}
public void testGrouping() {
// Equal to vo2
VO vo1 = new VO(1, 100);
// Equal to vo1 and vo3
VO vo2 = new VO(3, 105);
// Equal to vo2 but not vo1 (age difference > 2),
// so it belongs into a new bucket
VO vo3 = new VO(5, 110);
// Equal to vo3, so it belongs into the same bucket as vo3
VO vo4 = new VO(7, 116);
List<VO> values = Arrays.asList(vo1, vo2, vo3, vo4);
//Group values using isEqual(VO, VO) into buckets somehow
//Any two values in a bucket must pass the check
//Expected without any specific order:
//[[vo1, vo2, [vo3, vo4]]
}
private boolean isEqual(VO a, VO b) {
return Math.abs(a.age - b.age) <= 2 && Math.abs(a.val - b.val) <= 10;
}
This is just a simplified example of the data that I have, in reality the comparison method is more complicated than that. Important is that each object must match each other object in its bucket regarding the check. The objects cannot be grouped/mapped by a specific value.
I already have code which does this but takes three levels of for-loops and which took me about a day to write. I'm curious if this can be achieved easier with streams.
Try mapping your items first, then use Collectors.groupingBy():
values.stream().map(name -> name.split("\\s+")).collect(groupingBy(a -> a[1]));
This does not return lists but arrays - but the concept is the same.
You can modify a bit the isEqual method so that you can use it as a comparator for the TreeMap. Then you can collect the TreeMap<VO, List<VO>> as follows. This code works in Java 7:
public static int isEqual(VO a, VO b) {
if (Math.abs(a.age - b.age) > 2 || Math.abs(a.val - b.val) > 10)
return 1;
else if (Math.abs(a.age - b.age) <= 2 && Math.abs(a.val - b.val) <= 10)
return 0;
else
return -1;
}
public static void main(String[] args) {
VO vo1 = new VO(1, 100);
VO vo2 = new VO(3, 105);
VO vo3 = new VO(5, 110);
VO vo4 = new VO(7, 116);
List<VO> values = Arrays.asList(vo1, vo2, vo3, vo4);
Map<VO, List<VO>> buckets = new TreeMap<>(new Comparator<VO>() {
#Override
public int compare(VO o1, VO o2) {
return isEqual(o1, o2);
}
});
for (VO vo : values) {
List<VO> list = buckets.get(vo);
if (list == null) {
list = new ArrayList<>();
}
list.add(vo);
buckets.put(vo, list);
}
// output
System.out.println(buckets);
//{VO{1=100}=[VO{1=100}, VO{3=105}], VO{5=110}=[VO{5=110}, VO{7=116}]}
}
public static class VO {
public int age, val;
public VO(int age, int val) {
this.age = age;
this.val = val;
}
#Override
public String toString() {
return "VO{" + age + "=" + val + "}";
}
}
Suppose I have this array:
double[][] Q = new double[n1][n2];
I could index the values of Q by using int indexes, such as Q[2][1]. But in my case, n1 is a byte[], not an int. I still know the possible values of n1 (e.g, all possible combinations of the array values). What collection should I use instead of an array?
HashMap<byte[], Double>[] Q = new HashMap[n2];
This was my solution, but I'm not sure it is adequate. To index, I can do
byte[] n1 = {1,0,6,1,4,2,5,1};
Q[1].get(n1);
Is there a better way to do this? Something that is more performant? I think having an array of HashMaps is not ideal, but can I add that int to my key? How?
As stated in the comments,
The problem with using an array as a hashmap key (aside from the fact that it's mutable) is that arrays don't calculate their hashcode based on their contents; so you couldn't actually look up a value in the map unless you have the actual key instance.
So how do I use an array as an indexing key? A stupid solution would be to always convert it to String before using it, but I'm sure there are better and proper solutions.
I think the best way to go for your problem is to define a complex key class, which basically consists of your byte[] and int, write reliable hashCode and equals-Methods for this class, and use it as a key to the HashMap.
This way, you can use the runtime and memory efficiency of a hashmap and encapsulate the complexity of the key in a separate class.
Code to illustrate:
public static class MyKey {
private int i;
private byte[] b;
private MyKey(int i, byte[] b) {
this.i = i;
this.b = b;
}
public static MyKey of(int i, byte ... b) {
return new MyKey(i, b);
}
#Override
public int hashCode() { // Autogenerated from eclipse
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(b);
result = prime * result + i;
return result;
}
#Override
public boolean equals(Object obj) { // Autogenerated from eclipse
if(this == obj)
return true;
if(obj == null)
return false;
if(getClass() != obj.getClass())
return false;
MyKey other = (MyKey) obj;
if(!Arrays.equals(b, other.b))
return false;
if(i != other.i)
return false;
return true;
}
}
public static void main(String[] args) {
HashMap<MyKey, Double> valueMap = new HashMap<>();
valueMap.put(MyKey.of(1, (byte)2, (byte)4, (byte)7), 0.1);
valueMap.put(MyKey.of(1, new byte[] { 3, 8, 14 }), 0.1);
}
This could be the entirely wrong usage for the datatype, but I'm trying to create a Map-Set of some sort, so before insertion into the map I'm trying to use
//Material is custom class
Map<Integer, Material> x = TreeMap();
if(!x.containsValue(new Material))
x.put(int val, new Material);
My expectation of this is that it would compare two objects generated from the same data would return true, but it appears that this is not the case. Why is that, and is there an existing solution other than iterating through the entire map to find out if any element contains a Material where all fields are the same as the incoming?
Having a well defined class Material
public class Material extends Comparable<Material> {
final int a;
final String b;
...
public Material(int a, String b, ...) {
this.a = a;
this.b = b;
}
#Override
int compareTo(Material other) {
int cmp = Integer.compare(a, other.a);
if (cmp == 0) {
cmp = b.compareTo(other.b);
}
...
return cmp;
}
#Override
public boolean equals(Object other) {
if (other == null || !(other instanceof Material)) {
return false;
}
return compareTo((Material) other) == 0;
}
#Override int hashCode() {
return ...;
}
A Set suffices:
Set<Material> materials = new HashSet<>(); // hashCode+equals
Set<Material> materials = new TreeSet<>(); // compareTo
Now I have made the fields of Material final so the object is immutable, as changing would play havoc with the sets ordering.
For a mutable quality, like quantity:
Map<Material, Integer> stock = new HashMap<>();
Material material = new Material(...);
stock.put(material, 100);
int quantity = stock.getOrDefault(material, 0); // Java 8
int quantity = 0; // Java 7
{
Integer q = stock.get(material);
if (q != null) {
quantity = q;
}
}
I have developed a garbage collector friendly String cache for my Android game. Its purpose is to handle Strings for ints. I made a silly mistake implementing it but the bug never disclosed itself in desktop. In Android, however, the cache started returning funny Strings at once:
class IntStringCache {
private final Map<IntStringCache.IntCacheKey, String> cachedStrings = new HashMap<IntStringCache.IntCacheKey, String>();
private final IntCacheKey tempIntCacheKey = new IntCacheKey(0);
public String getStringFor(int i) {
tempIntCacheKey.setIntValue(i);
String stringValue = cachedStrings.get(tempIntCacheKey);
if (stringValue == null) {
stringValue = String.valueOf(i);
// ERROR - putting the same object instead of new IntCachKey(i)
cachedStrings.put(tempIntCacheKey, stringValue);
}
return stringValue;
}
public int getSize() {
return cachedStrings.size();
}
private class IntCacheKey {
private int intValue;
private IntCacheKey(int intValue) {
this.intValue = intValue;
}
private void setIntValue(int intValue) {
this.intValue = intValue;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result + intValue;
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
IntCacheKey other = (IntCacheKey) obj;
if (!getOuterType().equals(other.getOuterType()))
return false;
if (intValue != other.intValue)
return false;
return true;
}
private IntStringCache getOuterType() {
return IntStringCache.this;
}
}
And the tests all of which pass:
public class IntStringCacheTest {
private IntStringCache intStringCache = new IntStringCache();
#Test
public void shouldCacheString() {
// given
int i = 1;
// when
String s1 = intStringCache.getStringFor(i);
String s2 = intStringCache.getStringFor(i);
// then
assertThat(s1).isNotNull();
assertThat(s1).isEqualTo(String.valueOf(i));
assertThat(s1).isSameAs(s2);
}
#Test
public void shouldCacheTwoValues() {
// given
int i1 = 1;
int i2 = 2;
int expectedCacheSize = 2;
// when
String s1 = intStringCache.getStringFor(i1);
String s2 = intStringCache.getStringFor(i2);
// then
assertThat(intStringCache.getSize()).isEqualTo(expectedCacheSize);
assertThat(s1).isSameAs(intStringCache.getStringFor(i1));
assertThat(s2).isSameAs(intStringCache.getStringFor(i2));
}
}
Note:
assertThat(String.valueOf(1)).isSameAs(String.valueOf(1));
fails.
The fact that the second test passes is interesting as, with the bug, there should be one key in the map that gets updated. This may be explained with hashCode() that could make the same key go into two different buckets inside HashMap. But how is it possible that the same key (even if in two buckets) returns the same two Stings? It seems that even though there is a bug in the code the HashMap does the job correctly.
My Android Java implementation, on the other hand, returns wrong number Strings with this bug at once.
You should consider replacing this entire class with SparseArray or its Support Library equivalent SparseArrayCompat (if you need it on <3.0 devices) as they are specifically designed to map integers to objects in a memory efficient way.