From 2cec5a1118fa1b5873d13ecc0333c5b46ce46020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=ED=9B=84?= <128132449+jangyeonghu@users.noreply.github.com> Date: Sun, 12 Oct 2025 23:09:48 +0900 Subject: [PATCH] docs: item 22,27,32,40,43,48,60,63,66 --- ...54\354\232\251\355\225\230\353\235\274.md" | 66 ++++++++++ ...34\352\261\260\355\225\230\353\235\274.md" | 32 +++++ ...40\354\244\221\355\225\230\353\235\274.md" | 69 +++++++++++ ...54\354\232\251\355\225\230\353\235\274.md" | 63 ++++++++++ ...0\353\235\274_\354\230\201\355\233\204.md" | 116 ++++++++++++++++++ ...54\354\232\251\355\225\230\353\235\274.md" | 77 ++++++++++++ ...01\354\232\251\355\225\230\353\235\274.md" | 77 ++++++++++++ ...54\354\232\251\355\225\230\353\235\274.md" | 0 ...0\353\235\274_\354\230\201\355\233\204.md" | 76 ++++++++++++ ...0 \355\224\274\355\225\230\353\235\274.md" | 66 ++++++++++ ...74\354\235\230\355\225\230\353\235\274.md" | 37 ++++++ ...54\354\232\251\355\225\230\353\235\274.md" | 55 +++++++++ 12 files changed, 734 insertions(+) create mode 100644 "item22/item22_\354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\212\224 \355\203\200\354\236\205\354\235\204 \354\240\225\354\235\230\355\225\230\353\212\224 \354\232\251\353\217\204\353\241\234\353\247\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" create mode 100644 "item27/item27_\353\271\204\352\262\200\354\202\254 \352\262\275\352\263\240\353\245\274 \354\240\234\352\261\260\355\225\230\353\235\274.md" create mode 100644 "item32/item32_\354\240\234\353\204\244\353\246\255\352\263\274 \352\260\200\353\263\200\354\235\270\354\210\230\353\245\274 \355\225\250\352\273\230 \354\223\270 \353\225\214\353\212\224 \354\213\240\354\244\221\355\225\230\353\235\274.md" create mode 100644 "item40/item40_@Override \354\225\240\353\204\210\355\205\214\354\235\264\354\205\230\354\235\204 \354\235\274\352\264\200\353\220\230\352\262\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" create mode 100644 "item42/item42_\354\235\265\353\252\205 \355\201\264\353\236\230\354\212\244\353\263\264\353\213\244\353\212\224 \353\236\214\353\213\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274_\354\230\201\355\233\204.md" create mode 100644 "item43/item43_\353\236\214\353\213\244\353\263\264\353\213\244\353\212\224 \353\251\224\354\204\234\353\223\234 \354\260\270\354\241\260\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" create mode 100644 "item48/item48_\354\212\244\355\212\270\353\246\274 \353\263\221\353\240\254\355\231\224\353\212\224 \354\243\274\354\235\230\355\225\264\354\204\234 \354\240\201\354\232\251\355\225\230\353\235\274.md" rename "item53_\352\260\200\353\263\200\354\235\270\354\210\230\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274/item53_\352\260\200\353\263\200\354\235\270\354\210\230\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274.md" => "item53/item53_\352\260\200\353\263\200\354\235\270\354\210\230\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274.md" (100%) create mode 100644 "item54/item54_null\354\235\264 \354\225\204\353\213\214, \353\271\210 \354\273\254\353\240\211\354\205\230\354\235\264\353\202\230 \353\260\260\354\227\264\354\235\204 \353\260\230\355\231\230\355\225\230\353\235\274_\354\230\201\355\233\204.md" create mode 100644 "item60/item60_\354\240\225\355\231\225\355\225\234 \353\213\265\354\235\264 \355\225\204\354\232\224\355\225\230\353\213\244\353\251\264 float\354\231\200 double\354\235\200 \355\224\274\355\225\230\353\235\274.md" create mode 100644 "item63/item63_\353\254\270\354\236\220\354\227\264 \354\227\260\352\262\260\354\235\200 \353\212\220\353\246\254\353\213\210 \354\243\274\354\235\230\355\225\230\353\235\274.md" create mode 100644 "item66/item66_\353\204\244\354\235\264\355\213\260\353\270\214 \353\251\224\354\204\234\353\223\234\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274.md" diff --git "a/item22/item22_\354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\212\224 \355\203\200\354\236\205\354\235\204 \354\240\225\354\235\230\355\225\230\353\212\224 \354\232\251\353\217\204\353\241\234\353\247\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/item22/item22_\354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\212\224 \355\203\200\354\236\205\354\235\204 \354\240\225\354\235\230\355\225\230\353\212\224 \354\232\251\353\217\204\353\241\234\353\247\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..41a025f --- /dev/null +++ "b/item22/item22_\354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\212\224 \355\203\200\354\236\205\354\235\204 \354\240\225\354\235\230\355\225\230\353\212\224 \354\232\251\353\217\204\353\241\234\353\247\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,66 @@ +## item22 인터페이스는 타입을 정의하는 용도로만 사용하라 + +- 인터페이스의 역할: 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것이다. (인터페이스는 오직 이 용도로만 사용해야 한다.) +- 이 지침을 어긴 케이스: 상수 인터페이스 +- 상수 인터페이스: 메서드 없이, 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스 + +```java + public interface Constants { + static final int WIDTH = 800; + static final int HEIGHT = 600; + static final String TITLE = "My App"; + } + //상수 인터페이스를 상속해서 사용 + public class MyWindow implements Constants { + public void print() { + System.out.println("창 너비: " + WIDTH); + } + } +``` + +- 클래스 내부에서 사용하는 상수는 내부 구현에 해당된다. 즉, 상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스의 API로 노출하는 행위다. +- 그 이유는 사용자의 입장에서 위의 코드는 아래의 코드처럼 보인다. + + ```java + MyWindow window = new MyWindow(); + window instanceof Constants //true! + ``` + + 즉,외부에서는 *이 클래스가 Constants라는 타입을 구현하고 있다*고 인식하게 된다. + +- 상수 인터페이스는 사용자에게 아무런 의미가 없고 오힐 사용자에게 혼란을 주기도 하며, 더 심하게는 클라이언트 코드가 내부 구현에 해당하는 이 상수들에 종속되게 한다. +- 예를 들어, + + ```java + if (userInput.equals(Constants.TITLE)) { + ... + } + ``` + + 이런 코드를 사용하다가 Constants.TITLE이 더 이상 필요가 없어져서 지우고 싶어도 이걸 사용하는 모든 사용자의 코드가 깨지거나 이런 오류를 방지하기 위해 억지로 인스페이스를 계속 유지시켜야 한다. + +--- + +- 그러면 이런 상수들의 묶음은 어떻게 표시해야 좋을까? + +1. 특정 클래스나 인터페이스와 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가해야 한다. 대펴적으로 Integer와 Double에 선언된 MAX/MIN_VALUE가 있다. +2. 열거 타입으로 나타내기 적합한 상수라면 열거 타입으로 만들어 공개하면 된다. +3. 인스턴스화할 수 없는 유틸리티 클래스에 담아서 공개하면 된다. + 위의 예시를 유틸리티 클래스를 바꾸어 보면 아래와 같은 코드가 된다. + +```java +public class Constants { + private Constants() {} //인스턴스화 방지 + + public static final int WIDTH = 800; + public static final int HEIGHT = 600; + public static final String TITLE = "My App"; +} +``` + +- 유틸리티 클래스에 정의된 상수를 클라이언트에서 사용하려면 클래스 이름까지 함께 명시해야 한다. (_Constants.WIDTH_) -> 정적 임포트하여 클래스 이름은 생략할 수도 있다. + +--- + +- 핵심 정리 + - _인터페이스는 타입을 정의하는 용도로만 사용해야 한다. 상수 공개용 수단으로 사용하지 말자._ diff --git "a/item27/item27_\353\271\204\352\262\200\354\202\254 \352\262\275\352\263\240\353\245\274 \354\240\234\352\261\260\355\225\230\353\235\274.md" "b/item27/item27_\353\271\204\352\262\200\354\202\254 \352\262\275\352\263\240\353\245\274 \354\240\234\352\261\260\355\225\230\353\235\274.md" new file mode 100644 index 0000000..586f4bc --- /dev/null +++ "b/item27/item27_\353\271\204\352\262\200\354\202\254 \352\262\275\352\263\240\353\245\274 \354\240\234\352\261\260\355\225\230\353\235\274.md" @@ -0,0 +1,32 @@ +## item27 비검사 경고를 제거하라 + +- 모든 비검사 경고를 제거한다면 그 코드는 타입 안정성이 보장된다. 즉, 런타임에 ClassCastException이 발생할 일이 없고, 우리가 의도한 대로 잘 동작하리라 확신할 수 있다. +- 경고를 제거할 수는 없지만 타입 안전하다고 확신할 수 있다면 @SuppressWarnings("unchecked") 애너테이션을 달아 경고를 숨길 수 있다. +- 만약 안전하다고 검증된 비검사 경고를 그대로 두면, 진짜 문제를 알리는 새로운 경고가 나와도 눈치채지 못할 수 있다. 따라서 안전함이 검증됐다면 숨겨주는 습관을 가지도록 하자. +- @SuppressWarnings 애너테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있지만 항상 가능한 한 좁은 범위에 적용하는 것이 좋다. (보통 변수 선언, 아주 짧은 메서드 혹은 생성자) 클래스같은 큰 범위에 적용하면 심각한 경고를 놓칠 수 있기 때문이다. +- 한 줄이 넘는 메서드나 생성자에 달린 @SuppressWarnings 애너테이션을 발견하면 지역변수 선언 쪽으로 옮기자. +- 예를 들어, + +```java + public T[] toArray(T[] a) { + if (a.length < size) + return (T[]) Arrays.copyOf(elements, size, a.getClass()); + System.arraycopy(elements, 0, a, 0, size); + if(a.length > size) + a[size] = null; + return a; + } +``` + +ArrayList의 toArray 메서드를 컴파일 하면 아래와 같은 경고가 발생한다. + +```java + ArrayList.java:305: warning: [unchecked] unchecked cast + return (T[]) Arrays.copyOf(elements, size, a.getClass()); + required: T[] + found: Object[] +``` + +@SuppressWarnings 애너테이션은 선언에만 달 수 있기 때문에 return문에는 달지 못한다. 그렇다면 메서드 자체에 달고 싶겠지만, 범위가 필요이상으로 넓어지게 된다. 그 대신 반환값을 담을 지역변수를 하나 선언하고 그 변수에 @SuppressWarnings 애너테이션을 달아주면 된다. + +- _@SuppressWarnings 애너테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다._ diff --git "a/item32/item32_\354\240\234\353\204\244\353\246\255\352\263\274 \352\260\200\353\263\200\354\235\270\354\210\230\353\245\274 \355\225\250\352\273\230 \354\223\270 \353\225\214\353\212\224 \354\213\240\354\244\221\355\225\230\353\235\274.md" "b/item32/item32_\354\240\234\353\204\244\353\246\255\352\263\274 \352\260\200\353\263\200\354\235\270\354\210\230\353\245\274 \355\225\250\352\273\230 \354\223\270 \353\225\214\353\212\224 \354\213\240\354\244\221\355\225\230\353\235\274.md" new file mode 100644 index 0000000..6c31ede --- /dev/null +++ "b/item32/item32_\354\240\234\353\204\244\353\246\255\352\263\274 \352\260\200\353\263\200\354\235\270\354\210\230\353\245\274 \355\225\250\352\273\230 \354\223\270 \353\225\214\353\212\224 \354\213\240\354\244\221\355\225\230\353\235\274.md" @@ -0,0 +1,69 @@ +## item32 제네릭과 가변인수를 함께 쓸 때는 신중하라. + +- 가변인수는 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있지만, 구현 방식에서 허점이 있다. + + -> 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어지는데, 내부로 감춰야 했을 이 배열이 클라이언트에 노출되는 문제가 생겼다. + + 즉, _varargs 매개변수에 제네릭이나 매개변수화 타입이 포함되면 컴파일 경고가 발생한다._ + + -> varargs 매개변수는 배열이다. 여기에 제네릭 타입을 넣으면 제네릭 배열(JAVA에서 지원하지 않음)이 된다. 배열과 제네릭의 타입 시스템 차이에 의해 타입 안전성이 붕괴되고, 이로 인해 *힙 오염*이 발생된다. + +```java + static void dangerous(List... stringLists) { + List intList = List.of(42); + Object[] objects = stringLIsts; + objects[0] = intList; //힙 오염 발생 + String s = stringLists[0].get(0); //ClassCastException + //마지막 줄에 컴파일러가 생성한 (보이지 않는) 형변환이 숨어있기 때문이다. + } +``` + +- 제네릭 배열을 프로그래머가 직접 생성하는 건 허용하지 않으면서 제네릭 varargs 매개변수를 받는 메서드를 선언할 수 있는 이유가 무엇일까? -> 즉, 왜 오류가 아닌 경고로 끝일까? + + 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드가 실무에서 매우 유용하기 때문이다. 그래서 언어 설계자는 이 모순을 수용하기로 했다. (실제 자바 라이브러리도 이런 메서드를 여럿 제공한다.) + +- 자바 7이전에는 @SuppressWarnings("unchecked")로 일일히 경고를 숨겨야 했지만, 자바 7에서 @SafeVarargs 애너테이션(메서드 작성자가 그 메서드가 타입 안정함을 보장하는 장치)이 추가 되어 작성자가 클라이언트 측에서 발생하는 경고를 숨길 수 있게 되었다. +- 메서드가 안전한지는 어떻게 확신할 수 있을까? + + -> 메서드가 이 배열에 아무 것도 저장하지 않고 그 배열의 참조가 밖으로 노출되지 않는다면 타입 안전하다. 바꿔 말해서, 이 varargs 매개변수 배열이 호출자로부터 그 메서드로 순수하게 인수들을 전달하는 일만 한다면 그 메서드는 안전하다. + +- 단, 이것이 무조건적으로 옳은 것은 아니다. + + 예를 들어, + + ```java + static T[] pickTwo(T a, T b, T c) { + switch(ThreadLocalRandom.current().nextInt(3)) { + case 0: return toArray(a, b); + case 1: return toArray(b, c); + case 2: return toArray(c, a); + } + throw new AssertionError(); //도달할 수 없다. + } + + public static void main(String[] args) { + String[] attributes = pickTwo("좋은","빠른","저렴한"); + } + ``` + + - pickTwo의 반환값을 attributes에 저장하기 위해 String[]로 형변환하는 코드를 컴파일러가 자동 생성한다는 점을 놓쳤다. Object[]는 String[]의 하위 타입이 아니므로 이 형변환은 실패한다. + + _따라서, 제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하면 안전하지 않다!_ + + - 물론 여기에도 예외 2가지가 있다. + + 1. @SafeVarargs로 제대로 애노테이트된 또 다른 varargs 메서드에 넘기는 것은 안전하다. + 2. 그저 이 배열 내용의 일부 함수를 호출만 하는(varags를 받지 않는) 일반 메서드에 넘기는 것도 안전하다. + +- @SafeVarargs 애너테이션을 사용할 때 정하는 규칙 : 제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 모든 메서드에 @SafeVarargs를 달아야 한다.(안전하지 않은 varargs 메서드는 절대 작성해서는 안 된다.) + +- 정리 + + 1. varages 매개변수 배열에 아무것도 저장하지 않는다. + 2. 그 배열(혹은 복제본)을 신뢰할 수 없는 코드에 노출하지 않는다. + + *** + + 참고 + + @SafeVarargs 애너테이션이 유일한 정답이 아니다. varargs 매개변수를 List 매개변수로 바꿀 수도 있다. (아이템 28의 내용) diff --git "a/item40/item40_@Override \354\225\240\353\204\210\355\205\214\354\235\264\354\205\230\354\235\204 \354\235\274\352\264\200\353\220\230\352\262\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/item40/item40_@Override \354\225\240\353\204\210\355\205\214\354\235\264\354\205\230\354\235\204 \354\235\274\352\264\200\353\220\230\352\262\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..799dbc4 --- /dev/null +++ "b/item40/item40_@Override \354\225\240\353\204\210\355\205\214\354\235\264\354\205\230\354\235\204 \354\235\274\352\264\200\353\220\230\352\262\214 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,63 @@ +## item40 @Override 애너테이션을 일관되게 사용하라 + +- @Override는 프로그래머에게 가장 중요한 애너테이션일 것이다. 이것은 메서드 선언에만 달 수 있고, 이 애너테이션이 달렸다는 것은 상위 타입의 메서드를 재정의했다는 것을 의미한다. +- @Override 애너테이션을 일관되게 사용하면 여러 버그들을 예방해준다. 다음의 Bigram 프로그램을 살펴보자.(바이그램, 여기서는 영어 알파벳 2개로 구성된 문자열을 표현) + +```java +public class Bigram { + private final char first; + private final char second; + + public Bigram(char first, char second) { + this.first = first; + this.second = second; + } + public boolean equals(Bigram b) { + return b.first == first && b.second = second; + } + public int hashCode() { + return 31 * first + second; + } + + public static void main(String[] args) { + Set s = new HashSet<>(); + + for(int i = 0; i < 10; i++) + for(char ch = 'a'; ch <= 'z'; ch++) + s.add(new Bigram(ch,ch)); + System.out.println(s.size()); + } +} +``` + +- 위의 코드는 a부터 z까지 26개의 바이그램을 10번 반복해 집합에 추가한 다음, 집합의 크기를 출력하려는 것으로 보인다. Set은 중복을 허용하지 않기 때문에 결과는 26으로 나올길 원했던 것 같다. 하지만 실제로는 260이 출력된다. 그 원인을 하나 하나 살펴보자. + + 1. 작성자는 equals 메서드를 재정의하려 한 것으로 보이고 hashCode도 함께 재정의해야 한다는 사실을 잊지 않았다. 하지만 equals는 재정의가 아니라 _다중정의_ 해버렸다. + 2. Object의 equals를 재정의하려면 매개변수 타입을 Object로 해야했는데 그렇게 하지 않았다. 그래서 Objcet에서 상속한 equals와는 별개인 equals를 새로 정의한 꼴이 됐다. + 3. Object의 equals는 ==연산자와 똑같이 객체 식별성만을 확인한다. 따럿 같은 소문자를 소유한 바이그램 10개의 각각이 서로 다른 객체로 인식되고 결과가 260이 된 것이다. + + 이제 이를 해결하기 위해 equals 메서드에 @Override를 붙이면 + + ```java + Bigram.java:10: method does not override or implement a method from a supertype + @Override public boolean equals(Bigram b) + ``` + + 꼴의 컴파일 오류가 발생한다. 앞서 말했듯 매개변수 타입이 Object 타입이어야 하는데 Bigram이므로 오류가 발생한 것이다. 이를 올바르게 수정하면 아래와 같은 형태이다. + + ```java + @Override + public boolean equals(Object o) { + if(!(o instanceof Bigram)) return false; + Bigram b = (Bigram) o; + return b.first == first && b.second == second; + } + ``` + +- 중요! + + _상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달자!_ + + 예외는 한 가지 뿐이다. 구체 클래스에서 상위 클래스의 추상 메서드를 재정의할 때는 굳이 @Override를 달지 않아도 된다. 물론 재정의 메서드 모두에 @Override를 일괄로 붙여두는게 좋아 보인다면 그래도 상관없다. + +- 한편, IDE는 @Override를 일관되게 사용하도록 부추기기도 한다. @Override가 달려있지 않은 메서드가 실제로는 재정의를 했다면 경고를 준다. 또 @Override는 클래스뿐 아니라 인터페이스의 메서드를 재정의할 때도 사용할 수 있다.디폴트 메서드를 지원하기 시작하면서, 인터페이스의 메서드를 구현한 메서드에도 @Override를 다는 습관을 들이면 시그니처가 올바른지 재차 확신할 수 있다. diff --git "a/item42/item42_\354\235\265\353\252\205 \355\201\264\353\236\230\354\212\244\353\263\264\353\213\244\353\212\224 \353\236\214\353\213\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274_\354\230\201\355\233\204.md" "b/item42/item42_\354\235\265\353\252\205 \355\201\264\353\236\230\354\212\244\353\263\264\353\213\244\353\212\224 \353\236\214\353\213\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274_\354\230\201\355\233\204.md" new file mode 100644 index 0000000..1052e03 --- /dev/null +++ "b/item42/item42_\354\235\265\353\252\205 \355\201\264\353\236\230\354\212\244\353\263\264\353\213\244\353\212\224 \353\236\214\353\213\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274_\354\230\201\355\233\204.md" @@ -0,0 +1,116 @@ +예전에는 함수 타입을 표현할 떄 추상 메서드를 하나만 담은 인터페이스나 클래스를 사용했다. + +이때 이런 인터페이스의 인스턴스를 '함수 객체'라고 부른다. + +JDK 1.1 이후에는 익명 클래스를 사용해서 함수 객체를 만들었다. + +아래 함수를 익명 클래스를 활용한 정렬 함수의 예이다. +```java +Collections.sort(words, new Comparator() { + public int compare(String s1, String s2) { + return Integer.compare(s1.length(), s2.length()); + } +}); +``` + +하지만 코드가 너무 길다. + +### 람다식 + +자바8에 오면서 위의 함수 객체는 람다식을 활용해서 만들 수 있게 되었다. + +람다는 익명 클래스와 비슷한 기능을 수행하지만 훨씬 명확하고 간결하게 표현할 수 있다. + +```java +Collections.sort(words, + (s1, s2) -> Integer.compare(s1.length(), s2.length())); +``` + +여기서 람다식이나 s1,s2의 타입은 따로 명세해주지 않아도 알아서 타입 추론을 해준다. + +코드의 간결함을 위해 타입을 더 명확히 명세해줄 필요가 있거나 컴파일러가 오류를 뱉을때를 제외하면 타입 명세를 생략하는 것이 좋다. + +> [!info] +> 컴파일러는 타입을 추론하기 위한 대부분의 정보를 제네릭에서 가져온다. 따라서 람다를 사용해야 한다면 더욱이 로 타입을 지양하고 제네릭을 활용해야 한다. + + +위의 코드에서 비교자 생성 메서드를 활용하면 코드를 줄일 수 있으며, List 인터페이스의 sort 메서드를 활용하면 더 줄어들 수 있다. +```java +Collections.sort(words, comparinglnt(String::length)); + +words.sort(comparinglnt(String::length)); +``` + +### 활용 + +아이템 34에서는 Operation 열거 타입에서 각 상수별 apply 메서드를 재정의했다. +```java +public 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); +} +``` +- 이렇게 구현하는 방식보다는 열거 타입에 인스턴스 필드를 두는 방식이 좋다. +- 이때 람다를 활용하면 아래와 같은 형태로 구현할 수 있다. + + +```java +public 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); + } +} +``` +- 각 상수별 동작을 람다로 구현해 생성자에 넘기면, 생성자는 이 람다를 인스턴스 필드에 저장해둔다. +> [!info] +> DoubleBinaryOperator 인터페이스는 함수 인터페이스 중 하나로, 저장된 람다를 double로 실행할 수 있게 한다. + + +### 주의점 + +- 코드 자체로 로직이 설명되지 않거나 코드가 길어진다면 쓰면 안된다. + - 람다는 메서드나 클래스와는 달리 이름이 없으며, 문서화도 할 수 없기 때문 + - 만약 3줄을 넘어간다면 더 줄이거나 람다를 쓰지 않는것이 좋다. +- 람다는 인스턴스 필드나 메서드를 사용하지 못한다. +- 추상 클래스의 인스턴스를 만들어야 하거나, 추상 메서드가 여러개인 인터페이스의 인스턴스를 만들 때는 익명 클래스를 사용해야 한다. +- 만약 함수 객체가 자기 자신을 참조해야 한다면 익명 클래스를 활용해야 한다. + - 람다는 자기 자신이 아닌 바깥 인스턴스를 가리키기 때문 +- 람다(그리고 익명 클래스)는 직렬화 방식이 VM별로 다르기 때문에 절대 삼가야 한다. + - 만약 직렬화가 필요하다면 private 정적 중첩 클래스를 사용해야 한다. + + +### 정리 +> 람다를 활용하면 작은 함수 객체를 간결하게 구현할 수 있다. +> 다만 항상 람다를 남용해서는 안되며, 상황에 알맞게 람다와 익명 클래스를 활용하자. \ No newline at end of file diff --git "a/item43/item43_\353\236\214\353\213\244\353\263\264\353\213\244\353\212\224 \353\251\224\354\204\234\353\223\234 \354\260\270\354\241\260\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/item43/item43_\353\236\214\353\213\244\353\263\264\353\213\244\353\212\224 \353\251\224\354\204\234\353\223\234 \354\260\270\354\241\260\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..d626b36 --- /dev/null +++ "b/item43/item43_\353\236\214\353\213\244\353\263\264\353\213\244\353\212\224 \353\251\224\354\204\234\353\223\234 \354\260\270\354\241\260\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,77 @@ +### item43 람다보다는 메서드 참조를 사용하라. + +- 자바에는 람다보다 함수 객체를 간결하게 만드는 메서드 참조(method reference)라는 방법이 있다. + + 이를 설명하기 위해 merge 메서드를 예시로 들어보겠다. + + ```java + map.merge(key, 1, (count, incr) -> count + incr); + ``` + + 위의 예시는 람다를 이용해 merge 메서드를 사용한 것이다. merge 메서드는 키, 값, 함수를 인수로 받으며, 주어진 키가 맵 안에 아직 없다면 주어진 {키, 값} 쌍을 그대로 저장한다. 반대로 키가 이미 있다면 (인수로 받은)함수를 현재 값과 주어진 값에 적용한 다음, 그 결과로 현재 값을 덮어쓴다. 즉, 맵에 {키, 함수의 결과} 쌍을 저장한다. + + 위의 식도 충분히 간단하지만 메서드 참조를 쓰면 이것보다 훨씬 간결하게 코드 작성이 가능해진다. + + ```java + map.merge(key, 1, Integer: :sum); + ``` + + (추가 설명) -> 람다는 기존에 존재하는 메서드를 그대로 호출하는 것이라면, 메서드 참조는 그 메서드를 직접 가리키는 방식이다. + + 매개변수 수가 늘어날수록 메서드 참조로 제거한 수 있는 코드양도 늘어난다. 하지만 어떤 람다에서는 매개변수의 이름 자체가 프로그래머에게 좋은 가이드가 되기도 한다. + +- 람다로 할 수 없는 일이라면 메서드 참조로도 할 수 있다.(마지막에 보충 설명) 보통 메서드 참조를 사용하는 편이 더 짧고 간결하므로 람다를 대신할 좋은 대안이 되어준다. 즉, 람다로 작성할 코드를 새로운 메서드에 담은 다음, 람다 대신 그 메서드 참조를 사용하는 식이다. + + 하지만 언제나 그렇듯 늘 그런 것은 아니다. 때론 람다가 메서드 참조 보다 간결할 때가 있다. 주로 메서드와 람다가 같은 클래스에 있을 때 그렇다. + + ```java + service.execute(GoshThisClassIsHumangous: :action); //메서드 참조 + service.execute((): :action); //람다식 + ``` + +- 메서드 참조의 유형 + + 1. 정적 메서드 참조(위에서 설명한 것) + + ```java + Function func = (s) -> Integer.parseInt(s); + Function func = Integer: :parseInt; + ``` + + 2. 특정 객체의 인스턴스 메서드 참조 + + ```java + String str = "hello"; + Supplier sup = () -> str.length(); + Supplier sup = str: :length; + ``` + + 3. 임의 객체의 인스턴스 메서드 참조 + + ```java + List list = Arrays.asList("a", "b", "c"); + + // 람다식 + list.forEach(s -> System.out.println(s)); + + // 메서드 참조 + list.forEach(System.out::println); + ``` + + 4. 생성자 참조(클래스& 배열) + + ```java + //생성자 메서드 참조 + Supplier> sup = () -> new ArrayList<>(); + Supplier> sup = ArrayList::new; + + //배열 메서드 참조 + IntFunction lambda = size -> new String[size]; + IntFunction methodRef = String[]::new; + ``` + +- 보충 설명 + + 람다로는 불가능하나 메서드 참조로는 가능한 유일한 형태가 하나 있는데 바로 제네릭 함수 타입 구현이다. + + 함수형 인터페이스의 추상 메서드가 제네릭일 수 있듯이 함수 타입도 제네릭일 수 있다. 이는 메서드 참조 표현식으로는 구현할 수 있지만, 제네릭 람다식이라는 문법은 존재하지 않기 때문에 람다식으로는 불가능하다. diff --git "a/item48/item48_\354\212\244\355\212\270\353\246\274 \353\263\221\353\240\254\355\231\224\353\212\224 \354\243\274\354\235\230\355\225\264\354\204\234 \354\240\201\354\232\251\355\225\230\353\235\274.md" "b/item48/item48_\354\212\244\355\212\270\353\246\274 \353\263\221\353\240\254\355\231\224\353\212\224 \354\243\274\354\235\230\355\225\264\354\204\234 \354\240\201\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..ad363c9 --- /dev/null +++ "b/item48/item48_\354\212\244\355\212\270\353\246\274 \353\263\221\353\240\254\355\231\224\353\212\224 \354\243\274\354\235\230\355\225\264\354\204\234 \354\240\201\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,77 @@ +### item48 스트림 병렬화는 주의해서 적용하라 + +- 자바는 주류 언어 중, 동시성 프로그래밍 측면에서 항상 앞서 있었다. 자바 8부터 parallel 메서드만 한 번 호출하면 파이프라인을 작성하기가 점점 쉬워지고 있지만, 이를 빠르게 작성하는 일은 별개의 이야기이다. +- 동시성 프로그래밍을 할 때는 안전성과 응답 가능 상태를 유지하기 위해 애써야 하는데, 병렬 스트림 파이프라인 프로그래밍에서도 다를 바 없다. +- 아이템 45에 다루었던 메르센 소수를 다시 살펴보자. + +```java +public static void main(String[] args) { + prims().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)) + .filter(mersenne -> mersenne.isProbablePrime(50)) + .limit(20) + .forEach(System.out.println); +} + +static Stream primes() { + return Stream.iterate(TWO, BigInteger::nextProbablePrime); +} +``` + +- 이 프로그램의 속도를 높이고 싶어 스트림 파이프라인의 parallel()을 호출했다고 치자. 이렇게 하면 안타깝게도 아무것도 출력하지 못하면서 CPU는 90%나 잡아먹는 상태가 무한히 계속된다(응답 불가). 결국에는 완료될지 몰라도 1시간 반이 지나 강제 종료할 때까지 아무 결과 출력하지 않는다. + + 왜 그럴까? 그 이유는, 스트림 라이브러리가 이 파이프라인을 병렬화하는 방법을 찾아내지 못했기 때문이다. + + - 환경이 아무리 좋더라도 데이터 소스가 Stream.iterate거나 중간 연산으로 limit를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다. 그런데 위의 코드는 두 문제를 모두 지니고 있다. + + 뿐만 아니라, 파이프라인 병렬화는 limit를 다룰 때 CPU 코어가 남는다면 원소를 몇 개 더 처리한 후 제한된 개수 이후의 결과를 버려도 아무런 해가 없다고 가정한다. 그런데 이 코드의 경우 새롭게 메르센 소수를 찾을 때마다 그 전 소수를 찾을 때보다 두 배 정도 더 오래 걸린다. 즉, 원소 하나를 계산하는 비용이 그 이전까지의 원소 전부를 계산한 비용을 합친 것만큼 든다는 뜻이다. + +- 스트림의 소스가 ArrayList, HashMap 등의 인스턴스거나 배열, int 범위, long 범위일 때 병렬화의 효과가 가장 좋다. 이 자료들은 모두 데이터를 원하는 크기로 정확하게 나눌 수 있고 다수의 스레드에 분배하기 좋다는 특성이 있다. 또 원소들을 순차적으로 실행할 때 참조 지역성이 뛰어나다. + +- 참조 지역성이란 이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있다는 뜻이다. (마치 배열처럼) + +- 하지만 참조들이 가리키는 실제 객체가 메모리에서 서로 떨어져 있을 수 있는데, 이러면 참조 지역성이 떨어진다. 참조 지역성이 낮으면 스레드는 데이터가 주 메모리에서 캐시 메모리로 전송되어 오기만을 기다린다. 따라서, 참조 지역성은 다량의 데이터를 처리하는 벌크 연산을 병렬화할 때 아주 중요한 요소이다. (참고로 참조 지역성이 가장 뛰어난 자료구조는 배열이다.) + +- 스트림 파이프라인의 종단 연산의 동작 방식도 병렬 수행 효율에 큰 영향을 준다. 종단 연산이 파이프라인 전체 작업에서 상당 비중을 차지하기 때문이다. + + 종단 연산 중 병렬화에 가장 적합한 것은 축소이다. 축소는 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업으로, Stream의 reduce, min, max, count, sum 같은 메서드들이 수행한다. 반면, 가변 축소를 수행하는 Stream의 collect 메서드는 컬렉션들을 합치는 부담이 크기 때문에 병렬화에 적합하지 않다. + +- 지금까지 내용에서의 교훈! _스트림을 마구 잡이로 병렬화하거나 잘못 병렬화하면 성능이 나빠질 뿐만 아니라 결과 자체가 잘못되거나 예상 못한 동작이 발생할 수 있다!_ +- 결과가 잘못되거나 오작동하는 것을 안전 실패라고 한다. 안전 실패는 병렬화한 파이프라인이 사용하는 메서드나 객체가 명세대로 동작하지 않을 때 벌어진다. + Stream 명세는 이때 사용하는 객체에 관해 규약을 정해놨다. + + -예를 들어, Stream의 reduce 연산에 건네지는 accumulator(누적기)와 combiner(결합기) 함수는 반드시 결합법칙을 만족하고, 간섭받지 않고, 상태를 갖지 않아야 한다. + + 이 요구사항을 지키지 못하는 상태라도 파이프라인을 *순차적*으로만 수행하면 문제없다. + +- 스트림 병렬화는 데이터 소스 스트림이 효율적으로 나눠지고, 병렬화하거나 빨리 끝나는 종단 연산을 사용하고, 함수 객체들도 간섭하지 않더라도, 파이프라인이 수행하는 진짜 작업이 병렬화에 드는 추가 비용을 상쇄하지 못한다면 성능 향상은 미미할 수 있다. + + 그래서 이를 실제로 성능 향상이 될지 추정해보는 간단한 방법이 있다. (스트림 안의 원소 수) \* (원소당 수행되는 코드줄 수) 공식을 이용하는 것이다. 이 값이 최소 수십만은 되어야 성능 향상을 맛볼 수 있다. + +- 스트림 병렬화는 오직 성능 최적화 수단임을 기억해야 한다. 반드시 테스르를 거쳐서 병렬화를 사용할 가치가 있는지 확인해야 한다. 그렇지 않으면 이를 사용해도 손해만 보는 상황이 생긴다. + +-마지막으로 병렬화가 효과를 제대로 발휘한 간단한 예를 보자. + +```java +static long pi(long n) { + return LongStream.rangeClosed(2, n) + .mapToObj(BigInteger::valueOf) + .filter(i -> i.isProbablePrime(50)) + .count(); +} +``` + +n보다 작거나 같은 소수의 개수를 계산하는 함수이다. 이를 돌려보면 31초가 걸렸는데 여기에 parallel() 호출을 하나 추가하면 9.2초로 단축됐다. + +```java +static long pi(long n) { + return LongStream.rangeClosed(2, n) + .parallel() + .mapToObj(BigInteger::valueOf) + .filter(i -> i.isProbablePrime(50)) + .count(); +} +``` + +물론 이 역시도 항상 옳은 것은 아니다. n이 엄청 거대해지면 레머의 공식이라는 훨씬 효율적인 알고리즘을 사용하는 것이 옳다. + +- 무작위 수들로 이뤄진 스트림을 병렬화할 때는 ThreadLocalRandom(구식으로는 그냥 Random)보다는 SplittableRandom 인스턴스를 이용하자. SplittableRandom은 정확히 이럴 때 사용하라고 설계된 것이라 성능이 선형으로 증가한다. 한편 ThreadLocalRandom은 단일 스레드에서 쓰고자 만들어졌다. 병렬 스트림용 데이터 소스에도 쓸 수 있지만 SplittableRandom만큼 빠르지는 않다. 마지막으로 그냥 Random은 모든 연산을 동기화하기 때문에 병렬 처리하면 최악의 성능을 보일 것이다. diff --git "a/item53_\352\260\200\353\263\200\354\235\270\354\210\230\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274/item53_\352\260\200\353\263\200\354\235\270\354\210\230\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/item53/item53_\352\260\200\353\263\200\354\235\270\354\210\230\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274.md" similarity index 100% rename from "item53_\352\260\200\353\263\200\354\235\270\354\210\230\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274/item53_\352\260\200\353\263\200\354\235\270\354\210\230\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274.md" rename to "item53/item53_\352\260\200\353\263\200\354\235\270\354\210\230\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274.md" diff --git "a/item54/item54_null\354\235\264 \354\225\204\353\213\214, \353\271\210 \354\273\254\353\240\211\354\205\230\354\235\264\353\202\230 \353\260\260\354\227\264\354\235\204 \353\260\230\355\231\230\355\225\230\353\235\274_\354\230\201\355\233\204.md" "b/item54/item54_null\354\235\264 \354\225\204\353\213\214, \353\271\210 \354\273\254\353\240\211\354\205\230\354\235\264\353\202\230 \353\260\260\354\227\264\354\235\204 \353\260\230\355\231\230\355\225\230\353\235\274_\354\230\201\355\233\204.md" new file mode 100644 index 0000000..c806c39 --- /dev/null +++ "b/item54/item54_null\354\235\264 \354\225\204\353\213\214, \353\271\210 \354\273\254\353\240\211\354\205\230\354\235\264\353\202\230 \353\260\260\354\227\264\354\235\204 \353\260\230\355\231\230\355\225\230\353\235\274_\354\230\201\355\233\204.md" @@ -0,0 +1,76 @@ +```java +private final List chessesInStock = ...; + +/** + * (Qreturn 매장 안의 모든 치즈 목록을 반환한다. + * 단, 재고가 하나도 없다면 null을 반환한다. + */ +public List getCheeses() { + return cheesesInStock.isEmpty() ? null + : new ArrayList<>(cheesesInStock); +} +``` + +- 만약 위의 코드처럼 null을 반환한다면, 받는 쪽에서 이 null을 처리하는 코드를 따로 만들어 주어야 한다 +```java +List cheeses = shop.getCheeses(); +if (cheeses != null && cheeses.contains(Cheese.STILTON)) +System.out.println("좋았어, 바로 그거야."); +``` + +- 이처럼 방어하는 코드를 넣어두지 않으면 오류가 발생할 수 있다. +- 반대로 반환하는 쪽에서도 특수 처리를 해야 하기 때문에 코드는 더 복잡해진다. + +### 빈 컬렉션 + +빈 컨테이너를 null대신 반환하면 성능이 저하 된다는 주장은 두 가지로 반박이 가능하다. +1. 빈 컨테이너 할당은 딱히 신경 쓸 정도의 성능 저하가 일어나지 않는다. +2. 빈 컬렉션과 배열은 굳이 새로 할당하지 않고도 반환할 수 있다. + +다음 코드는 빈 컬렉션을 반환하는 전형적인 코드이다. +```java +public List getCheeses() { + return new ArrayList<>(cheesesInStock); +} +``` + +만약 빈 컬렉션 할당이 확연히 성능 저하를 일으킨다고 한다면, 빈 불변 컬렉션을 반환하자. + +Collections에는 emptyList,emptyMap,emptySet 등이 존재한다. +```java +public List getCheeses() { + return cheesesInStock.isEmpty() ? Collections.emptyList() + : new ArrayListo(cheesesInStock); +} + +``` + +다만 정말로 성능 저하가 발생함을 확인한 후, 성능이 개선됨을 확인했을 경우에 사용하자. + + +### 빈 배열 + +- 배열을 반환할 때도 절대 null 말고 길이가 0인 배열을 반환하자. +- 아래 코드는 길이가 0일수도 있는 배열을 반환하는 방법이다. +```java +public Cheese[] getCheeses() { + return cheesesInStock.toArray(new Cheese[0]); +} +``` +- 여기서 toArray에 집어넣은 배열은 반환 타입을 알려주는 역할을 한다. +- 만약 집어넣는 배열을 새로 생성하는 것이 성능 저하 같다면 아래와 같이 하면 된다. +```java +private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0]; + +public Cheese[] getCheeses() { + return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY); +} +``` +- 미리 final 배열을 생성해두고, 필요할 때마다 동일한 배열을 집어넣어주면 된다. +- 만약 0인 경우에만 EMPTY_CHEESE_ARRAY를 그대로 반환한다. + +> [! ] +> toArray에 집어 넣은 배열 a가 충분히 크다면 그 안에 원소를 집어넣고, 그렇지 않다면 새로 생성한다. 위의 코드에서는 원소가 0일 때만 EMPTY_CHEESE_ARRAY를 그대로 반환할 것이다. + +- 다만 오히려 성능이 떨어진다는 말도 있는 만큼, 성능 개선이 목적이라면 추천하지 않는다 + diff --git "a/item60/item60_\354\240\225\355\231\225\355\225\234 \353\213\265\354\235\264 \355\225\204\354\232\224\355\225\230\353\213\244\353\251\264 float\354\231\200 double\354\235\200 \355\224\274\355\225\230\353\235\274.md" "b/item60/item60_\354\240\225\355\231\225\355\225\234 \353\213\265\354\235\264 \355\225\204\354\232\224\355\225\230\353\213\244\353\251\264 float\354\231\200 double\354\235\200 \355\224\274\355\225\230\353\235\274.md" new file mode 100644 index 0000000..83f7b7d --- /dev/null +++ "b/item60/item60_\354\240\225\355\231\225\355\225\234 \353\213\265\354\235\264 \355\225\204\354\232\224\355\225\230\353\213\244\353\251\264 float\354\231\200 double\354\235\200 \355\224\274\355\225\230\353\235\274.md" @@ -0,0 +1,66 @@ +### item60 정확한 답이 필요하다면 float과 double은 피하라. + +- float과 double 타입은 과학과 공학 계산용으로 설계되었다. 이진 부동소수섬 연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 `근사치`로 계산하도록 설계되었다. +- 앞의 말에서 알 수 있듯이 `근사치`를 계산하는 것이기 때문에 정확한 결과가 필요할 때는 사용하면 안 된다. (0.1 혹은 10의 음의 거듭제곱 수를 표현할 수 없기 때문이다.) +- 예를 들면 + + ```java + public static void main(String[] args) { + double funds = 1.00; + int itemsBought = 0; + for(double price = 0.10; funds >= price; price += 0.10) { + funds -= price; + itemsBought++; + } + System.out.println(intemsBought + "개 구입"); + System.out.println("잔돈(달러)" + funds); + } + ``` + + 원래의 계산대로면 `0.1 + 0.2 + 0.3 + 0.4 = 1.0`으로 총 4개의 사탕을 구매하고 잔돈은 0원이 남아야 하지만, 실제로는 3개의 사탕을 구매하고 잔돈이 0.399999999999달러가 남게 된다. + + 그렇다면, 이 문제를 해결하기 위해선 어떻게 해야될까? BigDecimal로 교체해주면 된다. + + ```java + public static void main(String[] args) { + final BigDecimal TEN_CENTS = new BigDecimal("0.10"); + + int itemsBought = 0; + BigDecimal funds = new BigDecimal("1.00"); + + for(BigDecimal price = TEN_CENTS; + funds.compareTo(price) >= 0; + price = price.add(TEN_CENTS)) { + funds = funds.subtract(price); + itemsBought++; + } + System.out.println(intemsBought + "개 구입"); + System.out.println("잔돈(달러)" + funds); + } + ``` + + 부정확한 값이 사용되는 걸 막기 위해 BigDecimal의 생성자 중 문자열을 받는 생성자를 사용했다. + + 이렇게 하면 앞의 double 코드에서 발생했던 오류를 말끔히 해결할 수 있다. 하지만 코드에서도 보이듯 문제점이 있다. + + 1. 기본 타입보다 훨씬 쓰기가 불편하고 가독성이 떨어진다. + 2. 훨씬 느리다. + +- 이 방법도 마음에 들지 않으면 BigDecimal 대신 int 혹은 long을 사용하면 된다. 다만 그럴 경우 다룰 수 있는 값의 크기가 제한되고, 소수점을 직접 관리해줘야 한다. long은 double의 타입만 바꿔주면 되기 때문에 생략하고 int의 예를 들어보겠다. + + int로 생성할 때는 그냥 간단하게 소수점을 없애면 된다. 즉, 달러대신 센트로 계산하는 것이다. + + ```java + public static void main(String[] args) { + int funds = 100; + int itemsBought = 0; + for(int price = 10; funds >= price; price += 10) { + funds -= price; + itemsBought++; + } + System.out.println(intemsBought + "개 구입"); + System.out.println("잔돈(센트)" + funds); + } + ``` + +`결론! 언제나 그렇듯 완전무결한 코드는 없다.` diff --git "a/item63/item63_\353\254\270\354\236\220\354\227\264 \354\227\260\352\262\260\354\235\200 \353\212\220\353\246\254\353\213\210 \354\243\274\354\235\230\355\225\230\353\235\274.md" "b/item63/item63_\353\254\270\354\236\220\354\227\264 \354\227\260\352\262\260\354\235\200 \353\212\220\353\246\254\353\213\210 \354\243\274\354\235\230\355\225\230\353\235\274.md" new file mode 100644 index 0000000..4db7960 --- /dev/null +++ "b/item63/item63_\353\254\270\354\236\220\354\227\264 \354\227\260\352\262\260\354\235\200 \353\212\220\353\246\254\353\213\210 \354\243\274\354\235\230\355\225\230\353\235\274.md" @@ -0,0 +1,37 @@ +### item63 문자열 연결은 느리니 주의하라 + +- 문자열 연결 연산자(+)는 문자열을 하나로 합쳐주는 편리한 수단이다. 그런데 크기가 작은 문자열 표현을 만들 때는 괜찮지만, 본격적으로 사용하기 시작하면 성능 저하를 감내하기 어렵다. +- `문자열 연결 연산자로 문자열 n개를 잇는 시간은 n^2에 비례한다.` -> 문자열은 불변이라서 두 문자열을 연결할 경우 양쪽의 내용을 모두 복사해야 하기 때문이다. + + - 예를 들면 + + ```java + public String statement() { + String result = ""; + for (int i = 0; i < numItems(); i++) { + result += lineForItem(i); + } + return result; + } + ``` + + 위의 코드는 result 변수에 아이템의 내용들을 하나하나 이어붙이는 코드이다. 앞서 말했듯이 아이템의 품목이 많지 않다면 큰 문제가 되지 않지만, 품목이 많을 경우 이 메서드는 심각하게 느려질 수 있다. + + - 성능을 포기하고 싶지 않다면 String 대신 StringBuilder를 사용하자. + + ```java + public String statement2() { + StringBuilder b = new StringBuilder(numItems() * LINW_WIDTH); + for (int i = 0; i < numItems(); i++) { + b.append(lineForItem(i)); + } + + return b.toString(); + } + ``` + + statement2가 약 6.5배 빠르다는 결과를 받을 수 있다. statement 메서드의 수행 시간은 품목 수의 제곱이 비례해 늘어나고 statement2는 선형으로 늘어나므로, 품목 수가 늘어날수록 성능 격차는 점점 벌어질 것이다. + +- 핵심 정리 + + -> `많은 문자열을 연결할 때는 문자열 연결 연산자(+)는 피하자.` diff --git "a/item66/item66_\353\204\244\354\235\264\355\213\260\353\270\214 \353\251\224\354\204\234\353\223\234\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/item66/item66_\353\204\244\354\235\264\355\213\260\353\270\214 \353\251\224\354\204\234\353\223\234\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..bf1485b --- /dev/null +++ "b/item66/item66_\353\204\244\354\235\264\355\213\260\353\270\214 \353\251\224\354\204\234\353\223\234\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,55 @@ +### item66 네이티브 메서드는 신중히 사용하라 + +- 자바 네이티브 인터페이스(JNI)는 자바 프로그램이 네이티브 메서드를 호출하는 기술이다. 여기서 `네이티브 메서드란 C나 C++같은 외부 프로그래밍 언어로 작성한 메서드를 말한다.`(네이티브 메서드를 사용할 때는 native 키워드를 이용한다.) 전통적으로 네이티브 메서드의 주요 쓰임은 총 세 가지이다. + +``` +1. 레지스트리 같은 플랫폼 특화 기능을 사용한다. +2. 네이티브 코드로 작성된 기존 라이브러리를 사용한다. +레거시 데이터를 사용하는 레거시 라이브러기가 대표적인 예이다. -> 레거시 데이터란 번역하면 오래된 데이터이다. 즉, 예전에 작성한 코드에서 사용하는 데이터들을 의미한다. +3. 성능 개선을 목적으로 성능에 결정적인 영향을 주는 영역만 따로 네이티브 언어로 작성한다. +``` + +- 다음은 실제 네이티브 메서드를 쓰는 예시이다. + +```java +public class Thread implements Runnable { +... + public synchronized void start() { + if (threadStatus != 0) + throw new IllegalThreadStateException(); + + group.add(this); + + boolean started = false; + try { + start0(); + started = true; + } finally { + try { + if (!started) { + group.threadStartFailed(this); + } + } catch (Throwable ignore) { + } + } + } + + private native void start0(); + ... +} +``` + +- 과거에는 위와 같은 이유로 네이티브 메서드를 사용했지만, 최근엔 자바가 크게 발전하면서 네이티브 메서드를 거의 사용하지 않고 성능적인 면에서도 오히려 자바의 라이브러리들이 고성능인 경우도 많다. 그래서 특히, 성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않는다. +- 이러한 예시로는 BigInteger가 있다. java.math가 처음 추가된 자바 1.1시절에는 C로 작성한 고성능 라이브러리에 의존했다. 그러다 자바 3 때 순수 자바로 다시 구현되면서 세심한 튜닝한 결과, 원래의 네이티브 구현보다도 더 빨라졌다. +- 네이티브 메서드에는 심각한 단점도 있다. + +``` +1. 네이티브 언어가 안전하지 않으므로(아이템50) 네이티브 메서드를 사용하는 애플리케이션더 메모리 훼손이 오류로부터 더 이상 안전하지 않다. +2. 네이티브 언어는 자바보다 플랫폼을 많이 타서 이식성도 낮다. +3. 디버깅도 더 어렵다. 주의하지 않으면 속도가 오히려 느려질 수도 있다. +4. 가비지 컬렉터가 네이티브 메모리는 자동 회수하지 못하고, 심지어 추적조차 할 수 없다(아이템8). +5. 자바 코드와 네이티브 코드의 경계를 넘나들 때마다 비용도 추가된다. +6. 네이티브 메서드와 자바 코드 사이의 '접착 코드'를 작성해야 하는데, 이는 귀찮은 작업이고 가독성도 떨어진다. +``` + +- 한편, 네이티브 라이브러리 쪽은 GNU 다중 정밀 연산 라이브러리(GMP)를 필두로 개선 작업이 계속돼왔다. 그래서 정말로 고성능의 다중 정밀 연산이 필요한 자바 프로그래머라면 네이티브 메서드를 통해 GMP를 사용하는 걸 고려해도 좋다.