Как сообщить, что порядок вставки имеет значение на карте?

24

Я выбираю набор кортежей из базы данных и помещаю их в карту. Запрос к базе данных является дорогостоящим.

Не существует очевидного естественного упорядочения элементов на карте, но, тем не менее, порядок вставки имеет значение. Сортировка карты была бы тяжелой операцией, поэтому я хочу избежать этого, учитывая, что результат запроса уже отсортирован так, как я хочу. Поэтому я просто сохраняю результат запроса в LinkedHashMapи возвращаю карту из метода DAO:

public LinkedHashMap<Key, Value> fetchData()

У меня есть метод, processDataкоторый должен выполнить некоторую обработку на карте - изменение некоторых значений, добавление некоторых новых ключей / значений. Определяется как

public void processData(LinkedHashMap<Key, Value> data) {...}

Однако несколько линтеров (Sonar и т. Д.) Жалуются, что типом «данных» должен быть интерфейс, такой как «Map», а не реализация «LinkedHashMap» ( squid S1319 ).
Так что в основном это говорит о том, что я должен иметь

public void processData(Map<Key, Value> data) {...}

Но я хочу, чтобы сигнатура метода говорила, что порядок отображения имеет значение - это имеет значение для алгоритма processData- чтобы мой метод не передавался просто какой-либо случайной карте.

Я не хочу использовать SortedMap, потому что он (из javadocjava.util.SortedMap ) "упорядочен в соответствии с естественным упорядочением его ключей или компаратором, обычно предоставляемым во время создания отсортированной карты".

Мои ключи не имеют естественного порядка , и создание Comparator, который ничего не делает, кажется многословным.

И я все еще хотел бы, чтобы это была карта, putпозволяющая избежать дублирования ключей и т. Д. Если бы не так, это dataмогло бы быть List<Map.Entry<Key, Value>>.

Итак, как мне сказать, что мой метод хочет карту, которая уже отсортирована ? К сожалению, нет java.util.LinkedMapинтерфейса, или я бы использовал это.

Видар С. Рамдал
источник

Ответы:

56

Так что пользуйтесь LinkedHashMap.

Да , вы должны использовать Mapнад конкретной реализации всякий раз , когда это возможно, и да , это является лучшей практики.

Тем не менее, это странно специфическая ситуация, когда реализация на Mapсамом деле имеет значение. Это не будет верно для 99,9% случаев в вашем коде, когда вы используете Map, и все же вы здесь, в этой ситуации 0,1%. Sonar не может этого знать, и поэтому Sonar просто советует вам избегать использования конкретной реализации, поскольку в большинстве случаев это будет правильным.

Я бы сказал, что если вы можете обосновать использование конкретной реализации, не пытайтесь наносить помаду на свинью. Тебе нужна, а LinkedHashMapне а Map.

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

Нил
источник
1
Прагматичный подход, который мне нравится.
Видар С. Рамдал
20
Я почти полностью согласен с ответом. Я бы просто сказал, что вы не прокляты для сонара. Вы всегда можете настроить его, чтобы игнорировать эту конкретную ошибку / предупреждение. См stackoverflow.com/questions/10971968/...
Владимир Стокичем
11
if you are new to programming and stumble upon this answer, don't think this allows you to go against best practice because it doesn't.- Хороший совет, если бы существовала такая вещь, как «лучшая практика». Лучший совет: научитесь принимать правильные решения. Следуйте практике, если это имеет смысл, но позвольте инструментам и властям направлять ваш мыслительный процесс, а не диктовать его.
Роберт Харви
13
Примечание: когда гидролокатор сообщает вам что-то, вы можете закрыть его как «не будет решено» и оставить примечание, почему вы этого не сделаете. Таким образом, не только гидролокатор перестанет беспокоить вас, но и вы поймете, почему вы это сделали.
Вальфрат
2
Я думаю, что аспект, который делает это исключением из общего принципа, заключается в том, что LinkedHashMap имеет контракт, который является специфическим для этой реализации и не выражен в каком-либо интерфейсе. Это не обычный случай. Таким образом, единственный способ выразить зависимость от этого контракта - использовать тип реализации.
Дана
21

Вы боретесь с тремя вещами:

