How to use Java Stream Map a List<Src> to an Object Tgt?

Viewed 210

First of all, this question is from my real work. I need to solve it and I do some trade off working to realize it unperfect. What's more, I abstract and simplify the basic data structure to avoid sensitive data.

My source and target Object which is Src and Tgt is as followed:

public class Src {
    int id;
    boolean flag;
    int count;
    // Constructor Getter and Setter methods......
}

public class Tgt {
    int id;
    int true_count;
    int false_count;
    // Constructor Getter and Setter methods......
}

Now I have a list of Src Object named srcList. I want to combine the list to one Object named Tgt which operation is like count how many flag in Src Object is true or false in the list.

In java8 Stream, I haved tried to use flatmap and map operator to convert List to one Object like followed code:


public class TestSrc2Tgt {

    public List<Tgt> listSrc2ListTgt(List<Src> srcList){
        Tgt tgt = new Tgt();
        for(Src src : srcList) {
            tgt.setId(src.getId());
            if(src.isFlag()) {
                tgt.setTrue_count(tgt.getTrue_count + src.getCount());
            }
            else {
                tgt.setFalse_count(tgt.getFalse_count + src.getCount());
            }
        }
        /* it is my question and I do not want to use List
            What I want is convert List<Src> to Tgt which is a kind of combine srcList
         */
        List<Tgt> tgtList = new ArrayList<>();
        tgtList.add(tgt);
        return tgtList;
    }

    public static void main(String[] args){

        TestSrc2Tgt testSrc2Tgt = new TestSrc2Tgt();

        List<Src> srcList = new ArrayList<>();
        srcList.add(new Src(1, true, 15));
        srcList.add(new Src(1, false, 20));
        srcList.add(new Src(1, false, 110));
        srcList.add(new Src(2, true, 40));
        srcList.add(new Src(2, false, 250));
        srcList.add(new Src(2, true, 420));

        // 1. Cluster the id and generate list by id
        Map<Integer, List<Src>> srcMap = srcList.stream()
                .collect(Collectors.groupingBy(Src::getId, Collectors.toList()));
        // 2. Count how many Src flag is true or false by listSrc2ListTgt method
        // Where I hate is here
        List<Tgt> tgtMaplist =
                srcMap.entrySet().stream().map( e -> {return testSrc2Tgt.listSrc2ListTgt(e.getValue());})
                        .map(item->item.get(0)).collect(Collectors.toList());

        tgtMaplist.stream().forEach(t -> {
            System.out.println(t.getId());
            System.out.println(t.getFalse_count());
            System.out.println(t.getTrue_count());
        });
    }
}

The code could really work but I hate the listSrc2ListTgt() method and .map(item->item.get(0)) in getting List<Tgt> tgtMaplist. My idea is a one way converting from List<Src> to Tgt but not convert from List<Src> to List<Tgt>(only one element) and then get Tgt from List<Tgt>. It is a wasted and unperfect way of realizing.

In ideal situation

public Tgt listSrc2ListTgt(List<Src> srcList){
    Tgt tgt = new Tgt();
    for(Src src : srcList) {
        tgt.setId(src.getId());
        if(src.isFlag()) {
            tgt.setTrue_count(src.getCount());
        }
        else {
            tgt.setFalse_count(src.getCount());
        }
    }
    return tgt;
}

And in main method:

List<Tgt> tgtMaplist = srcMap.entrySet().stream()
                         .map( e -> {return testSrc2Tgt.listSrc2ListTgt(e.getValue());})
                         .collect(Collectors.toList());

It is more comfortable. Because there is no intermediate process taken from the tgtlist by using .get(0) method, which seems less redundant. However the ideal code is unable to pass the compiler.

5 Answers

Your ideal solution should work, if you return tgt instead of tgtList inside ListSrc2ListTgt:

public Tgt ListSrc2ListTgt(List<Src> srcList) {
    Tgt tgt = new Tgt();
    for(Src src : srcList) {
        tgt.setId(src.getId());
        if(src.isFlag()) {
            tgt.setTrue_count(src.getCount());
        } else {
            tgt.setFalse_count(src.getCount());
        }
    }
    return tgt;
}

And to improve the readability and usability of your stream in main method:

  • don't use entrySet() if you don't need the keys. use values() instead.
  • the lambda can then be shortened with a method reference.
List<Tgt> tgtMaplist = srcMap.values().stream()
        .map(testSrc2Tgt::ListSrc2ListTgt)
        .collect(Collectors.toList());

