4

Given a simple list of strings:

List<String> strings = Arrays.asList("Mary", "James", "Jim", "Camilla");

I'd like to process each item of the list with a anotherMethod() method, however the first item shall be processed additionally by someMethod().

I've tried something like this:

List<String> strings = Arrays.asList("Mary", "James", "Jim", "Camilla");

var result = strings.stream()
     .findFirst()
     .map(myString -> someMethod(myString))
     // "resume" stream
     .map(myString -> anotherMethod(myString))
     .toList();     

Is there a way to handle such a scenario using a Stream or something similar?


Update 1

Here is an example to help you better understand the motivation behind my question.

List<String> strings = Arrays.asList("Mary", "James", "Jim", "Camilla");

public String someMethod(String s) {
  return s.toUpperCase();
}

public String anotherMethod(String s) {
  return s.toLowerCase();
}

The final result shall be a List<String> with the following elements, that is, only the first element is converted to upper-case while all other elements are converted to lower-case.

"MARY", "james", "jim", "camilla"

Robert Strauch
  • 12,055
  • 24
  • 120
  • 192

5 Answers5

5

findFirst collapses the stream to an Optional<String>, and there is nothing to resume.

Optional<String> mary = Stream.of("Mary", "James", "Jim", "Camilla").findFirst();

Do you want to reduce the stream to only one element? Then the method limit(1) might be what you are looking for and then concat it with a copy where you skip(1) the element.

List<String> strings = Arrays.asList("Mary", "James", "Jim", "Camilla");
List<String> result = Stream.concat(
      strings.stream()
        .limit(1)
        .map(this::someMethod),
      strings.stream()
        .skip(1))
        .map(this::anotherMethod)
     ).toList();

Edit: After Update 1:

You wouldn't want to apply toLowerCase() to the first element after you applied toUpperCase().

Valerij Dobler
  • 1,848
  • 15
  • 25
2

You could create an IntStream over the indexes.

IntStream.range(0, strings.size())
    .mapToObj(i -> {
        var s = strings.get(i);
        if (i == 0) s = someMethod(s);
        return anotherMethod(s);
    }).toList();
Unmitigated
  • 76,500
  • 11
  • 62
  • 80
2

Optional.stream()

Your idea of using findFirst() is perfectly viable.

We can apply map() on the Optional result and make use of the Java 9 Optional.stream() (to "resume" the stream).

flatMap() operation in conjunction with Stream.concat() would be handy for combining the very first element with the rest part of the stream.

Apply the final transformation and collect the result into a list.

List<String> strings = Arrays.asList("Mary", "James", "Jim", "Camilla");

List<String> result = strings.stream().findFirst()
    .map(MyClass::someMethod)
    .stream()
    .flatMap(s -> Stream.concat(
        Stream.of(s), 
        strings.stream().skip(1).map(MyClass::anotherMethod)
    ))
    .toList();

Iterator.forEachRemaining()

Another option would be to use Java 8 Iterator.forEachRemaining():

List<String> strings = Arrays.asList("Mary", "James", "Jim", "Camilla");
List<String> result = new ArrayList<>();

Iterator<String> iterator = strings.iterator();
result.add(someMethod(iterator.next()));
iterator.forEachRemaining(s -> result.add(anotherMethod(s));
Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • 2
    The OP wants to process all the elements from the original stream, and interpose *extra* processing for the first one of them. Handling the second and subsequent elements of the original stream is whath they mean by "resuming" the stream. The approach based on `Optional.stream()` doesn't do that at all. – John Bollinger Dec 20 '22 at 22:19
  • 1
    The collector-based approach, on the other hand, has a subtler flaw: it does not satisfy the associativity constraint on collectors. It will likely work in many cases, but it will not produce the same results when a computation is split. – John Bollinger Dec 20 '22 at 22:24
  • @JohnBollinger The second solution handles everything using a single stream. And BTW, from this statement I infer that any approach will do *Is there a way to handle such a scenario using a Stream or something similar?* – Alexander Ivanchenko Dec 20 '22 at 22:25
  • 1
    The issue is not about ordering. It is about there (possibly) being multiple partial results to combine. Your accumulator will perform extra processing on the first item accumulated into *each* partial result, but that processing is wanted only for the first item overall in the stream. – John Bollinger Dec 20 '22 at 22:49
  • 1
    This is a matter of correctness, not overhead. Your collector will produce *different output* depending on how many ways the stream is split. Which is where I started: it does not satisfy the associativity constraint on collectors. – John Bollinger Dec 20 '22 at 23:00
  • @JohnBollinger Agree, updated the answer. – Alexander Ivanchenko Dec 20 '22 at 23:19
  • 1
    I like `Iterator.forEachRemaining()` for this. It's not a stream, but I would account it "something similar", and it affords a clear, compact implementation. – John Bollinger Dec 21 '22 at 13:25
0

Try StreamEx or abacus-common (I'm the developer of this library)

var result = StreamEx.of(strings)
           .mapFirstOrElse(String::toUpperCase, String::toLowerCase)
           .toList();

result.forEach(System.out::println); 
user_3380739
  • 1
  • 14
  • 14
0
 final AtomicBoolean processFirstElement = new AtomicBoolean(true);
        strings.stream()
                .map(input->processFirstElement.getAndSet(false)?
                                someMethod(input)
                                :input)
                .map(YourClass::anotherMethod)
                .collect(Collectors.toList());

Ideally we should avoid using mutable objects,AtomicBoolean should be able to maintain happens-before and hence prevent racing condition.

Sagar
  • 104
  • 1
  • 5