1. Отримання траси стеку

Отримання траси стеку

Мова програмування Java надає програмісту дуже багато способів отримати інформацію про те, що наразі відбувається в програмі. І це не просто слова.

Наприклад, програми мовою C++ після компіляції перетворюються на один великий файл машинного коду, і все, що під час виконання є доступним програмісту, — це адреса шматка пам'яті з машинним кодом, який зараз виконується. Негусто, еге ж?

А от у Java навіть після компіляції класи залишаються класами, методи й змінні нікуди не діваються, і програміст має багато способів отримати дані про те, що наразі відбувається в програмі.

Траса стеку

Наприклад, у будь-який момент роботи програми можна дізнатися клас та ім'я методу, який зараз виконується. І навіть не тільки отримати ім'я одного методу, а інформацію про весь ланцюжок викликів методів від поточного до методу main().

Список, який складається з поточного методу; методу, що його викликав; методу, що викликав цей другий метод і т. д., називається stack trace (траса стеку). Отримати його можна за допомогою команди

StackTraceElement[] methods = Thread.currentThread().getStackTrace();

Її також можна записати у два рядки:

Thread current = Thread.currentThread();
StackTraceElement[] methods = current.getStackTrace();

Статичний метод currentThread() класу Thread повертає посилання на об'єкт типу Thread, який містить інформацію про поточну нитку (про поточний потік виконання). Докладніше про нитки ви дізнаєтеся на 7-му й 8-му рівнях квесту Java Core.

У цього об'єкта Thread є метод getStackTrace(), який повертає масив елементів StackTraceElement, де кожен з елементів містить інформацію про один метод. Усі елементи разом і утворюють трасу стеку.

Приклад:

Код
public class Main
{
   public static void main(String[] args)
   {
      test();
   }

   public static void test()
   {
      Thread current = Thread.currentThread();
      StackTraceElement[] methods = current.getStackTrace();

      for(var info: methods)
         System.out.println(info);
   }
}
Виведення на екран
java.base/java.lang.Thread.getStackTrace(Thread.java:1606)
Main.test(Main.java:11)
Main.main(Main.java:5)

Як ми бачимо з виведення на екран, у наведеному прикладі метод getStackTrace() повернув масив з трьох елементів:

  • Метод getStackTrace() класу Thread
  • Метод test() класу Main
  • Метод main() класу Main

З цієї траси стеку можна зробити такий висновок:

  • Метод Thread.getStackTrace() було викликано з методу Main.test() у рядку 11 файлу Main.java
  • Метод Main.test() було викликано з методу Main.main() у рядку 5 файлу Main.java
  • Метод Main.main() ніхто не викликав — це перший метод у ланцюжку викликів.

До речі, на екрані було відображено лише частину всієї наявної інформації. Решту можна отримати безпосередньо з об'єкта StackTraceElement



2. StackTraceElement

Клас StackTraceElement, як випливає з його назви, створено для зберігання інформації щодо одного елемента траси стеку — тобто щодо одного методу з траси стеку (StackTrace).

Об'єкти цього класу мають такі методи:

Метод Опис
String getClassName()
Повертає ім'я класу
String getMethodName()
Повертає ім'я методу
String getFileName()
Повертає ім'я файлу (в одному файлі може бути багато класів)
int getLineNumber()
Повертає номер рядка (у файлі), в якому був виклик методу
String getModuleName()
Повертає ім'я модуля (може бути null)
String getModuleVersion()
Повертає версію модуля (може бути null)

За допомогою цих методів можна отримати повнішу інформацію щодо поточного стеку викликів:

Код Виведення на екран Примітка
public class Main
{
   public static void main(String[] args)
   {
      test();
   }

   public static void test()
   {
      Thread current = Thread.currentThread();
      StackTraceElement[] methods = current.getStackTrace();

      for(StackTraceElement info: methods)
      {
         System.out.println(info.getClassName());
         System.out.println(info.getMethodName());

         System.out.println(info.getFileName());
         System.out.println(info.getLineNumber());

         System.out.println(info.getModuleName());
         System.out.println(info.getModuleVersion());
         System.out.println();
      }
   }
}
java.lang.Thread
getStackTrace
Thread.java
1606
java.base
11.0.2

