🐾
개발자국
  • 🐶ABOUT
  • 🚲프로그래밍
    • 객체 지향 프로그래밍
    • 오브젝트
      • 1장: 객체, 설계
      • 2장: 객체지향 프로그래밍
      • 3장: 역할, 책임, 협력
      • 4장: 설계 품질과 트레이드오프
      • 5장: 책임 할당하기
      • 6장: 메시지와 인터페이스
      • 7장: 객체 분해
      • 8장: 의존성 관리하기
      • 9장: 유연한 설계
      • 10장: 상속과 코드 재사용
      • 11장: 합성과 유연한 설계
      • 12장: 다형성
      • 13장: 서브클래싱과 서브타이핑
      • 14장: 일관성 있는 협력
      • 15장: 디자인 패턴과 프레임워크
    • 도메인 주도 개발 시작하기
      • 1장: 도메인 모델 시작하기
      • 2장: 아키텍처 개요
      • 3장: 애그리거트
      • 4장: 리포지토리와 모델 구현
      • 5장: 스프링 데이터 JPA를 이용한 조회 기능
      • 6장: 응용 서비스와 표현 영역
      • 7장: 도메인 서비스
      • 8장: 애그리거트 트랜잭션 관리
      • 9장: 도메인 모델과 바운디드 컨텍스트
      • 10장: 이벤트
      • 11장: CQRS
    • 클린 아키텍처
      • 만들면서 배우는 클린 아키텍처
        • 계층형 아키텍처의 문제와 의존성 역전
        • 유스케이스
        • 웹 어댑터
        • 영속성 어댑터
        • 아키텍처 요소 테스트
        • 경계 간 매핑 전략
        • 애플리케이션 조립
        • 아키텍처 경계 강제하기
        • 지름길 사용하기
        • 아키텍처 스타일 결정하기
    • 디자인 패턴
      • 생성(Creational) 패턴
        • 팩토리 패턴
        • 싱글톤 패턴
        • 빌더 패턴
        • 프로토타입 패턴
      • 행동(Behavioral) 패턴
        • 전략 패턴
        • 옵저버 패턴
        • 커맨드 패턴
        • 템플릿 메서드 패턴
        • 반복자 패턴
        • 상태 패턴
        • 책임 연쇄 패턴
        • 인터프리터 패턴
        • 중재자 패턴
        • 메멘토 패턴
        • 비지터 패턴
      • 구조(Structural) 패턴
        • 데코레이터 패턴
        • 어댑터 패턴
        • 퍼사드 패턴
        • 컴포지트 패턴
        • 프록시 패턴
        • 브리지 패턴
        • 플라이웨이트 패턴
      • 복합 패턴
  • 시스템 설계
    • 1. 사용자 수에 따른 규모 확장성
    • 2. 개략적 규모 추정
    • 3. 시스템 설계 접근법
    • 4. 처리율 제한 장치
    • 5. 안정 해시
    • 6. 키-값 저장소
    • 7. 유일한 ID 생성기
    • 8. URL 단축기
    • 9. 웹 크롤러
    • 10. 알림 시스템
    • 11. 뉴스 피드
    • 12. 채팅 시스템
    • 13. 검색어 자동완성
    • 14. 유튜브 스트리밍
    • 15. 구글 드라이브
    • ⭐️. 캐싱 전략
    • ⭐️. 재고 시스템으로 알아보는 동시성이슈 해결방법
    • ⭐️. 실습으로 배우는 선착순 이벤트 시스템
  • 🏝️자바
    • 자바의 내부 속으로
      • Java 언어의 특징
      • JDK
      • JVM
        • 메모리 관리
        • Garbage Collector
          • 기본 동작
          • Heap 영역을 제외한 GC 처리 영역
          • (WIP) GC 알고리즘
        • 클래스 로더
      • 자바 실행 방식
      • 메모리 모델과 관리
      • 바이트 코드 조작
      • 리플렉션
      • 다이나믹 프록시
      • 어노테이션 프로세서
    • 자바의 기본
      • 데이터 타입, 변수, 배열
    • 이펙티브 자바
      • 2장: 객체의 생성과 파괴
        • item 1) 생성자 대신 정적 팩토리 메서드를 고려하라
        • item2) 생성자에 매개변수가 많다면 빌더를 고려하라
        • item3) private 생성자나 열거 타입으로 싱글톤임을 보증하라
        • item4) 인스턴스화를 막으려면 private 생성자를 사용
        • item5) 자원을 직접 명시하는 대신 의존 객체 주입 사용
        • item6) 불필요한 객체 생성 지양
        • item7) 다 쓴 객체는 참조 해제하라
        • item8) finalizer와 cleaner 사용 자제
        • item9) try-with-resources를 사용하자
      • 3장: 모든 객체의 공통 메서드
        • item 10) equals는 일반 규약을 지켜 재정의 하자
        • item 11) equals 재정의 시 hashCode도 재정의하라
        • item 12) 항상 toString을 재정의할 것
        • item 13) clone 재정의는 주의해서 진행하라
        • item 14) Comparable 구현을 고려하라
      • 4장: 클래스와 인터페이스
        • item 15) 클래스와 멤버의 접근 권한을 최소화하라
        • item 16) public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
        • item 17) 변경 가능성을 최소화하라
        • item 18) 상속보다는 컴포지션을 사용하라
        • item 19) 상속을 고려해 설계하고 문서화하고, 그러지 않았다면 상속을 금지하라
        • item 20) 추상 클래스보다는 인터페이스를 우선하라
        • item 21) 인터페이스는 구현하는 쪽을 생각해 설계하라
        • item 22) 인터페이스는 타입을 정의하는 용도로만 사용하라
        • item 23) 태그 달린 클래스보다는 클래스 계층구조를 활용하라
        • item 24) 멤버 클래스는 되도록 static으로 만들라
        • item 25) 톱레벨 클래스는 한 파일에 하나만 담으라
      • 5장: 제네릭
        • item 26) 로 타입은 사용하지 말 것
        • item 27) unchecked 경고를 제거하라
        • item 28) 배열보다 리스트를 사용하라
        • item 29) 이왕이면 제네릭 타입으로 만들라
        • item 30) 이왕이면 제네릭 메서드로 만들라
        • item 31) 한정적 와일드카드를 사용해 API 유연성을 높이라
        • item 32) 제네릭과 가변 인수를 함께 사용
        • item 33) 타입 안전 이종 컨테이너를 고려하라
      • 6장: 열거 타입과 어노테이션
        • item 34) int 상수 대신 열거 타입을 사용하라
        • item 35) ordinal 메서드 대신 인스턴스 필드를 사용하라
        • item 36) 비트 필드 대신 EnumSet을 사용하라
        • item 37) ordinal 인덱싱 대신 EnumMap을 사용하라
        • item 38) 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
        • item 39) 명명 패턴보다 어노테이션을 사용하라
        • item 40) @Override 어노테이션을 일관되게 사용하라
        • item 41) 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
      • 7장: 람다와 스트림
        • item 42) 익명 클래스보다는 람다를 사용하라
        • item 43) 람다보다는 메서드 참조를 사용하라
        • item 44) 표준 함수형 인터페이스를 사용하라
        • item 45) 스트림은 주의해서 사용하라
        • item 46) 스트림에서는 부작용 없는 함수를 사용하라
        • item 47) 반환 타입으로는 스트림보다 컬렉션이 낫다
        • item 48) 스트림 병렬화는 주의해서 적용하라
      • 8장: 메서드
        • item 49) 매개변수가 유효한지 검사하라
        • item 50) 적시에 방어적 복사본을 만들라
        • item 51) 메서드 시그니처를 신중히 설계하라
        • item 52) 다중정의는 신중히 사용하라
        • item 53) 가변인수는 신중히 사용하라
        • item 54) null이 아닌, 빈 컬렉션이나 배열을 반환하라
        • item 55) 옵셔널 반환은 신중히 하라
        • item 56) 공개된 API 요소에는 항상 문서화 주석을 작성하라
      • 9장: 일반적인 프로그래밍 원칙
        • item 57) 지역 변수의 범위를 최소화하라
        • item 58) 전통적인 for문보다 for-each문을 사용하기
        • item 59) 라이브러리를 익히고 사용하라
        • item 60) 정확한 답이 필요하다면 float, double은 피하라
        • item 61) 박싱된 기본타입보단 기본 타입을 사용하라
        • item 62) 다른 타입이 적절하다면 문자열 사용을 피하라
        • item 63) 문자열 연결은 느리니 주의하라
        • item 64) 객체는 인터페이스를 사용해 참조하라
        • item 65) 리플렉션보단 인터페이스를 사용
        • item 66) 네이티브 메서드는 신중히 사용하라
        • item 67) 최적화는 신중히 하라
        • item 68) 일반적으로 통용되는 명명 규칙을 따르라
      • 10장: 예외
        • item 69) 예외는 진짜 예외 상황에만 사용하라
        • item 70) 복구할 수 있는 상황에서는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라
        • item 71) 필요 없는 검사 예외 사용은 피하라
        • item 72) 표준 예외를 사용하라
        • item 73) 추상화 수준에 맞는 예외를 던지라
        • item 74) 메서드가 던지는 모든 예외를 문서화하라
        • item 75) 예외의 상세 메시지에 실패 관련 정보를 담으라
        • item 76) 가능한 한 실패 원자적으로 만들라
        • item 77) 예외를 무시하지 말라
      • 11장: 동시성
        • item 78) 공유 중인 가변 데이터는 동기화해 사용하라
        • item 79) 과도한 동기화는 피하라
        • item 80) 스레드보다는 실행자, 태스크, 스트림을 애용하라
        • item 81) wait와 notify보다는 동시성 유틸리티를 애용하라
        • item 82) 스레드 안전성 수준을 문서화하라
        • item 83) 지연 초기화는 신중히 사용하라
        • item 84) 프로그램의 동작을 스레드 스케줄러에 기대지 말라
      • 12장: 직렬화
        • item 85) 자바 직렬화의 대안을 찾으라
        • item 86) Serializable을 구현할지는 신중히 결정하라
        • item 87) 커스텀 직렬화 형태를 고려해보라
        • item 88) readObject 메서드는 방어적으로 작성하라
        • item 89) 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라
        • item 90) 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
    • 모던 자바 인 액션
      • 1장: 자바의 역사
      • 2장: 동작 파라미터화
      • 3장: 람다
      • 4장: 스트림
      • 5장: 스트림 활용
      • 6장: 스트림으로 데이터 수집
      • 7장: 병렬 데이터 처리와 성능
      • 8장: 컬렉션 API 개선
      • 9장: 람다를 이용한 리팩토링, 테스팅, 디버깅
      • 10장: 람다를 이용한 DSL
      • 11장: null 대신 Optional
      • 12장: 날짜와 시간 API
      • 13장: 디폴트 메서드
      • 14장: 자바 모듈 시스템
      • 15장: CompletableFuture와 Reactive 개요
      • 16장: CompletableFuture
      • 17장: 리액티브 프로그래밍
      • 18장: 함수형 프로그래밍
      • 19장: 함수형 프로그래밍 기법
      • 20장: 스칼라 언어 살펴보기
    • 자바의 이모저모
      • Javax
      • Objects
      • NIO
      • Thread
      • Concurrent
        • Atomic
        • Executor, ExecutorService
        • Interrupt
      • Assertions
    • Netty
      • 네티 맛보기
      • 네티의 주요 특징
      • 채널 파이프라인
      • 이벤트 루프
      • 바이트 버퍼
      • 부트스트랩
      • 네티 테스트
      • 코덱
      • 다양한 ChannelHandler와 코덱
      • 웹소켓
      • UDP 브로드캐스팅
    • 자바 병렬 프로그래밍
      • 2장: 스레드 안전성
      • 15장: 단일 연산 변수와 논블로킹 동기화
  • 🏖️코틀린
    • 코틀린 인 액션
      • 코틀린 언어의 특징
      • 코틀린 기초
      • 함수 정의와 호출
      • 클래스, 객체, 인터페이스
      • 람다
      • 타입 시스템
      • 연산자 오버로딩과 기타 관례
      • 고차 함수
      • 제네릭스
      • 어노테이션과 리플렉션
      • DSL 만들기
  • 🌸스프링
    • Spring Core
      • Cron Expression
      • Bean
        • Lifecycle
        • Aware
    • Spring MVC
    • Spring Security
      • 로그인 처리
      • 로그아웃 처리
      • JWT 인증 방식
      • 메소드별 인가 처리
    • Spring Data
      • Pageable
      • Spring Data Couchbase
      • Spring Data Redis
        • Serializer
    • Spring REST Docs
    • Spring Annotations
    • Spring Cloud
      • Service Discovery
      • API Gateway
      • Spring Cloud Config
      • MicroService Communication
      • Data Synchronization
    • Test
      • 테스트 용어 정리
      • JUnit
      • Spring Boot Test
      • Mockito
    • QueryDSL
      • 프로젝트 환경설정
      • 기본 문법
      • 중급 문법
      • 순수 JPA와 QueryDSL
      • 스프링 데이터 JPA와 QueryDSL
    • Lombok
      • @Data
      • @Builder
      • Log Annotations
  • 🕋DB
    • MySQL
      • CentOS7에서 MySQL 8 버전 설치하기
    • MongoDB
      • 
    • Redis
      • Sentinel
      • Cluster
      • Transaction
      • 자료구조
        • String
        • List
        • Set
        • Hash
        • Bitmaps
        • SortedSet
      • Lettuce 단일 서버, 클러스터 서버, 풀링 사용 방법
  • 📽️인프라
    • 리눅스
      • 주요 명령어 모음
    • Docker
      • Docker
      • Docker Compose
      • Docker Swarm
      • Docker Network
      • Linux에서 root 아닌 유저로 docker 실행하기
    • Kubernetes
      • 기초 개념
      • Pod
      • Configuration
      • ReplicationSet
      • Network
      • ConfigMap & Secret
      • Volume, Mount, Claim
      • Controller
      • Multi Container Pod
      • StatefulSet & Job
      • Rollout & Rollback
      • Helm
      • 개발 워크플로우와 CI/CD
      • Container Probes
      • Resource Limit
      • Logging & Monitoring
      • Ingress
      • Security
      • Multi Node/Architecture Cluster
      • Workload & Pod management
      • CRD & Operator
      • Serverless Function
      • K8S Cheat Sheet
    • Kafka
      • 카프카 개요
      • 카프카 설치 및 실습
      • Kafka Broker
      • Topic, Partition, Record
      • Producer
      • Consumer
      • Kafka Streams
      • Kafka Connect
      • MirrorMaker
  • AWS
    • AWS Console / CLI / SDK
    • IAM
    • EC2
      • EC2 Advanced
    • ELB / ASG
    • RDS / Aurora / ElastiCache
    • DynamoDB
    • DocumentDB / Neptune / Keyspaces / QLDB / Timestream
    • Route 53
    • Beanstalk
    • Solution Architect
    • S3
      • 보안
    • CloudFront
    • Global Accelerator
    • AWS Storage
    • Messaging
    • Container
    • Serverless
    • Data Analysis
    • Machine Learning
    • Monitoring
    • Security
    • VPC
    • Data Migration
    • 기타 서비스
  • 🏔️CS
    • 운영 체제
      • Introduction
      • System Structures
      • Process
      • Synchronization
      • Muitithreaded Programming
      • Process Scheduling
      • Memory Management
      • Virtual Memory
    • 네트워크
      • 네트워크 기초
      • 네트워크 통신 방식
      • OSI 7계층
        • 1계층: 물리계층
        • 2계층: 데이터 링크 계층
        • 3계층: 네트워크 계층
        • 4계층: 전송 계층
        • 5계층: 세션 계층
        • 6계층: 표현 계층
        • 7계층: 응용 계층
      • TCP/IP 스택
      • ARP
      • 데이터 크기 조절
      • WDM
      • NAT
      • DNS
      • DHCP
      • VPN
      • 네이글 알고리즘
      • 서버 네트워크
      • 네트워크 보안
        • 보안의 기본
        • 보안 장비
      • 이중화
    • 데이터베이스
      • 트랜잭션
    • 컴퓨터 구조
      • 개요
      • Instruction Set Architecture
      • Procedure Call & Return
      • Linking
      • Pipeline
      • Memory Hierarchy
      • Virtual Memory
      • Interrupt / Exception, IO
    • 자료 구조
      • Array
      • List
      • Map
      • Set
      • Queue
      • PriorityQueue
      • Stack
    • 웹 기술
      • HTTP
        • 쿠키와 세션
  • 🪂Big Data
    • Apache Hadoop
  • 🕹️ETC
    • Git
      • 내부 구조
      • 내가 자주 사용하는 명령어 모음
      • Commit Convention
    • 이력서 작성하기
    • Embedded
      • 라즈베리파이에서 네오픽셀 적용기
    • 기술블로그 모음집
