4

I have a JSON that I convert into POJOs. The JSON is read from a GZIPInputStream gis.

ObjectMapper mapper = new ObjectMapper();

TypeReference<Map<Long, ConfigMasterAirportData>> typeRef =
                new TypeReference<Map<Long, ConfigMasterAirportData>>() {};

Map<Long, ConfigMasterAirportData> configMasterAirportMap = 
                mapper.readValue(gis, typeRef);

I do not want new Long objects to be created for each entry. I want it to get Long objects from a custom LongPool I have created. Is there a way to pass such a LongPool to the mapper?

If not, is there another JSON library I can use to do that?

Devstr
  • 4,431
  • 1
  • 18
  • 30
Pranav Kapoor
  • 1,171
  • 3
  • 13
  • 32
  • Why do you want this? How many entries you have in that map? Sounds like premature optimisation to me. – Devstr Feb 20 '18 at 11:18
  • The long ids are used in multiple maps - about 25 maps. This is one of the maps. We are using Long Pools to optimize space. We have around 200K entries in the map. – Pranav Kapoor Feb 20 '18 at 13:07
  • 1
    So 25 maps by 200k entries by 16 bytes (size of Long) is 80 megabytes. This is maximum amount you can save by pooling. Is saving 80 megabytes critical for your app? Please consider the cost of maintaining that code. – Devstr Feb 20 '18 at 13:51
  • @Devstr Yes, thats about 10% of our memory footprint. – Pranav Kapoor Feb 21 '18 at 06:58
  • Cool then, I'll post a solution bit later. I suggest to use wrapper object for your map and construct it via anysetter – Devstr Feb 21 '18 at 08:47
  • Do you use Guava in your project? It has interners already implemented – Devstr Feb 21 '18 at 08:49
  • Alternatively you can use trove library to avoid using long objects entirely https://github.com/palantir/trove-3.0.3/blob/master/README.md – Devstr Feb 21 '18 at 08:58
  • And there seem to be a Jackson serializers and deserialisers for it https://bitbucket.org/marshallpierce/jackson-datatype-trove/src/d7386afab0eece6f34a0af69b76b478f417f0bd4/src/main/java/com/palominolabs/jackson/datatype/trove/deser/?at=master I'll write a proper answer I guess – Devstr Feb 21 '18 at 09:05
  • We dont use Guava in our code expect for the Guava BiMap. It looks cool though. We'll try to use it more in our codebase – Pranav Kapoor Feb 23 '18 at 15:46

2 Answers2

4

There are many ways to achieve this if you are sure that object pooling is required in your case.

First of all, Java already does Long object pooling for a small range between -128 and 127 inclusive. See source code of Long.valueOf.

Let us have 2 JSON objects that we want to deserialize: map1 and map2:

    final String map1 = "{\"1\": \"Hello\", \"10000000\": \"world!\"}";
    final String map2 = "{\"1\": \"You\", \"10000000\": \"rock!\"}";

Standard deserialization

If we use standard deserialization:

    final ObjectMapper mapper = new ObjectMapper();
    final TypeReference<Map<Long, String>> typeRef = new TypeReference<Map<Long, String>>() {};
    final Map<Long, String> deserializedMap1 = mapper.readValue(map1, typeRef);
    final Map<Long, String> deserializedMap2 = mapper.readValue(map2, typeRef);

    printMap(deserializedMap1);
    printMap(deserializedMap2);

Where printMap is defined as

private static void printMap(Map<Long, String> longStringMap) {
    longStringMap.forEach((Long k, String v) -> {
        System.out.printf("key object id %d \t %s -> %s %n", System.identityHashCode(k), k, v);
    });
}

we get the following output:

key object id 1635756693     1 -> Hello 
key object id 504527234      10000000 -> world! 
key object id 1635756693     1 -> You 
key object id 101478235      10000000 -> rock! 

Note that key 1 is the same object with hashcode 1635756693 in both maps. This is due to built-in pool for [-128,127] range.

Solution1: @JsonAnySetter deserialization

We can define a wrapper object for the map and use @JsonAnySetter annotation to intercept all key-value pairs being deserialized. Then we can intern each Long object using Guava StrongInterner:

static class CustomLongPoolingMap {
    private static final Interner<Long> LONG_POOL = Interners.newStrongInterner();
    private final Map<Long, String> map = new HashMap<>();

    @JsonAnySetter
    public void addEntry(String key, String value) {
        map.put(LONG_POOL.intern(Long.parseLong(key)), value);
    }

    public Map<Long, String> getMap() {
        return map;
    }
}

We will use it like this:

    final ObjectMapper mapper = new ObjectMapper();
    final Map<Long, String> deserializedMap1 = mapper.readValue(map1, CustomLongPoolingMap.class).getMap();
    final Map<Long, String> deserializedMap2 = mapper.readValue(map2, CustomLongPoolingMap.class).getMap();

