본문 바로가기


Java

[JAVA Stream] stream(), peek()의 함정

Java8 에서 stream 에는 두가지의 반복문이 사용가능한데

peek() 과 forEach() 가 있다.

두가지의 차이점이라고하면 forEach 는 그자체만 사용가능하지만 peek() 은 그렇지 않다.

 

이유는 단순한데 forEach는 return 값이 void 라서 최종처리메소드로 쓰일 수 있지만 peek은 stream 을 return 해서 불가능하다.

/*peek 구현체*/
@Override
public final Stream<P_OUT> peek(Consumer<? super P_OUT> action) {
    Objects.requireNonNull(action);
    return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
                                 0) {
        @Override
        Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
            return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
                @Override
                public void accept(P_OUT u) {
                    action.accept(u);
                    downstream.accept(u);
                }
            };
        }
    };
}

/*forEach 구현체*/
@Override
public void forEach(Consumer<? super P_OUT> action) {
    evaluate(ForEachOps.makeRef(action, false));
}

 

그래서 peek() 은 최종처리 메소드가 필요한데 sum 같은 최종처리 메소드가 필요합니다. 이 때 sum의 위치에 올 수 있는 것 중 하나가 바로

boolean 값을 리턴해주는 allMatch(), noneMatch(), anyMatch() 같은 것들 인데

이 과정에서 peek() 내부에 절대 상태변화가 들어가는 코드를 작성하면 안된다.

 

예를 들어

class Member {
	String name;
	Integer age;
	public Member(String name, Integer age) {
		this.name = name;
		this.age = age;
	}
	public boolean equals(Member member) {
		return (this.name.equals(member.name) && this.age.equals(member.age));
	}
}
public void peekTest() {
	Member member1 = new Member("MuMu", 30);
	Member member2 = new Member("MuNu", 40);
	Member member3 = new Member("MuLu", 50);
	List<Member> members = Lists.newArrayList(member1, member2, member3);
	System.out.println("TEST 1");
	members.stream()
		.peek(m ->
			System.out.println("My name is " + m.name))
		.anyMatch(m -> m.equals(member1));
	System.out.println("TEST 2");
	members.stream()
		.peek(m ->
			System.out.println("My name is " + m.name))
		.anyMatch(m -> m.equals(member2));
	System.out.println("TEST 3");
	members.stream()
		.peek(m ->
			System.out.println("My name is " + m.name))
		.anyMatch(m -> m.equals(member3));
}

이런 코드가 있다고 했을 때 다음과 같은 결과가 나온다.

TEST 1
My name is MuMu
My name is MuNu
TEST 2
My name is MuMu
TEST 3
My name is MuMu

 

forEach 와는 다르게 peek() 은 최종 처리 구문에 따라 실행순서를 조절한다. 모든 element 에 적용되는 것이아니라 Match 관련 메소드는 중간에 하나라도 결과값이 false가 되면 이 이후를 수행하지 않는다.

따라서 절대 peek 에는 처리 대상의 상태가 변경될 수 있는 코드를 넣지 않아야 한다. 

물론 이외에도 stream 사용 중에 원본 collection 의 데이터를 변경하는 코드는 분명 좋지 않은 코드이다. 실제로 match 같은 메소드가 없더라도 peek에 remove 같은 액션을 해보면 다 사라지지 않는 것을 알 수 있다. 그러니까 Stream 내에서 원본 list의 상태를 변경하지말자

 

끝!