Powered by GitBook
On this page
  • 람다
  • 람다의 구성
  • 스타일
  • 람다 사용하기
  • 실행 어라운드 패턴
  • 함수형 인터페이스
  • 개념
  • 함수형 인터페이스 종류
  • 함수 디스크립터
  • @FunctionalInterface
  • 예외 처리
  • 형식 검사, 추론, 제약
  • 형식 검사
  • 형식 추론
  • 제약
  • 메서드 참조
  • 유형
  • 마무리
  • 람다 표현식 조합을 위한 유용한 메서드
  • Comparator
  • Predicate
  • Function
  1. 자바
  2. 모던 자바 인 액션

3장: 람다

익명클래스 대신 람다로 간결한 코드를 작성하자.

Previous2장: 동작 파라미터화Next4장: 스트림

Last updated 10 months ago

람다

  • 메서드로 전달할 수 있는 익명 함수를 단순화한 것

  • 특징

    • 익명: 일반적인 메서드와 달리 이름이 없어, 구현해야 할 코드에 대한 걱정거리가 줄어든다.

    • 함수: 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 발생 가능한 예외 리스트를 포함한다.

    • 전달: 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.

    • 간결성: 익명 클래스처럼 불필요한 선언 등의 코드를 작성할 필요가 없다.

  • 익명 클래스에서 구현했던 메서드의 바디만 직접 전달하는 것과 비슷하다.

