¿Java SE 8 tiene pares o tuplas?


185

Estoy jugando con operaciones funcionales perezosas en Java SE 8, y quiero mapun índice ia un par / tupla (i, value[i]), luego filterbasado en el segundo value[i]elemento y finalmente generar solo los índices.

Debo seguir sufriendo esto: ¿Cuál es el equivalente del par C ++ <L, R> en Java? en la audaz nueva era de lambdas y arroyos?

Actualización: presenté un ejemplo bastante simplificado, que tiene una solución ordenada ofrecida por @dkatzel en una de las respuestas a continuación. Sin embargo, no se generaliza. Por lo tanto, permítanme agregar un ejemplo más general:

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

}

Esto da una salida incorrecta[0, 0, 0] que corresponde a los recuentos de las tres columnas que son todas false. Lo que necesito son los índices de estas tres columnas. La salida correcta debería ser [0, 2, 4]. ¿Cómo puedo obtener este resultado?


2
Ya existe AbstractMap.SimpleImmutableEntry<K,V>desde hace años ... Pero de todos modos, en lugar de la cartografía ia (i, value[i])simplemente para filtrar por value[i]ida y vuelta a la cartografía i: ¿por qué no filtro value[i]en el primer lugar, sin la asignación?
Holger

@Holger Necesito saber qué índices de una matriz contienen valores que coinciden con un criterio. No puedo hacerlo sin preservar ien la secuencia. También necesito value[i]los criterios. Por eso lo necesito(i, value[i])
nigromante

1
@necromancer Correcto, solo funciona si es barato obtener el valor del índice, como una matriz, una colección de acceso aleatorio o una función económica. Supongo que el problema es que quería presentar un caso de uso simplificado, pero se simplificó demasiado y, por lo tanto, sucumbió a un caso especial.
Stuart Marks

1
@necromancer Edité un poco el último párrafo para aclarar la pregunta que creo que estás haciendo. ¿Es correcto? Además, ¿es esta una pregunta sobre un gráfico dirigido (no acíclico)? (No es que importe mucho). Finalmente, ¿debería ser la salida deseada [0, 2, 4]?
Stuart Marks

1
Creo que la solución correcta para solucionar esto es tener una futura versión de Java compatible con tuplas como un tipo de retorno (como un caso especial de Object) y que las expresiones lambda puedan usar dicha tupla directamente para sus parámetros.
Thorbjørn Ravn Andersen

Respuestas:


206

ACTUALIZACIÓN: Esta respuesta es en respuesta a la pregunta original, ¿Java SE 8 tiene pares o tuplas? (E implícitamente, si no, ¿por qué no?) El OP ha actualizado la pregunta con un ejemplo más completo, pero parece que se puede resolver sin usar ningún tipo de estructura de pares. [Nota de OP: aquí está la otra respuesta correcta .]


La respuesta corta es no. Debe rodar el suyo o traer una de las varias bibliotecas que lo implementan.

Se Pairpropuso y rechazó tener una clase en Java SE al menos una vez. Vea este hilo de discusión en una de las listas de correo de OpenJDK. Las compensaciones no son obvias. Por un lado, hay muchas implementaciones de pares en otras bibliotecas y en el código de la aplicación. Eso demuestra una necesidad, y agregar tal clase a Java SE aumentará la reutilización y el uso compartido. Por otro lado, tener una clase Pair aumenta la tentación de crear estructuras de datos complicadas a partir de pares y colecciones sin crear los tipos y abstracciones necesarios. (Esa es una paráfrasis del mensaje de Kevin Bourillion de ese hilo).

Recomiendo a todos que lean todo el hilo de correo electrónico. Es notablemente perspicaz y no tiene daño. Es bastante convincente. Cuando comenzó, pensé: "Sí, debería haber una clase Pair en Java SE", pero cuando el hilo llegó a su fin, había cambiado de opinión.

Sin embargo, tenga en cuenta que JavaFX tiene la clase javafx.util.Pair . Las API de JavaFX evolucionaron por separado de las API de Java SE.

