O Java SE 8 tem pares ou tuplas?

185

Estou brincando com operações funcionais preguiçosas no Java SE 8 e quero mapum índice ipara um par / tupla (i, value[i]), depois com filterbase no segundo value[i]elemento e, finalmente, produzir apenas os índices.

Ainda devo sofrer o seguinte: Qual é o equivalente do par C ++ <L, R> em Java? na nova era ousada de lambdas e córregos?

Atualização: apresentei um exemplo bastante simplificado, que tem uma solução interessante oferecida pelo @dkatzel em uma das respostas abaixo. No entanto, não generaliza. Portanto, deixe-me adicionar um exemplo mais geral:

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))
    );
  }

}

Isso fornece uma saída incorreta,[0, 0, 0] que corresponde às contagens das três colunas que são todas false. O que eu preciso são os índices dessas três colunas. A saída correta deve ser [0, 2, 4]. Como posso obter esse resultado?

necromante
fonte
2
Há já AbstractMap.SimpleImmutableEntry<K,V>há anos ... Mas de qualquer maneira, em vez de mapeamento ipara (i, value[i])apenas para filtrar por value[i]e mapeamento de volta para i: porque não basta filtro value[i]em primeiro lugar, sem o mapeamento?
Holger
@ Holger Preciso saber quais índices de uma matriz contêm valores que correspondem a um critério. Não posso fazer isso sem preservar ino fluxo. Eu também preciso value[i]dos critérios. É por isso que eu preciso(i, value[i])
necromancer
1
@ necromancer Certo, só funciona se for barato obter o valor do índice, como uma matriz, uma coleção de acesso aleatório ou uma função barata. Acho que o problema é que você queria apresentar um caso de uso simplificado, mas foi simplificado demais e, portanto, sucumbiu a um caso especial.
Stuart Marcas
1
@ necromancer Editei um pouco o último parágrafo para esclarecer a pergunta que acho que você está fazendo. Está certo? Além disso, essa é uma pergunta sobre um gráfico direcionado (não acíclico)? (Não que isso importe muito.) Finalmente, a saída desejada deve ser [0, 2, 4]?
Stuart Marcas
1
Acredito que a solução certa para corrigir isso é ter uma tupla futura de suporte a release Java como um tipo de retorno (como um caso especial de Object) e ter expressões lambda capazes de usar essa tupla diretamente para seus parâmetros.
Thorbjørn Ravn Andersen

Respostas:

206

ATUALIZAÇÃO: Esta resposta é uma resposta à pergunta original, o Java SE 8 possui pares ou tuplas? (E implicitamente, se não, por que não?) O OP atualizou a pergunta com um exemplo mais completo, mas parece que pode ser resolvido sem o uso de qualquer tipo de estrutura de pares. [Nota do OP: aqui está a outra resposta correta .]


A resposta curta é não. Você precisa rolar por conta própria ou trazer uma das várias bibliotecas que a implementa.

Ter uma Pairaula no Java SE foi proposto e rejeitado pelo menos uma vez. Veja este tópico de discussão em uma das listas de discussão do OpenJDK. As compensações não são óbvias. Por um lado, existem muitas implementações de pares em outras bibliotecas e no código do aplicativo. Isso demonstra uma necessidade e a inclusão dessa classe no Java SE aumentará a reutilização e o compartilhamento. Por outro lado, ter uma classe Pair aumenta a tentação de criar estruturas de dados complicadas a partir de pares e coleções sem criar os tipos e abstrações necessários. (Essa é uma paráfrase da mensagem de Kevin Bourillion desse tópico.)

Eu recomendo que todos leiam todo esse tópico de email. É notavelmente perspicaz e não tem flamage. É bastante convincente. Quando começou, pensei: "Sim, deveria haver uma classe Pair no Java SE", mas quando o segmento chegou ao fim, eu havia mudado de idéia.

Observe, no entanto, que o JavaFX possui a classe javafx.util.Pair . As APIs do JavaFX evoluíram separadamente das APIs do Java SE.

