익명 클래스
- Inner class, 이름이 없는 클래스를 말한다
- 클래스 정의와 동시에 객체를 생성할 수 있다
- Java의 Interface와 Class 모두 익명 함수로 객체를 만들 수 있다
익명 함수를 사용하는 이유
- 프로그램 내에서 한 번만 객체로 만드는 데 사용되는 클래스의 경우 클래스를 정의하고 생성하는 것이 비효율적이기 때문이다
- 익명 함수로 클래스 정의와 구현을 동시에 하여 코드 길이를 줄일 수 있다
- Runnanle이나 Event Listener 객체를 생성하는데 주로 사용된다
1) 익명 함수로 Interface 구현 및 객체 생성하기
Java의 Interface나 Abstract 클래스는 익명 함수로 구현과 동시에 객체를 생성할 수 있다
다음과 같은 방식으로 익명 클래스의 객체를 생성할 수 있다
new InterfaceName() { ... }
이번엔 익명 인터페이스 예시를 보자
먼저 인터페이스를 정의한다
interface MyInterfae {
void doSomething();
}
아래와 같이 익명 함수를 사용하여 클래스 구현과 동시에 객체를 생성할 수 있다
myClass 객체는 이름이 없는 클래스로 만들어진 객체이다
일반적으로 한번만 생성하고 재생성되지 않는 객체를 만들 때 익명 함수를 사용한다
public class Example {
interface MyInterface {
void doSomething();
}
public static void main(String[] args) {
MyInterface myClass = new MyInterface() {
@Override
public void doSomething() {
System.out.println("doSomething");
}
};
myClass.doSomething();
}
}
Output:
doSomething
2) 익명 함수로 클래스 구현 및 객체 생성
익명 함수를 사용하면 Parent 클래스를 상속하여 클래스를 정의하고 객체를 생성할 수 있다
Syntax는 다음과 같다
new ParentClass() { ... }
이제 익명 클래스 생성 예시를 보자
다음과 같이 Parent 클래스를 정의한다
💡 abstract로 만들어도 되고, 일반 클래스로 만들어도 된다
public abstarct class MyAbstractClass {
public abstract void doSomething();
}
다음과 같이 클래스 구현과 동시에 객체를 생성할 수 있다
public class Example2 {
public static abstract class MyAbstractClass {
public abstract void doSomething();
}
public static void main(String[] args) {
MyAbstractClass myClass = new MyAbstractClass() }
@Override
public void doSomething() {
System.out.println("doSomething");
}
};
myClass.doSomething();
}
}
3) 익명 함수에서 상수 및 변수 참조
다음과 같이 익명 함수 내에 멤버 변수는 참조할 수 있다
MyInterface myClass = new MyInterface() {
private int bb = 0;
@Overrid
public void doSomething() {
System.out.println("doSomething: " + bb);
}
};
static의 경우 static final 로 선언한 상수는 참조 가능하지만, final이 아닌 static 변수는 참조할 수 없고 컴파일 에러가 발생한다
MyInterface myClass = new MyInterface() {
static final int AA = 0;
// static int aa = 0; // compile error
@Override
public void doSomething() {
System.out.println("doSomething: " + AA);
}
};
익명 클래스의 외부의 변수는 final 또는 Effectvely final 만 참조할 수 있다
아래의 코드는 컴파일이 된다
int aa = 0;
final int bb = 0;
MyInterface myClass = new MyInterface() {
private int bb = 0;
@Override
public void doSomething() {
System.out.println("doSomething: " + aa);
System.out.println("doSomething: " + bb);
}
};
람다식 (Lambda expression)
람다식은 Java 8에 도입되었으며, 함수형 인터페이스 객체를 표현한다
즉, 람다식은 추상 메소드(인터페이스)를 구현하는데, 이름이 없기 때문에 익명 함수(Anonymous Function)와 비슷하다
람다식은 익명 함수(클래스, 객체)를 사용하는 것보다 더 적은 코드로 동일한 내용을 구현할 수 있다
함수형 프로그래밍을 할 때 람다식으로 코드를 간결하게 하고 가독성을 향상시킬 수 있다
1) 람다식과 익명 함수 차이점
public class Example {
interface Math {
int sum(int n1, int n2);
}
static class MyMath implements Math {
public int sum(int n1, int n2) {
reutrn n1 + n2;
}
}
static int doSomething(Math math) {
return math.sum(10, 20);
}
pulbic static void main(String[] args) {
Math math = new MyMath();
int result = doSomething(math);
System.out.println(result); // 30
}
}
위의 예제는 익명 함수를 사용하여 동일한 내용을 구현한 예제이다
한번만 사용되는 객체를 만들기 위해 클래스를 구현하지 않아도 되기 때문에 익명 객체는 클래스 이름이 없다
public class Example2 {
interface Math {
int sum(int n1, int n2);
}
static int doSomething(Math math) {
return math.sum(10, 20);
}
public static void main(Stirng[] args) {
int result = doSomething((n1, n2) -> n1 + n2); // Lambda
System.out.println(result); // 30
}
}
위의 예제는 람다식을 사용하여 동일한 내용을 다시 구현한 것이다
익명 함수와 매우 비슷하지만 불필요한 선언부들이 모두 생략되어 있고, 인터페이스 함수의 구현부만 정의하고 있어 코드가 가장 간결하다
2) 람다식의 Syntax
(parameter) -> expression
일반적으로 람다식은 () -> expression 처럼 한 줄로 작성한다
람다식이 어떤 값을 리턴해야 한다면 표현식(expression)의 수행 결과가 리턴된다
(parameter1, parameter2) -> expression
인자가 많을 때는 위와 같이 인자를 여러 개 추가할 수 있다
(parameter1, parameter2) -> { code block }
표현식을 한 줄로 구현하기 어려울 때는 { } 를 사용하여 여러 줄로 구현할 수 있다
대신 명시적으로 return을 입력해줘야 한다
int result = doSomething((n1, n2) -> {
int res1 = n1 * 10;
int res2 = n2 * 10;
return res1 + res2;
});
위와 같이 괄호를 사용하여 여러 줄의 코드로 구현하면 된다!
3) 람다식의 특징
람다식의 특징을 정리해보면 다음과 같다
- 클래스를 구현하지 않아도 된다
- 인터페이스(추상 메서드)를 구현한다
- Abstract 클래스의 경우, 익명 함수는 가능하지만 람다식은 구현이 안된다
- 인자를 전달할 수 있고, 결과를 리턴할 수 있다
- 코드가 간결하다
4) 람다식과 함수형 인터페이스(Functional Interface)
람다식은 위의 예제처럼 직접 정의한 인터페이스에 대해 사용할 수 있지만, 이미 정의된 함수형 인터페이스에도 사용할 수 있다
함수형 인터페이스는 아래와 같은 함수들을 제공하고 있기 때문에, 직접 정의하지 않고 아래의 함수를 이용하면 된다
- Runnable : 인자를 받지 않고 리턴 값 없음
- Supplier : 인자를 받지 않고 T 타입 객체 리턴
- Consumer : T타입 객체를 인자로 받고, R 타입 객체 리턴
- Function<T, R> : T 타입 객체를 인자로 받고, R 타입 객체 리턴
- Predicate : T타입 객체를 인자로 받고, boolean 객체 리턴
Supplier<String> getString = () -> "Happy new year!";
System.out.println(getString.get()); // Happy new year!
Supplier<T> 를 예로 들면, 위의 코드에서 인자를 받지 않고 String을 리턴하는 Supplier의 람다식을 구현하였다
get() 호출 시, 람다식에서 정의된 구현이 동작하면서 String이 리턴된다
다른 예시로 위에서 구현한 sum() 예제를 함수형 인터페이스와 람다식을 이용하여 다시 구현해보자!
import java.util.function.BiFunction;
public class Example2 {
static int doSomething(BiFunction<Integer, Integer, Integer> sum) {
return sum.apply(10, 20);
}
public static void main(String[] args_ {
int resut = doSomething((n1, n2) -> n1 + n2); // Lambda
System.out.println(result); // 30
}
}
BiFunction<T, U, R> 는 T와 U 타입의 인자를 전달하고 R 타입의 객체를 리턴하는 함수형 인터페이스이다
기존에 직접 정의한 Math 인터페이스는 BiFunction로 대체할 수 있기 때문에 삭제해도 된다
apply()를 호출할 때 전달된 인자와 함께 람다식이 수행되고 결과가 리턴된다
만약 위의 코드를 익명함수로 구현하면 아래와 같이 구현할 수 있다
import java.util.function.BiFunction;
public class Example2 {
static int doSomething(BiFunction<Integer, Integer, Integer> sum) {
return sum.apply(10, 20);
}
public static void main(Stirng[] args) {
int result = doSomething(new BiFunction<Integer, Integer, Integer> () {
@Override
public Integer apply(Integer n1, Integer n2) {
return n1 + n2;
}
}); // Annonymous function;
}
}
람다식의 장점
- 이름 없는 함수 선언 가능
- 소스코드의 분량을 줄일 수 있고, 반복 코드 관리에 유용하다
- 코드를 파라미터로 전달이 가능하며, 동작을 정의하여 메서드에 전달할 때 편리하게 사용이 가능하다
⚠️ 람다식 사용 시 주의 사항 및 제약 사항
- 람다는 메서드나 클래스와 달리 이름이 없고 문서화도 불가능하다
- 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말자
- 람다는 1줄일 때 가장 좋고, 길어야 3줄 안에 끝내는게 좋다 (3줄을 넘어가면 가독성이 심하게 나빠진다)
- 람다가 길거나 읽기 어렵다면 더 간단히 줄여보거나 람다를 쓰지 않는 쪽으로 리팩토링하자
- 열거 타입 생성자 안의 람다는 열거 타입의 인스턴스 멤버에 접근할 수 없다
- 열거 타입 생성자에 넘겨지는 인수들의 타입도 컴파일 타임에 추론되는데, 인스턴스는 런타임에 만들어지기 때문이다
- 람다는 함수형 인터페이스에서만 쓰인다
- 람다는 함수형 인터페이스(추상 메서드 하나짜리 인터페이스)가 아닌 곳에서는 사용할 수 없다
- 따라서 추상 클래스의 인스턴스를 만들 때는 익명 클래스를 써야 한다
- 또한 추상 메서드가 여러 개인 인터페이스의 인스턴스를 만들 때도 익명 클래스를 써야 한다
- 람다는 자신을 참조할 수 없다
- 람다에서의 this 키워드는 바깥 인스턴스를 가리킨다
- 반면 익명 클래스에서의 this는 익명 클래스의 인스턴스 자신을 가리킨다
- 따라서 함수 객체가 자신을 참조해야 한다면 반드시 익명 클래스를 써야 한다
- 람다는 직렬화 형태가 구현 별로 다를 수 있다 (like 익명클래스)
- 따라서 람다를 직렬화하는 일은 극히 삼가야 한다 (익명 클래스의 인스턴스도 마찬가지다)
- 직렬화해야만 하는 함수 객체가 있다면 private 정적 중첩 클래스의 인스턴스를 사용하자
참고)
https://codechacha.com/ko/java-anonymous-class/
https://codechacha.com/ko/java-lambda-expression/
https://devfunny.tistory.com/691
'야미스터디 > Java' 카테고리의 다른 글
[Java] Java 8 - 함수형 인터페이스(Functional Interface) (0) | 2022.12.11 |
---|---|
[Java - Stream] 04. mapping (0) | 2022.12.09 |
[Java - Stream] 03. filtering (0) | 2022.12.07 |
[Java - Stream] 02. 컬렉션과 Stream의 구현방식의 차이 (0) | 2022.12.07 |
[Java - Stream] 01. 스트림을 학습해야 하는 이유 (0) | 2022.12.07 |
댓글