Como se puede ver en la pregunta vinculada ¿ Cuál es el equivalente del par C ++ en Java? Hay un espacio de diseño bastante grande que rodea lo que aparentemente es una API tan simple. ¿Deberían los objetos ser inmutables? ¿Deberían ser serializables? ¿Deberían ser comparables? ¿La clase debería ser final o no? ¿Deberían ordenarse los dos elementos? ¿Debería ser una interfaz o una clase? ¿Por qué parar en parejas? ¿Por qué no triples, cuádruples o tuplas N?

Y, por supuesto, existe el inevitable cambio de nombres para los elementos:

  • (a, b)
  • (primer segundo)
  • (izquierda derecha)
  • (coche, cdr)
  • (foo, bar)
  • etc.

Un gran problema que apenas se ha mencionado es la relación de los pares con los primitivos. Si tiene un (int x, int y)dato que representa un punto en el espacio 2D, representarlo ya que Pair<Integer, Integer>consume tres objetos en lugar de dos palabras de 32 bits. Además, estos objetos deben residir en el montón e incurrirán en gastos generales de GC.

Parecería claro que, al igual que Streams, sería esencial que hubiera especializaciones primitivas para pares. ¿Queremos ver:

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

Incluso un IntIntPairtodavía requeriría un objeto en el montón.

Estos, por supuesto, recuerdan la proliferación de interfaces funcionales en el java.util.functionpaquete en Java SE 8. Si no desea una API hinchada, ¿cuáles dejaría de lado? También podría argumentar que esto no es suficiente, y que las especializaciones para, por ejemplo, también Booleandeberían agregarse.

Mi sensación es que si Java hubiera agregado una clase Pair hace mucho tiempo, habría sido simple, o incluso simplista, y no habría satisfecho muchos de los casos de uso que estamos imaginando ahora. Tenga en cuenta que si se hubiera agregado Par en el marco de tiempo JDK 1.0, ¡probablemente habría sido mutable! (Mire java.util.Date.) ¿La gente habría estado contenta con eso? Supongo que si hubiera una clase Pair en Java, sería un poco no muy útil y todos seguirían implementando los suyos para satisfacer sus necesidades, habría varias implementaciones de Pair y Tuple en bibliotecas externas, y la gente todavía estaría discutiendo / discutiendo sobre cómo arreglar la clase Pair de Java. En otras palabras, más o menos en el mismo lugar en el que estamos hoy.

Mientras tanto, se está trabajando para abordar el problema fundamental, que es un mejor soporte en la JVM (y eventualmente en el lenguaje Java) para los tipos de valor . Ver este documento Estado de los valores . Este es un trabajo preliminar y especulativo, y cubre solo temas desde la perspectiva de JVM, pero ya tiene una buena cantidad de pensamiento detrás de él. Por supuesto, no hay garantías de que esto entrará en Java 9, o nunca entrará en ningún lado, pero sí muestra la dirección actual de pensar sobre este tema.


3
@necromancer Los métodos de fábrica con primitivas no ayudan Pair<T,U>. Dado que los genéricos deben ser de tipo de referencia. Cualquier primitiva se encuadrará cuando se almacene. Para almacenar primitivas realmente necesitas una clase diferente.
Stuart Marks

3
@necromancer Y sí, en retrospectiva, los constructores primitivos en caja no deberían haber sido públicos, y valueOfdeberían haber sido la única forma de obtener una instancia en caja. Pero esos han estado allí desde Java 1.0 y probablemente no valga la pena intentar cambiar en este momento.
Stuart Marks

3
Obviamente, solo debe haber un público Pairo Tupleclase con un método de fábrica que cree las clases de especialización necesarias (con almacenamiento optimizado) de forma transparente en segundo plano. Al final, las lambdas hacen exactamente eso: pueden capturar un número arbitrario de variables de tipo arbitrario. Y ahora imagina un soporte de idioma que permite crear la clase de tupla apropiada en tiempo de ejecución activada por una invokedynamicinstrucción ...
Holger

