Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

FindBugs помогает узнать Java отменнее

Anna | 4.06.2014 | нет комментариев
Статические анализаторы кода любят за то, что они помогают обнаружить ошибки, сделанные по невнимательности. Но значительно увлекательнее то, что они помогают поправить ошибки, сделанные по незнанию. Даже если в официальной документации к языку всё написано, не факт, что все программисты это наблюдательно прочитали. И программистов дозволено осознать: всю документацию читать замучаешься.

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

В этом посте я расскажу о некоторых тонкостях Java, о которых я узнал в итоге применения статического анализатора FindBugs. Допустимо, какие-то вещи окажутся непредвиденными и для вас. Значимо, что все примеры не умозрительны, а основаны на настоящем коде.

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

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

Type var = condition ? valTrue : valFalse;

и

Type var;
if(condition)
  var = valTrue;
else
  var = valFalse;

Оказалось, что здесь есть тонкость. Так как тернарный оператор может быть частью трудного выражения, его итогом должен быть определенный тип, определённый на этапе компиляции. Следственно, скажем, при правдивом условии в if-форме компилятор приводит valTrue сразу к типу Type, а в форме тернарного оператора сперва приводит к всеобщему типу valTrue и valFalse (невзирая на то, что valFalse не вычисляется), а после этого теснее итог приводит к типу Type. Правила приведения оказываются не вовсе банальными, если в выражении участвуют простые типы и обёртки над ними (Integer, Double и т. д.) Все правила детально описаны в JLS 15.25. Посмотрим на некоторые примеры.

Number n = flag ? new Integer(1) : new Double(2.0);

Что будет в n, если flag установлен? Объект Double со значением 1.0. Компилятору смешны наши неуклюжие попытки сделать объект. Так как 2-й и 3-й довод — обёртки над различными простыми типами, компилятор разворачивает их и приводит к больше точному типу (в данном случае 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 is unboxed and coerced for ternary operator in TestTernary.main(String[])

A wrapped primitive value is unboxed and converted to another primitive type as part of the evaluation of a conditional ternary operator (the b? e1: e2 operator). The semantics of Java mandate that if e1 and e2 are wrapped numeric values, the values are unboxed and converted/coerced to their common type (e.g, if e1 is of type Integer and e2 is of type Float, then e1 is unboxed, converted to a floating point value, and boxed. See 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 is unboxed and then immediately reboxed in TestTernary.main(String[])
NP_NULL_ON_SOME_PATH: Possible null pointer dereference of null in TestTernary.main(String[])
There is a branch of statement that, if executed, guarantees 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 написано:

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

И это подлинно так, если посмотреть внутреннюю реализацию SimpleDateFormat. В процессе выполнения способа format() объект пишет в поля класса, следственно одновременное применение SimpleDateFormat из 2-х потоков приведёт с некоторой вероятностью к неправильному итогу. Вот что пишет FindBugs по этому поводу:

STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: Call to method of static java.text.DateFormat in TestDate.getDate()
As the JavaDoc states, DateFormats are inherently unsafe for multithreaded use. The detector has found a call to an instance of DateFormat that has been obtained via a static field. This looks suspicous.

For more information on this see Sun Bug #6231579 and Sun Bug #6178997.

Подводные камни BigDecimal

Узнав, что класс BigDecimal разрешает беречь дробные числа произвольной точности, и увидев, что у него есть конструктор от double, некоторые решат, что всё ясно и дозволено делать так:

System.out.println(new BigDecimal(1.1));

Делать так подлинно никто не воспрещает, вот только итог может показаться непредвиденным: 1.100000000000000088817841970012523233890533447265625. Так происходит, потому что простой 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() being called to compare two java.math.BigDecimal numbers. This is normally a mistake, as two BigDecimal objects are only equal if they are equal in both value and scale, so that 2.0 is not equal to 2.00. To compare BigDecimal objects for mathematical equality, use compareTo() instead.

Переводы строк и printf

Неоднократно программисты, перешедшие на Java позже Си, с радостью открывают для себя PrintStream.printf(а также PrintWriter.printf и т. д.). Мол, отменно, это я знаю, прямо как в Си, ничего нового учить не нужно. На самом деле есть различия. Одно из них кроется в переводах строк.

В языке Си есть распределение на текстовые и бинарные потоки. Итог символа ‘n’ в текстовый поток любым методом механически будет преобразован в системно-зависимый перевод строки (“rn” на Windows). В Java такого распределения нет: нужно передавать в выходной поток верную последовательность символов. Это механически делают, скажем, способы семейства PrintStream.println. Но при применении printf передача ‘n’ в строке формата — это легко ‘n’, а не системно-зависимый перевод строки. К примеру, напишем такой код:

System.out.printf("%sn", "str#1");
System.out.println("str#2");

Перенаправив итог в файл, увидим:

Таким образом дозволено получить необычную комбинацию переводов строки в одном потоке, что выглядит неаккуратно и может снести крышу какому-нибудь парсеру. Ошибку дозволено длинно не примечать, исключительно если вы предпочтительно трудитесь на Unix-системах. Для того, Дабы вставить верный перевод строки с поддержкой printf, применяется особый символ форматирования “%n”. Вот что пишет FindBugs по этому поводу:

VA_FORMAT_STRING_USES_NEWLINE: Format string should use %n rather than n in TestNewline.main(String[])

This format string include a newline character (n). In format strings, it is generally preferable better to use %n, which will produce the platform-specific line separator.

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

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB