[java] Null 체크 체인 대 NullPointerException 잡기

웹 서비스는 거대한 XML을 반환하고 그에 대한 깊이 중첩 된 필드에 액세스해야합니다. 예를 들면 :

return wsObject.getFoo().getBar().getBaz().getInt()

문제는 즉 getFoo(), getBar(),getBaz() 가) 모두 반환 될 수null .

그러나 null모든 경우에 확인하면 코드가 매우 장황 해지고 읽기 어려워집니다. 또한 일부 필드의 수표를 놓칠 수 있습니다.

if (wsObject.getFoo() == null) return -1;
if (wsObject.getFoo().getBar() == null) return -1;
// maybe also do something with wsObject.getFoo().getBar()
if (wsObject.getFoo().getBar().getBaz() == null) return -1;
return wsObject.getFoo().getBar().getBaz().getInt();

쓰는 것이 허용됩니까?

try {
    return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
    return -1;
}

아니면 반 패턴으로 간주 될까요?



답변

잡기는 NullPointerExceptionA는 할 정말 문제가있는 것은 그들은 거의 어디서든 일어날 수 있기 때문이다. 버그에서 하나를 가져 와서 실수로 잡아서 모든 것이 정상인 것처럼 계속해서 실제 문제를 숨기는 것은 매우 쉽습니다. 처리하기가 너무 까다롭기 때문에 아예 피하는 것이 가장 좋습니다. (예를 들어 null의 자동 언 박싱에 대해 생각해보십시오 Integer.)

Optional대신 수업 을 사용하는 것이 좋습니다 . 존재하거나없는 값으로 작업 할 때 가장 좋은 방법입니다.

이를 사용하여 다음과 같이 코드를 작성할 수 있습니다.

public Optional<Integer> m(Ws wsObject) {
    return Optional.ofNullable(wsObject.getFoo()) // Here you get Optional.empty() if the Foo is null
        .map(f -> f.getBar()) // Here you transform the optional or get empty if the Bar is null
        .map(b -> b.getBaz())
        .map(b -> b.getInt());
        // Add this if you want to return null instead of an empty optional if any is null
        // .orElse(null);
        // Or this if you want to throw an exception instead
        // .orElseThrow(SomeApplicationException::new);
}

왜 선택 사항입니까?

사용 Optional의 대신을null부재 할 수있는 값 하면 그 사실이 독자에게 매우 눈에 잘 띄고 명확 해지며 유형 시스템은 실수로 잊어 버리지 않도록합니다.

당신은 또한 같은, 더 편리하게 값 작업을위한 방법에 접근 map하고 orElse.


결석이 유효하거나 오류입니까?

그러나 중간 메서드가 null을 반환하는 것이 유효한 결과인지 또는 그것이 오류의 신호인지 생각해보십시오. 항상 오류 인 경우 특수 값을 반환하거나 중간 메서드 자체가 예외를 throw하는 것보다 예외를 throw하는 것이 좋습니다.


더 많은 선택 사항이 있습니까?

반면에 중간 메서드에없는 값이 유효한 경우 다음으로 전환 할 수 있습니다. Optional s로 할 수도 있습니까?

그런 다음 다음과 같이 사용할 수 있습니다.

public Optional<Integer> mo(Ws wsObject) {
    return wsObject.getFoo()
        .flatMap(f -> f.getBar())
        .flatMap(b -> b.getBaz())
        .flatMap(b -> b.getInt());
}

선택 사항이 아닌 이유는 무엇입니까?

내가 사용하지 않는다고 생각할 수있는 유일한 이유는 이것이 Optional코드의 성능에 매우 중요한 부분이고 가비지 수집 오버 헤드가 문제로 판명되는 경우입니다. 몇 때문이다 Optional오브젝트 코드가 실행될 때마다 할당하고, VM이 있습니다 사람들을 최적화 할 수 없습니다. 이 경우 원래 if-test가 더 좋을 수 있습니다.


답변

나는 고려하는 것이 좋습니다 Objects.requireNonNull(T obj, String message). 다음과 같이 각 예외에 대한 자세한 메시지가 포함 된 체인을 만들 수 있습니다.

requireNonNull(requireNonNull(requireNonNull(
    wsObject, "wsObject is null")
        .getFoo(), "getFoo() is null")
            .getBar(), "getBar() is null");

