Несколько индексов для коллекции Java - самое основное решение? - PullRequest
47 голосов
/ 23 марта 2010

Я ищу самое простое решение для создания нескольких индексов в коллекции Java.

Требуемая функциональность:

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

Боковые условия:

  • Нет зависимости от больших (например, Lucene) библиотек. Нет необычных или плохо проверенных библиотек. Нет базы данных.
  • Библиотека вроде Apache Commons Collections и т. Д. Будет в порядке.
  • Еще лучше, если он работает только с JavaSE (6.0).
  • Редактировать: Нет самостоятельного решения (спасибо за ответы, предлагающие это - хорошо, что они здесь для полноты, но у меня уже есть решение, очень похожее на решение Джея) Всякий раз, когда несколько человек узнайте, что они реализовали одно и то же, это должно быть частью какой-то общей библиотеки.

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

Спасибо, Крис

Обновление

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

Вот что я действительно хочу: иметь функциональность в (a) Коллекциях Apache Commons или (b) в Google Collections / Guava. Или, может быть, очень хорошая альтернатива.

Неужели другие люди тоже пропускают эту функцию в этих библиотеках? Они предоставляют всевозможные вещи, такие как MultiMaps, MulitKeyMaps, BidiMaps, ... Я чувствую, что это прекрасно вписалось бы в эти библиотеки - его можно назвать MultiIndexMap. Что ты думаешь?

Ответы [ 14 ]

19 голосов
/ 23 марта 2010

Каждый индекс будет в основном отдельным Map. Вы можете (и, вероятно, должны) абстрагировать это за классом, который управляет поисками, индексацией, обновлениями и удалениями для вас. Это не было бы трудно сделать это довольно обобщенно. Но нет, для этого нет стандартного стандартного класса, хотя его можно легко построить из классов Java Collections.

12 голосов
/ 29 сентября 2012

Взгляните на CQEngine (механизм сбора запросов) , он точно подходит для такого рода требований и основан на IndexedCollection.

Также см. Связанный вопрос Как запрашивать коллекции объектов в Java (Criteria / SQL-like)? для получения дополнительной информации.

7 голосов
/ 23 марта 2010

Моей первой мыслью было бы создать класс для индексируемой вещи, а затем создать несколько HashMap для хранения индексов с тем же объектом, добавленным в каждую из HashMaps. Для добавления вы просто добавляете один и тот же объект к каждому HashMap. Удаление потребует поиска в каждом HashMap ссылки на целевой объект. Если удаление должно быть быстрым, вы можете создать два HashMaps для каждого индекса: один для индекса в значение, а другой для значения в индекс. Конечно, я бы обернул все, что вы делаете в классе с четко определенным интерфейсом.

Не похоже, что это будет сложно. Если вы заранее знаете номера и типы индексов и класс виджета, это будет довольно просто, например:

public class MultiIndex
{
  HashMap<String,Widget> index1=new HashMap<String,Widget>();
  HashMap<String,Widget> index2=new HashMap<String,Widget>();
  HashMap<Integer,Widget> index3=new HashMap<Integer,Widget>();

  public void add(String index1Value, String index2Value, Integer index3Value, Widget widget)
  {
    index1.put(index1Value, widget);
    index2.put(index2Value, widget);
    index3.put(index3Value, widget);
  }
  public void delete(Widget widget)
  {
    Iterator i=index1.keySet().iterator(); 
    while (i.hasNext())
    {
      String index1Value=(String)i.next();
      Widget gotWidget=(Widget) index1.get(index1Value);
      if (gotWidget.equals(widget))
        i.remove();
    }
    ... similarly for other indexes ...
  }
  public Widget getByIndex1(String index1Value)
  {
    return index1.get(index1Value);
  }
  ... similarly for other indexes ...

  }
}

Если вы хотите сделать его универсальным и принять любой объект, иметь переменное число и типы индексов и т. Д., Это немного сложнее, но не намного.