Como se pode ver na questão vinculada Qual é o equivalente do par C ++ em Java? existe um espaço de design bastante grande em torno do que aparentemente é uma API tão simples. Os objetos devem ser imutáveis? Eles devem ser serializáveis? Eles deveriam ser comparáveis? A aula deve ser final ou não? Os dois elementos devem ser ordenados? Deve ser uma interface ou uma classe? Por que parar em pares? Por que não triplas, quads ou N-tuplas?

E, claro, há a inevitável nomeação de bikeshed para os elementos:

  • (a, b)
  • (primeiro segundo)
  • (esquerda direita)
  • (carro, cdr)
  • (foo, bar)
  • etc.

Um grande problema que quase não foi mencionado é o relacionamento dos pares com os primitivos. Se você possui um (int x, int y)dado que representa um ponto no espaço 2D, representá-lo como Pair<Integer, Integer>consome três objetos em vez de duas palavras de 32 bits. Além disso, esses objetos devem residir no heap e incorrem em sobrecarga do GC.

Parece claro que, como Streams, seria essencial haver especializações primitivas para Pares. Queremos ver:

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

Mesmo um IntIntPairainda exigiria um objeto na pilha.

Naturalmente, elas lembram a proliferação de interfaces funcionais no java.util.functionpacote no Java SE 8. Se você não quer uma API inchada, quais você deixaria de fora? Você também pode argumentar que isso não é suficiente e que especializações para, por exemplo, Booleandevem ser adicionadas também.

Meu sentimento é que se o Java tivesse adicionado uma classe Pair há muito tempo, seria simples, ou até simplista, e não teria satisfeito muitos dos casos de uso que estamos imaginando agora. Considere que se o Pair tivesse sido adicionado no período de tempo do JDK 1.0, provavelmente teria sido mutável! (Veja java.util.Date.) As pessoas ficariam felizes com isso? Meu palpite é que, se houvesse uma classe Pair em Java, seria meio que não muito útil e todo mundo continuaria se adaptando para satisfazer suas necessidades, haveria várias implementações Pair e Tuple em bibliotecas externas, e as pessoas ainda estariam discutindo / discutindo sobre como corrigir a classe Pair do Java. Em outras palavras, meio que no mesmo lugar em que estamos hoje.

Enquanto isso, algum trabalho está em andamento para resolver a questão fundamental, que é o melhor suporte na JVM (e eventualmente na linguagem Java) para tipos de valor . Consulte este documento Estado dos valores . Este é um trabalho preliminar e especulativo, e cobre apenas questões da perspectiva da JVM, mas já tem uma boa quantidade de pensamento por trás. Obviamente, não há garantias de que isso chegue ao Java 9, ou que entre em qualquer lugar, mas mostra a direção atual de se pensar neste tópico.

Stuart Marks
fonte
3
@ necromancer Os métodos de fábrica com primitivos não ajudam Pair<T,U>. Como os genéricos devem ser do tipo de referência. Quaisquer primitivas serão colocadas em caixa quando forem armazenadas. Para armazenar primitivos, você realmente precisa de uma classe diferente.
Stuart Marcas
3
@ necromancer E sim, em retrospecto, os construtores primitivos em caixa não deveriam ser públicos e valueOfdeveriam ter sido a única maneira de obter uma instância em caixa. Mas eles estão lá desde o Java 1.0 e provavelmente não valem a pena tentar mudar neste momento.
Stuart Marcas
3
Obviamente, deve haver apenas um público Pairou Tupleclasse com um método de fábrica, criando as classes de especialização necessárias (com armazenamento otimizado) de forma transparente em segundo plano. No final, os lambdas fazem exatamente isso: eles podem capturar um número arbitrário de variáveis ​​do tipo arbitrário. E agora uma imagem de suporte de linguagem que permite criar a classe tupla apropriado em tempo de execução desencadeada por uma invokedynamicinstrução ...
Holger
3
@ Holger Algo assim poderia funcionar se alguém estivesse adaptando tipos de valor para a JVM existente, mas a proposta de Tipos de Valor (agora "Projeto Valhalla" ) é muito mais radical. Em particular, seus tipos de valor não seriam necessariamente alocados em heap. Além disso, diferentemente dos objetos de hoje, e como os primitivos de hoje, os valores não teriam identidade.
Stuart Marcas
2
@ Stuart Marks: Isso não interferiria, pois o tipo que eu descrevi poderia ser o tipo “encaixotado” para esse tipo de valor. Com uma invokedynamicfábrica baseada semelhante à criação de lambda, essa adaptação posterior não seria problema. A propósito, as lambdas também não têm identidade. Conforme explicitamente declarado, a identidade que você pode perceber hoje é um artefato da implementação atual.
Holger
46

