Preface
이번 장에선 람다식에 대해 공부했다.
지금껏 람다식이란 단어를 여러 번 들어만 봤을 뿐 어떤 개념인지 전혀 몰랐는데, 이제서야 사용 방법을 알게 되었다.
람다식을 사용하는 이유는 함수를 보다 짧게 작성하여 가독성을 높이기 위함이라고 하는데,
아무리 봐도 정말 간단한 몇몇 함수를 제외하면 람다식을 통해 작성한 코드가 훨씬 길고 복잡한 것 같다.
이에 추가적으로 함수적 인터페이스까지 사용해서 코드를 작성하게 되면 가독성도 급격히 떨어지는 느낌이다.
입사한 기업에서 람다식을 통한 코드 작성을 요구하지 않는 이상 나는 굳이 람다식 사용을 고집하진 않을 듯하다.
본문에서 함수적 인터페이스 메소드의 종류는 리턴 타입과 이름만 다를 뿐 대부분 동일한 맥락의 메소드이므로 생략했다.
1. 람다식이란?
- 함수적 프로그래밍은 병렬 처리와 이벤트 지향 프로그래밍에 적합하다.
- 람다식: 객체 지향 언어보단 함수 지향 언어에 가까운 익명함수를 생성하기 위한 식
1) 자바 코드가 간결해진다.
2) 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계할 수 있다.
3) 매개 변수를 가진 코드 블록 형태이지만, 런타임 시에는 인터페이스의 익명 구현 객체를 생성한다.
2. 람다식 기본 문법
- 람다식 작성 방법
1) (타입 매개변수): 오른쪽 중괄호 블록을 실행하기 위해 필요한 값 제공
2) (->): 매개 변수를 이용해서 중괄호를 실행한다는 의미
(타입 매개변수, ...) -> { 실행문; ... }
(int a) -> {System.out.println(a);}
3) 람다식에선 보통 매개 변수의 타입을 언급하지 않는다.
(a) -> {System.out.println(a);}
4) 매개 변수가 한 개일 땐 괄호를 생략할 수 있고, 실행문이 하나일 땐 중괄호를 생략할 수 있다.
a -> System.out.println(a)
5) 매개 변수가 없을 땐 반드시 빈 괄호를 작성해야 한다.
() -> { 실행문; }
6) 중괄호를 실행하고 결과값을 리턴해야 하는 경우 return문으로 결과값을 지정할 수 있다.
(x, y) -> { return x + y; };
7) 중괄호에 return문만 있을 경우, return문을 생략할 수 있다.
(x, y) -> x + y
3. 타겟 타입과 함수적 인터페이스
- 람다식
1) 단순히 메소드를 선언하는 것이 아니라 메소드를 가지고 있는 객체를 생성한다.
2) 인터페이스 변수에 대입된다.
→ 인터페이스의 익명 구현 객체를 생성한다.
3) 대입될 인터페이스의 종류에 따라 작성 방법이 달라진다.
4) 타겟 타입(target type): 람다식이 대입될 인터페이스
- 람다식이 하나의 메소드를 정의하므로 두 개 이상의 추상 메소드가 선언된 인터페이스는 람다식을 이용해 구현 객체를 생성할 수 없다.
→ 하나의 추상 메소드가 선언된 인터페이스만 람다식의 타겟 타입이 될 수 있다.
- 함수적 인터페이스(functional interface): 하나의 추상 메소드가 선언된 인터페이스
1) 인터페이스 선언 시 @FunctionalInterface 어노테이션을 붙이면 된다.
→ 두 개 이상의 추상 메소드가 선언되면 컴파일 오류를 발생시키며, 필수 사항은 아니고 선택사항이다.
- 매개 변수와 리턴값이 없는 람다식
package ch14;
@FunctionalInterface
interface MyFunctionalInterface {
public void method();
}
public class MyFunctionalInterfaceExample {
public static void main(String[] args) {
MyFunctionalInterface fi;
fi = () -> {
String str = "method call1";
System.out.println(str);
};
fi.method();
fi = () -> { System.out.println("method call2"); };
fi.method();
fi = () -> System.out.println("method call3");
//실행문이 하나일 땐 중괄호 생략 가능
fi.method();
}
}
- 매개 변수가 있는 람다식
package ch14;
@FunctionalInterface
interface MyFunctionalInterface1 {
public void method(int x);
}
public class MyFunctionalInterfaceExample1 {
public static void main(String[] args) {
MyFunctionalInterface1 fi;
fi = (x) -> {
int result = x * 5;
System.out.println(result);
};
fi.method(2);
fi = (x) -> {
System.out.println(x * 5);
};
fi.method(2);
fi = x -> System.out.println(x * 5);
// 매개 변수가 하나일 땐 괄호 생략 가능
fi.method(2);
}
}
- 리턴값이 있는 람다식
package ch14;
@FunctionalInterface
interface MyFunctionalInterface2 {
public int method(int x, int y);
}
public class MyFunctionalInterfaceExample2 {
public static void main(String[] args) {
MyFunctionalInterface2 fi;
fi = (x, y) -> {
int result = x + y;
return result;
};
System.out.println(fi.method(2, 5));
fi = (x, y) -> {
return x + y;
};
System.out.println(fi.method(2, 5));
fi = (x, y) -> x + y;
System.out.println(fi.method(2, 5));
//return문만 있을 경우, 중괄호와 return문 생략 가능
fi = (x, y) -> sum(x, y);
System.out.println(fi.method(2, 5));
}
public static int sum(int x, int y) {
return (x + y);
}
}
4. 클래스 멤버와 로컬 변수 사용
- 람다식의 실행 블록에는 클래스의 멤버(필드, 메소드) 및 로컬 변수를 사용할 수 있다.
→ 로컬 변수는 제약 사항이 있다.
- 보통 익명 객체 내부에서 this는 익명 객체의 참조이지만, 람다식에서 this는 람다식을 실행한 객체의 참조이다.
→ 아래 예제에서 람다식 내부의 this는 Inner이다.
package ch14;
class UsingThis {
public int outterField = 10;
class Inner {
int innerField = 20;
void method() {
MyFunctionalInterface fi = () -> {
System.out.println("outterField: " + outterField);
System.out.println("outterFiled: " + UsingThis.this.outterField + "\n");
System.out.println("innerField: " + innerField);
System.out.println("innerField: " + this.innerField + "\n");
};
fi.method();
}
}
}
public class UsingThisExample {
public static void main(String[] args) {
UsingThis usingThis = new UsingThis();
UsingThis.Inner inner = usingThis.new Inner();
inner.method();
}
}
- 람다식은 주로 메소드 내부에서 작성되므로 로컬 익명 구현 객체를 생성시킨다고 봐야 한다.
→ 메소드의 매개 변수 및 로컬 변수를 사용하려면 변수가 final 특성을 가져야 한다. (read만 허용, 값 변경 불가)
5. 표준 API의 함수적 인터페이스
- 람다식을 이용한 Runnable 인스턴스 생성
package ch14;
public class RunnableExample {
public static void main(String[] args) {
Runnable runnable = () -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
//public class RunnableExample {
//
// public static void main(String[] args) {
//
// Thread thread = new Thread(() -> {
// for (int i = 0; i < 10; i++) {
// System.out.println(i);
// }
// });
// thread.start();
// }
//
//}
- 함수적 인터페이스는 java.util.function 표준 API 패키지로 제공된다.
종류 | 추상 메소드 특징 |
Consumer | - 매개값은 있고, 리턴값은 없음 |
Supplier | - 매개값은 없고, 리턴값은 있음 |
Function | - 매개값도 있고, 리턴값도 있음 - 주로 매개값을 리턴값으로 매핑(타입 변환) |
Operator | - 매개값도 있고, 리턴값도 있음 - 주로 매개값을 연산하고 결과를 리턴 |
Predicate | - 매개값은 있고, 리턴 타입은 boolean - 매개값을 조사해서 true/false를 리턴 |
- Consumer 함수적 인터페이스: 리턴값이 없는 accept( ) 메소드를 가지고 있다.
→ 단지 매개값을 사용만 할 뿐, 리턴값은 없다.
인터페이스명 | 추상 메소드 | 설명 |
Consumer<T> | void accept(T t) | 객체 T를 받아 소비 |
BiConsumer<T, U> | void accept(T t, U u) | 객체 T와 U를 받아 소비 |
DoubleConsumer | void accept(Double value) | double 값을 받아 소비 |
IntConsumer | void accept(int value) | int 값을 받아 소비 |
LongConsumer | void accept(long value) | long 값을 받아 소비 |
ObjDoubleConsumer<T> | void accept(T t, double value) | 객체 T와 double 값을 받아 소비 |
ObjIntConsumer<T> | void accept(T t, int value) | 객체 T와 int 값을 받아 소비 |
ObjLongConsumer<T> | void accept(T t, long value) | 객체 T와 long 값을 받아 소비 |
package ch14;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.DoubleConsumer;
import java.util.function.ObjIntConsumer;
public class ConsumerExample {
public static void main(String[] args) {
Consumer<String> consumer = t -> System.out.println(t + 8);
consumer.accept("java");
BiConsumer<String, String> biConsumer = (t, u) -> System.out.println(t + u);
biConsumer.accept("Java", "8");
DoubleConsumer doubleConsumer = d -> System.out.println("Java" + d);
doubleConsumer.accept(8.0);
ObjIntConsumer<String> objIntConsumer = (t, i) -> System.out.println(t + i);
objIntConsumer.accept("Java", 8);
}
}
- Supplier 함수적 인터페이스: 매개 변수가 없고 리턴값이 있는 getXXX( ) 메소드를 가지고 있다.
→ 실행 후 호출한 곳으로 데이터를 리턴하는 역할을 한다.
인터페이스명 | 추상 메소드 | 설명 |
Supplier<T> | T get( ) | T 객체를 리턴 |
BooleanSupplier | boolean getAsBoolean( ) | boolean 값을 리턴 |
DoubleSupplier | double getAsDouble( ) | double 값을 리턴 |
IntSupplier | int getAsInt( ) | int 값을 리턴 |
LongSupplier | long getAsLong( ) | long 값을 리턴 |
package ch14;
import java.util.function.IntSupplier;
public class SupplierExample {
public static void main(String[] args) {
IntSupplier intSupplier = () -> {
int num = (int) (Math.random() * 6) + 1;
return num;
};
int num = intSupplier.getAsInt();
System.out.println("눈의 수: " + num);
}
}
- Function 함수적 인터페이스: 매개값과 리턴값이 있는 applyXXX( ) 메소드를 가지고 있다.
→ 매개값을 리턴값으로 매핑하는 역할을 한다.
package ch14;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.ToIntFunction;
class Student {
private String name;
private int englishScore;
private int mathScore;
public Student(String name, int englishScore, int mathScore) {
this.name = name;
this.englishScore = englishScore;
this.mathScore = mathScore;
}
public String getName() {
return name;
}
public int getEnglishScore() {
return englishScore;
}
public int getMathScore() {
return mathScore;
}
}
public class FunctionExample1 {
private static List<Student> list = Arrays.asList(new Student("Kim", 90, 96), new Student("Park", 95, 93));
public static void printString(Function<Student, String> function) {
for (Student student : list) {
System.out.print(function.apply(student) + " ");
}
System.out.println();
}
public static void printInt(ToIntFunction<Student> function) {
for (Student student : list) {
System.out.print(function.applyAsInt(student) + " ");
}
System.out.println();
}
public static void main(String[] args) {
System.out.println("학생이름");
printString(t -> t.getName());
System.out.println("영어 점수");
printInt(t -> t.getEnglishScore());
System.out.println("수학 점수");
printInt(t -> t.getMathScore());
}
}
package ch14;
import java.util.Arrays;
import java.util.List;
import java.util.function.ToIntFunction;
public class FunctionExample2 {
private static List<Student> list = Arrays.asList(new Student("Kim", 90, 96), new Student("Park", 95, 93));
public static double avg(ToIntFunction<Student> function) {
int sum = 0;
for (Student student : list) {
sum += function.applyAsInt(student);
}
double avg = (double) sum / list.size();
return avg;
}
public static void main(String[] args) {
double englishAvg = avg(s -> s.getEnglishScore());
System.out.println("영어 평균 점수: " + englishAvg);
double mathAvg = avg(s -> s.getMathScore());
System.out.println("수학 평균 점수: " + mathAvg);
}
}
- Operator 함수적 인터페이스: 매개 변수와 리턴값이 있는 aplyXXX( ) 메소드를 가지고 있다.
→ 매개값을 이용해서 연산을 수행한 후 동일한 타입으로 리턴값을 제공하는 역할을 한다.
package ch14;
import java.util.function.IntBinaryOperator;
public class OperatorExample {
private static int[] scores = { 92, 95, 87 };
public static int maxOrMin(IntBinaryOperator operator) {
int result = scores[0];
for (int score : scores) {
result = operator.applyAsInt(result, score);
}
return result;
}
public static void main(String[] args) {
int max = maxOrMin((a, b) -> {
if (a >= b) {
return a;
} else {
return b;
}
});
System.out.println("최대값: " + max);
int min = maxOrMin((a, b) -> {
if (a <= b) {
return a;
} else {
return b;
}
});
System.out.println("최소값: " + min);
}
}
- Predicate 함수적 인터페이스: 매개 변수와 boolean 리턴값이 있는 testXXX( ) 메소드를 가지고 있다.
→ 매개값을 조사해서 true/false를 리턴하는 역할을 한다.
package ch14;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
class Student1 {
private String name;
private String sex;
private int score;
public Student1(String name, String sex, int score) {
this.name = name;
this.sex = sex;
this.score = score;
}
public String getSex() {
return sex;
}
public int getScore() {
return score;
}
}
public class PredicateExample {
private static List<Student1> list = Arrays.asList(new Student1("Kim", "man", 90),
new Student1("Park", "woman", 90), new Student1("Lee", "man", 95), new Student1("Choi", "woman", 92));
public static double avg(Predicate<Student1> predicate) {
int count = 0;
int sum = 0;
for (Student1 student : list) {
if (predicate.test(student)) {
count++;
sum += student.getScore();
}
}
return (double) sum / count;
}
public static void main(String[] args) {
double maleAvg = avg(t -> t.getSex().equals("man"));
System.out.println("mens average score: " + maleAvg);
double femaleAvg = avg(t -> t.getSex().equals("woman"));
System.out.println("womens average score: " + femaleAvg);
}
}
- 디폴트 메소드: 인터페이스에서 선언하지만, 구현 객체가 있어야 사용 가능한 메소드
- 디폴트 및 정적 메소드는 추상 메소드가 아니므로 함수적 인터페이스에 선언되어도 무방하다.
→ function 패키지 내의 함수적 인터페이스는 하나 이상의 디폴트 및 정적 메소드를 가지고 있다.
- Consumer, Function, Operator 종류의 함수적 인터페이스는 andThen( )과 compose( ) 디폴트 메소드를 지닌다.
1) 두 메소드 모두 두 개의 함수적 인터페이스를 순차적으로 연결하고, 첫 번째 처리 결과를 두 번째 매개값으로 제공해서 최종 결과값을 얻을 때 사용한다.
2) 차이점이라면 전자는 앞의 인터페이스부터, 후자는 뒤의 인터페이스부터 처리한다.
- Consumer 종류의 함수적 인터페이스는 처리 결과를 리턴하지 않으므로 andThen( ) 디폴트 메소드는 함수적 인터페이스의 호출 순서만 정한다.
- Function과 Operator 종류의 함수적 인터페이스는 먼저 실핸한 함수적 인터페이스의 결과를 다음 함수적 인터페이스의 매개값으로 넘겨준 후 최종 처리 결과를 리턴한다.
- Predicate 종류의 함수적 인터페이스는 and( ), or( ), negate( ) 디폴트 메소드를 지닌다.
→ 각각 논리 연산자인 &&, ||, !과 대응된다.
- Predicate 함수적 인터페이스는 isEqual( ) 메소드도 지닌다.
→ test( ) 매개값인 sourceObject와 isEqual( )의 매개값인 targetObject를 equals( )의 매개값으로 제공하고, Objects.equals(sourceObject, targetObject)의 리턴값을 얻어 새로운 Predicate<T>를 생성한다.
Predicate<Object> predicate = Predicate.isEqual(targetObject);
boolean result = predicate.test(sourceObject);
- BinaryOperator<T> 함수적 인터페이스는 minBy( )와 maxBy( ) 정적 메소드를 지닌다.
→ 매개값으로 제공되는 Comparator를 이용해 최대 T와 최소 T를 얻는 BinaryOperator<T>를 리턴한다.
6. 메소드 참조
- 메소드 참조(Method References): 메소드를 참조해서 매개 변수의 정보 및 리턴 타입을 알아내어 람다식에서 불필요한 매개 변수를 제거하는 것
1) 인터페이스의 익명 구현 객체로 생성되므로 타겟 타입에 따라 형태가 달라진다.
2) 정적 메소드와 인스턴스 참조, 매개 변수의 메소드 참조, 생성자 참조 등이 있다.
//메소드 참조
(left, right) -> Math.max(left, right);
IntBinaryOperator operator = Math :: max;
//정적 메소드 참조
클래스 :: 메소드
//인스턴스 메소드 참조
참조변수 :: 메소드
//매개변수의 메소드 참조
(a, b) -> a.instanceMethod(b);
클래스 :: instanceMethod
//생성자 참조
(a, b) -> return new 클래스(a, b);
클래스 :: new
'Java > 이것이 자바다' 카테고리의 다른 글
컬렉션 프레임워크 (0) | 2023.05.02 |
---|---|
이자바 14장(람다식) 확인문제 (0) | 2023.04.28 |
이자바 13장(제네릭) 확인문제 (0) | 2023.04.26 |
제네릭 (0) | 2023.04.25 |
이자바 12장(멀티 스레드) 확인문제 (0) | 2023.04.25 |
댓글