Подтвердить что ты не робот

Хранить заказ Enums в Java

В java EnumSet хранит элементы, которые он содержит в битовой маске/битовом векторе, используя long (RegularEnumSet) или long[] (JumboEnumSet). Теперь я столкнулся с прецедентом, в котором у меня много тысяч объектов домена (пусть их называют Node), каждый из которых будет отображать все элементы перечисления (пусть вызов Flag) в порядке, который будет зависеть от каждого объекта.

В настоящее время я сохраняю заказ как Guava ImmutableSet, потому что это гарантирует сохранение порядка вставки. Тем не менее, я использовал методы, описанные на этой странице, чтобы сравнить использование памяти в EnumSet<Flag>, a ImmutableSet<Flag> и a Flag[]. Вот результаты, когда a) Flag имеет 64 элемента перечисления и b) все три варианта содержат все 64 элемента:

EnumSet: 32 байт
ImmutableSet: 832 байт
Массив: 272 байта

Итак, мой вопрос: есть ли уловкий способ упаковать порядок перечисления в числовое значение, чтобы получить объем памяти меньше, чем размер массива? Если это имеет значение: в моем случае я бы предположил, что порядок всегда содержит все элементы Enum.

Чтобы уточнить: мое перечисление намного меньше этого, и на данный момент у меня нет проблем с памятью, и вряд ли эта ситуация когда-нибудь вызовет проблемы с памятью. Это просто, что эта неэффективность меня беспокоит даже на этом микроскопическом уровне.

Update:

После предложений из различных ответов и комментариев я придумал эту структуру данных, которая использует массив байтов. Предостережение: он не реализует интерфейс Set (не проверяет уникальные значения), и он не будет масштабироваться до больших перечислений, кроме того, что может содержать байт. Кроме того, сложность довольно ужасная, так как Enum.values ​​() нужно повторять повторно (см. Здесь для обсуждение этой проблемы), но здесь говорится:

public class EnumOrdering<E extends Enum<E>> implements Iterable<E> {
    private final Class<E> type;
    private final byte[] order;

    public EnumOrdering(final Class<E> type, final Collection<E> order) {
        this.type = type;

        this.order = new byte[order.size()];

        int offset = 0;
        for (final E item : order) {
            this.order[offset++] = (byte) item.ordinal();
        }

    }

    @Override
    public Iterator<E> iterator() {
        return new AbstractIterator<E>() {
            private int offset = -1;
            private final E[] enumConstants = type.getEnumConstants();

            @Override
            protected E computeNext() {
                if (offset < order.length - 1) {
                    return enumConstants[order[++offset]];
                }
                return endOfData();
            }
        };
    }
}

Объем памяти:

EnumOrdering: 104

Это довольно хороший результат, благодаря bestsss и JB Nizet!

Обновление: я изменил код, чтобы реализовать только Iterable, потому что что-нибудь еще потребует разумных реализаций для equals/hashCode/contains и т.д.

4b9b3361

Ответ 1

есть ли уловкий способ упаковать порядок перечисления в числовое значение

Да, вы можете представить упорядочение как числовое значение, хотя для его использования вам нужно преобразовать обратно в массив byte/int. А так как есть 64! возможные порядки из 64 значений и 64! больше, чем Long.MAX_VALUE, вам нужно сохранить номер в BigInteger. Я предполагаю, что это будет наиболее экономичный способ хранения заказов, хотя то, что вы получаете в памяти, вы теряете во времени из-за необходимости преобразования числа в массив.

Для алгоритмов преобразования между представлениями числа/массива см. этот вопрос.

Здесь альтернатива вышесказанному, не знаю, насколько он эффективен, как на этом, и вам придется преобразовать код из int в BigInteger, но этого должно быть достаточно, чтобы дать вы идете:

