Есть ли в Java SE 8 пары или кортежи?

185

Я играю с ленивыми функциональными операциями в Java SE 8, и я хочу mapиндексировать iпару / кортеж (i, value[i]), затем filterна основе второго value[i]элемента и, наконец, выводить только индексы.

Должен ли я все еще страдать так: Что эквивалентно паре C ++ <L, R> в Java? в смелую новую эпоху лямбд и ручьев?

Обновление: я представил довольно упрощенный пример, в котором есть аккуратное решение, предложенное @dkatzel в одном из ответов ниже. Однако это не обобщает. Поэтому позвольте мне добавить более общий пример:

package com.example.test;

import java.util.ArrayList;
import java.util.stream.IntStream;

public class Main {

  public static void main(String[] args) {
    boolean [][] directed_acyclic_graph = new boolean[][]{
        {false,  true, false,  true, false,  true},
        {false, false, false,  true, false,  true},
        {false, false, false,  true, false,  true},
        {false, false, false, false, false,  true},
        {false, false, false, false, false,  true},
        {false, false, false, false, false, false}
    };

    System.out.println(
        IntStream.range(0, directed_acyclic_graph.length)
        .parallel()
        .mapToLong(i -> IntStream.range(0, directed_acyclic_graph[i].length)
            .filter(j -> directed_acyclic_graph[j][i])
            .count()
        )
        .filter(n -> n == 0)
        .collect(() -> new ArrayList<Long>(), (c, e) -> c.add(e), (c1, c2) -> c1.addAll(c2))
    );
  }

}

Это дает неправильный вывод, [0, 0, 0]который соответствует количеству всех трех столбцов false. Что мне нужно, это индексы этих трех столбцов. Правильный вывод должен быть [0, 2, 4]. Как я могу получить этот результат?

некромант
источник
2
Там уже в AbstractMap.SimpleImmutableEntry<K,V>течение многих лет ... Но в любом случае, вместо отображения , iчтобы (i, value[i])только для фильтрации по value[i]и отображению обратно i: почему не только фильтр, value[i]в первую очередь, без отображения?
Хольгер
@Holger Мне нужно знать, какие индексы массива содержат значения, которые соответствуют критериям. Я не могу сделать это без сохранения iв потоке. Мне тоже нужны value[i]критерии. Вот почему мне нужно(i, value[i])
некромант
1
@necromancer Правильно, это работает, только если дешево получить значение из индекса, такого как массив, коллекция с произвольным доступом или недорогая функция. Думаю, проблема в том, что вы хотели представить упрощенный вариант использования, но он был упрощен и, таким образом, уступил специальному случаю.
Стюарт Маркс
1
@necromancer Я немного отредактировал последний абзац, чтобы уточнить вопрос, который, я думаю, ты задаешь. Это правильно? Кроме того, это вопрос о ориентированном (не ациклическом) графе? (Не то, чтобы это имело большое значение.) Наконец, должен ли быть желаемый результат [0, 2, 4]?
Стюарт Маркс
1
Я полагаю, что правильное решение для исправления этой проблемы - иметь будущие кортежи поддержки выпуска Java в качестве возвращаемого типа (как особый случай Object) и иметь лямбда-выражения, способные использовать такой кортеж напрямую для своих параметров.
Турбьёрн Равн Андерсен

Ответы:

206

ОБНОВЛЕНИЕ: Этот ответ является ответом на оригинальный вопрос: есть ли в Java SE 8 пары или кортежи? (И неявно, если нет, почему бы и нет?) ОП обновил вопрос более полным примером, но кажется, что его можно решить без использования какой-либо структуры Pair. [Примечание от ОП: вот другой правильный ответ .]


Краткий ответ: нет. Вы должны либо свернуть свою собственную, либо ввести одну из нескольких библиотек, которая ее реализует.

Наличие Pairкласса в Java SE было предложено и отклонено по крайней мере один раз. Смотрите эту ветку обсуждения в одном из списков рассылки OpenJDK. Компромиссы не очевидны. С одной стороны, существует много реализаций Pair в других библиотеках и в коде приложения. Это демонстрирует необходимость, и добавление такого класса в Java SE увеличит повторное использование и совместное использование. С другой стороны, наличие класса Pair увеличивает соблазн создания сложных структур данных из пар и коллекций без создания необходимых типов и абстракций. (Это парафраз сообщения Кевина Буриллиона из этой ветки .)