Во-первых, это библиотека контейнеров Java. Ничто в его таксономии не дает вам способа определить, повторяется ли класс в предсказуемом порядке. Нет IteratesInInsertedOrderMapинтерфейса, который мог бы быть реализован LinkedHashMap, что делает проверку типов (и использование альтернативных реализаций, которые ведут себя одинаково) невозможной. Вероятно, так задумано, потому что суть в том, что вы действительно должны иметь дело с объектами, которые ведут себя как абстракция Map.

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

В-третьих, и это, вероятно, суть этого, LinkedHashMapможет быть не тот инструмент для работы. Карты предназначены для случайного, а не упорядоченного доступа. Если вы processData()просто перебираете записи по порядку и не нуждаетесь в поиске других записей по ключу, вы заставляете конкретную реализацию Mapделать работу a List. С другой стороны, если вам требуется и то, и другое, LinkedHashMapэто правильный инструмент, потому что известно, что он делает то, что вы хотите, и вы более чем оправданы в этом.

Blrfl
источник
2
«LinkedHashMap может быть неправильным инструментом для работы». Да, может быть. Когда я говорю, что мне нужно OrderedMap, я точно так же могу сказать UniqueList. Пока это какая-то коллекция с определенным порядком итераций, она перезаписывает дубликаты при вставке.
Видар С. Рамдал
2
@ VidarS.Ramdal Запрос к базе данных будет идеальным местом, чтобы отсеять дубликаты. Если ваша база данных не может этого сделать, вы всегда можете оставить временные Setключи только во время создания списка a, чтобы найти их.
Blrfl
О, я вижу, я вызвал замешательство. Да, результат запроса к базе данных не содержит дубликатов. Но processDataизменяет карту, заменяя некоторые значения, вводя новые ключи / значения. Таким образом, processDataможно ввести дубликаты, если он работает на чем-то, кроме Map.
Видар С. Рамдал
7
@ VidarS.Ramdal: Похоже, вам нужно написать свой собственный UniqueList(или OrderedUniqueList) и использовать это. Это довольно просто, и делает ваше предполагаемое использование более понятным.
TMN
2
@ TMN Да, я начал думать в этом направлении. Если вы хотите опубликовать свое предложение в качестве ответа, оно наверняка получит мое одобрение.
Видар С. Рамдал
15

Если все, от чего вы получаете, LinkedHashMap- это возможность перезаписывать дубликаты, но вы действительно используете их как единое целое List, то я бы посоветовал лучше связать это использование с вашей собственной пользовательской Listреализацией. Вы можете ее на основе существующего Java - классе коллекций и просто переопределить любые addи removeметоды , чтобы обновить резервное хранилище и следить ключ для обеспечения уникальности. Если вы дадите этому отличительному имени как, то ProcessingListстанет ясно, что аргументы, представленные вашему processDataметоду, должны обрабатываться особым образом.

TMN
источник
5
Это может быть хорошей идеей в любом случае. Черт возьми, у вас даже может быть однострочный файл, который создается ProcessingListкак псевдоним для LinkedHashMap- вы всегда можете решить заменить его чем-то другим позже, если вы оставите публичный интерфейс без изменений.
CompuChip
11

Я слышу, как вы говорите: «У меня есть одна часть моей системы, которая создает LinkedHashMap, а в другой части моей системы мне нужно принимать только объекты LinkedHashMap, которые были созданы первой частью, так как объекты, созданные каким-то другим процессом, выиграли» не работает правильно. "

Это заставляет меня думать, что проблема здесь заключается в том, что вы на самом деле пытаетесь использовать LinkedHashMap, поскольку он в основном соответствует данным, которые вы ищете, но на самом деле его нельзя заменить каким-либо другим экземпляром, кроме тех, которые вы создаете. Что вы на самом деле хотите сделать, так это создать свой собственный интерфейс / класс, который создает ваша первая часть, а ваша вторая часть потребляет. Он может обернуть «реальный» LinkedHashMap и обеспечить получение карты или реализовать интерфейс карты.

Это немного отличается от ответа CandiedOrange тем, что я бы рекомендовал инкапсулировать реальную карту (и делегировать ей вызовы по мере необходимости), а не расширять ее. Иногда это одна из тех священных войн в стиле, но мне кажется, что это не «Карта с какими-то дополнительными вещами», это «Моя сумка полезной информации о состоянии, которую я могу внутренне представить с помощью Карты».

