Раскройте секреты системы исключений Java

Java задняя часть переводчик

Я считаю, что все используют механизм исключений Java каждый день, и я думаю, что все знакомы с процессом выполнения try-catch-finally. В этой статье будут представлены некоторые детали механизма исключений Java.Хотя эти проблемы небольшие, они играют важную роль в производительности и удобочитаемости кода.

1. Введение в систему исключений Java

Прежде чем изучать технологию, вы должны сначала встать на командные высоты и не обращать внимания на общую ситуацию с технологией, а также контролировать всю структуру контекста технологии с макро-перспективы. Таким образом, вы можете сосредоточиться на изучении наиболее важных аспектов архитектуры и не копаться в мельчайших подробностях при изучении деталей. Итак, прежде чем раскрыть некоторые неизвестные вам секреты исключений Java, давайте рассмотрим систему исключений Java.

Throwable — это родительский класс верхнего уровня всей системы исключений Java, который имеет два подкласса: Error и Exception.

Ошибка представляет собой фатальную системную ошибку, ошибку, которую программа не может обработать, например OutOfMemoryError, ThreadDeath и т. д. При возникновении этих ошибок виртуальная машина Java может только завершить поток.

Исключение — это исключение, которое может обработать сама программа.Этот тип исключения делится на две категории: исключение времени выполнения и исключение, не связанное с выполнением.

Исключения времени выполнения — это исключения класса RuntimeException и его подклассов, таких как NullPointerException, IndexOutOfBoundsException и т. д. Эти исключения являются непроверяемыми исключениями, и разработчики могут выбирать, захватывать и обрабатывать их или нет. Эти исключения обычно вызываются логическими ошибками программы, и программа должна максимально избегать возникновения таких исключений с логической точки зрения.

В системе исключений Exception, за исключением исключений класса RuntimeException и его подклассов, все они являются проверенными исключениями. Когда вы вызываете метод, выбрасывающий эти исключения, вы должны обрабатывать эти исключения. Если его не обработать, программа не скомпилируется. Например: IOException, SQLException, пользовательское исключение и т. д.

2. try-with-resources

До JDK 1.7 обработка операций ввода-вывода была громоздкой. Поскольку IOException является проверенным исключением, вызывающая сторона должна обрабатывать их с помощью try-catch, а поскольку ресурс необходимо закрыть после завершения операции ввода-вывода, метод закрытия ресурса close() также вызовет проверенное исключение, поэтому попробуйте Для борьбы с ним также необходимо использовать -catch. Поэтому небольшой кусок кода операции ввода-вывода будет вложен и обернут сложным try-catch, что сильно снижает читабельность программы.

Стандартный код операции ввода-вывода выглядит следующим образом:

