람다란 무엇인가?
람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다.
람다표현식의 구조
(Item i1, Item i2) → i1.getTime.compareTo(i2.getTime)
(Item i1, Item i2) // 파라미터 리스트
→ // 화살표
i1.getTime.compareTo(i2.getTime) // 람다 바디
// 파라미터 리스트, 화살표, 람다 바디로 구성된다.
Java
복사
기본 문법
// 파라미터 리스트만 존재하는 경우
(String s) -> System.out.println(s);
// 리턴만 존재하는 경우
() -> "return"; // 파라미터가 없는경우 명시적으로 () 해줘야함, 자동으로 축약 return 형이된다.
// 리턴과 파라미터 리스트 모두존재
(String s) -> s;
//상세 문법:https://khj93.tistory.com/entry/JAVA-람다식Rambda란-무엇이고-사용법
Java
복사
함수형 인터페이스
정확히 하나의 추상메서드를 지정하는 인터페이스다. 추상화의 기초가 된다.
함수형 인터페이스로 뭘 할 수 있을까?
함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급 할 수 있다. 즉, 람다 표현식이 함수형인터페이스의 구현체로서 역할을 한다.
함수 디스크립터, 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가르킨다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.
// 나만의 이해
interface tester{
boolean test(String str);
}
/*
여기서 시그니처란 return Type(boolean)과 parameter Type(String) 이다.
그렇다면 함수 디스크립터라는건 앞서 살펴본 파라미터 리스트와 람다 바디의 리턴이다.
*/
...
(String s) -> true;
Java
복사
어라운드 패턴이란?
/*
이렇한 메서드가 있다
만약 당신이 br을 여러번 호출 해야한다면 main 문은 아마
processFile() 준비 -> 실행
processFile() 준비 -> 실행
...
이렇게 변할 것이다.
이렇듯 준비작업의 반복이 이어진다.
여기서 어라운드 패턴의 적용이 시작된다.
간단히 말해 어라운드 패턴이란 특정 함수의 아니면 특정작업에서의 일반적으로 실행해야할
코드 블럭을 묶는 느낌이다.
*/
public String processFile() throws IOException{
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){ //준비
return br.readLine(); // 실행
}
}
Java
복사
어라운드의 활용
1 동작파라미터를 기억하라
2 함수형 인터페이스를 이용해서 동작전달
3 동작실행
4 람다 전달
/*
이렇듯 동작을 추상화시키고 이를 이용하여 귀찮은 준비작업을 배제할 수 있다. 또한 main을
본다면
BufferedReaderProcesser process =
1 BufferedReader br -> 4 br.readLine() + br.readLine(); //실행
processFile(process); // 준비
위와 많은 차이가 난다.
준비작업은 단 한번 뿐이다.
또한 실행작업을 두번이나 실행 할 수 있었다.
*/
public String processFile(2 BufferedReaderProcesser process) throws IOException{
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){ //준비
return 3 process.process(br); // 실행
}
}
interface BufferedReaderProcesser{
String process(BufferedReader br) throws IOException;
}
Java
복사
함수형 인터페이스 사용
함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터라고 한다. 하지만 우리가 매번 함수형 인터페이스의 형태로 람다를 사용하고자 한다면 매번 함수형 인터페이스를 선언해줘야 할 것이다.
// 이러한 형태로 매번 선언해줘야한다.
@FunctionalInterface
public interface FunctionDescripter {
public String printString(String str);
// retrun String + paramater String str = 시그니처
}
Java
복사
이러한 점에서 자바 API는 공통 시그니처를 편하게 사용할 수 있는 인터페이스를 지원한다.
// Predicate
/*
T 타입의 paramater을 boolean 형태로 반환한다
*/
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
Java
복사
// Consumer
/*
T 타입의 paramater을 void 형태로 반환한다.
*/
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
Java
복사
// Function
/*
T 타입의 paramter을 R 타입 형태로 반환한다.
*/
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
Java
복사
기본형 특화 람다
자바의 모든 형식은 참조형 아니면 기본형에 해당, 근데 Consumer은 참조형만 사용가능.. 그래서 기본형을 참조형으로 변환하는 기능 제공, 이것이 바로 박싱이고 반대로 변환하는 것을 언박싱이라한다. 그리고 프로그래머가 편하게 코드를 구현할 수 있도록 박싱과 언박싱이 자동으로 이루어지는 오토박싱이라는 기능 제공
•
하지만 비용소모 큼, 박싱 값이 기본형을 감싸는 래퍼이며 힙에 저장된다. 그리고 박싱한 값은 메모리를 더 소비하며 기본형을 가져올때 메모리 탐색하는 과정 필요
•
그래서 제공하는 것이 IntPredicate 같은 기능 이러면
/*
이러한 불편한점을 비용의 이유로 기본형특화 람다형을 제공
*/
// basic api + primitive 타입 = Error
Predicate<int> primitiveAutoBoxingPredicate = (int i) -> i%2==0;
// basic api + 래퍼 타입 = 비용증가(Auto Boxing)
Predicate<Integer> primitiveAutoBoxingPredicate = (Integer i) -> i%2==0;
// 기본타입 특화형 api + primitive 타입 = 비용감소(no Auto Boxing)
IntPredicate intPredicate = (int i) -> i%2==0;
Java
복사
형식 검사, 형식 추론, 제약
람다는 인터페이스의 인스턴스라고 생각할 수 있다.
그럼 도대체 어떻게 인스턴스인지 파악하는 것 일까?
형식검사를 통하여 확인한다.
람다는 x → x +1 == 1 이라는 코드에서 x가 Interger인지 파악하는 것 일까?
형식 추론을 통하여 확인한다.
•
콘텍스트(Context) : 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수
•
대상형식(Target Type) : 인터페이스의 시그니처에서 바라는 형식, 예를 들자면 paramater은 String 이고 return은 boolean 형태
형식검사
(String str) -> str.length()<2; 의 형식 검사?
1 람다가 사용된 콘텍스트는 무엇인가? 우선 isTrue의 정의 확인 → 1.1 → 1.2
2 대상 형식은 Predicate<String>이다.
3 Predicate<String> 인터페이스의 추상 메서드는 무엇인가?
4 Predicate의 추상은 boolean test(T t) 이다.
5 String을 인수로 받아 boolean을 반환하는 test 메서드다.
6 test의 시그니처와 동일한 람다 시그니처인가? 맞다. T → boolean == Item → boolean
public class FormIspection {
public static void main(String[] args) {
String len1String = "1";
Predicate<String> p = 1 (String str) -> str.length()<2;
/*
6 (String str) -> str.length()<2;의 디스크립터 즉 리턴과 파라미터가
5번의 시그니처 즉, 리턴과 파라미터와 동일한가?
*/
if (isTrue(len1String, 1.1 p)){
System.out.println("len1String의 값은 문자열길이가 2이하 입니다.");
}else{
System.out.println("len1String의 값은 문자열길이가 2이상 입니다.");
}
}
public static boolean 1.2 isTrue(String str,2 Predicate<String> p){
return p.test(str);
};
}
... other Interface
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
3,4 boolean test(T t);
//5 return boolean, paramater T Type이다. 근데 우리는 paramater String Type
/*
생각해볼거리
그럼 시그니처가 같고 디스크립터가 같으면 호환되는 것인가?
예를 들어 Function<String,boolean> 이 있다고 가정하면 위의 코드가 정상 실행될까?
가능하다. 왜냐하면 함수의 시그니처가 동일하고 디스크립터도 동일하기 때문에
*/
Java
복사
형식 추론
(str) -> str.length()<2; 의 형식 추론?
1. str의 type 설정이 없다.
2. str의 형식을 Predicate의 <String> 에서 찾았다.
3. String을 기반으로 str의 형식을 추론한다.
상황에 따라서 명시적으로 하는게 좋을 수 있다. 아니면 가독성을 위해서 제외해도 된다.
/*
동일한 예제이다.
하지만 다른점은 람다 표현식에서 Type이 빠진 상태다.
근데 어떻게 str변수를 String으로 인식하고 lenght() 메서드를 사용할 수 있는 것일까?
이런 의문점에서 시작된다.
*/
public class FormInfer {
public static void main(String[] args) {
String len1String = "1";
Predicate<String> p = 1 (4 str) -> str.length()<2;
// 1번은 (str) -> str.length()<2; 전체를 의미
// 4번은 (str) paramater type 한정
if (isTrue(len1String, p)){
System.out.println("len1String의 값은 문자열길이가 2이하 입니다.");
}else{
System.out.println("len1String의 값은 문자열길이가 2이상 입니다.");
}
}
public static boolean isTrue(String str, 2 Predicate<3 String> p){
// 2번은 Predicate<String> p) 전체를 의미
// 3번은 <String> paramater type 한정
return p.test(str);
};
}
... other Interface
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
Java
복사
지역변수의 사용
자신의 람다 바디 안의 인수만 사용했다. 그런데 외부 즉, 람다 바디 밖의 지역변수를 사용하고 싶을 때가 있다. 이런 경우를 람다 캡처링이라 부른다. 하지만 제약이 따른다.지역변수는 명시적으로 final 선언이 되거나 아니면 실질적으로 final로 선언된 변수와 똑같이 사용되어야한다.
왜 지역 변수 제약이 필요한 것일까?
우선 내부적으로 지역변수는 스택, 인스턴스 변수는 힙에 저장된다. 그래서 람다에서 지역변수에 바로 접근 할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서 해당 변수에 접근하려 할 수 있다. 그래서 사라진 변수를 못찾기에 복사본을 제공한다. 복사본을 제공한다는 의미는 값이 불변이 되어야 한다는 의미와 같다. 복사했는데 변경되면 마치 데이터베이스의 트랜젝션에서 일어날 수 있는 오류?가 발생할 수 있다.
메서드 참조
메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.
•
기존의 코드 inventer.sort((Apple a1, Apple a2) → a1.getWeight().compareTo(a2.getWeight))) 를 메서드 참조형으로 전환한다면
inventer.sort((Apple a1, Apple a2) → comparing(Apple::getWeight)) 형으로 사용가능
여기서 중요한 점 기존에 나는 단순 참조형으로만 사용했다.
예를 들자면 inventer.sort(Apple::getWeight) 이런식으로 사용가능
근데 안에서 다시 참조시킬 수 있는 방법도 있다.
메서드 참조가 왜 중요한가?
가독성과 깊은 관련이 있다. 우리가 많이 사용하는 화살표 → 를 이용하는 방법도 좋다 근데 이방법을 풀어보자면 "메서드를 이렇게 호출하고 이렇게 사용해라" 가 된다. 하지만 메서드 참조의 경우 "이 메서드를 사용해"가 된다. 놀라울 정도로 축약형을 만들 수 있다.
메서드 참조를 만드는 방법
◦
정적 메소드 참조 → Integer::parseInt 의 형식 Integet class 내부의 정적 메서드 parseInt() 참조
◦
다양한 형식의 인스턴스 메서드 참조→ String::length String class의 인스턴스 메서드 lenght() 참조
◦
기존 객체의 인스턴스 메서드 참조→ Transaction expensiveTransaction = new Transaction();
Value v = expensiveTransaction::getValue; 형태로 가능
생성자 참조
Class::new 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다.
class ConstructorTest{
public String name;
ConstructorTest(){
}
ConstructorTest(String name){
this.name = name;
}
public String getName() {
return name;
}
}
Java
복사
public class ConstructorReference {
public static void main(String[] args) {
Supplier<ConstructorTest> spplierConstructorInstance = ConstructorTest::new;
ConstructorTest s1 = spplierConstructorInstance.get();
Function<String, ConstructorTest> functionConstructorInstance =
ConstructorTest::new;
ConstructorTest f1 = functionConstructorInstance.apply("function 이용");
System.out.println(f1.getName());
}
}
Java
복사
public class ConstructorReference {
static List<String> names = Arrays.asList("a","b","c");
static List<ConstructorTest> constructorTestList =
map(names,ConstructorTest::new);
public static void main(String[] args) {
constructorTestList.forEach(s -> System.out.println(s.getName()));
}
public static <T,R> List<R> map(List<T> names, Function<T,R> f){
List<R> resultList = new ArrayList<>();
for (T name : names){
resultList.add(f.apply(name));
}
return resultList;
}
}
Java
복사
•
여기서 의문점 그럼 biFunction에서 3개의 인수를 받아서 만드는 방법은 없을까
•
우리는 직접 함수형 인터페이스를 제작해서 넣어줘야한다.
람다, 메소드 참조 활용하기
class Orange{
public Integer weight;
public Orange(Integer weight)
this.weight = weight;
}
public Integer getWeight() {
return weight;
}
}
Java
복사
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()));
}
Java
복사
class OrangeComparator implements Comparator<Orange> {
//파라미터화된 코드
public int compare(Orange o1, Orange o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
}
Java
복사
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()));
}
}
Java
복사
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()));
}
Java
복사
•
형식 추론 간결화
public static void main(String[] args) {
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
System.out.println();
oranges.sort((o1, o2) -> o1.getWeight().compareTo(o2.getWeight()));
oranges.forEach(s -> System.out.print(" "+s.getWeight()));
}
Java
복사
•
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()));
}
Java
복사
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()));
}
Java
복사
람다 표현식을 조합할 수 있는 유용한 메서드
comparator 조합
•
역정렬 : inventory.sort(comparing(item::getWeight)); 를 역정렬 시킨다고 가정한다면 우리는 따로 재정의할 필요없이 reversed라는 디폴트 메서드를 이용하면 된다. inventory.sort(comparing(item::getWeight).reversed());
•
Comparator 연결 : 만약 무게가 같은 item이 있다고 가정해보자. 그럼 어떤방식으로 이 item을 정렬해야할까? 이러한 의문에서 출발하여 도와주는 두번째 comparator를 만들수 있다.
inventory.sort(comparing(item::getWeight).reversed().thenComparing(item::getCountry)); 와 같은 방식으로 여기서 중요한 메소드는 thenComparing이다.
predicate 조합
복잡한 predicate를 만들수 있도록 nagate, and, or 세가지를 제공한다.
•
nagate : Predicate<Item> notRedItem = redItem.negate();
→ 기존에 redItem의 반전된 객체를 뽑아낸다.
•
and : Predicate<Item> redAndHeavyItem = redItem.and(item → item.getWeight() >150);
→ 기존에 redItem에서 뽑아낸 빨간 아이템 + 150 이상의 무게를 가진 item 추출
•
or : 은 기존에 동일하다. 위의 예시로 조합해서 만든다면
→ redItem의 결과는 빨강item이다. 여기에 or을 조합한다면 빨강이거나 무게가 150이상인 item이 된다.
•
복합 : and, or, nagate를 복합적으로 사용할 수 있다.
class Item{
enum Color{
RED, GREEN
}
public Color color;
public Integer weight;
public Item(Color color, Integer weight){
this.color = color;
this.weight = weight;
}
public Color getColor() {
return color;
}
public Integer getWeight() {
return weight;
}
@Override
public String toString() {
return "Item{" +
"color=" + color +
", weight=" + weight +
'}';
}
}
Java
복사
public class Predicated {
static List<Item> items = Arrays.asList(
new Item(Item.Color.GREEN, 123),
new Item(Item.Color.RED, 125),
new Item(Item.Color.RED, 12412),
new Item(Item.Color.GREEN, 1212),
new Item(Item.Color.RED, 123),
new Item(Item.Color.GREEN, 124),
new Item(Item.Color.RED, 1232),
new Item(Item.Color.GREEN, 1212)
);
public static void main(String[] args) {
System.out.println("redItem 만 뽑아내기");
Predicate<Item> redItem = (item) -> item.getColor().equals(Item.Color.RED);
items.stream().filter(redItem).forEach(item -> System.out.println(item.toString()));
System.out.println("notRedItem 만 뽑아내기");
Predicate<Item> notRedItem = redItem.negate();
items.stream().filter(notRedItem).forEach(item -> System.out.println(item.toString()));
System.out.println("notRedItem 만 뽑아내기이거나 150이상인것만 뽑아내기");
Predicate<Item> notRedItemOrHaevyItem = redItem.negate().or(item -> item.getWeight()>150);
items.stream().filter(notRedItemOrHaevyItem).forEach(item -> System.out.println(item.toString()));
System.out.println("notRedItem과 150이상인것만 뽑아내기");
Predicate<Item> notRedItemAndHaevyItem = redItem.negate().and(item -> item.getWeight()>150);
items.stream().filter(notRedItemAndHaevyItem).forEach(item -> System.out.println(item.toString()));
System.out.println("notRedItem과 150이상인것만 뽑아내기 이거나 빨강이면서 150이하인것만");
Predicate<Item> notRedItemAndHaevyItemORGreenItemAndNotHaevyItem =
redItem.negate().and(item -> item.getWeight()>150)
.or(redItem.and(item-> item.getWeight()<150));
items.stream().filter(notRedItemAndHaevyItemORGreenItemAndNotHaevyItem).forEach(item -> System.out.println(item.toString()));
}
}
Java
복사
Function 조합
andThen, compose 두가지 디폴트 메서드를 제공한다.
•
andThen : x → x + 1 = sumOne함수 , 숫자 2를 곱하는 sumOneAndThenMultTwo함수가 있다고 가정
순서 sumOne 실행 → sumOneAndThenMultTwo 실행 = 4
•
compose : 똑같은 가정
순서 multTwoComposeSumOne 실행 → sumOne 실행 = 3
// andThen
public static void main(String[] args) {
Integer one = 1;
Function<Integer, Integer> sumOne = x -> x+1;
System.out.println(sumOne.apply(one));
Function<Integer, Integer> sumOneAndThenMultTwo = sumOne.andThen(x -> x*2);
System.out.println(sumOneAndThenMultTwo.apply(one));
}
Java
복사
// compose
public static void main(String[] args) {
Integer one = 1;
Function<Integer, Integer> sumOne = x -> x+1;
System.out.println(sumOne.apply(one));
Function<Integer, Integer> multTwoComposeSumOne = sumOne.compose(x -> x*2);
System.out.println(multTwoComposeSumOne.apply(one));
}
Java
복사
andThen을 이용한 파이프라인이 가능하다. 물론 compose도 가능하다.