객체지향 + 함수적 프로그래밍
- 자바는 객체 지향 흐로그래밍이 소프트웨어 개발의 주요 패러다임이었던 1990년대에 디자인되었다.
- 최근들어 함수적 프로그래밍이 다시 부각되고 있는데, 병렬 처리와 이벤트 지향 프로그래밍에 적합하기 때문이다.
- 객체 지향 프로그래밍과 함수적 프로그래밍을 혼합하여 더욱 효율적인 프로그래밍이 될 수 있도록 개발 언어가 변화하고 있다.
Lambda Expressions
- 함수적 프로그래밍을 위해 자바 8부터 람다식을 지원한다.
- 익명 함수를 생성하기 위한 식으로, 객체지향 언어보다는 함수 지향 언어에 가깝다.
- 자바에서 람다식을 수용한 이유 : 자바 코드가 매우 간결해지고, 컬렉션 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 집계할 수 있기 때문
- 람다식 -> 매개변수를 가진 코드 블록 -> 익명 구현 객체
- 예시
1
2
3
| Runnable runnable = new Runnable() { // 익명 구현 객체
public void run() {...}
}
|
람다식으로 표현
1
| Runnable runnable = () -> {...} // 람다식
|
(매개변수) -> {실행코드}
형태로 작성되는데, 함수 정의 형태를 띄고 있지만, 런타임시에는 인터페이스의 익명 구현 객체로 생성된다.
- 어떤 인터페이스를 구현할 것인가는 대입되는 인터페이스가 무엇이냐에 달려있다.
- 위의 코드는 Runnable 변수에 대입되기 때문에 람다식은 Runnable의 익명 구현 객체를 생성하게 된다.
람다식 기본 문법
(타입 매개변수, ...) -> { 실행문; ...}
() -> {실행문}
(x, y) -> { return x+y; }
(x, y) -> x + y
타겟 타입과 함수적 인터페이스
- 람다식의 형태는 마치 자바의 메소드를 선언하는 것 처럼 보인다.
- 자바는 메소드를 단독 선언이 불가능하고, 항상 클래스의 구성 멤버로 선언한다.
- 따라서 람다식은 단순히 메소드를 선언하는 것이 아니라 이 메소드를 가지고 있는 객체를 생성해낸다.
인터페이스 변수 = 람다식;
- 람다식은 인터페이스 변수에 대입된다.
- 람다식은 인터페이스의 익명 구현 객체를 생성한다는 뜻이 된다.
- 인터페이스는 직접 객체화할 수 없기 때문에 구현 클래스가 필요한데, 람다식은 익명 구현 클래스를 생성하고 객체화한다.
- 람다식은 대입될 인터페이스의 종류에 따라 작성 방법이 달라지기 때문에, 람다식이 대입될 인터페이스를 람다식의
target type
이라 한다.
함수적 인터페이스
- 람다식은 하나의 메소드를 정의하기 때문에 두 개 이상의 추상 메소드가 선언된 인터페이스는 람다식을 통해서 구현 객체를 생성할 수 없다.
함수적 인터페이스
, functional interface
: 하나의 추상 메소드가 선언된 인터페이스만이 람다식의 타겟 타입이 될 수 있고, 이를 부르는 말.
@FunctionalInterface
어노테이션을 붙여서 함수적 인터페이스를 작성할 때 두 개 이상의 추상 메소드가 선언되지는 않았는지 컴파일러가 체크해준다.
- 어노테이션은 없어도 되지만 실수로 2개 이상의 추상 메소드를 선언하면 어노테이션이 컴파일 오류를 발생시키기 때문에 붙여주는 것이 좋다.
매개 변수와 리턴값이 없는 람다식
1
2
3
4
| @FunctionalInterface
public interface myFunctionalInterface {
public void method();
}
|
이 인터페이스를 타겟 타입으로 갖는 람다식을 작성하는 방법
1
| myFunctionalInterface fi = () -> {...}
|
method()가 매개변수를 가지지 않기 때문에 매개변수가 없다.
람다식이 대입된 인터페이스의 참조변수는 다음과 같이 method()를 호출할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| 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();
}
}
|
매개 변수가 있는 람다식
1
2
3
4
| @FunctionalInterface
public interface myFunctionalInterface {
public void method(int x);
}
|
이 인터페이스를 타겟 타입으로 갖는 람다식을 작성하는 방법
1
2
3
| myFunctionalInterface fi = (x) -> {...}
// 또는
myFunctionalInterface fi = x -> {...}
|
리턴값이 있는 람다식
1
2
3
4
| @FunctionalInterface
public interface myFunctionalInterface {
public void method(int x, int y);
}
|
이 인터페이스를 타겟 타입으로 갖는 람다식을 작성하는 방법
1
2
| myFunctionalInterface fi = (x, y) -> {...; return 값;}
// return문만 있을 경우 중괄호와 return 생략 가능
|
클래스 멤버와 로컬 변수 사용
- 람다식 실행 블록에는 클래스의 멤버(필드 및 메소드)와 로컬 변수를 사용할 수 있다.
- 클래스의 멤버는 제약 없이 사용이 가능하다.
- 로컬 변수는 제약 사항이 따른다.
클래스의 멤버 사용
- 일반적으로 익명 객체 내부에서
this
는 익명 객체의 참조지만, 람다식에서 this
는 내부적으로 생성되는 익명 객체의 참조가 아니라 람다식을 실행한 객체의 참조다.
- 아래 예시에서는 중첩 객체 Inner에서 람다식을 실행했기 때문에, 람다식 내부에서
this
는 중첩 객체 Inner다.
1
2
3
| public interface MyFunctionalInterface {
public void method();
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class UsingThis {
public int outterField = 10;
class Inner {
int innerField = 20;
void method() {
// 람다식
MyFunctionalInterface fi = () -> {
System.out.println(outterField);
System.out.println(ThisExample.this.outterField);
// 바깥 객체의 참조를 얻기 위해서는 클래스명.this를 사용
System.out.println(innerField);
System.out.println(this.innerField);
// 람다 내부에서 this는 Inner객체를 참조
};
fi.method();
}
}
}
|
1
2
3
4
5
6
7
| public class UsingThisExample {
public static void main(String[] args) {
UsingThis usingThis = new UsingThis();
UsingThis.Inner inner = usingThis.new Inner();
inner.method();
}
}
|
로컬 변수 사용
- 람다식은 메소드 내부에서 주로 작성되기 때문에 로컬 익명 구현 객체를 생성시킨다고 봐야 한다.
- 람다식에서 메소드의 매개변수 또는 로컬 변수를 사용하면 이 두 변수는
final
특성을 가져야 한다.
- 따라서 매개변수 또는 로컬 변수를 람다식에서 읽는 것은 허용되지만, 람다식 내부 또는 외부에서 변경될 수는 없다.
표준 API의 함수적 인터페이스
- 자바에서 제공되는 표준 API에서 한 개의 추상 메소드를 가지는 인터페이스들은 모두 람다식을 이용해서 익명 구현 객체로 표현이 가능하다.
- 예) 스레드의 작업을 정의하는 Runnable interface
1
2
3
4
5
6
7
8
9
10
11
12
| public class RunnableExample {
public static void main(String[] args) {
Runnable runnable = () -> {
for (int i = 0; i < 10; i++) {
System.out.prinln(i);
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
|
Thread 생성자를 호출할 때 람다식을 매개값으로 대입할 수도 있다.
1
2
3
4
5
| Thread thread = new Thread() -> {
for (int i = 0; i < 10; i++) {
System.out.prinln(i);
}
}
|
- 자바 8부터 빈번하게 사용되는 함수적 인터페이스
functional interface
는 java.util.function
표준 API패키지로 제공한다.
- 이 패키지가 함수적 인터페이스를 제공하는 목적은 메소드 똔느 생성자의 매개타입으로 사용되어서 람다식을 대입할 수 있도록 하기 위해서다.
java.util.function 패키지의 함수적 인터페이스 구분
구분 기준 : 인터페이스에 선언된 추상 메소드의 매개값, 리턴값의 유무
종류 |
추상메소드 특징 |
Consumer |
매개값 O 리턴값 X |
Supplier |
매개값 X 리턴값 O |
Function |
매개값 O 리턴값 O (주로 매개값을 리턴값으로 매핑하여 타입변환) |
Operator |
매개값 O 리턴값 O (주로 매개값을 연산하고 결과를 리턴) |
Predicate |
매개값 O 리턴값 boolean (매개값을 조사해서 true/false를 리턴) |
Consumer 함수적 인터페이스
- 리턴값이 없는
accept()
메소드를 가지고 있다.
accept()
: 단지 매개값을 소비하는 역할만 한다. (사용만 하고 리턴이 없다는 뜻)
Consumer<T>
인터페이스
- 추상메소드
void accept(T t)
- 객체 T를 받아 소비
1
| Consumer<String> consumer = t => { t를 소비하는 실행문; };
|
- 타입 파라미터 T에 String이 대입되었기 때문에 람다식의 t 매개 변수타입은 String이 된다.
BiConsumer<T,U>
인터페이스
- 추상메소드
void accept(T t, U u)
- 객체 T와 U를 받아 소비
1
| BiConumer<String, String> conumer = (t, u) -> { t와 u를 소비하는 실행문; }
|
DoubleConsumer
인터페이스
- 추상메소드
void accept(double value)
- double 값을 받아 소비
1
| DoubleConsumer consumer = d -> { d를 소비하는 실행문; }
|
- 매개값으로 double 하나를 가지므로 람다식도 한 개의 매개변수 사용. d는 고정적으로 double타입이 된다.
ObjIntConsumer<T>
인터페이스
- 추상메소드
void accept(int value)
- int값을 받아 소비
1
| ObjIntConsumer<String> consumer = (t, i) -> { t와 i를 소비하는 실행문; }
|
Consumer 사용 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| 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 + "16");
Consumer.accept("java");
// Java16
BiConsumer<String, String> bigConsumer = t -> System.out.println(t + u);
BiConsumer.accept("java", "15");
// Java16
DoubleConsumer doubleConsumer = d -> System.out.println("Java" + d);
Consumer.accept(16.0);
// Java16.0
ObjIntConsumer<String> objIntConsumer = (t, i) -> System.out.println(t + i);
ObjIntConsumer.accept("Java"+16);
// Java16
}
}
|
Supplier 함수적 인터페이스
- 매개변수가 없고 리턴값이 있는
getXXX()
메소드를 가지고 있다.
Supplier<T>
인터페이스
1
| Supplier<String> supplier = () -> { ...; return "문자열"; }
|