Цепочка проверки 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;
}

или это будет считаться антипаттерн?

19 ответов


догоняет NullPointerException это действительно проблематично сделать, так как они могут произойти практически в любом месте. Очень легко получить один от ошибки, поймать его случайно и продолжать, как будто все нормально, тем самым скрывая реальную проблему. это так сложно справиться, поэтому лучше всего избегать вообще. (например, подумайте об автоматическом распаковывании 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 an -1 int instead of an empty optional if any is null
        // .orElse(-1);
        // Or this if you want to throw an exception instead
        // .orElseThrow(SomeApplicationException::new);
}

почему необязательно?

используя Optionals вместо null для значений, которые могут отсутствовать, делает этот факт очень заметным и понятным для читателей, и система типов гарантирует, что вы случайно не забудете об этом.

вы также получаете доступ к методам работы с такими значениями более удобно, как map и orElse.


действительно ли отсутствие или ошибка?

но также подумайте, является ли это допустимым результатом для промежуточных методов для возврата null или если это признак ошибки. Если это всегда ошибка, то, вероятно, лучше выбросить исключение, чем возвращать специальное значение, или для самих промежуточных методов выбросить исключение.


поболе опционные?

если с другой стороны отсутствуют значения из промежуточных методов допустимы, возможно, вы можете переключиться на Optionals для них тоже?

тогда вы можете использовать их следующим образом:

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-тесты могут быть лучше.


Я предлагаю подумать над Objects.requireNonNull(T obj, String message). Вы можете создавать цепочки с подробным сообщением для каждого исключения, например

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

Я бы предложил вам не использовать специальные возвращать значения, как -1. Это не стиль Java. Java разработала механизм исключений, чтобы избежать этого старомодного способа, который пришел из языка C.

бросать NullPointerException это тоже не лучший вариант. Вы можете предоставить свое собственное исключение (сделав его проверил чтобы гарантировать, что он будет обработан пользователем или unchecked для более простой обработки) или использовать конкретное исключение из синтаксического анализатора XML, который вы используете.


предполагая, что структура классов действительно находится вне нашего контроля, как это кажется, я думаю, что поймать NPE, как предложено в вопросе, действительно разумное решение, если производительность не является серьезной проблемой. Одно небольшое улучшение может заключаться в том, чтобы обернуть логику броска/улова, чтобы избежать беспорядка:

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);

как уже указал Тома в комментарии

следующее заявление не подчиняется закон Деметры,

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

что вы хотите int и вы можете сделать его из Foo. закон Деметры говорит, никогда не разговаривай с незнакомцами. Для вашего случая вы можете скрыть фактическую реализацию под капотом Foo и Bar.

теперь, вы можете создать метод в Foo для извлечения int С Baz. В конечном счете, Foo будет Bar и Bar мы можем получить доступ к Int не подвергая Baz на Foo. Таким образом, проверки null, вероятно, разделены на разные классы, и только необходимые атрибуты будут разделены между классами.


мой ответ идет почти в той же строке, что и @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;

можно добавить проверку на null для wsObject также, если есть шанс, что этот объект будет нулевым.


чтобы улучшить читаемость, вы можете использовать несколько переменных, например

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();

вы говорите, что некоторые методы "может вернуть null " но не говорите, при каких обстоятельствах они возвращаются null. Вы говорите, что поймали NullPointerException но вы не говорите, почему вы ловите его. Это отсутствие информации говорит о том, что у вас нет четкого понимания того, для чего существуют исключения и почему они превосходят альтернативу.

рассмотрим метод класса, который предназначен для выполнения действия, но метод не может гарантия он будет выполнять действие, потому что обстоятельств, не зависящих от него (что на самом деле в случае все методы в Java). Мы вызываем этот метод, и он возвращается. Код, вызывающий этот метод, должен знать, был ли он успешным. Откуда ему знать? Как его можно структурировать, чтобы справиться с двумя возможностями-успеха или неудачи?

используя исключения, мы можем писать методы, которые имеют успех как условие поста. Если метод возвращается, он был успешным. Если это бросает исключение, оно не удалось. Это большая победа для ясности. Мы можем написать код, который четко обрабатывает обычный случай успеха, и переместить весь код обработки ошибок в catch положения. Часто выясняется, что детали того, как или почему метод был неудачным, не важны для вызывающего абонента, поэтому то же самое catch предложение может использоваться для обработки нескольких типов сбоев. И часто случается, что методу не нужно ловить исключения на всех, но может просто разрешить их распространять до его абонент. Исключения из-за ошибок программы находятся в этом последнем классе; немногие методы могут реагировать соответствующим образом, когда есть ошибка.