/**
   * Returns ith permutation of the n numbers [from, ..., to]
   * (Note that n == to - from + 1).
   * permutations are numbered from 0 to n!-1, if i is outside this
   * range it is treated as i%n! 
   * @param i
   * @param from
   * @param n
   * @return
   */
  public static int[] perm(long i, int from, int to)
  {
    // method specification numbers permutations from 0 to n!-1.
    // If you wanted them numbered from 1 to n!, uncomment this line.
    //  i -= 1;
    int n = to - from + 1;

    int[] initArr  = new int[n];             // numbers [from, ..., to]
    int[] finalArr = new int[n];             // permutation of numbers [from, ..., to]

    // populate initial array
    for (int k=0; k<n; k++)
      initArr[k] = k+from;

    // compute return array, element by element
    for (int k=0; k<n; k++) {
      int index = (int) ((i%factorial(n-k)) / factorial(n-k-1));

      // find the index_th element from the initial array, and
      // "remove" it by setting its value to -1
      int m = convertIndex(initArr, index);
      finalArr[k] = initArr[m];
      initArr[m] = -1;
    }

    return finalArr;
  }


  /** 
   * Helper method used by perm.
   * Find the index of the index_th element of arr, when values equal to -1 are skipped.
   * e.g. if arr = [20, 18, -1, 19], then convertIndex(arr, 2) returns 3.
   */
  private static int convertIndex(int[] arr, int index)
  {
    int m=-1;
    while (index>=0) {
      m++;
      if (arr[m] != -1)
        index--;
    }

    return m;
  }

В основном вы начинаете с вашего массива init в своем естественном порядке, затем перебираете свой последний массив, каждый раз вычисляя, какой из остальных элементов должен быть размещен дальше. Эта версия "удаляет" элементы из массива init, устанавливая значение -1. Вероятно, было бы более интуитивно понятно использовать List или LinkedList, я только что вставил это из какого-то старого кода, в котором я лежал.

С помощью вышеуказанных методов и с этим как main:

public static void main(String[] args) {
    int n = (int) factorial(4);
    for ( int i = 0; i < n; i++ ) {
      System.out.format( "%d: %s\n", i, Arrays.toString( perm(i, 1, 4 ) ) );
    }
}

Вы получаете следующий результат:

0: [1, 2, 3, 4]
1: [1, 2, 4, 3]
2: [1, 3, 2, 4]
3: [1, 3, 4, 2]
4: [1, 4, 2, 3]
5: [1, 4, 3, 2]
6: [2, 1, 3, 4]
7: [2, 1, 4, 3]
8: [2, 3, 1, 4]
9: [2, 3, 4, 1]
10: [2, 4, 1, 3]
11: [2, 4, 3, 1]
12: [3, 1, 2, 4]
13: [3, 1, 4, 2]
14: [3, 2, 1, 4]
15: [3, 2, 4, 1]
16: [3, 4, 1, 2]
17: [3, 4, 2, 1]
18: [4, 1, 2, 3]
19: [4, 1, 3, 2]
20: [4, 2, 1, 3]
21: [4, 2, 3, 1]
22: [4, 3, 1, 2]
23: [4, 3, 2, 1]

Вот исполняемая версия ideone.

Судя по BigInteger.bitLength(), должно быть возможно сохранить порядок 64 элементов не более 37 байт (плюс накладные расходы на использование экземпляра BigInteger). Я не знаю, стоит ли этого, но это делает приятное упражнение!

Ответ 2

Если у вас есть 64 значения enum, вы можете использовать массив байтов, в котором каждый байт будет содержать порядковый номер одного из элементов перечисления. Для этого потребуется 3 * (64 + 16) = 240 байтов для 3 массивов из 64 байтов (16 байтов - это стоимость байтового массива, независимо от его длины).

Это все еще пустое пространство, так как каждый байт может хранить 8 бит, но вам нужно всего лишь 6 для хранения чисел от 0 до 63. Таким образом, вы можете применить алгоритм умной упаковки, который будет использовать 3 байта (24 бита) для хранения 4 порядковых номера. Это приведет к 3 * (64 * 3 / 4 + 16) = 192 байтам.

Я сосать байт манипуляции, поэтому я оставлю реализацию как упражнение для вас.