1장 안녕하세요, 람다 표현식입니다!
섹션 1
Java의 코딩 스타일은 엄청난 변화에 직면해 있습니다.
우리의 일상 업무는 더욱 단순해지고, 편리해지고, 표현력이 더욱 풍부해질 것입니다. 새로운 프로그래밍 방법인 Java는 수십 년 전에 다른 프로그래밍 언어에도 등장했습니다. 이러한 새로운 기능이 Java에 도입된 후에는 더 간결하고 우아하며 표현력이 뛰어나고 오류가 적은 코드를 작성할 수 있습니다. 더 적은 코드로 다양한 전략과 디자인 패턴을 구현할 수 있습니다.
이 책에서는 일상적인 프로그래밍의 예를 통해 함수형 프로그래밍을 살펴보겠습니다. 이 새롭고 우아한 디자인 및 코딩 방법을 사용하기 전에 먼저 이 방법의 장점을 살펴보겠습니다.
당신이 생각하는 방식을 변경
명령형 스타일 - 이는 Java 언어가 처음부터 제공되어 온 접근 방식입니다. 이 스타일을 사용하면 Java에게 각 단계에서 수행할 작업을 지시한 다음 실제로 단계별로 실행하는 모습을 지켜봐야 합니다. 물론 이것은 좋지만 약간 초보적인 것 같습니다. 코드가 약간 장황해 보이기 때문에 언어가 좀 더 똑똑해졌으면 좋겠습니다. 어떻게 해야 하는지 알려주는 대신 원하는 것을 알려주기만 하면 됩니다. 다행스럽게도 Java는 마침내 우리가 이러한 소망을 실현하는 데 도움을 줄 수 있습니다. 이 스타일의 장점과 차이점을 이해하기 위해 몇 가지 예를 살펴보겠습니다.
정상적인 방법
두 가지 친숙한 예부터 시작해 보겠습니다. 이는 시카고가 지정된 도시 컬렉션에 있는지 확인하는 명령 방법입니다. 이 책에 나열된 코드는 부분적인 조각일 뿐이라는 점을 기억하세요.
다음과 같이 코드 코드를 복사합니다.
부울 발견 = false;
for(문자열 도시 : 도시) {
if(city.equals("시카고")) {
발견 = 사실;
부서지다;
}
}
System.out.println("시카고를 찾았나요?:" + 발견);
이 명령형 버전은 여러 실행 부분으로 나누어져 있어 약간 장황하고 초보적인 것처럼 보입니다. 먼저, 발견이라는 부울 태그를 초기화한 다음 컬렉션의 각 요소를 순회합니다. 찾고 있는 도시가 발견되면 이 태그를 설정한 다음 루프에서 빠져나와 마지막으로 검색 결과를 인쇄합니다.
더 나은 방법
주의 깊은 Java 프로그래머는 이 코드를 읽고 나면 다음과 같이 보다 간결하고 명확한 방법을 빨리 생각해 낼 것입니다.
다음과 같이 코드 코드를 복사합니다.
System.out.println("시카고를 찾았나요?:" + city.contains("시카고"));
이는 또한 필수 작성 스타일이기도 합니다. 포함 메소드가 이를 직접 수행합니다.
실질적인 개선
이와 같은 코드를 작성하면 다음과 같은 몇 가지 장점이 있습니다.
1. 더 이상 변경 가능한 변수를 조작하지 마세요
2. 반복을 맨 아래 레이어로 캡슐화합니다.
3. 코드가 더 간단해졌습니다.
4. 코드가 더 명확해지고 집중됩니다.
5. 우회 횟수를 줄이고 코드와 비즈니스 요구 사항을 더욱 긴밀하게 통합하세요.
6. 오류 발생 가능성이 적습니다.
7. 이해하기 쉽고 유지 관리가 쉽습니다.
좀 더 복잡한 예를 들어보겠습니다.
이 예는 너무 간단합니다. 컬렉션에 요소가 존재하는지 여부를 쿼리하는 것은 Java의 모든 곳에서 볼 수 있습니다. 이제 파일 구문 분석, 데이터베이스와의 상호 작용, 웹 서비스 호출, 동시 프로그래밍 등과 같은 고급 작업을 수행하기 위해 명령형 프로그래밍을 사용한다고 가정합니다. 이제 우리는 이 단순한 시나리오뿐만 아니라 Java를 사용하여 더욱 간결하고 우아하며 오류 없는 코드를 작성할 수 있습니다.
옛날 방식
또 다른 예를 살펴보겠습니다. 우리는 가격 범위를 정의하고 다양한 방법으로 할인된 총 가격을 계산합니다.
다음과 같이 코드 코드를 복사합니다.
최종 목록<BigDecimal> 가격 = Arrays.asList(
new BigDecimal("10"), new BigDecimal("30"), new BigDecimal("17"),
new BigDecimal("20"), new BigDecimal("15"), new BigDecimal("18"),
new BigDecimal("45"), new BigDecimal("12"));
20위안을 넘으면 10% 할인이 된다고 가정하고 먼저 일반적인 방법으로 구현해보겠습니다.
다음과 같이 코드 코드를 복사합니다.
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;
for(BigDecimal 가격 : 가격) {
if(price.compareTo(BigDecimal.valueOf(20)) > 0)
totalOfDiscountedPrices =
totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("할인된 가격의 총액: " + totalOfDiscountedPrices);
이 코드는 매우 익숙할 것입니다. 먼저 변수를 사용하여 총 가격을 저장한 다음 모든 가격을 반복하여 20위안보다 큰 가격을 찾아 할인된 가격을 계산하고 총 가격에 추가합니다. 할인 후 가격입니다.
프로그램의 출력은 다음과 같습니다.
다음과 같이 코드 코드를 복사합니다.
할인된 총 가격: 67.5
결과는 완전히 정확하지만 코드가 약간 지저분합니다. 우리가 있는 방식대로만 글을 쓸 수 있는 것은 우리 잘못이 아닙니다. 그러나 이러한 코드는 기본 유형의 편집증을 겪을 뿐만 아니라 단일 책임 원칙에도 위배됩니다. 집에서 일하고 코더가 되고 싶은 자녀가 있는 경우, 코드를 숨겨야 합니다. 그들이 이를 보고 실망하며 “이 일로 돈을 버나요?”라고 말할 수 있습니다.
더 좋은 방법이 있습니다
우리는 더 잘할 수 있고, 훨씬 더 잘할 수 있습니다. 우리 코드는 요구 사항 사양과 약간 비슷합니다. 이를 통해 비즈니스 요구 사항과 구현된 코드 간의 격차를 줄여 요구 사항을 잘못 해석할 가능성을 줄일 수 있습니다.
더 이상 Java에서 변수를 생성하고 끝없이 할당하도록 두지 않습니다. 다음 코드와 같이 더 높은 수준의 추상화에서 해당 변수와 통신해야 합니다.
다음과 같이 코드 코드를 복사합니다.
최종 BigDecimal totalOfDiscountedPrices =
가격.스트림()
.filter(가격 -> 가격.compareTo(BigDecimal.valueOf(20)) > 0)
.map(가격 -> 가격.곱하기(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("할인된 가격의 총액: " + totalOfDiscountedPrices);
소리내어 읽어보세요. 20위안보다 큰 가격을 필터링하고 할인된 가격으로 변환한 다음 합산합니다. 이 코드는 요구 사항을 설명하는 데 사용한 프로세스와 정확히 동일합니다. 자바에서는 위와 같이 긴 코드 줄을 접어서 메소드 이름 앞의 마침표에 맞춰 한 줄씩 정렬하는 것도 매우 편리합니다.
코드는 매우 간단하지만 Java8에서는 많은 새로운 기능을 사용합니다. 먼저 가격표의 스트림 메소드를 호출합니다. 이는 나중에 논의할 수많은 편리한 반복기에 대한 문을 열어줍니다.
전체 목록을 직접 순회하는 대신 필터 및 맵과 같은 몇 가지 특수 메서드를 사용합니다. 이러한 메소드는 이전에 사용했던 JDK의 메소드와는 달리 익명 함수(람다 표현식)를 매개변수로 허용합니다. (이에 대해서는 나중에 자세히 논의하겠습니다). map() 메소드에서 반환된 가격의 합계를 계산하기 위해 Reduce() 메소드를 호출합니다.
포함 메소드와 마찬가지로 루프의 본문은 숨겨집니다. 그러나 맵 방법(및 필터 방법)은 훨씬 더 복잡합니다. 전달된 람다 식을 호출하여 가격표의 각 가격을 계산하고 그 결과를 새 컬렉션에 넣습니다. 마지막으로 이 새 컬렉션에 대해 축소 메서드를 호출하여 최종 결과를 얻습니다.
위 코드의 출력은 다음과 같습니다.
다음과 같이 코드 코드를 복사합니다.
할인된 총 가격: 67.5
개선이 필요한 부분
이는 이전 구현에 비해 크게 개선되었습니다.
1. 잘 구성되어 있지만 복잡하지 않습니다.
2. 낮은 수준의 작업이 없습니다.
3. 로직을 강화하거나 수정하기 쉽습니다.
4. 메소드 라이브러리에 의한 반복
5. 루프 본문의 지연 평가가 효율적입니다.
6. 쉽게 병렬화 가능
아래에서는 Java가 이를 구현하는 방법에 대해 설명합니다.
세상을 구하기 위해 람다 표현식이 등장했습니다
람다 표현식은 명령형 프로그래밍의 문제로부터 우리를 구해 주는 지름길입니다. Java가 제공하는 이 새로운 기능은 우리의 원래 프로그래밍 방법을 변경하여 우리가 작성하는 코드를 간결하고 우아하게 만들고 오류가 덜 발생할 뿐만 아니라 더 효율적이고 쉽게 최적화하고 개선하고 병렬화하도록 만들었습니다.
섹션 2: 함수형 프로그래밍의 가장 큰 이점
기능적 스타일 코드는 신호 대 잡음 비율이 더 높습니다. 작성되는 코드는 적지만 행이나 표현식당 더 많은 작업이 수행됩니다. 명령형 프로그래밍과 비교할 때 함수형 프로그래밍은 우리에게 많은 이점을 제공합니다.
종종 버그의 원인이 되고 코드 병렬화를 어렵게 만드는 명시적인 변수 수정이나 할당은 피합니다. 명령줄 프로그래밍에서는 루프 본문의 totalOfDiscountedPrices 변수에 지속적으로 값을 할당합니다. 기능적 스타일에서는 코드가 더 이상 명시적인 수정 작업을 거치지 않습니다. 수정되는 변수가 적을수록 코드에 포함되는 버그도 줄어듭니다.
기능적 스타일 코드는 쉽게 병렬화될 수 있습니다. 계산에 시간이 많이 걸리는 경우 목록 요소를 동시에 쉽게 실행할 수 있습니다. 명령형 코드를 병렬화하려면 totalOfDiscountedPrices 변수의 동시 수정으로 인해 발생하는 문제에 대해서도 걱정해야 합니다. 함수형 프로그래밍에서는 이 변수가 완전히 처리된 후에만 이 변수에 액세스하므로 스레드 안전 문제가 제거됩니다.
코드가 더 표현력이 좋습니다. 명령형 프로그래밍은 초기화 값 생성, 가격 반복, 변수에 할인 가격 추가 등 수행할 작업을 설명하기 위해 여러 단계로 나뉩니다. 반면 함수형 프로그래밍에서는 목록의 매핑 메서드가 할인을 포함한 값을 반환하도록 하면 됩니다. . 새 가격 목록을 만든 다음 누적하면 됩니다.
함수형 프로그래밍은 더 간단합니다. 명령형 프로그래밍보다 동일한 결과를 얻는 데 필요한 코드가 더 적습니다. 코드가 깔끔하다는 것은 작성해야 할 코드, 읽을 코드가 적고 유지 관리할 코드가 적다는 것을 의미합니다. "간결할수록 간결해야 할까요?"(7페이지)를 참조하세요.
기능적 코드는 더 직관적입니다. 코드를 읽는 것은 문제를 설명하는 것과 비슷하며, 구문에 익숙해지면 이해하기 쉽습니다. map 메소드는 컬렉션의 각 요소에 대해 지정된 함수(할인 가격 계산)를 실행한 후 아래 그림과 같이 결과 집합을 반환합니다.
그림 1 - 맵은 컬렉션의 각 요소에 대해 지정된 기능을 수행합니다.
람다 표현식을 사용하면 Java에서 함수형 프로그래밍의 강력한 기능을 최대한 활용할 수 있습니다. 함수형 스타일을 사용하면 표현력이 더 풍부하고 간결하며 할당량이 적고 오류가 적은 코드를 작성할 수 있습니다.
객체 지향 프로그래밍 지원은 Java의 주요 장점입니다. 함수형 프로그래밍과 객체지향 프로그래밍은 상호 배타적이지 않습니다. 스타일의 실제 변화는 명령줄 프로그래밍에서 선언적 프로그래밍으로 이루어졌습니다. Java 8에서는 기능 지향과 객체 지향이 효과적으로 통합될 수 있습니다. 우리는 계속해서 OOP 스타일을 사용하여 도메인 엔터티와 해당 상태 및 관계를 모델링할 수 있습니다. 또한 함수를 사용하여 동작이나 상태 전환, 워크플로 및 데이터 처리를 모델링하고 복합 함수를 생성할 수도 있습니다.
섹션 3: 기능적 스타일을 사용하는 이유는 무엇입니까?
우리는 함수형 프로그래밍의 장점을 살펴봤습니다. 하지만 이 새로운 스타일을 사용할 가치가 있을까요? 이것은 단지 작은 개선인가, 아니면 완전한 변화인가? 이에 대해 실제로 시간을 보내기 전에 답변해야 할 실질적인 질문이 여전히 많이 있습니다.
다음과 같이 코드 코드를 복사합니다.
샤오밍이 물었다.
코드가 적다는 것은 단순함을 의미합니까?
단순성은 덜 복잡하지만 최종적으로 의도를 효과적으로 표현할 수 있음을 의미합니다. 이점은 광범위합니다.
코드를 작성하는 것은 재료를 모으는 것과 같습니다. 단순함은 재료를 양념에 섞을 수 있다는 것을 의미합니다. 간결한 코드를 작성하려면 노력이 필요합니다. 읽어야 할 코드가 적고 정말 유용한 코드가 투명하게 드러납니다. 이해하기 어렵거나 세부사항을 숨기는 숏코드는 간결하기보다는 짧습니다.
단순한 코드는 실제로 민첩한 디자인을 의미합니다. 형식적인 절차가 없는 간단한 코드입니다. 즉, 아이디어를 빠르게 시험해 보고 잘 작동하면 계속 진행하고 잘 작동하지 않으면 빠르게 건너뛸 수 있습니다.
Java로 코드를 작성하는 것은 어렵지 않으며 구문도 간단합니다. 그리고 우리는 이미 기존 라이브러리와 API를 매우 잘 알고 있습니다. 정말 어려운 점은 이를 사용하여 엔터프라이즈 수준의 애플리케이션을 개발하고 유지 관리하는 것입니다.
우리는 동료들이 올바른 시간에 데이터베이스 연결을 닫는지, 트랜잭션을 계속 점유하지 않는지, 예외가 적절한 계층에서 올바르게 처리되는지, 잠금이 올바르게 획득 및 해제되는지 등을 확인해야 합니다.
개별적으로 보면 이러한 문제는 큰 문제가 아닙니다. 그러나 현장의 복잡성과 결합하면 문제가 매우 어려워지고 개발 자원이 부족하며 유지 관리가 어렵습니다.
이러한 전략을 여러 개의 작은 코드 조각으로 캡슐화하고 독립적으로 제약 조건 관리를 수행하게 하면 어떻게 될까요? 그러면 전략을 실행하기 위해 지속적으로 에너지를 소비할 필요가 없습니다. 이것은 엄청난 개선입니다. 함수형 프로그래밍이 어떻게 이를 수행하는지 살펴보겠습니다.
미친 반복
우리는 목록, 세트, 맵을 처리하기 위해 다양한 반복을 작성해 왔습니다. Java에서 반복자를 사용하는 것은 매우 일반적이지만 너무 복잡합니다. 여러 줄의 코드를 차지할 뿐만 아니라 캡슐화하기도 어렵습니다.
컬렉션을 어떻게 반복하고 인쇄합니까? for 루프를 사용할 수 있습니다. 컬렉션에서 일부 요소를 어떻게 필터링합니까? 여전히 for 루프를 사용하지만 수정 가능한 변수를 추가해야 합니다. 이러한 값을 선택한 후 이를 사용하여 최소값, 최대값, 평균값 등 최종 값을 찾는 방법은 무엇입니까? 그런 다음 변수를 재활용하고 수정해야 합니다.
이런 종류의 반복은 만병통치약과 같습니다. 모든 것을 할 수 있지만 모든 것이 희박합니다. 이제 Java는 많은 작업을 위한 내장 반복자를 제공합니다. 예를 들어 루프만 수행하는 작업, 매핑 작업을 수행하는 작업, 값을 필터링하는 작업, 축소 작업을 수행하는 작업 등 최대값, 최소값, 최소값과 같은 편리한 함수가 많이 있습니다. 평균 등 또한 이러한 작업은 잘 결합될 수 있으므로 이를 하나로 묶어 비즈니스 논리를 구현할 수 있으며 이는 간단하고 코드가 덜 필요합니다. 게다가 작성된 코드는 문제를 설명하는 순서와 논리적으로 일치하기 때문에 가독성이 높습니다. 2장, 19페이지의 컬렉션 사용에서 이러한 몇 가지 예를 볼 수 있으며 이 책은 그러한 예들로 가득 차 있습니다.
전략 적용
정책은 전사적 애플리케이션 전반에 걸쳐 구현됩니다. 예를 들어, 보안을 위해 작업이 올바르게 인증되었는지 확인해야 하고, 트랜잭션이 빠르게 실행될 수 있는지 확인해야 하며, 수정 로그가 올바르게 업데이트되는지 확인해야 합니다. 이러한 작업은 일반적으로 다음 의사 코드와 유사한 서버 측의 일반 코드 조각으로 끝납니다.
다음과 같이 코드 코드를 복사합니다.
트랜잭션 트랜잭션 = getFromTransactionFactory();
//... 트랜잭션 내에서 실행할 작업 ...
checkProgressAndCommitOrRollbackTransaction();
UpdateAuditTrail();
이 접근 방식에는 두 가지 문제가 있습니다. 첫째, 노력이 중복되는 경우가 많으며 유지 관리 비용도 증가합니다. 둘째, 비즈니스 코드에서 발생할 수 있는 예외를 잊어버리기 쉽습니다. 이는 트랜잭션 수명 주기 및 수정 로그 업데이트에 영향을 미칠 수 있습니다. 이는 try 및 finally 블록을 사용하여 구현되어야 하지만 누군가 이 코드를 건드릴 때마다 이 전략이 파괴되지 않았는지 다시 확인해야 합니다.
또 다른 방법은 팩토리를 제거하고 이 코드를 팩토리 앞에 넣는 것입니다. 트랜잭션 개체를 가져오는 대신 다음과 같이 실행된 코드를 잘 관리되는 함수에 전달하세요.
다음과 같이 코드 코드를 복사합니다.
runWithinTransaction((트랜잭션 트랜잭션) -> {
//... 트랜잭션 내에서 실행할 작업 ...
});
이는 작은 단계이지만 많은 수고를 덜어줍니다. 상태 확인과 로그 업데이트를 동시에 수행하는 전략은 추상화되어 runWithinTransaction 메서드에 캡슐화됩니다. 우리는 트랜잭션 컨텍스트에서 실행해야 하는 코드 조각을 이 메서드에 보냅니다. 더 이상 누군가가 이 단계를 수행하는 것을 잊어버리거나 예외를 적절하게 처리하지 않는 것에 대해 걱정할 필요가 없습니다. 정책을 구현하는 기능이 이미 이를 처리합니다.
5장에서 이 전략을 적용하기 위해 람다 표현식을 사용하는 방법을 다룰 것입니다.
확장 전략
전략은 어디에나 있는 것 같습니다. 이를 적용하는 것 외에도 엔터프라이즈 애플리케이션은 이를 확장해야 합니다. 일부 구성 정보를 통해 일부 작업을 추가하거나 삭제하려고 합니다. 즉, 모듈의 핵심 로직이 실행되기 전에 처리할 수 있습니다. 이는 Java에서 매우 일반적이지만 미리 생각하고 설계해야 합니다.
확장해야 하는 구성 요소에는 일반적으로 하나 이상의 인터페이스가 있습니다. 구현 클래스의 인터페이스와 계층 구조를 신중하게 설계해야 합니다. 이는 잘 작동할 수 있지만 유지 관리해야 할 인터페이스와 클래스가 많이 남게 됩니다. 이러한 디자인은 쉽게 다루기 힘들고 유지 관리가 어려워져 궁극적으로 확장 목적이 무산될 수 있습니다.
확장 가능한 전략을 설계하는 데 사용할 수 있는 기능적 인터페이스와 람다 표현식이라는 또 다른 솔루션이 있습니다. 새 인터페이스를 만들거나 동일한 메서드 이름을 따를 필요가 없습니다. 구현할 비즈니스 논리에 더 집중할 수 있습니다. 이에 대해서는 73페이지의 장식용 람다 식 사용에서 언급하겠습니다.
동시성이 쉬워졌습니다
갑자기 심각한 성능 문제가 표면화되면 대규모 애플리케이션이 출시 단계에 가까워지고 있습니다. 팀에서는 엄청난 양의 데이터를 처리하는 대규모 모듈에 성능 병목 현상이 발생한다는 사실을 신속하게 파악했습니다. 팀의 누군가는 멀티 코어의 장점을 최대한 활용하면 시스템 성능이 향상될 수 있다고 제안했습니다. 그러나 이 거대한 모듈이 이전 Java 스타일로 작성된다면 이 제안이 가져다주는 기쁨은 곧 산산조각이 날 것입니다.
팀은 이 거대 기업을 직렬 실행에서 병렬 실행으로 변경하려면 많은 노력이 필요하고, 복잡성이 추가되며, 멀티스레딩 관련 BUG가 쉽게 발생한다는 사실을 빠르게 깨달았습니다. 성능을 향상시키는 더 좋은 방법은 없을까요?
스위치를 누르고 아이디어를 표현하는 것처럼 직렬 실행을 선택하든 병렬 실행을 선택하든 상관없이 직렬 코드와 병렬 코드가 동일할 수 있을까요?
이것은 나니아에서만 가능한 일처럼 들리지만, 완전히 기능적인 측면에서 발전한다면 이 모든 것이 현실이 될 것입니다. 내장된 반복자와 기능적 스타일은 병렬화의 마지막 장애물을 제거합니다. JDK 설계에서는 눈에 띄지 않는 몇 가지 코드 변경만으로 직렬 실행과 병렬 실행 사이를 전환할 수 있습니다. 이에 대해서는 145페이지의 "병렬화로의 도약"에서 언급할 것입니다.
이야기를 해라
비즈니스 요구 사항을 코드 구현으로 전환하는 과정에서 많은 것이 손실됩니다. 손실되는 정보가 많을수록 오류 발생 가능성과 관리 비용도 높아집니다. 코드가 요구 사항을 설명하는 것처럼 보이면 읽기가 더 쉬울 것이고 요구 사항에 대해 사람들과 논의하기가 더 쉬울 것이며 그들의 요구 사항을 충족하기가 더 쉬울 것입니다.
예를 들어, 제품 관리자가 "모든 주식의 가격을 구하고 가격이 500위안보다 큰 주식을 찾아 배당금을 지불할 수 있는 총 자산을 계산하십시오"라고 말하는 것을 듣습니다. Java가 제공하는 새로운 기능을 사용하여 다음을 작성할 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
tickers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum()
이 변환 과정은 기본적으로 변환할 것이 없기 때문에 거의 무손실입니다. 이는 실제로 작동하는 함수형 스타일이며 책 전체, 특히 8장, 람다 표현식을 사용하여 프로그램 구축, 137페이지에서 이에 대한 더 많은 예를 볼 수 있습니다.
방역에 집중
시스템 개발에서는 일반적으로 핵심 비즈니스와 이에 필요한 세분화된 논리를 분리해야 합니다. 예를 들어, 주문 처리 시스템은 다양한 거래 소스에 대해 다양한 과세 전략을 사용하기를 원할 수 있습니다. 나머지 처리 논리에서 세금 계산을 분리하면 코드의 재사용성과 확장성이 향상됩니다.
객체지향 프로그래밍에서는 이러한 우려를 격리(isolation)라고 부르며, 일반적으로 이 문제를 해결하기 위해 전략 패턴이 사용됩니다. 해결책은 일반적으로 일부 인터페이스와 구현 클래스를 만드는 것입니다.
더 적은 코드로 동일한 효과를 얻을 수 있습니다. 또한, 많은 코드를 작성하거나 정체될 필요 없이 빠르게 자체 제품 아이디어를 시험해 볼 수 있습니다. 이 패턴을 생성하고 람다 표현식을 사용한 우려 격리(63페이지)에서 경량 함수를 통해 우려 격리를 수행하는 방법을 자세히 살펴보겠습니다.
게으른 평가
엔터프라이즈급 애플리케이션을 개발할 때 웹 서비스와 상호 작용하고, 데이터베이스를 호출하고, XML을 처리할 수 있습니다. 우리가 수행해야 할 작업은 많지만 항상 모든 작업이 필요한 것은 아닙니다. 특정 작업을 피하거나 최소한 일시적으로 불필요한 작업을 지연시키는 것은 성능을 향상하거나 프로그램 시작 및 응답 시간을 줄이는 가장 쉬운 방법 중 하나입니다.
이것은 작은 일이지만 순수한 OOP 방식으로 구현하려면 많은 작업이 필요합니다. 일부 중량 개체의 초기화를 지연하려면 다양한 개체 참조를 처리하고 널 포인터를 확인해야 합니다.
그러나 새로운 Optinal 클래스와 이것이 제공하는 일부 기능적 스타일 API를 사용하면 이 프로세스가 매우 간단해지고 코드가 더 명확해집니다. 이에 대해서는 105페이지의 지연 초기화에서 설명하겠습니다.
테스트 용이성 향상
코드의 처리 논리가 적을수록 오류가 수정될 가능성이 줄어듭니다. 일반적으로 기능 코드는 수정하기 쉽고 테스트하기 쉽습니다.
또한 4장, 람다 표현식으로 디자인 및 5장, 리소스 사용과 마찬가지로 람다 표현식도 가벼운 모의 개체로 사용되어 예외 테스트를 더 명확하고 이해하기 쉽게 만들 수 있습니다. 람다 표현식은 훌륭한 테스트 도구 역할을 할 수도 있습니다. 많은 일반적인 테스트 사례에서는 람다 식을 허용하고 처리할 수 있습니다. 이러한 방식으로 작성된 테스트 케이스는 회귀 테스트가 필요한 기능의 본질을 포착할 수 있습니다. 동시에 다양한 람다 식을 전달하여 테스트해야 하는 다양한 구현을 완료할 수 있습니다.
JDK 자체의 자동화된 테스트 사례는 람다 표현식의 좋은 응용 사례이기도 합니다. 더 자세히 알고 싶다면 OpenJDK 저장소의 소스 코드를 살펴보세요. 이러한 테스트 프로그램을 통해 람다 표현식이 테스트 케이스의 주요 동작을 매개변수화하는 방법을 확인할 수 있습니다. 예를 들어 "결과에 대한 컨테이너 생성"과 "매개변수화된 사후 조건 추가 확인"과 같은 테스트 프로그램을 구축합니다.
우리는 함수형 프로그래밍을 통해 고품질 코드를 작성할 수 있을 뿐만 아니라 개발 과정에서 발생하는 다양한 문제를 우아하게 해결한다는 사실을 살펴보았습니다. 이는 나중에 소개할 몇 가지 지침을 따르는 한 프로그램 개발이 더 빠르고 쉬워지고 오류가 줄어들 것임을 의미합니다.
섹션 4: 혁명이 아닌 진화
함수형 프로그래밍의 이점을 누리기 위해 다른 언어로 전환할 필요는 없습니다. 변경해야 할 것은 Java를 사용하는 방식뿐입니다. C++, Java, C#과 같은 언어는 모두 명령형 프로그래밍과 객체 지향 프로그래밍을 지원합니다. 그러나 이제 그들은 함수형 프로그래밍을 수용하기 시작했습니다. 우리는 두 가지 코드 스타일을 모두 살펴보고 함수형 프로그래밍이 가져올 수 있는 이점에 대해 논의했습니다. 이제 이 새로운 스타일을 배우는 데 도움이 되는 몇 가지 주요 개념과 예를 살펴보겠습니다.
Java 언어 개발 팀은 Java 언어 및 JDK에 기능적 프로그래밍 기능을 추가하는 데 많은 시간과 에너지를 소비했습니다. 그것이 가져오는 이점을 누리려면 먼저 몇 가지 새로운 개념을 도입해야 합니다. 다음 규칙을 따르는 한 코드 품질을 향상시킬 수 있습니다.
1. 선언적
2. 불변성 촉진
3. 부작용을 피하세요
4. 진술보다 표현을 선호하라
5. 고차함수를 활용한 디자인
이러한 실용적인 지침을 살펴보겠습니다.
선언적
우리가 익숙한 명령형 프로그래밍의 핵심은 가변성과 명령 기반 프로그래밍입니다. 우리는 변수를 생성한 다음 계속해서 그 값을 수정합니다. 또한 반복의 인덱스 플래그 생성, 값 증가, 루프 종료 여부 확인, 배열의 N 번째 요소 업데이트 등과 같은 실행해야 할 자세한 지침을 제공합니다. 과거에는 도구의 특성과 하드웨어의 한계로 인해 이런 방식으로만 코드를 작성할 수 있었습니다. 또한 불변 컬렉션에서는 선언적 포함 메서드가 명령형 메서드보다 사용하기 쉽다는 것도 확인했습니다. 모든 어려운 문제와 낮은 수준의 작업은 라이브러리 함수에서 구현되므로 더 이상 이러한 세부 사항에 대해 걱정할 필요가 없습니다. 단순화를 위해 선언적 프로그래밍도 사용해야 합니다. 불변성과 선언적 프로그래밍은 함수형 프로그래밍의 핵심이며 이제 Java가 마침내 이를 현실로 만듭니다.
불변성 촉진
변경 가능한 변수가 있는 코드에는 다양한 활동 경로가 있습니다. 더 많은 것을 변경할수록 원래 구조가 파괴되고 더 많은 오류가 발생하기가 더 쉬워집니다. 여러 변수가 수정되는 코드는 이해하기 어렵고 병렬화도 어렵습니다. 불변성은 본질적으로 이러한 걱정을 제거합니다. Java는 불변성을 지원하지만 이를 요구하지는 않습니다. 하지만 우리는 할 수 있습니다. 객체 상태를 수정하는 오래된 습관을 바꿔야 합니다. 우리는 가능한 한 불변 객체를 사용해야 합니다. 변수, 멤버 및 매개변수를 선언할 때 Joshua Bloch의 "Effective Java", "Treat object as immutable"에서 유명한 말처럼 최종적으로 선언하십시오. 객체를 생성할 때 String과 같은 불변 객체를 생성해 보십시오. 컬렉션을 생성할 때 Arrays.asList() 및 컬렉션의 unmodifyingList()와 같은 메서드를 사용하는 등 변경 불가능하거나 수정 불가능한 컬렉션을 생성해 보십시오. 가변성을 피함으로써 순수한 함수, 즉 부작용이 없는 함수를 작성할 수 있습니다.
부작용을 피하다
인터넷에서 주식 가격을 가져와 공유 변수에 쓰는 코드를 작성한다고 가정해 보겠습니다. 가져올 가격이 많으면 시간이 많이 걸리는 작업을 순차적으로 수행해야 합니다. 멀티스레딩의 장점을 활용하려면 경쟁 조건을 방지하기 위해 스레딩 및 동기화의 번거로움을 처리해야 합니다. 결과적으로 프로그램의 성능이 매우 저하되어 사람들이 실을 유지하기 위해 먹고 자는 것을 잊어버리게 됩니다. 부작용이 제거된다면 이러한 문제를 완전히 피할 수 있습니다. 부작용이 없는 함수는 불변성을 촉진하고 범위 내의 입력이나 기타 항목을 수정하지 않습니다. 이러한 종류의 함수는 읽기 쉽고 오류가 적으며 최적화하기 쉽습니다. 부작용이 없기 때문에 경쟁 조건이나 동시 수정에 대해 걱정할 필요가 없습니다. 뿐만 아니라 이러한 기능을 병렬로 쉽게 실행할 수 있으며 이에 대해서는 145페이지에서 설명합니다.
표현을 선호하세요
명령문은 수정을 강요하기 때문에 뜨거운 감자입니다. 표현식은 불변성과 함수 구성을 촉진합니다. 예를 들어 먼저 for 문을 사용하여 할인 후 총 가격을 계산합니다. 이러한 코드는 가변성과 장황한 코드로 이어집니다. 보다 표현적이고 선언적인 버전의 map 및 sum 메서드를 사용하면 수정 작업을 피할 수 있을 뿐만 아니라 함수를 서로 연결할 수 있습니다. 코드를 작성할 때 명령문 대신 표현식을 사용하도록 노력해야 합니다. 이렇게 하면 코드가 더 간단해지고 이해하기 쉬워집니다. 코드는 문제를 설명했을 때와 마찬가지로 비즈니스 로직에 따라 실행됩니다. 요구 사항이 변경되면 간결한 버전이 수정하기가 더 쉽습니다.
고차 함수를 사용한 설계
Java는 Haskell과 같은 기능적 언어처럼 불변성을 강요하지 않지만 변수를 수정할 수는 있습니다. 따라서 Java는 순수한 함수형 프로그래밍 언어가 아니며 앞으로도 그럴 것입니다. 그러나 Java의 함수형 프로그래밍에는 고차 함수를 사용할 수 있습니다. 고차 함수는 다음 단계로 재사용됩니다. 고차 함수를 사용하면 작고 전문적이며 응집력이 뛰어난 성숙한 코드를 쉽게 재사용할 수 있습니다. OOP에서는 객체를 메소드에 전달하고 메소드에서 새 객체를 생성한 다음 객체를 반환하는 데 익숙합니다. 고차 함수는 메소드가 객체에 수행하는 것과 동일한 작업을 함수에 수행합니다. 고차 함수를 사용하면 가능합니다.
1. 함수를 함수로 전달
2. 함수 내에서 새 함수를 만듭니다.
3. 함수 내의 반환 함수
우리는 이미 함수에서 다른 함수로 매개변수를 전달하는 예를 보았고 나중에 함수를 생성하고 반환하는 예를 볼 것입니다. "매개변수를 함수에 전달하는" 예를 다시 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
가격.스트림()
.filter(가격 -> 가격.compareTo(BigDecimal.valueOf(20)) > 0) .map(가격 -> 가격.곱하기(BigDecimal.valueOf(0.9)))
정오표 보고 • 토론
.reduce(BigDecimal.ZERO, BigDecimal::add);
이 코드에서는 가격 -> 가격.곱하기(BigDecimal.valueOf(0.9)) 함수를 지도 함수에 전달합니다. 전달된 함수는 고차 함수 맵이 호출될 때 생성됩니다. 일반적으로 함수에는 함수 본문, 함수 이름, 매개변수 목록, 반환 값이 있습니다. 즉시 생성된 이 함수에는 매개변수 목록, 화살표(->), 짧은 함수 본문이 있습니다. 매개변수 유형은 Java 컴파일러에 의해 추론되며 반환 유형도 암시적입니다. 이것은 익명 함수이므로 이름이 없습니다. 하지만 우리는 이를 익명 함수라고 부르지 않고 람다 표현식이라고 부릅니다. 익명 함수를 매개변수로 전달하는 것은 Java에서 새로운 것이 아닙니다. 이전에는 종종 익명 내부 클래스를 전달했습니다. 익명 클래스에 메서드가 하나만 있어도 클래스를 만들고 인스턴스화하는 과정을 거쳐야 합니다. 람다 표현식을 사용하면 가벼운 구문을 즐길 수 있습니다. 그뿐만 아니라, 우리는 항상 일부 개념을 다양한 객체로 추상화하는 데 익숙했지만 이제는 일부 동작을 람다 식으로 추상화할 수 있습니다. 이 코딩 스타일을 사용한 프로그래밍에는 여전히 약간의 생각이 필요합니다. 우리는 이미 뿌리박힌 명령적 사고를 기능적 사고로 바꿔야 합니다. 처음에는 약간 고통스러울 수도 있지만, 곧 익숙해질 것입니다. 계속해서 심화되면서 기능하지 않는 API는 점차 뒤쳐지게 될 것입니다. 먼저 이 주제를 멈추고 Java가 람다 표현식을 처리하는 방법을 살펴보겠습니다. 예전에는 항상 객체를 메서드에 전달했지만 이제는 함수를 저장하고 전달할 수 있습니다. 함수를 매개변수로 사용하는 Java 능력의 비결을 살펴보겠습니다.
섹션 5: 일부 구문 설탕을 추가했습니다.
이는 Java의 원래 함수를 사용하여 달성할 수도 있지만 람다 표현식은 구문상의 설탕을 추가하여 일부 단계를 절약하고 작업을 더 단순하게 만듭니다. 이렇게 작성된 코드는 개발 속도가 빨라질 뿐만 아니라 우리의 아이디어를 더 잘 표현해줍니다. 과거에 우리가 사용했던 많은 인터페이스에는 Runnable, Callable 등 단 하나의 메서드만 있었습니다. 이러한 인터페이스는 JDK 라이브러리의 모든 곳에서 찾을 수 있으며, 사용되는 곳에서는 일반적으로 함수를 사용하여 수행할 수 있습니다. 이전에는 단일 메서드 인터페이스만 필요했던 라이브러리 함수가 이제 함수형 인터페이스에서 제공하는 구문적 설탕 덕분에 경량 함수를 전달할 수 있습니다. 함수형 인터페이스는 하나의 추상 메서드만 가진 인터페이스입니다. Runnable, Callable 등 단 하나의 메서드만 있는 인터페이스를 살펴보세요. 이 정의가 해당 인터페이스에 적용됩니다. JDK8에는 함수, 조건자, 소비자, 공급자 등 더 많은 인터페이스가 있습니다. (157페이지, 부록 1에 더 자세한 인터페이스 목록이 있습니다) 기능적 인터페이스에는 인터페이스에서 구현되는 여러 정적 메서드와 기본 메서드가 있을 수 있습니다. @FunctionalInterface 주석을 사용하여 기능적 인터페이스에 주석을 달 수 있습니다. 컴파일러는 이 주석을 사용하지 않지만 이 인터페이스의 유형을 더 명확하게 식별할 수 있습니다. 뿐만 아니라,이 주석과 인터페이스에 주석을 달면 컴파일러는 기능 인터페이스 규칙에 맞는 지 여부를 강제로 확인합니다. 메소드가 매개 변수로 기능 인터페이스를 수신하는 경우 전달할 수있는 매개 변수에는 다음이 포함됩니다.
1. 익명의 내부 클래스, 가장 오래된 방법
2.지도 방법에서했던 것처럼 lambda 표현
3. 메소드 또는 생성자에 대한 참조 (나중에 이야기 할 것입니다)
메소드 매개 변수가 기능 인터페이스 인 경우 컴파일러는 LAMBDA 표현식 또는 메소드 참조를 매개 변수로 행복하게 허용합니다. Lambda 표현식을 방법으로 전달하면 컴파일러는 먼저 표현식을 해당 기능 인터페이스의 인스턴스로 변환합니다. 이 변형은 단순히 내부 클래스를 생성하는 것 이상입니다. 이 동기식 생성 인스턴스의 메소드는 매개 변수의 기능 인터페이스의 추상 방법에 해당합니다. 예를 들어, 맵 메소드는 기능적 인터페이스 함수를 매개 변수로 수신합니다. MAP 메소드를 호출 할 때 Java 컴파일러는 아래 그림과 같이 동기식으로 생성됩니다.
Lambda 표현식의 매개 변수는 인터페이스의 추상 방법의 매개 변수와 일치해야합니다. 이 생성 된 방법은 Lambda 표현의 결과를 반환합니다. 리턴 유형이 추상 메소드와 직접 일치하지 않으면이 메소드는 반환 값을 적절한 유형으로 변환합니다. 우리는 이미 Lambda 표현이 방법으로 전달되는 방법에 대한 개요를 가지고 있습니다. 방금 이야기 한 내용을 신속하게 검토 한 다음 Lambda 표현 탐험을 시작합시다.
요약
이것은 자바의 완전히 새로운 영역입니다. 고차 기능을 통해 우아하고 유창한 기능 스타일 코드를 작성할 수 있습니다. 이러한 방식으로 작성된 코드는 간결하고 이해하기 쉽고 오류가 거의 없으며 유지 보수 및 병렬화에 도움이됩니다. Java 컴파일러는 마법을 작동하며 기능 인터페이스 매개 변수를받는 경우 Lambda 표현식 또는 메소드 참조를 전달할 수 있습니다. 우리는 이제 Lambda Expressions의 세계와 JDK 라이브러리가 그들의 재미를 느끼기 위해 적응할 수 있습니다. 다음 장에서는 프로그래밍에서 가장 일반적인 세트 작업으로 시작하여 Lambda 표현의 힘을 발휘할 것입니다.