Кэширование объектов, построенных с несколькими параметрами

У меня есть фабрика, которая создает объекты класса MyClass, возвращая уже сгенерированные, когда они существуют. Поскольку у меня есть метод создания (getOrCreateMyClass), принимающий несколько параметров, как лучше всего использовать карту для хранения и извлечения объектов?

Мое текущее решение заключается в следующем, но мне это не кажется слишком ясным. Я использую метод hashCode (слегка модифицированный) класса MyClass для построения int на основе параметров класса MyClass и использую его в качестве ключа карты.

import java.util.HashMap;
import java.util.Map;

public class MyClassFactory {

    static Map<Integer, MyClass> cache = new HashMap<Integer, MyClass>();

    private static class MyClass {
        private String s;
        private int i;

        public MyClass(String s, int i) {
        }

        public static int getHashCode(String s, int i) {
            final int prime = 31;
            int result = 1;
            result = prime * result + i;
            result = prime * result + ((s == null) ? 0 : s.hashCode());
            return result;
        }

        @Override
        public int hashCode() {
            return getHashCode(this.s, this.i);
        }

    }


    public static MyClass getOrCreateMyClass(String s, int i) {
        int hashCode =  MyClass.getHashCode(s, i);
        MyClass a = cache.get(hashCode);
        if (a == null) {
            a = new MyClass(s, i);
             cache.put(hashCode , a);

        } 
        return a;
    }

}

person cdarwin    schedule 14.11.2010    source источник
comment
Выглядит хорошо для меня. Почему тебе это не нравится?   -  person skaffman    schedule 14.11.2010
comment
Что ж, мне это не кажется слишком объектно-ориентированным, и в настоящее время у меня есть неизвестная ошибка с такой фабрикой, но, возможно, проблема не в этом коде...   -  person cdarwin    schedule 14.11.2010
comment
извините, я забыл строку!! (это был пример кода), исправление cache.put   -  person cdarwin    schedule 15.11.2010
comment
Если getOrCreateMyClass будет методом, с помощью которого все клиенты будут получать экземпляры MyClass, я бы рекомендовал дать ему более простое имя, например просто get... возможно, ему не нужно так много информации о том, как он работает в своем имени. Вы, вероятно, захотите, чтобы в его Javadoc упоминалось, что он всегда возвращает один и тот же экземпляр для данной пары входных данных, конечно.   -  person ColinD    schedule 15.11.2010
comment
Это называется шаблоном наилегчайшего веса.   -  person WesternGun    schedule 04.05.2020


Ответы (2)


Вам действительно не следует использовать хэш-код в качестве ключа на вашей карте. Хэш-код класса не обязательно гарантирует, что он не будет одинаковым для любых двух неравных экземпляров этого класса. Действительно, ваш метод хэш-кода определенно может создать один и тот же хэш-код для двух неравных экземпляров. Вам необходимо реализовать equals на MyClass, чтобы проверить, что два экземпляра MyClass равны на основе равенства String и int, которые они содержат. Я бы также рекомендовал сделать поля s и i final, чтобы обеспечить более надежную гарантию неизменности каждого экземпляра MyClass, если вы собираетесь использовать его таким образом.

Кроме того, я думаю, что вам на самом деле нужен interner... то есть что-то, что гарантирует, что вы будете хранить не более 1 экземпляра данного MyClass в памяти за раз . Правильным решением для этого является Map<MyClass, MyClass>... точнее ConcurrentMap<MyClass, MyClass>, если есть шанс, что getOrCreateMyClass будет вызван из нескольких потоков. Теперь вам нужно создать новый экземпляр MyClass, чтобы проверить кеш при использовании этого подхода, но на самом деле это неизбежно... и это не имеет большого значения, потому что MyClass создать легко.

В Guava есть кое-что, что делает всю работу за вас: это Внутренний интерфейс и соответствующий Interners фабричный/служебный класс. Вот как вы можете использовать его для реализации getOrCreateMyClass:

private static final Interner<MyClass> interner = Interners.newStrongInterner();

public static MyClass getOrCreateMyClass(String s, int i) {
  return interner.intern(new MyClass(s, i));
}

Обратите внимание, что при использовании сильного интернера, как и в вашем примере кода, каждый MyClass будет храниться в памяти до тех пор, пока интернер находится в памяти, независимо от того, имеет ли что-либо еще в программе ссылку на данный экземпляр. Если вместо этого вы используете newWeakInterner, когда в вашей программе больше нет ничего, использующего данный экземпляр MyClass, этот экземпляр будет иметь право на сборку мусора, что поможет вам не тратить память на экземпляры, которые вам не нужны.

Если вы решите сделать это самостоятельно, вам нужно будет использовать кэш ConcurrentMap и использовать putIfAbsent. Вы можете взглянуть на реализацию сильного интернера Guava для справки, я думаю... подход со слабыми ссылками намного сложнее.

person ColinD    schedule 14.11.2010
comment
+1: Если MyClass действительно так же дешев для создания, как и Pair, и если OP хочет добавить Guava в качестве зависимости, это, безусловно, звучит как лучшее решение. - person Don Roby; 15.11.2010

Ваш getOrCreateMyClass, похоже, не добавляется в кеш, если он создается.

Я думаю, что это также не будет работать правильно, когда хэш-коды сталкиваются. Идентичные хэш-коды не означают одинаковых объектов. Это может быть источником ошибки, которую вы упомянули в комментарии.

Вы можете рассмотреть возможность создания общего класса Pair с фактическими методами equals и hashCode и использования класса Pair<String, Integer> в качестве ключа карты для вашего кеша.

Изменить:

Проблема дополнительного потребления памяти при хранении ключа Pair<String, Integer> и значения MyClass может быть лучше всего решена путем преобразования Pair<String, Integer> в поле MyClass и, таким образом, наличия только одной ссылки на этот объект.

Однако со всем этим вам, возможно, придется беспокоиться о проблемах с потоками, которые, похоже, еще не решены и которые могут быть еще одним источником ошибок.

И действительно ли это хорошая идея, зависит от того, намного ли дороже создание MyClass, чем создание ключа карты.

Еще одно изменение:

Ответ ColinD также разумен (и я проголосовал за него), если строительство MyClass не обходится дорого.

Другой подход, который, возможно, стоит рассмотреть, заключается в использовании вложенной карты Map<String, Map<Integer, MyClass>>, которая потребует двухэтапного поиска и немного усложнит обновление кеша.

person Don Roby    schedule 14.11.2010
comment
+1 за замечание о столкновении. Я бы не советовал использовать Pair в качестве ключа, так как вы сохраните две ссылки на каждый объект в MyClass, что приведет к еще большему потреблению памяти кешем. - person Jorn; 15.11.2010
comment
Мне нужно создать тысячи объектов MyClass, поэтому я надеялся каждый раз не создавать новые ключевые объекты Pair. Замечание о столкновении действительно интересное - person cdarwin; 15.11.2010
comment
Я также не в восторге от влияния на потребление памяти. Я не могу придумать альтернативу, которая значительно уменьшила бы потребление памяти без повторного введения риска коллизии хэш-кода. - person Don Roby; 15.11.2010
comment
Мне абсолютно необходимо кэшировать эти объекты, так как одновременно должен существовать только один объект, построенный с заданными значениями параметров. Мне нравится идея внутреннего состояния внутри объекта, если я не ошибаюсь, это паттерн Memento. - person cdarwin; 15.11.2010