Você pode dar uma olhada nessas classes internas:

senerh
fonte
3
Esta é a resposta correta, tanto quanto a funcionalidade interna para pares. Observe que SimpleImmutableEntryapenas garante que as referências armazenadas no Entryarquivo não sejam alteradas, e que os campos dos objetos keye vinculados value(ou dos objetos aos quais eles vinculam) não sejam alterados.
Luke Hutchison
22

Infelizmente, o Java 8 não introduziu pares ou tuplas. Você sempre pode usar org.apache.commons.lang3.tuple, é claro (que pessoalmente uso em combinação com Java 8) ou pode criar seus próprios wrappers. Ou use o Maps. Ou coisas assim, conforme explicado na resposta aceita para a pergunta à qual você vinculou.


ATUALIZAÇÃO: O JDK 14 está introduzindo registros como um recurso de visualização. Essas não são tuplas, mas podem ser usadas para salvar muitos dos mesmos problemas. No seu exemplo específico acima, isso pode ser algo como isto:

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))
        );
    }
}

Quando compilado e executado com o JDK 14 (no momento da redação, este é um build de acesso antecipado) usando o --enable-previewsinalizador, você obtém o seguinte resultado:

[CountForIndex[index=0, count=0], CountForIndex[index=2, count=0], CountForIndex[index=4, count=0]]
blalasaadri
fonte
Na verdade, uma das respostas de @StuartMarks me permitiu resolvê-lo sem tuplas, mas como não parece generalizar, provavelmente vou precisar disso eventualmente.
Necromancer
@ necromancer Sim, é uma resposta muito boa. A biblioteca apache ainda pode ser útil às vezes, mas tudo se resume ao design da linguagem Javas. Basicamente, as tuplas teriam que ser primitivas (ou similares) para funcionar como em outros idiomas.
precisa saber é o seguinte
1
Caso você não tenha notado, a resposta incluía este link extremamente informativo: cr.openjdk.java.net/~jrose/values/values-0.html sobre a necessidade e as perspectivas de tais primitivos, incluindo tuplas.
Necromancer
17

Parece que o exemplo completo pode ser resolvido sem o uso de qualquer tipo de estrutura de pares. A chave é filtrar os índices da coluna, com o predicado verificando a coluna inteira, em vez de mapear os índices da coluna para o número de falseentradas nessa coluna.

O código que faz isso está aqui:

    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()));

Isso resulta em resultado do [0, 2, 4]qual acho que o resultado correto solicitado pelo OP.

Observe também a boxed()operação que coloca os intvalores em Integerobjetos. Isso permite que você use o toList()coletor preexistente , em vez de precisar escrever funções do coletor que fazem o boxe.