Я рекомендую всем прочитать всю эту электронную почту. Это удивительно проницательно и не имеет никакого пламени. Это довольно убедительно. Когда он начался, я подумал: «Да, в Java SE должен быть класс Pair», но к тому времени, когда поток достиг своего конца, я передумал.

Однако обратите внимание, что JavaFX имеет класс javafx.util.Pair . API JavaFX развивались отдельно от API Java SE.

Как видно из связанного вопроса, что является эквивалентом пары C ++ в Java? Существует довольно большое пространство дизайна, окружающее, по-видимому, такой простой API. Должны ли объекты быть неизменными? Должны ли они быть сериализуемыми? Должны ли они быть сопоставимы? Класс должен быть окончательным или нет? Стоит ли заказывать два элемента? Должен ли это быть интерфейс или класс? Зачем останавливаться на парах? Почему не тройки, четверки или N-кортежи?

И, конечно же, существует неизбежная система именования элементов:

  • (а, б)
  • (первая секунда)
  • (лево право)
  • (автомобиль, CDR)
  • (фу, бар)
  • и т.п.

Одна большая проблема, которая едва упоминалась, - это отношение пар к примитивам. Если у вас есть (int x, int y)геодезический , которая представляет собой точку в 2D пространстве, представляя это как Pair<Integer, Integer>потребляет три объекта вместо двух 32-битных слов. Кроме того, эти объекты должны находиться в куче и подвергаться GC-нагрузке.

Казалось бы, ясно, что, как и в Streams, важно, чтобы существовали примитивные специализации для пар. Хотим ли мы увидеть:

Pair
ObjIntPair
ObjLongPair
ObjDoublePair
IntObjPair
IntIntPair
IntLongPair
IntDoublePair
LongObjPair
LongIntPair
LongLongPair
LongDoublePair
DoubleObjPair
DoubleIntPair
DoubleLongPair
DoubleDoublePair

Даже IntIntPairесли все равно потребуется один объект в куче.

Это, конечно, напоминает распространение функциональных интерфейсов в java.util.functionпакете в Java SE 8. Если вы не хотите раздутого API, какие из них вы бы оставили? Вы также можете утверждать, что этого недостаточно, и что специализации, скажем, также Booleanследует добавить.

У меня такое ощущение, что если бы Java давно добавила класс Pair, это было бы просто или даже упрощенно, и это не удовлетворило бы многие варианты использования, которые мы предполагаем сейчас. Учтите, что если бы Pair был добавлен во временные рамки JDK 1.0, он, вероятно, был бы изменчивым! (Посмотрите на java.util.Date.) Будут ли люди довольны этим? Я предполагаю, что если бы в Java существовал класс Pair, он был бы своего рода не очень полезным, и каждый все равно будет крутить свои собственные, чтобы удовлетворить свои потребности, во внешних библиотеках были бы различные реализации Pair и Tuple, и люди все еще будут спорить / обсуждать, как исправить класс Pair в Java. Другими словами, вроде в том же месте, в котором мы находимся сегодня.

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

Стюарт Маркс
источник
3
@necromancer Фабричные методы с примитивами не помогают Pair<T,U>. Поскольку дженерики должны быть ссылочного типа. Любые примитивы будут упакованы, когда они будут сохранены. Для хранения примитивов вам действительно нужен другой класс.
Стюарт Маркс
3
@necromancer И да, оглядываясь назад, конструкторы в штучной упаковке не должны были быть открытыми и valueOfдолжны были быть единственным способом получить экземпляр в штучной упаковке. Но они были там начиная с Java 1.0 и, вероятно, не стоит пытаться изменить на этом этапе.
Стюарт Маркс
3
Очевидно, что должен быть только один открытый класс Pairили Tupleкласс с фабричным методом, создающим необходимые классы специализации (с оптимизированным хранилищем) прозрачно в фоновом режиме. В конце концов, лямбды делают именно это: они могут захватывать произвольное количество переменных произвольного типа. А теперь представьте себе языковую поддержку, позволяющую создавать соответствующий класс кортежей во время выполнения, запускаемый invokedynamicинструкцией…
Хольгер,
3
@Holger Нечто подобное могло бы сработать, если бы кто-то модернизировал типы значений в существующей JVM, но предложение «Типы значений» (теперь «Project Valhalla» ) гораздо более радикально. В частности, его типы значений не обязательно должны быть распределены в куче. Кроме того, в отличие от современных объектов и современных примитивов, ценности не будут иметь идентичности.
Стюарт Маркс
2
@Stuart Marks: Это не помешает, так как описанный мной тип может быть «коробочным» типом для такого типа значения. С invokedynamicфабрикой на основе, похожей на создание лямбды, такая последующая модернизация не будет проблемой. Кстати, лямбды тоже не имеют идентичности. Как прямо указано, идентичность, которую вы можете воспринимать сегодня, является артефактом текущей реализации.
Хольгер
46

