자바의 람다식에 대해 학습하세요.
- 람다식 사용법
- 함수형 인터페이스
- Variable Capture
- 메소드, 생성자 레퍼런스
람다식 사용법
람다식
- JDK 1.8 부터 추가
- 람다식의 도입으로 인해 자바는 객체지향언어인 동시에 함수형 언어가 되었다.
- 메서드를 하나의 식으로 표현한 것
- 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로,
람다식을 익명 함수(anonymous function) 이라고도 한다.
람다식 작성하기
반환타입 메서드이름(매개변수 선언){
...
}
(매개변수 선언) -> {
...
}
int method() {
return (int) ((Math.random() * 5) + 1);
}
()->(int)(Math.random() * 5) + 1;
- 반환값이 있는 메서드의 경우
return문 대신 식으로 표현한다.
식의 연산결과가 자동적으로 반환값이 된다.
끝에 ; 를 붙이지 않는다.
(int a, int b) -> a > b ? a : b
- 선언된 매개변수의 타입은 추론이 가능한 경우 생략 가능하다.
(a, b) -> a > b ? a : b
- 선언된 매개변수가 하나인 경우 괄호()를 생랼할 수 있다
매개변수의 타입이 있다면 괄호()를 생략할 수 없다.
a -> a + a // 정상
int a -> a + a // 비정상
- 괄호{} 안의 문장이 하나일 경우 괄호{}를 생략할 수 있다.
() -> System.out.Println("Test") // 가능
() -> {System.out.Println("Test")} // 가능
함수형 인터페이스
- 람다식은 익명 클래스의 객체와 동등하다.
- 람다식과 아래(익명 클래스의 객체 내부 메소드)는 같다.
- 람다식은 익명 클래스의 객체 내부 메소드를 호출하는 것과 비슷하다.
- 람다는 익명 내부 클래스와 다르다.
(int a , int b) -> a > b ? a : b
new Object() {
int max(int a, int b){
retrun a > b ? a : b;
}
}
익명 내부 클래스 vs 람다식
- 컴파일 시, 익명클래스는 클래스명$1, 클래스명$2 ... 같이 클래스 파일이 생성된다.
- 익명 내부 클래스는 자신을 감싸는 클래스와 다른 Scope 이다.
람다는 자신을 감싸는 클래스와 같은 Scope 이다.
자바에서 람다를 익명클래스로 컴파일 하지 않는 이유
- java9 이전에 람다를 쓰기 위한 retrolambda 같은 라이브러리나, kotlin 같은 언어에서는
컴파일 시점에 람다를 단순히 익명 클래스로 치환한다. - 하지만 익명 클래스로 람다를 사용할 경우 아래와 같은 문제가 발생할 수 있다.
항상 새로운 인스턴스로 할당된다.
람다식마다 클래스가 하나씩 생기게 된다. - 람다식을 사용한 소스의 바이트 코드에는, INVOKEDYNAMIC CALL → INDY 부분이 있다.
- INDY가 호출되게되면 bootstrap 영역의 lambdafactory.metafactory()를 수행하게 된다.
- lambdafactory.metafactory() : java runtime library의 표준화 method
- 어떤 방법으로 객체를 생성할지 dynamically 를 결정한다.
- 클래스를 새로 생성, 재사용, 프록시, 래퍼클래스 등등 성능향상을 위한 최적화된 방법을 사용하게 된다.
- java.lang.invoke.CallSite 객체를 return 한다.
- LambdaMetaFactory ~ 이렇게 되어 있는 곳의 끝에 CallSite 객체를 리턴하게 된다.
- 해당 lambda의 lambda factory, MethodHandle을 멤버변수로 가지게 된다.
- 람다가 변환되는 함수 인터페이스의 인터페이스를 반환한다.
- 한번만 생성되고 재호출시 재사용이 가능하다.
람다식으로 정의된 익명 객체의 메서드를 호출하는 방법
- 참조변수를 만든다.
참조변수의 타입은 클래스 또는 인터페이스가 될 것이다. - 해당 클래스 또는 인터페이스는 람다식과 동등한 메서드가 정의되어 있어야 한다.
타입 f = (int a , int b) -> a > b ? a : b;
interface MyFunction{
public abstract int max(int a, int b);
}
MyFunction f = new MyFunction(){
public int max(int a, int b){
return a > b ? a: b;
}
}
int big = f.max(5, 3);
함수형 인터페이스의 등장
- 하나의 메서드가 선언된 인터페이스를 정의하여
람다식을 다루는 것은 기존 자바의 규칙을 어기지 않으면서 자연스럽다. - 때문에 인터페이스를 통해 람다식을 다루기로 결졍되었으며,
람다식을 다루기 위한 인터페이스를 함수형 인터페이스 (funtional interface)라고 부르기로 했다.
@FunctionalInterface
interface MyFunction{
public abstract int max(int a, int b);
}
- 함수형 인터페이스에서는 오직 하나의 추상 메서드만 정의되어 있어야 한다.
- 다만 static, default 메서드의 개수에는 제약이 없다.
함수형 인터페이스 타입의 매개변수와 반환타입
@FunctionalInterface
interface MyFunction {
void myMethod(); // 추상 메서드
}
- 메서드의 매개변수가 함수형 인터페이스 MyFunction인 경우
이 메서드 호출 시 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다.
void aMethod(MyFunction f) {
f.myMethod(); // MyFunction에 정의된 메서드 호출
}
//1
MyFunction f = () -> System.out.println("myMethod()");
aMethod(f);
//2
aMethod(() -> System.out.println("myMethod()"));
- 메서드의 반환타입이 함수형 인터페이스 타입이라면,
이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나
람다식을 직접 반환할 수 있다.
MyFunction myMethod() {
MyFunction f = () -> {};
//1
return f;
//2
return () -> {};
}
- 메서드를 변수처럼 주고받을 수 있다.
람다식의 타입과 형변환
- 람다식은 익명 객체이고 익명 객체는 타입이 없다.
대입 연산자의 양변의 타입을 일치시키기 위하여 형변환이 필요하다 - () -> {} 람다식은 MyFunction 인터페이스를 직접 구현하지는 않았지만,
인터페이스를 구현한 클래스 객체와 동일하기에 아래와 같은 형변환을 허용한다.
@FunctionalInterface
public interface MyFunction {
void run();
}
public class MyFunctionTest {
public static void main(String[] args) {
MyFunction f = () -> {}; // 형변환 생략 가능 - 정상
MyFunction f1 = (MyFunction) (() -> {}); // 정상
Object obj = (Object) (() -> {}); // 에러
Object obj1 = (Object)(MyFunction) (() -> {}); // 정상
String str = ((Object)(MyFunction) (() -> {})).toString(); // 정상
}
}
- 오직 함수형 인터페이스로만 형변환이 가능하다.
- Object 타입으로는 형변환 할 수 없다.
굳이 Object 타입으로 형변환 하려면, 먼저 함수형 인터페이스로 변환해야 한다.
Variable Capture
Lambda Capturing (람다 캡쳐링)
- 람다식에서 외부 지역변수를 참조하는 행위
- 람다에서 접근 가능한 변수는 세가지 종류가 있다.
1. 지역 변수
2. static 변수
3. 인스턴스 변수 - 지역변수는 변경이 불가능하며, 나머지 변수들은 읽고 쓰기가 가능하다.
- 람다는 지역변수가 존재하는 스택에 직접 접근하지 않고,
지역 변수를 자신(람다가 동작하는 쓰레드)의 스택에 복사한다.
각 쓰레드마다 고유한 스택을 가지고 있어서 지역 변수가 존재하는 쓰레드가 사라져도 람다는 복사된 값을 참조하여 에러가 발생하지 않는다. - 멀티 쓰레드 환경이라면, 여러 개의 쓰레드에서 람다식을 사용하며 람다 캡쳐링이 발생하게 된다.
- 이 때 외부변수 값의 불변성을 보장하지 못하여 동기화 문제가 발생한다.
- 이러한 문제로 지역변수는 final, Effectively Final 제약조건을 갖게 된다.
- 인스턴스 / static 변수는 힙 영역에 위치하고, 힙 영역은 모든 쓰레드가 공유하는 메모리 영역이므로 동기화 문제가 일어나지 않는다.
@FunctionalInterface
public interface MyFunction {
void run();
}
class Outer {
int val = 10;
static int staticVal = 0;
class Inner {
void method(int i) {
int innerval = 30;
MyFunction f = () -> {
System.out.println(i);
System.out.println(val);
System.out.println(innerval);
System.out.println(staticVal);
System.out.println(this.getClass());
val = 456;
innerval = 123; // 에러, 지역변수는 final 제약조건을 갖는다. Variable used in lambda expression should be final or effectively final
staticVal = 789;
};
f.run();
}
}
}
public class CaptureEx {
public static void main(String[] args) {
Outer.Inner testClass = new Outer().new Inner();
testClass.method(100);
}
}
// 100
// 10
// 30
// 0
// class lambdaex.Outer$Inner
- 람다식 내에서 참조하는 지역변수는 final을 붙이지 않아도 상수로 간주된다.
- 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아닌
람다식을 실행한 객체의 참조이므로, this는 중첩 객체인 inner 이다.
java.util.function 패키지
- java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메소드를
함수형 인터페이스로 미리 정의해 놓았다. - 가능하면 이 패키지의 함수형 인터페이스를 사용하자.
기본적인 함수형 인터페이스
- Function의 변형으로 Predicate가 있는데, 반환값이 boolean이 것만 제외하면 Function과 동일하다.
매개변수가 두 개인 함수형 인터페이스
UnaryOperator, BinaryOperator
public class FuncInterfaceTest {
public static void main(String[] args) {
Supplier<Integer> s = () -> (int) (Math.random() * 100) + 1;
Consumer<Integer> c = i -> System.out.print(i + ",");
Predicate<Integer> p = i -> i % 2 == 0;
Function<Integer, Integer> f = i -> i / 10 * 10;
List<Integer> list = new ArrayList<>();
makeRandomList(s,list);
System.out.println(list);
printEvenNum(p,c,list);
List<Integer> newList = doSomeTing(f,list);
System.out.println(newList);
}
static <T> List<T> doSomeTing(Function<T,T> f, List<T> list){
List<T> newList = new ArrayList<>(list.size());
for (T t : list) {
newList.add(f.apply(t));
}
return newList;
}
static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) {
System.out.print("[");
for (T t : list) {
if (p.test(t)) {
c.accept(t);
}
System.out.println("]");
}
}
static <T> void makeRandomList(Supplier<T> s, List<T> list) {
for (int i=0; i < 10; i++){
list.add(s.get());
}
}
}
// [48, 4, 25, 17, 42, 46, 85, 55, 47, 11]
// [48,4,42,46,]
// [40, 0, 20, 10, 40, 40, 80, 50, 40, 10]
기본형을 사용하는 함수형 인터페이스
public class FuncInterfaceTest1 {
public static void main(String[] args) {
IntSupplier s = () -> (int) (Math.random() * 100) + 1;
IntConsumer c = i -> System.out.print(i + ",");
IntPredicate p = i -> i % 2 == 0;
IntUnaryOperator op = i -> i / 10 * 10;
int[] arr = new int[10];
makeRandomList(s,arr);
System.out.println(Arrays.toString(arr));
printEvenNum(p,c,arr);
int[] newArr = doSomeTing(op,arr);
System.out.println(Arrays.toString(newArr));
}
static int[] doSomeTing(IntUnaryOperator f, int[] arr){
int[] newArr = new int[arr.length];
for (int i=0;i<newArr.length;i++) {
newArr[i] = f.applyAsInt(arr[i]);
}
return newArr;
}
static void printEvenNum(IntPredicate p, IntConsumer c, int[] arr) {
System.out.print("[");
for (int t : arr) {
if (p.test(t)) {
c.accept(t);
}
}
System.out.println("]");
}
static void makeRandomList(IntSupplier s, int[] arr) {
for (int i=0; i < arr.length; i++){
arr[i] = s.getAsInt();
}
}
}
// [99, 27, 12, 21, 71, 84, 45, 11, 95, 18]
// [12,84,18,]
// [90, 20, 10, 20, 70, 80, 40, 10, 90, 10]
Function 함수형 인터페이스의 default, static 메서드
- Default - andThen()
- f.andThen(g)
- 함수 f를 먼저 적용하고 그 다음에 함수 g를 적용
- Default - compose()
- f.compose(g)
- andThen()과 반대 함수 g를 먼저 적용하고 그 다음 f를 적용
- Static - identity()
- 이전과 이후가 동일한 '항등함수'가 필요할 때 사용
- x -> x, Function.identity() 두 개가 동일
- 항등 함수는 잘 사용되지 않는 편이며, map()으로 변환작업할 때, 변환없이 그대로 처리하고자 할 때 사용
Predicate 함수형 인터페이스의 default, static 메서드
- 여러 조건식들을 논리 연산자인 &&(and), ||(or), !(not) 으로 연결해서 하나의 식을 구성할 수 있는 것처럼, 여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.
- static 메소드인 isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용한다.
public class PredicateExample {
public static void main(String[] args) {
Function<String, Integer> f = s -> Integer.parseInt(s, 16);
Function<Integer, String> g = i -> Integer.toBinaryString(i);
Function<String, String> h = f.andThen(g);
Function<Integer, Integer> h2 = f.compose(g);
System.out.println(h.apply("EE"));
System.out.println(h2.apply(2));
Function<String, String> f2 = x -> x;
System.out.println(f2.apply("AAA"));
Predicate<Integer> p = i -> i<100;
Predicate<Integer> q = i -> i<200;
Predicate<Integer> r = i -> i%2==0;
Predicate<Integer> notP = p.negate();
Predicate<Integer> all = notP.and(q.or(r));
System.out.println(all.test(150));
String str1 = "ABC";
String str2 = "abc";
Predicate<String> p2 = Predicate.isEqual(str1);
boolean test = p2.test(str2);
System.out.println(test);
}
}
// 11101110
// 16
// AAA
// true
// false
메소드, 생성자 레퍼런스
메소드 참조 (method reference)
- 메소드를 참조하는 방법
1. 클래스이름::메소드이름
2. 참조변수::메소드이름
Function<String, Integer> f = s -> Integer.parseInt(s);
// 메소드 참조
Function<String, Integer> f = Integer::parseInt;
BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equlas(s2);
BiFunction<String, String, Boolean> f = String::equals;
종류 | 람다 | 메소드 참조 |
static 메소드 참조 | x -> ClassName.method(x) | ClassName::method |
인스턴스 메소드 참조 | (obj, x) -> obj.method(x) | ClassName::method |
특정 객체 인스턴스 메소드 참조 | x -> obj.method(x) | obj:method |
생성자의 메소드 참조
Supplier<MyClass> s = () -> new MyClass(); // 람다식
Supplier<MyClass> s = MyClass:new; // 메소드 참조
// 매개변수가 있는 경우
Function<Integer, MyClass> f = i -> new MyClasS(i); // 람다식
Function<Integer, MyClass> f = MyClass:new; // 메소드 참조
BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s); // 람다식
BiFunction<Integer, String, MyClass> bf = MyClass::new; // 메소드 참조
// 배열을 생성하는 방법
Function<Integer, int[]> f = x -> new int[x]; // 람다
Function<Integer, int[]> f2 = int[]::new; // 메소드 참조
class Myclass{
int iv;
public Myclass() {
}
public Myclass(int iv) {
this.iv = iv;
}
}
public class ReferenceExample {
public static void main(String[] args) {
Function<String, Integer> f = s -> Integer.parseInt(s);
Function<String, Integer> f1 = Integer::parseInt;
System.out.println(f1.apply("100") + 100);
Supplier<Myclass> s = () -> new Myclass();
Supplier<Myclass> s1 = Myclass::new;
System.out.println(s1.get());
Function<Integer, Myclass> f2 = Myclass::new;
Myclass m = f2.apply(100);
System.out.println(m.iv);
System.out.println(f2.apply(200).iv);
Function<Integer, int[]> f3 = int[]::new;
System.out.println(f3.apply(10).length);
};
}
// 200
// lambdaex.Myclass@5e9f23b4
// 100
// 200
// 10
'Back-end > java' 카테고리의 다른 글
스터디 14주차 - 제네릭 (0) | 2021.07.08 |
---|---|
스터디 13주차 - I/O (0) | 2021.07.07 |
스터디 12주차 - 어노테이션 (0) | 2021.07.06 |
스터디 11주차 - Enum (0) | 2021.07.05 |
스터디 10주차 - 멀티쓰레드 프로그래밍 (0) | 2021.07.02 |