Что такое трейс ошибки

Практика показала, что хардкорные расшифровки с наших докладов хорошо заходят, так что мы решили продолжать. Сегодня у нас в меню смесь из подходов к поиску и анализу ошибок и крэшей, приправленная щепоткой полезных инструментов, подготовленная на основе доклада Андрея Паньгина aka apangin из Одноклассников на одном из JUG’ов (это была допиленная версия его доклада с JPoint 2016). В без семи минут двухчасовом докладе Андрей подробно рассказывает о стек-трейсах и хип-дампах.

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

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

Когда я делал тренировочный прогон этого доклада у нас в офисе, один из коллег спросил: «Все это очень интересно, но на практике это кому-нибудь вообще полезно?» После этого разговора первым слайдом в свою презентацию я добавил страницу с вопросами по теме на StackOverflow. Так что это актуально.

Сам я работаю ведущим программистом в Одноклассниках. И так сложилось, что зачастую мне приходится работать с внутренностями Java — тюнить ее, искать баги, дергать что-то через системные классы (порой не совсем легальными способами). Оттуда я и почерпнул большую часть информации, которую сегодня хотел вам представить. Конечно, в этом мне очень помог мой предыдущий опыт: я 6 лет работал в Sun Microsystems, занимался непосредственно разработкой виртуальной Java-машины. Так что теперь я знаю эту тему как изнутри JVM, так и со стороны пользователя-разработчика.

Стек-трейсы

Стек-трейсы exception

Когда начинающий разработчик пишет свой «Hello world!», у него выскакивает эксепшн и ему демонстрируется стек-трейс, где произошла эта ошибка. Так что какие-то представления о стек-трейсах есть у большинства.

Перейдем сразу к примерам.

Я написал небольшую программку, которая в цикле 100 миллионов раз производит такой эксперимент: создает массив из 10 случайных элементов типа long и проверяет, сортированный он получился или нет.

package demo1;

import java.util.concurrent.ThreadLocalRandom;

public class ProbabilityExperiment {

    private static boolean isSorted(long[] array) {
        for (int i = 0; i < array.length; i++) {
            if (array[i] > array[i + 1]) {
                return false;
            }
        }
        return true;
    }

    public void run(int experiments, int length) {
        int sorted = 0;

        for (int i = 0; i < experiments; i++) {
            try {
                long[] array = ThreadLocalRandom.current().longs(length).toArray();
                if (isSorted(array)) {
                    sorted++;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        System.out.printf("%d of %d arrays are sortedn", sorted, experiments);
    }

    public static void main(String[] args) {
        new ProbabilityExperiment().run(100_000_000, 10);
    }
}

По сути он считает вероятность получения сортированного массива, которая приблизительно равна 1/n!. Как это часто бывает, в программке ошиблись на единичку:

for (int i = 0; i < array.length; i++)

Что произойдет? Эксепшн, выход за пределы массива.
Давайте разбираться, в чем дело. У нас в консоль выводится:

java.lang.ArrayIndexOutOfBoundsException

но стек-трейсов никаких нет. Куда делись?

В HotSpot JVM есть такая оптимизация: у эксепшенов, которые кидает сама JVM из горячего кода, а в данном случае код у нас горячий — 100 миллионов раз дергается, стек-трейсы не генерируются.
Это можно исправить с помощью специального ключика:

-XX:-OmitStackTraceInFastThrow

Теперь попробуем запустить пример. Получаем все то же самое, только все стек-трейсы на месте.

Подобная оптимизация работает для всех неявных эксепшенов, которые бросает JVM: выход за границы массива, разыменование нулевого указателя и т.д.

Раз оптимизацию придумали, значит она зачем-то нужна? Понятно, что программисту удобнее, когда стек-трейсы есть.

Давайте измерим, сколько «стоит» у нас создание эксепшена (сравним с каким-нибудь простым Java-объектом, вроде Date).

@Benchmark
public Object date() {
    return new Date();
}

@Benchmark
public Object exception() {
    return new Exception();
}

С помощью JMH напишем простенькую бенчмарку и измерим, сколько наносекунд занимают обе операции.

Оказывается, создать эксепшн в 150 раз дороже, чем обычный объект.
И тут не все так просто. Для виртуальной машины эксепшн не отличается от любого другого объекта, но разгадка кроется в том, что практически все конструкторы эксепшн так или иначе сводятся к вызову метода fillInStackTrace, который заполняет стек-трейс этого эксепшена. Именно заполнение стек-трейса отнимает время.

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

Метод fillInStackTrace публичный, не final. Давайте его просто переопределим:

@Benchmark
public Object exceptionNoStackTrace() {
    return new Exception() {
        @Override
        public Throwable fillInStackTrace() {
            return this;
        }
    };
}

Теперь создание обычного объекта и эксепшена без стек-трейса отнимают одинаковое время.

Есть и другой способ создать эксепшн без стек-трейса. Начиная с Java 7, у Throwable и у Exception есть protected-конструктор с дополнительным параметром writableStackTrace:

protected Exception(String message, Throwable cause,
                    boolean enableSuppression,
                    boolean writableStackTrace);

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

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

А сколько стоит бросить эксепшн?

Рассмотрим разные случаи: когда он бросается и ловится в одном методе, а также ситуации с разной глубиной стека.

@Param("1", "2", "10", "100", "1000"})
int depth;

@Benchmark
public Object throwCatch() {
    try {
        return recursive(depth);
    } catch (Exception e) {
        return e;
    }
}

Вот, что дают измерения:

Т.е. если у нас глубина небольшая (эксепшн ловится в том же фрейме или фреймом выше — глубина 0 или 1), эксепшн ничего не стоит. Но как только глубина стека становится большой, затраты совсем другого порядка. При этом наблюдается четкая линейная зависимость: «стоимость» исключения почти линейно зависит от глубины стека.

Дорого стоит не только получение стек-трейса, но и дальнейшие манипуляции — распечатка, отправка по сети, запись, — все, для чего используется метод getStackTrace, который переводит сохраненный стек-трейс в объекты Java.

@Benchmark
public Object fillInStackTrace() {
    return new Exception();
}

@Benchmark
public Object getStackTrace() {
    return new Exception().getStackTrace();
}

Видно, что преобразование стек-трейса в 10 раз «дороже» его получения:

Почему это происходит?

Вот метод getStackTrace в исходниках JDK:

Сначала через вызов нативного метода мы узнаем глубину стека, потом в цикле до этой глубины вызываем нативный метод, чтобы получить очередной фрейм и сконвертировать его в объект StackTraceElement (это нормальный объект Java с кучей полей). Мало того, что это долго, процедура отнимает много памяти.

Более того, в Java 9 этот объект дополнен новыми полями (в связи с известным проектом модуляризации) — теперь каждому фрейму приписывается отметка о том, из какого он модуля.

Привет тем, кто парсит эксепшены с помощью регулярных выражений. Готовьтесь к сюрпризам в Java 9 — появятся еще и модули.

Давайте подведем итоги

  • создание самого объекта эксепшн — дешевое;
  • занимает время получение его стек-трейса;
  • еще дороже — преобразование этого внутреннего стек-трейса в Java-объект в StackTraceElement. Сложность этого дела прямо пропорциональна глубине стека.
  • бросание эксепшн — быстрое, оно почти ничего не стоит (почти как безусловный переход),
  • но только если эксепшн ловится в том же фрейме. Тут надо дополнить еще, что JIT у нас умеет инлайнить методы, поэтому один скомпилированный фрейм может в себя включать несколько Java-методов, заинлайниных друг в друга. Но если эксепшн ловится где-то глубже по стеку, его дороговизна пропорциональна глубине стека.

Пара советов:

  • отключайте на продакшене оптимизацию, возможно, это сэкономит много времени на отладке:
    -XX:-OmitStackTraceInFastThrow

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

Стек-трейсы в тред дампах

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

Фрагменты вывода этой утилиты:

Что здесь видно? Какие есть потоки, в каком они состоянии и их текущий стек.

Более того, если потоки захватили какие-то локи, ожидают входа в synchronized-секцию или взятия ReentrantLock, это также будет отражено в стек-трейсе.

Порой полезным оказывается малоизвестный идентификатор:

Он напрямую связан с ID потока в операционной системе. Например, если вы смотрите программой top в Linux, какие треды у вас больше всего едят CPU, pid потока — это и есть тот самый nid, который демонстрируется в тред дампе. Можно тут же найти, какому Java-потоку он соответствует.

В случае с мониторами (с synchronized-объектами) прямо в тред дампе будет написано, какой тред и какие мониторы держит, кто пытается их захватить.

В случае с ReentrantLock это, к сожалению, не так. Здесь видно, как Thread 1 пытается захватить некий ReentrantLock, но при этом не видно, кто этот лок держит. На этот случай в VM есть опция:

-XX:+PrintConcurrentLocks

Если мы запустим то же самое с PrintConcurrentLocks, в тред дампе увидим и ReentrantLock.

Здесь указан тот самый id лока. Видно, что его захватил Thread 2.

Если опция такая хорошая, почему бы ее не сделать «по умолчанию»?

Она тоже чего-то стоит. Чтобы напечатать информацию о том, какой поток какие ReentrantLock’и держит, JVM пробегает весь Java heap, ищет там все ReentrantLock’и, сопоставляет их с тредами и только потом выводит эту информацию (у треда нет информации о том, какие локи он захватил; информация есть только в обратную сторону — какой лок связан с каким тредом).

В указанном примере по названиям потоков (Thread 1 / Thread 2) непонятно, к чему они относятся. Мой совет из практики: если у вас происходит какая-то длинная операция, например, сервер обрабатывает клиентские запросы или, наоборот, клиент ходит к нескольким серверам, выставляйте треду понятное имя (как в случае ниже — прямо IP того сервера, к которому клиент сейчас идет). И тогда в дампе потока сразу будет видно, ответа от какого сервера он сейчас ждет.

Хватит теории. Давайте опять к практике. Этот пример я уже не раз приводил.

package demo2;

import java.util.stream.IntStream;

public class ParallelSum {
    static int SUM = IntStream.range(0, 100).parallel().reduce(0, (x, y) -> x + y);

    public static void main(String[] args) {
        System.out.println(SUM);
    }
}

Запускаем программку 3 раза подряд. 2 раза она выводит сумму чисел от 0 до 100 (не включая 100), третий — не хочет. Давайте смотреть тред дампы:

Первый поток оказывается RUNNABLE, выполняет наш reduce. Но смотрите, какой интересный момент: Thread.State вроде как RUNNABLE, но при этом написано, что поток in Object.wait().

Мне тоже это было не понятно. Я даже хотел сообщить о баге, но оказывается, такая бага заведена много лет назад и закрыта с формулировкой: «not an issue, will not fix».
В этой программке действительно есть дедлок. Его причина — инициализация классов.

Выражение выполняется в статическом инициализаторе класса ParallelSum:

static int SUM = IntStream.range(0, 100).parallel().reduce(0, (x, y) -> x + y);

Но поскольку стрим параллельный, исполнение происходит в отдельных потоках ForkJoinPool, из которых вызывается тело лямбды:

(x, y) -> x + y

Код лямбды записан Java-компилятором прямо в классе ParallelSum в виде приватного метода. Получается, что из ForkJoinPool мы пытаемся обратиться к классу ParallelSum, который в данный момент находится на этапе инициализации. Поэтому потоки начинают ждать, когда же закончится инициализация класса, а она не может закончиться, поскольку ожидает вычисления этой самой свертки. Дедлок.

Почему вначале сумма считалась? Просто повезло. У нас небольшое количество элементов суммируется, и иногда все исполняется в одном потоке (другой поток просто не успевает).

Но почему же тогда поток в стек-трейсе RUNNABLE? Если почитать документацию к Thread.State, станет понятно, что никакого другого состояния здесь быть не может. Не может быть состояния BLOCKED, поскольку поток не заблокирован на Java-мониторе, нет никакой synchronized-секции, и не может быть состояния WAITING, потому что здесь нет никаких вызовов Object.wait(). Синхронизация происходит на внутреннем объекте виртуальной машины, который, вообще говоря, даже не обязан быть Java-объектом.

Стек-трейс при логировании

Представьте себе ситуацию: в куче мест в нашем приложении что-то логируется. Было бы полезно узнать, из какого места появилась та или иная строчка.

В Java нет препроцессора, поэтому нет возможности использовать макросы __FILE__, __LINE__, как в С (эти макросы еще на этапе компиляции преобразуются в текущее имя файла и строку). Поэтому других способов дополнить вывод именем файла и номером строки кода, откуда это было напечатано, кроме как через стек-трейсы, нет.

public static String getLocation() {
    StackTraceElement s = new Exception().getStackTrace()[2];
	return s.getFileName() + ':' + s.getLineNumber();
}

Генерим эксепшн, у него получаем стек-трейс, берем в данном случае второй фрейм (нулевой — это метод getLocation, а первый — вызывает метод warning).

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

Помимо getStackTrace у исключения есть метод getStackTrace объекта Thread.

Thread.current().getStackTrace()

Будет ли так быстрее?

Нет. JVM никакой магии не делает, здесь все будет работать через тот же эксепшн с точно таким же стек-трейсом.

Но хитрый способ все-таки есть:

public static String getLocation() {
    StackTraceElement s = sun.misc.SharedSecrets.getJavaLangAccess()
            .getStackTraceElement(new Exception(), 2);

    return s.getFileName() + ':' + s.getLineNumber();
}

Я люблю всякие приватные штуки: Unsafe, SharedSecrets и т.д.

Есть аксессор, который позволяет получить StackTraceElement конкретного фрейма (без необходимости преобразовывать весь стек-трейс в Java-объекты). Это будет работать быстрее. Но есть плохая новость: в Java 9 это работать не будет. Там проделана большая работа по рефакторингу всего, что связано со стек-трейсами, и таких методов там теперь просто нет.

Конструкция, позволяющая получить какой-то один фрейм, может быть полезна в так называемых Caller-sensitive методах — методах, чей результат может зависеть от того, кто их вызывает. В прикладных программах с такими методами приходится сталкиваться нечасто, но в самой JDK подобных примеров немало:

В зависимости от того, кто вызывает Class.forName, поиск класса будет осуществляться в соответствующем класс-лоадере (того класса, который вызвал этот метод); аналогично — с получением ResourceBundle и загрузкой библиотеки System.loadLibrary. Также информация о том, кто вызывает, полезна при использовании различных методов, которые проверяют пермиссии (а имеет ли данный код право вызывать этот метод). На этот случай в «секретном» API предусмотрен метод getCallerClass, который на самом деле является JVM-интринсиком и вообще почти ничего не стоит.

sun.reflect.Reflection.getCallerClass

Как уже много раз говорилось, приватный API — это зло, использовать которое крайне не рекомендуется (сами рискуете нарваться на проблемы, подобные тем, что ранее вызвал Unsafe). Поэтому разработчики JDK задумались над тем, что раз этим пользуются, нужна легальная альтернатива — новый API для обхода потоков. Основные требования к этому API:

  • чтобы можно было обойти только часть фреймов (если нам нужно буквально несколько верхних фреймов);
  • возможность фильтровать фреймы (не показывать ненужные фреймы, относящиеся к фреймворку или системным классам);
  • чтобы эти фреймы конструировались ленивым образом (lazy) — если нам не нужно получать информацию о том, с каким файлом он связан, эта информация преждевременно не извлекается;
  • как в случае с getCallerClass — нам нужно не имя класса, а сам инстанс java.lang.Class.

Известно, что в публичном релизе Java 9 будет java.lang.StackWalker.
Получить его экземпляр очень просто — методом getInstance. У него есть несколько вариантов — дефолтный StackWalker или незначительно конфигурируемый опциями:

  • опция RETAIN_CLASS_REFERENCE означает, что вам нужны не имена классов, а именно инстансы;
  • прочие опции позволяют показать в стек-трейсе фреймы, относящиеся к системным классам и классам рефлекшн (по умолчанию они не будут показаны в стек-трейсе).

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

Простейший пример, как этим пользоваться:

StackWalker sw = StackWalker.getInstance();
sw.forEach(System.out::println);

Берем StackWalker и вызываем метод forEach, чтобы он обошел все фреймы. В результате получим такой простой стек-трейс:

То же самое с опцией SHOW_REFLECT_FRAMES:

StackWalker sw = StackWalker.getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES);
sw.forEach(System.out::println);

В этом случае добавятся методы, относящиеся к вызову через рефлекшн:

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

StackWalker sw = StackWalker.getInstance(StackWalker.Option.SHOW_HIDDEN_FRAMES);
sw.forEach(System.out::println);

В стек-трейсе появятся методы динамически-сгенерированных классов лямбд:

А теперь самый главный метод, который есть в StackWalker API — метод walk с такой хитрой непонятной сигнатурой с кучей дженериков:

public <T> T walk(Function<? super Stream<StackFrame>, ? extends T> function)

Метод walk принимает функцию от стек-фрейма.

Его работу проще показать на примере.

Несмотря на то, что все это выглядит страшно, как этим пользоваться — очевидно. В функцию передается стрим, а уже над стримом можно проводить все привычные операции. К примеру, вот так выглядел бы метод getCallerFrame, который достает только второй фрейм: пропускаются первые 2, потом вызывается findFirst:

public static StackFrame getCallerFrame() {
    return StackWalker.getInstance()
            .walk(stream -> stream.skip(2).findFirst())
            .orElseThrow(NoSuchElementException::new);
}

Метод walk возвращает тот результат, который возвращает эта функция стрима. Все просто.
Для данного конкретного случая (когда нужно получить просто Caller класс) есть специальный shortcut метод:

return StackWalker.getInstance(RETAIN_CLASS_REFERENCE).getCallerClass();

Еще один пример посложнее.

Обходим все фреймы, оставляем только те, которые относятся к пакету org.apache, и выводим первые 10 в список.

StackWalker sw = StackWalker.getInstance();

List<StackFrame> frames = sw.walk(stream ->
        stream.filter(sf -> sf.getClassName().startsWith("org.apache."))
              .limit(10)
              .collect(Collectors.toList()));

Интересный вопрос: зачем такая длинная сигнатура с кучей дженериков? Почему бы просто не сделать у StackWalker метод, который возвращает стрим?

public Stream<StackFrame> stream();

Если дать API, который возвращает стрим, у JDK теряется контроль над тем, что дальше над этим стримом делают. Можно дальше этот стрим положить куда-то, отдать в другой поток, попробовать его использовать через 2 часа после получения (тот стек, который мы пытались обойти, давно потерян, а тред может быть давно убит). Таким образом будет невозможно обеспечить «ленивость» Stack Walker API.

Основной поинт Stack Walker API: пока вы находитесь внутри walk, у вас зафиксировано состояние стека, поэтому все операции на этом стеке можно делать lazy.

На десерт еще немного интересного.

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

/* package-private */
interface LiveStackFrame extends StackFrame {