람다의 구성

  • 파라미터 리스트

    • 람다 바디에서 사용되는 파라미터를 입력받는다.

  • 화살표(→)

    • 화살표는 람다의 파라미터 리스트와 바디를 구분한다.

  • 람다 바디

    • 람다의 반환값에 해당하는 표현식이다.

스타일

  • 일반 표현식 스타일

    • (parameters) -> expression

    • 람다 표현식에는 return이 함축되어 있으므로 return을 명시적으로 사용하지 않아도 된다.

    • (String s) -> "Iron Man" 형태는 파라미터를 입력받아 "Iron Man"을 return하는 람다 식이다.

  • 블록 스타일

    • (parameters) -> { statements; }

    • 블록에는 여러 행의 문장을 포함하는 구문이 들어가며, 리턴 타입이 void가 아니라면 return을 명시적으로 사용해야한다.

    • () -> {} 형태는 파라미터가 없는 void 람다 식이다.

    • (Integer i) -> {return "Alan" + i;} 형태는 파라미터를 입력받아 특정 문자열을 반환하는 람다 식이다.

람다 사용하기

실행 어라운드 패턴

  • DB 등의 자원을 처리할 때 자원을 초기화/준비 -> 작업 처리 -> 자원을 정리/마무리 하는 순서로 이뤄진다.

  • 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 실행 어라운드 패턴이라고 부른다.

  • 아래는 try-with-resources 구문을 사용해 파일에서 한 행을 읽는 코드이다.