You can map your Src into Tgt first. Then using toMap map by id and merge the list of Tgt into one using the merge function. Then get the map values in an ArrayList.

Map<Integer, Tgt> tgtMap = srcList.stream()
                .map(s -> new Tgt(s.getId(),                     
                                 (s.isFlag() ? s.getCount() : 0),
                                 (s.isFlag() ? 0 : s.getCount())))
                .collect(Collectors.toMap(Tgt::getId, e -> e,    
                                    (a, b) -> new Tgt(a.getId(), 
                                                      a.getTrue_count() + b.getTrue_count(), 
                                                      a.getFalse_count() + b.getFalse_count())));

List<Tgt> tgtList = new ArrayList(tgtMap.values()); // Create list from map values

Here using map() transform Src object into Tgt object

s -> new Tgt(s.getId(), (s.isFlag() ? s.getCount() : 0),(s.isFlag() ? 0 :s.getCount()))

And then using Collectors.toMap map by Tgt::getId which is first parameter of toMap.The next one is the key of the map e -> e for the whole Tgt obj as key. And finally last parameter in the merge function will used for merging two Tgt objects into one Tgt object so that all values of the same key merged.

Collectors.toMap(Tgt::getId,
                 e -> e,
                 (a, b) -> new Tgt(a.getId(), 
                                   a.getTrue_count() + b.getTrue_count(), 
                                   a.getFalse_count() + b.getFalse_count()))

To simplify the code with explanations

Map<Integer, Tgt> tgtMap = srcList.stream()
                .map(s -> /*transform into Tgt*/)
                .collect(Collectors.toMap(Tgt::getId, e -> e, /*Merge function*/));

Not strictly an answer to your exact question, but a simpler approach to find the 3 things asked for:

int id = srcList.get(srcList.size() - 1).getId();
int true_count = srcList.stream().filter(Src::getFlag).count();
int false_count = srcList.size() - true_count;

Here is a pretty simply way using Java streams:

List<Src> srcList = new ArrayList<Src>();
srcList.add(new Src(1, true, 15));
srcList.add(new Src(1, false, 20));
srcList.add(new Src(1, false, 110));
srcList.add(new Src(2, true, 40));
srcList.add(new Src(2, false, 250));
srcList.add(new Src(2, true, 420));

List<Tgt> tgtList = srcList.stream()
    .collect(Collectors.groupingBy(Src::getId, Collectors.toList()))
    .entrySet().stream()
    .map(e -> 
        new Tgt(e.getKey(), 
                e.getValue().stream().mapToInt(s -> s.getFlag() ? s.getCount() : 0).sum(),
                e.getValue().stream().mapToInt(s -> !s.getFlag() ? s.getCount() : 0).sum())
    ).collect(Collectors.toList());

System.out.println(tgtList);

Here is the output:

[Tgt [id=1, true_count=15, false_count=130], Tgt [id=2, true_count=460, false_count=250]]

EDIT: I would prefer to not use streams so we don't loop over the data so many times.

Map<Integer, Tgt> tgtMap = new HashMap<Integer, Tgt>();
for (Src src: srcList) {
    Tgt tgt = new Tgt(src.getId(), src.getFlag() ? src.getCount() : 0, !src.getFlag() ? src.getCount() : 0);
    tgtMap.compute(src.getId(), (k, v) -> 
        v == null ? tgt
            : new Tgt(src.getId(), 
                tgt.getTrue_count() + v.getTrue_count(), 
                tgt.getFalse_count() + v.getFalse_count()));
}

For this code you need a Pair class - either from a library, or you can create it yourself. Instead of custom collector you could use Collectors.partitioningBy, but this way it's more efficient.

List<Tgt> tgtList = srcList.stream()
        .collect(Collectors.groupingBy(Src::getId, Collector.of(
                () -> new Pair<>(0L, 0L),
                (pair, src) -> {
                    if (src.isFlag())
                        pair.setKey(pair.getKey() + src.getCount());
                    else
                        pair.setValue(pair.getValue() + src.getCount());
                },
                (pairA, pairB) -> new Pair<>(pairA.getKey() + pairB.getKey(), pairA.getValue() + pairB.getValue())
        )))
        .entrySet()
        .stream()
        .map(entry -> new Tgt(entry.getKey(), (int) (long) entry.getValue().getKey(), (int) (long) entry.getValue().getValue()))
        .collect(Collectors.toList());
Related