3
@Holger Algo así podría funcionar si uno estuviera actualizando tipos de valor en la JVM existente, pero la propuesta de Tipos de Valor (ahora "Proyecto Valhalla" ) es mucho más radical. En particular, sus tipos de valor no estarían necesariamente asignados a montón. Además, a diferencia de los objetos de hoy, y como las primitivas de hoy, los valores no tendrían identidad.
Stuart Marks

2
@Stuart Marks: Eso no interferiría, ya que el tipo que describí podría ser el tipo "en caja" para dicho tipo de valor. Con una invokedynamicfábrica con base similar a la creación lambda, tal modificación posterior no sería un problema. Por cierto, las lambdas tampoco tienen identidad. Como se indicó explícitamente, la identidad que puede percibir hoy es un artefacto de la implementación actual.
Holger

46

Puede echar un vistazo a estas clases integradas:


3
Esta es la respuesta correcta, en cuanto a la funcionalidad incorporada para pares. Tenga en cuenta que SimpleImmutableEntrysolo garantiza que las referencias almacenadas en el Entryno cambien, no que los campos de los objetos vinculados keyy value(o los de los objetos a los que se vinculan) no cambien.
Luke Hutchison el

22

Lamentablemente, Java 8 no introdujo pares o tuplas. Por supuesto, siempre puede usar org.apache.commons.lang3.tuple (que personalmente uso en combinación con Java 8) o puede crear sus propios contenedores. O usa Mapas. O cosas así, como se explica en la respuesta aceptada a esa pregunta a la que se vinculó.


ACTUALIZACIÓN: JDK 14 está introduciendo registros como una función de vista previa. Estas no son tuplas, pero se pueden usar para salvar muchos de los mismos problemas. En su ejemplo específico de arriba, eso podría verse más o menos así:

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

Cuando se compila y ejecuta con JDK 14 (en el momento de la escritura, esta es una compilación de acceso temprano) usando el --enable-previewindicador, obtiene el siguiente resultado:

[CountForIndex[index=0, count=0], CountForIndex[index=2, count=0], CountForIndex[index=4, count=0]]

En realidad, una de las respuestas de @StuartMarks me permitió resolverlo sin tuplas, pero como no parece generalizarse, probablemente lo necesite con el tiempo.
nigromante

@necromancer Sí, es una muy buena respuesta. La biblioteca apache puede ser útil a veces, pero todo se reduce al diseño del lenguaje Javas. Básicamente, las tuplas tendrían que ser primitivas (o similares) para funcionar como lo hacen en otros idiomas.
blalasaadri

1
En caso de que no lo haya notado, la respuesta incluyó este enlace extremadamente informativo: cr.openjdk.java.net/~jrose/values/values-0.html sobre la necesidad y las perspectivas de tales primitivas, incluidas las tuplas.
nigromante

17

Parece que el ejemplo completo se puede resolver sin el uso de ningún tipo de estructura de pares. La clave es filtrar en los índices de la columna, con el predicado comprobando la columna completa, en lugar de asignar los índices de la columna al número de falseentradas en esa columna.

El código que hace esto está aquí:

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

Esto da como resultado una salida de la [0, 2, 4]cual creo que es el resultado correcto solicitado por el OP.

También tenga en cuenta la boxed()operación que encajona los intvalores en Integerobjetos. Esto le permite a uno usar el toList()recopilador preexistente en lugar de tener que escribir las funciones del recopilador que hacen el propio boxeo.


1
+1 as bajo la manga :) Esto todavía no se generaliza, ¿verdad? Ese fue el aspecto más sustancial de la pregunta porque espero enfrentar otras situaciones en las que un esquema como este no funcionará (por ejemplo, columnas con no más de 3 valores true). Por consiguiente, aceptaré su otra respuesta como correcta, ¡pero también señalaré esta! Muchas gracias :)
nigromante

Esto es correcto pero acepta la otra respuesta del mismo usuario. (véanse los comentarios anteriores y en otras partes).
nigromante