Вы можете взглянуть на эти встроенные классы:

senerh
источник
3
Это правильный ответ, поскольку встроенный функционал для пар. Обратите внимание, что это SimpleImmutableEntryгарантирует только то, что ссылки, хранящиеся в папке Entrynot, не изменяются, а не то, что поля связанных объектов keyи valueобъектов (или объектов, на которые они ссылаются) не меняются.
Люк Хатчисон
22

К сожалению, в Java 8 не было пар или кортежей. Конечно, вы всегда можете использовать org.apache.commons.lang3.tuple (который я лично использую в сочетании с Java 8) или вы можете создавать свои собственные обертки. Или используйте Карты. Или что-то в этом роде, как объясняется в принятом ответе на тот вопрос, с которым вы связаны.


ОБНОВЛЕНИЕ: JDK 14 представляет записи в качестве функции предварительного просмотра. Это не кортежи, но их можно использовать для решения многих проблем. В вашем конкретном примере сверху это может выглядеть примерно так:

public class Jdk14Example {
    record CountForIndex(int index, long count) {}

    public static void main(String[] args) {
        boolean [][] directed_acyclic_graph = new boolean[][]{
                {false,  true, false,  true, false,  true},
                {false, false, false,  true, false,  true},
                {false, false, false,  true, false,  true},
                {false, false, false, false, false,  true},
                {false, false, false, false, false,  true},
                {false, false, false, false, false, false}
        };

        System.out.println(
                IntStream.range(0, directed_acyclic_graph.length)
                        .parallel()
                        .mapToObj(i -> {
                            long count = IntStream.range(0, directed_acyclic_graph[i].length)
                                            .filter(j -> directed_acyclic_graph[j][i])
                                            .count();
                            return new CountForIndex(i, count);
                        }
                        )
                        .filter(n -> n.count == 0)
                        .collect(() -> new ArrayList<CountForIndex>(), (c, e) -> c.add(e), (c1, c2) -> c1.addAll(c2))
        );
    }
}

При компиляции и запуске с JDK 14 (на момент написания, это ранняя сборка с доступом) с использованием --enable-previewфлага вы получите следующий результат:

[CountForIndex[index=0, count=0], CountForIndex[index=2, count=0], CountForIndex[index=4, count=0]]
blalasaadri
источник
На самом деле один из ответов @StuartMarks позволил мне решить его без кортежей, но, поскольку он, кажется, не обобщает, он, вероятно, понадобится в конце концов.
некромант
@necromancer Да, это очень хороший ответ. Иногда библиотека apache может пригодиться, но все сводится к дизайну языка Javas. По сути, кортежи должны быть примитивами (или похожими), чтобы работать так же, как и на других языках.
blalasaadri
1
Если вы этого не заметили, ответ содержал эту чрезвычайно информативную ссылку: cr.openjdk.java.net/~jrose/values/values-0.html о необходимости и перспективах таких примитивов, включая кортежи.
Некромант
17

Похоже, что полный пример может быть решен без использования какой-либо структуры Pair. Ключ заключается в том, чтобы фильтровать индексы столбцов с помощью предиката, проверяющего весь столбец, вместо сопоставления индексов столбцов с количеством falseзаписей в этом столбце.

Код, который делает это здесь:

    System.out.println(
        IntStream.range(0, acyclic_graph.length)
            .filter(i -> IntStream.range(0, acyclic_graph.length)
                                  .noneMatch(j -> acyclic_graph[j][i]))
            .boxed()
            .collect(toList()));

Это приводит к выводу, [0, 2, 4]который я считаю правильным результатом, запрошенным OP.

