Основы Java — синтаксический сахар try-with-resource

Java

Оригинальный адрес: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()код, который определенно гарантирует, что реальный поток закрыт.