    public Object[] getMonitors();
    public Object[] getLocals();
    public Object[] getStack();
    public static StackWalker getStackWalker();
}

Защита здесь не ахти какая: класс просто сделали непубличным. Но кто же нам мешает взять рефлекшн и попробовать его? (Примечание: в актуальных сборках JDK 9 доступ к непубличному API через рефлекшн запрещён. Чтобы его разрешить, необходимо добавить опцию JVM --add-opens=java.base/java.lang=ALL-UNNAMED)

Пробуем на таком примере. Есть программа, которая рекурсивным методом ищет выход из лабиринта. У нас есть квадратное поле size x size. Есть метод visit с текущими координатами. Мы пытаемся из текущей клетки пойти влево / вправо / вверх / вниз (если они не заняты). Если дошли из правой-нижней клетки в левую-верхнюю, считаем, что нашли выход и распечатываем стек.

package demo3;

import java.util.Random;

public class Labyrinth {
    static final byte FREE = 0;
    static final byte OCCUPIED = 1;
    static final byte VISITED = 2;

    private final byte[][] field;

    public Labyrinth(int size) {
        Random random = new Random(0);
        field = new byte[size][size];

        for (int x = 0; x < size; x++) {
            for (int y = 0; y < size; y++) {
                if (random.nextInt(10) > 7) {
                    field[x][y] = OCCUPIED;
                }
            }
        }

        field[0][0] = field[size - 1][size - 1] = FREE;
    }

    public int size() {
        return field.length;
    }

    public boolean visit(int x, int y) {
        if (x == 0 && y == 0) {
            StackTrace.dump();
            return true;
        }

        if (x < 0 || x >= size() || y < 0 || y >= size() || field[x][y] != FREE) {
            return false;
        }

        field[x][y] = VISITED;
        return visit(x - 1, y) || visit(x, y - 1) || visit(x + 1, y) || visit(x, y + 1);
    }

    public String toString() {
        return "Labyrinth";
    }

    public static void main(String[] args) {
        Labyrinth lab = new Labyrinth(10);
        boolean exitFound = lab.visit(9, 9);
        System.out.println(exitFound);
    }
}

Запускаем:

Если я делаю обычный dumpStack, который был еще в Java 8, получаем обычный стек-трейс, из которого ничего не понятно. Очевидно — рекурсивный метод сам себя вызывает, но интересно, на каком шаге (и с какими значениями координат) вызывается каждый метод.

Заменим стандартный dumpStack на наш StackTrace.dump, который через рефлекшн использует live стек-фреймы:

package demo3;

import java.lang.reflect.Method;
import java.util.Arrays;

public class StackTrace {

    private static Object invoke(String methodName, Object instance) {
        try {
            Class<?> liveStackFrame = Class.forName("java.lang.LiveStackFrame");
            Method m = liveStackFrame.getMethod(methodName);
            m.setAccessible(true);
            return m.invoke(instance);
        } catch (ReflectiveOperationException e) {
            throw new AssertionError("Should not happen", e);
        }
    }

    public static void dump() {
        StackWalker sw = (StackWalker) invoke("getStackWalker", null);
        sw.forEach(frame -> {
            Object[] locals = (Object[]) invoke("getLocals", frame);
            System.out.println(" at " + frame + "  " + Arrays.toString(locals));
        });
    }
}

В первую очередь надо получить соответствующий StackWalker, вызвав метод getStackWalker. Все фреймы, которые будут передаваться в getStackWalker, на самом деле будут экземплярами лайв стек-фрейма, у которого есть дополнительные методы, в частности, getLocals для получения локальных переменных.

Запускаем. Получаем то же самое, но у нас отображается весь путь из лабиринта в виде значений локальных переменных:


На этом мы заканчиваем первую часть поста. Вторая часть здесь.

Лично встретиться с Андреем в Москве можно будет уже совсем скоро — 7-8 апреля на JPoint 2017. В этот раз он выступит с докладом «JVM-профайлер с чувством такта», в котором расскажет, как можно получить честные замеры производительности приложения, комбинируя несколько подходов к профилированию. Доклад будет «всего» часовой, зато в дискуссионной зоне никто не будет ограничивать вас от вопросов и горячих споров!

Кроме этого, на JPoint есть целая куча крутых докладов практически обо всем из мира Java — обзор планируемых докладов мы давали в другом посте, а просто программу конференции вы найдете на сайте мероприятия.

Простыми словами, трассировка стека – это список методов, которые были вызваны до момента, когда в приложении произошло исключение.

Простой случай

В указанном примере мы можем точно определить, когда именно произошло исключение. Рассмотрим трассировку стека:

Exception in thread "main" java.lang.NullPointerException
       at com.example.myproject.Book.getTitle(Book.java:16)
       at com.example.myproject.Author.getBookTitles(Author.java:25)
       at com.example.myproject.Bootstrap.main(Bootstrap.java:14)

Это пример очень простой трассировки. Если пойти по списку строк вида «at…» с самого начала, мы можем понять, где произошла ошибка. Мы смотрим на верхний вызов функции. В нашем случае, это:

at com.example.myproject.Book.getTitle(Book.java:16)

Для отладки этого фрагмента открываем Book.java и смотрим, что находится на строке 16:

public String getTitle() {
    System.out.println(title.toString()); <-- line 16
    return title;
}

Это означает то, что в приведенном фрагменте кода какая-то переменная (вероятно, title) имеет значение null.

Пример цепочки исключений

Иногда приложения перехватывают исключение и выбрасывают его в виде другого исключения. Обычно это выглядит так:

try {
    ....
} catch (NullPointerException e) {
    throw new IllegalStateException("A book has a null property", e)
}

Трассировка в этом случае может иметь следующий вид:

Exception in thread "main" java.lang.IllegalStateException: A book has a null property
       at com.example.myproject.Author.getBookIds(Author.java:38)
       at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Caused by: java.lang.NullPointerException
       at com.example.myproject.Book.getId(Book.java:22)
       at com.example.myproject.Author.getBookIds(Author.java:35)
       ... 1 more

В этом случае разница состоит в атрибуте «Caused by» («Чем вызвано»). Иногда исключения могут иметь несколько секций «Caused by». Обычно необходимо найти исходную причину, которой оказывается в самой последней (нижней) секции «Caused by» трассировки. В нашем случае, это:

Caused by: java.lang.NullPointerException <-- root cause
       at com.example.myproject.Book.getId(Book.java:22) <-- important line

Аналогично, при подобном исключении необходимо обратиться к строке 22 книги Book.java, чтобы узнать, что вызвало данное исключение – NullPointerException.

Еще один пугающий пример с библиотечным кодом

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

javax.servlet.ServletException: Произошло что–то ужасное
   at com.example.myproject.OpenSessionInViewFilter.doFilter(OpenSessionInViewFilter.java:60)
   at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
   at com.example.myproject.ExceptionHandlerFilter.doFilter(ExceptionHandlerFilter.java:28)
   at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
   at com.example.myproject.OutputBufferFilter.doFilter(OutputBufferFilter.java:33)
   at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
   at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388)
   at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
   at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
   at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
   at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418)
   at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
   at org.mortbay.jetty.Server.handle(Server.java:326)
   at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
   at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:943)
   at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:756)
   at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:218)
   at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
   at org.mortbay.jetty.bio.SocketConnector$Connection.run(SocketConnector.java:228)
   at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)
Caused by: com.example.myproject.MyProjectServletException
   at com.example.myproject.MyServlet.doPost(MyServlet.java:169)
   at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
   at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
   at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
   at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1166)
   at com.example.myproject.OpenSessionInViewFilter.doFilter(OpenSessionInViewFilter.java:30)
   ... 27 more
Caused by: org.hibernate.exception.ConstraintViolationException: could not insert: [com.example.myproject.MyEntity]
   at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:96)
   at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
   at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:64)
   at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2329)
   at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2822)
   at org.hibernate.action.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:71)
   at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:268)
   at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:321)
   at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204)
   at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:130)
   at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.saveWithGeneratedOrRequestedId(DefaultSaveOrUpdateEventListener.java:210)
   at org.hibernate.event.def.DefaultSaveEventListener.saveWithGeneratedOrRequestedId(DefaultSaveEventListener.java:56)
   at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsTransient(DefaultSaveOrUpdateEventListener.java:195)
   at org.hibernate.event.def.DefaultSaveEventListener.performSaveOrUpdate(DefaultSaveEventListener.java:50)
   at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:93)
   at org.hibernate.impl.SessionImpl.fireSave(SessionImpl.java:705)
   at org.hibernate.impl.SessionImpl.save(SessionImpl.java:693)
   at org.hibernate.impl.SessionImpl.save(SessionImpl.java:689)
   at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
   at java.lang.reflect.Method.invoke(Method.java:597)
   at org.hibernate.context.ThreadLocalSessionContext$TransactionProtectionWrapper.invoke(ThreadLocalSessionContext.java:344)
   at $Proxy19.save(Unknown Source)
   at com.example.myproject.MyEntityService.save(MyEntityService.java:59) <-- relevant call (see notes below)
   at com.example.myproject.MyServlet.doPost(MyServlet.java:164)
   ... 32 more
Caused by: java.sql.SQLException: Violation of unique constraint MY_ENTITY_UK_1: duplicate value(s) for column(s) MY_COLUMN in statement [...]
   at org.hsqldb.jdbc.Util.throwError(Unknown Source)
   at org.hsqldb.jdbc.jdbcPreparedStatement.executeUpdate(Unknown Source)
   at com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeUpdate(NewProxyPreparedStatement.java:105)
   at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:57)
   ... 54 more

В этом примере приведен далеко не полный стек вызовов. Что вызывает здесь наибольший интерес, так это поиск функций из нашего кода – из пакета com.example.myproject. В предыдущем примере мы сначала хотели отыскать «первопричину», а именно:

Caused by: java.sql.SQLException

Однако все вызовы методов в данном случае относятся к библиотечному коду. Поэтому мы перейдем к предыдущей секции «Caused by» и найдем первый вызов метода из нашего кода, а именно:

at com.example.myproject.MyEntityService.save(MyEntityService.java:59)

Аналогично предыдущим примерам, необходимо обратить внимание на MyEntityService.java, строка 59: именно здесь появилась ошибка (в данном случае ситуация довольно очевидная, так как об ошибке сообщает SQLException, но в этом вопросе мы рассматриваем именно процедуру отладки с помощью трассировки).

Перевод ответа: «What is a stack trace, and how can I use it to debug my application errors?» @Rob Hruska

Что такое stack trace, и как с его помощью находить ошибки при разработке приложений?

Иногда при запуске своего приложения я получаю подобную ошибку:

Мне сказали, что это называется «трассировкой стека» или «stack trace». Что такое трассировка? Какую полезную информацию об ошибке в разрабатываемой программе она содержит?

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

user avatar

Простыми словами, трассировка стека – это список методов, которые были вызваны до момента, когда в приложении произошло исключение.

Простой случай

В указанном примере мы можем точно определить, когда именно произошло исключение. Рассмотрим трассировку стека:

Это пример очень простой трассировки. Если пойти по списку строк вида «at…» с самого начала, мы можем понять, где произошла ошибка. Мы смотрим на верхний вызов функции. В нашем случае, это:

Для отладки этого фрагмента открываем Book.java и смотрим, что находится на строке 16 :

Это означает то, что в приведенном фрагменте кода какая-то переменная (вероятно, title ) имеет значение null .

Пример цепочки исключений

Иногда приложения перехватывают исключение и выбрасывают его в виде другого исключения. Обычно это выглядит так:

Трассировка в этом случае может иметь следующий вид:

В этом случае разница состоит в атрибуте «Caused by» («Чем вызвано»). Иногда исключения могут иметь несколько секций «Caused by». Обычно необходимо найти исходную причину, которой оказывается в самой последней (нижней) секции «Caused by» трассировки. В нашем случае, это:

Аналогично, при подобном исключении необходимо обратиться к строке 22 книги Book.java , чтобы узнать, что вызвало данное исключение – NullPointerException .

Еще один пугающий пример с библиотечным кодом

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

В этом примере приведен далеко не полный стек вызовов. Что вызывает здесь наибольший интерес, так это поиск функций из нашего кода – из пакета com.example.myproject . В предыдущем примере мы сначала хотели отыскать «первопричину», а именно:

Однако все вызовы методов в данном случае относятся к библиотечному коду. Поэтому мы перейдем к предыдущей секции «Caused by» и найдем первый вызов метода из нашего кода, а именно:

Что такое трассировка стека и как ее использовать для отладки ошибок приложения?

Иногда, когда я запускаю свое приложение, я получаю ошибку, которая выглядит примерно так:

Люди называют это «трассировкой стека». Что такое трассировка стека? Что он может сказать мне об ошибке в моей программе?

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

  • 28 Кроме того, если строка трассировки стека не содержит имени файла и номера строки, класс для этой строки не был скомпилирован с отладочной информацией.

Проще говоря, трассировки стека — это список вызовов методов, в процессе которых приложение находилось в момент создания исключения.

Простой пример

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

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

Чтобы отладить это, мы можем открыть Book.java и посмотрите на строку 16 , который:

Это означало бы, что что-то (возможно, title ) является null в приведенном выше коде.

Пример с цепочкой исключений

Иногда приложения перехватывают исключение и повторно генерируют его как причину другого исключения. Обычно это выглядит так:

Это может дать вам трассировку стека, которая выглядит так:

Что отличает этот, так это «Причина». Иногда исключения содержат несколько разделов «Причина». Для них обычно требуется найти «основную причину», которая будет одним из самых низких разделов «Причина» в трассировке стека. В нашем случае это:

Опять же, за этим исключением мы хотели бы посмотреть на строку 22 из Book.java чтобы увидеть, что может вызвать NullPointerException Вот.

Более устрашающий пример с библиотечным кодом

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

В этом примере многое другое. Что нас больше всего беспокоит, так это поиск методов из наш код, что было бы чем угодно в com.example.myproject пакет. Во втором примере (выше) мы сначала хотели бы найти основную причину, а именно:

Однако все вызовы методов под этим кодом являются библиотечным кодом. Итак, мы перейдем к пункту «Причина» над ним и найдем первый вызов метода, исходящий из нашего кода, а именно:

Как и в предыдущих примерах, мы должны посмотреть на MyEntityService.java онлайн 59 , потому что именно здесь возникла эта ошибка (это немного очевидно, что пошло не так, поскольку SQLException сообщает об ошибке, но процедура отладки — это то, что нам нужно).

  • 4 @RobHruska — Очень хорошо объяснено. +1. Знаете ли вы какие-либо парсеры, которые принимают трассировку исключения в виде строки и предоставляют полезные методы для анализа трассировки стека? — например, getLastCausedBy () или getCausedByForMyAppCode («com.example.myproject»)
  • 1 @AndyDufresne — я не встречал ни одного, но, опять же, я тоже особо не смотрел.
  • 1 Предлагаемое улучшение: объясните первую строку трассировки стека, которая начинается с Exception in thread ‘main’ в вашем первом примере. Я думаю, было бы особенно полезно объяснить, что эта строка часто сопровождается сообщением, например значением переменной, которое может помочь диагностировать проблему. Я сам попытался внести правку, но мне не удается уместить эти идеи в существующую структуру вашего ответа.
  • 5 Также в java 1.7 добавлено «Подавлено:», в котором перечислены трассировки стека подавленных исключений перед отображением «Вызвано:» для этого исключения. Он автоматически используется конструкцией try-with-resource: docs.oracle.com/javase/specs/jls/se8/html/… и содержит исключения, если таковые возникли при закрытии ресурса (ов).
  • Существует JEP openjdk.java.net/jeps/8220715, цель которого — еще больше улучшить понятность, особенно NPE, путем предоставления таких деталей, как «Невозможно записать поле ‘nullInstanceField’, потому что ‘this.nullInstanceField’ имеет значение null».

Я отправляю этот ответ, поэтому самый верхний ответ (при сортировке по активности) не является просто неправильным.

Что такое Stacktrace?

Трассировка стека — очень полезный инструмент отладки. Он показывает стек вызовов (то есть стек функций, которые были вызваны до этого момента) в момент возникновения неперехваченного исключения (или время, когда трассировка стека была сгенерирована вручную). Это очень полезно, потому что это не только показывает вам, где произошла ошибка, но и то, как программа оказалась в этом месте кода. Это приводит к следующему вопросу:

Что такое исключение?

Исключение — это то, что среда выполнения использует, чтобы сообщить вам, что произошла ошибка. Популярные примеры: NullPointerException, IndexOutOfBoundsException или ArithmeticException. Каждая из них возникает, когда вы пытаетесь сделать что-то, что невозможно. Например, NullPointerException будет выброшено, когда вы попытаетесь разыменовать Null-объект:

Что мне делать с трассировками стека / исключениями?

Сначала выясните, что вызывает исключение. Попробуйте поискать в Google название исключения, чтобы выяснить, в чем причина этого исключения. В большинстве случаев это вызвано неправильным кодом. В приведенных выше примерах все исключения вызваны неправильным кодом. Итак, для примера NullPointerException вы можете убедиться, что a в то время никогда не бывает нулевым. Вы можете, например, инициализировать a или включите проверку, подобную этой:

Таким образом, нарушающая строка не выполняется, если a==null . То же самое и с другими примерами.

Иногда вы не можете быть уверены, что не получите исключения. Например, если вы используете сетевое соединение в своей программе, вы не можете помешать компьютеру потерять подключение к Интернету (например, вы не можете запретить пользователю отключать сетевое подключение компьютера). В этом случае сетевая библиотека, вероятно, выдаст исключение. Теперь вы должны поймать исключение и справиться Это. Это означает, что в примере с сетевым подключением вы должны попытаться повторно открыть соединение или уведомить пользователя или что-то в этом роде. Кроме того, всякий раз, когда вы используете catch, всегда перехватывайте только исключение, которое хотите перехватить, не используйте общие операторы catch, такие как catch (Exception e) это поймает все исключения. Это очень важно, потому что в противном случае вы можете случайно поймать неправильное исключение и отреагировать неправильно.

Почему я не должен использовать catch (Exception e) ?

Давайте воспользуемся небольшим примером, чтобы показать, почему не следует просто перехватывать все исключения:

Этот код пытается поймать ArithmeticException вызвано возможным делением на 0. Но он также улавливает возможное NullPointerException это брошено, если a или же b находятся null . Это означает, что вы можете получить NullPointerException но вы будете рассматривать это как ArithmeticException и, вероятно, сделаете неправильный поступок. В лучшем случае вы все равно пропустите исключение NullPointerException. Подобные вещи значительно усложняют отладку, так что не делайте этого.

TL; DR

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

Если 1. невозможно, перехватите конкретное исключение и обработайте его.

  • Никогда не добавляйте просто try / catch и игнорируйте исключение! Не делай этого!
  • Никогда не использовать catch (Exception e) , всегда перехватывайте определенные исключения. Это избавит вас от головной боли.
  • 1 хорошее объяснение того, почему нам следует избегать маскировки ошибок
  • 2 Я отправляю этот ответ, поэтому самый верхний ответ (при сортировке по активности) не является просто неправильным Я понятия не имею, о чем вы говорите, поскольку это, вероятно, уже изменилось. Но принятый ответ определенно интереснее;)
  • 1 Насколько я знаю, тот, который я имел в виду, к настоящему времени удален. По сути, он гласил: «просто попробуйте catch (Exception e) и игнорируйте все ошибки». Принятый ответ намного старше моего, поэтому я стремился высказать немного другое мнение по этому поводу. Я не думаю, что кому-то поможет просто скопировать чей-то ответ или осветить то, что другие люди уже хорошо осветили.
  • Сказать «Не ловить исключение» — это заблуждение — это только один вариант использования. Ваш пример великолепен, но как насчет того, где вы находитесь в верхней части цикла потока (внутренний запуск)? Вы должны ВСЕГДА перехватывать исключение (или, может быть, Throwable) там и регистрировать его, чтобы оно не исчезло незаметно (исключения, генерируемые при запуске, обычно не регистрируются правильно, если вы не настроили свой поток / регистратор для этого).
  • 1 Я не включил этот особый случай, поскольку он имеет значение только для многопоточности. В однопоточном режиме просочившееся исключение убивает программу и явно регистрируется в журнале. Если кто-то не знает, как правильно обрабатывать исключения, он обычно еще не знает, как использовать многопоточность.

Чтобы добавить к тому, что сказал Роб. Установка точек останова в приложении позволяет выполнять пошаговую обработку стека. Это позволяет разработчику использовать отладчик, чтобы увидеть, в какой именно момент метод делает что-то непредвиденное.

Поскольку Роб использовал NullPointerException (NPE), чтобы проиллюстрировать что-то общее, мы можем помочь устранить эту проблему следующим образом:

если у нас есть метод, который принимает такие параметры, как: void (String firstName)

В нашем коде мы хотели бы оценить это firstName содержит значение, мы бы сделали это так: if(firstName == null || firstName.equals(»)) return;

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

if(dog == null || dog.firstName == null) return;

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

  • Согласовано. Этот подход можно использовать, чтобы узнать, какая ссылка в заявлении null когда NullPointerException рассматривается, например.
  • 16 При работе со String, если вы хотите использовать метод equals, я думаю, что лучше использовать константу в левой части сравнения, например: Вместо: if (firstName == null || firstName.equals (» «)) возвращение; Я всегда использую: if ((«»). Equals (firstName)) Это предотвращает исключение Nullpointer

Есть еще одна функция stacktrace, предлагаемая семейством Throwable — возможность манипулировать информация трассировки стека.

Стандартное поведение:

Управляемая трассировка стека:

  • 2 Не знаю, как я к этому отношусь . учитывая характер потока, я бы посоветовал новым разработчикам не определять собственную трассировку стека.

Чтобы понять имя: Трассировка стека — это список исключений (или вы можете сказать список «Причина по»), от самого поверхностного исключения (например, исключения уровня обслуживания) до самого глубокого (например, исключения базы данных). Точно так же, как причина, по которой мы называем это «стеком», заключается в том, что стек первым пришел последним (FILO), самое глубокое исключение произошло в самом начале, затем была сгенерирована цепочка исключений, серия последствий, поверхностное исключение было последним. одно произошло вовремя, но мы видим это в первую очередь.

Ключ 1: Здесь необходимо понять сложную и важную вещь: самая глубокая причина может не быть «основной причиной», потому что, если вы напишете какой-то «плохой код», он может вызвать какое-то исключение внизу, которое глубже, чем его уровень. Например, неправильный sql-запрос может вызвать сброс соединения SQLServerException в нижней части вместо синтаксической ошибки, которая может быть только в середине стека.

-> Найдите основную причину, посередине — это ваша работа.

Ключ 2: Еще одна сложная, но важная вещь — внутри каждого блока «Причина по», первая строка была самым глубоким слоем и занимала первое место для этого блока. Например,

Book.java:16 был вызван Auther.java:25, который был вызван Bootstrap.java:14, Book.java:16 был основной причиной. Здесь прикрепите диаграмму, отсортируйте стек трассировки в хронологическом порядке.

Чтобы добавить к другим примерам, есть внутренние (вложенные) классы которые появляются с $ подписать. Например:

Результатом будет эта трассировка стека:

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

Если вы получили трассировку стека и хотите отследить причину исключения, хорошей отправной точкой для понимания этого будет использование Консоль Java Stack Trace в Затмение. Если вы используете другую IDE, может быть аналогичная функция, но этот ответ касается Eclipse.

Во-первых, убедитесь, что все ваши источники Java доступны в проекте Eclipse.

Тогда в Ява перспективы, нажмите на Приставка вкладка (обычно внизу). Если представление консоли не отображается, перейдите к пункту меню Окно -> Показать вид и выберите Приставка.

Затем в окне консоли нажмите следующую кнопку (справа)

а затем выберите Консоль Java Stack Trace из раскрывающегося списка.

Вставьте трассировку стека в консоль. Затем он предоставит список ссылок на ваш исходный код и любой другой доступный исходный код.

Вот что вы можете увидеть (изображение из документации Eclipse):

Самый последний сделанный вызов метода будет Топ стека, которая является верхней строкой (исключая текст сообщения). Спуск по стеку уходит в прошлое. Вторая строка — это метод, вызывающий первую строку и т. Д.

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

Что такое трассировка стека и как я могу использовать ее для отладки ошибок моего приложения?

Иногда, когда я запускаю свое приложение, я получаю ошибку, которая выглядит примерно так:

Люди называют это «трассировкой стека». Что такое трассировка стека? Что она может сказать мне об ошибке в моей программе?

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

7 ответов

Проще говоря, трассировка стека — это список вызовов методов, которые приложение выполняло при возникновении исключения.

Простой пример

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

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

Чтобы отладить это, мы можем открыть Book.java и посмотреть на строку 16 , которая:

Это будет означать, что что-то (вероятно, title ) есть null в приведенном выше коде.

Пример с цепочкой исключений

Иногда приложения перехватывают исключение и повторно генерируют его как причину другого исключения. Обычно это выглядит так:

Это может дать вам трассировку стека, которая выглядит так:

Что отличает этот, так это «Вызвано». Иногда исключения содержат несколько разделов «Причина». Для них обычно требуется найти «основную причину», которая будет одним из самых низких разделов «Причина» в трассировке стека. В нашем случае это:

Опять же, с этим исключением мы хотели бы взглянуть на строку 22 из Book.java , чтобы увидеть, что может вызвать здесь NullPointerException .

Более сложный пример с библиотечным кодом

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

В этом примере многое другое. Что нас больше всего беспокоит, так это поиск методов, взятых из нашего кода , то есть чего угодно в пакете com.example.myproject . Во втором примере (выше) мы сначала хотели бы найти основную причину, а именно:

Однако все вызовы методов под этим кодом являются библиотечным кодом. Итак, мы перейдем к пункту «Причина» над ним и найдем первый вызов метода, исходящий из нашего кода, а именно:

Как и в предыдущих примерах, мы должны посмотреть на MyEntityService.java в строке 59 , потому что именно здесь возникла эта ошибка (это немного очевидно, что пошло не так, поскольку SQLException сообщает об ошибке, но процедура отладки что мы ищем).

Что такое Stacktrace?

Трассировка стека — очень полезный инструмент отладки. Он показывает стек вызовов (то есть стек функций, которые были вызваны до этого момента) в момент возникновения неперехваченного исключения (или время, когда трассировка стека была сгенерирована вручную). Это очень полезно, потому что показывает не только, где произошла ошибка, но и то, как программа оказалась в этом месте кода. Это приводит к следующему вопросу:

Что такое исключение?

Исключение — это то, что среда выполнения использует, чтобы сообщить вам, что произошла ошибка. Популярные примеры — NullPointerException, IndexOutOfBoundsException или ArithmeticException. Каждая из них возникает, когда вы пытаетесь сделать что-то, что невозможно. Например, при попытке разыменовать объект Null будет выброшено исключение NullPointerException:

Что делать с трассировками стека / исключениями?

Сначала выясните, что вызывает исключение. Попробуйте поискать в Google имя исключения, чтобы выяснить, в чем причина этого исключения. В большинстве случаев это вызвано неправильным кодом. В приведенных выше примерах все исключения вызваны неправильным кодом. Итак, для примера NullPointerException вы можете убедиться, что a никогда не имеет значения NULL в это время. Вы можете, например, инициализировать a или включить проверку, подобную этой:

Таким образом, нарушающая строка не выполняется, если a==null . То же самое и с другими примерами.

Иногда вы не можете быть уверены, что не получите исключения. Например, если вы используете сетевое соединение в своей программе, вы не можете помешать компьютеру потерять подключение к Интернету (например, вы не можете запретить пользователю отключать сетевое подключение компьютера). В этом случае сетевая библиотека, вероятно, выдаст исключение. Теперь вы должны перехватить исключение и обработать его. Это означает, что в примере с сетевым подключением вы должны попытаться повторно открыть соединение или уведомить пользователя или что-то в этом роде. Кроме того, всякий раз, когда вы используете catch, всегда перехватывайте только то исключение, которое хотите перехватить, не используйте общие операторы перехвата, такие как catch (Exception e) , которые перехватывали бы все исключения. Это очень важно, потому что в противном случае вы можете случайно поймать неправильное исключение и отреагировать неправильно.

Почему мне не следует использовать catch (Exception e) ?

Давайте воспользуемся небольшим примером, чтобы показать, почему не следует просто перехватывать все исключения:

Этот код пытается поймать ArithmeticException , вызванное возможным делением на 0. Но он также улавливает возможное NullPointerException , которое выбрасывается, если a или b являются null . Это означает, что вы можете получить NullPointerException , но вы будете рассматривать его как ArithmeticException и, вероятно, сделаете неправильный шаг. В лучшем случае вы все равно пропустите исключение NullPointerException. Подобные вещи значительно усложняют отладку, так что не делайте этого.

TL; DR

  1. Выясните, в чем причина исключения, и исправьте ее, чтобы исключение вообще не генерировалось.
  2. Если 1. невозможно, перехватите конкретное исключение и обработайте его.
    • Никогда не добавляйте просто try / catch и игнорируйте исключение! Не делай этого!
    • Никогда не используйте catch (Exception e) , всегда перехватывайте определенные исключения. Это избавит вас от головной боли.

Чтобы добавить к тому, что сказал Роб. Установка точек останова в вашем приложении позволяет выполнять пошаговую обработку стека. Это позволяет разработчику использовать отладчик, чтобы увидеть, в какой именно момент метод делает что-то неожиданное.

Поскольку Роб использовал NullPointerException (NPE), чтобы проиллюстрировать что-то общее, мы можем помочь устранить эту проблему следующим образом:

Если у нас есть метод, который принимает такие параметры, как: void (String firstName)

В нашем коде мы хотели бы оценить, что firstName содержит значение, мы бы сделали это так: if(firstName == null || firstName.equals(«»)) return;

Вышесказанное не позволяет нам использовать firstName в качестве небезопасного параметра. Поэтому, выполняя нулевые проверки перед обработкой, мы можем помочь убедиться, что наш код будет работать правильно. Чтобы расширить пример, в котором используется объект с методами, мы можем посмотреть здесь:

if(dog == null || dog.firstName == null) return;

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

Чтобы понять название : трассировка стека — это список исключений (или вы можете сказать список «Причина по»), от самого поверхностного исключения (например, исключение уровня сервиса) до самого глубокого ( например, исключение базы данных). Точно так же, как причина, по которой мы называем это «стеком», заключается в том, что стек является первым зашел последним (FILO), самое глубокое исключение произошло в самом начале, затем была сгенерирована цепочка исключений, серия последствий, поверхностное исключение было последним. одно произошло вовремя, но мы видим это в первую очередь.

Ключ 1 . Здесь необходимо понять сложную и важную вещь: самая глубокая причина может не быть «основной причиной», потому что, если вы напишете какой-то «плохой код», это может вызвать какое-то исключение ниже который глубже его слоя. Например, неверный sql-запрос может вызвать сброс соединения SQLServerException в нижней части вместо синтаксической ошибки, которая может быть только в середине стека.

-> Найдите основную причину в вашей работе.

Ключ 2 . Еще одна сложная, но важная вещь — внутри каждого блока «Причина по», первая строка была самым глубоким слоем и занимала первое место в этом блоке. Например,

Book.java:16 был вызван Auther.java:25, который был вызван Bootstrap.java:14, Book.java:16 был основной причиной. Здесь прикрепите диаграмму, отсортируйте стек трассировки в хронологическом порядке.

Есть еще одна функция трассировки стека, предлагаемая семейством Throwable — возможность манипулировать информацией трассировки стека.

Стандартное поведение:

Обработка трассировки стека:

Чтобы добавить к другим примерам, есть внутренние (вложенные) классы , которые отмечены знаком $ . Например:

Результатом будет эта трассировка стека:

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

Если вы получили трассировку стека и хотите отследить причину исключения, хорошей отправной точкой для понимания этого является использование Java Stack Trace Console в Eclipse . Если вы используете другую IDE, может быть аналогичная функция, но этот ответ касается Eclipse.

Во-первых, убедитесь, что все ваши источники Java доступны в проекте Eclipse.

Затем в перспективе Java щелкните вкладку Консоль (обычно внизу). Если представление консоли не отображается, перейдите к пункту меню Окно -> Показать представление и выберите Консоль .

Затем в окне консоли нажмите следующую кнопку (справа)

А затем в раскрывающемся списке выберите Консоль трассировки стека Java .

Вставьте трассировку стека в консоль. Затем он предоставит список ссылок на ваш исходный код и любой другой доступный исходный код.

Вот что вы можете увидеть (изображение из документации Eclipse):

Diagram from Eclipse documentation

Самый последний сделанный вызов метода будет вершиной стека, то есть верхней строкой (за исключением текста сообщения). Спуск по стеку уходит в прошлое. Вторая строка — это метод, вызывающий первую строку и т. Д.

Если вы используете программное обеспечение с открытым исходным кодом, вам может потребоваться загрузить и прикрепить к своему проекту источники, если вы хотите изучить. Загрузите исходные jar-файлы в своем проекте, откройте папку Referenced Libraries , чтобы найти jar-файл для вашего модуля с открытым исходным кодом (тот, который содержит файлы классов), затем щелкните правой кнопкой мыши, выберите Properties и прикрепите исходный jar.

Вы достигли нового уровня

Учеба в вузе приучила нас к мысли, что принципиальных отличий между теорией и практикой нет. Нет, вы конечно, понимаете, что это не одно и то же. Но принципиальной разницы не видите. А она есть.

Большинство людей ставит знак равенства между «я знаю» и «я умею». А вы – нет?

А как насчет таких примеров?

1) Я знаю, что курить вредно, но я – курю.

2) Я знаю, что МакДональдс вреден, но ем фастфуд.

3) Я знаю правила дорожного движения, но ездить не умею.

4) Я знаю, что бег полезен, но не бегаю по утрам.

Очень часто люди принимают «я знаю» за «я умею». Хороший пример с правилами дорожного движения. Если человек знает правила и знает, как ездить, значит ли это, что он умеет? Нет. А если он думает, что — значит? Ну и зачем ему инструктор, когда он уже и так все знает и умет?

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

В обычном вузе вас кормят только знаниями, а умения/навыки вам придётся приобрести самим. Что я слышу? у вас в вузе была не только теория, но и практика?

Ок, если вы — студент физического факультета, сделайте мне рабочую модель парового двигателя с КПД хотя бы 20%. Спорю, что вы знаете как, но не умете, да?

Вы – химик? Сделайте черный бездымный порох. Тоже вроде бы знаете как, но не умеете?

Математик? Составьте уравнение полета артиллерийского снаряда. Не забудьте учесть форму снаряда. Материальные точки не летают в реальной жизни. Как не бывает и шарообразных коней.

Биолог? Выделите пенициллин. Это плесень, встречается на дынях, чтоб вы знали. Знаете как – отлично! Сможете?

Экономист? Постройте прогноз роста цен на нефть. Построили? А теперь превратите $2,000 в $200,000 за год на основании вашего прогноза. Вы хоть раз на FOREX играли? За реальные деньги? Или тоже только знаете, что это такое?

Международная экономика? Отлично! Где мне открыть офшор? В Гонконге, Ирландии или США. Почему? Даже если вы знаете, что вряд ли, вы не сможете этого сделать, т.к. никогда раньше не делали. И даже понятия не имеете, как это делается.

Вас этому в вузе не учили? Чего это я задаю задания, которые вы не проходили? А потому что это задания из реальной жизни. Это и есть практика, а все что вы учили в вузе: шарообразные кони, совершенная конкуренция – этого ничего в реальной жизни не существует.

Что же это я маркетологов забыл? Куда мне лучше потратить $500, чтобы о моем курсе узнало как можно больше людей? На рекламу? Вы вообще в курсе, что уже устарела не только классическая реклама, но и концепция Уникального Торгового Предложения, которая, как я уверен, преподносилась вам в вузе чуть ли не как панацея от всех бед?

Забудьте о том, что вы что-то знаете. Спросите себя – что вы умеете? Полезного? За что готовы платить деньги? Которых вам будет хватать?

Так что, друзья, давайте скажем спасибо такому замечательному курсу, как JavaRush. Благодаря которому, вы будете не только знать, как программировать, но и уметь программировать. А также сможете устроиться на работу, и, через пару лет, получать достойную зарплату. Которой, надеюсь, вам будет хватать для безбедной жизни.

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

Чем раньше вы это поймете, тем лучше.

Хочешь стать программистом — купи подписку на полный курс. Более тысячи задач, 600+ минилекций, сайт, форум, плагин к IDEA, подсказки, видеоуроки, мотивирующие видео,… Это все будет твоим. JavaRush — реально крутой способ стать Java-программистом.

— Привет! Сегодня я расскажу тебе, что такое стек-трейс. Но сначала расскажу, что такое стек.

— Представь себе стопку бумаг — деловых поручений для некоторого исполнителя. Сверху на стопку можно класть новое задание, и с верха стопки задание можно брать. При таком подходе задания будут исполняться не по порядку поступления. Задание, положенное самым последним, будет взято исполнителем самым первым. Такая структура элементов коллекции называется стеком – стопкой.

В Java для этого есть специальная коллекция – Stack. Это коллекция, у которой есть методы «добавить элемент» и «взять(достать/забрать) элемент». Как ты уже догадался, первым будет взят элемент, добавленный самым последним.

— Хм. Вроде не сложно и понятно.

— Отлично. Тогда сейчас объясню, что такое стек-трейс.

— Представь себе, что в Java функция А вызвала функцию Б, а та вызвала функцию В, а та, в свою очередь, функцию Г. Так вот, чтобы выйти из функции Б, нужно сначала выйти из функции В, а для этого выйти из функции Г. Это очень похоже на стек.

— А чем похоже?

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

— Ну, некоторая аналогия есть, но не уверен, что я все понял правильно.

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

— С вызовом функций то же самое. Функция А вызывает функцию Б, а та вызывает функцию В. И чтобы выйти из А, надо сначала выйди из Б, а для этого надо выйти из В.

— Подожди. Если я все правильно понял, то весь этот стек сведется к «взять можно только самый последний положенный лист», «выйти можно только из последней функции, в которую зашли». Так?

— Да. Так вот – последовательность вызовов функций — это и есть «стек вызовов функций», он же просто «стек вызовов». Функция, вызванная последней, должна завершиться самой первой. Давай посмотрим это на примере:

Получение и вывод текущего стека вызовов:
public class ExceptionExample
{
  public static void main(String[] args)
  {
    method1();
  }

  public static void method1()
  {
    method2();
  }

  public static void method2()
  {
    method3();
  }

  public static void method3()
  {
    StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
    for (StackTraceElement element : stackTraceElements)
    {
       System.out.println(element.getMethodName());
    }
  }
}
Вот какой результат мы получим:
getStackTrace
method3
method2
method1
main

— Ок. С вызовом функций похоже все понятно. А что это еще за StackTraceElement?

— Java-машина ведет запись всех вызовов функций. У нее есть для этого специальная коллекция – стек. Когда одна функция вызывает другую, Java-машина помещает в этот стек новый элемент StackTraceElement. Когда функция завершается этот элемент удаляется из стека. Таким образом, в этом стеке всегда хранится актуальная информация о текущем состоянии «стека вызовов функций».

— Каждый StackTraceElement содержит информацию о методе, который был вызван. В частности можно получить имя этого метода с помощью функции getMethodName.

— В примере выше ты можешь видеть демонстрацию этого дела:

1) Получаем «стек вызовов»:

2) Проходимся по нему с помощью цикла for-each. Надеюсь, ты его еще не забыл.

3) Печатаем в System.out имена методов.

— Интересная штука и, похоже, совсем не сложная. Спасибо, Риша!

— Привет, Амиго!

— Привет, Диего!

— Вот тебе одна маленькая задачка на вывод стек-трейса.

Задачи
1. Каждый метод должен возвращать свой StackTrace

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

2. И снова StackTrace

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

3. Метод должен вернуть номер строки кода, из которого вызвали этот метод

Написать пять методов, которые вызывают друг друга. Метод должен вернуть номер строки кода, из которого вызвали этот метод. Воспользуйся функцией: element.getLineNumber().

4. Стек-трейс длиной 10 вызовов

Напиши код, чтобы получить стек-трейс длиной 10 вызовов.

5. Метод должен возвращать результат – глубину её стек-трейса

Написать метод, который возвращает результат – глубину его стек трейса – количество методов в нем (количество элементов в списке). Это же число метод должен выводить на экран.