Output:

key object id 1635756693     1 -> Hello 
key object id 1596467899     10000000 -> world! 
key object id 1635756693     1 -> You 
key object id 1596467899     10000000 -> rock! 

Now you can see that key 10000000 is also the same object in both maps with hashcode 1596467899

Solution 2: Register custom KeyDeserializer

Define custom KeySerializer:

public static class MyCustomKeyDeserializer extends KeyDeserializer {
    private static final Interner<Long> LONG_POOL = Interners.newStrongInterner();
    @Override
    public Long deserializeKey(String key, DeserializationContext ctxt) {
        return LONG_POOL.intern(Long.parseLong(key));
    }
}

And register it with the ObjectMapper:

    final SimpleModule module = new SimpleModule();
    module.addKeyDeserializer(Long.class, new MyCustomKeyDeserializer());
    final ObjectMapper mapper = new ObjectMapper().registerModule(module);
    final TypeReference<Map<Long, String>> typeRef = new TypeReference<Map<Long, String>>() {};
    final Map<Long, String> deserializedMap1 = mapper.readValue(map1, typeRef);
    final Map<Long, String> deserializedMap2 = mapper.readValue(map2, typeRef);

Solution 3: Use custom KeyDeserializer via @JsonDeserialize annotation

Define a wrapper object

static class MapWrapper {
    @JsonDeserialize(keyUsing = MyCustomKeyDeserializer.class)
    private Map<Long, String> map1;
    @JsonDeserialize(keyUsing = MyCustomKeyDeserializer.class)
    private Map<Long, String> map2;
}

And deserialize it:

    final ObjectMapper mapper = new ObjectMapper();
    final String json = "{\"map1\": " + map1 + ", \"map2\": " + map2 + "}";
    final MapWrapper wrapper = mapper.readValue(json, MapWrapper.class);
    final Map<Long, String> deserializedMap1 = wrapper.map1;
    final Map<Long, String> deserializedMap2 = wrapper.map2;

Solution 4: Use Trove library TLongObjectMap to avoid using Long objects entirely

Trove library implements maps that use primitive types for keys to remove overhead of boxed objects entirely. It's in somewhat dormant state however.

You need in your case TLongObjectHashMap.

There is a library that defines a deserializer for TIntObjectMap: https://bitbucket.org/marshallpierce/jackson-datatype-trove/src/d7386afab0eece6f34a0af69b76b478f417f0bd4/src/main/java/com/palominolabs/jackson/datatype/trove/deser/TIntObjectMapDeserializer.java?at=master&fileviewer=file-view-default

I think it will be quite easy to adapt it for TLongObjectMap.


Full code for this answer can be found here: https://gist.github.com/shtratos/f0a81515d19b858dafb71e86b62cb474

I've used answers to this question for solutions 2 & 3: Deserializing non-string map keys with Jackson

Devstr
  • 4,431
  • 1
  • 18
  • 30
0

Not sure about Jackson library, but with Google Gson you can quite simply do it by registering a custom type adapter whose responsibility is to resolve every key the way you want it:

public class DeserializeJsonMapWithCustomKeyResolver {

    public static void main(String[] args) {
        final String JSON = "{ \"1\" : { \"value\" :1 }, \"2\" : { \"value\" : 2} }";
        final Type mapType = new TypeToken<Map<Long, ConfigMasterAirportData>>() {}.getType();
        final Map<String, ConfigMasterAirportData> map =
            new GsonBuilder().registerTypeAdapter(mapToken, new PooledLongKeyDeserializer())
                .create()
                .fromJson(JSON, mapType);
        System.out.println(map);
    }

    static Long longFromString(String value)
    {
        System.out.println("Resolving value : " + value);
        // TODO: replace with your LongPool call here instead; may need to convert from String
        return Long.valueOf(value);
    }

    static class PooledLongKeyDeserializer implements
        JsonDeserializer<Map<Long, ConfigMasterAirportData>>
    {
        @Override
        public Map<Long, ConfigMasterAirportData> deserialize(
            JsonElement json,
            Type typeOfT,
            JsonDeserializationContext context)
            throws JsonParseException
        {
            final Map<Long, ConfigMasterAirportData> map = json.getAsJsonObject()
                .entrySet()
                .stream()
                .collect(
                    Collectors.toMap(
                        e -> longFromString(e.getKey()),
                        e -> context.deserialize(e.getValue(),
                            TypeToken.get(ConfigMasterAirportData.class).getType())
                    ));
            return map;
        }
    }

    static class ConfigMasterAirportData {
        public int value;

        @Override
        public String toString() { return "ConfigMasterAirportData{value=" + value + '}'; }
    }
}
yegodm
  • 1,014
  • 7
  • 15