5 голосов
/ 30 марта 2010

У вас много действительно жестких требований, которые, кажется, очень специфичны для ваших нужд. Большинство вещей, о которых вы говорите, нежизнеспособны, потому что многие люди имеют такие же точные потребности, которые в основном определяют базовый механизм базы данных. Вот почему они являются «большими» библиотеками. Вы говорите «нет базы данных», но в своей основе каждая система индексации - это «база данных» терминов и документов. Я бы сказал, что Коллекция - это «база данных». Я бы сказал, взгляните на Space4J .

Я бы сказал, что если вы не нашли то, что искали, запустите проект на GitHub и продолжайте самостоятельно его кодировать и делитесь результатами.

4 голосов
/ 02 ноября 2013

Вам нужно проверить Бун. :)

http://rick -hightower.blogspot.com / 2013/11 / что-если-ява-коллекции-и-java.html

Вы можете добавить n поисковых индексов и поисковых индексов. Это также позволяет эффективно запрашивать примитивные свойства.

Вот пример, взятый из вики (я автор).

  repoBuilder.primaryKey("ssn")
          .searchIndex("firstName").searchIndex("lastName")
          .searchIndex("salary").searchIndex("empNum", true)
          .usePropertyForAccess(true);

Вы можете переопределить это, указав истинный флаг в качестве второго аргумента для searchIndex.

Обратите внимание, что empNum является уникальным поисковым индексом.

Что если бы было легко запрашивать сложный набор объектов Java во время выполнения? Что если бы существовал API, который поддерживал ваши индексы объектов (на самом деле только TreeMaps и HashMaps) в синхронизации.? Ну, тогда у вас будет репозиторий Boon. В этой статье показано, как использовать утилиты репозитория Boon для запроса объектов Java. Это первая часть. Там может быть много, много частей. :) Репозиторий Boon значительно упрощает выполнение запросов к коллекциям на основе индексов. Почему Boon data repo

Репозиторий Boon позволяет вам рассматривать коллекции Java больше как базу данных, по крайней мере, когда дело доходит до запросов к коллекциям. Репозиторий Boon не является базой данных в памяти и не может заменить организацию ваших объектов в структуры данных, оптимизированные для вашего приложения. Если вы хотите тратить свое время на предоставление клиентской ценности, создание своих объектов и классов и использование API коллекций для ваших структур данных, то DataRepo предназначена для вас. Это не исключает возможности использования книг Кнута и создания оптимизированной структуры данных. Это просто помогает упростить мирские дела, чтобы вы могли тратить время на то, чтобы делать сложные вещи. Родился по надобности

Этот проект возник из необходимости. Я работал над проектом, который планировал хранить большую коллекцию доменных объектов в оперативной памяти, и кто-то задал очень важный вопрос, который я пропустил. Как мы собираемся запросить эти данные. Я ответил, что мы будем использовать API коллекций и API потоковой передачи. Тогда я попытался сделать это ... Хммм ... Я также устал использовать потоковый API JDK 8 для большого набора данных, и это было медленно. (Репозиторий Boon работает с JDK7 и JDK8). Это был линейный поиск / фильтр. Это по замыслу, но для того, что я делал, это не сработало. Мне нужны индексы для поддержки произвольных запросов. Репозиторий Boon дополняет потоковый API.

Репозиторий Boon не стремится заменить потоковый API JDK 8, и на самом деле он хорошо работает с ним. Репозиторий Boon позволяет создавать индексированные коллекции. Индексы могут быть любыми (это подключаемые). На данный момент индексы репо данных Boon основаны на ConcurrentHashMap и ConcurrentSkipListMap. По своему дизайну репозиторий Boon работает со стандартными библиотеками коллекций. Не планируется создавать набор пользовательских коллекций. Нужно иметь возможность подключить Guava, Concurrent Trees или Trove, если вы этого хотите. Это обеспечивает упрощенный API для этого. Он позволяет выполнять линейный поиск для определения завершенности, но я рекомендую использовать его в первую очередь для использования индексов, а затем использовать потоковый API для остальных (для безопасности типов и скорости).