public String processFile() throws IOException {
	try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
		return br.readLine(); // 실제 필요한 작업 수행
	}
}
  • 이제 함수형 인터페이스를 이용해 동작을 전달해보자. 정의한 BufferedReaderProcessor 인터페이스를 processFile 메서드의 인수로 전달할 수 있다.

@FuntionalInterface
public interface BufferedReaderProcessor {
	String process(BuffredReader b) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
	try (BuffredReader br = new BuffredReader(new FileReader("data.txt"))) {
		return p.process(br);
	}
}
  • 다양한 동작들을 람다식 형태로 processFile 메서드에 전달할 수 있다.

// 한 행을 처리하는 코드
String oneLine = processFile((BufferedReader br) -> br.readLine());

// 두 행을 처리하는 코드
Strine twoLines = processFile((BuffredReader br) -> br.readLine() + br.readLine());

함수형 인터페이스

개념

  • 정확히 하나의 추상 메서드를 지정하는 인터페이스

  • 여러 디폴트 메서드가 있어도 추상 메서드가 하나뿐이면 함수형 인터페이스다.

  • 다음은 함수형 인터페이스의 예시이다.

public interface Comparator<T> {
    int compare(T o1, T o2);
}

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call() throws Exception;
}
  • 람다 표현식으로 이러한 함수형 인터페이스의 추상 메서드를 구현해 직접 전달할 수 있다.

