Select "maximum" elements given a certain criterion in a "stream"

Asked

Viewed 710 times

9

I have a collection of elements that I’m going through in a stream. Suppose it is to take the highest value element (integer) given a classification (string). This class is enough to exemplify my case:

class Elemento {
  final String classificacao;
  final int valor;

  Elemento(String classificacao, int valor) {
    this.classificacao = classificacao;
    this.valor = valor;
  }

  // getters, para permitir um uso mais funcional
}

I need to take, for the elements of the same "classification", the higher "value".

My first strategy was to group in a map <String, List<Elemento>> to, on its values, catch the biggest Elemento:

Collection<Elemento> elementos = ...; // povoa os valores

elementos.stream()
  .collect(Collectors.groupingBy(Elemento::getClassificacao))
  .values().stream()
  .map(l -> l.stream().max(Elemento::getValor).orElse(null))
  .filter(Objects::nonNull)
  ...; // mais

Is there any way to do this without using this intermediate list map?


The real case

In fact, the particular case is to take the most specific method within a collection of bridge methods, because there have been cases of "name conflicts" when implementing generic method of a generic interface. In my real case, I have it to extract my elements:

private static int determineSuperclass(Method ma, Method mb) {
  Class<?> ra = ma.getReturnType();
  Class<?> rb = mb.getReturnType();

  if (ra.equals(rb)) {
    return 0;
  } else if (ra.isAssignableFrom(rb)) {
    return -1;
  } else if (rb.isAssignableFrom(ra)) {
    return +1;
  } else {
    return 0;
  }
}

// ...

Class<T> inputClazz = ...; // povoa inputClazz
Stream.of(inputClazz.getMethods())
  .filter(m -> m.getParameterCount() == 0)
  .filter(m -> m.getName().startsWith("get"))
  .filter(m -> !Void.class.equals(m.getReturnType()))
  .filter(m -> Modifier.isPublic(m.getModifiers()))
  .filter(m -> !Modifier.isStatic(m.getModifiers()))
  .collect(Collectors.groupingBy(Method::getName))
  .values().stream()
  .map(l -> l.stream().max(MyClass::determineSuperclass).orElse(null))
  .filter(Objects::nonNull)
  ...; // faço minha própria coleção

In my case, the problem was when I implemented a generic interface. In this case, the interface was:

interface HasKey<K> {
  K getKey();
}

And the "problem" even happens in anonymous classes, like:

HasKey<Integer> abc = new HasKey<Integer> {
  @Override
  public Integer getKey() {
    return 1;
  }
};

When it’s called abc.getClass().getMethods(), two methods called getKey():

public java.lang.Object myPackage.MyClass$1.getKey())
public java.lang.Integer myPackage.MyClass$1.getKey())

But my intention remains as, from a stream, take the "largest" element of a given "subgroup", only without using the intermediate list map explicitly.

2 answers

5


The initial version of the question asked: Is there any way to do it without using this Map explicit intermediary?

Enough to use collectingAndThen:

Stream<Elemento> stream = elementos.stream().collect(
    Collectors.collectingAndThen(
        Collectors.groupingBy(Elemento::getClassificacao),
        map -> map.values().stream()
                  .map(l -> l.stream().max(Comparator.comparing(Elemento::getValor)).orElse(null))
                  .filter(Objects::nonNull)));

First he applies the groupingBy, and then the result is passed to the finisher (lambda which is passed as the second parameter). In this case, it receives the result of the groupingBy (the Map) and extracts the highest value element for each classification.

In the end, a Map, but we can consider that it is in an "implicit" way (can we?). There may be some internal optimization in this method, but it may not, and in the end it will be the same. I don’t know, I didn’t see that much difference to your solution, and I think mine got a little more confused to read and understand.

The result is a Stream of Elemento, that you can keep using the way you need to.


Avoiding the list map

After editing, you have been asked to delete the list map.

To avoid the list map, you can collect using Collectors.toMap:

Map<String, Integer> results = elementos.stream()
    .collect(Collectors.toMap(Elemento::getClassificacao, Elemento::getValor, Math::max));

The first parameter defines the key of the map (in this case, the classification), and the second parameter, the respective value (in this case, the valor element). The third parameter sets the tiebreaker criterion if there is more than one value for the same key (I used Math::max, so he gets the biggest valor).