Stuart Marks
fonte
1
+1 ás na manga :) Isso ainda não generaliza, certo? Esse foi o aspecto mais substancial da questão, porque espero enfrentar outras situações em que um esquema como esse não funcione (por exemplo, colunas com no máximo 3 valores true). Consequentemente, aceitarei sua outra resposta como correta, mas também aponto para essa! Muito obrigado :)
necromante
Isso está correto, mas aceitando a outra resposta pelo mesmo usuário. (ver comentários acima e noutros locais.)
necromante
1
@ necromancer Certo, essa técnica não é totalmente geral nos casos em que você deseja o índice, mas o elemento de dados não pode ser recuperado ou calculado usando o índice. (Pelo menos não facilmente.) Por exemplo, considere um problema em que você está lendo linhas de texto de uma conexão de rede e deseja encontrar o número da linha Nésima linha que corresponda a algum padrão. A maneira mais fácil é mapear cada linha em um par ou em alguma estrutura de dados composta para numerar as linhas. Provavelmente, existe uma maneira hacky e eficaz de fazer isso sem uma nova estrutura de dados.
Stuart Marcas
@StuartMarks, um par é <T, U>. um triplo <T, U, V>. etc. Seu exemplo é uma lista, não um par.
Pacerier
7

O Vavr (anteriormente chamado Javaslang) ( http://www.vavr.io ) também fornece tuplas (até 8). Aqui está o javadoc: https://static.javadoc.io/io.vavr/vavr/0.9.0/io/vavr/Tuple.html .

Este é um exemplo simples:

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

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

Por que o JDK em si não veio com um tipo simples de tuplas até agora é um mistério para mim. Escrever classes de wrapper parece ser um negócio diário.

wumpz
fonte
Algumas versões do vavr usavam arremessos furtivos sob o capô. Cuidado para não usá-los.
Thorbjørn Ravn Andersen
7

Desde o Java 9, você pode criar instâncias Map.Entrymais fáceis do que antes:

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

Map.entryretorna um não modificável Entrye proíbe nulos.

ZhekaKozlov
fonte
6

Como você se preocupa apenas com os índices, não precisa mapear para tuplas. Por que não escrever um filtro que use os elementos de pesquisa em sua matriz?

     int[] value =  ...


IntStream.range(0, value.length)
            .filter(i -> value[i] > 30)  //or whatever filter you want
            .forEach(i -> System.out.println(i));
dkatzel
fonte
+1 para uma solução ótima e prática. No entanto, não tenho certeza se isso generaliza para a minha situação em que estou gerando os valores em tempo real. Fiz minha pergunta como uma matriz para oferecer um caso simples para pensar e você apresentou uma excelente solução.
Necromancer
5

Sim.

Map.Entrypode ser usado como um Pair.

Infelizmente, isso não ajuda nos fluxos do Java 8, pois o problema é que, embora os lambdas possam aceitar vários argumentos, a linguagem Java apenas permite retornar um único valor (objeto ou tipo primitivo). Isso implica que, sempre que você tiver um fluxo, você receberá um único objeto da operação anterior. Isso é uma falta na linguagem Java, porque se vários valores de retorno fossem suportados E os fluxos os suportassem, poderíamos ter tarefas não triviais muito mais agradáveis ​​feitas por fluxos.

Até então, há pouco uso.

EDIT 2018-02-12: Enquanto trabalhava em um projeto, escrevi uma classe auxiliar que ajuda a lidar com o caso especial de ter um identificador no início do fluxo que você precisa mais tarde, mas a parte do fluxo intermediária não o conhece. Até que eu possa liberá-lo, ele estará disponível no IdValue.java com um teste de unidade no IdValueTest.java

Thorbjørn Ravn Andersen
fonte
2

O Eclipse Collections possui Paire todas as combinações de pares primitivos / de objetos (para todas as oito primitivas).

A Tuplesfábrica pode criar instâncias Paire osPrimitiveTuples fábrica pode ser usada para criar todas as combinações de pares primitivo / objeto.

Nós os adicionamos antes do lançamento do Java 8. Eles foram úteis para implementar Iteradores de chave / valor para nossos mapas primitivos, que também suportamos em todas as combinações primitivas / objetos.

Se você deseja adicionar a sobrecarga extra da biblioteca, pode usar a solução aceita por Stuart e coletar os resultados em uma primitiva IntListpara evitar o boxe. Adicionamos novos métodos no Eclipse Collections 9.0 para permitir que Int/Long/Doublecoleções sejam criadas a partir do Int/Long/DoubleStreams.

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

Nota: Sou um colaborador das Coleções Eclipse.

Donald Raab
fonte