// 람다로 Runnable의 구현체 생성 가능
Runnable r1 = () -> System.out.println("Hello world 1");

// 익명 클래스 사용해 구현체 생성
Runnable r2 = new Runnable() {
	public void run() {
		System.out.println("Hello World 2");
	}
}

public static void process(Runnable r) {
	r.run();
}
process(r1);
process(r2);
// Runnable 구현체를 미리 생성해두지 않고 바로 만들어 사용 가능
process(() -> System.out.println("Hello Wordl 3"));

함수형 인터페이스 종류

Predicate<T>

  • test라는 추상 메서드를 정의한다.

  • test 메서드는 제네릭 형식 T의 객체를 인수로 받아 boolean 값을 반환한다.

  • 다음 코드는 Predicate 인터페이스를 구현해 String 객체가 Empty가 아닌 경우를 필터링한다.

public <T> List<T> filter(List<T> list, Predicate<T> p) {
	List<T> results = new ArrayList<>();
	for(T t: list) {
		if(p.test(t)) {
			results.add(t);
		}
	}
	return results;
}

// T 객체를 입력받아 boolean을 반환하도록 한다.
Predicate<String> nonEmptyStringPredicate = (String) -> !s.isEmpty();

// String 리스트 중 Empty가 아닌 String만 필터링한다.
List<String> nonEmpty= filter(listOfStrings, nonEmptyStringPredicate);

Consumer<T>

  • 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다.

  • T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.

  • 다음은 Consumer와 람다를 이용해서 리스트의 모든 항목을 출력하는 예제다.

public <T> void forEach(List<T> list, Consumer<T> c) {
	for (T t: list) {
		c.accept(t);
	}
}

forEach(
	Arrays.asList(1, 2, 3, 4, 5),
	(Integer i) -> System.out.println(i) // Consumer 인터페이스의 accept 메서드를 람다로 구현
);

Function<T, R>

  • 제네릭 형식 T 객체를 인수로 받아 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의한다.

  • 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.

    • ex) 사과를 입력받아 사과의 무게 정보만 추출, 문자열을 입력받아 문자열의 길이만 추출

  • 다음 코드는 String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer 리스트로 반환한다.

public <T, R> List<R> map(List<T> list, Function<T, R> f) {
	List<R> result = new ArrayList<>();
	for (T t : list) {
		result.add(f.apply(t));
	}
	return result;
}

List<Integer> l = map(
	Arrays.asList("lambdas", "in", "action"),
	(String s) -> s.length()
);