Пиковый пик перед пошаговым

Допустим, у вас есть метод, который создает 200 000 объектов сотрудников, таких как:

 List<Employee> employees = TestHelper.createMetricTonOfEmployees(200_000);

Так что теперь у нас 200 000 сотрудников. Давайте поищем их ...

Первая упаковка Сотрудники в поисковом запросе:

   employees = query(employees);

Сейчас ищем:

  List<Employee> results = query(employees, eq("firstName", firstName));

Так в чем же основное отличие вышеперечисленного от потокового API?

  employees.stream().filter(emp -> emp.getFirstName().equals(firstName)

Примерно в 20 000% быстрее использовать Boon's DataRepo! Ах, сила HashMaps и TreeMaps. :) Существует API, который выглядит так же, как ваши встроенные коллекции. Существует также API, который больше похож на объект DAO или объект Repo.

Простой запрос с объектом Repo / DAO выглядит так:

  List<Employee> employees = repo.query(eq("firstName", "Diana"));

Более сложный запрос будет выглядеть так:

  List<Employee> employees = repo.query(
      and(eq("firstName", "Diana"), eq("lastName", "Smith"), eq("ssn", "21785999")));

Или это:

  List<Employee> employees = repo.query(
      and(startsWith("firstName", "Bob"), eq("lastName", "Smith"), lte("salary", 200_000),
              gte("salary", 190_000)));

Или даже это:

  List<Employee> employees = repo.query(
      and(startsWith("firstName", "Bob"), eq("lastName", "Smith"), between("salary", 190_000, 200_000)));

Или, если вы хотите использовать потоковый API JDK 8, это работает против него:

  int sum = repo.query(eq("lastName", "Smith")).stream().filter(emp -> emp.getSalary()>50_000)
      .mapToInt(b -> b.getSalary())
      .sum();

Выше было бы намного быстрее, если бы число сотрудников было довольно большим. Это сузило бы сотрудников, чье имя начиналось с Смита и получало зарплату выше 50 000. Допустим, у вас было 100 000 сотрудников и только 50 по имени Смит, так что теперь вы быстро сужаетесь до 50, используя индекс, который эффективно вытягивает 50 сотрудников из 100 000, а затем мы фильтруем всего 50 вместо целых 100 000.

Вот контрольный прогон из репо линейного поиска по сравнению с индексированным поиском в наносекундах:

Name index  Time 218 
Name linear  Time 3709120 
Name index  Time 213 
Name linear  Time 3606171 
Name index  Time 219 
Name linear  Time 3528839

Кто-то недавно сказал мне: «Но с потоковым API вы можете запустить фильтр в parralel).

Посмотрим, как работает математика:

3,528,839 / 16 threads vs. 219

201,802 vs. 219 (nano-seconds).

Индексы победили, но это было фото-финиш. НЕ! :)

Это было только на 9500% быстрее, а не на 40 000% быстрее. Так близко .....

Я добавил еще несколько функций. Они активно используют индексы. :)

repo.updateByFilter (values ​​(value ("firstName", "Di")), и (eq ("firstName", "Diana"), eq ("lastName", "Smith"), eq ("ssn", "21785999")));

Выше было бы эквивалентно

ОБНОВЛЕНИЕ Сотрудник e SET e.firstName = 'Di' ГДЕ e.firstName = 'Диана' и e.lastName = 'Smith' и e.ssn = '21785999'

Это позволяет вам устанавливать несколько полей одновременно для нескольких записей, поэтому, если вы выполняли массовое обновление.

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

  repo.updateByFilter("firstName", "Di",
          and( eq("firstName", "Diana"),
          eq("lastName", "Smith"),
                  eq("ssn", "21785999") ) );