1
@necromancer Correcto, esta técnica no es totalmente general en los casos en que desea el índice, pero el elemento de datos no se puede recuperar ni calcular utilizando el índice. (Al menos no fácilmente). Por ejemplo, considere un problema en el que está leyendo líneas de texto desde una conexión de red y desea encontrar el número de línea de la enésima línea que coincide con algún patrón. La forma más fácil es mapear cada línea en un par o alguna estructura de datos compuestos para numerar las líneas. Sin embargo, es probable que haya una forma hacky y de efectos secundarios de hacer esto sin una nueva estructura de datos.
Stuart Marks

@StuartMarks, un par es <T, U>. un triple <T, U, V>. etc. Su ejemplo es una lista, no un par.
Pacerier

7

Vavr (anteriormente llamado Javaslang) ( http://www.vavr.io ) también proporciona tuplas (hasta un tamaño de 8). Aquí está el javadoc: https://static.javadoc.io/io.vavr/vavr/0.9.0/io/vavr/Tuple.html .

Este es un ejemplo simple:

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

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

Por qué JDK en sí no vino con un tipo simple de tuplas hasta ahora es un misterio para mí. Escribir clases de envoltura parece ser un asunto de todos los días.


Algunas versiones de vavr utilizan tiros furtivos debajo del capó. Tenga cuidado de no usar esos.
Thorbjørn Ravn Andersen

7

Desde Java 9, puede crear instancias Map.Entrymás fáciles que antes:

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

Map.entrydevuelve un no modificable Entryy prohíbe los nulos.


6

Como solo te interesan los índices, no es necesario que asignes tuplas en absoluto. ¿Por qué no simplemente escribir un filtro que usa los elementos de búsqueda en su matriz?

     int[] value =  ...


IntStream.range(0, value.length)
            .filter(i -> value[i] > 30)  //or whatever filter you want
            .forEach(i -> System.out.println(i));

+1 para una excelente solución práctica. Sin embargo, no estoy seguro de si se generaliza a mi situación en la que estoy generando los valores sobre la marcha. Planteé mi pregunta como una matriz para ofrecer un caso simple en el que pensar y se te ocurrió una excelente solución.
nigromante

5

Si.

Map.Entryse puede usar como a Pair.

Desafortunadamente, no ayuda con las secuencias Java 8, ya que el problema es que, aunque las lambdas pueden tomar múltiples argumentos, el lenguaje Java solo permite devolver un único valor (objeto o tipo primitivo). Esto implica que cada vez que tiene una secuencia termina pasando un solo objeto de la operación anterior. Esto es una falta en el lenguaje Java, porque si se admitieran múltiples valores de retorno Y los flujos los admitieran, podríamos tener tareas no triviales mucho más agradables realizadas por los flujos.

Hasta entonces, solo hay poco uso.

EDITAR 12-02-2018: Mientras trabajaba en un proyecto, escribí una clase auxiliar que ayuda a manejar el caso especial de tener un identificador más temprano en la secuencia que necesita en un momento posterior, pero la parte de la secuencia intermedia no lo sabe. Hasta que pueda lanzarlo solo, está disponible en IdValue.java con una prueba unitaria en IdValueTest.java


2

Eclipse Collections tiene Pairy todas las combinaciones de pares primitivos / objeto (para las ocho primitivas).

La Tuplesfábrica puede crear instancias de Pair, y la PrimitiveTuplesfábrica se puede usar para crear todas las combinaciones de pares primitivos / objeto.

Agregamos estos antes de que se lanzara Java 8. Fueron útiles para implementar Iteradores clave / valor para nuestros mapas primitivos, que también admitimos en todas las combinaciones primitivas / objeto.

Si está dispuesto a agregar la sobrecarga adicional de la biblioteca, puede usar la solución aceptada de Stuart y recopilar los resultados en una primitiva IntListpara evitar el boxeo. Agregamos nuevos métodos en Eclipse Collections 9.0 para permitir Int/Long/Doubleque se creen colecciones desde Int/Long/DoubleStreams.

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

Nota: Soy un committer para Eclipse Collections.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.