Итак, те методы, которые возвращают null.

  • тут null значение указывает на ошибку в коде? Если это так,вы вообще не должны ловить исключение. И ваш код не должен пытаться угадать сам себя. Просто напишите то, что понятно и лаконично, исходя из предположения, что это сработает. Является ли цепочка вызовов методов четкой и сжатой? Тогда просто используй их.
  • тут null значение указывает на недопустимый ввод в вашу программу? Если это так, a NullPointerException не является подходящим исключением для броска, потому что обычно он зарезервирован для указания ошибок. Вероятно, вы хотите создать пользовательское исключение, производное от IllegalArgumentException (если вы хотите исключение непроверенное) или IOException (если вы хотите проверить исключение). Требуется ли ваша программа для предоставления подробного синтаксиса сообщения об ошибках при недопустимом вводе? Если это так, проверьте каждый метод на null возвращаемое значение, то бросание соответствующего диагностического исключения-единственное, что вы можете сделать. Если ваша программа не должна предоставлять подробную диагностику, связывая вызовы метода вместе, ловя любые NullPointerException и затем бросание вашего пользовательского исключения является самым ясным и кратким.

один из ответов утверждает, что вызовы цепного метода нарушают закон Деметра!--17--> и так плохо. Это утверждение ошибочно.

  • когда дело доходит до разработки программ, на самом деле нет никаких абсолютных правил о том, что хорошо, а что плохо. Есть только эвристика: правила, которые верны много (даже почти все) времени. Частью навыка программирования является знание того, когда можно нарушить эти правила. Поэтому краткое утверждение, что " это против правил X " на самом деле это вообще не ответ. Это один из ситуации, когда правило должны быть сломано?
  • на закон Деметры это действительно правило о дизайне интерфейса API или класса. При проектировании классов полезно иметь иерархия абстракций. У вас есть классы низкого уровня, которые используют языковые примитивы для непосредственного выполнения операций и представления объектов в абстракции более высокого уровня, чем языковые примитивы. У вас есть классы среднего уровня, которые делегируют классы низкого уровня и реализуют операции и представления на более высоком уровне, чем классы низкого уровня. У вас есть классы высокого уровня, которые делегируют классы среднего уровня и реализуют операции и абстракции еще более высокого уровня. (Я говорил здесь только о трех уровнях абстракции, но возможны и другие). Это позволяет вашему коду выражать себя в терминах соответствующих абстракций на каждом уровне, тем самым скрывая сложность. Обоснование для закон Деметры если у вас есть цепочка вызовов методов, это предполагает, что у вас есть класс высокого уровня, достигающий через класс среднего уровня, чтобы иметь дело непосредственно с деталями низкого уровня, и поэтому ваш класс среднего уровня не предоставил абстрактную операцию среднего уровня, которая нужна классу высокого уровня. Но, кажется, это не ситуация у вас здесь: вы не проектировали классы в цепочке вызовов методов, они являются результатом некоторого автоматически сгенерированного кода сериализации XML (не так ли?), и цепочка вызовов не нисходит через иерархию абстракций, потому что des-сериализованный XML находится на одном уровне иерархии абстракций (правильно?)?

не поймать NullPointerException. Вы не знаете, откуда он исходит (я знаю, что это маловероятно в вашем случае, но может быть что-то еще бросило его), и это медленно. Вы хотите получить доступ к указанному полю, и для этого каждое другое поле должно быть не нулевым. Это прекрасная веская причина, чтобы проверить каждое поле. Я бы, вероятно, проверил его в одном, а затем создал метод для удобочитаемости. Как указывали другие, уже возвращаясь -1 очень oldschool, но я не знаю, есть ли у вас причина для этого или нет (например, разговор с другой системой).

public int callService() {
    ...
    if(isValid(wsObject)){
        return wsObject.getFoo().getBar().getBaz().getInt();
    }
    return -1;
}


public boolean isValid(WsObject wsObject) {
    if(wsObject.getFoo() != null &&
        wsObject.getFoo().getBar() != null &&
        wsObject.getFoo().getBar().getBaz() != null) {
        return true;
    }
    return false;
}