Также обратите внимание на boxed()операцию, которая упаковывает intзначения в Integerобъекты. Это позволяет использовать ранее существовавший toList()коллектор вместо того, чтобы выписывать функции коллектора, которые сами выполняют сборку.

Стюарт Маркс
источник
1
+1 туз в рукаве :) Это все еще не обобщает, верно? Это был более существенный аспект вопроса, потому что я ожидаю столкнуться с другими ситуациями, когда такая схема не будет работать (например, столбцы с не более чем 3 значениями true). Соответственно, я приму ваш другой ответ как правильный, но также укажу на этот! Большое спасибо :)
Некромант
Это правильно, но принимает другой ответ того же пользователя. (см. комментарии выше и в других местах.)
Некромант
1
@necromancer Правильно, этот метод не является полностью общим в тех случаях, когда вы хотите индекс, но элемент данных не может быть извлечен или вычислен с использованием индекса. (По крайней мере, нелегко.) Например, рассмотрим проблему, когда вы читаете строки текста из сетевого подключения, и вы хотите найти номер строки N-й строки, который соответствует некоторому шаблону. Самый простой способ - отобразить каждую строку в пару или некоторую составную структуру данных для нумерации строк. Хотя, вероятно, есть хакерский, побочный эффект сделать это без новой структуры данных.
Стюарт Маркс
@StuartMarks, пара - это <T, U>. тройной <T, U, V>. и т. д. Ваш пример - список, а не пара.
Пейсер
7

Vavr (ранее называемый Javaslang) ( http://www.vavr.io ) также предоставляет кортежи (размером до 8). Вот этот javadoc: https://static.javadoc.io/io.vavr/vavr/0.9.0/io/vavr/Tuple.html .

Это простой пример:

Tuple2<Integer, String> entry = Tuple.of(1, "A");

Integer key = entry._1;
String value = entry._2;

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

wumpz
источник
В некоторых версиях vavr используются подлые броски под капотом. Будьте осторожны, чтобы не использовать их.
Торбьерн Равн Андерсен
7

Начиная с Java 9, вы можете создавать экземпляры Map.Entryпроще, чем раньше:

Entry<Integer, String> pair = Map.entry(1, "a");

Map.entryвозвращает неизменяемое Entryи запрещает нули.

ZhekaKozlov
источник
6

Поскольку вы заботитесь только об индексах, вам вообще не нужно отображать кортежи. Почему бы просто не написать фильтр, который использует элементы поиска в вашем массиве?

     int[] value =  ...


IntStream.range(0, value.length)
            .filter(i -> value[i] > 30)  //or whatever filter you want
            .forEach(i -> System.out.println(i));
dkatzel
источник
+1 за отличное, практичное решение. Однако я не уверен, обобщает ли это мою ситуацию, когда я генерирую значения на лету. Я поставил свой вопрос в виде массива, чтобы предложить простой пример для размышления, и вы нашли отличное решение.
некромант
5

Да.

Map.Entryможет быть использован в качестве Pair.

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

До тех пор, есть только небольшая польза.

РЕДАКТИРОВАТЬ 2018-02-12: Работая над проектом, я написал вспомогательный класс, который помогает обрабатывать особый случай наличия идентификатора раньше в потоке, который вам нужен позже, но промежуточная часть потока не знает об этом. Пока я не смогу выпустить его самостоятельно, он доступен на IdValue.java с модульным тестом на IdValueTest.java.

Турбьерн Равн Андерсен
источник
2

Eclipse Collections имеет Pairи все комбинации пар примитивов / объектов (для всех восьми примитивов).

TuplesЗавод может создавать экземпляры Pair, и PrimitiveTuplesзавод может быть использован для создания всех комбинаций примитивного пара / объектов.

Мы добавили их до выпуска Java 8. Они были полезны для реализации итераторов ключ / значение для наших примитивных карт, которые мы также поддерживаем во всех комбинациях примитивов / объектов.

Если вы готовы добавить дополнительные накладные расходы библиотеки, вы можете использовать принятое решение Стюарта и собрать результаты в примитив, IntListчтобы избежать упаковки. Мы добавили новые методы в Eclipse Collections 9.0, чтобы позволить Int/Long/Doubleсоздавать коллекции из Int/Long/Doubleпотоков.

IntList list = IntLists.mutable.withAll(intStream);

Примечание: я являюсь коммиттером для Eclipse Collections.

Дональд Рааб
источник