Java 8: ¿forma preferida de contar las iteraciones de una lambda?

82

A menudo me enfrento al mismo problema. Necesito contar las ejecuciones de una lambda para usar fuera de la lambda. P.ej:

myStream.stream().filter(...).forEach(item->{ ... ; runCount++);
System.out.println("The lambda ran "+runCount+"times");

El problema es que runCount debe ser final, por lo que no puede ser un int. No puede ser un entero porque es inmutable. Podría convertirlo en una variable de nivel de clase (es decir, un campo) pero solo lo necesitaré en este bloque de código. Sé que hay varias formas, solo tengo curiosidad por saber cuál es su solución preferida para esto. ¿Utiliza un AtomicInteger o una referencia de matriz o de alguna otra manera?

Stuart Marks
fuente
7
@ Sliver2009 No, no lo es.
Rohit Jain
4
@Florian Tienes que usar AtomicIntegeraquí.
Rohit Jain

Respuestas:

70

Permíteme reformatear un poco tu ejemplo por el bien de la discusión:

long runCount = 0L;
myStream.stream()
    .filter(...)
    .forEach(item -> { 
        foo();
        bar();
        runCount++; // doesn't work
    });
System.out.println("The lambda ran " + runCount + " times");

Si realmente necesita incrementar un contador desde dentro de una lambda, la forma típica de hacerlo es convertir el contador en AtomicIntegero AtomicLongy luego llamar a uno de los métodos de incremento en él.

Podría usar un elemento único into una longmatriz, pero eso tendría condiciones de carrera si la transmisión se ejecuta en paralelo.

Pero observe que el flujo termina en forEach, lo que significa que no hay valor de retorno. Puede cambiar forEacha a peek, que pasa los elementos y luego contarlos:

long runCount = myStream.stream()
    .filter(...)
    .peek(item -> { 
        foo();
        bar();
    })
    .count();
System.out.println("The lambda ran " + runCount + " times");

Esto es algo mejor, pero todavía un poco extraño. La razón es que forEachy peeksolo pueden hacer su trabajo a través de efectos secundarios. El estilo funcional emergente de Java 8 es evitar efectos secundarios. Hicimos un poco de eso extrayendo el incremento del contador en una countoperación en la secuencia. Otros efectos secundarios típicos son la adición de elementos a las colecciones. Por lo general, estos se pueden reemplazar mediante el uso de colectores. Pero sin saber qué trabajo real está tratando de hacer, no puedo sugerir nada más específico.

Stuart Marks
fuente
8
Cabe señalar que peekdeja de funcionar, una vez que las countimplementaciones comienzan a utilizar atajos para las SIZEDtransmisiones. Puede que eso nunca sea un problema con filtered stream, pero puede crear grandes sorpresas si alguien cambia el código en un momento posterior…
Holger
14
Declare final AtomicInteger i = new AtomicInteger(1);y en algún lugar de su uso de lambda i.getAndAdd(1). Detente y recuerda lo agradable que int i=1; ... i++solía ser.
aliopi
2
Si Java implementara interfaces como Incrementableen clases numéricas, incluyendo AtomicInteger, y declarara operadores como ++funciones de aspecto elegante, no necesitaríamos sobrecargar operadores y aún tendríamos un código muy legible.
SeverityOne
37

Como alternativa a la sincronización de AtomicInteger, se podría usar una matriz de enteros . Siempre que la referencia a la matriz no obtenga otra matriz asignada (y ese es el punto), se puede usar como una variable final mientras que los valores de los campos pueden cambiar arbitrariamente.

    int[] iarr = {0}; // final not neccessary here if no other array is assigned
    stringList.forEach(item -> {
            iarr[0]++;
            // iarr = {1}; Error if iarr gets other array assigned
    });
Spray de duque
fuente
Si desea asegurarse de que la referencia no obtenga otra matriz asignada, puede declarar iarr como variable final. Pero como señala @pisaruk, esto no funcionará en paralelo.
themathmagician
1
Creo que, para simplificar foreachdirectamente en la colección (sin flujos), este es un enfoque bastante bueno. Gracias !!
Sabir Khan
Esta es la solución más simple, siempre que no esté ejecutando las cosas en paralelo.
David DeMar
17
AtomicInteger runCount = 0L;
long runCount = myStream.stream()
    .filter(...)
    .peek(item -> { 
        foo();
        bar();
        runCount.incrementAndGet();
    });
System.out.println("The lambda ran " + runCount.incrementAndGet() + "times");
Leandruz
fuente
15
Por favor edición con más información. Código de sólo y "Prueba esto" respuestas se desanimó , ya que no contienen el contenido de búsqueda, y no explican por qué alguien debe "probar esto". Nos esforzamos aquí por ser un recurso de conocimiento.
Mogsdad
7
Tu respuesta me confunde. Tienes dos variables nombradas runCount. Sospecho que tenía la intención de tener solo uno de ellos, pero ¿cuál?
Ole VV
1
Encontré que runCount.getAndIncrement () es más adecuado. ¡Gran respuesta!
Kospol
2
El AtomicIntegerme ayudó pero me gustaría init connew AtomicInteger(0)
Stefan Höltker
1) Este código no se compilará: el flujo no tiene una operación de terminal que devuelva un 2) Incluso si lo hiciera, el valor de 'runCount' siempre sería '1': - el flujo no tiene operación de terminal, así que peek () argumento lambda nunca se llamará - La línea System.out incrementa el recuento de ejecución antes de mostrarlo
Cédric
8

