Stream mit Sonderbehandlung des ersten Elements

looparda

Top Contributor
Ich suche nach einem eleganten Weg einen Stream zu durchlaufen mit Sonderbehandlung des ersten Elements. Ich habe ein paar Ansätze und Libraries gefunden und ausprobiert, aber bin noch nicht so zufrieden, da alle über den Index gehen und ich das gern weghaben möchte. Vielleicht fällt jemandem noch etwas besseres ein oder jemand kennt eine passende Lib.

[CODE lang="java" title="StreamFirstTest"]
import static org.assertj.core.api.Assertions.*;

import java.util.List;

import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;

class StreamFirstTest {

private final List<String> input = Lists.newArrayList("a", "b", "c");
private final List<String> expected = Lists.newArrayList("first", "b", "c");

@Test
public void test_mapThingsWithSpecialCaseFirstItem() {
final List<String> actual = StreamFirst.mapThingsWithSpecialCaseFirstItem(input);

assertThat(actual).isEqualTo(expected);
}

@Test
public void test_mapThingsWithSpecialCaseFirstItem_Index() {
final List<String> actual = StreamFirst.mapThingsWithSpecialCaseFirstItem_Index(input);

assertThat(actual).isEqualTo(expected);
}

@Test
public void test_mapThingsWithSpecialCaseFirstItem_StreamEx_EntryStream() {
final List<String> actual = StreamFirst.mapThingsWithSpecialCaseFirstItem_StreamEx_EntryStream(input);

assertThat(actual).isEqualTo(expected);
}

@Test
public void test_mapThingsWithSpecialCaseFirstItem_Guava() {
final List<String> actual = StreamFirst.mapThingsWithSpecialCaseFirstItem_Guava_mapWithIndex(input);

assertThat(actual).isEqualTo(expected);
}
}
[/CODE]
[CODE lang="java" title="StreamFirst"]import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javax.util.streamex.EntryStream;

import com.google.common.collect.Streams;

public class StreamFirst {

private static String mapping(String item, boolean isFirstItem) {
if (isFirstItem) {
return "first";
} else {
return item;
}
}

public static List<String> mapThingsWithSpecialCaseFirstItem(List<String> list) {
return list
.stream()
.map(item -> mapping(item, list.indexOf(item) == 0))
.collect(Collectors.toList());
}

public static List<String> mapThingsWithSpecialCaseFirstItem_Index(List<String> list) {
return IntStream.range(0, list.size())
.mapToObj(i -> mapping(list.get(i), i == 0))
.collect(Collectors.toList());
}

public static List<String> mapThingsWithSpecialCaseFirstItem_StreamEx_EntryStream(List<String> list) {
return EntryStream.of(list)
.mapKeyValue((index, item) -> mapping(item, index == 0))
.toList();
}

@SuppressWarnings("UnstableApiUsage")
public static List<String> mapThingsWithSpecialCaseFirstItem_Guava_mapWithIndex(List<String> list) {
return Streams.mapWithIndex(list.stream(), (item, index) -> mapping(item, index == 0))
.collect(Collectors.toList());
}

}[/CODE]
 
Hmmm.. da fällt mir sonst nur noch skip() ein:
Java:
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Stream.*;
...
public static List<String> mapThingsWithSpecialCaseFirstItem_Skip(List<String> list) {
  return list.isEmpty()
       ? emptyList()
       : concat(of("first"), list.stream().skip(1)).collect(toList());
}
 
Ich habe die mapping Funktion zum besseren Verständnis sehr vereinfacht dargestellt und tatsächlich geht es auch nicht um Strings. Ich benötige ein
  1. Mapping und
  2. im Mapping den Zugriff auf das aktuelle Element und ob es das erste ist.
 
Benötigt die Mapping-Funktion denn auch im Falle des ersten Elementes das Element selbst (weil sie bei dir aktuell im Falle des ersten Elementes ja eine Konstante zurückliefert)?

Falls nicht, dann hier eine ganz generische Version:
Java:
public static <T, R> List<R> mapThingsWithSpecialCaseFirstItem_Skip(
    List<T> list,
    BiFunction<T, Boolean, R> mapFunction) {
  return list.isEmpty()
       ? emptyList()
       : concat(of(mapFunction.apply(null, true)),
                list.stream()
                    .skip(1)
                    .map(e -> mapFunction.apply(e, false)))
        .collect(toList());
}
 