Main
test
Main.java
11
null
null

Main
main
Main.java
5
null
null
ім'я класу
ім'я методу
ім'я файлу
номер рядка
ім'я модуля
версія модуля

ім'я класу
ім'я методу
ім'я файлу
номер рядка
ім'я модуля
версія модуля

ім'я класу
ім'я методу
ім'я файлу
номер рядка
ім'я модуля
версія модуля


3. Стек

Що таке Stack Trace (траса стеку), ви вже знаєте, а що ж таке власне Stack (стек)?

Стек — це структура зберігання даних, в яку можна додавати елементи і з якої можна їх забирати. Причому брати елементи можна лише з кінця: спочатку доданий останнім, потім — передостанній і т. д.

Сама назва Stack перекладається з англійської як «стіс», а стек дійсно дуже схожий на стіс паперу. Якщо ви покладете на стіс паперу аркуші 1, 2 і 3, узяти їх ви зможете лише в зворотному порядку: спочатку третій, потім другий, і тільки потім перший.

У Java навіть є спеціальна колекція з такою поведінкою і такою самою назвою — Stack. Цей клас своєю поведінкою дуже схожий на ArrayList і LinkedList.  Натомість він ще має методи, що реалізують поведінку стеку:

Методи Опис
T push(T obj)
Додає елемент obj у кінець списку (на верх стосу)
T pop()
Забирає елемент з верху стосу (висота стосу зменшується)
T peek()
Повертає елемент з верху стосу (стіс не змінюється)
boolean empty()
Перевіряє, чи не є колекція порожньою
int search(Object obj)
Шукає об'єкт із колекції, повертає його index

Приклад:

Код Вміст стеку (вершина — праворуч)
Stack<Integer> stack = new Stack<Integer>():
stack.push(1);
stack.push(2);
stack.push(3);
int x = stack.pop();
stack.push(4);
int y = stack.peek();
stack.pop();
stack.pop();

[1]
[1, 2]
[1, 2, 3]
[1, 2]
[1, 2, 4]
[1, 2, 4]
[1, 2]
[1]

Стек використовується в програмуванні досить часто. Тож це корисна колекція.



4. Виведення траси стеку під час обробки помилок

Чому ж список викликів методів назвали StackTrace? А тому, що якщо уявити список методів у вигляді стосу аркушів з іменами методів, то під час виклику чергового методу на цей стіс кладеться аркуш з іменем методу, на нього — наступний і т. д.

Коли метод завершується, аркуш з верху стопки видаляється. Не можна видалити аркуш із середини стосу, не видаливши всі аркуші, які лежать на ньому, — не можна припинити роботу методу, не завершивши всі викликані ним методи в ланцюжку викликів.

Винятки

Ще одне цікаве застосування стеку — обробка винятків.

Коли в програмі виникає помилка і створюється виняток, у нього записується поточна stack trace: масив, який складається зі списку методів починаючи з методу main і закінчуючи методом, де сталася помилка. Там навіть є рядок, в якому було створено виняток!

Ця траса стеку для виявлення помилки зберігається всередині винятку, і її можна легко витягти звідти за допомогою методу StackTraceElement[] getStackTrace()

Приклад:

Код Примітка
try
{
   // тут може виникнути виняток
}
catch(Exception e)
{
   StackTraceElement[] methods = e.getStackTrace()
}




Захоплюємо виняток

Отримуємо з нього трасу стеку на момент виникнення помилки.

Це метод класу Throwable, а отже, усі його класи-спадкоємці (тобто взагалі всі винятки) мають метод getStackTrace(). Дуже зручно, чи не так?

Друк траси стеку для виявлення помилки

До речі, клас Throwable має ще один метод для роботи зі stack trace: він виводить у консоль усю інформацію щодо траси стеку, яка зберігається всередині винятку. Цей метод так і називається: printStackTrace().

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

Приклад:

Код
try
{
   // тут може виникнути виняток
}
catch(Exception e)
{
   e.printStackTrace();
}
Виведення на екран
java.base/java.lang.Thread.getStackTrace(Thread.java:1606)
Main.test(Main.java:11)
Main.main(Main.java:5)