Don't give up!

[JAVA] 자바의 정석 정리 (14장 - 람다와 스트림) 본문

개발서적/JAVA의 정석

[JAVA] 자바의 정석 정리 (14장 - 람다와 스트림)

Heang Lee 2021. 6. 12. 22:11
자바의 정석을 읽고 정리한 내용입니다.

Java의 정석 - YES24

 

Java의 정석

최근 7년동안 자바 분야의 베스트 셀러 1위를 지켜온 `자바의 정석`의 최신판. 저자가 카페에서 12년간 직접 독자들에게 답변을 해오면서 초보자가 어려워하는 부분을 잘 파악하고 쓴 책. 뿐만 아

www.yes24.com


1. 람다식

(1) 람다식은 메서드를 하나의 식으로 표현한 것. 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.

(2) 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로 람다식을 익명함수라고도 한다. 

int[] arr = new int[5];
Arrays.setAll(arr, (i) ->(int)(Math.random()*5)+1);
//위의 람다식은 아래의 함수와 같은 역할을 수행한다.
int method(){
    return (int) (Math.random()*5)+1;
}

(3) 람다식은 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통 { } 사이에 ->를 추가한다.

(4) 반환 값이 있는 경우 return문 대신 식의 연산결과가 자동적으로 반환값이 되도록 할 수 있다. 이때는 끝에 ;를 붙이지 않는다.

(5) 람다식에 선언된 매개변수의 타입은 추론이 가능한 경우 생략할 수 있다. 반환타입이 없는 이유도 추론이 가능하기 때문. (단, 매개변수중 어느 하나의 타입만 생략하는 것은 허용되지 않는다.)

(6) 선언된 매개변수가 하나뿐인 경우에는 괄호를 생략할 수 있다. 단, 매개변수의 타입이 있으면 괄호를 생략할 수 없다.

int max(int a, int b){
	return a > b ? a : b;
}
(int a, int b) -> {return a > b ? a : b; }	//OK
(int a, int b) -> return a > b ? a : b;	//ERROR!
(int a, int b) -> a > b ? a : b	//OK
(int a, b) -> a > b ? a : b //ERROR!
(a, b) -> a > b ? a : b	//OK
a -> a * a //OK
int a -> a * a //ERROR!
(int[] arr) -> {
	int sum = 0;
    for(int i : arr)
    	sum+=i;
    return sum;
}//OK

2. 함수형 인터페이스

(1) 람다식은 익명 클래스의 객체와 동등하다.

(2) 람다식을 다루기 위한 인터페이스를 함수형 인터페이스라고 부른다.

(3) 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다. 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있도록 하기 때문. (static 메서드와 default 메서드에는 제약이 없다.)

(4) 메서드의 매개변수가 함수형 인터페이스라면 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야한다. (참조변수 없이 직접 람다식을 매개변수로 지정하는 것도 가능하다.)

(5) 메서드의 반환타입이 함수형 인터페이스타입이라면 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있다.

