在Java中如何返回有限数量的缓存实例?

3

我有一个“配置”类,它成为其他几个类的字段。它表示这些其他类允许或禁止操作的某种配置或“能力”。目前,配置类包含一组四个独立的布尔值,并且可能会保持这样--或者增加另一个布尔值。配置是不可变的:一旦创建了对象,配置就永远不会改变。

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);
    }
}

因为我的C/硬件限制背景,我下一步的实现(试图减少内存占用)将使用一个int作为位图:1->第一个布尔值,2->第二个,4->第三个,8->第四个。这样,我只需存储一个整数,所需的布尔函数如下:
它工作得很好,而且非常节省内存。但是我的同事们(他们一直使用Java)对此不太赞同。
不同配置的数量有限(布尔值的组合),但使用它们的对象数量非常大。为了降低内存消耗,我考虑了一些"多个单例"、枚举或缓存实例的方式。现在问题是什么方法最好?

1
首先,你应该更加关注好的设计而不是“内存效率”。在没有真正问题的情况下优化代码往往会导致低劣的代码。在你的情况下,仅仅将配置表示为一组4个布尔值的想法就听起来有点奇怪。使用一个由4个布尔值组成的列表有什么问题吗? - GhostCat
这些对象创建起来贵吗?如果是,使用枚举。如果不是,只需创建允许创建多个实例,并覆盖equals/hashCode方法。 - Andy Turner
"在我一直和Java打交道的同事中,这被他们不赞成。请他们解释一下他们的顾虑。" - Andy Turner
顺便说一句,如果你是Java新手,我强烈推荐阅读Josh Bloch的《Effective Java第二版》- 这可能会帮助你理解同事们使用的习惯用法。特别是,第32条对这个问题很相关。 - Andy Turner
@Jägermeister 对的。但我们必须在RAM中加载约6K个对象才能运行,目前我们只能在分配给JVM的4GB中容纳约3K个对象。这当然是一个真正的问题。当然,我的_one-int-implementation_只能多装10%的RAM对象,因此改进并不大。 - manuelvigarcia
@AndyTurner 配置对象的创建成本并不高:它们只需要保存四个布尔值。但是,每个我们需要处理的“怪物”对象大约有四个配置对象,而我们正在讨论6K个对象;因此,将24K个配置对象减少到仅16个是一些改进。 他们皱眉表示:太像C语言,过于复杂,只是为了获取真/假。 - manuelvigarcia
4个回答

1
我建议采用以下方法,这很容易扩展,只需将另一个能力添加到您的枚举中即可。
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);
    }
}

Java 7 受限。 - manuelvigarcia
  1. 使用 EnumSet 而不是 HashSet 来保存枚举实例:它被设计成可以通过 Enum 实例的 ordinal 属性来使 contains(等等)操作更加便宜;
  2. 在 Java 8 之前,使用 EnumSet.copyOf(Arrays.asList(configuredAbilities)) 来完成此操作(实际上,您还需要检查参数列表为空的情况)。
- Andy Turner
这并没有真正回答问题:“如何在Java中返回有限数量的缓存实例?”。这只是展示了如何定义实例类。 - Andy Turner
编辑了 Andy Turner 的评论并适用于 Java 7。 - garnulf
我在这里看到了一些线索...有些东西是我可以使用的。但是,我仍然需要为每个配置获取一个Configuration类的实例。由于我得到的能力被分解成布尔值,所以将它们转换为Set并不像使用Arrays.asList那样容易。 - manuelvigarcia
哇!!这变化很大啊。我没看到有guava版本。我试了一下提供的选项比较,效果不太好。我会尽快发布结果,包括这个版本。 - manuelvigarcia

1

我认为多例模式是最有效的方法来实现这个:

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);
    }
}

这并没有真正回答问题:“如何在Java中返回有限数量的缓存实例?”。这只是展示了如何定义实例类。 - Andy Turner
通过 getConfig 方法,您只需要知道您将布尔值放置在配置数组中的位置,然后将该位置提供给 getConfig 方法并获取您刚刚设置的配置即可。 - hadilq
但是每个你想配置的对象仍然有一个 Configuration 类的实例,对吗? - manuelvigarcia
@manuelvigarcia 是的。我编辑了我的答案以更好地处理它。这是你心中想要的吗? - hadilq
那个方面的东西。现在只会创建有限数量的实例,我认为可以放弃位图映射并使其更易读“对于所有读者”。 - manuelvigarcia
什么行?!这就是多例模式应该的样子。它们是有限制的,因为想要使用这些配置的对象数量是有限的。每个实例对象都有一个对应的键。 - hadilq

0

我想分享一下基于你的回答所做的调查,因此我会发布一个带有这些结果的答案。这样可能更清楚为什么我选择了一个答案而不是其他答案。

裸结果排名如下(用于 600 个“怪物”对象的内存使用量,为所需内存的 10%):

  1. 琐碎选项:四个布尔值的类:22,200,040
  2. 初始选项:具有一个整数作为位图的类:22,200,040
  3. “多例”选项:一个工厂类,返回对琐碎选项类的引用:4,440,040
  4. EnumSet(没有 guava 缓存):53,401,896(在这个中我可能搞砸了,因为结果不如预期...我可能稍后会进一步处理)
  5. 带有 guava 缓存的 EnumSet:4,440,040

由于我的测试首先运行一系列比较,以确保所有实现在所有配置下都给出完全相同的结果,因此已经清楚,4.440.040是我用来保存项目的List<>的大小,因为在我决定在测量内存之前将其设置为null之前,这些数字始终为0

请不要深入研究我如何测量内存消耗(在每个列表被释放并设置为null之前和之后使用gc(); freeMemory();),因为我对所有方法都使用了相同的方法,并且每次执行20次并以不同的执行顺序进行。结果足够一致。

这些结果表明,multiton解决方案是最易于实现且性能最佳的解决方案。这就是为什么我将其设置为所选答案的原因。

作为旁注/好奇心,请注意,这项调查所涉及的项目已选择了平凡的选项作为解决方案,而大部分调查是为了满足我的好奇心--还有一些隐藏的愿望,希望能够证明其他解决方案比平凡的解决方案效率更高...但不是--。 这就是为什么我花了这么长时间才得出结论的原因。

0
如果配置实现对象很小且创建不昂贵,则无需缓存它们。因为每个怪物对象都必须保留对其每个配置的引用,在机器级别上,引用是指针,并且使用的内存至少与int相同。
@gamulf提出的EnumSet方法可能可以直接使用而无需缓存,因为根据EnumSet javadoc:
枚举集在内部表示为位向量。这种表示非常紧凑和高效。此类的空间和时间性能应该足够好,以允许其用作传统基于int的“位标志”的高质量、类型安全的替代品。
我没有对其进行基准测试,但是使用@gamulf的解决方案可能会使缓存变得无用,因为Configuration对象仅包含一个不超过int的EnumSet。
如果您有一个重型配置类(在内存或创建成本方面),并且只有少量可能的配置,则可以在类中使用静态HashSet成员和静态工厂方法来返回缓存的对象:
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
        ...
    }
}

但正如我之前所说,对于像 @gamulf 提出的那样轻量级的对象来说,这可能是无用的。


这应该是对@gamulf答案的评论,但内容略长... - Serge Ballesta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接