— Привет, Амиго! Сегодня будет очень интересный урок. Сегодня я расскажу тебе об исключениях. Исключения – это специальный механизм для контроля над ошибками в программе. Вот примеры ошибок, которые могут возникнуть в программе:

1 Программа пытается записать файл на заполненный диск.

2 Программа пытается вызвать метод у переменной, которая хранит ссылку – null.

3 Программа пытается разделить число на 0.

Все эти действия приводят к возникновению ошибки. Обычно это приводит к закрытию программы — продолжать выполнять дальше код не имеет смысла.

— Почему?

— А есть ли смысл крутить руль, если машина слетела с трассы и падает с обрыва?

— Программа что, должна завершиться?

— Да. Раньше так и было. Любая ошибка приводила к завершению программы.

— Это очень разумный подход.

— А разве не лучше было бы попробовать работать дальше?

— Ага. Ты набрал большущий текст в Word’е, сохранил его, он не сохранился, но программа говорит тебе, что все в порядке. И ты продолжаешь набирать его дальше. Глупо, да?

— Ага.

— Потом разработчики придумали интересный ход: каждая функция возвращала статус своей работы. 0 означал, что она отработала как надо, любое другое значение – что произошла ошибка: это самое значение и было кодом ошибки.

— Но был у такого подхода и минус. После каждого(!) вызова функции нужно было проверять код (число), который она вернула. Во-первых, это было неудобно: код по обработке ошибок исполнялся редко, но писать его нужно было всегда. Во-вторых, функции часто сами возвращают различные значения – что делать с ними?

— Ага. Я тоже об этом подумал.

— Но потом наступило светлое будущее — появились исключения и механизм обработки ошибок. Вот как это работает:

1 Когда возникает ошибка, Java-машина создаёт специальный объект – exception – исключение, в который записывается вся информация об ошибке. Для разных ошибок есть разные исключения.

2 Затем это «исключение» приводит к тому, что программа тут же выходит из текущей функции, затем выходит из следующей функции, и так пока не выйдет из метода main. Затем программа завершается. Еще говорят, что Java-машина «раскручивает назад стек вызовов».

— Но ты же сказал, что теперь программа не обязательно завершается.

— Верно, потому что есть способ перехватить исключение. В нужном месте, для нужных нам исключений мы можем написать специальный код, который будет перехватывать эти исключения и что-то делать. Важное.

— Для этого есть специальная конструкция try-catch. Вот как это работает:

Вот пример программы, которая перехватывает исключение – деление на 0. И продолжает работать.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ExceptionExample2
{
    public static void main(String[] args)
    {
        System.out.println("Program starts");

        try
        {
            System.out.println("Before method1 calling");
            method1();
            System.out.println("After method1 calling. Never will be shown");
        }
        catch (Exception e)
        {
           System.out.println("Exception has been caught");
        }

        System.out.println("Program is still running");
    }

    public static void method1()
    {
        int a = 100;
        int b = 0;
        System.out.println(a / b);
    }
}
Вот что будет выведено на экран:
5
9
15
18
«Program starts»
«Before method1 calling»
«Exception has been caught»
«Program is still running»

— А почему не будет выведено «After method1 calling. Never will be shown»?

— Рада, что ты спросил. В строчке 25 у нас было деление на ноль. Это привело к возникновению ошибки – исключения. Java-машина создала объект ArithmeticException с информацией об ошибке. Этот объект является исключением.

— Внутри метода method1 возникло исключение. И это привело к немедленному завершению этого метода. Оно привело бы и к завершению метода main, если бы не было блока try-catch.

— Если внутри блока try возникает исключение то, оно захватывается в блоке catch. Остаток кода в блоке try, не будет исполнен, а сразу начнётся исполнение блока catch.

— Как-то не очень понятно.

— Другими словами этот код работает так:

1 Если внутри блока try возникло исключение, то код перестаёт исполняться, и начинает исполняться блок catch.

2 Если исключение не возникло, то блок try исполняется до конца, а catch никогда так и не начнёт исполняться.

— Гм?

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

— Теперь вроде понятно.

— Вот и отлично.

— А что значит Exception внутри catch?

Все исключения – это классы, унаследованные от класса Exception. Мы можем перехватить любое из них, указав в блоке catch его класс, или все сразу, указав общий родительский класс — Exception. Затем из переменной e (эта переменная хранит ссылку на объект исключения), можно получить всю необходимую информацию о возникшей ошибке.

— Круто! А если в моем методе могут возникнуть разные исключения, можно обрабатывать их по-разному?

— Не можно, а нужно. Сделать это можно вот так:

Пример:
public class ExceptionExample2
{
    public static void main(String[] args)
    {
        System.out.println("Program starts");

        try
        {
            System.out.println("Before method1 calling");
            method1();
            System.out.println("After method1 calling. Never will be shown ");
        }
        catch (NullPointerException e)
        {
           System.out.println("Reference is null. Exception has been caught");
        }
        catch (ArithmeticException e)
        {
            System.out.println("Division by zero. Exception has been caught");
        }
        catch (Exception e)
        {
            System.out.println("Any other errors. Exception has been caught");
        }

        System.out.println("Program is still running");
    }

    public static void method1()
    {
        int a = 100;
        int b = 0;
        System.out.println(a / b);
    }
}

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

— Гм. Ну, вроде понятно. Сам такого не напишу, конечно, но если в коде встречу – пугаться не буду.

— Решила поднять сегодня ещё одну тему. В Java все исключения делятся на два типа – контролируемые/проверяемые (checked) и неконтролируемые/непроверяемые (unchecked): те, которые перехватывать обязательно, и те, которые перехватывать не обязательно. По умолчанию – все исключения обязательно нужно перехватывать.

— А можно в коде специально выбрасывать исключения?

— В своем коде ты сам можешь выкидывать исключения. Ты даже можешь написать свои собственные исключения. Но это мы разберем позже. Сейчас же давай научимся работать с исключениями, которые выбрасывает Java-машина.

— Ок.

— Если в методе выбрасываются (возникают) исключения ClassNotFoundException и FileNotFoundException, программист обязан указать их в сигнатуре метода (в заголовке метода). Это checked исключения. Вот как это обычно выглядит:

Примеры проверяемых (checked) исключений
public static void method1() throws ClassNotFoundException, FileNotFoundException
public static void main() throws IOException
public static void main() //не выбрасывает никаких исключений

— Т.е. мы просто пишем throws и перечисляем исключения через запятую. Так?

— Да. Но тут интересно другое. Чтобы программа скомпилировалась, метод, который вызывает method1 в примере ниже, должен сделать две вещи: или перехватить эти исключения или пробросить их дальше (тому, кто его вызвал) указав их в своём заголовке.

— Еще раз. Если ты в методе main хочешь вызвать метод какого-то объекта, в заголовке которого прописано throws FileNotFoundException, … то тебе надо сделать одно из двух:

1) Перехватывать исключения FileNotFoundException, …

Тебе придется обернуть код вызова опасного метода в блок try-catch

2) Не перехватывать исключения FileNotFoundException, …

Тебе придется добавить эти исключения в список throws своего метода main.

— А можно пример?

— Вот, смотри:

Примеры проверяемых (checked) исключений
public static void main(String[] args)
{
    method1();
}

public static void method1() throws  FileNotFoundException, ClassNotFoundException
{
    //тут кинется исключение  FileNotFoundException, такого файла нет
    FileInputStream fis = new FileInputStream("C2:\badFileName.txt");
}

— Этот пример не скомпилируется, т.к. метод main вызывает метод method1(), который выкидывает исключения, обязательные к перехвату.

— Чтобы пример скомпилировался, надо добавить обработку исключений в метод main. Сделать это можно двумя способами:

Способ 1: просто пробрасываем исключение выше (вызывающему):
public static void main(String[] args)  throws FileNotFoundException, ClassNotFoundException 
{
    method1();
}

public static void method1() throws FileNotFoundException, ClassNotFoundException
{
    //тут кинется исключение  FileNotFoundException, такого файла нет
    FileInputStream fis = new FileInputStream("C2:\badFileName.txt");
}

— А тут перехватываем его с помощью try-catch:

Способ 2: перехватываем исключение:
public static void main(String[] args)
{
    try
    {
        method1();
    }
    catch(Exception e)
    {
    }
}

public static void method1() throws FileNotFoundException, ClassNotFoundException
{
    //тут кинется исключение  FileNotFoundException, такого файла нет
    FileInputStream fis = new FileInputStream("C2:\badFileName.txt");
}

— Что-то понемногу проясняется.

— Посмотри на пример ниже, чтобы разобраться:

Не обрабатываем исключения – нужно пробросить их дальше, тому, кто знает как
public static void method2() throws FileNotFoundException, ClassNotFoundException
{
    method1();
}
Обрабатываем одно исключение, второе – пробрасываем:
public static void method3() throws ClassNotFoundException
{
    try
    {
        method1();
    }
    catch (FileNotFoundException e)
    {
        System.out.println("FileNotFoundException has been caught.");
    }
}
Перехватываем оба – ничего не пробрасываем:
public static void method4()
{
    try
    {
        method1();
    }
    catch (FileNotFoundException e)
    {
        System.out.println("FileNotFoundException has been caught.");
    }
    catch (ClassNotFoundException e)
    {
        System.out.println("ClassNotFoundException has been caught.");
    }
}

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

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

Код с использованием исключений Примерная расшифровка
class ExceptionExampleOriginal
{


    public static void main(String[] args)
    {
        System.out.println("main begin");
        try
        {
            System.out.println("main before call");

            method1();



            System.out.println("main after call");
        }
        catch (RuntimeException e)
        {


            String s = e.getMessage();
            System.out.println(s);
        }
        System.out.println("main end");
    }

    public static void method1()
    {
        System.out.println("method1 begin");
        method2();

        System.out.println("method1 end");
    }

    public static void method2()
    {
      System.out.println("method2");
      String s = "Message: Unknown Exception";
      throw new RuntimeException(s);

    }
}
public class ExceptionExample
{
    private static Exception exception = null;

    public static void main(String[] args)
    {
        System.out.println("main begin");

       
        System.out.println("main before call");

        method1();

        if (exception == null)
        {
            System.out.println("main after call");
        }
        else if (exception instanceof RuntimeException)
        {
            RuntimeException e = (RuntimeException) exception;
            exception = null;
            String s = e.getMessage();
            System.out.println(s);
        }
        System.out.println("main end");
    }

    public static void method1()
    {
        System.out.println("method1 begin");
        method2();
        if (exception != null) return;
        System.out.println("method1 end");
    }

    public static void method2()
    {
        System.out.println("method2");
        String s = "Message: Unknown Exception";
        exception = new RuntimeException(s);
        return;
    }
}

— Ничего не понятно.

— Ок. Давай я тебе объясню, что происходит.

— В примере слева мы по цепочке вызываем несколько методов. В method2 мы специально создаем и выкидываем исключение (инициируем ошибку).

— В примере справа показано, что примерно при этом происходит.

— Посмотри на method2. Создание исключение превратилось вот во что: создали объект типа RuntimeException, сохранили его в специальную переменную exception и тут же вышли из метода – return.

— В методе method1, после вызова method2 стоит проверка – есть исключение или нет, если исключение есть, тогда метод method1 тут же завершается. Такая проверка неявно производится после вызова каждого(!) метода в Java.

— Ого!

— Вот тебе и ого.

— В колонке справа в методе main я написал, что примерно происходит при перехвате исключения с помощью конструкции try-catch. Если исключения не было, то все продолжает работать, как и запланировано. Если исключение было, и оно было такого типа, как указано в catch, тогда мы его обрабатываем.

— А что значит throw и instanceof?

– Посмотри на последнюю строку throw new RuntimeException(s). Таким способом мы создаем и кидаем исключение. Пока мы так делать не будем. Это только для примера.

— А с помощью команды «а instanceof B» мы проверяем, имеет ли объект a тип B. Т.е. имеет ли объект, который хранится в переменной exception, тип RuntimeException. Это логическое условие.

— Ну, вроде понятно. Немного.

— Привет, Амиго! Вот тебе интересные задачи на захват исключений.

Задачи
1. Исключение при работе с числами

Перехватить исключение (и вывести его на экран), указав его тип, возникающее при выполнении кода:
int a = 42 / 0;

2. Исключение при работе со строками

Перехватить исключение (и вывести его на экран), указав его тип, возникающее при выполнении кода:
String s = null;
String m = s.toLowerCase();

3. Исключение при работе с массивами

Перехватить исключение (и вывести его на экран), указав его тип, возникающее при выполнении кода:
int[] m = new int[2];
m[8] = 5;

4. Исключение при работе с коллекциями List

Перехватить исключение (и вывести его на экран), указав его тип, возникающее при выполнении кода:
ArrayList<String> list = new ArrayList<String>();
String s = list.get(18);

5. Исключение при работе с коллекциями Map

Перехватить исключение (и вывести его на экран), указав его тип, возникающее при выполнении кода:
HashMap<String, String> map = new HashMap<String, String>(null);
map.put(null, null);
map.remove(null);

Подсказка: напиши программу, посмотри, какое исключение возникает, а потом поменяй код и перехвати его.

— И ещё немного интересных уроков. Мне так нравится преподавать.

— Хочу рассказать тебе, как работает множественный catch. Все очень просто: при возникновении исключения в блоке try, выполнение программы передаётся на первый catch.

Если тип, указанный внутри круглых скобок бока catch, совпадает с типом объекта-исключения, то начинается выполнение кода внутри блока {}. Иначе переходим к следующему catch. Там проверка повторяется.

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

— Ясно. Будет выполнен тот catch, тип в котором совпадает с типом исключения.

— Да, верно. Но в реальности все немного сложнее. Дело в том, что классы можно наследовать друг от друга. И если класс «Корова» унаследовать от класса «Животное», то объект типа «Корова» можно хранить не только в переменной типа «Корова», но и в переменной типа «Животное».

— И?

— Т.к. все исключения унаследованы от классов Exception или RuntimeException (который тоже унаследован от Exception), то их все можно перехватить командами catch (Exception e) или catch (RuntimeException e).

— И?

— Отсюда два вывода. Во-первых, с помощью команды catch(Exception e) можно перехватить любое исключение вообще. Во-вторых — порядок блоков catch имеет значение.

Примеры:

— Возникший при делении на 0 ArithmeticException будет перехвачен во втором catch.

Код
try
{
    System.out.println("Before method1 calling.");
    int a = 1 / 0;
    System.out.println("After method1 calling. Never will be shown.");
}
catch (NullPointerException e)
{
    System.out.println("Reference is null. Exception has been caught.");
}
catch (ArithmeticException e)
{
    System.out.println("Division by zero. Exception has been caught.");
}
catch (Exception e)
{
    System.out.println("Any other errors. Exception has been caught.");
}

— В примере ниже возникший ArithmeticException будет перехвачен в первом catch, т.к. классы всех исключений унаследованы от Exception. Т.е. Exception захватывает любое исключение.

