Skip to content

Commit 643b71f

Browse files
hoTan35JangYeongHu
andauthored
docs: item 22,27,32,40,43,48,60,63,66 (#52)
Co-authored-by: 장영후 <128132449+jangyeonghu@users.noreply.github.com>
1 parent 379c983 commit 643b71f

9 files changed

+542
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
## item22 인터페이스는 타입을 정의하는 용도로만 사용하라
2+
3+
- 인터페이스의 역할: 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것이다. (인터페이스는 오직 이 용도로만 사용해야 한다.)
4+
- 이 지침을 어긴 케이스: 상수 인터페이스
5+
- 상수 인터페이스: 메서드 없이, 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스
6+
7+
```java
8+
public interface Constants {
9+
static final int WIDTH = 800;
10+
static final int HEIGHT = 600;
11+
static final String TITLE = "My App";
12+
}
13+
//상수 인터페이스를 상속해서 사용
14+
public class MyWindow implements Constants {
15+
public void print() {
16+
System.out.println("창 너비: " + WIDTH);
17+
}
18+
}
19+
```
20+
21+
- 클래스 내부에서 사용하는 상수는 내부 구현에 해당된다. 즉, 상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스의 API로 노출하는 행위다.
22+
- 그 이유는 사용자의 입장에서 위의 코드는 아래의 코드처럼 보인다.
23+
24+
```java
25+
MyWindow window = new MyWindow();
26+
window instanceof Constants //true!
27+
```
28+
29+
즉,외부에서는 *이 클래스가 Constants라는 타입을 구현하고 있다*고 인식하게 된다.
30+
31+
- 상수 인터페이스는 사용자에게 아무런 의미가 없고 오힐 사용자에게 혼란을 주기도 하며, 더 심하게는 클라이언트 코드가 내부 구현에 해당하는 이 상수들에 종속되게 한다.
32+
- 예를 들어,
33+
34+
```java
35+
if (userInput.equals(Constants.TITLE)) {
36+
...
37+
}
38+
```
39+
40+
이런 코드를 사용하다가 Constants.TITLE이 더 이상 필요가 없어져서 지우고 싶어도 이걸 사용하는 모든 사용자의 코드가 깨지거나 이런 오류를 방지하기 위해 억지로 인스페이스를 계속 유지시켜야 한다.
41+
42+
---
43+
44+
- 그러면 이런 상수들의 묶음은 어떻게 표시해야 좋을까?
45+
46+
1. 특정 클래스나 인터페이스와 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가해야 한다. 대펴적으로 Integer와 Double에 선언된 MAX/MIN_VALUE가 있다.
47+
2. 열거 타입으로 나타내기 적합한 상수라면 열거 타입으로 만들어 공개하면 된다.
48+
3. 인스턴스화할 수 없는 유틸리티 클래스에 담아서 공개하면 된다.
49+
위의 예시를 유틸리티 클래스를 바꾸어 보면 아래와 같은 코드가 된다.
50+
51+
```java
52+
public class Constants {
53+
private Constants() {} //인스턴스화 방지
54+
55+
public static final int WIDTH = 800;
56+
public static final int HEIGHT = 600;
57+
public static final String TITLE = "My App";
58+
}
59+
```
60+
61+
- 유틸리티 클래스에 정의된 상수를 클라이언트에서 사용하려면 클래스 이름까지 함께 명시해야 한다. (_Constants.WIDTH_) -> 정적 임포트하여 클래스 이름은 생략할 수도 있다.
62+
63+
---
64+
65+
- 핵심 정리
66+
- _인터페이스는 타입을 정의하는 용도로만 사용해야 한다. 상수 공개용 수단으로 사용하지 말자._
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## item27 비검사 경고를 제거하라
2+
3+
- 모든 비검사 경고를 제거한다면 그 코드는 타입 안정성이 보장된다. 즉, 런타임에 ClassCastException이 발생할 일이 없고, 우리가 의도한 대로 잘 동작하리라 확신할 수 있다.
4+
- 경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면 @SuppressWarnings("unchecked") 애너테이션을 달아 경고를 숨길 수 있다.
5+
- 만약 안전하다고 검증된 비검사 경고를 그대로 두면, 진짜 문제를 알리는 새로운 경고가 나와도 눈치채지 못할 수 있다. 따라서 안전함이 검증됐다면 숨겨주는 습관을 가지도록 하자.
6+
- @SuppressWarnings 애너테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있지만 항상 가능한 한 좁은 범위에 적용하는 것이 좋다. (보통 변수 선언, 아주 짧은 메서드 혹은 생성자) 클래스같은 큰 범위에 적용하면 심각한 경고를 놓칠 수 있기 때문이다.
7+
- 한 줄이 넘는 메서드나 생성자에 달린 @SuppressWarnings 애너테이션을 발견하면 지역변수 선언 쪽으로 옮기자.
8+
- 예를 들어,
9+
10+
```java
11+
public <T> T[] toArray(T[] a) {
12+
if (a.length < size)
13+
return (T[]) Arrays.copyOf(elements, size, a.getClass());
14+
System.arraycopy(elements, 0, a, 0, size);
15+
if(a.length > size)
16+
a[size] = null;
17+
return a;
18+
}
19+
```
20+
21+
ArrayList의 toArray 메서드를 컴파일 하면 아래와 같은 경고가 발생한다.
22+
23+
```java
24+
ArrayList.java:305: warning: [unchecked] unchecked cast
25+
return (T[]) Arrays.copyOf(elements, size, a.getClass());
26+
required: T[]
27+
found: Object[]
28+
```
29+
30+
@SuppressWarnings 애너테이션은 선언에만 달 수 있기 때문에 return문에는 달지 못한다. 그렇다면 메서드 자체에 달고 싶겠지만, 범위가 필요이상으로 넓어지게 된다. 그 대신 반환값을 담을 지역변수를 하나 선언하고 그 변수에 @SuppressWarnings 애너테이션을 달아주면 된다.
31+
32+
- _@SuppressWarnings 애너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다._
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
## item32 제네릭과 가변인수를 함께 쓸 때는 신중하라.
2+
3+
- 가변인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있지만, 구현 방식에서 허점이 있다.
4+
5+
-> 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어지는데, 내부로 감춰야 했을 이 배열이 클라이언트에 노출되는 문제가 생겼다.
6+
7+
즉, _varargs 매개변수에 제네릭이나 매개변수화 타입이 포함되면 컴파일 경고가 발생한다._
8+
9+
-> varargs 매개변수는 배열이다. 여기에 제네릭 타입을 넣으면 제네릭 배열(JAVA에서 지원하지 않음)이 된다. 배열과 제네릭의 타입 시스템 차이에 의해 타입 안전성이 붕괴되고, 이로 인해 *힙 오염*이 발생된다.
10+
11+
```java
12+
static void dangerous(List<String>... stringLists) {
13+
List<Integer> intList = List.of(42);
14+
Object[] objects = stringLIsts;
15+
objects[0] = intList; //힙 오염 발생
16+
String s = stringLists[0].get(0); //ClassCastException
17+
//마지막 줄에 컴파일러가 생성한 (보이지 않는) 형변환이 숨어있기 때문이다.
18+
}
19+
```
20+
21+
- 제네릭 배열을 프로그래머가 직접 생성하는 건 허용하지 않으면서 제네릭 varargs 매개변수를 받는 메서드를 선언할 수 있는 이유가 무엇일까? -> 즉, 왜 오류가 아닌 경고로 끝일까?
22+
23+
제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용하기 때문이다. 그래서 언어 설계자는 이 모순을 수용하기로 했다. (실제 자바 라이브러리도 이런 메서드를 여럿 제공한다.)
24+
25+
- 자바 7이전에는 @SuppressWarnings("unchecked")로 일일히 경고를 숨겨야 했지만, 자바 7에서 @SafeVarargs 애너테이션(메서드 작성자가 그 메서드가 타입 안정함을 보장하는 장치)이 추가 되어 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 되었다.
26+
- 메서드가 안전한지는 어떻게 확신할 수 있을까?
27+
28+
-> 메서드가 이 배열에 아무 것도 저장하지 않고 그 배열의 참조가 밖으로 노출되지 않는다면 타입 안전하다. 바꿔 말해서, 이 varargs 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 그 메서드는 안전하다.
29+
30+
- 단, 이것이 무조건적으로 옳은 것은 아니다.
31+
32+
예를 들어,
33+
34+
```java
35+
static <T> T[] pickTwo(T a, T b, T c) {
36+
switch(ThreadLocalRandom.current().nextInt(3)) {
37+
case 0: return toArray(a, b);
38+
case 1: return toArray(b, c);
39+
case 2: return toArray(c, a);
40+
}
41+
throw new AssertionError(); //도달할 수 없다.
42+
}
43+
44+
public static void main(String[] args) {
45+
String[] attributes = pickTwo("좋은","빠른","저렴한");
46+
}
47+
```
48+
49+
- pickTwo의 반환값을 attributes에 저장하기 위해 String[]로 형변환하는 코드를 컴파일러가 자동 생성한다는 점을 놓쳤다. Object[]는 String[]의 하위 타입이 아니므로 이 형변환은 실패한다.
50+
51+
_따라서, 제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다!_
52+
53+
- 물론 여기에도 예외 2가지가 있다.
54+
55+
1. @SafeVarargs로 제대로 애노테이트된 또 다른 varargs 메서드에 넘기는 것은 안전하다.
56+
2. 그저 이 배열 내용의 일부 함수를 호출만 하는(varags를 받지 않는) 일반 메서드에 넘기는 것도 안전하다.
57+
58+
- @SafeVarargs 애너테이션을 사용할 때 정하는 규칙 : 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달아야 한다.(안전하지 않은 varargs 메서드는 절대 작성해서는 안 된다.)
59+
60+
- 정리
61+
62+
1. varages 매개변수 배열에 아무것도 저장하지 않는다.
63+
2. 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다.
64+
65+
***
66+
67+
참고
68+
69+
@SafeVarargs 애너테이션이 유일한 정답이 아니다. varargs 매개변수를 List 매개변수로 바꿀 수도 있다. (아이템 28의 내용)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
## item40 @Override 애너테이션을 일관되게 사용하라
2+
3+
- @Override는 프로그래머에게 가장 중요한 애너테이션일 것이다. 이것은 메서드 선언에만 달 수 있고, 이 애너테이션이 달렸다는 것은 상위 타입의 메서드를 재정의했다는 것을 의미한다.
4+
- @Override 애너테이션을 일관되게 사용하면 여러 버그들을 예방해준다. 다음의 Bigram 프로그램을 살펴보자.(바이그램, 여기서는 영어 알파벳 2개로 구성된 문자열을 표현)
5+
6+
```java
7+
public class Bigram {
8+
private final char first;
9+
private final char second;
10+
11+
public Bigram(char first, char second) {
12+
this.first = first;
13+
this.second = second;
14+
}
15+
public boolean equals(Bigram b) {
16+
return b.first == first && b.second = second;
17+
}
18+
public int hashCode() {
19+
return 31 * first + second;
20+
}
21+
22+
public static void main(String[] args) {
23+
Set<Bigram> s = new HashSet<>();
24+
25+
for(int i = 0; i < 10; i++)
26+
for(char ch = 'a'; ch <= 'z'; ch++)
27+
s.add(new Bigram(ch,ch));
28+
System.out.println(s.size());
29+
}
30+
}
31+
```
32+
33+
- 위의 코드는 a부터 z까지 26개의 바이그램을 10번 반복해 집합에 추가한 다음, 집합의 크기를 출력하려는 것으로 보인다. Set은 중복을 허용하지 않기 때문에 결과는 26으로 나올길 원했던 것 같다. 하지만 실제로는 260이 출력된다. 그 원인을 하나 하나 살펴보자.
34+
35+
1. 작성자는 equals 메서드를 재정의하려 한 것으로 보이고 hashCode도 함께 재정의해야 한다는 사실을 잊지 않았다. 하지만 equals는 재정의가 아니라 _다중정의_ 해버렸다.
36+
2. Object의 equals를 재정의하려면 매개변수 타입을 Object로 해야했는데 그렇게 하지 않았다. 그래서 Objcet에서 상속한 equals와는 별개인 equals를 새로 정의한 꼴이 됐다.
37+
3. Object의 equals는 ==연산자와 똑같이 객체 식별성만을 확인한다. 따럿 같은 소문자를 소유한 바이그램 10개의 각각이 서로 다른 객체로 인식되고 결과가 260이 된 것이다.
38+
39+
이제 이를 해결하기 위해 equals 메서드에 @Override를 붙이면
40+
41+
```java
42+
Bigram.java:10: method does not override or implement a method from a supertype
43+
@Override public boolean equals(Bigram b)
44+
```
45+
46+
꼴의 컴파일 오류가 발생한다. 앞서 말했듯 매개변수 타입이 Object 타입이어야 하는데 Bigram이므로 오류가 발생한 것이다. 이를 올바르게 수정하면 아래와 같은 형태이다.
47+
48+
```java
49+
@Override
50+
public boolean equals(Object o) {
51+
if(!(o instanceof Bigram)) return false;
52+
Bigram b = (Bigram) o;
53+
return b.first == first && b.second == second;
54+
}
55+
```
56+
57+
- 중요!
58+
59+
_상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달자!_
60+
61+
예외는 한 가지 뿐이다. 구체 클래스에서 상위 클래스의 추상 메서드를 재정의할 때는 굳이 @Override를 달지 않아도 된다. 물론 재정의 메서드 모두에 @Override를 일괄로 붙여두는게 좋아 보인다면 그래도 상관없다.
62+
63+
- 한편, IDE는 @Override를 일관되게 사용하도록 부추기기도 한다. @Override가 달려있지 않은 메서드가 실제로는 재정의를 했다면 경고를 준다. 또 @Override는 클래스뿐 아니라 인터페이스의 메서드를 재정의할 때도 사용할 수 있다.디폴트 메서드를 지원하기 시작하면서, 인터페이스의 메서드를 구현한 메서드에도 @Override를 다는 습관을 들이면 시그니처가 올바른지 재차 확신할 수 있다.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
### item43 람다보다는 메서드 참조를 사용하라.
2+
3+
- 자바에는 람다보다 함수 객체를 간결하게 만드는 메서드 참조(method reference)라는 방법이 있다.
4+
5+
이를 설명하기 위해 merge 메서드를 예시로 들어보겠다.
6+
7+
```java
8+
map.merge(key, 1, (count, incr) -> count + incr);
9+
```
10+
11+
위의 예시는 람다를 이용해 merge 메서드를 사용한 것이다. merge 메서드는 키, 값, 함수를 인수로 받으며, 주어진 키가 맵 안에 아직 없다면 주어진 {키, 값} 쌍을 그대로 저장한다. 반대로 키가 이미 있다면 (인수로 받은)함수를 현재 값과 주어진 값에 적용한 다음, 그 결과로 현재 값을 덮어쓴다. 즉, 맵에 {키, 함수의 결과} 쌍을 저장한다.
12+
13+
위의 식도 충분히 간단하지만 메서드 참조를 쓰면 이것보다 훨씬 간결하게 코드 작성이 가능해진다.
14+
15+
```java
16+
map.merge(key, 1, Integer: :sum);
17+
```
18+
19+
(추가 설명) -> 람다는 기존에 존재하는 메서드를 그대로 호출하는 것이라면, 메서드 참조는 그 메서드를 직접 가리키는 방식이다.
20+
21+
매개변수 수가 늘어날수록 메서드 참조로 제거한 수 있는 코드양도 늘어난다. 하지만 어떤 람다에서는 매개변수의 이름 자체가 프로그래머에게 좋은 가이드가 되기도 한다.
22+
23+
- 람다로 할 수 없는 일이라면 메서드 참조로도 할 수 있다.(마지막에 보충 설명) 보통 메서드 참조를 사용하는 편이 더 짧고 간결하므로 람다를 대신할 좋은 대안이 되어준다. 즉, 람다로 작성할 코드를 새로운 메서드에 담은 다음, 람다 대신 그 메서드 참조를 사용하는 식이다.
24+
25+
하지만 언제나 그렇듯 늘 그런 것은 아니다. 때론 람다가 메서드 참조 보다 간결할 때가 있다. 주로 메서드와 람다가 같은 클래스에 있을 때 그렇다.
26+
27+
```java
28+
service.execute(GoshThisClassIsHumangous: :action); //메서드 참조
29+
service.execute((): :action); //람다식
30+
```
31+
32+
- 메서드 참조의 유형
33+
34+
1. 정적 메서드 참조(위에서 설명한 것)
35+
36+
```java
37+
Function<String, Integer> func = (s) -> Integer.parseInt(s);
38+
Function<String, Integer> func = Integer: :parseInt;
39+
```
40+
41+
2. 특정 객체의 인스턴스 메서드 참조
42+
43+
```java
44+
String str = "hello";
45+
Supplier<Integer> sup = () -> str.length();
46+
Supplier<Integer> sup = str: :length;
47+
```
48+
49+
3. 임의 객체의 인스턴스 메서드 참조
50+
51+
```java
52+
List<String> list = Arrays.asList("a", "b", "c");
53+
54+
// 람다식
55+
list.forEach(s -> System.out.println(s));
56+
57+
// 메서드 참조
58+
list.forEach(System.out::println);
59+
```
60+
61+
4. 생성자 참조(클래스& 배열)
62+
63+
```java
64+
//생성자 메서드 참조
65+
Supplier<List<String>> sup = () -> new ArrayList<>();
66+
Supplier<List<String>> sup = ArrayList::new;
67+
68+
//배열 메서드 참조
69+
IntFunction<String[]> lambda = size -> new String[size];
70+
IntFunction<String[]> methodRef = String[]::new;
71+
```
72+
73+
- 보충 설명
74+
75+
람다로는 불가능하나 메서드 참조로는 가능한 유일한 형태가 하나 있는데 바로 제네릭 함수 타입 구현이다.
76+
77+
함수형 인터페이스의 추상 메서드가 제네릭일 수 있듯이 함수 타입도 제네릭일 수 있다. 이는 메서드 참조 표현식으로는 구현할 수 있지만, 제네릭 람다식이라는 문법은 존재하지 않기 때문에 람다식으로는 불가능하다.

0 commit comments

Comments
 (0)