В языке Java try-catch-finally кажется простым и безобидным, но по-настоящему «контролировать» его непросто. Если больше ничего не скажешь, то возьмем, наконец, пример. Не смотрите на его единственную функцию, но он "скрыто убийственный" при использовании. Если не верите, давайте взглянем на следующее Примеры...
Яма 1: наконец-то использован возврат
Если RETURN используется в finally, даже если в try-catch есть операция RETURN, она не вернет результат немедленно, но затем будет возвращено выражение в finally. На данный момент возникает проблема:Если в finally есть оператор return, он напрямую вернет результат в finally, тем самым безжалостно отбрасывая возвращаемое значение в try.
① Пример кода счетчика
public static void main(String[] args) throws FileNotFoundException {
System.out.println("执行结果:" + test());
}
private static int test() {
int num = 0;
try {
// num=1,此处不返回
num++;
return num;
} catch (Exception e) {
// do something
} finally {
// num=2,返回此值
num++;
return num;
}
}
Результат выполнения приведенного выше кода выглядит следующим образом:
② Анализ причин
Если в finally есть оператор return, возвращаемое значение в try-catch будет перезаписано, и если программист не обнаружит эту проблему при написании кода, то результат выполнения программы будет неверным.
③ Решения
Если в try-catch-finally есть возвращаемое значение,Убедитесь, что оператор return появляется только один раз в конце метода..
④ Положительный пример кода
public static void main(String[] args) throws FileNotFoundException {
System.out.println("执行结果:" + testAmend());
}
private static int testAmend() {
int num = 0;
try {
num = 1;
} catch (Exception e) {
// do something
} finally {
// do something
}
// 确保 return 语句只在此处出现一次
return num;
}
Яма 2: код в finally «не выполняется»
Если вышеприведенный пример относительно прост, то следующий пример даст вам другое ощущение, просто взгляните на код напрямую.
① Пример кода счетчика
public static void main(String[] args) throws FileNotFoundException {
System.out.println("执行结果:" + getValue());
}
private static int getValue() {
int num = 1;
try {
return num;
} finally {
num++;
}
}
Результат выполнения приведенного выше кода выглядит следующим образом:
② Анализ причин
**Я думал, что результатом казни будет 2, но никак не ожидал, что будет 1**, говоря словами Мастера Ма: «Я неосторожен, вспышки нет».
Можно спросить: если код заменить на ++num, получится ли 2?
Извините, что говорю вам, нет, результат выполнения по-прежнему 1. Так почему же это так? Чтобы действительно понять это, мы должны начать с байт-кода этого кода.
Байт-код, окончательно сгенерированный приведенным выше кодом, выглядит следующим образом:
// class version 52.0 (52)
// access flags 0x21
public class com/example/basic/FinallyExample {
// compiled from: FinallyExample.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/example/basic/FinallyExample; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V throws java/io/FileNotFoundException
L0
LINENUMBER 13 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "\u6267\u884c\u7ed3\u679c:"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKESTATIC com/example/basic/FinallyExample.getValue ()I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 14 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 3
MAXLOCALS = 1
// access flags 0xA
private static getValue()I
TRYCATCHBLOCK L0 L1 L2 null
L3
LINENUMBER 18 L3
ICONST_1
ISTORE 0
L0
LINENUMBER 20 L0
ILOAD 0
ISTORE 1
L1
LINENUMBER 22 L1
IINC 0 1
L4
LINENUMBER 20 L4
ILOAD 1
IRETURN
L2
LINENUMBER 22 L2
FRAME FULL [I] [java/lang/Throwable]
ASTORE 2
IINC 0 1
L5
LINENUMBER 23 L5
ALOAD 2
ATHROW
L6
LOCALVARIABLE num I L0 L6 0
MAXSTACK = 1
MAXLOCALS = 3
}
Упрощенная версия этих байт-кодов показана ниже:Если вы хотите понять эти байт-коды, вы должны сначала понять значение этих байт-кодов, которые можно найти на официальном сайте Oracle (документы на английском языке):docs.Oracle.com/JavaColor/spec…
Брат Лей делает простой перевод этих байт-кодов здесь:
iconst помещает значение типа int в стек операндов. istore должен хранить int в локальной переменной. iload загружает значения int из локальных переменных. iinc увеличивает локальные переменные путем индексации. ireturn возвращает значение типа int из стека операндов. astore сохраняет ссылку в локальную переменную.
С этой информацией давайте переведем приведенное выше содержимое байт-кода:
0 iconst_1 在操作数栈中存储数值 1
1 istore_0 将操作数栈中的数据存储在局部变量的位置 0
2 iload_0 从局部变量读取值到操作数栈
3 istore_1 将操作数栈中存储 1 存储在局部变量的位置 1
4 iinc 0 by 1 把局部变量位置 0 的元素进行递增(+1)操作
7 iload_1 将局部位置 1 的值加载到操作数栈中
8 ireturn 返回操作数栈中的 int 值
Благодаря приведенной выше информации вы, возможно, не сможете интуитивно увидеть внутренний процесс выполнения этого метода. Это не имеет значения, брат Лей подготовил для вас блок-схему выполнения метода:
Из приведенного выше рисунка видно, что перед выполнением оператора finally (iinc 0, 1) в таблице локальных переменных хранятся два фрагмента информации.И позиция 0, и позиция 1 хранят значение int со значением 1. Перед выполнением finally (iinc 0, 1) накапливается только значение позиции 0, а затем значение позиции 1 (1) возвращается в стек операндов, поэтому при выполнении операции возврата (ireturn) будет быть возвращены из операции.Результат читается в стеке с возвращаемым значением 1, поэтому окончательное выполнение будет 1 вместо 2.
③ Решения
Что касается того, как виртуальная машина Java компилирует блоки finally, заинтересованные читатели могут обратиться к Разделу 7.13 Компиляция finally в Спецификации виртуальной машины JavaTM, второе издание. Там есть подробное описание того, как виртуальная машина Java компилирует блоки finally.
На самом деле, виртуальная машина Java вставляет блок операторов finally непосредственно как подпрограмму (для этой подпрограммы я не знаю, как ее перевести, поэтому не перевожу вообще, чтобы избежать двусмысленности и недопонимания) непосредственно перед оператор передачи управления блока операторов try или блока операторов catch. Однако есть еще один фактор, который нельзя игнорировать: перед выполнением подпрограммы (то есть блока finally) блок try или catch будет сохранять свое возвращаемое значение в таблице локальных переменных до тех пор, пока подпрограмма не будет выполнена. , восстановить зарезервированное возвращаемое значение в стек операндов, а затем вернуть его вызывающей стороне метода (вызывающему) с помощью инструкции return или throw.
Поэтому, если в try-catch-finally есть операция возврата, ** необходимо убедиться, что оператор return появляется только один раз в конце метода! ** Это гарантирует, что все коды операций в try-catch-finally вступят в силу.
④ Положительный пример кода
private static int getValueByAmend() {
int num = 1;
try {
// do something
} catch (Exception e) {
// do something
} finally {
num++;
}
return num;
}
Яма 3: «Нефинальное» выполнение кода в finally
① Пример кода счетчика
public static void main(String[] args) throws FileNotFoundException {
execErr();
}
private static void execErr() {
try {
throw new RuntimeException();
} catch (RuntimeException e) {
e.printStackTrace();
} finally {
System.out.println("执行 finally.");
}
}
Результат выполнения приведенного выше кода выглядит следующим образом:Как видно из вышеуказанных результатовКод в finally не выполняется последним, но до того, как catch напечатает исключение,Почему это?
② Анализ причин
Настоящая причина вышеупомянутых проблем на самом деле не из-за try-catch-finally.Когда мы открываем исходный код e.printStackTrace, мы можем увидеть некоторые подсказки.Исходный код выглядит следующим образом:Как видно из рисунка выше, при выполнении e.printStackTrace() и, наконец, при выводе информации один и тот же объект не используется.Наконец, используя стандартный выходной поток: System.out, и e.printstacktrace () используется в стандартной ошибке выходной поток: system.err.println, они выполняют тот же эффект, что и:
public static void main(String[] args) {
System.out.println("我是标准输出流");
System.err.println("我是标准错误输出流");
}
Порядок результатов выполнения приведенного выше кода также случайный, и причину всего этого можно увидеть в комментариях и документации стандартного потока вывода ошибок (System.err):
Мы просто делаем простой перевод приведенных выше комментариев:
"Стандартный" поток вывода ошибок. Поток открыт и готов принимать выходные данные. Как правило, этот поток соответствует хост-среде, указанному пользователем выводу дисплея или другому назначению вывода. По соглашению, даже если основной поток вывода (поток вывода out) перенаправляется в файл или другое место назначения, этот поток вывода (поток вывода с ошибкой) можно использовать для отображения сообщений об ошибках или другой информации, которая должна быть немедленно доведена до сведения пользователя. .
Как видно из комментариев к исходному коду, стандартный поток вывода ошибок (System.err) и стандартный поток вывода (System.out) используют разные объекты потока, даже если стандартный поток вывода расположен в других файлах, это не повлияет на стандартный поток вывода ошибок. Тогда мы можем сделать смелое предположение: оба выполняются независимо, и для более эффективного вывода информации о потоке они выполняются параллельно, поэтому в результате мы видим, что порядок печати всегда случайный.
Чтобы проверить эту точку зрения, мы перенаправляем стандартный поток вывода в файл, а затем наблюдаем, может ли System.err нормально печатать Код реализации выглядит следующим образом:
public static void main(String[] args) throws FileNotFoundException {
// 将标准输出流的信息定位到 log.txt 中
System.setOut(new PrintStream(new FileOutputStream("log.txt")));
System.out.println("我是标准输出流");
System.err.println("我是标准错误输出流");
}
Результат выполнения приведенного выше кода выглядит следующим образом:При выполнении программы мы обнаруживаем, что в корневом каталоге проекта появился новый файл log.txt, открываем этот файл и видим следующие результаты:
Из приведенных выше результатов видно, что стандартный поток вывода и стандартный поток вывода ошибок выполняются независимо друг от друга, и JVM будет запускать их параллельно для эффективного выполнения, поэтому окончательный результат, который мы видим, таков: выполняется до отлова.
③ Решения
Зная причину, с проблемой легко справиться, нам нужно только изменить объект вывода в try-catch-finally на объект унифицированного потока вывода, чтобы решить эту проблему.
④ Положительный пример кода
private static void execErr() {
try {
throw new RuntimeException();
} catch (RuntimeException e) {
System.out.println(e);
} finally {
System.out.println("执行 finally.");
}
}
После перехода к унифицированному объекту потока вывода я вручную выполнил его n раз и не обнаружил никаких проблем.
Яма 4: код в finally «не выполняется»
Наконец код будет реализован? Если это до того, как я не стесняюсь сказать «да», но после того, как избиения потерпели общество, я могу ответить:При нормальных обстоятельствах код в finally будет выполнен, но в особых случаях код в finally не обязательно будет выполнен., например, в следующих случаях:
- System.exit выполняется в операторе try-catch;
- В операторе try-catch возникает бесконечный цикл;
- Выключите питание или произойдет сбой JVM до окончательного выполнения.
Если возникнет какое-либо из вышеперечисленных условий, в конечном итоге код не будет выполнен.Хотя это небольшое подозрение в «подъемной планке», Мерфи Лоу говорит нам, что если что-то может случиться, то произойдет он. Таким образом, с строгой точки зрения, эта точка зрения все еще существует., особенно для новичков, легко написать бесконечный цикл, который вы не можете узнать, не зная его, не так ли?
① Пример кода счетчика
public static void main(String[] args) {
noFinally();
}
private static void noFinally() {
try {
System.out.println("我是 try~");
System.exit(0);
} catch (Exception e) {
// do something
} finally {
System.out.println("我是 fially~");
}
}
Результат выполнения приведенного выше кода выглядит следующим образом:Из приведенных выше результатов видно, что код в finally не выполняется.
② Решение
Исключите код System.exit в коде, если это не требуется для бизнеса, но такжеСледует отметить, что если код System.exit появится в try-catch, код в finally не будет выполнен.
Суммировать
В этой статье мы показываем некоторые проблемы, в конце концов, есть очень практичные галантерейные товары, и есть некоторые примеры, которые кажутся «варварами», но все они подтверждают одно со стороны, то есть попробуйте-поймайте, наконец, что вы хотите полностью понять Это не простое дело. Наконец, чтобы подчеркнуть, что если есть операция, которая возвращает возвращаемое значение в try-catch-finally, тоПросто убедитесь, что оператор return появляется только один раз в конце метода!
Ссылки и благодарности
Alibaba "Руководство по развитию Java"
developer.ibm.com/zh/articles/j-lo-finally
Никакого общественного беспокойства «Java Chiry Community» не найдет более сухой.
Посетите Github, чтобы узнать больше интересных вещей:GitHub.com/VIP камень/Али…