primitive type을 위한 함수형 인터페이스

  • 제네릭 파라미터에는 reference type(참조형)만 입력받을 수 있다.

  • 따라서 기본형을 입출력으로 사용할 때 자동으로 박싱 타입으로 변환(오토박싱)하는 과정을 피할 수 있도록 각 primitive type을 위한 함수형 인터페이스를 제공한다.

  • 특정 형식을 입력받는 함수형 인터페이스의 경우 IntPredicate, DoublePredicate, LongBinaryOperator , IntFunction 처럼 타입명이 함께 붙는다.

Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
addNumbers.test(1000);  // int -> Integer로 박싱 과정이 발생

IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000); // int -> int 그대로 사용하므로 박싱 과정이 발생 x

다양한 함수형 인터페이스 정리

함수 디스크립터

  • 함수 디스트립터(function descriptor): 람다 표현식의 시그니처를 서술하는 메서드

  • 함수형 인터페이스의 추상 메서드 시그니처(입력 인자, 반환값 등 메서드의 특성)는 람다 표현식의 시그니처와 동일해야 한다.

  • 아래와 같이 함수형 인터페이스의 추상 메서드 시그니처와 람다의 시그니처가 다르다면 유효하지 않은 람다식이 된다. (Predicate는 boolean을 반환해야 하는데, 람다 식에서는 int를 반환하고 있어 메서드 시그니처 불일치!)

Predicate<Apple> p = (Apple a) -> a.getWeight();

@FunctionalInterface

  • 함수형 인터페이스임을 가리키는 어노테이션

  • 함수형 인터페이스가 아니라면 컴파일 에러가 발생한다.

예외 처리

  • 아래와 같이 Checked Exception인 IOException을 명시적으로 선언하는 함수형 인터페이스를 만들었다면, 예외를 던질 수 있는 람다를 사용해도 된다.

@FunctionalInterface
public interface BufferedReaderProcessor {
	String process(BufferedReader b) throws IOException;
}

BufferedReaderProcessor p = (BufferedReader br) -> br.readline();
  • 하지만 자바에서 제공하는 함수형 인터페이스의 경우 Checked Exception을 던지는 동작을 허용하지 않는다.

  • 따라서 아래와 같이 람다식 내부에서 try/catch 블록으로 Checked Exception을 처리해주어야 한다.

Function<BufferedReader, String> f = (BufferedReader b) -> { 
    try { 
        return b.readLine();
    } 
    catch (IOException e) {
        throw new RuntimeException(e);
    }
};

형식 검사, 추론, 제약

  • 람다 표현식 자체에 어떤 함수형 인터페이스를 구현하는지 정보가 포함되어 있지 않으므로 람다의 형식을 파악해야 한다. 람다의 형식을 파악하기 위해 다음 세 가지의 과정을 거친다.

형식 검사

  • 람다가 사용되는 콘텍스트(context)를 이용해서 람다의 형식을 추론할 수 있다.

  • 어떤 콘텍스트(ex. 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수 등)에서 기대되는 람다 표현의 형식을 대상 형식(target type)이라고 부른다.

List<Apple> heavierThan150g = 
	filter(inventory, (Apple apple) -> apple.getWeight() > 150);
  • 형식 검사 과정은 다음과 같다.

  1. 람다가 사용된 콘텍스트를 파악하기 위해 람다를 입력 인자로 받는 메서드의 선언을 먼저 확인한다.

    메서드는 파라미터로 Predicate<Apple> 형식(대상 형식)을 기대하며, Predicate 함수형 인터페이스는 test라는 한 개의 추상 메서드를 정의한다.

  2. test 메서드의 시그니처를 통해 Apple 객체를 입력받아 boolean을 반환하는 함수 디스크립터를 확인할 수 있다.

  3. 함수 디스크립터와 람다의 시그니처(Apple 객체를 입력받아 boolean을 반환)를 비교하여 일치하면 형식 검사를 완료한다.

  • 하나의 람다 표현식은 다양한 함수형 인터페이스에 할당될 수 있다.

  • 대상 형식이 다르더라도 람다 표현식과 호환되는 추상 메서드를 가진다면 아래처럼 정상적으로 할당된다.

Callable<Integer> c = () -> 42;
PriviligedAction<Integer> p = () -> 42;

예를들어 list.add(s) 메서드는 boolean을 반환하지만, 다음과 같이 작성하면 유효하게 할당되며 반환되는 값은 무시된다.

Consumer<String> cs = s -> list.add(s);

아래처럼 같은 함수형 디스크립터를 가진 두 함수형 인터페이스를 갖는 메서드를 오버로딩할 때 명시적으로 함수형 인터페이스로 캐스팅하여 분명하게 할 수 있다.

public void execute(Runnable runnable) {

runnable.run();

}

public void execute(Action<T> action) {

action.act();

}

execute((Action) () -> {});

