JavaRush /Java блог /Random UA /FindBugs допомагає дізнатися Java краще
articles
15 рівень

FindBugs допомагає дізнатися Java краще

Стаття з групи Random UA
Статичні аналізатори коду люблять за те, що вони допомагають знайти помилки, зроблені через неуважність. Але набагато цікавіше те, що вони допомагають виправити помилки, зроблені через незнання. Навіть якщо в офіційній документації до мови написано все, не факт, що всі програмісти це уважно прочитали. І програмістів можна зрозуміти: всю документацію читати замучишся. У цьому плані статичний аналізатор схожий на досвідченого товариша, який сидить поряд і дивиться, як ви пишете код. Він не тільки підказує вам: «ось тут ти помабовся, коли копіпастил», а й каже: «ні, так писати не можна, он сам у документацію глянь». Такий товариш корисніший за саму документацію, тому що він підказує тільки ті речі, з якими ви реально стикаєтеся в роботі, і мовчить про ті, які вам ніколи не знадобляться. У цьому пості я розповім про деякі тонкощі Java, які я дізнався в результаті використання статичного аналізатора FindBugs. Можливо, якісь речі виявляться несподіваними для вас. Важливо, що це приклади не умоглядні, а засновані на реальному коді.

Тернарний оператор?:

Здавалося б, немає нічого простішого за тернарного оператора, але в нього є свої підводні камені. Я вважав, що немає принципової різниці між конструкціями Type var = condition ? valTrue : valFalse; і Type var; if(condition) var = valTrue; else var = valFalse; виявилося, що тут є тонкість. Оскільки тернарний оператор то, можливо частиною складного висловлювання, його результатом може бути конкретний тип, визначений етапі компіляції. Тому, скажімо, за справжньої умови в if-формі компілятор наводить valTrue відразу до типу Type, а у формі тернарного оператора спершу призводить до загального типу valTrue і valFalse (незважаючи на те, що valFalse не обчислюється), а потім результат приводить до типу Тип. Правила приведення виявляються не зовсім тривіальними, якщо у виразі беруть участь примітивні типи та обгортки над ними (Integer, Double і т.д.). Усі правила докладно описані в JLS 15.25. Подивимося деякі приклади. Number n = flag ? new Integer(1) : new Double(2.0); Що буде у n, якщо flag встановлений? Об'єкт Double із значенням 1.0. Компілятор смішні наші незграбні спроби створити об'єкт. Так як другий і третій аргумент - обгортки над різними примітивними типами, компілятор розгортає їх і призводить до більш точного типу (у цьому випадку double). А після виконання тернарного оператора для присвоєння знову виконується боксинг. По суті, код еквівалентний такому: Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); З точки зору компілятора код не містить проблем і чудово компілюється. Але FindBugs видає попередження:
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: Primitive value є unboxed і coerced для ternary operator в TestTernary.main(String[]) A zabalené primitive value є unboxed і спрямований на будь-який примітивний тип, як частина evaluation of a conditional ). Зустріч Java mandate, що якщо e1 і e2 є зарядженими числовими значеннями, значеннями є unboxed і convertible/coerced to the common type (eg, if e1 is of type Integer and e2 is of type Float, then e1 is unboxed, con Sea JLS Section 15.25 Зрозуміло, FindBugs попереджає і про те, що Integer.valueOf(1) ефективніше, ніж new Integer(1), але це вже всі і так знають.
Або такий приклад: Integer n = flag ? 1 : null; Автор хоче помістити null у n, якщо прапор не встановлено. Думаєте, спрацює? Так. Але давайте ускладнимо: Integer n = flag1 ? 1 : flag2 ? 2 : null; Здавалося б, особливої ​​різниці немає. Однак тепер, якщо обидва прапори скинуті, цей рядок генерує NullPointerException. Варіанти для правого тернарного оператора – int і null, тому результуючий тип Integer. Варіанти для лівого – int та Integer, тому за правилами Java результат – int. Для цього треба зробити unboxing, викликавши intValue, що і видає виняток. Код еквівалентний такому: Integer n; if( flag1 ) n = Integer.valueOf(1); else { if( flag2 ) n = Integer.valueOf(Integer.valueOf(2).intValue()); else n = Integer.valueOf(((Integer)null).intValue()); } Тут FindBugs видає два повідомлення, яких достатньо, щоб запідозрити помилку:
BX_UNBOXING_IMMEDIATELY_REBOXED: Boxed value є unboxed і потім є immediately reboxed в TestTernary.main(String[]) NP_NULL_ON_SOME_PATH: Можливо, null pointer dereference of null in TestTernary.main(String[]) Thereis that a null value will be dereferenced, which would generate a NullPointerException when the code is executed.
Ну і останній приклад на цю тему: double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } Не дивно, що цей код не працює: як функція, що повертає примітивний тип, може повернути null? Дивно, що вона без проблем компілюється. Ну, чому компілюється — ви вже зрозуміли.

DateFormat

