https://velog.io/@heoseungyeon/람다-표현식
https://highlighter9.tistory.com/41
https://catsbi.oopy.io/dc24ed56-6ce8-4ed6-b270-cc2b97f4ad5a
Java8에서 새롭게 추가된 기능 중 대표적인 기능은 람다 표현식입니다.
람다 표현식은 함수형 프로그래밍을 위한 필수 요소입니다.
1) 익명성
2) 함수
3) 전달
4) 간결성
(Item i1, Item i2) → i1.getTime.compareTo(i2.getTime)
(Item i1, Item i2) // 파라미터 리스트
→ // 화살표
i1.getTime.compareTo(i2.getTime) // 람다 바디
// 파라미터 리스트, 화살표, 람다 바디로 구성된다.
함수형 인터페이스의 추상 메서드를 즉석으로 제공(구현 객체 생성 - 전달)
할 수 있으며, 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급한다.시그니처
시그니처(signature)는 프로그래밍 언어와 관련하여 주로 메서드나 함수의 이름과
그것의 매개변수 타입들을 나타냅니다. 즉, 함수나 메서드의 "서명"이라고 볼 수 있습니다.
시그니처는 오버로딩(overloading)과 관련 있습니다. 자바 같은 언어에서 두 메서드가
같은 이름을 가지지만 다른 매개변수 타입 또는 개수를 갖는다면, 그 두 메서드는 다른 시그니처를 갖습니다.
이러한 시그니처의 차이로 인해 메서드 오버로딩이 가능합니다.
예를 들어,
void print(String message) { ... }
void print(int number) { ... }
위의 두 print 메서드는 다른 시그니처를 가집니다. 첫 번째는 String 타입의 매개변수를 받고,
두 번째는 int 타입의 매개변수를 받습니다. 그러나 반환 타입은 시그니처에 포함되지 않기 때문에
반환 타입만 다른 메서드는 오버로드할 수 없습니다.
람다 표현식의 문맥에서 "시그니처"는 람다의 매개변수 타입들을 지칭하며,
해당 람다가 사용될 함수형 인터페이스의 추상 메서드의 시그니처와 일치해야 합니다.
------
함수 디스크립터와 람다 표현식의 시그니처는 Java 8의 함수형 프로그래밍에 관련된 중요한 개념들입니다.
함수 디스크립터 (Function Descriptor):
함수형 인터페이스의 추상 메서드의 시그니처를 의미합니다.
함수형 인터페이스는 정확히 하나의 추상 메서드만을 가진 인터페이스를 말합니다.
그러나 default 메서드나 static 메서드는 여러 개 있을 수 있습니다.
예를 들어, java.util.function.Predicate<T> 함수형 인터페이스는
boolean test(T t)라는 하나의 추상 메서드를 가지므로,
이 메서드의 시그니처가 해당 인터페이스의 함수 디스크립터가 됩니다.
람다 표현식의 시그니처 (Lambda Expression Signature):
람다 표현식의 시그니처는 람다의 파라미터와 그 결과로 대응되는 함수 디스크립터와 일치해야 합니다.
예를 들어, 위에서 언급한 Predicate<T>의 test 메서드를 구현하는
람다는 다음과 같은 형태를 가져야 합니다: (T t) -> boolean 표현식
람다 표현식을 사용할 때, 그 람다가 대입되는 대상 타입(target type)의
함수형 인터페이스의 함수 디스크립터와 람다의 시그니처가 일치해야 합니다.
이 일치하는 과정을 타입 검사(type checking)라고 하며,
이를 통해 Java 컴파일러는 람다 표현식이 올바른 시그니처를 가지고 있는지 확인합니다.
1) 싱글 파라미터 : 파라미터가 하나인 경우, 괄호가 필요 없습니다.
2) 중괄호 선택 : 한 문장일 경우 중괄호가 필요 없습니다.
3) return 키워드 선택 : 한문장일 경우 생략이 가능합니다. 다만 중괄호를 포함한 경우 무조건 return 키워드를 포함해야합니다.
4) 매개변수 화살표 (→) : 매개변수 화살표를 통해 함수 몸체를 가리킬 수 있습니다.
1️⃣ 싱글 파라미터 예시
(param) -> param+1
// 괄호 생략 가능
param -> param+1
2️⃣ 중괄호 선택 예시
param -> { param+1 }
// 중괄호 생략 가능
param -> param+1
3️⃣ return 키워드 선택 예시
param -> { return param+1; }
// return 키워드 생략 가능
param -> param+1
4️⃣ 매개변수 화살표 (→)
// 함수 몸체를 가리키는 (->) 화살표 사용
param -> param+1
축약형
이다. 메서드 레퍼런스를 새로운 기능이 아니라 하나의 메서드를 참조하는 람다를 편리하게 표현할 수 있는 문법으로 간주 할 수 있다.기존의 코드 inventer.sort((Apple a1, Apple a2)
→ a1.getWeight().compareTo(a2.getWeight))) 를 메서드 참조형으로 전환한다면
inventer.sort((Apple a1, Apple a2) → comparing(Apple::getWeight)) 형으로 사용가능
람다 | 메서드 레퍼런스 단축 표 |
---|---|
(Apple a) → a.getWeight() | Apple::getWeight |
() → Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) ⇒ str.substring(i) | String::substring |
(String s) → System.out.println(s) | System.out::println |
가독성과 깊은 관련이 있다. 우리가 많이 사용하는 화살표 → 를 이용하는 방법도 좋다 근데 이방법을 풀어보자면 "메서드를 이렇게 호출하고 이렇게 사용해라" 가 된다. 하지만 메서드 참조의 경우 "이 메서드를 사용해"가 된다. 놀라울 정도로 축약형을 만들 수 있다.
class Orange{
public Integer weight;
public Orange(Integer weight)
this.weight = weight;
}
public Integer getWeight() {
return weight;
}
}
**1단계 : 코드 전달**
// 동작은 파라미터화
public static void main(String[] args) {
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
System.out.println();
oranges.sort(new OrangeComparator()); // 동작 파라미터화
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
}
class OrangeComparator implements Comparator<Orange> {
//파라미터화된 코드
public int compare(Orange o1, Orange o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
}
**2단계 : 익명 클래스 사용**
public static void main(String[] args) {
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
System.out.println();
oranges.sort(new Comparator<Orange>(){
public int compare(Orange o1, Orange o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
}
}
**3단계 : 람다 표현식 사용**
public static void main(String[] args) {
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
System.out.println();
oranges.sort((Orange o1, Orange o2) -> o1.getWeight().compareTo(o2.getWeight()));
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
}
형식 추론 간결화
public static void main(String[] args) {
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
System.out.println();
oranges.sort((Orange o1, Orange o2) -> o1.getWeight().compareTo(o2.getWeight()));
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
}
comparing을 이용한 간결화
public static void main(String[] args) {
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
System.out.println();
Comparator<Orange> c = Comparator.comparing((Orange o) -> o.getWeight());
oranges.sort(c); // 1번 방식
oranges.sort(Comparator.comparing(o -> o.getWeight())); // 2번방식
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
}
**4단계 : 메서드 참조 사용**
public static void main(String[] args) {
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
System.out.println();
oranges.sort(Comparator.comparing(Orange::getWeight));
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
}
변화 가능한 Process 코드를 파라미터로 분리
하여 언제든 대응가능 하도록 구성한 패턴이 바로, 실행어라운드 패턴이다. 그리고 실행 어라운드 패턴에서 코드를 넘길 때, 사용되는 기술이 람다!List<Company> companiesHasCafeteria= filter(list, (Company company)-> company.hasCafeteria());
메소드의 두 번쨰 파라미터인 Predicate<Company> p
가 기대하는 대상 형식(target type)을 나타낸다.//Before
List<Company> companiesHasCafeteria= filter(list, (Company company)-> company.hasCafeteria());
//After
List<Company> companiesHasCafeteria= filter(list, company-> company.hasCafeteria());
람다 표현식을 이용하려면 오버라이드 할 메서드가 포함된 함수형 인터페이스
가 필요합니다.
함수형 인터페이스란?
함수형 인터페이스(Functional Interface)는 함수를 하나만 가지는 인터페이스를 의미합니다. 함수형 인터페이스는
@FunctionalInterface
어노테이션을 붙여 표현합니다.
1) 함수형 인터페이스 생성
@FunctionalInterface
public interface UserPredicate{
boolean test(User user);
}
2) 클래스 생성
public class EmailPredicate implements UserPredicate{
private String email;
public EmailPredicate(String email){
this.email = email;
}
@Override
public boolean test(User user){
return email.equals(user.getEmail());
}
public String getEmail() {
return email;
}
}
UserPredicate userPredicate = new EmailPredicate("dia0312@naver.com") {
@Override
public boolean test(User user) {
return this.getEmail().equals(user.getEmail());
}
};
UserPredicate userPredicate = (user) -> "dia0312@naver.com".equals(user.getEmail());
람다의 바디(구현부)에는 파라미터를 제외하고도 바디 외부에 있는 변수를 참조할 수 있습니다.
이렇게 람다 시그니처의 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 자유 변수(Free Variable)
이라고 부릅니다.이런 자유 변수를 참조하는 행위를 람다 캡쳐링(Lambda Capturing)
이라고 합니다.
1️⃣ "지역 변수는 final로 선언되어 있어야 한다" 예시 코드
final String email = "dia0312@naver.com";
int age4 = getAge(users, user -> email.equals(user.getEmail()));
2️⃣ "final로 선언되지 않은 지역변수는 final처럼 동작해야 한다." 예시 코드
**// 값이 한번 초기화되고 재할당 되지 않음 -> final 처럼 동작함.**
String email = "dia0312@naver.com";
int age4 = getAge(users, user -> email.equals(user.getEmail()));
❌ "final로 선언되지 않은 지역변수는 final처럼 동작해야 한다." 예시 실패 코드
String email = "dia0312@naver.com";
**// 값을 재할당하였기 때문에 -> final 처럼 동작하지 않음.**
email = "sonny@naver.com";
int age4 = getAge(users, user -> email.equals(user.getEmail()));
다음과 같이 "Variable used in lambda expression should be final or effectively final"
에러 메시지를 확인할 수도 있습니다.
final
또는 유효하게 final
인지 확인함으로써 Java는 이 문맥에서 스레드 안전성을 보장합니다.코드에 대한 추론
이 쉬워집니다.나중에 실행될 수 있기 때문
에 (람다를 생성한 메서드가 실행을 마친 후에도 가능하게) 그들이 포착하는 변수가 일관성을 유지하도록 보장하는 것이 중요합니다. 이러한 변수가 변경 허용될 경우 람다가 실제로 실행될 때의 동작을 예측하기가 어렵게 됩니다.이 제한은 주로 지역 변수와 매개 변수에 관련되어 있습니다. 인스턴스 변수나 정적 변수는 이러한 제한이 없습니다. 그러나 람다 내의 인스턴스나 정적 변수를 사용할 때, 특히 스레드 안전성을 고려하여 주의해야 합니다.
Heap vs Stack 영역
**comparator 조합**
**predicate 조합**
**Function 조합**
https://www.tomatodeveloper.blog/2c5e8373-0b73-4da9-b933-bab52eb6d650
https://incheol-jung.gitbook.io/docs/study/java-8-in-action/2020-03-10-java8inaction-chap4
https://highlighter9.tistory.com/42
https://velog.io/@dongvelop/Moder-Java-in-Action-4장.-스트림-소개
필요성
멀티 코어 아키텍처를 활용한 병렬 방식? → 단순 반복 처리에 의해 복잡하고 어렵다.
또한 복잡한 코드는 디버깅도 어렵다.
⇒ 스트림으로 이를 쉽고 간단하게 처리할 수 있다.
stream()
을 parallelStream()
으로 바꾸면 이 코드를 멀티코어 아키텍처에서 병렬 실행할 수 있다!선언형 코드와 동작 파라미터화
를 이용하면 변하는 요구사항에 쉽게 대응할 수 있다!가독성과 명확성을 유지할 수 있다!
이때, 빌딩 블록 연산은 filter
, sorted
, map
, collect
를 말한다.
결과적으로 우리는 데이터 처리 과정을 병렬화하면서 스레드와 락을 걱정할 필요가 없다
자바 8 이전
List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish d: menu) {
`if(d.getCalories() < 400) {
lowCaloricDishes.add(d);
}
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2) {
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d: lowCaloricDishes) {
lowCaloricDishesName.add(d.getName());
}
vs
자바 8 이후
List<String> lowCaloricDishesName = menu.stream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dishes::getCalories))
.map(Dish::getName)
.collect(toList());
스트림이란 '데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소'로 정의할 수 있다.
컬렉션
과 마찬가지로 스트림
은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스
를 제공한다. 컬렉션의 주제는 데이터
이고 스트림의 주제는 계산
이다. 추후 이 차이에 대해 더 살펴보겠다.컬렉션
, 배열
, I/O 자원
등의 데이터 제공 소스로부터 데이터를 소비(consume
)한다.순차적
으로 또는 병렬
로 실행할 수 있다.stream()
메서드가 추가되었다.용도:
저장 및 변경:
데이터의 변환과 처리
에 중점을 둡니다. 스트림 자체는 데이터를 저장하거나 변경할 수 없다.재사용성:
컨슈머:
성능:
무한성:
메모리 소비:
- 컬렉션: 모든 데이터가 메모리에 저장됩니다.
- 스트림: 데이터는 필요할 때 처리되며, 모든 데이터가 메모리에 저장되지 않을 수 있다(지연 계산)
- 가장 큰 차이점은 데이터를 언제 계산하느냐다. 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 하나 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료 구조다.
- 즉, 컬렉션은 삽입 삭제가 가능하지만 스트림의 경우는 불가하다. 스트림 관점에서 메모리 저장 영역이 어디에 저장될지 모르기에
이러한 차이점들을 통해 컬렉션은 데이터의 저장과 관리에 중점을 둔 반면, 스트림은 데이터의 연속적인 처리에 중점을 둔다는 것을 알 수 있습니다.
딱 한번만 탐색할 수 있다.
외부 반복과 내부 반복
List<String> names = new ArrayList<>();
for(Dish dish: menu) {
names.add(dish.getName());
}
List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while (iterator.hasNext()) { //명시적 반복
Dish dish = iterator.next();
names.add(dish.getName());
}
List<String> names = menu.stream()
.map(Dish::getName)
.collect(toList());
병렬성 구현을 자동
으로 선택한다.List<String> names = menu.stream()
.filter(d -> d.getCalories() > 300) // 중간 연산
.map(Dish::getName) // 중간 연산
.limit(3) // 중간 연산
.collect(toList()); // 최종 연산
중간 연산의 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 하지 않는다는 것. 즉, 게으르게 처리한다. → 합쳐진 중간 연산을 최종 연산으로 한 번에 처리한다!
- 람다가 현재 처리중인 요리를 출력하기
```java
public class HighCaloriesNames {
public static void main(String[] args) {
List<String> names = menu.stream()
.filter(dish -> {
System.out.println("filtering " + dish.getName());
return dish.getCalories() > 300;
})
.map(dish -> {
System.out.println("mapping " + dish.getName());
return dish.getName();
})
.limit(3)
.collect(toList());
System.out.println(names);
}
}
```
최종 연산은 스트림 파이프라인에서 결과를 도출한다.
보통 최종 연산에 의해 List, Integer, void 등 스트림 이외의 결과를 반환한다.
- 스트림의 모든 요리 출력 예제
```java
menu.stream().forEach(System.out::println);
```
스트림 이용 과정은 아래 세 가지로 요약할 수 있다.
- 질의를 수행할 데이터 소스(like 컬렉션)
- 스트림 파이프라인을 구성할 중간 연산 연결
- 스트림 파이프라인을 실행하고 결과를 만들 최종 연산
- 스트림 파이프라인의 개념은 빌더 패턴과 비슷하다!
- 빌터 패턴에서는 호출을 연결해서 설정을 만든다(= 중간 연산)
- 그리고 준비된 설정에
build()
메서드를 호출한다. (= 최종 연산)
findFirst
나 findAny
와 같은 메서드일 경우, 스트림의 모든 요소를 처리할 필요가 없을 수도 있지만 잘못된 스트림 구성으로 인해 그렇게 동작할 수 있습니다.parallel()
메서드를 제공합니다. 그러나 모든 작업이 병렬 처리에 적합한 것은 아닙니다. 데이터 분할 및 병합 비용, 스레드 관리 오버헤드 등으로 인해 항상 성능 향상을 가져오는 것은 아닙니다.
정보 감사합니다.