Вот некоторые основные возможности выбора:

  List <Map<String, Object>> list =
     repo.query(selects(select("firstName")), eq("lastName", "Hightower"));

Вы можете выбрать столько, сколько захотите. Вы также можете вернуть список отсортированным:

  List <Map<String, Object>> list =
     repo.sortedQuery("firstName",selects(select("firstName")),
       eq("lastName", "Hightower"));

Вы можете выбрать свойства связанных свойств (например, employee.department.name).

  List <Map<String, Object>> list = repo.query(
          selects(select("department", "name")),
          eq("lastName", "Hightower"));

  assertEquals("engineering", list.get(0).get("department.name"));

Выше было бы попытаться использовать поля классов. Если вы хотите использовать фактические свойства (emp.getFoo () или emp.foo), то вам нужно использовать selectPropertyPath.

  List <Map<String, Object>> list = repo.query(
          selects(selectPropPath("department", "name")),
          eq("lastName", "Hightower"));

Обратите внимание, что select ("отдела", "имя") намного быстрее, чем selectPropPath ("отдел", "имя"), что может иметь значение в узком цикле.

По умолчанию все поисковые индексы и индексы поиска допускают дублирование (кроме индекса первичного ключа).

  repoBuilder.primaryKey("ssn")
          .searchIndex("firstName").searchIndex("lastName")
          .searchIndex("salary").searchIndex("empNum", true)
          .usePropertyForAccess(true);

Вы можете переопределить это, указав истинный флаг в качестве второго аргумента для searchIndex.

Обратите внимание, что empNum является уникальным поисковым индексом.

Если вы предпочитаете или нуждаетесь, вы можете получить даже простой поиск в виде карт:

  List<Map<String, Object>> employees = repo.queryAsMaps(eq("firstName", "Diana"));

Я не уверен, является ли это функцией или ошибкой. Я думал, что когда вы работаете с данными, вам нужно представлять эти данные таким образом, чтобы не связывать потребителей данных с вашим реальным API. Кажется, что для этого нужно иметь карту типа String / основных типов. Обратите внимание, что преобразование объекта в карту идет глубоко, как в:

  System.out.println(employees.get(0).get("department"));

Урожайность:

{class=Department, name=engineering}

Это может быть полезно для отладки и специальных запросов для инструментов. Я рассматриваю возможность добавления поддержки для простого преобразования в строку JSON.

Добавлена ​​возможность запрашивать свойства коллекции. Это должно работать с коллекциями и массивами так глубоко, как вам нравится. Прочитайте это снова, потому что это была настоящая MF для реализации!

  List <Map<String, Object>> list = repo.query(
          selects(select("tags", "metas", "metas2", "metas3", "name3")),
          eq("lastName", "Hightower"));

  print("list", list);

  assertEquals("3tag1", idx(list.get(0).get("tags.metas.metas2.metas3.name3"), 0));

Печать из вышеперечисленного выглядит так:

list [{tags.metas.metas2.metas3.name3=[3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3,
3tag1, 3tag2, 3tag3, 3tag1, 3tag2, 3tag3]},
...

Я создал несколько классов отношений, чтобы проверить это:

public class Employee {
List <Tag> tags = new ArrayList<>();
{
    tags.add(new Tag("tag1"));
    tags.add(new Tag("tag2"));
    tags.add(new Tag("tag3"));

}
...
public class Tag {
...
List<Meta> metas = new ArrayList<>();
{
    metas.add(new Meta("mtag1"));
    metas.add(new Meta("mtag2"));
    metas.add(new Meta("mtag3"));

}

}
public class Meta {
 ...
   List<Meta2> metas2 = new ArrayList<>();
   {
       metas2.add(new Meta2("2tag1"));
       metas2.add(new Meta2("2tag2"));
       metas2.add(new Meta2("2tag3"));

   }

}

...
public class Meta2 {



List<Meta3> metas3 = new ArrayList<>();
{
    metas3.add(new Meta3("3tag1"));
    metas3.add(new Meta3("3tag2"));
    metas3.add(new Meta3("3tag3"));

}
public class Meta3 {

...

Вы также можете искать по типу:

  List<Employee> results = sortedQuery(queryableList, "firstName", typeOf("SalesEmployee"));

  assertEquals(1, results.size());
  assertEquals("SalesEmployee", results.get(0).getClass().getSimpleName());

Вышеприведенное находит всех сотрудников с простым именем класса SalesEmployee. Он также работает с полным именем класса, как в:

  List<Employee> results = sortedQuery(queryableList, "firstName", typeOf("SalesEmployee"));

  assertEquals(1, results.size());
  assertEquals("SalesEmployee", results.get(0).getClass().getSimpleName());

Вы также можете искать по фактическому классу:

  List<Employee> results = sortedQuery(queryableList, "firstName", instanceOf(SalesEmployee.class));

  assertEquals(1, results.size());
  assertEquals("SalesEmployee", results.get(0).getClass().getSimpleName());

Вы также можете запрашивать классы, которые реализуют определенные интерфейсы:

  List<Employee> results = sortedQuery(queryableList, "firstName",      
                              implementsInterface(Comparable.class));

  assertEquals(1, results.size());
  assertEquals("SalesEmployee", results.get(0).getClass().getSimpleName());

Вы также можете индексировать вложенные поля / свойства, и они могут быть полями коллекции или полями, не являющимися коллекциями свойств, так глубоко вложенными, как вам бы хотелось:

  /* Create a repo, and decide what to index. */
  RepoBuilder repoBuilder = RepoBuilder.getInstance();

  /* Look at the nestedIndex. */
  repoBuilder.primaryKey("id")
          .searchIndex("firstName").searchIndex("lastName")
          .searchIndex("salary").uniqueSearchIndex("empNum")
          .nestedIndex("tags", "metas", "metas2", "name2");

Позже вы можете использовать nestedIndex для поиска.

  List<Map<String, Object>> list = repo.query(
          selects(select("tags", "metas", "metas2", "name2")),
          eqNested("2tag1", "tags", "metas", "metas2", "name2"));

Безопасный способ использовать nestedIndex - это использовать eqNested. Вы можете использовать eq, gt, gte и т. Д., Если у вас есть такой индекс:

  List<Map<String, Object>> list = repo.query(
          selects(select("tags", "metas", "metas2", "name2")),
          eq("tags.metas.metas2.name2", "2tag1"));

Вы также можете добавить поддержку подклассов

  List<Employee> queryableList = $q(h_list, Employee.class, SalesEmployee.class,  
                  HourlyEmployee.class);
  List<Employee> results = sortedQuery(queryableList, "firstName", eq("commissionRate", 1));
  assertEquals(1, results.size());
  assertEquals("SalesEmployee", results.get(0).getClass().getSimpleName());

  results = sortedQuery(queryableList, "firstName", eq("weeklyHours", 40));
  assertEquals(1, results.size());
  assertEquals("HourlyEmployee", results.get(0).getClass().getSimpleName());

В хранилище данных есть аналогичная функция в методе DataRepoBuilder.build (...) для указания подклассов. Это позволяет без видимых полей запроса образовывать подклассы и классы в одном репо или поисковой коллекции.

4 голосов
/ 30 марта 2010

Коллекции Google LinkedListMultimap

О вашем первом требовании

  • При удалении значения все записи индекса, связанные с этим значением, должны быть удалены.

Я думаю, что нет ни библиотеки, ни помощника, который бы ее поддерживал.

Вот как я это сделал, используя LinkedListMultimap

Multimap<Integer, String> multimap = LinkedListMultimap.create();

// Three duplicates entries
multimap.put(1, "A");
multimap.put(2, "B");
multimap.put(1, "A");
multimap.put(4, "C");
multimap.put(1, "A");

System.out.println(multimap.size()); // outputs 5

Чтобы получить ваше первое требование, помощник может сыграть хорошую работу

public static <K, V> void removeAllIndexEntriesAssociatedWith(Multimap<K, V> multimap, V value) {
    Collection<Map.Entry<K, V>> eCollection = multimap.entries();
    for (Map.Entry<K, V> entry : eCollection)
        if(entry.getValue().equals(value))
            eCollection.remove(entry);
}

...

removeAllIndexEntriesAssociatedWith(multimap, "A");

System.out.println(multimap.size()); // outputs 2

Коллекции Google

  • легкий
  • Поддерживается Joshua Block (Effective Java)
  • Хорошие функции, такие как ImmutableList, ImmutableMap и т. Д.
2 голосов
/ 02 апреля 2010

Похоже, вашей главной целью является удаление объекта из всех индексов при его удалении из одного.

Самый простой подход - добавить еще один слой косвенности: вы сохраняете фактический объект в Map<Long,Value> и используете двунаправленную карту (которую вы найдете в Jakarta Commons и, вероятно, в Google Code) для своих индексов как Map<Key,Long>. Когда вы удаляете запись из определенного индекса, вы берете значение Long из этого индекса и используете его для удаления соответствующих записей из основной карты и других индексов.

Одной из альтернатив BIDIMap является определение ваших «индексных» карт как Map<Key,WeakReference<Long>>; однако для этого потребуется реализовать ReferenceQueue для очистки.


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

public class MultiKey
implements Comparable<Object>
{
   private Comparable<?>[] _keys;
   private Comparable _matchKey;
   private int _matchPosition;

   /**
    *  This constructor is for inserting values into the map.
    */
   public MultiKey(Comparable<?>... keys)
   {
      // yes, this is making the object dependent on externally-changable
      // data; if you're paranoid, copy the array
      _keys = keys;
   }


   /**
    *  This constructor is for map probes.
    */
   public MultiKey(Comparable key, int position)
   {
      _matchKey = key;
      _matchPosition = position;
   }


   @Override
   public boolean equals(Object obj)
   {
      // verify that obj != null and is castable to MultiKey
      if (_keys != null)
      {
         // check every element
      }
      else
      {
         // check single element
      }
   }


   public int compareTo(Object o)
   {
      // follow same pattern as equals()
   }
}
2 голосов
/ 28 марта 2010

Я написал интерфейс Table, который включает такие методы, как

V put(R rowKey, C columnKey, V value) 
V get(Object rowKey, Object columnKey) 
Map<R,V> column(C columnKey) 
Set<C> columnKeySet()
Map<C,V> row(R rowKey)
Set<R> rowKeySet()
Set<Table.Cell<R,C,V>> cellSet()

Мы хотели бы включить его в будущий релиз Guava, но я не знаю, когда это произойдет. http://code.google.com/p/guava-libraries/issues/detail?id=173

1 голос
/ 12 июня 2018

Если вам нужно несколько индексов для ваших данных, вы можете создать и поддерживать несколько хеш-карт или использовать такую ​​библиотеку, как Data Store:

https://github.com/jparams/data-store

Пример:

Store<Person> store = new MemoryStore<>() ;
store.add(new Person(1, "Ed", 3));
store.add(new Person(2, "Fred", 7));
store.add(new Person(3, "Freda", 5));
store.index("name", Person::getName);
Person person = store.getFirst("name", "Ed");

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

1 голос
/ 31 марта 2010

Использование Префузия Таблицы . Они поддерживают столько индексов, сколько вы хотите, они быстрые (индексы - TreeMaps) и имеют хорошие опции фильтрации (булевы фильтры - нет проблем!). База данных не требуется, протестирована с большими наборами данных во многих приложениях визуализации информации.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...