-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GITBOOK-187: item 42 : 익명 클래스보다는 람다를 사용하라
- Loading branch information
1 parent
e1b25e3
commit b3e6b62
Showing
5 changed files
with
368 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
developLog/programming-lanuage/java/effective-java/6/undefined.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# 보충 : 리플렉션과 애너테이션 | ||
|
||
## 1. 애너테이션은 주석과 같은 의미라고 하는데 런타임시 어떻게 유지가 돼? 사라지지 않을까?' | ||
|
||
애너테이션(annotation)은 자바에서 소스 코드에 부가적인 정보를 추가하는 메타데이터 역할을 하는데요, 주석과는 달리 컴파일 이후에도 특정 애너테이션은 그대로 유지되어 런타임에서 활용될 수 있습니다. | ||
|
||
자바 애너테이션은 크게 세 가지로 나뉩니다. | ||
|
||
1. **SOURCE**: 컴파일러에게만 정보를 제공하고 컴파일된 후에는 사라지는 애너테이션입니다. 예를 들어, `@Override`는 컴파일 시에만 필요한 애너테이션이라 컴파일 후에는 제거됩니다. | ||
2. **CLASS**: 컴파일된 후 .class 파일에 저장되지만, 런타임에서는 사용되지 않는 애너테이션입니다. | ||
3. **RUNTIME**: 런타임 시에도 유지되어 리플렉션(reflection)을 통해 접근할 수 있는 애너테이션입니다. 예를 들어, Spring 프레임워크의 애너테이션(`@Autowired`, `@Service` 등)은 런타임 시 사용되기 때문에 `RUNTIME`으로 설정되어 있습니다. 이들은 런타임에 리플렉션을 통해 접근이 가능하여 프레임워크나 라이브러리에서 자동으로 객체를 주입하거나 설정할 때 활용됩니다. | ||
|
||
이렇게 **런타임에 유지되는 애너테이션**은 자바 리플렉션 API를 통해 동적으로 읽고 처리할 수 있어서, 프레임워크나 라이브러리에서는 코드의 동작을 제어하거나 설정하는 데 자주 사용합니다. | ||
|
||
|
||
|
||
## 2. 리플렉션 api가 뭔데 정확히 알려줘 | ||
|
||
리플렉션(Reflection) API는 자바에서 실행 중인 프로그램이 자신의 구조를 조사하고 수정할 수 있게 하는 강력한 기능입니다. 리플렉션을 통해 클래스의 메서드, 필드, 생성자 등에 대한 정보를 얻고 동적으로 접근할 수 있습니다. 예를 들어, 리플렉션을 통해 **클래스 이름을 알지 못하는 상태에서** 특정 클래스의 메서드를 호출하거나 필드 값을 읽고 쓸 수 있습니다. 이는 런타임에 동적으로 객체를 생성하거나, 메서드를 호출하는 데 유용합니다. | ||
|
||
리플렉션 API의 주요 클래스와 기능은 다음과 같습니다. | ||
|
||
#### 1. `Class` 클래스 | ||
|
||
자바의 모든 클래스는 `Class` 클래스의 인스턴스를 통해 그 정보를 참조할 수 있습니다. `Class` 객체를 통해 메서드, 필드, 생성자 등 다양한 정보를 얻을 수 있습니다. 예를 들어, `SomeClass.class` 또는 `Class.forName("패키지.클래스이름")`을 통해 `Class` 객체를 가져올 수 있습니다. | ||
|
||
#### 2. 주요 리플렉션 메서드 | ||
|
||
`Class` 객체를 통해 사용할 수 있는 주요 메서드는 다음과 같습니다. | ||
|
||
* **getMethods()** / **getDeclaredMethods()**: 클래스의 모든 메서드(또는 선언된 메서드)를 가져옵니다. | ||
* **getFields()** / **getDeclaredFields()**: 클래스의 모든 필드(또는 선언된 필드)를 가져옵니다. | ||
* **getConstructors()**: 클래스의 생성자 정보를 가져옵니다. | ||
* **getAnnotations()**: 클래스에 적용된 애너테이션을 가져옵니다. | ||
|
||
#### 3. 리플렉션의 동적 객체 생성 및 메서드 호출 | ||
|
||
리플렉션을 통해 동적으로 객체를 생성하고, 메서드를 호출할 수 있습니다. | ||
|
||
* **newInstance()**: 리플렉션을 통해 동적으로 객체를 생성합니다. | ||
* **invoke()**: 리플렉션을 사용하여 메서드를 호출합니다. | ||
|
||
#### 리플렉션의 예제 코드 | ||
|
||
다음은 간단한 리플렉션 사용 예제입니다. | ||
|
||
```java | ||
import java.lang.reflect.Method; | ||
|
||
public class ReflectionExample { | ||
public static void main(String[] args) { | ||
try { | ||
// Class 객체를 얻음 | ||
Class<?> clazz = Class.forName("mypackage.MyClass"); | ||
|
||
// 인스턴스 생성 | ||
Object obj = clazz.getDeclaredConstructor().newInstance(); | ||
|
||
// 메서드 호출 | ||
Method method = clazz.getMethod("myMethod", String.class); | ||
method.invoke(obj, "Hello, Reflection!"); | ||
|
||
} catch (Exception e) { | ||
e.printStackTrace(); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
이 예제에서는 `MyClass` 클래스의 `myMethod` 메서드를 런타임에 동적으로 호출합니다. 리플렉션 API를 사용하면 이처럼 컴파일 시점에 클래스나 메서드에 대한 정보를 알지 못해도 런타임에 이를 찾아내고 사용할 수 있습니다. | ||
|
||
#### 리플렉션의 장단점 | ||
|
||
**장점**: | ||
|
||
* 유연하고 동적인 코드 작성이 가능함. | ||
* 런타임에 클래스 구조를 조사하고 활용할 수 있어 프레임워크나 라이브러리 설계에 필수적임. | ||
|
||
**단점**: | ||
|
||
* 성능이 저하될 수 있음. | ||
* 캡슐화를 위반할 수 있어 잘못 사용할 경우 보안 취약점이 생길 수 있음. | ||
|
||
리플렉션은 강력하지만 복잡도와 성능 저하가 수반될 수 있어 필요한 경우에 신중하게 사용하는 것이 좋습니다. |
3 changes: 3 additions & 0 deletions
3
developLog/programming-lanuage/java/effective-java/7/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# 7장 : 람다와 스트림 | ||
|
||
자바 8에서 **함수형 인터페이스, 람다, 메서드 참조라는 개념**이 추가되면서 <mark style="color:red;">함수 객체를 더 쉽게 만들 수 있게 되었다.</mark> 이와 함께 스트림 API까지 추가되어 데이터 원소의 시퀀스 처리를 라이브러리 차원에서 지원하기 시작했다. 이번 장에서는 이 기능들을 효과적으로 사용하는 방법을 알아보겠다. |
278 changes: 278 additions & 0 deletions
278
developLog/programming-lanuage/java/effective-java/7/item-42.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
# item 42 : 익명 클래스보다는 람다를 사용하라 | ||
|
||
## 1. 익명 클래스 | ||
|
||
예전에는 <mark style="color:red;">자바에서 함수 타입을 표현할 때</mark> 추상 메서드를 하나만 담은 인터페이스(드물게는 추상 클래스)를 사용했다.  | ||
|
||
> 이런 인터페이스의 인스턴스를 `함수 객체(Rmction object)`라고 하여, 특정 함수나 동작을 나타내는 데 썼다. JDK 1.1 이 등장하면서 함수 객체를 만드는 주요 수단은 익명 클래스(아이템 24)가 되었다 | ||
### 1) 익명클래스란? | ||
|
||
익명 클래스(Anonymous Class) : 이름이 없는 클래스로, 주로 인터페이스의 구현체를 생성할 때 사용된다. 익명 클래스는 일회성으로 사용되며, <mark style="color:red;">클래스 정의와 동시에 인스턴스를 생성하여 사용할 수 있다.</mark> | ||
|
||
### 2) 익명 클래스의 인스턴스를 함수 객체로 사용 | ||
|
||
익명 클래스는 특히 <mark style="color:blue;">특정 기능을 정의하는 함수 객체로 자주 사용된다.</mark> | ||
|
||
예를 들어, 문자열 리스트를 길이 순으로 정렬하는 코드가 있다고 가정해 보자. 자바 8 이전에는 `Comparator` 인터페이스를 구현하는 익명 클래스를 다음과 같이 사용했다: | ||
|
||
```java | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.Comparator; | ||
import java.util.List; | ||
|
||
public class Main { | ||
public static void main(String[] args) { | ||
List<String> words = Arrays.asList("kim", "taeng", "mad", "play"); | ||
|
||
Collections.sort(words, new Comparator<String>() { | ||
public int compare(String s1, String s2) { | ||
return Integer.compare(s1.length(), s2.length()); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
``` | ||
|
||
비교 로직을 익명 클래스로 작성하여`Comparator` 구현했지만 익명 클래스 방식은 코드가 너무 길기 때문에 자바는 **함수형 프로그래밍(Functional Programming)**에 적합하지 않다. | ||
|
||
## 2. 자바 8에서의 람다 표현식 도입 | ||
|
||
자바 8에 와서 `JDK 1.8` 버전 부터는 추상 메서드 하나 짜리 인터페이스, 즉 함수형 인터페이스를 말하는데 그 인터페이스의 인스턴스를 람다식(lambda expression, 짧게 람다)라고 사용해 만들 수 있게 되었다 | ||
|
||
자바 8부터는 람다 표현식(lambda expression)을 도입하여, 코드가 더욱 간결해졌다. **람다**는 익명 클래스처럼 구현할 필요 없이 <mark style="color:blue;">함수형 인터페이스(추상 메서드가 하나인 인터페이스)의 인스턴스를 간단히 정의</mark>할 수 있다. | ||
|
||
```java | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.List; | ||
|
||
public class Main { | ||
public static void main(String[] args) { | ||
List<String> words = Arrays.asList("kim", "taeng", "mad", "play"); | ||
|
||
Collections.sort(words, | ||
(s1, s2) -> Integer.compare(s1.length(), s2.length())); | ||
} | ||
} | ||
``` | ||
|
||
람다는 `Comparator<String>` 인터페이스의 인스턴스를 생성하며, 여기서 매개변수(`s1`과 `s2`) 타입인`String과` 반환값의 타입인 int는컴파일러가 **타입 추론**을 통해 자동으로 결정한다. 상황에 따라 컴파일러가 타입을 결정하지 못할 때만 프로그래머가 직접 타입을 명시하면 된다. | ||
|
||
<mark style="color:red;">타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략하자.</mark> 그런 다음 컴파일러가 “타입을 알 수 없다”는 오류 를 낼 때만 해당 타입을 명시하면 된다. | ||
|
||
### 1) 타입 추론 관련 | ||
|
||
아이템 26에서는 제네릭의 로 타입을 쓰지 말라 했고, 아이템 29에서는 제네릭을 쓰라 했고, 아이템 30에서는 제네릭 메서드를 쓰라고 했다.  | ||
|
||
> 이 조언들은 람다와 함께 쓸 때는 두 배로 중요해진다.  | ||
컴파일러가 타입을 추론 하는 데 <mark style="color:red;">필요한 타입 정보 대부분을 제네릭에서 얻기 때문이다.</mark> 우리가 이 정보를 제공하지 않으면 컴파일러는 람다의 타입을 추론할 수 없게 되어, 결국 우리가 일일이 명시해야 한다. 좋은 예로, 코드 42-2에서 인수 <mark style="color:blue;">words가 매개변수화 타입인 List< String> 아니라 로 타입인 List였다면 컴파일 오류가 났을 것</mark>이다. | ||
|
||
### 2) 더 간결한 표현 람다식 : 함수형 인터페이스와 비교자 생성 메서드 | ||
|
||
함수형 인터페이스는 람다 표현식에 맞게 설계된 인터페이스로, `Comparator`도 대표적인 함수형 인터페이스이다. `Comparator`를 람다와 함께 사용할 때는 `comparingInt` 같은 **비교자 생성 메서드**를 사용하여 코드를 더욱 간결하게 작성할 수 있다. | ||
|
||
```java | ||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.Comparator; | ||
import java.util.List; | ||
|
||
public class Main { | ||
public static void main(String[] args) { | ||
List<String> words = Arrays.asList("kim", "taeng", "mad", "play"); | ||
Collections.sort(words, Comparator.comparingInt(String::length)); | ||
} | ||
} | ||
``` | ||
|
||
자바 8에서 `List` 인터페이스에 `sort` 메서드가 추가되면서 더욱 짧은 코드로 정렬을 수행할 수 있게 되었다. | ||
|
||
```java | ||
import java.util.Arrays; | ||
import java.util.Comparator; | ||
import java.util.List; | ||
|
||
public class Main { | ||
public static void main(String[] args) { | ||
List<String> words = Arrays.asList("kim", "taeng", "mad", "play"); | ||
words.sort(Comparator.comparingInt(String::length)); | ||
} | ||
} | ||
``` | ||
|
||
### 3) 열거 타입(Enum)에서 람다 활용 | ||
|
||
자바에서 **열거 타입**의 인스턴스는 고유의 동작을 지정할 수 있다. 자바 8 이전에는 `Operation` 같은 열거 타입의 `apply` 메서드를 각 상수별로 재정의하기 위해 <mark style="color:red;">상수별 클래스 몸체를 사용해야 했다.</mark> | ||
|
||
```java | ||
enum Operation { | ||
PLUS("+") { | ||
public double apply(double x, double y) { return x + y; } | ||
}, | ||
MINUS("-") { | ||
public double apply(double x, double y) { return x - y; } | ||
}, | ||
TIMES("*") { | ||
public double apply(double x, double y) { return x * y; } | ||
}, | ||
DIVIDE("/") { | ||
public double apply(double x, double y) { return x * y; } | ||
}; | ||
|
||
private final String symbol; | ||
|
||
Operation(String symbol) { this.symbol = symbol; } | ||
|
||
@Override public String toString() { return symbol; } | ||
public abstract double apply(double x, double y); | ||
} | ||
``` | ||
|
||
하지만, 자바 8 이후에는 람다 표현식을 사용하여 **열거 타입의 인스턴스 필드에 함수 객체를 저장**하는 방식으로 코드가 간결해졌다. 단순히 각 열거 타입 상수의 동작을 람다로 구현해 <mark style="color:red;">생 성자에 넘기고, 생성자는 이 람다를 인스턴스 필드로 저장</mark>해둔다. 그런 다음 apply 메서드에서 필드에 저장된 람다를 호출하기만 하면 된다. | ||
|
||
```java | ||
import java.util.function.DoubleBinaryOperator; | ||
|
||
enum Operation { | ||
PLUS("+", (x, y) -> x + y), | ||
MINUS("-", (x, y) -> x - y), | ||
TIMES("*", (x, y) -> x * y), | ||
DIVIDE("/", (x, y) -> x / y); | ||
|
||
private final String symbol; | ||
private final DoubleBinaryOperator op; | ||
|
||
Operation(String symbol, DoubleBinaryOperator op) { | ||
this.symbol = symbol; | ||
this.op = op; | ||
} | ||
|
||
@Override | ||
public String toString() { return symbol; } | ||
|
||
public double apply(double x, double y) { | ||
return op.applyAsDouble(x, y); | ||
} | ||
} | ||
|
||
public class Main { | ||
public static void main(String[] args) { | ||
// 사용은 아래와 같이 | ||
Operation.PLUS.apply(2, 3); | ||
} | ||
} | ||
``` | ||
|
||
람다 표현식이 도입되면서 열거 타입의 각 인스턴스에 대한 동작을 간단하게 정의할 수 있게 되었고, 코드가 더욱 간결하고 유지보수가 쉬워졌다. | ||
|
||
{% hint style="success" %} | ||
열거 타입 상수의 동작을 표현한 람다를 DoubleBinaryOperator 인터페이스 변수에 할당했다. **DoubleBinaryOperator**는 java.util.function 패키지가 제공하 는 다양한 함수 인터페이스 중 하나로, double 타입 인수 2개를 받아 double 타입 결과를 돌려준다. | ||
{% endhint %} | ||
|
||
<figure><img src="../../../../.gitbook/assets/화면 캡처 2024-11-11 111644.png" alt=""><figcaption></figcaption></figure> | ||
|
||
### 4) 람다 표현식의 제한 사항 | ||
|
||
람다 기반 Operation 열거 타입을 보면 상수별 클래스 몸체는 더 이상 사용할 이유가 없다고 느낄지 모르지만, 꼭 그렇지는 않다. 메서드나 클래스와 달리, 람다는 **이름이 없고 문서화도 못 한다.**  | ||
|
||
* **this 참조**: 람다 표현식에서 `this` 키워드는 바깥 인스턴스를 가리킨다. 익명 클래스에서는 `this`가 익명 클래스 자체를 참조하므로, 람다에서는 이를 활용할 수 없다.  | ||
|
||
```java | ||
import java.util.Arrays; | ||
import java.util.List; | ||
|
||
|
||
class Anonymous { | ||
public void say() {} | ||
} | ||
|
||
public class Main { | ||
public void someMethod() { | ||
List<Anonymous> list = Arrays.asList(new Anonymous()); | ||
|
||
Anonymous anonymous = new Anonymous() { | ||
@Override | ||
public void say() { | ||
System.out.println("this instanceof Anonymous : " + (this instanceof Anonymous)); | ||
} | ||
}; | ||
|
||
// this instanceof Anonymous : true | ||
anonymous.say(); | ||
|
||
// this instanceof Main : true | ||
list.forEach(o -> System.out.println("this instanceof Main : " + (this instanceof Main))); | ||
} | ||
|
||
public static void main(String[] args) { | ||
new Main().someMethod(); | ||
} | ||
} | ||
``` | ||
|
||
|
||
|
||
* **추상 클래스 인스턴스**: 추상 메서드가 하나인 함수형 인터페이스가 아닌 경우, 익명 클래스를 대신 사용할 수 없다. | ||
* **자신을 참조해야 할 때**: 람다에서 자신을 참조할 수 없으므로, 함수 객체가 자신을 참조해야 하는 경우에는 익명 클래스를 사용해야 한다. 즉, 추상 클래스의 인스턴스를 만들 때 람다를 사용할 수 없다.  | ||
|
||
```java | ||
abstract class Hello { | ||
public void sayHello() { | ||
System.out.println("Hello!"); | ||
} | ||
} | ||
|
||
public class Main { | ||
public static void main(String[] args) { | ||
// 이건 원래 안됨~ | ||
// Hello hello = new Hello(); | ||
|
||
Hello instance1 = new Hello() { | ||
private String msg = "Hi"; | ||
@Override public void sayHello() { | ||
System.out.println(msg); | ||
} | ||
}; | ||
|
||
Hello instance2 = new Hello() { | ||
private String msg = "Hola"; | ||
@Override public void sayHello() { | ||
System.out.println(msg); | ||
} | ||
}; | ||
|
||
// Hi! | ||
instance1.sayHello(); | ||
|
||
// Hola! | ||
instance2.sayHello(); | ||
|
||
// false | ||
System.out.println(instance1 == instance2); | ||
} | ||
} | ||
``` | ||
|
||
|
||
|
||
### 5) 람다와 직렬화의 문제 | ||
|
||
람다 표현식과 익명 클래스는 모두 **직렬화**에 주의가 필요하다. 람다 표현식의 직렬화는 구현에 따라 다르며, **VM별로 직렬화 형태가 다를 수 있다**. 따라서 람다를 직렬화하는 일은 극히 삼가야 한다. 익명 클래스 의 인스턴스도 마찬가지. <mark style="color:red;">직렬화해야만 하는 함수 객체가 있다면 가령 Comparator처럼 private 정적 중첩 클래스(아이템 24)의 인스턴스를 사용하자</mark> | ||
|
||
> 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다. | ||
##  핵심 정리 | ||
|
||
* 자바 8의 람다 표현식은 함수형 인터페이스 인스턴스를 더 간결하게 만들며, 함수형 프로그래밍의 문법적 장벽을 크게 낮췄다. | ||
* 익명 클래스는 함수형 인터페이스가 아닌 타입의 인스턴스를 만들 때 사용하며, 람다가 제공하지 않는 기능을 활용할 때 여전히 필요하다. | ||
* 람다와 제네릭을 함께 사용할 때 타입을 명시하여 컴파일 오류를 방지하는 것이 중요하다. | ||
|
||
|
||
|
||
> 참고 및 출처 | ||
> | ||
> * [https://madplay.github.io/post/prefer-lambdas-to-anonymous-classes](https://madplay.github.io/post/prefer-lambdas-to-anonymous-classes) |