Код
try
{
    System.out.println("Before method1 calling.");
    int a = 1/0;
    System.out.println("After method1 calling. Never will be shown.");
}
catch (Exception e)
{
    System.out.println("Any other errors. Exception has been caught.");
}
catch (NullPointerException e)
{
    System.out.println("Reference is null. Exception has been caught.");
}
catch (ArithmeticException e)
{
    System.out.println("Divided by zero. Exception has been caught.");
}

— В примере ниже исключение ArithmeticException не будет перехвачено, а будет выброшено дальше в вызывающий метод.

Код
try
{
    System.out.println("Before method1 calling.");
    int a = 1/0;
    System.out.println("After method1 calling. Never will be shown.");
}
catch (NullPointerException e)
{
    System.out.println("Reference is null. Exception has been caught.");
}

— Ну, вроде все понемногу проясняется. Непростая штука эти исключения.

— Это только кажется так. На самом деле – это чуть ли не самая простая вещь в Java.

— Не знаю, радоваться по этому поводу или огорчаться…

— Привет, Амиго! Я вчера напился и намудрил с твоими задачами, но ты же на меня не в обиде и все решишь? Это для твоего же блага. Держи:

Задачи
1. Исключения

Есть метод, который выбрасывает два исключения, унаследованные от Exception, и два унаследованных от RuntimeException: NullPointerException, ArithmeticException, FileNotFoundException, URISyntaxException.

Нужно перехватить NullPointerException и FileNotFoundException, но не перехватывать ArithmeticException и URISyntaxException. Как это сделать?

2. Перехватывание исключений

Есть три исключения последовательно унаследованные от Exception:
class Exception1 extends Exception

class Exception2 extends Exception1

class Exception3 extends Exception2
Есть метод, который описан так:
public static void method1() throws Exception1, Exception2, Exception3


Напиши catch, который перехватит все три Exception1, Exception2 и Exception3

3. Перехват выборочных исключений

1. Разберись, какие исключения бросает метод BEAN.methodThrowExceptions.
2. Метод processExceptions должен вызывать метод BEAN.methodThrowExceptions и обрабатывать исключения:
2.1. если возникло исключение FileSystemException, то логировать его (вызвать метод BEAN.log) и пробросить дальше
2.2. если возникло исключение CharConversionException или любое другое IOException, то только логировать его — вызвать метод BEAN.log
3. Добавь в сигнатуру метода processExceptions класс исключения, которое ты пробрасываешь в п.2.1.
4. В методе main обработай оставшееся исключение — логируй его. Используй try..catch

Подсказка:
Если ты захватил исключение MyException, которое не хотел захватывать, его можно пробросить дальше кодом вида:
catch (MyException e) {
   throw e;
}

4. Перехват checked исключений

В методе processExceptions обработайте все checked исключения.
Нужно вывести на экран каждое возникшее checked исключение.
Можно использовать только один блок try.

5. Перехват unchecked исключений

В методе processExceptions обработайте все unchecked исключения.
Нужно вывести стек-трейс каждого возникшего исключения используя метод printStack.
Можно использовать только один блок try.

— Сегодня я расскажу супер интересную тему – исключения. В своё время эта тема будоражила умы молодых учёных и программистов…

— Прости, мне нужно в лабораторию. Вот тебе конспект лекций. Думаю, ты сам разберёшься. Держи вот: Лекция по исключениям

— Амиго, как тебе сегодняшнее занятие? Твои позитронные мозги ещё не плавятся? Диего кого угодно доконает своими задачами. Предлагаю взять по пиву и расслабиться. Ты ещё стоишь?

Оригинал видео на YouTube

— Здорово, боец!

— Здравия желаю, капитан Бобров!

— У меня для тебя шикарная новость. Вот тебе задания для закрепления полученных навыков. Выполняй их каждый день, и твои навыки будут расти с неимоверной скоростью. Они специально разработаны для выполнения их в Intellij IDEA.

Дополнительные задания для выполнения в Intellij Idea
1. Деление на ноль

Создай метод public static void divisionByZero, в котором подели любое число на ноль и выведи на экран результат деления.
Оберни вызов метода divisionByZero в try..catch. Выведи стек-трейс исключения используя метод exception.printStackTrace()

2. Обратный отсчёт от 10 до 0

Написать в цикле обратный отсчёт от 10 до 0. Для задержки иcпользовать Thread.sleep(100);
Обернуть вызов sleep в try..catch.

3. Метод в try..catch

Вводить с клавиатуры числа. Код по чтению чисел с клавиатуры вынести в отдельный метод readData.
Обернуть все тело (весь код внутри readData, кроме объявления списка, где будут храниться числа и BufferedReader — а) этого метода в try..catch.
Если пользователь ввёл какой-то текст, вместо ввода числа, то метод должен перехватить исключение и вывести на экран все введенные числа в качестве результата.
Числа выводить с новой строки сохраняя порядок ввода.

4. Конвертер дат

Ввести с клавиатуры дату в формате «08/18/2013»
Вывести на экран эту дату в виде «AUG 18, 2013».
Воспользоваться объектом Date и SimpleDateFormat.

5. Гласные и согласные буквы

Написать программу, которая вводит с клавиатуры строку текста.
Программа должна вывести на экран две строки:
1) первая строка содержит только гласные буквы
2) вторая — только согласные буквы и знаки препинания из введённой строки.
Символы соединять пробелом, каждая строка должна заканчиваться пробелом.

Пример ввода:
Мама мыла раму.

Пример вывода:
а а ы а а у
М м м л р м .

6. Сказка «Красная Шапочка»

1. Есть пять классов: красная шапочка, бабушка, пирожок, дровосек, волк.
2. У каждого класса есть 2 поля: убил (killed ArrayList) и съел (ate ArrayList).
3. Необходимые объекты созданы (hood, grandmother, …).
4. Расставь правильно связи, кто кого съел и убил, чтобы получилась логика сказки «Красная Шапочка».

7. Расставь модификаторы static

Расставь модификаторы static так, чтобы пример скомпилировался

8. Список из массивов чисел

Создать список, элементами которого будут массивы чисел. Добавить в список пять объектов–массивов длиной 5, 2, 4, 7, 0 соответственно. Заполнить массивы любыми данными и вывести их на экран.

9. Десять котов

Создать класс кот – Cat, с полем «имя» (String).
Создать словарь Map(<String, Cat>) и добавить туда 10 котов в виде «Имя»-«Кот».
Получить из Map множество(Set) всех имен и вывести его на экран.

— Те задания были для духов. Для дедушек я добавил бонусные задания повышенной сложности. Только для старослужащих.

1. Нужно исправить программу, чтобы компилировалась и работала. Задача: Программа вводит два имени файла. И копирует первый файл на место, заданное вторым именем.
2. Нужно добавить в программу новую функциональность. Задача: Программа вводит два имени файла. И копирует первый файл на место заданное вторым именем.
Новая задача: Программа вводит два имени файла. И копирует первый файл на место, заданное вторым именем.

Если файла (который нужно копировать) с указанным именем не существует, то программа должна вывести надпись «Файл не существует.» и еще раз прочитать имя файла с консоли, а уже потом считывать файл для записи.
3. Задача по алгоритмам. Задача: Пользователь вводит с клавиатуры список слов (и чисел). Слова вывести в возрастающем порядке, числа — в убывающем.

Пример ввода:
Вишня
1
Боб
3
Яблоко
2
0
Арбуз

Пример вывода:
Арбуз
3
Боб
2
Вишня
1
0
Яблоко

From Wikipedia, the free encyclopedia

«Backtrace» redirects here. For the 2018 film, see Backtrace (film).

In computing, a stack trace (also called stack backtrace[1] or stack traceback[2]) is a report of the active stack frames at a certain point in time during the execution of a program. When a program is run, memory is often dynamically allocated in two places; the stack and the heap. Memory is continuously allocated on a stack but not on a heap, thus reflective of their names. Stack also refers to a programming construct, thus to differentiate it, this stack is referred to as the program’s function call stack. Technically, once a block of memory has been allocated on the stack, it cannot be easily removed as there can be other blocks of memory that were allocated before it. Each time a function is called in a program, a block of memory called an activation record is allocated on top of the call stack. Generally, the activation record stores the function’s arguments and local variables. What exactly it contains and how it’s laid out is determined by the calling convention.

Programmers commonly use stack tracing during interactive and post-mortem debugging. End-users may see a stack trace displayed as part of an error message, which the user can then report to a programmer.

A stack trace allows tracking the sequence of nested functions called — up to the point where the stack trace is generated. In a post-mortem scenario this extends up to the function where the failure occurred (but was not necessarily caused). Sibling calls do not appear in a stack trace.

Language support[edit]

Many programming languages, including Java[3] and C#,[4] have built-in support for retrieving the current stack trace via system calls. Before std::stacktrace was added in standard library as a container for std::stacktrace_entry, pre-C++23 has no built-in support for doing this, but C++ users can retrieve stack traces with (for example) the stacktrace library. In JavaScript, exceptions hold a stack property that contain the stack from the place where it was thrown.

Python[edit]

As an example, the following Python program contains an error.

def a():
    i = 0
    j = b(i)
    return j

def b(z):
    k = 5
    if z == 0:
        c()
    return k + z

def c():
    error()

a()

Running the program under the standard Python interpreter produces the following error message.

Traceback (most recent call last):
  File "tb.py", line 15, in <module>
    a()
  File "tb.py", line 3, in a
    j = b(i)
  File "tb.py", line 9, in b
    c()
  File "tb.py", line 13, in c
    error()
NameError: name 'error' is not defined

The stack trace shows where the error occurs, namely in the c function. It also shows that the c function was called by b, which was called by a, which was in turn called by the code on line 15 (the last line) of the program. The activation records for each of these three functions would be arranged in a stack such that the a function would occupy the bottom of the stack and the c function would occupy the top of the stack.

Java[edit]

In Java, stack traces can be dumped manually with Thread.dumpStack()[5] Take the following input:

public class Main {
  public static void main(String args[]) {
    demo();
  }
  static void demo() {
    demo1();
  }
  static void demo1() {
    demo2();
  }
  static void demo2() {
    demo3();
  }
  static void demo3() {
    Thread.dumpStack();
  }
}

The exception lists functions in descending order, so the most-inner call is first.

java.lang.Exception: Stack trace
        at java.lang.Thread.dumpStack(Thread.java:1336)
        at Main.demo3(Main.java:15)
        at Main.demo2(Main.java:12)
        at Main.demo1(Main.java:9)
        at Main.demo(Main.java:6)
        at Main.main(Main.java:3)

C and C++[edit]

Both C and C++ (pre-C++23) do not have native support for obtaining stack traces, but libraries such as glibc and boost provide this functionality.[6][7] In these languages, some compiler optimizations may interfere with the call stack information that can be recovered at runtime. For instance, inlining can cause missing stack frames, tail call optimizations can replace one stack frame with another, and frame pointer elimination can prevent call stack analysis tools from correctly interpreting the contents of the call stack.[6]

For example, glibc’s backtrace() function returns an output with the program function and memory address.

./a.out() [0x40067f]
./a.out() [0x4006fe]
./a.out() [0x40070a]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f7e60738f45]
./a.out() [0x400599]

As of C++23, stack traces can be dumped manually by printing the value returned by static member function std::stacktrace::current():[8]

std::cout << std::stacktrace::current() << 'n';

Rust[edit]

Rust has two types of errors. Functions that use the panic macro are «unrecoverable» and the current thread will become poisoned experiencing stack unwinding. Functions that return a std::result::Result are «recoverable» and can be handled gracefully.[9] However, recoverable errors cannot generate a stack trace as they are manually added and not a result of a runtime error.

As of June 2021, Rust has experimental support for stack traces on unrecoverable errors. Rust supports printing to stderr when a thread panics, but it must be enabled by setting the RUST_BACKTRACE environment variable.[10]

When enabled, such backtraces look similar to below, with the most recent call first.

thread 'main' panicked at 'execute_to_panic', main.rs:3
stack backtrace:
   0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace
   1: std::panicking::default_hook::{{closure}}
   2: std::panicking::default_hook
   3: std::panicking::rust_panic_with_hook
   4: std::panicking::begin_panic
   5: futures::task_impl::with
   6: futures::task_impl::park
...

See also[edit]

  • Tail call
  • Context (computing)
  • Stack overflow
  • Exception handling
  • Call stack

References[edit]

  1. ^ «libc manual: backtraces». gnu.org. Retrieved 8 July 2014.
  2. ^ «traceback — Print or retrieve a stack traceback». python.org. Retrieved 8 July 2014.
  3. ^ «Thread (Java SE 16 & JDK 16)». Java Platform Standard Edition & Java Development Kit Version 16 API Specification. 2021-03-04. Retrieved 2021-07-04.
  4. ^ «Environment.StackTrace Property (System)». Microsoft Docs. 2021-05-07. Retrieved 2021-07-04.
  5. ^ «Thread (Java Platform SE 8 )». docs.oracle.com. Retrieved 2021-06-15.
  6. ^ a b «Backtraces (The GNU C Library)». www.gnu.org. Retrieved 2021-06-15.
  7. ^ «Getting Started — 1.76.0». www.boost.org. Retrieved 2021-06-15.
  8. ^ «Working Draft, Standard for Programming Language C++» (PDF). open-std.org. ISO/IEC. 2021-10-23. p. 766.{{cite web}}: CS1 maint: url-status (link)
  9. ^ «rustonomicon unwinding — Rust». doc.rust-lang.org.
  10. ^ «std::backtrace — Rust». doc.rust-lang.org. Retrieved 2021-06-15.

Когда что-то идет не так в запущенном приложении Java, часто первым признаком того, что вы увидите, являются строки, напечатанные на экране, которые выглядят следующим образом:

Exception in thread "main" java.lang.RuntimeException: Something has gone wrong, aborting!
  at com.myproject.module.MyProject.badMethod(MyProject.java:22)
  at com.myproject.module.MyProject.oneMoreMethod(MyProject.java:18)
  at com.myproject.module.MyProject.anotherMethod(MyProject.java:14)
  at com.myproject.module.MyProject.someMethod(MyProject.java:10)
  at com.myproject.module.MyProject.main(MyProject.java:6)

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

Анатомия трассировки стека

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

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

Давайте проанализируем эту трассировку стека. Первая строка сообщает нам подробности исключения:

Это хорошее начало. Строка 2 показывает, какой код был запущен, когда это произошло:

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

Что пошло не так?

То, что вызывает исключение, обычно является явным оператором throw . Используя имя файла и номер строки, вы можете точно проверить, какой код вызвал исключение. Вероятно, это будет выглядеть примерно так:

  throw new RuntimeException("Something has gone wrong, aborting!");

Это отличное место для начала поиска основной проблемы: существуют ли какие-либо утверждения “если” вокруг этого? Что делает этот код? Откуда берутся данные, используемые в этом методе?

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

  • Исключение NullPointerException если obj является нулевым в коде, который вызывает obj.someMethod()
  • Арифметическое исключение, если деление на ноль происходит в целочисленной арифметике, т. е. 1/0 – любопытно, что нет исключения, если это вычисление с плавающей запятой, хотя, 1.0/0.0 возвращает бесконечность просто отлично!
  • Исключение NullPointerException, если нуль Целое число распаковывается в int в коде, подобном этому: Целое число; a++;
  • В Спецификации языка Java есть несколько других примеров, поэтому важно знать, что исключения могут возникать без явного вызова.