public class Demo {
    public static void main(String[] args) {
        BufferedInputStream bin = null;
        BufferedOutputStream bout = null;
        try {
            bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
            bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (bin != null) {
                try {
                    bin.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
                finally {
                    if (bout != null) {
                        try {
                            bout.close();
                        }
                        catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

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

В приведенных выше 40 строках кода менее 10 строк кода фактически обрабатывают операции ввода-вывода, а оставшиеся 30 строк кода используются для обеспечения разумного высвобождения ресурсов. Это, очевидно, приводит к менее читаемому коду. К счастью, JDK 1.7 предоставляет ресурсы для решения этой проблемы. Модифицированный код выглядит следующим образом:

public class TryWithResource {
    public static void main(String[] args) {
        try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
             BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
            int b;
            while ((b = bin.read()) != -1) {
                bout.write(b);
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Нам нужно поместить код объявления ресурса в круглые скобки после попытки, а затем поместить код обработки ресурса в {} после попытки, обработка исключений по-прежнему выполняется в блоке кода перехвата, и нет необходимости писать наконец кодовый блок.

Так почему же try-with-resources позволяет избежать большого количества кода выпуска ресурсов? Ответ заключается в том, что компилятор Java должен добавить блок finally за нас. Обратите внимание, что компилятор добавит только блоки кода finally, а процесс освобождения ресурсов должен обеспечиваться поставщиком ресурсов.

В JDK 1.7 реализованы все классы ввода-вывода.AutoCloseableинтерфейс, и необходимо реализовать один изclose()Функция, процесс освобождения ресурсов должен быть завершен в этой функции.

Затем компилятор автоматически добавит блок кода finally при компиляции иclose()Код освобождения ресурса в функции добавляется в блок finally. Это улучшает читаемость кода.

Проблема маскировки исключений

В блоке кода try-catch-finally, если все исключения выбрасываются в блоке try, блоке catch и блоке finally, то может быть выброшено только исключение в блоке finally, а исключение в блоке try и блоке catch будет быть замаскированным. ЭтоПроблема маскировки исключений. Как показано в следующем коде:

public class Connection implements AutoCloseable {
    public void sendData() throws Exception {
        throw new Exception("send data");
    }
    @Override
    public void close() throws Exception {
        throw new MyException("close");
    }
}

Сначала определитеConnectionкласс, который обеспечиваетsendData()иclose()метод, ради эксперимента, эти два метода не имеют никакой бизнес-логики и напрямую выдают исключение. Ниже мы используем этот класс.

public class TryWithResource {
    public static void main(String[] args) {
        try {
            test();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static void test() throws Exception {
        Connection conn = null;
        try {
            conn = new Connection();
            conn.sendData();
        }
        finally {
            if (conn != null) {
                conn.close();
            }
        }
    }
}

при исполненииconn.sendData(), метод выдает исключение вызывающей сторонеmain(), но блок finally выполняется перед броском. При выполнении блока finallyconn.close()метод, вызывающему объекту также генерируется исключение. В этот момент исключение, созданное блоком try, будет перезаписано, и только исключение из блока finally будет напечатано в основном методе. Результат выглядит следующим образом:

basic.exception.MyException: close
	at basic.exception.Connection.close(Connection.java:10)
	at basic.exception.TryWithResource.test(TryWithResource.java:82)
	at basic.exception.TryWithResource.main(TryWithResource.java:7)
	......

Это проблема защиты от исключений, связанная с try-catch-finally, и Try-with-resources может хорошо решить эту проблему. Так как же решить эту проблему?

Сначала мы перезаписываем этот код с помощью TRY-RESORCES:

public class TryWithResource {
    public static void main(String[] args) {
        try {
            test();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static void test() throws Exception {
        Connection conn = null;
        try (conn = new Connection();) {
            conn.sendData();
        }
    }
}

Чтобы получить четкое представление о том, что компилятор Java делает на try-with-resources, мы декомпилируем этот код и получаем следующий код:

public class TryWithResource {
    public TryWithResource() {
    }
    public static void main(String[] args) {
        try {
            // 资源声明代码
            Connection e = new Connection();
            Throwable var2 = null;
            try {
                // 资源使用代码
                e.sendData();
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                // 资源释放代码
                if(e != null) {
                    if(var2 != null) {
                        try {
                            e.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        e.close();
                    }
                }
            }
        } catch (Exception var14) {
            var14.printStackTrace();
        }
    }
}

Основная операция - 22 строкиvar2.addSuppressed(var11);. Компилятор сначала сохраняет исключения в блоке try и блоке catch в локальной переменной, а когда исключение снова генерируется в блоке finally, исключение передается через предыдущее исключение.addSuppressed()Метод добавляет текущее исключение в свой стек исключений, тем самым гарантируя, что исключения в блоках try и catch не будут потеряны. Когда используется попытка с ресурсами, вывод выглядит следующим образом:

java.lang.Exception: send data
	at basic.exception.Connection.sendData(Connection.java:5)
	at basic.exception.TryWithResource.main(TryWithResource.java:14)
	......
	Suppressed: basic.exception.MyException: close
		at basic.exception.Connection.close(Connection.java:10)
		at basic.exception.TryWithResource.main(TryWithResource.java:15)
		... 5 more

3. Процесс выполнения try-catch-finally

Как мы все знаем, сначала выполняется код в try.Если исключения не возникает, код в finally выполняется напрямую, если возникает исключение, сначала выполняется код в catch, а затем код в finally.

Я считаю, что описанный выше процесс всем знаком, но что делать, если в блоке try и блоке catch есть возврат? А бросить? В этот момент порядок выполнения изменится.

Однако все изменения неразделимы, нам нужно лишь помнить одно:Возврат и бросок, наконец, отменят возврат и бросок, пытаясь поймать. как мне это сказать? Читать дальше.

Чтобы объяснить эту проблему, давайте рассмотрим пример: что вернет функция test() в приведенном ниже коде?

public int test() {
    try {
        int a = 1;
        a = a / 0;
        return a;
    } catch (Exception e) {
        return -1;
    } finally{
        return -2;
    }
}

Ответ -2.

при выполнении кодаa = a / 0;При возникновении исключения код после него в блоке try уже не выполняется, а код в блоке catch выполняется напрямую; В блоке catch при выполненииreturn -1Раньше блок finally выполнялся первым; Поскольку в блоке finally есть инструкция return, то return в catch будет перезаписан, а return in fianlly будет выполняться напрямую.return -2После окончания программы. Таким образом, выход равен -2.

Точно так же замена return на throw дает тот же результат и, наконец, переопределяет return и throw в блоках try и catch.

Особое напоминание: использование операторов return в блоках finally запрещено! Пример здесь только для того, чтобы рассказать вам об этой особенности Java, и ее запрещено использовать в реальной разработке!

4. Дополнительно элегантно решает проблему NPE

NULL Указатель исключения представляет собой исключение времени выполнения. Для этого типа исключения, если нет четкой стратегии обработки, лучшая практика - позволить программе рано. Однако во многих сценариях не то, что разработчик не имеет Специальная стратегия обработки, но фундаментальное не осознало существование NULL Указанного исключения. Когда произойдет исключение, стратегия обработки также очень проста. Просто добавьте утверждение, если определить, где существует исключение, но такая стратегия CODING сделает нашу программу все более и более нуменными суждениями. Мы знаем, что хороший дизайн программы Должен сделать ключевое слово NULL как можно меньше в коде, а дополнительный класс, предоставленный Java8, не только уменьшает NullPointException, но также улучшает эстетику кода. Но сначала мы должны быть ясно, что это не замена для нулевого ключевого слова, но обеспечивает более элегантную реализацию для нулевого определения, что позволяет избежать нулевой точки зрения.

Предположим, что есть класс Person:

class Person{
    private long id;
    private String name;
    private int age;
    
    // 省略setter、getter
}

Когда мы вызываем интерфейс и получаем объект Person, мы можем обработать его следующими методами:

  • ofNullable(person)
    • Преобразовать объект Person в необязательный объект
    • позволить человеку быть пустым
Optional<Person> personOpt = Optional.ofNullable(person);
  • T orElse(T other)
    • Если пусто, укажите значение по умолчанию
personOpt.orElse(new Person("柴毛毛"));
  • T orElseGet(Supplier<? extends T> other)
    • Если он пуст, выполните соответствующий код и верните значение по умолчанию.
personOpt.orElseGet(()->{
    Person person = new Person();
    person.setName("柴毛毛");
    person.setAge(20);
    return person;
});
  • <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)
    • Если он пуст, то сгенерируйте исключение
personOpt.orElseThrow(CommonBizException::new);
  • <U>Optional<U> map(Function<? super T,? extends U> mapper)
    • Картирование (получение имени лично)
String name = personOpt
                .map(Person::getName)
                .orElseThrow(CommonBizException::new)
                .map(Optional::get);

5. Протокол обработки исключений

  • Класса RuntimeException, определенного в библиотеке классов Java, можно избежать путем предварительной проверки, и его не следует обрабатывать с помощью catch, например: IndexOutOfBoundsException, NullPointerException и т. д.
    • Положительный пример:if (obj != null) {...}
    • Пример счетчика:try { obj.method() } catch (NullPointerException e) {...}
  • Исключения не следует использовать для управления процессом и условного управления, поскольку эффективность обработки исключений ниже, чем у условных ветвей.
  • Большие участки кода поведения Try-Catch, это безответственное. Пожалуйста, отличайте улов, когда код стабильный и нестабильный код, и код ссылается на стабильный, в любом случае не сходит неправильным кодом. Для нестационарного завершения кода выложить максимально различить тип исключения, соответствующее обращение с исключением.
  • Перехватите исключение, чтобы обработать его, не перехватывайте его, а затем отбрасывайте, если вы не хотите его обрабатывать, сгенерируйте исключение вызывающему объекту. Самый удаленный бизнес-пользователь должен обрабатывать исключения и преобразовывать их в контент, понятный пользователям.
  • В код транзакции помещается блок try.После исключения catch, если вам нужно откатить транзакцию, вы должны обратить внимание на ручной откат транзакции.
  • Вы не можете использовать return в блоке finally.После возврата в блоке finally метод завершает выполнение, и оператор return в блоке try не будет выполнен.
  • Блок finally должен закрыть объект ресурса, объект потока и функцию try-catch в случае возникновения исключения. Примечание. Если JDK7 и выше, вы можете использовать метод try-with-resources.
  • В код транзакции помещается блок try.После исключения catch, если вам нужно откатить транзакцию, вы должны обратить внимание на ручной откат транзакции.
  • Исключение catch и исключение throw должны точно совпадать, иначе исключение catch является родительским классом исключения throw. То есть выбрасываемое исключение должно быть перехваченным исключением или его подклассом. Таким образом, аномально большое может быть уменьшено до малого.
  • Этот закон прямо запрещает использование NPE в качестве ответственности вызывающего абонента. Даже если вызываемый метод возвращает вызывающему объекту пустую коллекцию или пустой объект, он не сидит сложа руки и не расслабляется, мы должны учитывать сбой удаленного вызова, аномальные возвраты null, сцена запуска.
  • При определении различайте непроверенные и проверенные исключения, избегайте непосредственного создания RuntimeException, не говоря уже об исключении Exception или Throwable, и используйте пользовательские исключения с бизнес-значением. Мы рекомендуем пользовательские исключения, которые были определены в отрасли, такие как DAOException / ServiceException и т. д.
  • Использовать ли в коде «генерировать исключение» или «возвращать код ошибки»:
    • «Код ошибки» должен использоваться для открытого интерфейса http/api за пределами компании;
    • И выдается исключение внутренней рекомендации приложения;
    • Метод Result предпочтительнее для вызовов RPC между приложениями, инкапсулируя isSuccess, «код ошибки» и «краткую информацию об ошибке».