The result is a Map whose keys are the classifications, and the values are the highest valor of that classification.

  • To be honest, I still don’t like it =/ It seems more functional, but I still need to make a call to Map.values() inside the lambda. Therefore, it still has an explicit finger of the Map. Maybe a collector of its own will solve it? But, yes, the use of Map...

  • 1

    @Jeffersonquesado All right, I didn’t really like my solution either :-) Maybe a collector himself is the way (I just didn’t suggest it because, honestly, I never needed to create one - if there’s time I try and update the answer). But inside this collector you would have to use an algorithm that doesn’t create the map, otherwise it won’t do anything either :-)

  • Yeah, but at least you won’t need a list map... I think my biggest nuisance is this, I just don’t know how to really express it within the context

  • 1

    I edited the question to make it clear that it is list map that bothers me. Thank you for helping me see more correctly my question

  • 1

    @Jeffersonquesado I updated the answer. How is the first time I use the method collect with 3 parameters, may not be the best solution, but at least I was able to delete the list map

  • There is an error in solution 2 (.collect with 3 arguments). The putAll will eventually cause some maximum to be overwritten. It must have a new default method of Map as a putIf or something similar

  • @Jeffersonquesado Yes, it lacked to test better... Well, when I can I tidy up (maybe I can see this hj, but most likely it’s only tomorrow), thanks for warning :-) But what about the third solution? I think she already treats duplicate entries better (but I haven’t tested them yet)

  • the third seems right =]

  • @Jeffersonquesado I did some tests but they were not very conclusive, I could not reproduce the error that you mention, but if you said that is wrong I believe :-) - Anyway, I thought better to remove the solution 2 and leave only the 3, that from the tests I’ve done it seems to work better anyway.

Show 4 more comments

4

Why don’t you use the method Method.isBridge() to help you?

Here comes the MCVE:

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.stream.Stream;

public class MyClass {

    public static boolean isPublicDeclaredGetter(Method m) {
        int mod = m.getModifiers();
        Class<?> returnType = m.getReturnType();
        String name = m.getName();
        boolean is = name.startsWith("is");
        boolean get = name.startsWith("get");
        int suffixIndex = get ? 3 : 2;

        return returnType != void.class
                && returnType != Void.class
                && !"is".equals(name)
                && !"get".equals(name)
                && Modifier.isPublic(mod)
                && !Modifier.isStatic(mod)
                && !m.isBridge()
                && m.getParameterCount() == 0
                && (get || (is && (returnType == boolean.class || returnType == Boolean.class)))
                && Character.isUpperCase(name.substring(suffixIndex, suffixIndex + 1).charAt(0));
    }

    public interface A<X> {
        public X getFoo1();
        public X getFoo2();
        public int getBar1();
    }

    public interface B extends A<String> {
        public String getFoo2();
        public String getBar2();
        public boolean isOk();
        public static int getBad1() { return 0; }
        public void getBad2();
        public String isBad3();
        public int getBad4(int x);
        public String getulio();
        public String isam();
        public int get();
    }

    public static void main(String[] args) {
        Class<?> inputClazz = B.class;
        Stream.of(inputClazz.getMethods())
            .filter(MyClass::isPublicDeclaredGetter)
            .forEach(System.out::println);
    }
}

As you may have noticed, I put the rule to define whether it is a getter in the method isPublicDeclaredGetter. The reason is that the way you initially did, a method called isVisible would not be considered a getter while getulio would be. And a method called only get would also be considered as a getter. In addition, type objects Class can be compared with ==, not needing to use the equals. Finally, note the isBridge() there.

Here’s the way out:

public abstract boolean MyClass$B.isOk()
public abstract java.lang.String MyClass$B.getFoo2()
public abstract java.lang.String MyClass$B.getBar2()
public abstract java.lang.Object MyClass$A.getFoo1()
public abstract int MyClass$A.getBar1()

Note that no method was repeated, that where there were collisions of names, the most specific one was chosen and that no unwanted method appeared.

  • "Because you do not use the Method.isBridge method()?" A: ignorance on my part =)

  • I didn’t know you could use it boolean.class, only knew the Boolean.class

  • 1

    @Jeffersonquesado Yes, it gives. It produces the object Class that points to the primitive type. Already using Boolean, you reference the packaging class. And the most interesting is the void.class.

  • About using the Class.equals, is trauma even...

  • 1

    You solved my natural problem (the X), but I think I was able to make clear my artificial problem (the Y). So I will give a reward when possible, but I will accept hkotsubo’s reply

  • Your answer gave me a question: https://answall.com/q/408621/64969

Show 1 more comment

Browser other questions tagged

You are not signed in. Login or sign up in order to post.