web-dev-qa-db-fra.com

Java 8: Méthode préférée pour compter les itérations d'un lambda?

Je fais souvent face au même problème. J'ai besoin de compter les courses d'un lambda pour une utilisation en dehors du lambda. Par exemple.:

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

Le problème est que runCount doit être final, il ne peut donc s'agir d'un int. Cela ne peut pas être un entier parce que c'est immuable. Je pourrais en faire une variable de niveau de classe (c'est-à-dire un champ) mais je n'en aurai besoin que dans ce bloc de code . Je sais qu'il y a différentes façons, je me demande simplement quelle est votre solution préférée pour cela? Utilisez-vous un AtomicInteger ou une référence de tableau ou d'une autre manière? 

53
user4159962

Permettez-moi de reformater un peu votre exemple à des fins de discussion:

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

Si vous vraiment devez incrémenter un compteur à partir d'un lambda, la manière typique de le faire est de lui attribuer une valeur AtomicInteger ou AtomicLong, puis d'appeler l'une des méthodes d'incrémentation.

Vous pouvez utiliser un tableau int ou long à un seul élément, mais cela aurait des conditions de concurrence si le flux est exécuté en parallèle.

Mais notez que le flux se termine par forEach, ce qui signifie qu'il n'y a pas de valeur de retour. Vous pouvez changer la forEach en une peek, qui transmet les éléments, puis les compter:

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

C'est un peu mieux, mais toujours un peu étrange. La raison en est que forEach et peek ne peuvent faire leur travail que par le biais d’effets secondaires. Le style fonctionnel émergent de Java 8 consiste à éviter les effets secondaires. Nous avons un peu fait cela en extrayant l'incrément du compteur dans une opération count sur le flux. L'ajout d'éléments aux collections est un autre effet secondaire typique. Celles-ci peuvent généralement être remplacées par des collecteurs. Mais sans savoir quel travail vous essayez de faire, je ne peux suggérer rien de plus spécifique.

52
Stuart Marks

En guise d'alternative à la synchronisation avec AtomicInteger, vous pouvez utiliser un tableau integer. Tant que la référence au tableau ne reçoit pas un autre tableau assigné (et c'est le point), il peut être utilisé comme variable final, tandis que le values ​​ des champs peut changer arbitrairement.

    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
    });
24
Duke Spray
AtomicInteger runCount = 0L;
long runCount = myStream.stream()
    .filter(...)
    .peek(item -> { 
        foo();
        bar();
        runCount.incrementAndGet();
    });
System.out.println("The lambda ran " + runCount.incrementAndGet() + "times");
6
Leandruz

Une autre façon de procéder (utile si vous souhaitez que votre nombre ne soit incrémenté que dans certains cas, comme si une opération aboutissait) consiste à utiliser quelque chose comme ceci, en utilisant mapToInt() et 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");

Comme l'a noté Stuart Marks, cela reste un peu étrange, car cela n'évite pas complètement les effets secondaires (selon ce que font foo() et bar()).

Et une autre façon d’incrémenter une variable dans un lambda accessible à l’extérieur est d’utiliser une variable de classe:

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

Dans cet exemple, utiliser une variable de classe pour un compteur dans une méthode n'a probablement aucun sens, je vous déconseille donc cette option, sauf indication contraire. Conserver les variables de classe final si possible peut être utile en termes de sécurité des threads, etc. (voir http://www.javapractices.com/topic/TopicAction.do?Id=23 pour une discussion sur l'utilisation de final). 

Pour avoir une meilleure idée de la raison pour laquelle les lambdas fonctionnent de la même manière, https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood a un aspect détaillé.

3
A. D.

Pour moi, cela a fait l'affaire, j'espère que quelqu'un le trouvera utile:

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

getAndIncrement () Java informations de la documentation:

Incrémente de manière atomique la valeur actuelle, avec les effets de mémoire spécifiés par VarHandle.getAndAdd. Équivalent à getAndAdd (1).

2
Jose Ripoll

Vous ne devriez pas utiliser AtomicInteger, vous ne devriez pas utiliser de choses sauf si vous avez une très bonne raison de les utiliser. Et la raison d'utiliser AtomicInteger pourrait ne permettre que des accès simultanés ou tels que.

quand il s'agit de votre problème;

Le titulaire peut être utilisé pour le maintenir et l’incrémenter à l’intérieur d’un lambda. Et après que vous puissiez l'obtenir en appelant 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");
1
Ahmet Orhan

réduire fonctionne également, vous pouvez l'utiliser comme ça

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

Si vous ne voulez pas créer un champ parce que vous n'en avez besoin que localement, vous pouvez le stocker dans une classe anonyme:

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

Bizarre, je sais. Mais cela garde la variable temporaire en dehors de la portée locale.

0
shmosel