Оригинальный адрес:nuggets.capable/post/684490…
задний план
Как мы все знаем, все открытые системные ресурсы, такие как потоки, файлы или соединения сокетов, должны быть закрыты разработчиком вручную, иначе при непрерывной работе программы утечка ресурсов перерастет в крупную производственную аварию.
В мире Java есть кунг-фу, называемое наконец, которое может гарантировать, что когда вы занимаетесь боевыми искусствами, вы также можете выполнять некоторые операции по самопомощи. В древние времена код для обработки закрытия ресурсов обычно писался в блоке finally. Однако, если у вас одновременно открыто несколько ресурсов, произойдет кошмарный сценарий:
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) {
throw e;
}
finally {
if (bout != null) {
try {
bout.close();
}
catch (IOException e) {
throw e;
}
}
}
}
}
}
}
Боже мой! ! !Кода для закрытия ресурсов больше, чем бизнес-кода! ! ! Это потому, что нам нужно не только закрытьBufferedInputStream
, также необходимо убедиться, что в случае закрытияBufferedInputStream
Исключение произошло, когдаBufferedOutputStream
Он также должен быть правильно закрыт. Так что приходится прибегать к методу вложения finally в finally. Можно предположить, что чем больше ресурсов открыто, тем глубже будет вложенность наконец! ! !
Синтаксический сахар try-with-resource, добавленный в Java 1.7, открывает ресурсы без необходимости кодировщикам писать ресурсы для закрытия кода. Никогда не беспокойся о том, что я снова сломаю почерк! Давайте перепишем предыдущий пример с помощью try-with-resource:
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-resource, ресурс должен реализоватьAutoClosable
интерфейс. Класс реализации этого интерфейса необходимо переписатьclose
метод:
public class Connection implements AutoCloseable {
public void sendData() {
System.out.println("正在发送数据");
}
@Override
public void close() throws Exception {
System.out.println("正在关闭连接");
}
}
вызывающий класс:
public class TryWithResource {
public static void main(String[] args) {
try (Connection conn = new Connection()) {
conn.sendData();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
Вывод после запуска:
正在发送数据
正在关闭连接
принцип
Итак, как это делается? Я полагаю, что вы, умные люди, должны были догадаться, что, по сути, все это призрак бога-компилятора. Декомпилируем файл класса из предыдущего примера:
package com.codersm.trywithresource;
public class TryWithResource {
public TryWithResource() {
}
public static void main(String[] args) {
try {
Connection conn = new Connection();
Throwable var2 = null;
try {
conn.sendData();
} catch (Throwable var12) {
var2 = var12;
throw var12;
} finally {
if (conn != null) {
if (var2 != null) {
try {
conn.close();
} catch (Throwable var11) {
var2.addSuppressed(var11);
}
} else {
conn.close();
}
}
}
} catch (Exception var14) {
var14.printStackTrace();
}
}
}
Смотрите, в строках 15-27 компилятор автоматически генерирует для нас блок finally и вызывает в нем метод close ресурса, поэтому метод close в примере будет выполняться во время выполнения.
маска исключения
Если вы будете внимательны, вы, должно быть, обнаружили, что декомпилированный код (строка 21) на один код больше, чем код, написанный в древние времена.addSuppressed
. Чтобы понять назначение этого кода, давайте немного изменим предыдущий пример: мы изменим предыдущий код обратно на способ ручного закрытия исключений в древние времена, а вsendData
а такжеclose
Метод выдает исключение:
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");
}
}
Измените основной метод:
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();
}
}
}
}
После запуска мы обнаружили:
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)
......
Хорошо, вот проблема, поскольку мы можем генерировать только одно исключение за раз, то, что мы видим вверху, - это последнее генерируемое исключение, котороеclose
броски методаMyException
,а такжеsendData
брошенныйException
игнорируется. Это называется маскированием исключений. Из-за потери информации об исключениях экранирование исключений может привести к тому, что некоторые ошибки станет очень трудно найти.Программистам приходится работать сверхурочно, чтобы найти ошибки.Как можно не избавиться от такой опухоли! К счастью, для решения этой проблемы, начиная с Java 1.7, у больших парней естьThrowable
добавлен классaddSuppressed
Метод, поддерживающий присоединение одного исключения к другому, что позволяет избежать маскирования исключений. Итак, в каком формате будет выводиться информация о замаскированном исключении? Давайте снова запустим основной метод, который мы только что обернули с помощью try-with-resource:
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
Как видите, есть еще одно сообщение об исключенииSuppressed
Подсказка сообщает нам, что это исключение на самом деле состоит из двух исключений,MyException
является Подавленным исключением. поздравляю!
Меры предосторожности
В процессе использования try-with-resource вы должны понимать ресурсclose
Логика реализации внутри метода. В противном случае утечка ресурсов все равно может произойти.
Например, в Java BIO используется ряд шаблонов декораторов. при вызове декоратораclose
метод, он, по сути, вызывает поток, обернутый внутри декоратораclose
метод. Например:
public class TryWithResource {
public static void main(String[] args) {
try (FileInputStream fin = new FileInputStream(new File("input.txt"));
GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(new File("out.txt")))) {
byte[] buffer = new byte[4096];
int read;
while ((read = fin.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
В приведенном выше коде мы начинаемFileInputStream
читать байты и писать вGZIPOutputStream
середина.GZIPOutputStream
в реальностиFileOutputStream
декоратор. Из-за особенностей try-with-resource фактически скомпилированный код будет сопровождаться блоком finally, а внутри будут вызываться методы fin.close() и out.close(). Давайте посмотримGZIPOutputStream
Близкий метод класса:
public void close() throws IOException {
if (!closed) {
finish();
if (usesDefaultDeflater)
def.end();
out.close();
closed = true;
}
}
Мы видим, что переменная out на самом деле представляет оформленныйFileOutputStream
Добрый. при вызове переменной outclose
метод раньше,GZIPOutputStream
также сделалfinish
операции, операция будет продолжатьсяFileOutputStream
записать информацию о сжатии в середине, если в это время произойдет исключение,out.close()
Метод пропущен, но это метод закрытия ресурса нижнего уровня. Правильный способ — объявить ресурс нижнего уровня отдельно в try-with-resource, чтобы обеспечить соответствующийclose
Метод должен иметь возможность вызываться. В предыдущем примере нам нужно объявить каждыйFileInputStream
так же какFileOutputStream
:
public class TryWithResource {
public static void main(String[] args) {
try (FileInputStream fin = new FileInputStream(new File("input.txt"));
FileOutputStream fout = new FileOutputStream(new File("out.txt"));
GZIPOutputStream out = new GZIPOutputStream(fout)) {
byte[] buffer = new byte[4096];
int read;
while ((read = fin.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}
Поскольку компилятор автоматически сгенерируетfout.close()
код, который определенно гарантирует, что реальный поток закрыт.