редактировать: это спорно, если это disobeyes закон Деметры с WsObject, наверное, только структура данных (регистрация https://stackoverflow.com/a/26021695/1528880).


если вы не хотите, чтобы рефакторинг кода и вы можете использовать Java 8, можно использовать ссылки на метод.

сначала простая демонстрация (извините за статические внутренние классы)

public class JavaApplication14 
{
    static class Baz
    {
        private final int _int;
        public Baz(int value){ _int = value; }
        public int getInt(){ return _int; }
    }
    static class Bar
    {
        private final Baz _baz;
        public Bar(Baz baz){ _baz = baz; }
        public Baz getBar(){ return _baz; }   
    }
    static class Foo
    {
        private final Bar _bar;
        public Foo(Bar bar){ _bar = bar; }
        public Bar getBar(){ return _bar; }   
    }
    static class WSObject
    {
        private final Foo _foo;
        public WSObject(Foo foo){ _foo = foo; }
        public Foo getFoo(){ return _foo; }
    }
    interface Getter<T, R>
    {
        R get(T value);
    }

    static class GetterResult<R>
    {
        public R result;
        public int lastIndex;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) 
    {
        WSObject wsObject = new WSObject(new Foo(new Bar(new Baz(241))));
        WSObject wsObjectNull = new WSObject(new Foo(null));

        GetterResult<Integer> intResult
                = getterChain(wsObject, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);

        GetterResult<Integer> intResult2
                = getterChain(wsObjectNull, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);


        System.out.println(intResult.result);
        System.out.println(intResult.lastIndex);

        System.out.println();
        System.out.println(intResult2.result);
        System.out.println(intResult2.lastIndex);

        // TODO code application logic here
    }

    public static <R, V1, V2, V3, V4> GetterResult<R>
            getterChain(V1 value, Getter<V1, V2> g1, Getter<V2, V3> g2, Getter<V3, V4> g3, Getter<V4, R> g4)
            {
                GetterResult result = new GetterResult<>();

                Object tmp = value;


                if (tmp == null)
                    return result;
                tmp = g1.get((V1)tmp);
                result.lastIndex++;


                if (tmp == null)
                    return result;
                tmp = g2.get((V2)tmp);
                result.lastIndex++;

                if (tmp == null)
                    return result;
                tmp = g3.get((V3)tmp);
                result.lastIndex++;

                if (tmp == null)
                    return result;
                tmp = g4.get((V4)tmp);
                result.lastIndex++;


                result.result = (R)tmp;

                return result;
            }
}

выход

241
4

null
2

интерфейс Getter это просто функциональный интерфейс, вы можете использовать любой аналог.
GetterResult класс, аксессоры обнаженные вне для ясности, держит результат цепочка геттеров, если таковая имеется, или индекс последнего вызываемого геттера.

метод getterChain - Это простой, шаблонный кусок кода, который может быть сгенерирован автоматически (или вручную при необходимости).
Я структурировал код так, чтобы повторяющийся блок был самоочевидным.


Это не идеальное решение, так как вам все равно нужно определить одну перегрузку getterChain на количество геттеров.

Я бы рефакторинг кода вместо этого, но если не могу, и вы найдете часто вы можете подумать о создании класса с перегрузками, которые занимают от 2 до, скажем, 10 геттеров.


как говорили другие, уважение закона Деметры, безусловно, является частью решения. Другая часть, где это возможно, - изменить эти цепные методы, чтобы они не могли вернуться null. Вы можете избежать возвращения null вместо этого возвращая пустой String пустой Collection, или какой-то другой фиктивный объект, который означает или делает то, что вызывающий будет делать с null.


Я хотел бы добавить ответ, который фокусируется на значение ошибки. Исключение Null само по себе не дает никакого смысла полной ошибки. Поэтому я бы посоветовал не иметь с ними дело напрямую.

есть тысячи случаев, когда ваш код может пойти не так: не удается подключиться к базе данных, исключение ввода-вывода, ошибка сети... Если вы имеете дело с ними один за другим (например, проверка null здесь), это было бы слишком хлопотно.

в коде:

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

даже когда вы знаете, какое поле равно null, вы понятия не имеете, что происходит неправильно. Может быть, Bar равен null, но ожидается ли это? Или это ошибка данных? Подумайте о людях, которые читают ваш код

как в ответе xenteros, я бы предложил использовать пользовательские исключение непроверенное. Например, в этой ситуации: Foo может быть null (допустимые данные), но Bar и Baz никогда не должны быть null (недопустимые данные)

код можно переписать:

void myFunction()
{
    try 
    {
        if (wsObject.getFoo() == null)
        {
          throw new FooNotExistException();
        }

        return wsObject.getFoo().getBar().getBaz().getInt();
    }
    catch (Exception ex)
    {
        log.error(ex.Message, ex); // Write log to track whatever exception happening
        throw new OperationFailedException("The requested operation failed")
    }
}


void Main()
{
    try
    {
        myFunction();
    }
    catch(FooNotExistException)
    {
        // Show error: "Your foo does not exist, please check"
    }
    catch(OperationFailedException)
    {
        // Show error: "Operation failed, please contact our support"
    }
}

NullPointerException является исключением времени выполнения, поэтому, вообще говоря, не рекомендуется его ловить, но избегать его.

вам нужно будет поймать исключение везде, где вы хотите вызвать метод (или он будет распространяться вверх по стеку). Тем не менее, если в вашем случае вы можете продолжать работать с этим результатом со значением -1, и вы уверены, что он не будет распространяться, потому что вы не используете ни одну из "частей", которые могут быть нулевыми, тогда мне кажется правильным поймать это

Edit:

Я согласен с позднее ответ от @xenteros, будет лучше запустить собственное исключение вместо возврата -1 вы можете назвать его InvalidXMLException например.


следили за этим сообщением со вчерашнего дня.

я комментировал / голосовал комментарии, в которых говорится, что поймать NPE плохо. Вот почему я это делаю.

package com.todelete;

public class Test {
    public static void main(String[] args) {
        Address address = new Address();
        address.setSomeCrap(null);
        Person person = new Person();
        person.setAddress(address);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            try {
                System.out.println(person.getAddress().getSomeCrap().getCrap());
            } catch (NullPointerException npe) {

            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println((endTime - startTime) / 1000F);
        long startTime1 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            if (person != null) {
                Address address1 = person.getAddress();
                if (address1 != null) {
                    SomeCrap someCrap2 = address1.getSomeCrap();
                    if (someCrap2 != null) {
                        System.out.println(someCrap2.getCrap());
                    }
                }
            }
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println((endTime1 - startTime1) / 1000F);
    }
}

  public class Person {
    private Address address;

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }
}

package com.todelete;

public class Address {
    private SomeCrap someCrap;

    public SomeCrap getSomeCrap() {
        return someCrap;
    }

    public void setSomeCrap(SomeCrap someCrap) {
        this.someCrap = someCrap;
    }
}

package com.todelete;

public class SomeCrap {
    private String crap;

    public String getCrap() {
        return crap;
    }

    public void setCrap(String crap) {
        this.crap = crap;
    }
}

выход

3.216

0.002

Я вижу здесь явного победителя. Наличие если проверки слишком дешевле, чем поймать исключение. Я видел этот способ работы Java-8. Учитывая, что 70% текущих приложений все еще работают на Java-7, я добавляю этот ответ.

Нижняя Строка для любых критически важных приложений обработка NPE является дорогостоящей.


Если эффективность является проблемой, то следует рассмотреть вариант "catch". Если "catch" нельзя использовать, потому что он будет распространяться (как упоминалось в "SCouto"), используйте локальные переменные, чтобы избежать нескольких вызовов методов getFoo(), getBar() и getBaz().


стоит подумать о создании собственного исключения. Назовем это MyOperationFailedException. Вы можете бросить его вместо возврата значения. Результат будет таким же - вы выйдете из функции, но не вернете жестко закодированное значение -1, которое является Java anti-pattern. В Java мы используем исключения.

try {
    return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
    throw new MyOperationFailedException();
}

EDIT:

в соответствии с обсуждением в комментариях позвольте мне добавить что-то к моим предыдущим мыслям. В этом коде есть две возможности. Один это то, что вы принимаете null, а другой-что это ошибка.

Если это ошибка и она возникает, вы можете отлаживать свой код, используя другие структуры для отладки, Когда точек останова недостаточно.

если это приемлемо, вам все равно, где появился этот null. Если вы это сделаете, вы определенно не должны связывать эти запросы.


метод у вас длинный, но очень читаемый. Если бы я был новым разработчиком, приходящим в вашу базу кода, я мог бы увидеть, что вы делаете довольно быстро. Большинство других ответов (включая ловлю исключения), похоже, не делают вещи более читаемыми, а некоторые делают их менее читаемыми, на мой взгляд.

учитывая, что у вас, вероятно, нет контроля над сгенерированным источником и предполагая, что вам действительно просто нужно получить доступ к нескольким глубоко вложенным полям здесь и там, тогда я рекомендуется оборачивать каждый глубоко вложенный доступ с помощью метода.

private int getFooBarBazInt() {
    if (wsObject.getFoo() == null) return -1;
    if (wsObject.getFoo().getBar() == null) return -1;
    if (wsObject.getFoo().getBar().getBaz() == null) return -1;
    return wsObject.getFoo().getBar().getBaz().getInt();
}

Если вы обнаружите, что пишете много этих методов или если у вас возникнет соблазн сделать эти общедоступные статические методы, я бы создал отдельную объектную модель, вложенную, как вам хотелось бы, только с полями, о которых вы заботитесь, и преобразовать из объектной модели веб-служб в объектную модель.

когда вы общаетесь с удаленной веб-службой, очень типично иметь " удаленный домен " и "домен приложения" и переключение между ними. Удаленный домен часто ограничен веб-протоколом (например, вы не можете отправлять вспомогательные методы назад и вперед в чистой службе RESTful, а глубоко вложенные объектные модели распространены, чтобы избежать нескольких вызовов API) и поэтому не идеальны для прямого использования в вашем клиенте.

например:

public static class MyFoo {

    private int barBazInt;

    public MyFoo(Foo foo) {
        this.barBazInt = parseBarBazInt();
    }

    public int getBarBazInt() {
        return barBazInt;
    }

    private int parseFooBarBazInt(Foo foo) {
        if (foo() == null) return -1;
        if (foo().getBar() == null) return -1;
        if (foo().getBar().getBaz() == null) return -1;
        return foo().getBar().getBaz().getInt();
    }

}

return wsObject.getFooBarBazInt();

применяя закон Деметры,

class WsObject
{
    FooObject foo;
    ..
    Integer getFooBarBazInt()
    {
        if(foo != null) return foo.getBarBazInt();
        else return null;
    }
}

class FooObject
{
    BarObject bar;
    ..
    Integer getBarBazInt()
    {
        if(bar != null) return bar.getBazInt();
        else return null;
    }
}

class BarObject
{
    BazObject baz;
    ..
    Integer getBazInt()
    {
        if(baz != null) return baz.getInt();
        else return null;
    }
}

class BazObject
{
    Integer myInt;
    ..
    Integer getInt()
    {
        return myInt;
    }
}

давая ответ, который кажется отличным от всех других.

Я рекомендую вам проверить NULL на ifs.

причина :

мы не должны оставлять ни единого шанса для нашей программы, чтобы разбиться. NullPointer генерируется системой. поведение системы сгенерированного исключения не могут быть предсказаны. Вы не должны оставлять свой программа в руках системы, когда у вас уже есть способ обращения это ваше собственное. И поместите механизм обработки исключений для дополнительной безопасности.!!

для облегчения чтения кода попробуйте это для проверки условий:

if (wsObject.getFoo() == null || wsObject.getFoo().getBar() == null || wsObject.getFoo().getBar().getBaz() == null) 
   return -1;
else 
   return wsObject.getFoo().getBar().getBaz().getInt();

EDIT:

здесь вам нужно сохранить эти значения wsObject.getFoo(), wsObject.getFoo().getBar(), wsObject.getFoo().getBar().getBaz() in некоторая переменная. Я не делаю этого, потому что не знаю возвращения. типы этих функций.

любые предложения будут оцененный..!!


я написал класс Snag, которая позволяет определить путь для навигации по дереву объектов. Вот пример его использования:

Snag<Car, String> ENGINE_NAME = Snag.createForAndReturn(Car.class, String.class).toGet("engine.name").andReturnNullIfMissing();

это означает, что экземпляр ENGINE_NAME фактически называют Car?.getEngine()?.getName() на экземпляре, переданном ему, и return null если какая-либо ссылка возвращается null:

final String name =  ENGINE_NAME.get(firstCar);

он не опубликован на Maven, но если кто-то находит это полезным, это здесь (без гарантии конечно!)

это немного базовый, но, похоже, делает свою работу. Очевидно, что он более устарел с более поздними версиями Java и других языков JVM, которые поддерживают безопасную навигацию или Optional.