Если бы у вас было две переменные, которые вам нужно было бы передать таким образом, вы, вероятно, создали бы для него класс, не задумываясь об этом. Но иногда полезно иметь класс, даже если это всего лишь одна переменная-член, просто потому, что это логически одно и то же, не «значение», а «результат моей операции, с которой мне нужно что-то сделать позже».


источник
Мне это нравится думать - я был там :) MyBagOfUsefulInformationбудет нужен метод (или конструктор) для заполнения его: MyBagOfUsefulInformation.populate(SomeType data). Но dataдолжен быть отсортированный результат запроса. Так что будет SomeType, если нет LinkedHashMap? Я не уверен, что смогу сломать этот улов 22.
Видар С. Рамдал
Почему MyBagOfUsefulInformationDAO не может быть создан или кто-то еще генерирует данные в вашей системе? Зачем вам вообще нужно показывать основную карту остальному коду за пределами производителя и потребителя пакета?
В зависимости от вашей архитектуры вы можете использовать частный / защищенный / конструктор только для пакетов, чтобы обеспечить создание объекта только тем производителем, которому вы его хотите. Или вам может просто потребоваться сделать это как соглашение, что это может быть создано только правильной «фабрикой».
Да, в итоге я сделал нечто похожее, передав MyBagOfUsefulInformationпараметр DAO в качестве параметра: softwareengineering.stackexchange.com/a/360079/52573
Видар С. Рамдал
4

LinkedHashMap - единственная карта Java, которая имеет функцию порядка вставки, которую вы ищете. Поэтому отказ от Принципа обращения зависимостей заманчив и, возможно, даже практичен. Сначала подумайте, что нужно сделать, чтобы следовать ему. Вот что SOLID попросит вас сделать.

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

public interface Ramdal {
    //ISP asks for just the methods that processData() actually uses.
    ...
}

public class RamdalLinkedHashMap extends LinkedHashMap implements Ramdal{} 

Ramdal<Key, Value> ramdal = new RamdalLinkedHashMap<>();

ramdal.put(key1, value1);
ramdal.put(key2, value2);

processData(ramdal);

Это большой дизайн впереди? Возможно, это зависит от того, насколько вероятно, что вы когда-либо будете нуждаться в реализации LinkedHashMap. Но если вы не следуете DIP только потому, что это будет огромной болью, я не думаю, что котельная плита более болезненна, чем эта. Это шаблон, который я использую, когда хочу, чтобы неприкасаемый код реализовал интерфейс, которого нет. Самая болезненная часть действительно думает о хороших именах.

candied_orange
источник
2
Мне нравится название!
Видар С. Рамдал
1

Спасибо за хорошее предложение и пищу для размышлений.

В итоге я расширил создание нового класса карты, создав processDataметод экземпляра:

class DataMap extends LinkedHashMap<Key, Value> {

   processData();

}

Затем я реорганизовал метод DAO, чтобы он не возвращал карту, а вместо этого принимает targetкарту в качестве параметра:

public void fetchData(Map<Key, Value> target) {
  ...
  // for each result row
  target.put(key, value);
}

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

public DataMap fetchDataMap() {
  var dataMap = new DataMap();
  dao.fetchData(dataMap);
  return dataMap;
}

Это позволяет моей реализации Map управлять тем, как в нее вставляются записи, и скрывает требование упорядочения - теперь это деталь реализации DataMap.

Видар С. Рамдал
источник
0

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

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

Kapol
источник
0

Итак, позвольте мне попытаться понять ваш контекст здесь:

... порядок вставки имеет значение ... Сортировка карты была бы тяжелой операцией ...

... результат запроса уже отсортирован так, как я хочу

Теперь, что вы в настоящее время уже делаете:

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

А вот ваш текущий код:

public void processData(LinkedHashMap<Key, Value> data) {...}

Я предлагаю сделать следующее:

  • Используйте внедрение зависимостей и внедрите некоторые MyTupleRepository в метод обработки (MyTupleRepository - это интерфейс, реализованный объектами, которые извлекают ваши объекты кортежей, обычно из БД);
  • внутренне к методу обработки, поместите данные из репозитория (он же DB, который уже возвращает упорядоченные данные) в конкретную коллекцию LinkedHashMap, потому что это внутренняя деталь алгоритма обработки (потому что это зависит от того, как данные расположены в структуре данных );
  • Обратите внимание, что это в значительной степени то, что вы уже делаете, но в этом случае это будет сделано в рамках метода обработки. Ваш репозиторий создается где-то еще (у вас уже есть класс, который возвращает данные, это репозиторий в этом примере)

