Java Language
Java Pitfalls - Null 및 NullPointerException
수색…
비고
값 null
는, 형태가 참조 형인 필드의 초기화되어 있지 않은 값의 디폴트 값입니다.
NullPointerException
(또는 NPE)은 null
개체 참조에 대해 부적절한 작업을 수행하려고 할 때 throw되는 예외입니다. 이러한 작업에는 다음이 포함됩니다.
-
null
타겟 오브젝트로 인스턴스 메소드를 호출하면 (자) -
null
타겟 오브젝트의 필드에 액세스하는 것, -
null
배열 객체의 인덱스를null
거나 그 길이에 액세스하려고 시도하면, -
synchronized
블록에서null
객체 참조를 뮤텍스로 사용하면, -
null
객체 참조를 형 변환한다. -
null
객체 참조의 언 박싱, 및 -
null
오브젝트 참조를 슬로우합니다.
NPE의 가장 일반적인 근본 원인은 다음과 같습니다.
- 참조 유형이있는 필드를 초기화하는 것을 잊어 버리는 경우,
- 참조 유형의 배열 요소를 초기화하는 것을 잊거나
- 특정 상황에서
null
을 리턴하는 것으로 지정된 특정 API 메소드의 결과를 테스트하지 않습니다.
null
을 리턴하는 일반적으로 사용되는 메소드의 예는 다음과 같습니다.
-
Map
API의get(key)
메소드는 매핑이없는 키를 사용하여 호출하면null
을 반환합니다. -
ClassLoader
및Class
API의getResource(path)
및getResourceAsStream(path)
메소드는 자원을 찾을 수없는 경우null
을 리턴합니다. - 가비지 컬렉터가 참조를 클리어 한 경우,
Reference
API 내의get()
메소드는null
를 돌려null
. - 존재하지 않는 요청 매개 변수, 세션 또는 세션 속성을 가져 오는 경우 Java EE 서블릿 API의 다양한
getXxxx
메소드가null
을 리턴합니다.
null
을 명시 적으로 테스트하거나 "Yoda Notation"을 사용하는 것과 같은 원치 않는 NPE를 피하는 전략이 있지만 이러한 전략에는 코드에서 실제로 수정해야하는 문제를 숨기는 바람직하지 않은 결과가 종종 있습니다.
Pitfall - Primitive Wrappers를 불필요하게 사용하면 NullPointerExceptions가 발생할 수 있습니다.
때로는 새로운 Java를 사용하는 프로그래머는 원시 유형과 래퍼를 서로 교환하여 사용합니다. 이로 인해 문제가 발생할 수 있습니다. 다음 예제를 고려하십시오.
public class MyRecord {
public int a, b;
public Integer c, d;
}
...
MyRecord record = new MyRecord();
record.a = 1; // OK
record.b = record.b + 1; // OK
record.c = 1; // OK
record.d = record.d + 1; // throws a NullPointerException
MyRecord
클래스 1 은 기본 초기화를 사용하여 필드의 값을 초기화합니다. 따라서 레코드를 new
만들 때 a
및 b
필드는 0으로 설정되고 c
및 d
필드는 null
로 설정됩니다.
기본 초기화 된 필드를 사용하려고하면 int
필드가 항상 작동하지만 Integer
필드는 일부 경우에만 작동하고 다른 필드에서는 작동하지 않습니다. 특히, ( d
와 함께) 실패 할 경우, 오른쪽에있는 표현식이 null
참조의 언 박싱을 시도하고 이것이 NullPointerException
을 발생시키는 원인입니다.
이것을 살펴볼 수있는 몇 가지 방법이 있습니다.
필드
c
와d
가 프리미티브 래퍼 일 필요가 있다면, 우리는 디폴트 초기화에 의존해서는 안되며, 아니면null
테스트해야한다. 이전의 경우null
상태의 필드에 대한 확실한 의미가 없으면 올바른 접근 방법입니다.필드가 프리미티브 래퍼 일 필요가없는 경우 원시 래퍼로 만드는 것은 실수입니다. 이 문제 외에도 기본 래퍼는 기본 유형에 비해 추가 오버 헤드가 있습니다.
여기에서의 교훈은 꼭 필요한 경우가 아니면 원시 래퍼 유형을 사용하지 않는 것입니다.
1 -이 클래스는 좋은 코딩 방법의 예가 아닙니다. 예를 들어, 잘 설계된 클래스는 공개 필드를 갖지 않습니다. 그러나 이것이이 예제의 요점은 아닙니다.
Pitfall - null를 사용하여 빈 배열이나 컬렉션을 나타냅니다.
일부 프로그래머는 빈 배열이나 컬렉션을 나타내는 데 null
을 사용하여 공간을 절약하는 것이 좋습니다. 소량의 공간을 절약 할 수는 있지만 사실은 코드가 복잡해지고 부서지기 쉽다는 것입니다. 배열을 요약하는 두 가지 버전의 메소드를 비교하십시오.
첫 번째 버전은 일반적으로 메서드를 코딩하는 방법입니다.
/**
* Sum the values in an array of integers.
* @arg values the array to be summed
* @return the sum
**/
public int sum(int[] values) {
int sum = 0;
for (int value : values) {
sum += value;
}
return sum;
}
두 번째 버전은 빈 배열을 나타 내기 위해 null
을 사용하는 습관이있는 경우 메서드를 코딩하는 방법입니다.
/**
* Sum the values in an array of integers.
* @arg values the array to be summed, or null.
* @return the sum, or zero if the array is null.
**/
public int sum(int[] values) {
int sum = 0;
if (values != null) {
for (int value : values) {
sum += value;
}
}
return sum;
}
보시다시피 코드는 좀 더 복잡합니다. 이것은 null
방식으로 사용하기로 결정한 직접적인 원인이됩니다.
이제 null
) 일 수있는이 배열이 많은 장소에서 사용되는지 고려하십시오. 당신이 그것을 사용하는 각 장소에서 당신은 null
을 테스트 할 필요가 있는지 고려할 필요가 있습니다. null
테스트가 필요하다면 NullPointerException
이 발생할 수 있습니다. 따라서이 방법으로 null
을 사용하는 전략은 응용 프로그램을 더욱 허약하게 만듭니다. 즉 프로그래머 오류의 결과에 더 취약합니다.
여기서 교훈은 비어있는 배열과 비어있는 목록을 사용하여 그 뜻을 나타낼 때 사용하는 것입니다.
int[] values = new int[0]; // always empty
List<Integer> list = new ArrayList(); // initially empty
List<Integer> list = Collections.emptyList(); // always empty
공간 오버 헤드는 작으며, 이것이 가치있는 일이면 최소화하는 다른 방법이 있습니다.
핏방울 - 예기치 않은 "좋음"
StackOverflow에서 Answers에 다음과 같은 코드가 자주 표시됩니다.
public String joinStrings(String a, String b) {
if (a == null) {
a = "";
}
if (b == null) {
b = "";
}
return a + ": " + b;
}
종종 NullPointerException
을 피하기 위해 이와 같이 null
을 테스트하는 "우수 사례"라는 주장이 수반됩니다.
그것은 최선의 관행인가? 간단히 말해서 : 아니오.
우리의 joinStrings
에서 이것을 수행하는 것이 좋은지를 말하기 전에 몇 가지 기본 가정을 질문해야합니다.
"a"또는 "b"가 null이라는 것은 무엇을 의미합니까?
String
값은 0 개 이상의 문자 일 수 있으므로 이미 빈 문자열을 나타내는 방법이 있습니다. null
은 ""
과 다른 것을 의미합니까? 없으면 빈 문자열을 나타내는 두 가지 방법이있는 것이 문제입니다.
null은 초기화되지 않은 변수에서 왔습니까?
null
는, 초기화되어 있지 않은 필드 또는 초기화되어 있지 않은 배열 요소로부터 취득 할 수 있습니다. 이 값은 의도적으로 또는 실수로 초기화되지 않을 수 있습니다. 우연히 그랬다면 이것은 버그입니다.
null은 "모른다"또는 "누락 된 값"을 나타 냅니까?
때로는 null
이 진정한 의미를 가질 수 있습니다. 예를 들어 변수의 실제 값은 알 수 없거나 사용할 수 없거나 "선택 사항"입니다. Java 8에서는 Optional
클래스가 더 나은 표현 방법을 제공합니다.
이것이 버그 (또는 설계상의 오류)라면 우리는 "좋게"해야합니까?
코드의 해석 중 하나는 빈 문자열을 대신 사용하여 예기치 않은 null
을 "잘 작성하고"있다는 것입니다. 올바른 전략인가? NullPointerException
이 발생하도록하는 것이 더 낫겠습니까? 그리고 스택에서 예외를 잡아서 버그로 기록하십시오.
"잘하는"문제는 문제를 숨기거나 진단하기가 더 쉽다는 것입니다.
이것이 코드 품질에 효과적입니까?
일관성있게 "make good"접근 방식을 사용하면 코드에 많은 "방어적인"null 테스트가 포함됩니다. 이것은 읽는 것을 더 길고 더 어렵게 만들 것입니다. 또한이 모든 테스트와 "잘하는 것"은 응용 프로그램의 성능에 영향을 미칠 수 있습니다.
요약하자면
null
이 의미있는 값이면, null
경우를 테스트하는 것이 올바른 방법입니다. 추론는 경우이다 null
값이 의미가,이 명확하게 동의 어떤 방법의의 javadoc에 문서화되어야한다 null
값을하거나 반환합니다.
그렇지 않으면 예기치 않은 null
을 프로그래밍 오류로 처리하고 개발자가 코드에 문제가 있음을 알 수 있도록 NullPointerException
발생시키는 것이 더 좋습니다.
Pitfall - 예외를 throw하는 대신 null을 반환합니다.
일부 자바 프로그래머는 예외를 던지거나 전파하는 일반적인 혐오감을 가지고 있습니다. 이로 인해 다음과 같은 코드가 생성됩니다.
public Reader getReader(String pathname) {
try {
return new BufferedReader(FileReader(pathname));
} catch (IOException ex) {
System.out.println("Open failed: " + ex.getMessage());
return null;
}
}
그래서 그 문제는 무엇입니까?
문제는 getReader
가 Reader
열 수 없음을 나타 내기 위해 null
을 특수 값으로 반환한다는 것입니다. 이제 반환 값은 그것이 사용되기 전에 null
인지를 검사해야합니다. 테스트가 생략되면 결과는 NullPointerException
됩니다.
여기에는 실제로 세 가지 문제가 있습니다.
-
IOException
이 너무 빨리 잡혔습니다. - 이 코드의 구조는 리소스를 유출 할 위험이 있음을 의미합니다.
- 「실제의」
Reader
를 돌려 줄 수 없었기 때문에,null
가 사용되었습니다.
사실, 예외가 이처럼 일찌감치 잡힐 필요가 있다고 가정하면 null
을 반환하는 몇 가지 대안이 있습니다.
-
NullReader
클래스를 구현할 수 있습니다. 예를 들어 독자가 이미 "파일 끝"위치에있는 것처럼 API의 작업이 작동하는 경우입니다. - Java 8에서는
getReader
를Optional<Reader>
를 반환하는 것으로 선언 할 수 있습니다.
Pitfall - I / O 스트림을 닫을 때 초기화되지 않았는지 확인하지 않음
메모리 누수를 방지하려면 작업이 완료된 입력 스트림이나 출력 스트림을 닫는 것을 잊지 말아야합니다. 이것은 대개 catch
부분이없는 try
- catch
- finally
문으로 수행됩니다.
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
out.close();
}
}
위의 코드는 무죄로 보일 수 있지만 디버깅을 불가능하게 만들 수있는 결함이 있습니다. 여기서 광고하면 out
초기화된다 ( out = new FileOutputStream(filename)
)가 예외를 발생하고 out
것이다 null
때 out.close()
심한 결과 실행 NullPointerException
!
이를 방지하려면 스트림을 닫으려고 시도하기 전에 스트림이 null
이 아닌지 확인하십시오.
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
if (out != null)
out.close();
}
}
더 나은 접근법은 리소스가 -with-resource를 try
하는 것입니다. 왜냐하면 finally
블록을 필요로하지 않고 NPE를 던질 확률이 0 인 스트림을 자동으로 닫을 것이기 때문입니다.
void writeNullBytesToAFile(int count, String filename) throws IOException {
try (FileOutputStream out = new FileOutputStream(filename)) {
for(; count > 0; count--)
out.write(0);
}
}
Pitfall - "Yoda 표기법"을 사용하여 NullPointerException을 피하십시오
StackOverflow에 게시 된 많은 예제 코드에는 다음과 같은 스 니펫이 포함되어 있습니다.
if ("A".equals(someString)) {
// do something
}
someString
이 null
경우 가능한 NullPointerException
을 "방지"또는 "회피"합니다. 또한,
"A".equals(someString)
보다 낫다:
someString != null && someString.equals("A")
(더 간결하고 상황에 따라 더 효율적일 수 있습니다. 그러나 우리가 아래에서 논증하는 것처럼, 간결함은 부정적 일 수 있습니다.)
그러나 실제 함정은 습관 문제로 NullPointerExceptions
을 피하기 위해 Yoda 테스트 를 사용하고 있습니다.
"A".equals(someString)
를 쓸 때 someString
이 null
되는 경우를 실제로 "좋게 만든다". 그러나 또 다른 예 ( Pitfall - "좋지 않은"예상치 못한 null
)가 설명하는 것처럼, "좋은" null
값을 만드는 것은 여러 가지 이유로 해를 끼칠 수 있습니다.
이것은 요다 조건이 "모범 사례"가 아님을 의미합니다 1 . null
이 예상되지 않는 한 NullPointerException
이 발생하도록하여 단위 테스트 실패 (또는 버그 보고서)를 얻을 수 있도록하는 것이 좋습니다. 이를 통해 예기치 않은 / 원치 않는 null
이 나타나는 버그를 찾아서 수정할 수 있습니다.
Yoda 조건은 테스트중인 오브젝트가 null
을 리턴하는 것으로 문서화 된 API에서 왔기 때문에 null
이 예상되는 경우에만 사용해야합니다. 그리고 틀림없이, 테스트를 표현하는 덜 예쁜 방법 중 하나를 사용하는 것이 더 좋을 수 있습니다. 그 방법은 코드를 검토중인 사람에게 null
테스트를 강조하는 데 도움이되기 때문입니다.
1 - 위키 피 디아 (Wikipedia) 에 따르면 "최고의 코딩 사례는 소프트웨어 개발 커뮤니티가 시간이 지남에 따라 학습 한 비공식적 인 규칙으로, 소프트웨어의 품질을 향상시키는 데 도움이 될 수 있습니다." . Yoda 표기법을 사용하는 것은 이것을 달성하지 못합니다. 많은 상황에서 코드가 더 나 빠지게됩니다.