JavaRush /Java блог /Random /Stack Trace і з чим його їдять
Alukard
37 рівень
London

Stack Trace і з чим його їдять

Стаття з групи Random
У цій статті ви дізнаєтеся і зрозумієте, як працює таке явище в Java, як StackTrace, також відоме як "Трасування стека викликів". Ця інформація була структурована для новачків, які зіткнулися з цим поняттям на початку дев'ятого рівня Java Syntax. Я думаю всі з вас, хоч раз, але зустрічали схожі помилки під час роботи у вашому IDE, незалежно від того, чи то Idea, Eclipse, чи щось інше.
Exception in thread "main" java.lang.ArithmeticException
	at com.example.task01.Test.division(Test.java:10)
	at com.example.task01.Test.main(Test.java:6)
Це, як ви вже здогадалися, і є наше трасування. Але не поспішайте панікувати, зараз ми з вами розкладемо цей приклад на пальцях. Спочатку необхідно зрозуміти той факт, що StackTrace працює як Стек і це видно з його назви. На цьому місці ми зупинимося трохи детальніше. Принцип роботи колекції StackНа восьмому рівні ви вже познайомилися з колекціями і знаєте, що вони поділяються на три групи Set – множина, List – список, Map – словник (або карта). На думку JavaRush (c). Наш Stack є частиною групи List. Принцип його роботи можна описати як LIFO, що розшифровується як Last In First Out(Останній прийшов, перший пішов). А саме такий список схожий на стіс книжок, тому щоб узяти елемент, який ми поклали в Stack першим, нам необхідно спочатку витягти всі елементи, які ми додали в наш список після. Як це зазначено на малюнку вище, на відміну, наприклад, від звичайного списку ArrayList, де ми можемо отримати будь-який елемент зі списку за індексом. Ще раз для закріплення. Отримання елемента зі Стека можливе тільки з кінця! Тоді як перший доданий у нього елемент перебуває на початку (або на дні, як зручніше). Ось які методи має наш Stack: Object push() – додає елемент на верх стека. Object pop() – повертає елемент, що знаходиться у верхній частині стека, видаляючи його в процесі. Object peek() – повертає елемент, що знаходиться у верхній частині стека, але не видаляє його. int search() – шукає елемент у стеку. Якщо знайдено, повертається його зміщення від вершини стека. Інакше повертається -1. boolean empty() – перевіряє, чи є стек порожнім. Повертає true, якщо стек порожній. Повертає false, якщо стек містить елементи. Так для чого ж у Java потрібен StackTrace побудований на принципах роботи Stack? Давайте розберемо приклад помилки нижче, яка виникла в процесі виконання такої ось простої програми.
public class Test {

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

    public static int convertStringToInt(String s) {
        int x = Integer.parseInt(s);
        return x;
    }
}
У нас є клас Test із двома методами. Усім звичний main і convertStringToInt, логіка якого полягає в конвертуванні та поверненні отриманого ззовні (а саме з методу main) рядка в цілочисельне число типу int. Як ви бачите, ми навмисно передали замість рядка з якоюсь цифрою параметр null. Цей параметр наш метод не зміг правильно обробити, що призвело до помилки NumberFormatException. Як ви знаєте, програма починає відпрацьовувати свою роботу з методу main, і в цей момент вона створює новий Стек із назвою StackTrace, куди кладе поточне значення її роботи під номером 1, далі ми переходимо до методу convertStringToInt, і програма знову заводить параметри нашого знаходження у створений раніше StackTrace під номером 2, далі викликається невидимий нашому оку метод parseInt, що міститься в класі Integer, і це вже буде елемент під номером 3 нашого StackTrace, у цьому методі буде ще один внутрішній виклик, доданий до StackTrace під номером 4, для перевірки елемента на null, що й призведе до виникнення помилки. Програмі необхідно вивести нашу помилку із зазначенням всього ланцюжка наших переходів до моменту виникнення помилки. Тут їй і приходить на допомогу раніше створений StackTrace з даними наших переходів.
Exception in thread "main" java.lang.NumberFormatException: null
	at java.base/java.lang.Integer.parseInt(Integer.java:614)
	at java.base/java.lang.Integer.parseInt(Integer.java:770)
	at com.example.task01.Test.convertStringToInt(Solution.java:10)
	at com.example.task01.Test.main(Solution.java:6)
