람다 표현식(Lambda Expression)
람다 표현식(Lambda Expression)은 간단히 말하자면 함수(메서드)를 간단한 함수 '식(expression)'으로 표현하는 방법을 말한다.
// 메서드
int max(int a, int b) {
return a > b ? a : b;
}
람다식으로 표현하면 메서드 타입/이름, 매개변수의 타입 등의 불필요한 코드를 생략할수 있게 되어 코드가 매우 간결해지고 가독성을 높여준다..
// 람다식
(a,b) -> a > b ? a :b;
이러한 특징들은 람다 표현식을 식별자 없이 실행할 수 있는 함수 표현식을 의미하는 익명 함수(이름이 없는 함수, anonymous function) 라고도 한다.
람다 표현식 작성하기
화살표(->) 가호를 사용하여 람다 표현식을 작성할 수 있다.
- 메서드의 이름을 제거하고 '->'를 블록{} 앞에 추가한다.
(매개변수목록) -> { 몸체 }
메서드의 이름과 반환타입을 제거하고 '->'를 블록{} 앞에 추가한다.
(int a, int b) -> {return a > b ? a : b;}
작성 주의사항
1. 매개변수의 타입이 추론 가능하면 생략 가능(대부분의 경우 생략 가능)
(a,b) -> a > b ? a : b
2. 매개변수가 하나인 경우, 괄호 생략 가능(타입이 없을 때만)
a -> a * a // OK
int a -> a * a // 에러
3. 몸체 안의 문장이 하나의 명령문 일 때, 중괄호'{}' 생략 가능(끝에 ';'안 붙임)
(int i) -> System.out.println(i) // OK
(int a, int b) -> return a > b ? a : b // 에러, 괄호 생략 불가(return은 생략 가능)
-단 하나의 명령문이 return문이면 괄호{} 생략 불가
4. 반환값이 있는 경우, 식이나 값만 적고 retun문 생략 가능(끝에 ';'안 붙임)
(int a, int b) -> a > b ? a : b
람다 표현식은 익명 클래스(anonymous class)
함수와 메서드는 근본적으로는 동일한 의미를 가지지만 함수는 일반적인 용어, 메서드는 객체지향개념 용어로 함수는 클래스에 독립적이고 메서드는 클래스안에 작성되어야 하므로 클래스에 종속적이다. 따라서 자바의 메서드는 클래스 안에 작성 되어야 하며 메서드를 단독으로 선언할 수는 없다.
익명 클래스(anonymous class)
익명 클래스(anonymous class)란 내부 클래스(Inner class)로 다른 내부 클래스와 달리 이름을 가지지 않는 클래스를 말한다.
이름이 없다는 것은 중요하지 않아 나중에 다시 사용되지 않다는 것으로 재사용할 필요가 없는 클래스를 굳이 클래스를 정의하고 생성하는 것이 비효율적이기 때문에, 익명 클래스를 통해 코드를 줄이는 기법으로 볼 수 있다.
// 익명 클래스
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("익명 클래스");
}
};
이미 정의되어 있는 클래스의 멤버들을 재정의 하여 사용할 필요가 있을때 그리고 그것이 일회성으로 이용될때 사용하는 기법이다.
- 익명 클래스는 부모 클래스의 자원을 일회성으로 재정의하여 사용하기 위한 용도이다.
- 익명 클래스는 선언과 동시에 객체를 생성하므로, 단 하나의 객체만을 생성하는 일회용 클래스이다.
- 생성자를 선언할 수도 없으며, 오로지 단 하나의 클래스나 인터페이스를 상속받거나 구현할수 있다.
참조변수의타입 참조변수이름 = new 클래스이름(){ ...... }
익명 함수라 불리는 람다 표현식을 보면 자바의 메서드를 단독으로 선언하는 것으로 보이지만, 메서드는 단독으로 선언할 수 없기 때문에 결국 람다식은 객체로 실제로는 익명 클래스의 메서드 표현식을 생략하여 짧게 표현한 것이 람다식이다.
// 람다 표현식
Runnable r2 = () -> System.out.println("람다 표현식");
익명 구현 객체인 람다 표현식을 사용하기 위해서는 객체를 저장하기 위한 참조 변수가 필요하다.
참조변수의타입 참조변수이름 = 람다 표현식
위의 문법처럼 람다 표현식을 하나의 변수에 대입할 때 사용하는 참조 변수의 타입을 함수형 인터페이스라 부른다.
함수형 인터페이스
모든 익명 구현 객체를 람다식으로 표현할 수 있는 것은 아니다. 람다식으로 표현할 수 있는 익명 구현 객체에는 제약이 있다. 인터페이스이며 하나의 추상 메서드 만 선언되어 있는 인터페이스만 가능하다는 것이다.
이 때 필요한 것이 함수형 인터페이스다. 함수형 인터페이스란 단 하나의 추상 메서드가 선언된 인터페이스를 말한다.
- 만약 구현하려는 인터페이스가 두개 이상의 추상 메서드를 가지고 있다면 어떻게 될까?
interface Run{
public abstract void run();
public abstract void walk();
}
- 익명 클래스로 구현한다면 문제가 발생하지 않는다. 하지만 람다식으로 표현 하고자 한다면 이를 코드로 표현할 방법이 없다.
// 익명 클래스
Run run1 = new Run() {
@Override
public void run() {System.out.println("익명 클래스");}
@Override
public void walk() {System.out.print("입니다.");}
};
// 람다 표현식, 오류 발생
Run run2 = () -> System.out.println("람다 표현식");
Jdk 1.8 버전 부터 이용이 가능한 인터페이스의 final, default, static, private 메서드는 추상 메서드가 아니기 때문에,인터페이스에 들어있어도 추상 메서드가 한개라면 함수형 인터페이스로 취급 된다.
java.util.function 패키지
자주 사용되는 다양한 함수형 인터페이스를 제공하는 패키지다.
매개변수가 1개 이하인 함수형 인터페이스
함수형 인터페이스 | 메서드 | 설명 |
java.lang.Runnable | void run() | 매개변수도 없고, 반환값도 없음. |
Supplier<T> | T get() =( T )> | 매개변수는 없고, 반환값만 있음. |
Consumer<T> | =( T )> void accept(T t) | Supplier와 반대로 매개변수만 있고, 반환값이 없음 |
Function<T, R> | =( T )> R apply(T t) =( R )> | 일반적인 함수. 하나의 매개변수를 받아서 결과를 반환 |
Predicate<T> | =( T )> boolean test(T t) =( boolean )> | 조건식을 표현하는데 사용됨. 매개변수는 하나, 반환 타입은 boolean |
Function<Integer, Integer> f = a -> a / 10 * 10;
Predicate <Integer> p = b -> b == 1;
Predicate은 반환타입이 항상 Boolean이기 때문에 Function<T,R>과 달리 반환 타입을 지정해 주지 않는다.
매개변수가 2개인 함수형 인터페이스
Bi는 두개를 의미
함수형 인터페이스 | 메서드 | 설명 |
BiConsumer<T> | =( T, U )> void accept(T t, U u) | 두개의 매개변수만 있고, 반환값이 없음 |
BiFunction<T, U, R> | =( T, U )> R apply(T t, U u) =( R )> | 두 개의 매개변수를 받아서 하나의 결과를 반환 |
BiPredicate<T, U> | =( T, U )> boolean test(T t, U u) =( boolean )> | 조건식을 표현하는데 사용됨. 매개변수는 둘, 반환값은 boolean |
3개 이상의 매개변수가 필요한 경우 만들어 사용해야 한다.
@FunctionalInterface
interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
매개 변수의 타입과 반환타입이 일치하는 함수형 인터페이스
함수형 인터페이스 | 메서드 | 설명 |
UnaryOperator<T> | =( T )> T apply(T t) =( T )> | Function의 자손, Function과 달리 매개변수와 결과의 타입이 같다. |
BinaryOperator<T> | =( T, T )> T apply(T t, T t) =( T )> | BiFunction의 자손, BiFunction과 달리 매개변수와 결과의 타입이 같다. |
import java.util.function.*;
import java.util.*;
class Main {
public static void main(String[] args) {
Supplier<Integer> s = ()-> (int)(Math.random()*100)+1; // 1 ~ 100 난수
Consumer<Integer> c = i -> System.out.print(i+", "); // 1,
Predicate<Integer> p = i -> i%2==0; // 짝수인지 검사
Function<Integer, Integer> f = i -> i/10*10; // i의 일의 자리를 없앤다.
List<Integer> list = new ArrayList<>();
makeRandomList(s, list);
System.out.println(list);
printEvenNum(p, c, list);
List<Integer> newList = doSomething(f, list);
System.out.println(newList);
}
// 제네릭 메서드는 매개변수의 타입에 의해 타입이 추론되어 생략
// 리스트의 모든 요소의 일의자리를 없앤뒤 새로운 리스트에 추가한다. 새로운 리스트 반환
static <T> List<T> doSomething(Function<T, T> f, List<T> list) {
List<T> newList = new ArrayList<T>(list.size());
for(T i : list) {
newList.add(f.apply(i));
}
return newList;
}
// 리스트의 모든 요소를 짝수인지 검사한 뒤 출력
static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) {
System.out.print("[");
for(T i : list) {
if(p.test(i))
c.accept(i);
}
System.out.println("]");
}
// 매개변수로 받은 리스트에 1 ~ 100 사이의 난수를 추가한다.
static <T> void makeRandomList(Supplier<T> s, List<T> list) {
for(int i=0;i<10;i++) {
list.add(s.get());
}
}
}
predicate와 Functuon의 결합
prdicate의 결합
and(), or(), negate() 메서드로 (default메서드) 여러 Predicate를 하나로 결합할 수 있다.
import java.util.Objects;
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {...}
default Predicate<T> negate() {...}
default Predicate<T> or(Predicate<? super T> other) {...}
.....
}
- default, static, private 메서드는인터페이스에 들어있어도 추상 메서드가 한개라면 함수형 인터페이스로 취급된다.
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i % 2 == 0;
- and() : &&, or() : ||, negate() : !
Predicate<Integer> notP = p.negate(); // i >= 100
Predicate<Integer> all = notP.and(q).or(r); // 100 <= i && i < 200 || i % 2 == 0
Predicate<Integer> all2 = notP.and(q.or(r)); // 100 <= i && (i < 200 || i % 2 ==0)
System.out.println(all.test(2)); // true
System.out.println(all2.test(2)); // false
- 등가 비교를 위한 Predicate의 작성에는 isEqual()를 사용한다.(static메서드)
Predicate<String> p = Predicate.isEqual(str1);
Boolean result = p.test(str2);
// Boolean result = Predicate.isEqual(str1).test(str2);
// str1.equals(str2);와 같다.
function의 결합
3판 보기
컬렉션 프레임웍와 함수형 인터페이스
메서드 참조(method reference)
하나의 메서드만 호출하는 람다식은 '메서드 참조'로 간단히 할 수 있다.
종류 | 람다 | 메서드 참조 |
static 메서드 참조 | (x) -> ClassName.method(x) | ClassName::method |
인스턴스 메서드 참조 | (obj, x) -> obj.method(x) | ClassName::method |
특정 객체 인스턴스메서드 참조 | (x) -> obj.method(x) | obj::method |
인스턴스 생성 없이 '클래스이름.메서드이름()'으로 참조할 수 있다.
인스턴스 생성 후, '참조변수.메서드이름()'으로 참조할 수 있다.
static 메서드 참조
// 람다
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
// 메서드 참조
Function<String, Integer> f2 = Integer::parseInt;
함수형 인터페이스의 <입력, 출력>와 같은 정보를 통해 입력하는 값의 매개변수의 타입을 추정알 수 있어 생략이 가능하다.
인스턴스 메서드 참조
class Obj {
// 인스턴스 메서드
int method(String s1) {
return Integer.parseInt(s1);
}
}
// 람다
BiFunction <Obj, String, Integer> bp1 = (obj, x) -> obj.method(x);
// 메서드 참조
BiFunction <Obj, String, Integer> bp2 = Obj::method;
System.out.println(bp2.apply(new Obj(), "10"));
생성자와 메서드 참조
Supplier<Obj> a = () -> new Obj(); // 람다
Supplier<Obj> a1 = Obj::new; // 메서드 참조
배열과 메서드 참조
Function <Integer, int[]> f1 = x ->new int[x]; // 람다
Function <Integer, int[]> f2 = int[]::new; // 메서드 참조
'Java' 카테고리의 다른 글
[JAVA] 스트림(Stream) API(2, 중간 연산, Optinal<T>) (0) | 2024.02.29 |
---|---|
[JAVA] 스트림(Stream) API(1) (0) | 2024.02.25 |
[JAVA] 표준 입출력과 File 클래스, 직렬화(Serialization) (0) | 2024.01.18 |
[JAVA] 버퍼 스트림 (0) | 2024.01.18 |
[JAVA] 자바의 입출력과 스트림(I/O stream) (0) | 2024.01.18 |