2016-03-31 3 views
1

У меня есть коллекция элементов:общий способ сгруппировать объекты по нескольким свойствам в Java

class Item {   
    String type; 
    boolean flag; 
    int size; 
    ... 
} 

Есть несколько возможных типов (скажем, «а», «б» и «в») и, следовательно, несколько возможные комбинации значений типа-флага (["a" ; false], ["a" ; true"], ["b" ; false], ...). Мне нужно, чтобы свернуть элементы, которые имеют такую ​​же комбинацию значений, так что я collapse метод с этой подписью

Item collapse(Collection<Item> items) 

Что мне нужно разделить список элементов ввода в группы, которые имеют одинаковые type и flag значения

List<Collection<Item>> getGroups(Collection<Item> items) // method I need 

, так что я мог свернуть каждую группу

List<Item> r = getGroups(items).stream().map(Item::collapse).collect(toList()); 

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

Как это можно сделать красиво? Существует ли известное решение проблемы?

ответ

1

Чтобы узнать, какой из Item логически соответствует некоторым другим Item для рушится, я взял на себя ответственность за то, что сам класс Item. Вы можете переопределить метод equals, но если вы захотите положить их в Set, это может привести к нежелательным результатам, поэтому лучше всего использовать отдельный метод проверки.

Другой вариант - взять те поля, которые будут использоваться для этой проверки, и превратить их в внутренний класс Item. equals и hashCode могут быть переопределены только для внутреннего класса, а его экземпляры используются как ключ для Map.

Ничего из этого не будет автоматически включать любые новые поля, которые вы добавляете позже. Таким образом, каждый, кто поддерживает класс, должен удостовериться, что все элементы, которые должны быть включены в проверку (или equals/hashCode), добавляются к методу (методам).

Единственный способ, которым я действительно могу придумать, чтобы приблизиться к этому, - использовать отражение. Если что-то, что должно быть учтено, входит только в внутренний класс, это сработает. Если вы должны держать его непосредственно в классе Item, возможно, было бы полезно определить аннотацию (с сохранением времени исполнения). Код, выполняющий проверку (или equals/hashCode, если используется), может отражать класс и использовать каждое аннотированное поле.

аннотаций может выглядеть примерно так:

import java.lang.annotation.ElementType; 
import java.lang.annotation.Retention; 
import java.lang.annotation.RetentionPolicy; 
import java.lang.annotation.Target; 


@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.FIELD) 
public @interface CollapseField { 

} 

А затем используется так:

class Item {  
    @CollapseField 
    String type; 
    @CollapseField 
    boolean flag; 
    int size; 
    ... 
} 

код, используя это было тогда необходимо проверить, какие поля аннотированный и получить их значения (оба эти действия с использованием отражения) и проверить равенство с другими объектами, чтобы установить, которые принадлежат друг другу. Поскольку это может сильно повлиять на производительность, использование кэширования для чего-то вроде хеш-кода было бы хорошей идеей.

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

Наконец, это может показаться странным для Java, но, возможно, использование шаблона свойств вместо полей может иметь смысл. Хотя вы потеряете некоторую безопасность. Стив Йегге сделал длинный, но интересный пост: http://steve-yegge.blogspot.be/2008/10/universal-design-pattern.html

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

EDIT: Вот пример, где поля, которые будут использоваться для ключа сделаны во внутренний класс, который реализует equals и hashCode, поэтому он может быть использован в качестве ключа для Map:

import java.util.Objects; 

public class Item { 

    int size; 
    final Key key; 

    public class Key { 

     String type; 
     boolean flag; 

     public Key(String type, boolean flag) { 
      this.type = type; 
      this.flag = flag; 
     } 

     public String getType() { 
      return type; 
     } 

     public void setType(String type) { 
      this.type = type; 
     } 

     public boolean isFlag() { 
      return flag; 
     } 

     public void setFlag(boolean flag) { 
      this.flag = flag; 
     } 

     @Override 
     public int hashCode() { 
      int hash = 5; 
      hash = 89 * hash + Objects.hashCode(this.type); 
      hash = 89 * hash + (this.flag ? 1 : 0); 
      return hash; 
     } 

     @Override 
     public boolean equals(Object obj) { 
      if (obj == null) { 
       return false; 
      } 
      if (getClass() != obj.getClass()) { 
       return false; 
      } 
      final Key other = (Key) obj; 
      if (!Objects.equals(this.type, other.type)) { 
       return false; 
      } 
      if (this.flag != other.flag) { 
       return false; 
      } 
      return true; 
     } 

    } 

    public Item(String type, boolean flag, int size) { 
     key = new Key(type, flag); 
     this.size = size; 
    } 

    public String getType() { 
     return key.type; 
    } 

    public void setType(String type) { 
     this.key.type = type; 
    } 

    public boolean isFlag() { 
     return key.flag; 
    } 

    public void setFlag(boolean flag) { 
     this.key.flag = flag; 
    } 

    public int getSize() { 
     return size; 
    } 

    public void setSize(int size) { 
     this.size = size; 
    } 

    public Key getKey() { 
     return key; 
    } 

} 

Получатели и сеттеры на уровне Item передают некоторые из полей на Key. Обратите внимание, что получатели и сеттеры в Key могут не понадобиться, если вы только проходите через Item, так как поля напрямую доступны для содержащего класса. Если вам нужно добавить поле, которое должно быть частью ключа, добавьте его в Key. Если он не может использоваться для идентификации, добавьте его непосредственно в Item. equals и hashCode могут быть легко сгенерированы любым приемлемым IDE, если вы должны их обновить.

Обратите внимание, что это решение может сломаться, если вы используете класс в некоторой структуре, которая делает отражение или интроспекцию. В зависимости от того, как он приближается, поля в Key могут в конечном итоге рассматриваться как свойства Item (из-за геттеров/сеттеров) или нет. Что-то вроде JPA или контейнера EJB, приближающегося к полям непосредственно через отражение, может не сработать с этим.

+0

мммм, это выглядит более инженерии, особенно отражающая часть – AdamSkywalker

+0

Это Java для вас, мой друг. В отсутствие более мощных языков и средств метапрограммирования появляются странные перегруженные шаблоны. Именно поэтому существует такое смехотворное количество фабрик и абстрактных фабрик, и кто знает, что еще все в Java API и фреймворках. –

+0

О, и прежде, чем я забуду, определенно главная причина для такого огромного количества шаблонов. Возможно, загляните в Project Lombok, чтобы сделать вещи менее болезненными. Я не уверен, что он поможет вам в этом деле, но может (легко генерировать equals/hashCode с одной аннотацией). –

0

Полагая здесь мой текущий подход с составным ключом, но до сих пор ждет свежих идей

Collection<Collection<Item>> getGroups(Collection<Item> items) { 
    Map<String, Collection<Item>> itemsByKey = new HashMap<>(); 
    for (Item item : items) { 
     String key = item.key(); 
     Collection<Item> byKey = itemsByKey.get(key); 
     if (byKey == null) { 
      itemsByKey.put(key, (byKey = new ArrayList<>())); 
     } 
     byKey.add(item); 
    } 
    return itemsByKey.values(); 
} 

class Item { 
    public String key() { type + "-" + flag } 

Новые поля могут быть добавлены легко, но строковые ключи не выглядят большими.

+0

Это действительно не решает проблему необходимости жесткого кодирования используемых полей. Кроме этого, я не вижу проблемы с использованием строк в качестве ключей. Если ваш 'type' не может содержать используемый разделитель, он должен работать нормально, проверки равенства и hashCode для Strings работают хорошо, и это делается очень часто в Maps. Возможно, попробуйте немного оптимизировать, построив значение ключа один раз, а затем кешируя его. Поскольку ключ может использоваться часто, создавая его каждый раз, когда вы вызываете 'key()', это расточительно. Я считаю, что реализация String, вероятно, кэширует и хэш-код. –

3

Вы можете использовать Collectors.groupingBy:

public static void main(String[] args) { 
    List<Item> list = new ArrayList<>(); 
    list.add(new Item(1, true, 1)); 
    list.add(new Item(1, true, 2)); 

    list.add(new Item(1, false, 3)); 
    list.add(new Item(1, false, 4)); 

    list.add(new Item(2, true, 5)); 
    list.add(new Item(2, false, 6)); 

    Collection<List<Item>> result = list.stream() 
      .collect(Collectors.groupingBy(x -> Arrays.<Object>asList(x.keyA, x.keyB))) 
      .values(); 

    for (List<Item> items : result) { 
     System.out.println(items); 
    } 
} 

static class Item { 
    Integer keyA; 
    Boolean keyB; 
    Integer value; 

    public Item(Integer keyA, Boolean keyB, Integer value) { 
     this.keyA = keyA; 
     this.keyB = keyB; 
     this.value = value; 
    } 

    @Override 
    public String toString() { 
     return "Item{" + 
       "keyA=" + keyA + 
       ", keyB=" + keyB + 
       ", value=" + value + 
       '}'; 
    } 
} 
+0

это хорошо, он даже работает – AdamSkywalker

+1

Ухоженный! Похоже, мне нужно вникнуть в API Java SE 8 для обновления. –

Смежные вопросы