Работа с исключениями, создаваемыми библиотеками

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

Например, если мы используем класс Fraction из Apache Commons Lang и передаем ему некоторые входные данные следующим образом:

  Fraction.getFraction(numberOfFoos, numberOfBars);

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

Exception in thread "main" java.lang.ArithmeticException: The denominator must not be zero
  at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)   
  at com.project.module.MyProject.anotherMethod(MyProject.java:17)
  at com.project.module.MyProject.someMethod(MyProject.java:13)
  at com.project.module.MyProject.main(MyProject.java:9)

Многие хорошие библиотеки предоставляют Javadoc который содержит информацию о том, какие исключения могут возникать и почему. В этом случае Fraction.getfraction задокументировал выдаст исключение ArithmeticException, если дробь имеет нулевой знаменатель . Здесь это также ясно из сообщения, но в более сложных или неоднозначных ситуациях документы могут оказать большую помощь.

Чтобы прочитать эту трассировку стека, начните сверху с типа исключения – ArithmeticException и сообщения Знаменатель не должен быть равен нулю . Это дает представление о том, что пошло не так, но чтобы выяснить, какой код вызвал исключение, пропустите трассировку стека в поисках чего-то в пакете com.мой проект (это на 3-й строке здесь), затем отсканируйте до конца строки, чтобы увидеть, где находится код ( MyProject.java:17 ). Эта строка будет содержать некоторый код, который вызывает Fraction.getFraction . Это отправная точка для исследования: что передается в getFraction ? Откуда это взялось?

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

Наилучшая практика: Улавливание и Выбрасывание исключений

Допустим, мы работаем над большим проектом, который имеет дело с вымышленными Foobar , и наш код будет использоваться другими. Мы могли бы решить перехватить исключение ArithmeticException из Дроби и переопределить его как нечто специфичное для проекта, которое выглядит следующим образом:

  try {
    ....
    Fraction.getFraction(x,y);
    ....
  } catch ( ArithmeticException e ){ 
    throw new MyProjectFooBarException("The number of FooBars cannot be zero", e);
  }

Поймать исключение ArithmeticException и выбросить его имеет несколько преимуществ:

  • Наши пользователи защищены от необходимости заботиться об исключении ArithmeticException , что дает нам гибкость в изменении способа использования commons-lang.
  • Можно добавить больше контекста, например, указав, что проблема в количестве фообаров .
  • Это также может облегчить чтение трассировок стека, как мы увидим ниже.

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

Обратите внимание, что конструктор для Исключения Foo Bar моего проекта принимает 2 аргумента: сообщение и Исключение, которое его вызвало. Каждое исключение в Java имеет поле причина , и при выполнении перехвата и повторной обработки , как это, вы должны всегда устанавливать это, чтобы помочь людям отлаживать ошибки. Трассировка стека теперь может выглядеть примерно так:

Exception in thread "main" com.myproject.module.MyProjectFooBarException: The number of FooBars cannot be zero
  at com.myproject.module.MyProject.anotherMethod(MyProject.java:19)
  at com.myproject.module.MyProject.someMethod(MyProject.java:12)
  at com.myproject.module.MyProject.main(MyProject.java:8)
Caused by: java.lang.ArithmeticException: The denominator must not be zero
  at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)
  at com.myproject.module.MyProject.anotherMethod(MyProject.java:17)
  ... 2 more

Последнее созданное исключение находится в первой строке, а место, где оно было создано, все еще находится в строке 2. Однако этот тип трассировки стека может вызвать путаницу, поскольку catch-and-retrow изменил порядок вызовов методов по сравнению с трассировками стека, которые мы видели ранее. Основной метод больше не находится внизу, а код, который первым выдал исключение, больше не находится вверху. Когда у вас есть несколько этапов перехвата и повторного броска, он становится больше, но схема одна и та же:

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

Библиотеки против фреймворков

Разница между библиотекой и фреймворком в Java заключается в следующем:

  • Ваш код вызывает методы в библиотеке
  • Ваш код вызывается методами в фреймворке

Распространенным типом фреймворка является сервер веб-приложений, такой как Spark Java или Spring Boot . Используя Spark Java и Commons-Lang с нашим кодом, мы могли бы увидеть трассировку стека, подобную этой:

com.framework.FrameworkException: Error in web request
    at com.framework.ApplicationStarter.lambda$start$0(ApplicationStarter.java:15)
    at spark.RouteImpl$1.handle(RouteImpl.java:72)
    at spark.http.matching.Routes.execute(Routes.java:61)
    at spark.http.matching.MatcherFilter.doFilter(MatcherFilter.java:134)
    at spark.embeddedserver.jetty.JettyHandler.doHandle(JettyHandler.java:50)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1568)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
    at org.eclipse.jetty.server.Server.handle(Server.java:503)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:364)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:260)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:305)
    at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
    at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: com.project.module.MyProjectFooBarException: The number of FooBars cannot be zero
    at com.project.module.MyProject.anotherMethod(MyProject.java:20)
    at com.project.module.MyProject.someMethod(MyProject.java:12)
    at com.framework.ApplicationStarter.lambda$start$0(ApplicationStarter.java:13)
    ... 16 more
Caused by: java.lang.ArithmeticException: The denominator must not be zero
    at org.apache.commons.lang3.math.Fraction.getFraction(Fraction.java:143)
    at com.project.module.MyProject.anotherMethod(MyProject.java:18)
    ... 18 more

Хорошо, это уже довольно долго. Как и прежде, мы должны сначала заподозрить наш собственный код, но становится все труднее и труднее найти где это . Вверху находится исключение фреймворка, внизу – Библиотека, а прямо посередине – наш собственный код. Фу!

Сложный фреймворк и библиотека могут создать дюжину или более Вызвано разделами: , поэтому хорошей стратегией будет перейти к тем, кто ищет ваш собственный код: Вызвано: com.myproject... Затем прочтите этот раздел подробно, чтобы определить проблему.

Вкратце

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

Если у вас есть какие-либо советы или рекомендации по работе с Java Stacktraces, я бы хотел услышать о них, так что свяжитесь со мной и давайте поделимся тем, что мы знаем.

mgilliard@twilio.com @@Максимальная загрузка

Оригинал: “https://dev.to/twilio/how-to-read-and-understand-a-java-stacktrace-3796”

Что такое stack trace, и как с его помощью находить ошибки при разработке приложений?

Иногда при запуске своего приложения я получаю подобную ошибку:

Мне сказали, что это называется «трассировкой стека» или «stack trace». Что такое трассировка? Какую полезную информацию об ошибке в разрабатываемой программе она содержит?

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

user avatar

Простыми словами, трассировка стека – это список методов, которые были вызваны до момента, когда в приложении произошло исключение.

Простой случай

В указанном примере мы можем точно определить, когда именно произошло исключение. Рассмотрим трассировку стека:

Это пример очень простой трассировки. Если пойти по списку строк вида «at…» с самого начала, мы можем понять, где произошла ошибка. Мы смотрим на верхний вызов функции. В нашем случае, это:

Для отладки этого фрагмента открываем Book.java и смотрим, что находится на строке 16 :

Это означает то, что в приведенном фрагменте кода какая-то переменная (вероятно, title ) имеет значение null .

Пример цепочки исключений

Иногда приложения перехватывают исключение и выбрасывают его в виде другого исключения. Обычно это выглядит так:

Трассировка в этом случае может иметь следующий вид:

В этом случае разница состоит в атрибуте «Caused by» («Чем вызвано»). Иногда исключения могут иметь несколько секций «Caused by». Обычно необходимо найти исходную причину, которой оказывается в самой последней (нижней) секции «Caused by» трассировки. В нашем случае, это:

Аналогично, при подобном исключении необходимо обратиться к строке 22 книги Book.java , чтобы узнать, что вызвало данное исключение – NullPointerException .

Еще один пугающий пример с библиотечным кодом

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

В этом примере приведен далеко не полный стек вызовов. Что вызывает здесь наибольший интерес, так это поиск функций из нашего кода – из пакета com.example.myproject . В предыдущем примере мы сначала хотели отыскать «первопричину», а именно:

Однако все вызовы методов в данном случае относятся к библиотечному коду. Поэтому мы перейдем к предыдущей секции «Caused by» и найдем первый вызов метода из нашего кода, а именно:

Что такое трассировка стека и как ее использовать для отладки ошибок приложения?

Иногда, когда я запускаю свое приложение, я получаю ошибку, которая выглядит примерно так:

Люди называют это «трассировкой стека». Что такое трассировка стека? Что он может сказать мне об ошибке в моей программе?

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

  • 28 Кроме того, если строка трассировки стека не содержит имени файла и номера строки, класс для этой строки не был скомпилирован с отладочной информацией.

Проще говоря, трассировки стека — это список вызовов методов, в процессе которых приложение находилось в момент создания исключения.

Простой пример

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

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

Чтобы отладить это, мы можем открыть Book.java и посмотрите на строку 16 , который:

Это означало бы, что что-то (возможно, title ) является null в приведенном выше коде.

Пример с цепочкой исключений

Иногда приложения перехватывают исключение и повторно генерируют его как причину другого исключения. Обычно это выглядит так:

Это может дать вам трассировку стека, которая выглядит так:

Что отличает этот, так это «Причина». Иногда исключения содержат несколько разделов «Причина». Для них обычно требуется найти «основную причину», которая будет одним из самых низких разделов «Причина» в трассировке стека. В нашем случае это:

Опять же, за этим исключением мы хотели бы посмотреть на строку 22 из Book.java чтобы увидеть, что может вызвать NullPointerException Вот.

Более устрашающий пример с библиотечным кодом

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

В этом примере многое другое. Что нас больше всего беспокоит, так это поиск методов из наш код, что было бы чем угодно в com.example.myproject пакет. Во втором примере (выше) мы сначала хотели бы найти основную причину, а именно:

Однако все вызовы методов под этим кодом являются библиотечным кодом. Итак, мы перейдем к пункту «Причина» над ним и найдем первый вызов метода, исходящий из нашего кода, а именно:

Как и в предыдущих примерах, мы должны посмотреть на MyEntityService.java онлайн 59 , потому что именно здесь возникла эта ошибка (это немного очевидно, что пошло не так, поскольку SQLException сообщает об ошибке, но процедура отладки — это то, что нам нужно).

  • 4 @RobHruska — Очень хорошо объяснено. +1. Знаете ли вы какие-либо парсеры, которые принимают трассировку исключения в виде строки и предоставляют полезные методы для анализа трассировки стека? — например, getLastCausedBy () или getCausedByForMyAppCode («com.example.myproject»)
  • 1 @AndyDufresne — я не встречал ни одного, но, опять же, я тоже особо не смотрел.
  • 1 Предлагаемое улучшение: объясните первую строку трассировки стека, которая начинается с Exception in thread ‘main’ в вашем первом примере. Я думаю, было бы особенно полезно объяснить, что эта строка часто сопровождается сообщением, например значением переменной, которое может помочь диагностировать проблему. Я сам попытался внести правку, но мне не удается уместить эти идеи в существующую структуру вашего ответа.
  • 5 Также в java 1.7 добавлено «Подавлено:», в котором перечислены трассировки стека подавленных исключений перед отображением «Вызвано:» для этого исключения. Он автоматически используется конструкцией try-with-resource: docs.oracle.com/javase/specs/jls/se8/html/… и содержит исключения, если таковые возникли при закрытии ресурса (ов).
  • Существует JEP openjdk.java.net/jeps/8220715, цель которого — еще больше улучшить понятность, особенно NPE, путем предоставления таких деталей, как «Невозможно записать поле ‘nullInstanceField’, потому что ‘this.nullInstanceField’ имеет значение null».

Я отправляю этот ответ, поэтому самый верхний ответ (при сортировке по активности) не является просто неправильным.

Что такое Stacktrace?

Трассировка стека — очень полезный инструмент отладки. Он показывает стек вызовов (то есть стек функций, которые были вызваны до этого момента) в момент возникновения неперехваченного исключения (или время, когда трассировка стека была сгенерирована вручную). Это очень полезно, потому что это не только показывает вам, где произошла ошибка, но и то, как программа оказалась в этом месте кода. Это приводит к следующему вопросу:

Что такое исключение?

Исключение — это то, что среда выполнения использует, чтобы сообщить вам, что произошла ошибка. Популярные примеры: NullPointerException, IndexOutOfBoundsException или ArithmeticException. Каждая из них возникает, когда вы пытаетесь сделать что-то, что невозможно. Например, NullPointerException будет выброшено, когда вы попытаетесь разыменовать Null-объект:

Что мне делать с трассировками стека / исключениями?

Сначала выясните, что вызывает исключение. Попробуйте поискать в Google название исключения, чтобы выяснить, в чем причина этого исключения. В большинстве случаев это вызвано неправильным кодом. В приведенных выше примерах все исключения вызваны неправильным кодом. Итак, для примера NullPointerException вы можете убедиться, что a в то время никогда не бывает нулевым. Вы можете, например, инициализировать a или включите проверку, подобную этой:

Таким образом, нарушающая строка не выполняется, если a==null . То же самое и с другими примерами.

Иногда вы не можете быть уверены, что не получите исключения. Например, если вы используете сетевое соединение в своей программе, вы не можете помешать компьютеру потерять подключение к Интернету (например, вы не можете запретить пользователю отключать сетевое подключение компьютера). В этом случае сетевая библиотека, вероятно, выдаст исключение. Теперь вы должны поймать исключение и справиться Это. Это означает, что в примере с сетевым подключением вы должны попытаться повторно открыть соединение или уведомить пользователя или что-то в этом роде. Кроме того, всякий раз, когда вы используете catch, всегда перехватывайте только исключение, которое хотите перехватить, не используйте общие операторы catch, такие как catch (Exception e) это поймает все исключения. Это очень важно, потому что в противном случае вы можете случайно поймать неправильное исключение и отреагировать неправильно.

Почему я не должен использовать catch (Exception e) ?

Давайте воспользуемся небольшим примером, чтобы показать, почему не следует просто перехватывать все исключения:

Этот код пытается поймать ArithmeticException вызвано возможным делением на 0. Но он также улавливает возможное NullPointerException это брошено, если a или же b находятся null . Это означает, что вы можете получить NullPointerException но вы будете рассматривать это как ArithmeticException и, вероятно, сделаете неправильный поступок. В лучшем случае вы все равно пропустите исключение NullPointerException. Подобные вещи значительно усложняют отладку, так что не делайте этого.

TL; DR

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

