Language/Java

[JAVA] Collection class (컬렉션)의 forEach() method 알아보기

jungwon3004 2022. 1. 21. 16:43
728x90
반응형

0. forEach() method 발견

그동안 Collection instance (ex. HashMap, LinkedList, ArrayList, HashSet 등) 만들고 전체 데이터를 다룰 때, 자연스럽게 Iterator를 생성해서 진행했었다.

실제로 대부분 기본서에서도 그렇게 설명하고 있다.

나중에 유지보수할 때도 그게 좋다고 하기도 하고.

 

그러다 며칠 전 forEach()라는 method가 있다는 사실을 알게 되었다.

신기한 마음에 달려와 글을 써본다.

 

 

 

1. 맛보기

일단 간단한 코드를 보고 이야기하는게 더 좋을 듯하다.

Collection<String> col1 = Arrays.asList("1", "2", "3", "4");
Collection<String> col2 = new HashSet<>();
col2.addAll(test1);

col2.forEach(a -> System.out.print(a));
System.out.println();
col2.forEach(System.out::println);

//console
//1234
//1234

col2를 만드는 과정은 간단하다.

그냥 "1", "2", "3", "4"라는 값을 가지고 있는 HashSet<String>에서 Collection<String> 타입으로 만든 것이다.

여기까진 간단하니 설명하지 않고 넘어가겠다.

 

이제 그 col2라는 instance에서 forEach() method를 사용한 것을 보자.

저렇게 하고 나면 결과가 각각 1234로 동일하게 나온다.

 

col2.forEach(a->System.out.println(a));

col2.forEach(System.out::println);

아직 무엇인지는 모르더라도 결과 값이 1234인걸 알기 때문에 뭔가 전체를 돌면서 코드를 실행했겠구나 정도의 유추는 할 수 있을 것이다.

 

 

 

2. 뿌리를 찾아서

forEach()는 어디서 나오는 걸까?

Collection class와 Collections class를 아무리 찾아도 forEach()라는 method가 없다.

 

놀랍게도 forEach()는 Collection<T> 가 implements 하고 있는 interface인 Iterable<T>가 가지고 있는 default method이다.

 

Interface Iterable<T>

default void forEach(Consumer<? super T> action)

 

- the default implementation behaves as if:

for (T t : this)

    action.accept(t);

 

라는 설명이 나온다.

여기서 그럼 Consumer<T>를 한 번 더 찾아봐야 한다.

왜나하면, forEach()action.accpt(t)를 for loop 중이기 때문이다.

 

Interface Consumer<T>

void accept(T t)

: Performs this operation on the given argument.

 

Consumer<T>는 interface이고 accept(T t) method는 default가 아니기 때문에 사용할 때 override해야 한다.

다만 확실한 것은 설명에 쓰여있다.

어떤 것이든 T type의 Generic이 들어오면 전체를 돌면서 한 번씩 꺼내는 역할을 해야 한다는 말이 된다.

 

이렇게 뿌리를 알고나니 이제 forEach()의 사용법이 명확해졌다.

 

 

 

3. 사용법

col2.forEach(new Consumer<String>(){
	@Override
    public void accept(String tmp){
    	System.out.print(tmp);
    }
});

이렇게 forEach()의 parameter에는 Consumer<T>를 implements한 class의 instance, 즉 accept(T t)를 override한 새로운 class의 instance가 오면 된다.

 

위의 예시에서는 따로 class를 파일을 만들거나 하지 않고 anonymous class (익명클래스)를 이용한 코드에 new를 통해 instance를 만들어서 argument로 넣어주었다.

즉, 익명객체 (anonymous object)를 arugument로 넣은 것이다.

 

 

 

4. 응용

그럼 여기서 살짝 궁금증이 생길 것이다.

 

forEach()의 parameter에는 Consumer<T>를 implements한 class의 instance가 와야한다.

위 예시에서는 anonymous class에 new를 만들어서 instance를 만들었다.

여기까지는 다들 이해했을 것이다.

 

하지만 맨 처음 소개했던 예시

col2.forEach(a -> System.out.print(a));
col2.forEach(System.out::println);

이건 어떻게 가능한 것일까?

 

사실 이것도 같은 동일한 내용이다.

다만 표기법이 다른 것!

 

 

(1) Lambda 람다

col2.forEach(a -> System.out.print(a));
col2.forEach(new Consumer<String>(){
	@Override
    public void accept(String tmp){
    	System.out.print(tmp);
    }
});

사실 이건 완전히 동일한 문장이다.

람다식이라는 것이 Funtional Interface에 대해서 abstract method를 쉽고 간결하게 override해주는 방법이다.

 

이 과정에서 jdk가 알아서 나머지를 추측해주는 것이다.

forEach() 의 parameter로는 Consumer<T>를 implements한 class의 instance가 온다는 사실은 확실하다.

그러면 우리가 수정할 부분은 public void accept(T t); 를 override하는 것밖에 없다.

T type의 instance t 가 들어왔을 때 이걸 어떻게 처리할 것인가만 정의해주면 된다.

 

결국 a -> System.out.print(a) 라는 식은 a라는 parameter를 받으면 System.out.print(a)를 하라는 것을 적어준 것이고 나머지는 변화할 것이 없기 때문에 실행될 때 자동으로 처리해주는 것이다.

 

 

사실 조금 더 정확이히는

col2.forEach((a) -> {System.out.print(a);});

이게 정석이다.

하지만 parameter가 하나라면 (a) 에서 괄호를 생략할 수 있다

따라서

col2.forEach(a -> {System.out.print(a);});

이렇게 적어도 무방하다.

또한 실행코드가 1줄이라면 중괄호 { } 도 생략이 가능하다. 

col2.forEach(a -> System.out.print(a));

그래서 이런 코드가 등장했던 것이다.

 

 

(2) :: 이중 콜론 연산자 (= method reference expression)

col2.forEach(System.out::println);

다음으로 이건 뭘까?

 

이건 람다를 한 번 더 응용한 이중콜론 연산, 정식명칭은 method reference expression (메소드 참조 표현식)이다.

 

인스턴스::메소드명

의 형태로 나타내면 된다.

 

a -> System.out.println(a)

를 자세히 보면 System.out 이라는 instance의 method인 println()을 사용하는 것이다.

따라서 System.out :: println 이라고 적으면 된다.

심지어 method 뒤에 나오는 ( ) 괄호도 안 써도 된다.

 

728x90
반응형