🔥3장 함수형 프로그래밍
📌문제상황 - 여행 상품 개발
- 여행사에서 여행상품을 검색하는 application이 있다고 해보자.위 block은 data를 초기화하는 부분이고, 아랫부분은 도시의 이름에 따라 도시를 검색하는 내용이다.
- 요구사항이 추가로 들어와서 도시 이름이 아니라, 수도 이름으로 검색을 하고자 한다고 해보자. 그러면 다음과 같이 기능을 추가할 수 있다.
- 그런데 위와 같이 설계하면 다음과 같은 문제가 생긴다.
1. 조건에 따라 유사한 메서드가 계속해서 추가되는 구조다.
- if() {~~~} else if () {~~~~} else if .... if 절이 조건 수에 따라 늘어난다.
2. 클래스의 API가 너무 자주 바뀐다.
- 그래서 해결책으로 아래와 같이 조건절에 인터페이스를 둘 수 있다.
searchTravelInfo ( SearchCondition searchCondition ){
...
for(TravelInfo travel : travelList){
if(searchCondition.isMathched(travel)){
// do Logic
}
}
- java 8에서부터 인터페이스의 하나의 메서드만 정의한 것을 함수형 인터페이스라고 부른다.
📌익명클래스 - 람다
- 익명 클래스는 간편할 수 있으나, 소스코드 가독성이 떨어진다.
- 익명 클래스도 하나의 클래스이기 때문에 실제로 컴파일하면 클래스 파일이 별도로 생성된다. 이는 메모리 누수가 발생할 수 있다는 의미이다.
- 익명 클래스의 문제를 람다 표현식으로 해결할 수 있으나, 소스 코드의 재사용이라는 측면에서는 활용도가 떨어진다. 그래서 람다 표현식을 하나의 함수로 선언하고 재사용하는 방법을 사용할 수 있다.
🔥 4장
📌람다 표현식
- 람다 표현식이 나옴으로써 함수형 인터페이스, 메서드 참조 등이 나올 수 있었고 인터페이스와 메서드 참조를 바탕으로 스트림 API가 나올 수 있었다.
- 그래서 자바의 혁신의 한 축은 람다 표현식이라고 할 수 있다.
- 람다 표현식은 익명 클래스를 단순화하여 해당 표현식을 메서드의 parameter로 전달하거나, 인터페이스의 객체를 생성할 수 있게 해준다.
- 람다가 해결하고자 하는 것은 반복화되는 규격화 된 패턴의 제거다. 규격화 된 패턴은 아래와 같다.
1.메서드 이름
2. paramter
3. code 구현부
4. return
- 이 중, 이름과 return은 규격화를 위해 반복되는 부분이므로 삭제해도 무방하다.
- 람다 표현식은 1,4번은 과감히 삭제하고 인수(paramter)와 code 구현부에 집중한다. ( 2,3번 )
람다 구현규칙
() -> System.out.println("hi"); 📌 return type이 void
() -> "abc" 📌 return type이 string
- 값이 없으면 실행하는 것이고, 값이 있으면 값을 return 하는 것이라고 생각하면 된다.
- 람다 표현식 자체를 변수로 선언할 수도 있다.
🔥 람다 표현식 사용방법
- 아래의 코드 3개는 완전히 동치이다.
- 이 때, 주의해야 할 점은 람다 표현식 자체가 실행되는게 아니라, 람다 표현식은 말 그대로 식일 뿐이고 람다 표현식을 실행시키는 것은 코드를 호출하는 부분이다. ( 헷갈릴 수 있다 )
- 추가로 data 타입도 생략할 수 있다. (String a) -> a.length() 부분을 (a) -> a.length() 과 같이 type 생략이 가능하다. 그러나 개인적으로 데이터 타입 추론은 지양해야 한다고 생각한다.
- 람다 표현식 밖에서 생성한 변수도 참조 가능하다. 단, 해당 외부 변수를 참조하기 위해서는 해당 변수는 반드시 final이거나 final과 유사한 조건이어야 한다. final 값이 아니더라도, 값이 변경될 가능성이 없다면 컴파일러는 final 취급한다.
📌람다 표현식 기본
- 람다 표현식은 이름이 없고, 단지 paramter와 return type으로만 식별한다. 그렇다면 컴파일러는 어떻게 알고 이를 인식하고 해당 인터페이스의 구현체로 컴파일 할 수 있을까?
- 람다 표현식을 사용할 수 있는 인터페이스는 오직 public 메서드 하나만 갖고 있는 인터페이스여야 한다.
- java 8에서는 이를 특별히 함수형 인터페이스라고 부른다.
- 함수형 인터페이스에서 제공하는 단 하나의 추상 메서드를 함수형 메서드라고 부른다.
- Runnable, Comparator 등이 람다 표현식을 쓸 수 있던 이유는 해당 인터페이스들 모두가 단 하나의 추상 메서드를 갖고 있었기 때문이다.
- default, private 등의 메서드가 추가 돼있더라도, 단 하나의 추상메서드만이 존재한다면 함수형 메서드로 인식한다.
- 함수형 인터페이스라는 의미에서 @FunctionalInterface 어노테이션을 붙여줄 수 있다. 만약 해당 인터페이스가 두개 이상의 추상 메서드를 가지면 컴파일 과정에서 에러가 난다.
- 특별한 경우를 제외하고는, 기본 데이터 타입을 지원하는 함수형 인터페이스는 가급적 사용하지 않는다.
📌대표적 함수형 인터페이스
Consumer<T> void accept(T t) 📌사용만 할 때
Function<T,R> R apply(T t) 📌값을 변환 후 return 할 때
Predicate<T> boolean test(T t) 📌값을 검사할 때
Supplier<T> T get() 📌그냥 return 이 필요할 때
🔥Consumer
- 말 그대로 요청받은 내용을 '소비'한다.
- '소비' = 요청 받은 내용을 그대로 처리한다.
- 그래서 호출하는 쪽의 코드를 accpet 한다. 그래서 메서드 이름이 accept 이다.
참조 -- Consumer<String> consumer = (String name) -> System.out.println(name)
🔥 Function
- 말 그대로 함수이다. 메서드 이름은 apply로 동작한다.
- 두 개의 제네릭 타입을 정의해야 한다. T,R
- T는 인수 타입, R은 리턴 타입이다.
Function<String,Integer> 에서 String : paramter type // Integer : return type 이다.
🔥 Predicate
- predicate가 예측이라는 뜻인데, 이름에 걸맞게 bool 타입을 리턴한다.
- 메서드의 이름은 test이다.
🔥 Supplier
- 입력이 없고 출력만 있어서 공급자(Supplier)라는 이름이 붙었다.
- 공급자에게 값을 받아간다는 의미에서 메서드 이름은 get이다.
- 파라미터 없이 리턴 타입만 있는 메서드는 주로 지정된 정보를 확인하거나 조회할 때 유용하다.
📌기본형 데이터를 위한 인터페이스
- 기본 to 참조형 : boxing
- 참조 to 기본 : un-boxing
- 헷갈리면, 객체로 만들어주는 것이 boxing 이라고 생각하자.
- 이 때, 오토 박싱/언박싱은 JVM 입장에서 굉장히 비용이 많이 드는 작업이다.
- 그래서 기본형 데이터를 위한 인터페이스를 제공한다.
🔥 예제 - IntPredicate
- 위와 같은 기본형 타입들은 이미 타입들이 정해져 있기 때문에 제네릭 타입을 정의할 필요 없이 그냥 사용하면 된다.
📌 Operator interface : UnaryOperator - BinaryOperator
- 정수 혹은 실수형만을 위해 존재하는 인터페이스
- operator 는 function 인터페이스를 상속받는다. 모든 operator 인터페이스는 function 인터페이스의 하위 인터페이스이다.
- unary = 단항
- function의 실수형 계산만을 위해 제공되는 인터페이스이나, 잘 사용되지는 않는다.
📌 메서드 참조 ( Method Reference )
- 함수를 메서드 parameter로 전달하는 것을 메서드 참조라고 부른다.
- 마지막으로 호출할 메서드 값 앞에 :: 를 붙여준다.
예시
아래 두 개는 동치이다.
(String name) -> System.out.println(name)
System.out::println
- 메서드 참조의 경우 parameter를 생략할 수 있다. 이 때의 값은 생략된 parameter를 사용한다.
List<String> list = new ArrayList<>();
...
list.stream().forEach( (String name) -> System.out.println(name) )
위와 아래는 동치이다.
list.stream().forEach(System.out::println)
- 메서드 참조는 아래와 같이 3가지 유형이 있다.
1. 정적 메서드 참조 -> static method 참조
2. 비 한정적 메서드 참조 -> 특정 객체를 참조할 때 참조하기 위한 변수를 지정하지 않는다.
3. 한정적 메서드 참조 -> 참조하는 메서드가 특정 객체의 변수로 제한
이 부분은 넘어간다.
📌 생성자 참조
- 클래스명 :: new 의 형태
- 새로운 객체를 생성해서 return 해야할 때 사용한다.
📌 람다 표현식 조합
🔥 Consumer 조합
- Consumer에서는 default 메서드로 andThen 메서드를 제공한다.
- 두 개의 Consumer를 잇는다면 , 두 Consumer는 독립적으로 실행된다.
🔥 Predicate 조합
- 여러 개의 참, 거짓 조건식을 합친다.
- 여러 개로 연결된 Predicate의 bool값을 조합하여 최종 값을 return한다.
- 연결하는 함수의 제네릭 타입은 동일해야 한다.
- and와 or 메서드를 제공한다.
🔥 Function 조합
- andThen 으로 연결하고, apply로 호출한다.
- consumer와 유사하나, 앞의 결과가 뒤에 결과에 영향을 주기 때문에 상호 독립적이지 않다.
- 연결하는 함수의 제네릭 타입은 동일해야 한다.
- compose( 연결하고자 하는 인터페이스 ) .apply(변수) 로도 연결할 수 있다. compose는 뒤에서 앞으로 함수형 메서드가 호출되지만, 잘 사용되지는 않는다.
'책 정리 > 📌Practical 모던 자바' 카테고리의 다른 글
2장 인터페이스 (0) | 2021.10.30 |
---|---|
1장 java 버전별 변화 (0) | 2021.10.27 |
댓글