Для форматування дати і часу Java рекомендується користуватися класами, що реалізують інтерфейс DateFormat. Наприклад, це виглядає так: public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } Найчастіше клас багаторазово використовує той самий формат. Багатьом спадає на думку ідея оптимізації: навіщо щоразу створювати об'єкт формату, коли можна користуватися спільним екземпляром? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } Ось так красиво та здорово, тільки, на жаль, не працює. Точніше працює, але зрідка ламається. Справа в тому, що в документації до DateFormat написано:
Дані формати не synchronized. Це використовується для створення окремого формату положень для кожного thread. Якщо багаторазові штрихи користуються форматом поточно, він повинен бути synchronized externally.
І це справді так, якщо подивитися внутрішню реалізацію SimpleDateFormat. У процесі виконання методу format() об'єкт пише у поля класу, тому одночасне використання SimpleDateFormat із двох потоків призведе з деякою ймовірністю до неправильного результату. Ось що пише FindBugs із цього приводу:
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: Запустити метод static java.text.DateFormat in TestDate.getDate() Як JavaDoc states, DateFormats є врівноважено unsafe для multithreaded use. Послідовник має повідомити про те, щоб почати DateFormat, що він був охоплений через static field. Це випадки suspicous. Більше інформації на цьому сайті Sun Bug #6231579 і Sun Bug #6178997.

Підводне каміння BigDecimal

Дізнавшись, що клас BigDecimal дозволяє зберігати дрібні числа довільної точності, і побачивши, що у нього є конструктор від double, деякі вирішать, що все ясно і можна робити так: System.out.println(new BigDecimal(1.1)); Робити так дійсно ніхто не забороняє, ось тільки результат може здатися несподіваним: 1.10000000000000088817841970012523233890533447265625. Так відбувається, тому що примітивний double зберігається у форматі IEEE754, в якому неможливо уявити 1.1 ідеально точно (у двійковій системі числення виходить нескінченний періодичний дріб). Тому зберігається максимально близьке значення до 1.1. А конструктор BigDecimal(double) навпаки працює точно: він ідеально перетворює задане число в IEEE754 до десяткового вигляду (кінцевий двійковий дріб завжди представний у вигляді кінцевого десяткового). Якщо ж хочеться у вигляді BigDecimal саме 1.1, можна написати або new BigDecimal("1.1"), або BigDecimal.valueOf(1.1). Якщо число не виводити одразу, а зробити з ним якісь операції, можна й не зрозуміти, звідки береться помилка. FindBugs видає попередження DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE, у якому даються самі поради. А ось ще одна штука: BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); Фактично d1 і d2 є одним і тим же числом, але equals видає false, тому що він порівнює не тільки значення чисел, але і поточний порядок (число знаків після коми). Це написано в документації, але мало хто читатиме документацію до такого знайомого методу як equals. Така проблема може випливти далеко не відразу. Сам FindBugs про це, на жаль, не попереджає, але є популярне розширення до нього - fb-contrib, в якому цей баг враховано:
MDM_BIGDECIMAL_EQUALS equals() слід назвати дві java.math.BigDecimal numbers. Це є normally a mistake, as 2BigDecimal об'єкти є тільки еквівалент, і якщо вони є еквівалентом в межах значення і розмір, так що 2.0 не є еквівалентом до 2.00. Для того, щоб compare BigDecimal objects для математичного еквівалента, use compareTo() instead.

Переклади рядків та printf

Нерідко програмісти, що перейшли на Java після Сі, радо відкривають для себе PrintStream.printf (а також PrintWriter.printf і т. д.). Мовляв, чудово, це я знаю, як у Сі, нічого нового вчити не треба. Насправді, є відмінності. Одне з них у перекладах рядків. У мові Сі є поділ на текстові та бінарні потоки. Виведення символу '\n' у текстовий потік будь-яким способом автоматично буде перетворено на системно-залежний переклад рядка ("r\n" на Windows). У Java такого поділу немає: треба передавати у вихідний потік правильну послідовність символів. Це автоматично роблять, наприклад, методи сімейства PrintStream.println. Але під час використання printf передача '\n' у рядку формату — це просто '\n', а чи не системно-залежний переклад рядка. Наприклад, напишемо такий код: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); Перенаправивши результат у файл, побачимо: FindBugs допомагає дізнатися Java краще - 1 Таким чином можна отримати дивну комбінацію перекладів рядка в одному потоці, що виглядає неакуратно і може знести дах якомусь парсеру. Помилки можна довго не помічати, особливо якщо ви працюєте переважно на Unix-системах. Для того, щоб вставити правильний переклад рядка за допомогою printf, використовується спеціальний символ форматування %n. Ось що пише FindBugs із цього приводу:
VA_FORMAT_STRING_USES_NEWLINE: Формат string повинен використовувати %n rathan than \n in TestNewline.main(String[]) Цей формат string include a newline character (\n). У форматі strings, it is generally preferable better to use %n, which will produce platform-specific line separator.
Можливо, для деяких читачів усе перераховане давно відомо. Але я практично впевнений, що і для них знайдеться цікаве попередження статичного аналізатора, яке відкриє їм нові особливості мови програмування, що використовується.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