Пример кода

public interface MyTupleRepository {
    Collection<MyTuple> GetAll();
}

//Concrete implementation of data access object, that retrieves 
//your tuples from DB; this data is already ordered by the query
public class DbMyTupleRepository implements MyTupleRepository { }

//Injects some abstraction of repository into the processing method,
//but make it clear that some exception might be thrown if data is not
//arranged in some specific way you need
public void processData(MyTupleRepository tupleRepo) throws DataNotOrderedException {

    LinkedHashMap<Key, Value> data = new LinkedHashMap<Key, Value>();

    //Represents the query to DB, that already returns ordered data
    Collection<MyTuple> myTuples = tupleRepo.GetAll();

    //Optional: this would throw some exception if data is not ordered 
    Validate(myTuples);

    for (MyTupleData t : myTuples) {
        data.put(t.key, t.value);
    }

    //Perform the processing using LinkedHashMap...
    ...
}

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

Эмерсон Кардосо
источник
Хм, но как создать хранилище? Не будет ли это просто перенести проблему куда-то еще (туда, где MyTupleRepositoryона создается?)
Видар С. Рамдал
Я думаю, что столкнусь с той же проблемой, что и с ответом Питера Купера .
Видар С. Рамдал
Мое предложение включает в себя применение принципа внедрения зависимости; в этом примере; MyTupleRepository - это интерфейс, который определяет возможность получения упомянутых вами кортежей (которые запрашивают БД). Здесь вы вводите этот объект в метод обработки. У вас уже есть некоторый класс, который возвращает данные; это только абстрагирует его в интерфейсе, и вы вводите объект в метод 'processData', который внутренне использует LinkedHashMap, потому что это неотъемлемая часть обработки.
Эмерсон Кардосо
Я отредактировал свой ответ, пытаясь понять, что я предлагаю.
Эмерсон Кардосо
-1

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

Проблема 1: Вы не можете зависеть от порядка БД

Ваше описание сортировки ваших данных не ясно.

  • Самая большая потенциальная проблема заключается в том, что вы не указываете явную сортировку в своей базе данных через ORDER BYпредложение. Если вы не потому, что это кажется слишком дорогим, ваша программа имеет ошибку . Базы данных могут возвращать результаты в любом порядке, если вы их не указали; вы не можете полагаться на то, что данные будут возвращаться в порядке совпадения только потому, что вы выполнили запрос несколько раз, и он выглядит так. Порядок может измениться, потому что строки переставляются на диске, или некоторые удаляются, а новые занимают их место, или добавляется индекс. Вы должны указать какой-то ORDER BYпункт. Скорость бесполезна без правильности.
  • Также не ясно, что вы подразумеваете под порядком вставки. Если вы говорите о самой базе данных, у вас должен быть столбец, который фактически отслеживает это, и он должен быть включен в ваше ORDER BYпредложение. В противном случае у вас есть ошибки. Если такого столбца еще не существует, вам нужно добавить его. Типичными параметрами для таких столбцов могут быть столбец с меткой времени вставки или автоинкрементный ключ. Ключ автоинкремента более надежен.

Проблема 2: Эффективная сортировка в памяти

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

Виола. Теперь вам больше не нужно зависеть от порядка набора результатов базы данных.

Проблема 3: Вам даже нужно выполнить эту обработку в коде?

SQL на самом деле действительно мощный. Это удивительный декларативный язык, который позволяет вам выполнять множество преобразований и агрегаций в ваших данных. Большинство БД в настоящее время даже поддерживают операции с несколькими строками. Они называются оконными или аналитическими функциями:

Тебе даже нужно вытащить свои данные в память, как это? Или вы могли бы выполнить всю работу в запросе SQL с помощью оконных функций? Если вы можете сделать всю (или, может быть, даже значительную часть) работы в БД, это просто фантастика! Ваша проблема с кодом исчезнет (или станет намного проще)!

Проблема 4: Вы делаете что к этому data?