형식 추론

  • 자바 컴파일러는 람다 표현식이 사용된 콘텍스트(대상 형식)를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.

  • 즉, 대상 형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있다.

  • 결과적으로 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다.

  • 자바 컴파일러는 다음처럼 람다 파라미터 형식을 추론할 수 있다.

// 형식을 지정해주어 형식 추론이 필요 없음
List<Apple> greenApples =
	filter(inventory, Apple a -> GREEN.equals(a.getColor()));

// 형식을 지정하지 않아 형식 추론이 필요함
List<Apple> greenApples =
	filter(inventory, a -> GREEN.equals(a.getColor()));

제약

  • 람다 캡처링(capturing lambda): 익명 함수처럼 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 자유 변수(free variable)를 활용할 수 있다

  • 람다는 인스턴스 변수와 정적 변수 모두 자유롭게 사용할 수 있지만, 한번만 할당할 수 있도록 final로 선언되거나 final 변수와 동일한 상태여야만 한다.

  • 그렇지 않다면 Variable used in lambda expression should be final or effectively final 컴파일 오류가 발생한다.

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
  • 이러한 제약이 있는 이유는 람다가 스레드에서 실행될 때, 변수를 할당한 스레드가 사라져 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있어 복사본의 값이 바뀌지 않도록 해야 하기 때문이다.

  • 람다에 값을 복사해서 사용하는 이유는 인스턴스 변수는 힙에 저장되고 지역 변수는 스택에 위치하는데, 스택은 스레드마다 고유한 영역이므로 다른 스레드에서 접근할 수 없다. 따라서 다른 스레드의 람다 표현식에서도 해당 값에 접근할 수 있도록 복사해서 사용한다.

클로저

함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킨다.

예를 들어 클로저를 다른 함수의 인수로 전달할 수 있다. 클로저 외부에 정의된 변수의 값에 접근하고, 값을 바꿀 수 있다.

자바8의 람다와 익명 클래스는 클로저와 비슷한 동작을 수행하지만, 람다와 익명 클래스는 람다가 정의된 메서드의 지역 변수의 값은 바꿀 수 없다. 람다가 정의된 메서드의 지역 변수 값은 final 변수여야 한다.

지역 변수 값은 스택에 존재하므로 자신을 정의한 스레드와 생존을 같이 해야하며, 따라서 지역 변수는 final이어야 한다.

가변 지역 변수를 새로운 스레드에서 캡처할 수 있다면 안전하지 않은 동작을 수행할 가능성이 생긴다.

메서드 참조

  • 특정 메서드만을 호출하는 람다를 축약해서 표현하는 방식

  • 메서드명 앞에 구분자 '::' 를 붙여 메서드 참조를 활용 가능하다.

  • 기존의 메서드 정의를 재활용해 람다처럼 전달 가능하다.

  • 메서드 참조와 주어진 함수형 인터페이스의 디스크립터와 호환되어야 한다.

  • 컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷하게 메서드 참조가 주어진 함수형 인터페이스와 호환되는지 확인한다.

inventory.sort((Apple a1, Apple a2) 
	-> a1.getWeight().compareTo(a2.getWeight()));

// 메서드 참조와 java.util.Comparator.comparing을 활용한 코드
inventory.sort(comparing(Apple::getWeight));

유형

  1. 정적 메서드 참조

    • 클래스이름::정적메서드이름 형태로 참조할 수 있다.

    • Intege의 정적 메서드 parseInt()를 람다로 표현하면 Integer.parseInt()

    • 메서드 참조로 표현하면 Integer::parseInt

  2. 다양한 형식의 인스턴스 메서드 참조

    • 클래스이름::인스턴스메서드이름 형태로 참조할 수 있다.

    • String의 length()를 람다로 표현하면 (String s) -> s.length()

    • 메서드 참조로 표현하면 String::length

  3. 기존 객체의 인스턴스 메서드 참조

    • 객체이름::인스턴스메서드이름 형태로 참조할 수 있다.

    • expensiveTransaction.getValue()를 람다로 표현하면 () -> expensiveTransaction.getValue()

    • 메서드 참조로 표현하면 expensiveTransaction::getValue

    • 공통적으로 반복되는 작업을 수행하도록 만든 비공개 헬퍼 메서드를 유용하게 사용할 수 있다.

// 비공개 헬퍼 메서드 예시
private boolean isValidName(String string) {
	return Character.isUpperCase(string.charAt(0));
}

filter(list, this::isValidName); // filter(list, (String s) -> isValidName(s)); 와 같은 의미

생성자 참조

  • new 키워드를 사용해 생성자의 참조를 만들 수 있다.

  • 인수가 없는 생성자의 경우 Supplier 함수형 인터페이스의 시그니쳐와 같다.

Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
  • 인수가 하나인 생성자의 경우 Function 함수형 인터페이스의 시그니처와 같다.

Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);
  • 인수가 두개인 생성자의 경우 BiFunction 함수형 인터페이스의 시그니처와 같다.