Zuletzt bearbeitet:
Ja, leider schon. Die Konstante kommt auch durch die Vereinfachung des Problems.
😀

Als nächstes sagst du wahrscheinlich: "Das erste" ist auch eine Vereinfachung des Problems. In Wirklichkeit muss ein Element eine kompliziertere Prädikatsfunktion erfüllen, um als (aktuell vereinfacht) "das erste" zu gelten. 🙂

Und dann: Es gibt nicht nur ein "das erste" Element (bzw. ein prädikatserfüllendes Element), sondern mehrere. 😀
 
Basierend auf deinem Ansatz müsste sowas gehen. Aber ist wiederum nicht schön... unter anderem wegen Stream im Stream.
Java:
    public static <T, R> List<R> mapThingsWithSpecialCaseFirstItem_Skip(
            List<T> list,
            BiFunction<T, Boolean, R> mapFunction) {
        return list.isEmpty()
                ? emptyList()
                : concat(list.stream().limit(1).map(e -> mapFunction.apply(e, true)),
                list.stream().skip(1).map(e -> mapFunction.apply(e, false)))
                .collect(Collectors.toList());
    }
 
Vielleicht trifft es das Beispiel besser:
Java:
import static org.assertj.core.api.Assertions.*;

import java.util.List;

import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;

class StreamFirstTest {

    private final List<OrderItem> input = Lists.newArrayList(new OrderItem(2), new OrderItem(3), new OrderItem(4));
    private final List<String> expected = Lists.newArrayList(
            "first item in order with discount:1.8",
            "regular price:3.0",
            "regular price:4.0");

    @Test
    public void test_mapThingsWithSpecialCaseFirstItem() {
        final List<String> actual = StreamFirst2.mapThingsWithSpecialCaseFirstItem(input);

        assertThat(actual).isEqualTo(expected);
    }

}

import java.util.List;
import java.util.stream.Collectors;

public class StreamFirst2 {

    static class OrderItem {
        double price;
        OrderItem(double price) {
            this.price = price;
        }
    }

    public static String firstItemDiscountMapper(OrderItem item, boolean isFirstItem) {
        if (isFirstItem) {
            return "first item in order with discount:" + item.price*0.9;
        } else {
            return "regular price:" + item.price;
        }
    }

    public static List<String> mapThingsWithSpecialCaseFirstItem(List<OrderItem> list) {
        return list
                .stream()
                .map(item -> firstItemDiscountMapper(item, list.indexOf(item) == 0))
                .collect(Collectors.toList());
    }

}
 
Nimm doch einen Zustandsbehafteten Mapper:

Java:
public class SpecialOrderMapper() {
private boolean first = true;
public String mapToString(OrderItem item) {
        if (first) {
            first = false;
            return "first item in order with discount:" + item.price*0.9;
        } else {
            return "regular price:" + item.price;
        }
    }
}

public class BusinessCode {
    public List<String> mapThingsWithSpecialCaseFirstItem(List<OrderItem> list) {
       SpecialOrderMapper mapper = new SpecialOrderMapper();
        return list
                .stream()
                .map(specialMapper::mapToString)
                .collect(Collectors.toList());
    }
}
Ungetestet 🙂
 
Ich frage mich bei solchen Lösungen immer, ist das dann einfach parallelisierbar (bzw. funktioniert das beim parallelen Ausführen auch noch).

Daher was markiert das erste Element in einer Liste: der Index 0.

Daher mein Ansatz:
Java:
var whatever = IntStream.range(0, list.size()).mapToObj(i -> mapper.apply(list.get(i), i == 0)).collect(Collectors.toList());
 
Das entspricht dem mapThingsWithSpecialCaseFirstItem_Index Ansatz. Und ja, ich denke der Ansatz hat Vorteile, aber sieht trotzdem nicht elegant aus und drückt sofort ersichtlich aus: "Für das erste Element so, für den Rest so".
Oh shit ich hab nicht alles gelesen sorry.

