How do I return a limited number of cached instances in Java?

java enums singleton factory-pattern

106 просмотра

4 ответа

626 Репутация автора

I have a "configuration" class that becomes a field of several other classes. It indicates some kind of configuration or "abilities" of those other classes to allow or disallow actions. The configuration class as of now contains a set of four independent booleans and will likely remain like that --or grow with another bolean--. The configuration is immutable: once the object is created, the configuration will never change.

public class Configuration {
    private final boolean abilityOne;
    private final boolean abilityTwo;
    private final boolean abilityThree;
    private final boolean abilityFour;

    public Configuration (final boolean abilityOne, final boolean abilityTwo,
                          final boolean abilityThree, final boolean abilityFour) {
    this.configuration = ((1 * (abilityOne ? 1 : 0)) +
            (2 * (abilityTwo ? 1 : 0)) +
            (4 * (abilityThree ? 1 : 0)) +
            (8 * (abilityFour ? 1 : 0)));
 }

    public boolean isAbilityOne() {
        return((1 & this.configuration) > 0);
    }

    public boolean isAbilityTwo() {
        return((2 & this.configuration) > 0);
    }

    public boolean isAbilityThree() {
        return((4 & this.configuration) > 0);
    }

    public boolean isAbilityFour() {
        return((8 & this.configuration) > 0);
    }
}

Because of C / limited-hardware background, my next implementation (attempt at reducing memory footprint) was with an int used as a bit map: 1 -> first boolean, 2-> second, 4 -> third, 8-> fourth. This way I store an integer and the boolean functions I needed were like:

It works fine and it is quite memory efficient. But it is frowned upon by my Java-all-my-life colleagues.

The number of different configurations is limited (the combinations of boolean values), but the number of objects using them is very large. In order to decrease memory consumption I thought of some kind of "multi-singleton", enumeration or cached instances. And this is where I am now. What is best?

Автор: manuelvigarcia Источник Размещён: 19.07.2016 06:58

Ответы (4)


1 плюс

336 Репутация автора

I would suggest the following, it is very easy to expand as you just have to add another Ability to your enum.

enum Ability {
    Ability1, Ability2, Ability3, Ability4
}

public class Configuration {

   private static LoadingCache<Set<Ability>, Configuration> cache = CacheBuilder.newBuilder()
        .build(new CacheLoader<Set<Ability>, Configuration>() {
            @Override
            public Configuration load(Set<Ability> withAbilities) {
                return new Configuration(withAbilities);
            }

        });

    Set<Ability> abilities;

    private Configuration(Collection<Ability> withAbilities) {
        this.abilities = createAbilitySet(withAbilities);
    }

    public static Configuration create(Ability... withAbilities) {
        Set<Ability> searchedAbilities = createAbilitySet(Arrays.asList(withAbilities));
        try {
            return cache.get(searchedAbilities);
        } catch (ExecutionException e) {
            Throwables.propagateIfPossible(e);
            throw new IllegalStateException();
        }
    }

    private static Set<Ability> createAbilitySet(Collection<Ability> fromAbilities) {
        if (fromAbilities.size() == 0) {
            return Collections.emptySet();
        } else {
           return EnumSet.copyOf(fromAbilities);
        }
    }

    public boolean hasAbility(Ability ability) {
       return abilities.contains(ability);
    }
}
Автор: garnulf Размещён: 19.07.2016 07:33

1 плюс

637 Репутация автора

Решение

I think multiton pattern is the most efficient way to do this:

public class Configuration {

    private static Map<Long, Configuration> configurations = new HashMap<>();

    private long key;
    private long value;

    public static Configuration getInstanse(long key, boolean... configs) {
        if (configurations.containsKey(key)) {
            return configurations.get(key).setConfigs(configs);
        }
        Configuration configuration = new Configuration(key, configs);
        configurations.put(key, configuration);
        return configuration;
    }

    // Max number of configs.length is 64
    private Configuration(long key, boolean... configs) {
        this.key = key;
        setConfigs(configs);
    }

    private Configuration setConfigs(boolean[] configs) {
        this.value = 0L;
        boolean config;
        for (int i = 0; i < configs.length; i++) {
            config = configs[i];
            this.value = this.value | (config ? (1L << i) : 0L);
        }
    }

    public long getKey() {
        return key;
    }

    public boolean getConfig(int place) {
        return (value & (1L << place)) == (1L << place);
    }
}
Автор: hadilq Размещён: 19.07.2016 08:05

0 плюса

90574 Репутация автора

If the configuration implementation objects are small and not expensive to create, there is no need to cache them. Because each monster object will have to keep a reference to each of its configurations, and at machine level a reference is a pointer and uses at least the same memory as an int.

The EnumSet way proposed by @gamulf can probably be used as it without any caching, because according to EnumSet javadoc:

Enum sets are represented internally as bit vectors. This representation is extremely compact and efficient. The space and time performance of this class should be good enough to allow its use as a high-quality, typesafe alternative to traditional int-based "bit flags."

I did not benchmarked it, but caching is likely to be useless with @gamulf's solution because a Configuration object contains only an EnumSet that contains no more than an int.

If you had a heavy configuration class (in term of memory or expensive to create) and only a small number of possible configurations, you could use a static HashSet member in the class, and a static factory method that would return the cached object:

public class Configuration {
    static Set<Configuration > confs = new HashSet<>();
    ...

    public Configuration (Ability ... abs) {
        ...
    }

    public boolean hasAbility(Ability ab) {
        ...
    }

    static Configuration getConfiguration(Ability ... abs) {
        for (ConfImpl2 conf: confs) {
            if (conf.isSame(abs)) { return conf; }
        }
        ConfImpl2 conf = new ConfImpl2(abs);
        confs.add(conf);
        return conf;
    }
    private boolean isSame(Ability ... abs) {
        // ensures that this configuration has all the required abilities and only them
        ...
    }
}

But as I have already said, that is likely to be useless for objects as lightweight as those proposed by @gamulf

Автор: Serge Ballesta Размещён: 19.07.2016 10:13

0 плюса

626 Репутация автора

I want to share the investigation I made based on your answers, so I'm posting one answer with those results. This way it might be clearer why I choose one answer over other.

The bare result rank are as follows (memory used for 600 "monster" objects, 10% of what will be needed):

  1. trivial option: Class with four booleans inside: 22.200.040
  2. Initial option: Class with one integer as map of bits: 22.200.040
  3. "multiton" option: one factory class that returns references to the trivial option's Class: 4.440.040
  4. EnumSet (without guava cache): 53.401.896 (in this one I probably messed up, since results are not as expected... I might work further on this later on)
  5. EnumSet with guava cache: 4.440.040

Since my tests run first a series of comparisons to ensure that all implementations give the exact same results for all configurations, it has become clear that the 4.440.040 number is the size of the List<> I used to hold the items, for before I resolved to set it to null before measuring memory, those numbers were consistently 0.

Please don't go into how I measured memory consumption (gc(); freeMemory(); before and after I freed each list and set it to null), since I used the same method for all, and performed 20 executions each time and in different orders of execution. Results were consistent enough for me.

These results point at the multiton solution as the easiest of the best performing. That's why I set it as the selected answer.

As side note/curiosity, please be informed that the project for which this investigation started has selected the trivial option as the solution and most of this investigation was made to satisfy my own curiosity --and with some hidden desire to be able to demonstrate that some other solution would be soooo much more efficient than the trivial one... but no--. This is why it took me so long to come up with a conclusion.

Автор: manuelvigarcia Размещён: 28.07.2016 07:38
Вопросы из категории :
32x32