과 같은 특별한 반환 값을 사용하지 않는 것이 좋습니다 -1. 그것은 자바 스타일이 아닙니다. Java는 C 언어에서 나온 이러한 구식 방식을 피하기 위해 예외 메커니즘을 설계했습니다.

던지는 NullPointerException것도 최선의 선택이 아닙니다. 고유 한 예외를 제공하거나 ( 사용자가 처리 할 수 있는지 확인 하거나 더 쉬운 방법으로 처리하기 위해 선택 취소 ) 사용중인 XML 파서의 특정 예외를 사용할 수 있습니다.


답변

클래스 구조가 실제로 우리의 통제를 벗어났다고 가정하면 성능이 주요 관심사가 아니라면 질문에서 제안한 NPE를 잡는 것이 실제로 합리적인 해결책이라고 생각합니다. 한 가지 작은 개선 사항은 혼란을 피하기 위해 throw / catch 논리를 래핑하는 것입니다.

static <T> T get(Supplier<T> supplier, T defaultValue) {
    try {
        return supplier.get();
    } catch (NullPointerException e) {
        return defaultValue;
    }
}

이제 간단하게 다음을 수행 할 수 있습니다.

return get(() -> wsObject.getFoo().getBar().getBaz().getInt(), -1);


답변

댓글에서 Tom이 이미 지적했듯이

다음 진술 은 데메테르법칙을 위반합니다 .

wsObject.getFoo().getBar().getBaz().getInt()

원하는 것은 int에서 얻을 수 있습니다 Foo. 데메테르의 법칙은 말한다 낯선 사람에게 이야기하지 않았다 . 귀하의 경우 Foo및 의 후드 아래에서 실제 구현을 숨길 수 있습니다 Bar.

지금, 당신은의 방법을 만들 수 있습니다 Foo가져올 int에서를 Baz. 궁극적으로 Foo해야합니다 Bar및에 Bar우리가 액세스 할 수 있습니다 Int노출하지 않고 Baz직접 Foo. 따라서 널 검사는 아마도 다른 클래스로 나뉘며 필요한 속성 만 클래스간에 공유됩니다.


답변

내 대답은 @janki와 거의 같은 줄에 있지만 다음과 같이 코드 스 니펫을 약간 수정하고 싶습니다.

if (wsObject.getFoo() != null && wsObject.getFoo().getBar() != null && wsObject.getFoo().getBar().getBaz() != null)
   return wsObject.getFoo().getBar().getBaz().getInt();
else
   return something or throw exception;

wsObject해당 개체가 null 일 가능성이있는 경우 null 검사도 추가 할 수 있습니다 .


답변

일부 메서드는 “반환 할 수있다 null“고 말하지만 어떤 상황에서 반환되는지는 말하지 않습니다 null. 당신은 당신이NullPointerException 고 말하지만 왜 잡았는지 말하지 않습니다. 이러한 정보 부족은 예외가 무엇인지, 왜 예외가 대안보다 우월한 지에 대한 명확한 이해가 없음을 나타냅니다.

조치를 수행하기위한 클래스 메소드를 고려하십시오. 그러나 제어 할 수없는 상황 (사실 Java의 모든 메소드 의 경우)으로 인해 해당 메소드가 조치를 수행 할 것이라고 보장 할 수는 없습니다. . 이 메서드를 호출하면 반환됩니다. 해당 메서드를 호출하는 코드는 성공 여부를 알아야합니다. 어떻게 알 수 있습니까? 성공 또는 실패의 두 가지 가능성에 대처하기 위해 어떻게 구성 할 수 있습니까?

예외를 사용하여 성공한 메소드를 사후 조건으로 작성할 수 있습니다 . 메서드가 반환되면 성공한 것입니다. 예외가 발생하면 실패한 것입니다. 이것은 명확성을위한 큰 승리입니다. 정상적인 성공 사례를 명확하게 처리하는 코드를 작성하고 모든 오류 처리 코드를 catch절로 이동할 수 있습니다. 메서드가 어떻게 또는 왜 실패했는지에 대한 세부 정보가 호출자에게 중요하지 않기 때문에 동일한 catch절을 여러 유형의 실패를 처리하는 데 사용할 수 있습니다. 그리고 종종 방법은 캐치 예외를 필요로하지 않는 일이 전혀 하지만, 단지 그들에게 전파하도록 할 수 호출자. 프로그램 버그로 인한 예외는 후자의 클래스에 있습니다. 버그가있을 때 적절하게 반응 할 수있는 방법은 거의 없습니다.