Streams sind nicht wirklich geeignet um Zustände und Positionen zu halten oder zu verarbeiten. Solange hier kein Stream.withIndices oder so exisitiert musst du mit sowas wohl leben müssen, ohne jetzt irgendetwas neu zu stricken oder StreamEx zu verwenden.
 
Das scheint mir die einzige saubere Lösung zu sein:
Java:
    public static List<String> mapThingsWithSpecialCaseFirstItem(List<Double> orders) {
        Iterator<Double> iterator = orders.iterator();
        List<String> list = new LinkedList<>();
        if (iterator.hasNext())
            list.add(String.valueOf(iterator.next() * 0.9));
        iterator.forEachRemaining(o -> list.add(String.valueOf(o * 0.95)));
        return list;
    }

    public static void main(String[] args) {
        System.out.println(mapThingsWithSpecialCaseFirstItem(Arrays.asList(10.0, 10.0, 10.0)));
    }
 
Über Spliterator kann man immer ziemlich viel machen. Ich hab mal was kleines gebaut:
Java:
public class StreamEx {
   
    public static void main(String[] args) {
        List<Integer> lst = new ArrayList<>();
        for(int i = 0; i < 100; ++i) {
            lst.add(i);
        }
        List<String> res = specialFirstMapping(lst.stream(),
                    a -> "first",
                    b -> b.toString())
                .parallel()
                .collect(Collectors.toList());
        res.sort(Comparator.comparingInt(s -> {
            int i = 0;
            try {
                i = Integer.parseInt(s);
            }
            catch (Exception e) {
            }
            return i;
        }));
        System.out.println(res);
    }
   
   
    public static <T, R> Stream<R> specialFirstMapping(
            Stream<T> stream,
            Function<T, R> firstMapping,
            Function<T, R> restMapping) {
        return StreamSupport.stream(new SpecialFirstMappingSpliterator<>(
                stream.spliterator(), firstMapping, restMapping), stream.isParallel());
    }
   
   
    private static class SpecialFirstMappingSpliterator<T, R> implements Spliterator<R> {
       
        private final Spliterator<T> base;
        private final Function<T, R> firstMapping;
        private final Function<T, R> restMapping;
        private R firstObj = null;
        private boolean first = true;
       
        private SpecialFirstMappingSpliterator(Spliterator<T> base,
                Function<T, R> firstMapping,
                Function<T, R> restMapping) {
            this.base = base;
            this.firstMapping = firstMapping;
            this.restMapping  = restMapping;
        }
       
        @Override
        public boolean tryAdvance(Consumer<? super R> action) {
            if (firstObj != null) {
                action.accept(firstObj);
                firstObj = null;
                return true;
            }
            if (first) {
                first = false;
                return base.tryAdvance(t -> action.accept(firstMapping.apply(t)));
            }
            return base.tryAdvance(t -> action.accept(restMapping.apply(t)));
        }
       
        @Override
        public Spliterator<R> trySplit() {
            if (first) {
                base.tryAdvance(t -> { firstObj = firstMapping.apply(t); });
                Objects.requireNonNull(firstObj);
                first = false;
            }
            Spliterator<T> other = base.trySplit();
            if (other != null) {
                SpecialFirstMappingSpliterator<T,R> res = new SpecialFirstMappingSpliterator<T,R>(other, firstMapping, restMapping);
                res.first = false;
                return res;
            }
            return null;
        }
       
        @Override
        public long estimateSize() {
            return base.estimateSize();
        }
       
        @Override
        public int characteristics() {
            return base.characteristics();
        }
       
    }
   
}
Wie aber schon hier vermutet, wird es mit einer parallelen Verarbeitung schwierig. Ich habs über trySplit versucht. Allerdings garantiert die Methode nicht, welche Seite (links oder rechts) sie weiter bearbeitet und welche sie zurückgibt. Somit kann ich nur garantieren, dass EIN Element durch "firstMapping" behandelt wird, aber nicht welches.
Edit: Die parallele Verarbeitung klappt nun auch. Wenn man es nicht braucht, lässt man "trySplit" einfach "null" returnen und entfernt alles mit "firstObj".
 
Zuletzt bearbeitet:

Zurück
Oben