@FunctionalInterface
interface MyFunction{
    void myMethod();
}
void aMethod(MyFunction f){
    f.myMethod();
}
MyFunction f = () -> System.out.println("myMethod()");
aMethod(f);
aMethod( () -> System.out.println("myMethod()" );
MyFunction myMethod() {
    MyFunction f = ()->{};
    return f;
}

(6) 람다식은 익명 객체이고 익명 객체는 타입이 없다. 따라서 대입 연산자의 양변의 타입을 일치시키기 위해 형변환이 필요하다.

(7) 람다식은 인터페이스를 구현한 클래스 객체와 완전히 동일하기 때문에 동일한 인터페이스로의 형변환을 허용한다. 이러한 형변환은 생략가능하다.

(8) 람다식은 객체이지만 Object타입으로 형변환 할 수 없다. 람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.

(9) 람다식 내에서 참조하는 지역변수는 final이 붙지 않았어도 상수로 간주된다. 반면 클래스 인스턴스 변수는 상수로 간주되지 않아 값을 변경할 수 있다. (외부 지역변수와 같은 이름의 람다식 매개변수는 허용되지 않는다.)

3. java.util.function 패키지

(1) java.util.function 패키지에는 일반적으로 자주 쓰이는 형식의 메서드가 미리 정의되어 있다. 재사용성이나 유지보수 측면에서 가능하면 이 패키지의 인터페이스를 활용하는 것이 좋다.

함수형 인터페이스 메서드 설명
java.lang.Runnable void run( ) 매개변수 x, 반환값 x
Supplier<T> T get( ) 매개변수 x, 반환값만 있음
Consumer<T> void accept(T t) 매개변수만 있고 반환값 없음
Function<T,R> R apply(T t) 하나의 매개변수를 받아 결과를 반환
Predicate<T> boolean test(T t) 조건식을 표현하는데 사용, 매개변수는 하나, 반환 타입은 boolean
BiConsumer<T,U> void accept(T t, U u) 두개의 매개변수만 있고 반환값이 없음.
BiPredicate<T,U> boolean test(T t, U u) 조건식을 표현하는데 사용, 매개변수는 둘, 반환 타입은 boolean
BiFunction<T,U,R> R apply(T t, U u) 두 개의 매개변수를 받아서 하나의 결과를 반환

(2) UnaryOperator와 BinaryOperator는 Function의 자손 인터페이스로 매개변수의 타입과 반환 타입이 모두 일치한다.

(3) 두 람다식을 합성해서 새로운 람다식을 만들 수 있다. 함수형 인터페이스에 정의된 디폴트 메서드와 static 메서드는 이러한 작업을 돕는다. (ex Function 인터페이스의 andThen, compose, identity함수, Predicate의 and, or, negate, isEqual함수)

4. 메서드 참조

(1) 람다식이 하나의 메서드만 호출하는 경우에는 메서드 참조로 람다식을 간략히 할 수 있다.

(2) 하나의 메서드만 호출하는 람다식은 클래스이름::메서드이름 또는 참조변수::메서드이름으로 바꿀 수 있다.

종류 람다식 메서드 참조
static 메서드 참조 (x) -> ClassName.method(x) ClassName::method
인스턴스 메서드 참조 (obj, x) -> obj.method(x) ClassName::method
특정 객체 인스턴스메서드 참조 (x) -> obj.method(x) obj::method

(2) 생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다. 매개변수가 있는 생성자라면 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다. 

Supplier<MyClass> s = () -> new MyClass();//람다식
Supplier<MyClass> s = MyClass::new;//메서드 참조
Function<Integer, MyClass> f = (i) -> new MyClass(i);//람다식
Function<Integer, MyClass> f2 = MyClass::new;//메서드 참조
BiFunction<Integer, String, MyClass> bf = (i,s) -> new MyClass(i,s);//람다식
BiFunction<Integer, String, MyClass> bf2 = MyClass::new;//메서드 참조
Function<Integer, int[]> f = x -> new int[x];//람다식
Function<Integer, int[]> f2 = int[]::new;//메서드참조

5. 스트림

(1) 스트림은 데이터소스를 추상화하고 데이터를 다루는데 자주 사용되는 메서드들을 정의함으로서 코드의 재사용성을 높인다. 또한 스트림을 이용하면 배열이나 컬렉션 뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

(2) 스트림은 데이터 소스로부터 데이터를 읽기만 한다. 스트림으로 작업을 수행한 결과를 컬렉션이나 배열에 담아서 반환할 수는 있지만 스트림은 데이터 소스를 변경하지 않는다. 

(3) 스트림은 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야한다.

(4) 스트림이 제공하는 연산은 연산결과를 스트림으로 반환하는 중간 연산과 스트림의 요소를 소모하면서 연산을 수행하는 최종 연산으로 분류할 수 있다.

(5) 모든 중간 연산의 결과는 스트림이지만 연산 전의 스트림과 같은 것은 아니다. 스트림의 작업을 수행한 결과 스트림을 반환한다.

(6) 스트림 연산에서 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다. 중간 연산이 호출하는 것은 단지 어떤 작업이 수행되어야 하는지를 지정해주는 것일뿐, 최종 연산이 수행되어야 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.

중간 연산 설명
Stream<T> distinct( ) 중복을 제거
Stream<T> filter(Predicate<T> predicate) 조건에 안맞는 요소를 제외
Stream<T> limit(long maxsize) 스트림의 일부를 잘라낸다.
Stream<T> skip(long n) 스트림의 일부를 건너뛴다.
Stream<T> peek(Consumer<T> action) 스트림의 요소에 작업을 수행
Stream<T> sorted(Comparator<T> comparator) 스트림의 요소를 정렬한다.
Stream<R> map(Function<T,R> mapper)
IntStream mapToInt(ToIntFunction<T> mapper)
스트림의 요소를 변환한다.
최종 연산 설명
void forEach(Consumer<? super T> action) 각 요소에 지정된 작업 수행
long count( ) 스트림의 요소의 개수 반환
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)
스트림의 최대값/최소값을 반환
Optional<T> findAny( )
Optional<T> findFirst( )
스트림의 요소 하나를 반환
boolean allMatch(Predicate<T> p)
boolean anyMatch(Predicate<T> p)
boolean noneMatch(Predicate<> p)
주어진 조건에 대한 확인을 수행
Object[] toArray( )
A[] toArray(IntFunction<A[ ]> generator)
스트림의 모든 요소를 배열로 변환
Optional<T> reduce(BinaryOperator<T> accumulator) 스트림의 요소를 하나씩 줄여가면서 계산
R collect(Collector<T,A,R> collector) 스트임의 요소를 수집한다. 주로 요소를 그룹화하거나 분할한 결과를 컬렉션에 담아 반환하는데 사용