BiFunction<Color, Integer, Apple> c3 = Apple::new;
Apple a3 = c3.apply(GREEN, 110);
  • 인수를 세 개 이상 받고 싶은 경우, 직접 시그니처를 갖는 함수형 인터페이스를 만들어 사용해야 한다.

public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}

TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;
  • 다음과 같이 giveMeFruit 메서드를 호출할 때 Fruit의 이름과 무게를 넣어 실제 객체가 생성되도록 만들 수도 있다.

static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
	map.put("apple", Apple::new);
	map.put("orange", Orange::new);
}

public static Fruit giveMeFruit(String fruit, Integer weight) {
	return map.get(fruit.toLowerCase())
            .apply(weight);
}

마무리

  • 함수형 인터페이스 구현체로 시작해 람다, 메서드 참조를 활용하는 것까지의 진화과정을 살펴보자.

  • 함수형 인터페이스를 직접 구현한 클래스를 넘긴다.

public class AppleComparator implements Comparator<Apple> {
	public int compare(Apple a1, Apple a2) {
		return a1.getWeight().compareTo(a2.getWeight());
	}
}

inventory.sort(new AppleComparator());
  • 익명 클래스를 넘긴다.

inventory.sort(new Comparator<Apple>() {
	public int compare(Apple a1, Apple a2) {
		return a1.getWeight().compareTo(a2.getWeight());
	}
});
  • 람다 표현식을 넘긴다.

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
  • 정적 메서드인 comparing()을 사용해 간결화한다.

import static java.util.Comparator.comparing;
inventory.sort(comparing(apple -> apple.getWeight()));
  • 정적 메서드를 메서드 참조하여 간결화한다.

inventory.sort(comparing(Apple::getWeight));

람다 표현식 조합을 위한 유용한 메서드

  • 몇몇 함수형 인터페이스는 람다 표현식 조합을 위해 유용한 메서드들을 제공한다.

  • default method를 사용해 제공하여 함수형 인터페이스의 정의를 벗어나지 않는다.

  • 단순 람다 표현식을 조합해 복잡한 람다 표현식을 만들 수 있으며, 가독성도 해치지 않는다.

Comparator

reversed()

  • 주어진 비교자의 순서를 뒤바꾸는 default 메서드를 제공하여 역정렬할 수 있다.

default Comparator<T> reversed() {
	return Collections.reverseOrder(this);
}

inventory.sort(comparing(Apple::getWegiht).reversed());

thenComparing()

  • Comparator를 여러 개 나열해서 처음 비교한 값이 같다면 다음 비교자 기준으로 비교하도록 할 수 있다.

default Comparator<T> thenComparing(Comparator<? super T> other) {
    Objects.requireNonNull(other);
    return (Comparator<T> & Serializable) (c1, c2) -> {
        int res = compare(c1, c2);
        return (res != 0) ? res : other.compare(c1, c2);
    };
}

inventory.sort(comparing(Apple::getWeight)
                    .reversed()
    		    .thenComparing(Apple::getCountry));

Predicate

negate()

  • 특정 Predicate를 반전시키는 메서드이다.

  • 아래처럼 사과 색이 빨강이면 true를 반환하는 람다를 negate 메서드를 통해 반전시킬 수 있다.

Predicate<Apple> notRedApple = redApple.negate();

and(), or()

  • and 메서드와 or 메서드를 이용해 "빨갛고 150g 이상인 사과 또는 녹색 사과일 때 true를 반환"하도록 할 수 있다.

Predicate<Apple> redAndHeavyApple = 
		redApple.and(apple -> apple.getWeight() > 150)
			.or(apple -> GREEN.equals(a.getColor()));

Function

andThen()

  • 주어진 함수 적용 결과를 다른 함수의 입력으로 전달한다.

  • 다음 예제의 경우 f 함수 먼저 수행한 후 g 함수를 수행하게 된다.

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); // h(x) = g(f(x))
int result = h.apply(1) // result = 4

compose()

  • 입력 인자로 전달된 함수를 먼저 실행한 후 결과를 외부의 함수에 전달한다.

  • 아래 예제의 경우 g 함수를 먼저 수행한 후 f 함수를 수행한다.

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g); // h(x) = f(g(x))
int result = h.apply(1) // result = 3

를 입력인자로 받는 경우 람다를 사용할 수 있다.

람다의 바디에 이 있다면 void를 반환하지 않더라도 void 반환하는 함수 디스크립터와 호환된다.

🏝️
함수형 인터페이스
일반 표현식
https://www.geeksforgeeks.org/java-lambda-expressions-parameters/
https://javaconceptoftheday.com/java-8-functional-interfaces/