람다식은 익명 함수를 생성하기 위한 식으로 객체 지향 언어보다 함수지향 언어에 가깝다.
🚩 람다식의 형태는 매개 변수를 가진 코드 블록이지만, 런타임 시에는 익명 구현 객체를 생성한다.
람다식 → 매개 변수를 가진 코드 블록 → 익명 구현 객체 (매개변수) -> {실행코드}
원래 코드가 다음과 같다면
Runnalbe runnable = new Runnable() { ┐
public void run() { ... } │ 익명 구현 객체 ( new 이후부터 }; 까지를 말함 )
}; ┘
람다식으로 표현하면 다음과 같다.
Runnable runnable = () -> { ... } //람다식
✅ 함수적 스타일의 람다식
(타입 매개변수, ...) -> { 실행문; ... }
(타입 매개변수, ...)
는 오른쪽 { 중괄호 } 블록을 실행하기 위해 필요한 값을 제공한다.->
기호는 매개 변수를 이용해서 { 중괄호 } 를 실행한다는 뜻이다.ex) int 매개 변수 a의 값을 콘솔에 출력하는 람다식
(int a) -> { System.out.println(a); }
💡 매개 변수 타입은 런타임 시에 대입되는 값에 따라 자동으로 인식될 수 있기 때문에 람다식에서는 매개 변수의 타입을 일반적으로 언급하지 않는다.
(a) -> { System.out.println(a); }
💡 하나의 매개 변수만 있다면 ( 괄호 ) 를 생략할 수 있고, 하나의 실행문만 있다면 { 중괄호 } 도 생략할 수 있다.
a -> System.out.println(a)
💡 만약 매개 변수가 없다면 람다식에서 매개 변수 자리가 없어지므로 ( 빈 괄호 ) 를 반드시 사용해야 한다.
() -> { 실행문; ... }
💡 { 중괄호 } 를 실행하고 결과값을 리턴해야 한다면 다음과 같이 return문을 쓸 수 있다.
(x, y) -> { return x+y; };
💡 { 중괄호 } 에 return 문만 있을 경우, return문을 사용하지 않고 다음과 같이 작성하는 것이 정석이다.
(x, y) -> x+y
⚠️ 자바는 메소드를 단독으로 선언할 수 없고 항상 클래스의 구성 멤버로 선언하기 때문에 람다식은 단순히 메소드를 선언하는 것이 아니라 이 메소드를 가지고 있는 객체를 생성해 낸다.
⚠️ 람다식은 하나의 메소드를 정의하기 때문에, 두 개 이상의 추상 메소드의 구현 객체를 생성할 수 없다 !
하나의 추상 메소드가 선언된 인터페이스는 람다식의 타겟 타입이 될 수 있는데, 이러한 인터페이스를 함수적 인터페이스라고 한다.
두 개 이상의 추상 메소드가 선언되지 않도록 컴파일러가 체킹해주는 기능
☞ @FunctionalInterface
✅ 코드 형태
클래스/인터페이스 변수명 = () -> { ... }
ex) 매개 변수와 리턴값이 없는 추상 메소드를 가진 함수적 인터페이스가 있다고 가정해보자.
@FunctionalInterface
public 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();
}
}
✅ 코드 형태
클래스/인터페이스 변수명 = (매개변수) -> { ... }
ex) 매개 변수가 있고 리턴값이 없는 추상 메소드를 가진 함수적 인터페이스가 있다고 보자.
@FunctionalInterface
public interface MyfunctionalInterface {
public void method(int x);
}
public class MyfunctionalInterfaceExample {
public static void main(String... args) {
MyfunctionalInterface fi;
fi = (x) -> {
int result = x*5;
System.out.println(result);
};
fi.method(2);
fi = (x) -> {
System.out.println(x*5);
};
fi.method(2);
};
ex) 매개 변수와 리턴값이 있는 추상 메소드를 가진 함수적 인터페이스가 있다고 보자.
@FunctionalInterface
public interface MyfunctionalInterface {
public int method(int x, int y);
}
public class MyfunctionalInterfaceExample {
public static void main(String... args) {
MyfunctionalInterface 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));
}
람다식의 실행 블록에는 클래스의 멤버 (필드와 메소드) 및 로컬 변수를 사용할 수 있다. 여기서 클래스 멤버는 제약 사항이 없지만, 로컬 변수는 제약 사항이 따른다.
⚠️ 람다식에서
this
는 람다식을 실행한 객체의 참조가 된다.
public interface MyFunctionalInterface {
public void method();
}
public class UsingThis {
public int outterField = 10;
class Inner {
int innerField = 20;
void method() {
//람다식
MyFunctionalInterface fi = () -> {
System.out.println("outterField = " + outterField);
/* 바깥 객체의 참조를 얻기 위해 -> 클래스명.this */
System.out.println("UsingThis.this.outterField = " + UsingThis.this.outterField + "\n");
System.out.println("innerField = " + innerField);
/* 람다식 내부에서 this는 -> inner 객체 참조 */
System.out.println("this.innerField = " + this.innerField + "\n");
};
fi.method();
}
}
}
public interface MyFunctionalInterface {
public void method();
}
람다식은 메소드 내부에서 주로 작성되기 때문에 로컬 익명 구현 객체를 생성시킨다고 봐야 한다.
⚠️ 람다식에서 메소드의 매개 변수 또는 로컬 변수를 사용하기 위해선
final
을 사용해야 한다.
☞ 매개 변수나 로컬 변수는 메소드 실행이 끝나면 스택 메모리에서 사라지기 때문에 익명 객체에서 사용할 수 없게 된다 ! 따라서 final
키워드를 선언하여 메소드 내부에 지역변수로 복사되게 함으로써 익명 객체를 사용할 수 있다.
매개 변수 또는 로컬 변수를 람다식에서 읽는 것은 허용되지만, 람다식 내부 또는 외부에서 변경할 수 없다 !
public interface MyFunctionalInterface {
public void method();
}
public class UsingLocalVariable {
public void method(int arg) {
int localVar = 40;
/* final 특성 때문에 수정 불가 */
//arg = 50;
//localVar = 50
//람다식
MyFunctionalInterface fi = () -> {
System.out.println("arg = " + arg);
System.out.println("localVar: " + localVar + "\n");
};
fi.method();
}
}
public class UsingLocalVariableExample {
public static void main(String... args) {
UsingLocalVariable ulv = new UsingLocalVariable();
ulv.method(20);
}
}
메소드 참조(Method Referenece)는 메소드를 참조해서 매개 변수의 정보 및 리턴 타입을 알아내어, 람다식에서 불필요한 매개 변수를 제거하는 것이 목적이다.
💡 람다식은 종종 기존 메소드를 단순히 호출하는 경우가 많다.
예를 들어, 두 개의 값을 받아 큰 수를 리턴하는 Math 클래스의 max() 정적 메소드를 호출하는 람다식은 다음과 같다.
(left, right) -> Math.max(left, right);
람다식은 단순히 두 개의 값을 Math.max() 메소드의 매개값으로 전달하는 역할만 하기 때문에, 다음과 같이 메소드 참조를 이용하면 깔끔하게 처리할 수 있다.
Math :: max; //메소드 참조
메소드 참조는 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되므로 타겟 타입인 인터페이스의 추상 메소드가 어떤 매개 변수를 가지고, 리턴 타입이 무엇인가에 따라 달라진다.
💡 메소드 참조는 정적 또는 인스턴스 메소드를 참조할 수 있고, 생성자 참조도 가능하다.
✅ 코드형태 - 정적(static) 메소드를 참조할 경우
클래스 :: 메소드
✅ 코드형태 - 인스턴스 메소드를 참조할 경우
참조변수 :: 메소드
ex) Calculator의 정적 및 인스턴스 메소드를 참조해보자
public class Calculator {
public static int staticMethod(int x, int y) {
return x+y;
}
public int instanceMethod(int x, int y) {
return x+y;
}
}
public class MethodReferenceExample {
public static void main(String... args) {
IntBinaryOperator operator;
//정적 메소드 참조
operator = (x,y) -> Calculator.staticMethod(x,y);
System.out.println("Result1 = " + operator.applyAsInt(1, 2));
operator = Calculator :: staticMethod;
System.out.println("Result2 = " + operator.applyAsInt(3, 4));
//인스턴스 메소드 참조
Calculator obj = new Calculator();
operator = (x,y) -> obj.instanceMethod(x, y);
System.out.println("Result3 = " + operator.applyAsInt(5, 6));
operator = obj :: instanceMethod;
System.out.println("Result4 = " + operator.applyAsInt(7, 8));
}
}
✅ 코드 형태
클래스 :: 메소드 //정적 메소드 참조와 동일하지만 전혀 다른 코드가 실행된다.
ex) 두 문자열이 대소문자 구분없이 동일한 알파벳으로 구성되어 있는지 비교해보자.
public class ArgumentMethodReferenceExample {
public static void main(String... args) {
ToIntBiFunction<String, String> function;
function = (a,b) -> a.compareToIgnoreCase(b);
print(function.applyAsInt("Java8", "JAVA8"));
/* 메소드 참조 표현 */
function = String::compareToIgnoreCase;
print(function.applyAsInt("Java8", "JAVA8"));
}
private static void print(int order) {
if(order < 0) {
System.out.println("사전 순으로 먼저 옵니다.");
}
else if (order == 0) {
System.out.println("동일한 문자열입니다.");
}
else {
System.out.println("사전순으로 나중에 옵니다.");
}
}
}
생성자를 참조한다는 것은 객체 생성이 가능하다는 것을 의미한다.
단순히 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치할 수 있다.
✅ 코드 형태
클래스 :: new
ex) 생성자 참조를 이용해 객체를 생성해 보자.
public class ConstructorReferenceExample {
public static void main(String... args) {
/* 생성자 참조 - 매개변수 1개 */
Function<String, Member> function1 = Member::new;
Member member1 = function1.apply("angel");
/* 생성자 참조 - 매개변수 2개 */
BiFunction<String, String, Member> function2 = Member::new;
Member member2 = function2.apply("신천사", "angel");
}
}
public class Member {
private String name;
private String id;
public Member() {
System.out.println("Member() 실행");
}
public Member(String id) {
System.out.println("Member(String id) 실행");
}
public Member(String name, String id) {
System.out.println("Member(String name, String id) 실행");
this.name = name;
this.id = id;
}
}