Если 1. невозможно, перехватите конкретное исключение и обработайте его.

  • Никогда не добавляйте просто try / catch и игнорируйте исключение! Не делай этого!
  • Никогда не использовать catch (Exception e) , всегда перехватывайте определенные исключения. Это избавит вас от головной боли.
  • 1 хорошее объяснение того, почему нам следует избегать маскировки ошибок
  • 2 Я отправляю этот ответ, поэтому самый верхний ответ (при сортировке по активности) не является просто неправильным Я понятия не имею, о чем вы говорите, поскольку это, вероятно, уже изменилось. Но принятый ответ определенно интереснее;)
  • 1 Насколько я знаю, тот, который я имел в виду, к настоящему времени удален. По сути, он гласил: «просто попробуйте catch (Exception e) и игнорируйте все ошибки». Принятый ответ намного старше моего, поэтому я стремился высказать немного другое мнение по этому поводу. Я не думаю, что кому-то поможет просто скопировать чей-то ответ или осветить то, что другие люди уже хорошо осветили.
  • Сказать «Не ловить исключение» — это заблуждение — это только один вариант использования. Ваш пример великолепен, но как насчет того, где вы находитесь в верхней части цикла потока (внутренний запуск)? Вы должны ВСЕГДА перехватывать исключение (или, может быть, Throwable) там и регистрировать его, чтобы оно не исчезло незаметно (исключения, генерируемые при запуске, обычно не регистрируются правильно, если вы не настроили свой поток / регистратор для этого).
  • 1 Я не включил этот особый случай, поскольку он имеет значение только для многопоточности. В однопоточном режиме просочившееся исключение убивает программу и явно регистрируется в журнале. Если кто-то не знает, как правильно обрабатывать исключения, он обычно еще не знает, как использовать многопоточность.

Чтобы добавить к тому, что сказал Роб. Установка точек останова в приложении позволяет выполнять пошаговую обработку стека. Это позволяет разработчику использовать отладчик, чтобы увидеть, в какой именно момент метод делает что-то непредвиденное.

Поскольку Роб использовал NullPointerException (NPE), чтобы проиллюстрировать что-то общее, мы можем помочь устранить эту проблему следующим образом:

если у нас есть метод, который принимает такие параметры, как: void (String firstName)

В нашем коде мы хотели бы оценить это firstName содержит значение, мы бы сделали это так: if(firstName == null || firstName.equals(»)) return;

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

if(dog == null || dog.firstName == null) return;

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

  • Согласовано. Этот подход можно использовать, чтобы узнать, какая ссылка в заявлении null когда NullPointerException рассматривается, например.
  • 16 При работе со String, если вы хотите использовать метод equals, я думаю, что лучше использовать константу в левой части сравнения, например: Вместо: if (firstName == null || firstName.equals (» «)) возвращение; Я всегда использую: if ((«»). Equals (firstName)) Это предотвращает исключение Nullpointer

Есть еще одна функция stacktrace, предлагаемая семейством Throwable — возможность манипулировать информация трассировки стека.

Стандартное поведение:

Управляемая трассировка стека:

  • 2 Не знаю, как я к этому отношусь . учитывая характер потока, я бы посоветовал новым разработчикам не определять собственную трассировку стека.

Чтобы понять имя: Трассировка стека — это список исключений (или вы можете сказать список «Причина по»), от самого поверхностного исключения (например, исключения уровня обслуживания) до самого глубокого (например, исключения базы данных). Точно так же, как причина, по которой мы называем это «стеком», заключается в том, что стек первым пришел последним (FILO), самое глубокое исключение произошло в самом начале, затем была сгенерирована цепочка исключений, серия последствий, поверхностное исключение было последним. одно произошло вовремя, но мы видим это в первую очередь.

Ключ 1: Здесь необходимо понять сложную и важную вещь: самая глубокая причина может не быть «основной причиной», потому что, если вы напишете какой-то «плохой код», он может вызвать какое-то исключение внизу, которое глубже, чем его уровень. Например, неправильный sql-запрос может вызвать сброс соединения SQLServerException в нижней части вместо синтаксической ошибки, которая может быть только в середине стека.

-> Найдите основную причину, посередине — это ваша работа.

Ключ 2: Еще одна сложная, но важная вещь — внутри каждого блока «Причина по», первая строка была самым глубоким слоем и занимала первое место для этого блока. Например,

Book.java:16 был вызван Auther.java:25, который был вызван Bootstrap.java:14, Book.java:16 был основной причиной. Здесь прикрепите диаграмму, отсортируйте стек трассировки в хронологическом порядке.

Чтобы добавить к другим примерам, есть внутренние (вложенные) классы которые появляются с $ подписать. Например:

Результатом будет эта трассировка стека:

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

Если вы получили трассировку стека и хотите отследить причину исключения, хорошей отправной точкой для понимания этого будет использование Консоль Java Stack Trace в Затмение. Если вы используете другую IDE, может быть аналогичная функция, но этот ответ касается Eclipse.

Во-первых, убедитесь, что все ваши источники Java доступны в проекте Eclipse.

Тогда в Ява перспективы, нажмите на Приставка вкладка (обычно внизу). Если представление консоли не отображается, перейдите к пункту меню Окно -> Показать вид и выберите Приставка.

Затем в окне консоли нажмите следующую кнопку (справа)

а затем выберите Консоль Java Stack Trace из раскрывающегося списка.

Вставьте трассировку стека в консоль. Затем он предоставит список ссылок на ваш исходный код и любой другой доступный исходный код.

Вот что вы можете увидеть (изображение из документации Eclipse):

Самый последний сделанный вызов метода будет Топ стека, которая является верхней строкой (исключая текст сообщения). Спуск по стеку уходит в прошлое. Вторая строка — это метод, вызывающий первую строку и т. Д.

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

Что такое трассировка стека и как я могу использовать ее для отладки ошибок моего приложения?

Иногда, когда я запускаю свое приложение, я получаю ошибку, которая выглядит примерно так:

Люди называют это «трассировкой стека». Что такое трассировка стека? Что она может сказать мне об ошибке в моей программе?

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

7 ответов

Проще говоря, трассировка стека — это список вызовов методов, которые приложение выполняло при возникновении исключения.

Простой пример

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

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

Чтобы отладить это, мы можем открыть Book.java и посмотреть на строку 16 , которая:

Это будет означать, что что-то (вероятно, title ) есть null в приведенном выше коде.

Пример с цепочкой исключений

Иногда приложения перехватывают исключение и повторно генерируют его как причину другого исключения. Обычно это выглядит так:

Это может дать вам трассировку стека, которая выглядит так:

Что отличает этот, так это «Вызвано». Иногда исключения содержат несколько разделов «Причина». Для них обычно требуется найти «основную причину», которая будет одним из самых низких разделов «Причина» в трассировке стека. В нашем случае это:

Опять же, с этим исключением мы хотели бы взглянуть на строку 22 из Book.java , чтобы увидеть, что может вызвать здесь NullPointerException .

Более сложный пример с библиотечным кодом

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

В этом примере многое другое. Что нас больше всего беспокоит, так это поиск методов, взятых из нашего кода , то есть чего угодно в пакете com.example.myproject . Во втором примере (выше) мы сначала хотели бы найти основную причину, а именно:

Однако все вызовы методов под этим кодом являются библиотечным кодом. Итак, мы перейдем к пункту «Причина» над ним и найдем первый вызов метода, исходящий из нашего кода, а именно:

Как и в предыдущих примерах, мы должны посмотреть на MyEntityService.java в строке 59 , потому что именно здесь возникла эта ошибка (это немного очевидно, что пошло не так, поскольку SQLException сообщает об ошибке, но процедура отладки что мы ищем).

Что такое Stacktrace?

Трассировка стека — очень полезный инструмент отладки. Он показывает стек вызовов (то есть стек функций, которые были вызваны до этого момента) в момент возникновения неперехваченного исключения (или время, когда трассировка стека была сгенерирована вручную). Это очень полезно, потому что показывает не только, где произошла ошибка, но и то, как программа оказалась в этом месте кода. Это приводит к следующему вопросу:

Что такое исключение?

Исключение — это то, что среда выполнения использует, чтобы сообщить вам, что произошла ошибка. Популярные примеры — NullPointerException, IndexOutOfBoundsException или ArithmeticException. Каждая из них возникает, когда вы пытаетесь сделать что-то, что невозможно. Например, при попытке разыменовать объект Null будет выброшено исключение NullPointerException:

Что делать с трассировками стека / исключениями?

Сначала выясните, что вызывает исключение. Попробуйте поискать в Google имя исключения, чтобы выяснить, в чем причина этого исключения. В большинстве случаев это вызвано неправильным кодом. В приведенных выше примерах все исключения вызваны неправильным кодом. Итак, для примера NullPointerException вы можете убедиться, что a никогда не имеет значения NULL в это время. Вы можете, например, инициализировать a или включить проверку, подобную этой:

Таким образом, нарушающая строка не выполняется, если a==null . То же самое и с другими примерами.

Иногда вы не можете быть уверены, что не получите исключения. Например, если вы используете сетевое соединение в своей программе, вы не можете помешать компьютеру потерять подключение к Интернету (например, вы не можете запретить пользователю отключать сетевое подключение компьютера). В этом случае сетевая библиотека, вероятно, выдаст исключение. Теперь вы должны перехватить исключение и обработать его. Это означает, что в примере с сетевым подключением вы должны попытаться повторно открыть соединение или уведомить пользователя или что-то в этом роде. Кроме того, всякий раз, когда вы используете catch, всегда перехватывайте только то исключение, которое хотите перехватить, не используйте общие операторы перехвата, такие как catch (Exception e) , которые перехватывали бы все исключения. Это очень важно, потому что в противном случае вы можете случайно поймать неправильное исключение и отреагировать неправильно.

Почему мне не следует использовать catch (Exception e) ?

Давайте воспользуемся небольшим примером, чтобы показать, почему не следует просто перехватывать все исключения:

Этот код пытается поймать ArithmeticException , вызванное возможным делением на 0. Но он также улавливает возможное NullPointerException , которое выбрасывается, если a или b являются null . Это означает, что вы можете получить NullPointerException , но вы будете рассматривать его как ArithmeticException и, вероятно, сделаете неправильный шаг. В лучшем случае вы все равно пропустите исключение NullPointerException. Подобные вещи значительно усложняют отладку, так что не делайте этого.

TL; DR

  1. Выясните, в чем причина исключения, и исправьте ее, чтобы исключение вообще не генерировалось.
  2. Если 1. невозможно, перехватите конкретное исключение и обработайте его.
    • Никогда не добавляйте просто try / catch и игнорируйте исключение! Не делай этого!
    • Никогда не используйте catch (Exception e) , всегда перехватывайте определенные исключения. Это избавит вас от головной боли.

Чтобы добавить к тому, что сказал Роб. Установка точек останова в вашем приложении позволяет выполнять пошаговую обработку стека. Это позволяет разработчику использовать отладчик, чтобы увидеть, в какой именно момент метод делает что-то неожиданное.

Поскольку Роб использовал NullPointerException (NPE), чтобы проиллюстрировать что-то общее, мы можем помочь устранить эту проблему следующим образом:

Если у нас есть метод, который принимает такие параметры, как: void (String firstName)

В нашем коде мы хотели бы оценить, что firstName содержит значение, мы бы сделали это так: if(firstName == null || firstName.equals(«»)) return;

Вышесказанное не позволяет нам использовать firstName в качестве небезопасного параметра. Поэтому, выполняя нулевые проверки перед обработкой, мы можем помочь убедиться, что наш код будет работать правильно. Чтобы расширить пример, в котором используется объект с методами, мы можем посмотреть здесь:

if(dog == null || dog.firstName == null) return;

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

Чтобы понять название : трассировка стека — это список исключений (или вы можете сказать список «Причина по»), от самого поверхностного исключения (например, исключение уровня сервиса) до самого глубокого ( например, исключение базы данных). Точно так же, как причина, по которой мы называем это «стеком», заключается в том, что стек является первым зашел последним (FILO), самое глубокое исключение произошло в самом начале, затем была сгенерирована цепочка исключений, серия последствий, поверхностное исключение было последним. одно произошло вовремя, но мы видим это в первую очередь.

Ключ 1 . Здесь необходимо понять сложную и важную вещь: самая глубокая причина может не быть «основной причиной», потому что, если вы напишете какой-то «плохой код», это может вызвать какое-то исключение ниже который глубже его слоя. Например, неверный sql-запрос может вызвать сброс соединения SQLServerException в нижней части вместо синтаксической ошибки, которая может быть только в середине стека.

-> Найдите основную причину в вашей работе.

Ключ 2 . Еще одна сложная, но важная вещь — внутри каждого блока «Причина по», первая строка была самым глубоким слоем и занимала первое место в этом блоке. Например,

Book.java:16 был вызван Auther.java:25, который был вызван Bootstrap.java:14, Book.java:16 был основной причиной. Здесь прикрепите диаграмму, отсортируйте стек трассировки в хронологическом порядке.

Есть еще одна функция трассировки стека, предлагаемая семейством Throwable — возможность манипулировать информацией трассировки стека.

Стандартное поведение:

Обработка трассировки стека:

Чтобы добавить к другим примерам, есть внутренние (вложенные) классы , которые отмечены знаком $ . Например:

Результатом будет эта трассировка стека:

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

Если вы получили трассировку стека и хотите отследить причину исключения, хорошей отправной точкой для понимания этого является использование Java Stack Trace Console в Eclipse . Если вы используете другую IDE, может быть аналогичная функция, но этот ответ касается Eclipse.

Во-первых, убедитесь, что все ваши источники Java доступны в проекте Eclipse.

Затем в перспективе Java щелкните вкладку Консоль (обычно внизу). Если представление консоли не отображается, перейдите к пункту меню Окно -> Показать представление и выберите Консоль .

Затем в окне консоли нажмите следующую кнопку (справа)

А затем в раскрывающемся списке выберите Консоль трассировки стека Java .

Вставьте трассировку стека в консоль. Затем он предоставит список ссылок на ваш исходный код и любой другой доступный исходный код.

Вот что вы можете увидеть (изображение из документации Eclipse):

Diagram from Eclipse documentation

Самый последний сделанный вызов метода будет вершиной стека, то есть верхней строкой (за исключением текста сообщения). Спуск по стеку уходит в прошлое. Вторая строка — это метод, вызывающий первую строку и т. Д.

Если вы используете программное обеспечение с открытым исходным кодом, вам может потребоваться загрузить и прикрепить к своему проекту источники, если вы хотите изучить. Загрузите исходные jar-файлы в своем проекте, откройте папку Referenced Libraries , чтобы найти jar-файл для вашего модуля с открытым исходным кодом (тот, который содержит файлы классов), затем щелкните правой кнопкой мыши, выберите Properties и прикрепите исходный jar.

Понравилась статья? Поделить с друзьями:
  • Что такое точность и ошибка наблюдения
  • Чтобы определить причину ошибки посмотрите файлы dxerror log
  • Чтобы не допустить ошибку надо знать правила
  • Чтобы набраться опыта нужно совершать ошибки
  • Что является программной ошибкой