따라서 null.

  • 않는 null값은 코드에서 버그를 나타냅니다? 그렇다면 예외를 전혀 포착하지 않아야합니다. 그리고 당신의 코드는 두 번째 추측을 시도해서는 안됩니다. 그것이 효과가있을 것이라는 가정에 대해 명확하고 간결한 것을 작성하십시오. 일련의 메서드 호출이 명확하고 간결합니까? 그런 다음 사용하십시오.
  • 않는 null값은 프로그램에 잘못된 입력을 표시? 그렇다면 NullPointerException일반적으로 버그를 표시하기 위해 예약되어 있기 때문에 a 는 적절한 예외가 아닙니다. IllegalArgumentException(확인 되지 않은 예외IOException 를 원하는 경우 ) 또는 (확인 된 예외를 원하는 경우 ) 에서 파생 된 사용자 지정 예외를 throw하고 싶을 것입니다 . 잘못된 입력이있을 때 프로그램에서 자세한 구문 오류 메시지를 제공해야합니까? 그렇다면 각 메서드에서 null반환 값을 확인한 다음 적절한 진단 예외를 throw하는 것이 유일한 방법입니다. 프로그램이 자세한 진단을 제공 할 필요가없는 경우 메서드 호출을 함께 연결하고 임의의 항목을 포착 NullPointerException한 다음 사용자 지정 예외를 throw하는 것이 가장 명확하고 간결합니다.

답변 중 하나는 체인 메서드 호출 이 Demeter법칙을 위반 하므로 나쁘다고 주장합니다 . 그 주장은 잘못된 것입니다.

  • 프로그램 디자인과 관련하여 무엇이 좋은지 나쁜지에 대한 절대적인 규칙은 없습니다. 휴리스틱 (heuristic) 만 있습니다. 규칙은 거의 모든 시간에 적용됩니다. 프로그래밍 기술의 일부는 이러한 종류의 규칙을 어기는 것이 좋을 때를 아는 것입니다. 따라서 “이것은 규칙 X 에 위배된다”는 간결한 주장 은 실제로 전혀 대답이 아닙니다. 이것이 규칙이 해야하는 상황 중 하나입니까? 입니까?
  • 데메테르법칙 은 실제로 API 또는 클래스 인터페이스 디자인에 대한 규칙입니다. 클래스를 디자인 할 때 추상화 계층 구조 를 갖는 것이 유용합니다.. 언어 프리미티브를 사용하여 작업을 직접 수행하고 언어 프리미티브보다 높은 수준의 추상화에서 객체를 나타내는 저수준 클래스가 있습니다. 하위 수준 클래스에 위임하고 하위 수준 클래스보다 높은 수준에서 작업 및 표현을 구현하는 중간 수준의 클래스가 있습니다. 중간 수준의 클래스에 위임하고 더 높은 수준의 작업 및 추상화를 구현하는 높은 수준의 클래스가 있습니다. (여기서는 세 가지 수준의 추상화에 대해 이야기했지만 더 많은 것이 가능합니다.) 이를 통해 코드가 각 수준에서 적절한 추상화 측면에서 자신을 표현할 수 있으므로 복잡성을 숨길 수 있습니다. 데메테르 법칙의 근거메서드 호출 체인이있는 경우 중간 수준의 클래스를 통해 낮은 수준의 세부 정보를 직접 처리하는 높은 수준의 클래스가 있으므로 중간 수준의 클래스가 중간 수준의 추상 연산을 제공하지 않았 음을 의미합니다. 높은 수준의 수업이 필요합니다. 하지만 여기에있는 상황 이 아닌 것 같습니다 . 메서드 호출 체인에서 클래스를 디자인하지 않았고, 자동 생성 된 XML 직렬화 코드의 결과이며 (맞습니까?), 호출 체인이 내림차순이 아닙니다. des-serialized XML이 모두 추상화 계층의 동일한 수준에 있기 때문에 추상화 계층을 통해?

답변

가독성을 높이기 위해 다음과 같은 여러 변수를 사용할 수 있습니다.

Foo theFoo;
Bar theBar;
Baz theBaz;

theFoo = wsObject.getFoo();

if ( theFoo == null ) {
  // Exit.
}

theBar = theFoo.getBar();

if ( theBar == null ) {
  // Exit.
}

theBaz = theBar.getBaz();

if ( theBaz == null ) {
  // Exit.
}

return theBaz.getInt();