До виникнення помилки, програма йшла вглиб методів, але щойно виникла помилка, все починає відбуватися у зворотному порядку. Друкується рядок з описом проблеми (№1 в прикладі), далі береться останнє (і те, що перебуває на вершині) додане значення до нашого Стеку, воно було під номером чотири, і друкується в консоль (№2 в прикладі), і ми бачимо, що проблема виникла в класі Integer на 614 рядку коду і викликала цей рядок, рядок 770 методу parseInt того самого класу (№3 в прикладі), який під час додавання в Стек був під номером три, і цей метод класу Integer, який ми досі не бачимо, був викликаний вже нашим методом convertStringToInt, який розміщено на 10 рядку нашої програми (№4 в прикладі, а під час додавання він був другим), а його, зі свого боку, викликав main на 6 рядку (№5 в прикладі, а під час додавання, відповідно, перший). Ось так, складаючи в Стек крок за кроком наші викликані методи, ми змогли повернутися назад у main, паралельно друкуючи інформацію про те, що саме призвело до виникнення помилки. Але StackTrace це не тільки робота з помилками, він дає змогу отримати нам купу цікавої інформації про процес роботи нашого застосунку. Давайте розберемо ще один популярний приклад у коментарях до основної лекції 9го рівня. У нас є код і до нього відразу додам малюнок, що візуалізує процес роботи програми:
public class Test {
    public static void main(String[] args) {
        method1();
        method2();
    }
    public static void method1() {
        //не викликає нічого
    }
    public static void method2() {
        method3();
        method4();
    }
    public static void method3() {
        //не викликає нічого
    }
    public static void method4() {
        method5();
    }
    public static void method5() {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
        for (StackTraceElement element:stackTraceElements) {
            System.out.println(element.getMethodName());
        }
    }
}
Stack Trace і з чим його їдять - 2Тут наша програма безпомилково виконує свою роботу і завершується. Ось що ми побачимо у виведенні консолі:
getStackTrace method5 method4 method2 main Process finished with exit code 0
Як ми отримали таке виведення і що ж сталося в п'ятому методі починаючи з 20-го рядка? Боюся, найкраще, що я зможу зробити, це додати найпопулярніше пояснення (скорочено) юзера Кирила з коментарів до лекції. Звернемося до рядка створення StackTrace і розберемо його поелементно:
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
StackTraceElement[] – вказівка на тип масиву (На ранніх рівнях ви вже проходили масиви типу int[], String[], ось тут те ж саме). stackTraceElements – ім'я масиву, може бути будь-яким з урахуванням загальних правил іменування, на роботу це не впливає. Thread.currentThread() – отримання посилання на поточний потік, в якому виконуються методи, які ми хочемо відстежити (поки це не важливо, докладніше потоки ви будете розбирати на 16 рівні в квесті Java Core) getStackTrace() – отримуємо весь Стек методів, що викликаються (це звичайний геттер для StackTrace) Тепер подивимося, чим нам може бути корисний створений масив. Ми розуміємо, що в масиві зберігається інфа про виконані методи. І для цього в 21-му рядку ми запускаємо модифікований цикл for під назвою forEach (до речі, хто ще не вивчив цей цикл, раджу почитати про нього) і виводимо дані з масиву в консоль, а саме інформацію про те, які методи були виконані в процесі роботи за допомогою конструкції element.getMethodName(). Увага, як ми бачимо, нульовим елементом масиву в нас виявився сам getStackTrace() відповідно, оскільки в момент отримання масиву даних він був останнім методом, що виконався, і через це опинився на верхівці Стека, а пам'ятаючи про нашу конструкцію "Останній прийшов, перший пішов", він одразу ж першим додається в масив під нульовим елементом. Ось що ще ми можемо отримати зі StackTraceElement: String getClassName() – повертає ім'я класу. String getMethodName() – повертає ім'я методу. String getFileName() – повертає ім'я файлу (в одному файлі може бути багато класів). String getModuleName() – повертає ім'я модуля (може бути null). String getModuleVersion() – повертає версію модуля (може бути null). int getLineNumber() – повертає номер рядка у файлі, в якому був виклик методу. Тепер, коли ви зрозуміли загальний принцип роботи, раджу вам самим випробувати різні методи StackTrace у вашому Idea. Навіть якщо ви не зовсім усе засвоїли, продовжуйте навчання і мозаїка складеться так само, як склалася в мене щодо цього питання. Бажаю вам усім успіхів! P. S. Якщо вам сподобався цей матеріал, будь ласка, підтримайте лайком. Вам не важко, мені приємно. Спасибі і побачимося на 41 рівні ;)
Коментарі (3)
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ
Іван Рівень 70 Expert
21 червня 2023
Гарно розписано, дохідливо.
Роман Рівень 79 Expert
13 червня 2023
👍