No debe usar AtomicInteger, no debe usar cosas a menos que tenga una buena razón para hacerlo. Y la razón para usar AtomicInteger podría ser solo permitir accesos concurrentes o similares.

Cuando se trata de su problema;

El soporte se puede usar para mantenerlo e incrementarlo dentro de una lambda. Y después puede obtenerlo llamando a runCount.value

Holder<Integer> runCount = new Holder<>(0);

myStream.stream()
    .filter(...)
    .forEach(item -> { 
        foo();
        bar();
        runCount.value++; // now it's work fine!
    });
System.out.println("The lambda ran " + runCount + " times");
Ahmet Orhan
fuente
Hay algunas clases de Holder en el JDK. Éste parece ser javax.xml.ws.Holder.
Brad Cupit
1
De Verdad? ¿Y por qué?
lkahtz
1
Estoy de acuerdo, si sé que no estoy haciendo ninguna operación concurrente en lamda / stream, ¿por qué querría usar AtomicInteger que está diseñado para atender la concurrencia? Eso podría introducir bloqueo, etc., es decir, la razón por la cual, hace muchos años, el JDK introdujo el nuevo conjunto de colecciones y sus iteradores que no realizaban ningún bloqueo, por qué cargar algo con una capacidad de reducción del rendimiento, por ejemplo, bloquear cuando el bloqueo no es necesario para muchos escenarios.
Volksman
4

Para mí, esto funcionó, espero que alguien lo encuentre útil:

AtomicInteger runCount= new AtomicInteger(0);
myStream.stream().filter(...).forEach(item->{ ... ; runCount.getAndIncrement(););
System.out.println("The lambda ran "+runCount.get()+"times");

getAndIncrement () La documentación de Java dice:

Incrementa atómicamente el valor actual, con efectos de memoria según lo especificado por VarHandle.getAndAdd. Equivalente a getAndAdd (1).

José Ripoll
fuente
3

Otra forma de hacer esto (útil si desea que su recuento solo se incremente en algunos casos, como si una operación fue exitosa) es algo como esto, usando mapToInt()y sum():

int count = myStream.stream()
    .filter(...)
    .mapToInt(item -> { 
        foo();
        if (bar()){
           return 1;
        } else {
           return 0;
    })
    .sum();
System.out.println("The lambda ran " + count + "times");

Como señaló Stuart Marks, esto todavía es algo extraño, porque no evita completamente los efectos secundarios (dependiendo de qué foo()ybar() estamos haciendo).

Y otra forma de incrementar una variable en una lambda que es accesible fuera de ella es usar una variable de clase:

public class MyClass {
    private int myCount;

    // Constructor, other methods here

    void myMethod(){
        // does something to get myStream
        myCount = 0;
        myStream.stream()
            .filter(...)
            .forEach(item->{
               foo(); 
               myCount++;
        });
    }
}

En este ejemplo, usar una variable de clase para un contador en un método probablemente no tenga sentido, así que lo advierto a menos que haya una buena razón para hacerlo. Mantener las variables de clase, finalsi es posible, puede ser útil en términos de seguridad de subprocesos, etc. (consulte http://www.javapractices.com/topic/TopicAction.do?Id=23 para una discusión sobre el uso final).

Para tener una mejor idea de por qué las lambdas funcionan de la forma en que lo hacen, https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood tiene una vista detallada.

ANUNCIO
fuente
3

Si no desea crear un campo porque solo lo necesita localmente, puede almacenarlo en una clase anónima:

int runCount = new Object() {
    int runCount = 0;
    {
        myStream.stream()
                .filter(...)
                .peek(x -> runCount++)
                .forEach(...);
    }
}.runCount;

Extraño, lo sé. Pero mantiene la variable temporal incluso fuera del ámbito local.

shmosel
fuente
2
qué diablos está sucediendo aquí, necesita un poco más de explicación gracias
Alexander Mills
Básicamente, está incrementando y recuperando un campo en una clase anónima. Si me dice con qué está específicamente confundido, puedo intentar aclararlo.
shmosel
1
@MrCholo Es un bloque inicializador . Corre antes que el constructor.
shmosel
1
@MrCholo No, es un inicializador de instancia.
shmosel
1
@MrCholo Una clase anónima no puede tener un constructor declarado explícitamente.
shmosel
1

reducir también funciona, puedes usarlo así

myStream.stream().filter(...).reduce((item, sum) -> sum += item);
yao.qingdong
fuente
1

Otra alternativa es usar apache commons MutableInt.

MutableInt cnt = new MutableInt(0);
myStream.stream()
    .filter(...)
    .forEach(item -> { 
        foo();
        bar();
        cnt.increment();
    });
System.out.println("The lambda ran " + cnt.getValue() + " times");
Vojtěch Fried
fuente
0
AtomicInteger runCount = new AtomicInteger(0);

elements.stream()
  //...
  .peek(runCount.incrementAndGet())
  .collect(Collectors.toList());

// runCount.get() should have the num of times lambda code was executed
tsunllly
fuente