Предполагая, что вы не можете сделать все это в БД, позвольте мне сделать это прямо. Вы берете данные как карту (которая основана на вещах, по которым вы не хотите сортировать), затем вы перебираете их в порядке вставки и модифицируете карту на месте, заменяя значение некоторых ключей и добавляя новые?

Извините, но какого чёрта?

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

Вот, возможно, лучшая идея:

  • Есть ваша функция принимает List.
  • Есть несколько способов справиться с проблемой заказа.
    1. Apply Fail Fast. Выдает ошибку, если список не в том порядке, который требуется для функции. (Примечание. Вы можете использовать индекс сортировки из Задачи 2, чтобы узнать, так ли это.)
    2. Создайте отсортированную копию самостоятельно (снова используя индекс из задачи 2).
    3. Придумайте способ построения самой карты по порядку.
  • Создайте карту, которая вам нужна, внутри функции, чтобы вызывающий не заботился об этом.
  • Теперь итерируйте все, что у вас есть в порядке представления, и делайте то, что вам нужно.
  • Вернуть карту или преобразовать ее в соответствующее возвращаемое значение

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

Или, может быть, в этом есть больше смысла: избавиться от dataпараметра и заставить processDataфактически извлекать его собственные данные. Затем вы можете задокументировать, что вы делаете это, потому что у него очень специфические требования к способу извлечения данных. Другими словами, сделайте функцию владельцем всего процесса, а не только его части; взаимозависимости слишком сильны, чтобы разбить логику на более мелкие куски. (Измените имя функции в процессе.)

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

Резюме

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

Найдите кого-то, кто получает эту точку зрения: ваша задача сводить вашу проблему к набору простых, простых. Затем вы можете создать надежный, интуитивно понятный код. Поговори с ними. Хороший код и хороший дизайн заставляют вас думать, что любой идиот мог придумать их, потому что они просты и понятны. Может быть, есть старший разработчик, у которого есть такое мышление, с которым вы можете поговорить.

jpmc26
источник
«Что вы имеете в виду, что нет естественного порядка, но порядок вставки имеет значение? Вы говорите, что имеет значение, в каком порядке данные были вставлены в таблицу БД, но у вас нет столбца, который мог бы сказать вам, какие элементы порядка были вставлены?» - вопрос гласит: «Сортировка карты была бы тяжелой операцией, поэтому я хочу избежать этого, учитывая, что результат запроса уже отсортирован». Это ясно указывает на то , что является рассчитываемая определенный порядок в данных, так как в противном случае его сортировку было бы невозможно , а не тяжелым, но определенный порядок отличается от естественного порядка ключей.
Жюль
2
Другими словами, OP работает с результатами запроса select key, value from table where ... order by othercolumnи должен поддерживать порядок при их обработке. Порядок вставки, на который они ссылаются, - это порядок вставки в их карту , определяемый порядком, используемым в запросе, а не порядком вставки в базу данных . Это становится понятным благодаря их использованию LinkedHashMap, которое представляет собой структуру данных, которая имеет характеристики как пары a, так Mapи Listпары ключей и значений.
Жюль
@Jules Я немного почистю этот раздел, спасибо. (Я на самом деле вспомнил, что читал это, но когда я проверял вещи во время написания вопроса, я не мог его найти. LOL. Я тоже попал в сорняки.) Но вопрос не ясен о том, что они делают с БД запрос и есть ли у них явная сортировка или нет. Они также говорят, что «порядок ввода имеет значение». Дело в том, что даже если сортировка тяжелая, вы не можете полагаться на то, что БД просто волшебным образом упорядочит все правильно, если вы не скажете это явно. А если будут делать это в БД, то вы можете использовать «индекс» , чтобы сделать его эффективным в коде.
jpmc26
* написание ответа (Methinks, я должен скоро лечь спать.)
jpmc26
Да, Джулс прав. Там естьorder by пункт в запросе, но это не является тривиальным ( не только order by column), поэтому я хочу , чтобы избежать повторной реализации сортировки в Java. Хотя SQL является мощным (и мы говорим здесь о базе данных Oracle 11g), природа processDataалгоритма значительно упрощает его выражение в Java. И да, «порядок вставки» означает « порядок вставки карты », то есть порядок результата запроса.
Видар С. Рамдал