Lambda
Lambda
람다식 사용법
람다식 사용법은 다음과 같다.
() -> {}
인자 바디
- 인자가 없을때
- () → {바디}
- 인자가 한개일때
- (a) → {바디}
- a → {바디}
- 인자가 여러개일때
- (a, b) → {바디}
인자의 타입은 컴파일러가 추론하기때문에 생략이 가능하지만 명시할 수도 있다.
(Integer a, String b) → {바디}
바디
- 화살표 오른쪽에 함수 본문을 정의한다.
- 여러 줄인 경우에 {} 를 사용하여 묶는다.
- 한 줄인 경우에 {} 생략가능하며, return도 생략 가능하다.
() -> System.out.println("한 줄일 경우 중괄호 생략"); () -> 10; // return 생략 가능
함수형 인터페이스
👉 추상 메소드를 하나만 가지고 있는 인터페이스
→ SAM (Single Abstract Method)
👉@FuncationInterface
애노테이션을 가지고 있는 인터페이스
함수형 인터페이스란?
함수형 인터페이스는 추상 메소드를 하나만 가지고 있는 인터페이스로,
public interface RunSomething {
void doIt();
}
다음과 같이 두개이상의 추상 메소드를 가지고 있으면 함수형 인터페이스가 아니다.
public interface RunSomething {
void doIt(); // abstract 생략됨.
void doItAgain();
}
그러나, java 8부터 도입된 static 메소드, default 메소드가 있어도 추상메소드가 하나라면 함수형 인터페이스가 맞다.
public interface RunSomething {
void doIt();
static void printName() {
System.out.println("yesol");
}
default void printAge() {
System.out.println("50");
}
}
함수형 인터페이스를 정의할 때 자바에서 기본으로 제공되는 @FunctionalInterface
애노테이션을 사용하면 실수로 추상메소드를 한개 이상 정의했을 때 컴파일 에러를 발생시켜 안전하게 코딩이 가능하다.
@FunctionalInterface // 컴파일 에러
public interface RunSomething {
void doIt();
void doItAgain();
}
함수형인터페이스 사용
- java 8 이전
public class App {
public static void main(String[] args) {
// java 8 이전 익명 내부 클래스 (anonymous inner class)
RunSomething runSomething = new RunSomething() {
@Override
public void doIt() {
System.out.println("Hello, Java");
}
};
runSomething.doIt();
}
}
- java 8 이후
public class App {
public static void main(String[] args) {
RunSomething runSomething = () -> System.out.println("Hello, Java");
runSomething.doIt();
}
}
위 식은 람다 형태로 작성한 것으로 java 8이전의 식과 동작은 동일하다. 람다식은 실질적으로 자바에서는 함수형 인터페이스를 인라인으로 구현한 object로 볼 수 있다.
또한, 자바는 객체지향 언어이기때문에 위 람다식 (() -> System.out.println("Hello, Java")
) 은
- 변수에 할당도 가능하고
RunSomething runSomething = () -> System.out.println("Hello, Java");
- 메서드의 파라미터
- 람다식 자체를 리턴도 가능하다.
- 자바에서 함수형 프로그래밍
- 함수를 First class object 로 사용할 수 있다.
- 순수 함수 (Pure function)
-
사이드 이펙트 만들 수 없다. (함수 밖에 있는 값을 변경하지 못한다.)
-
상태가 없다 (함수 밖에 정의되어 있는)
@FunctionalInterface public interface RunSomething { int doIt(int number); } public static void main(String[] args) { RunSomething runSomething = (number -> { return number + 10; }); System.out.println(runSomething.doIt(1)); System.out.println(runSomething.doIt(1)); System.out.println(runSomething.doIt(1)); }
위 코드는 동일한 값을 넣었을 때 결과가 항상 동일한 코드이다. (상태가 없다)
하지만,
public static void main(String[] args) { int baseNumber = 10; RunSomething runSomething = (new RunSomething() { @Override public int doIt(int number) { return number + baseNumber; } }); }
위 코드는 pure 한 함수가 아니다. 이러한 경우를 상태값을 가지고 있다. 상태값에 의존한다 라고 할 수 있다.
입력 받은 값이 동일한 경우 결과가 항상 같아야 한다. 이것을 보장해주지 못한다면 함수형 프로그래밍이라고 보기 어렵다.
-
- 고차 함수 (High-Order Function)
- 함수가 함수를 매개변수로 받을 수 있고 함수를 리턴할 수도 있다.
- 불변성
자바에서 기본 제공하는 함수형 인터페이스 (java.util.function)
-
Function<T,R>
(T: 입력값 타입, R : 리턴값 타입)T타입을 받아서 R타입을 리턴하는 함수 인터페이스로
R apply(T t)
를 구현하면 된다.
public class Plus50 implements Function<Integer, Integer> { @Override public Integer apply(Integer integer) { return integer + 50; } }
public class App { public static void main(String[] args) { Plus50 plus50 = new Plus50(); System.out.println("plus50.apply(10) = " + plus50.apply(10)); } }
클래스를 지우고 람다식을 이용해서 바로 구현도 가능하다.
public class App { public static void main(String[] args) { Function<Integer, Integer> plus100 = (i -> { return i + 100; }); System.out.println("plus100.apply(9) = " + plus100.apply(9)); } }
- 함수 조합용 메소드
compose
- ()안의 function을 수행하고 나온 결과값을 두번째 function의 입력값으로 조합해준다.
public class App { public static void main(String[] args) { Function<Integer, Integer> plus100 = (i -> i + 100); Function<Integer, Integer> multiply2 = i -> i * 2; Function<Integer, Integer> multiply2AndPlus100 = plus100.compose(multiply2); System.out.println("multiply2AndPlus100.apply(2) = " + multiply2AndPlus100.apply(2)); } }
andThen
- 처음에 작성된 함수 먼저 실행되고 나온 결과값을 토대로 두번째로 작성된 function이 실행된다.
public class App { public static void main(String[] args) { Function<Integer, Integer> plus100 = (i -> i + 100); Function<Integer, Integer> multiply2 = i -> i * 2; Function<Integer, Integer> plusAndMulti = plus100.andThen(multiply2); System.out.println("plusAndMulti.apply(2) = " + plusAndMulti.apply(2)); } }
-
BiFunction<T, U, R>
(T, U : 입력값 타입, R: 리턴값 타입)Function<T,R>
과 동일한데 입력값을 두 개 받는 함수 인터페이스R apply(T t, U u)
public class App { public static void main(String[] args) { BiFunction<Integer, Integer, Integer> sum = (n1, n2) -> { return n1 + n2; }; System.out.println("sum.apply(4, 1000) = " + sum.apply(4, 1000)); } }
-
Consumer<T>
(T :입력값 타입 , 리턴값 없음)T 타입을 받고 아무값도 리턴하지 않는 함수 인터페이스
void Accept(T t)
public class App { public static void main(String[] args) { Consumer<String> printStr = s -> System.out.println(s); printStr.accept("안녕"); } }
- 함수 조합용 메서드
andThen
public class App { public static void main(String[] args) { Consumer<String> printStr = s -> System.out.println(s); Consumer<String> sleep = s -> { System.out.println("두번째 " + s); }; printStr.andThen(sleep).accept("메롱"); } }
-
Supplier<T>
(T : 리턴될 타입)T 타입의 값을 제공하는 함수 인터페이스
T get()
public class App { public static void main(String[] args) { Supplier<Integer> get20 = () -> 20; System.out.println("get20.get() = " + get20.get()); } }
-
Predicate<T>
(T : 입력값 타입, boolean 리턴)T 타입을 받아서 boolean을 리턴하는 함수 인터페이스
boolean test(T t)
public class App { public static void main(String[] args) { Predicate<String> startsWithZero = (s -> s.startsWith("0")); Predicate<Integer> isOdd = (i -> i % 2 !=0); System.out.println("startsWithZero.test() = " + startsWithZero.test("010-7777-7777")); System.out.println("isOdd.test(3) = " + isOdd.test(3)); } }
- 함수 조합용 메소드
and
public class App { public static void main(String[] args) { Predicate<String> startsWithZero = (s -> s.startsWith("0")); Predicate<String> endWithZero = (s -> s.endsWith("0")); System.out.println(endWithZero.and(startsWithZero).test("010-3333-3331")); } }
첫 번째 테스트는 통과했지만 두번째 테스트는 통과 못해서
false
리턴 (둘다 성공해야true
)or
public class App { public static void main(String[] args) { Predicate<String> startsWithZero = (s -> s.startsWith("0")); Predicate<String> endWithZero = (s -> s.endsWith("0")); System.out.println(endWithZero.or(startsWithZero).test("010-3333-3331")); }
첫 번째 테스트만 통과해도
true
(둘 중 하나 성공해야true
)negate
public class App { public static void main(String[] args) { Predicate<Integer> isOdd = (i -> i % 2 !=0); System.out.println(isOdd.negate().test(3)); } }
3은 홀수가 맞지만
negate
사용하면!
와 같은 효과를 나타낸다.
-
UnaryOperator<T>
(T : 입력값 타입, 입력값과 동일한 타입 리턴)Function<T, R>
의 특수한 형태로 입력값 타입과 리턴값 타입이 동일할 때 사용하는 함수 인터페이스이다.public class App { public static void main(String[] args) { Function<Integer, Integer> plus100 = i -> i + 100; UnaryOperator<Integer> plus = i -> i + 100; } }
Function<T, R>
상속
Function<T, R>
을 상속받기 때문에 Function이 제공하는 조합용 메서드도 사용 가능하다. -
BinaryOperator<T>
BiFunction<T, U, R>
이 입력값 두개, 리턴값 세개가 각자 다 다른 타입일 것으로 가정하고 만들어진 함수 인터페이스라면,BinaryOperator<T>
는 동일한 타입의 입력값 두개를 받아 리턴하는 인터페이스이다.
Variable Capture
변수 캡처란 예를들어,
public class VariableCapture {
public static void main(String[] args) {
VariableCapture test = new VariableCapture();
test.run();
}
private void run() {
String testStr = "변수 캡처";
class LocalClass {
void printStr() {
System.out.println(testStr);
}
}
}
}
run 메소드에서 갖고있는 변수 testStr
은 run 메소드 실행이 완료되면 사라진다. 그런데 run 메소드 안의 로컬클래스인 LocalClass
에서 testStr
를 사용해야 하니까 자바에서는 복사해오는데 이것이 변수 캡처이다.
이것은 내부클래스에 한한것은 아니고 저 상황이 일어날 수 있는 상황은 여러가지가 있다. (익명클래스, 내부클래스 …)
-
익명클래스 변수 캡쳐
public class VariableCapture2 { public static void main(String[] args) { VariableCapture2 test = new VariableCapture2(); test.run(); } private void run() { String testStr = "변수 캡처"; Consumer<Integer> integerConsumer = new Consumer<Integer>() { @Override public void accept(Integer integer) { System.out.println(testStr); } }; } }
-
람다 변수 캡쳐
public class VariableCapture2 { public static void main(String[] args) { VariableCapture2 test = new VariableCapture2(); test.run(); } private void run() { String testStr = "변수 캡처"; IntConsumer intConsumer = (i) -> System.out.println(testStr + i); intConsumer.accept(3); } }
자바 8 이전에는 로컬 변수를 참조할 때 final
키워드를 사용해야 변수 캡처를 할 수 있었다.
private void run() {
final String testStr = "변수 캡처"; // 자바 8 이전 에는 final 키워드 필수
class LocalClass {
void printStr() {
System.out.println(testStr);
}
}
}
final
키워드를 사용하지 않은 경우 concurrency 문제가 생길 수 있어서 컴파일러가 방지한다.
-
concurrency 문제
동일한 객체에 대해 공유하고 추후 그 객체가 변경되는 경우 예기치 않은 결과를 초래할 수 있고, 객체를 동시에 변경하면 손상 또는 일관성이 없는 상태가 될 수 있음.
그러나, 자바 8부터는 effective final 을 지원해 final
키워드를 사용하지 않은 변수를 로컬클래스, 익명 클래스 구현체 또는 람다에서 참조할 수 있다.
private void run() {
String testStr = "변수 캡처"; // 자바 8 이후 effective final인 경우 키워드 생략 가능
class LocalClass {
void printStr() {
System.out.println(testStr);
}
}
}
effective final 사실상 final이란 변수가 추후 변경되지 않는 상황의 변수를 지칭한다.
그러면 나중에 변수가 변경된다면 ? → effective final
이 아니라면?
public class VariableCapture2 {
public static void main(String[] args) {
VariableCapture2 test = new VariableCapture2();
test.run();
}
private void run() {
int baseNumber = 10;
class LocalClass {
void printNumber() {
System.out.println(baseNumber); // 컴파일 에러 발생
}
}
Consumer<Integer> consumer = new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(baseNumber); // 컴파일 에러 발생
}
};
IntConsumer printInt = (i) -> {
System.out.println(i + baseNumber); // 컴파일 에러 발생
};
baseNumber++;
LocalClass localClass = new LocalClass();
localClass.printNumber();
consumer.accept(1);
}
}
Variable ‘baseNumber’ is accessed from within inner class, needs to be final or effectively final 같은 컴파일 에러가 발생된다.
람다 vs 익명클래스 vs 로컬클래스
- 공통점
- Variable Capture 지원
- effective final 지원
- 차이점
- 쉐도잉
- 익명클래스, 로컬클래스 쉐도잉 된다.
- 왜? 각각 다른 스콥을 가지고 있기 때문에
- 람다는 쉐도잉 안된다.
- 왜? 람다는 해당 람다식을 구현한 곳(ex. 메소드)과 같은 스콥을 갖고 있기 때문에
- 익명클래스, 로컬클래스 쉐도잉 된다.
- 쉐도잉
쉐도잉
public class VariableCapture2 {
public static void main(String[] args) {
VariableCapture2 test = new VariableCapture2();
test.run();
}
private void run() {
int baseNumber = 10;
class LocalClass {
void printNumber() {
int baseNumber = 11;
System.out.println(baseNumber);
}
}
Consumer<Integer> consumer = new Consumer<Integer>() {
int baseNumber = 13;
@Override
public void accept(Integer integer) {
System.out.println(baseNumber);
}
};
LocalClass localClass = new LocalClass();
localClass.printNumber();
consumer.accept(1);
}
}
run 메소드의 지역변수인 baseNumber
의 값은 10이었는데 로컬클래스, 익명클래스와 run 메소드는 서로 다른 scope 에 있기 때문에 가려져서 11과 13이 출력되는 것이다. 이것을 쉐도잉 이라한다.
하지만 람다는 run() 메소드와 같은 scope에 있어서 쉐도잉이 불가능하다.
메소드, 생성자 레퍼런스
method reference는 람다 표현식이 단 하나의 메소드만을 호출하는 경우에 해당 람다 표현식에서 불필요한 매개변수를 제거하고 사용할 수 있도록 해준다.
메소드 참조를 사용하면 불필요한 매개변수를 제거하고 ‘::
’ 기호를 사용하여 표현할 수 있다.
✏️ - 스태틱 메소드 참조
클래스타입::스태틱메소드이름
- 특정 객체의 인스턴스 메소드 참조
참조변수타입::인스턴스메소드이름
- 임의 객체의 인스턴스 메소드 참조
타입::인스턴스메소드이름
- 생성자 참조
타입::new
다음과 같은 클래스가 있을 때,
public class Greeting {
private String name;
public Greeting() {
}
public Greeting(String name) {
this.name = name;
}
public String hello(String name) {
return "hello " + name;
}
public static String hi(String name) {
return "hi " + name;
}
}
-
스태틱 메소드 참조
UnaryOperator<String> hi = Greething::hi; hi.apply("java");
-
특정 객체의 인스턴스 메소드 참조
Greeting greeting = new Greeting(); UnaryOperator<String> hello = greeting::hello; hello.apply("java");
-
임의 객체의 인스턴스 메소드 참조
String[] names = {"younghee", "dongsu", "gildong"}; Arrays.sort(names, (o1, o2) -> 0);
위와 같은 코드가 가능한 것은
sort
의 두번째 인자가 Comparator로 함수형 인터페이스이기때문에 그 자리에 람다를 넣을 수 있는것이다.람다를 넣을 수 있다는 말은 메소들 레퍼런스를 사용할 수 있다는 말로, 다음과 같은 코드가 가능하다.
public class App { public static void main(String[] args) { String[] names = {"younghee", "dongsu", "gildong"}; Arrays.sort(names, String::compareToIgnoreCase); System.out.println(Arrays.toString(names)); } }
younghee
뒤에 오는dongsu
를compareToIgnoreCase
의 파라미터로 넘겨서 int값을 리턴dongsu
와gildong
과 비교하여 int값 리턴
→ 임의의 인스턴스들 (
younghee
,dongsu
,gildong
) 를 거쳐가며 인스턴스 메소드 사용(compareToIgnoreCase
) -
생성자 레퍼런스
- default 생성자 (입력값 x)
Function<String, Greeting> newGreeting = Greeting::new; Greeting greetingInstance = newGreeting.get();
- 매개변수 받는 생성자 (입력값 o)
Funtion<String, Greeting> dongsuGreeting = Greeting::new; Greeting dongsu = dongsuGreeting.apply("dongsu"); System.out.println(dongsu.getName());
Reference
https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html