(7) 스트림은 기본적으로 Stream<T>이지만 오토박싱과 언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림(IntStream, LongStream, DoubleStream)이 제공된다. 기본형 타입의 값으로 작업하는데 유용한 메서드들이 포함되어 있다.

(8) parallel( )메서드는 내부적으로 프레임웍을 이용해서 스트림이 자동적으로 연산을 병렬로 수행하도록 한다. sequential( )메서드는 반대로 병렬로 처리되지 않도록 한다.

(9) 컬렉션은 stream( )를, 배열은 Stream.of( ) 또는 Arrays.stream( )메서드를 사용하여 스트림을 만들 수 있다. 기본형 스트림 클래스에도 of( )메서드가 정의되어 있으며, Arrays.stream( )메서드는 기본형 스트림 클래스에 대해 오버로딩이 되어 있다.

(10) empty( )메서드는 비어있는 스트림을 생성한다. 스트림에 연산을 수행한 결과가 하나도 없을 때 null보다 빈 스트림을 반환하는 것이 좋다.

(11) concat( )메서드는 두 스트림을 하나로 연결한다. 이때 두 스트림의 요소는 같은 타입이어야 한다.

(12) Optional 타입의 객체에는 모든 타입의 참조변수를 담을 수 있다. 최종 연산의 결과를 Optional객체에 담아 반환함으로서 NullPointerException이 발생하지 않는 보다 간결하고 안전한 코드를 작성하는 것이 가능하다.

(13) collect( )는 스트림의 요소를 수집하는 최종 연산이다. 매개변수의 타입이 Collector로, Collector 객체에 구현된 방법대로 스트림의 요소를 수집한다. 매개변수로 toList( ), toSet( ), toMap( ), toCollection( ), toArray( )메서드를 사용하여 요소를 컬렉션에 담아 반환할 수 있다.

(14) partitioningBy( )와 groupingBy( )는 스트림의 요소를 특정 기준으로 그룹화/분할한다. collect메서드의 매개변수로 사용하여 원하는 컬렉션을 얻을 수 있다.