Scala – первые впечатления

Written by elwood

Несколько дней ковыряю Scala. Делюсь первыми соображениями по этому поводу.

Отмеченные удобства Scala:

  • Лаконичный синтаксис: свойства, очень краткие определения бинов, вывод типов
  • Кортежи – и опять же в замечательном лаконичном синтаксисе
  • Анонимные функции, замыкания, вложенные функции
  • Immutable коллекции из коробки
  • Хороший плагин для IDEA (я использую IDEA 14 EAP)

Неудобства:

  • Медленная компиляция (хотя у меня классики-то микромаленькие ещё)
  • Хоть и хорошая, но всё-таки неидеальная поддержка в IDEA (подсказки включают в себя private методы/конструкторы, часто нет сообщения анализатора кода об ошибках, которые присутствуют, нельзя подсмотреть выведенный тип val/var. иногда кавардак в watches)
  • Нет break/continue (есть кривая эмуляция через бросание исключения)
  • Нет try-with-resources (есть кривая эмуляция, не включённая в стандартную библиотеку)
  • Mutable HashMap и Immutable HashMap – одинаковые имена классов. Неудобно
  • Нельзя сделать return в анонимной функции
  • Не поддерживается старый добрый цикл for со счётчиком
  • Нет операции инкремента и декремента
  • Конструкции вида while ((readed = inputStream.read(buffer)) != -1) не поддерживаются

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

Беззнаковая арифметика в Java

Written by elwood

Как известно, в Java нет беззнаковых типов. Если в Си вы могли написать unsigned int (char, long), то в Java так не получится. Однако нередко возникает необходимость в выполнении арифметических операций именно с числами без знака. На первый взгляд кажется, что беззнаковые типы в принципе-то и не особо нужны (подумаешь, MaxInt со знаком меньше в 2 раза, если нужны числа больше, я просто возьму long и далее BigInteger). Но основное различие на самом деле не в том, сколько различных неотрицательных чисел можно положить в signed или unsigned int, а в том, как над ними производятся арифметические операции и сравнения. Если вы работаете с бинарными протоколами или с двоичной арифметикой, где важен каждый используемый бит, нужно уметь выполнять все основные операции в беззнаковом режиме. Рассмотрим эти операции по порядку:

Преобразование byte в short (int, long)

Обычный каст (int) myByte выполнит расширение до 32 бит со знаком – это означает, что если старший бит байта был установлен в 1, то результатом будет то же самое отрицательное число, но записанное в 32-битном формате:

0xff -> 0xffffffff (-1)

Часто это не то, чего бы мы хотели. Для того, чтобы выполнить расширение до 32 бит без знака и получить 0x000000ff, в Java можно записать:

int myInt = myByte & 0xff;
short myShort = myByte & 0xff;

Сравнение без учёта знака

Для беззнакового сравнения есть лаконичная формула:

int compareUnsigned(int a, int b) {
    return Integer.compare( a ^ 0x80000000, b ^ 0x80000000 );
}

Для byte, short и long, соответственно, константы будут 0x80, 0x8000 и 0x8000000000000000L.

Сложение, вычитание и умножение

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

Деление

Деление -256 на 256 даст нам -1. А нам бы хотелось, чтобы 0xffffff00 / 0x100 давало 0x00ffffff, а не 0xffffffff. Для byte, short и int решением будет переход к числам большей разрядности:

int a = 0xffffff00;
int b = 0x100;
int c = (int) ((a & 0xffffffffL) / b); // convert a to long before division

Но что делать с long ? Переходить на BigInteger в таких случаях не вариант – слишком медленно. Остаётся только брать всё в свои руки и реализовывать деление вручную. К счастью, всё уже украдено до нас – в google guava есть реализация беззнакового деления для long, причём довольно шустрая. Если вы не используете эту библиотеку, проще всего выдрать кусок кода прямо из файла UnsignedLongs.java:

  /**
   * Returns dividend / divisor, where the dividend and divisor are treated as unsigned 64-bit
   * quantities.
   *
   * @param dividend the dividend (numerator)
   * @param divisor the divisor (denominator)
   * @throws ArithmeticException if divisor is 0
   */
  public static long divide(long dividend, long divisor) {
    if (divisor < 0) { // i.e., divisor >= 2^63:
      if (compare(dividend, divisor) < 0) {
        return 0; // dividend < divisor
      } else {
        return 1; // dividend >= divisor
      }
    }
 
    // Optimization - use signed division if dividend < 2^63
    if (dividend >= 0) {
      return dividend / divisor;
    }
 
    /*
     * Otherwise, approximate the quotient, check, and correct if necessary. Our approximation is
     * guaranteed to be either exact or one less than the correct value. This follows from fact
     * that floor(floor(x)/i) == floor(x/i) for any real x and integer i != 0. The proof is not
     * quite trivial.
     */
    long quotient = ((dividend >>> 1) / divisor) << 1;
    long rem = dividend - quotient * divisor;
    return quotient + (compare(rem, divisor) >= 0 ? 1 : 0);
  }

Чтобы код компилировался, придётся также позаимствовать реализацию compare(long, long):

  /**
   * Compares the two specified {@code long} values, treating them as unsigned values between
   * {@code 0} and {@code 2^64 - 1} inclusive.
   *
   * @param a the first unsigned {@code long} to compare
   * @param b the second unsigned {@code long} to compare
   * @return a negative value if {@code a} is less than {@code b}; a positive value if {@code a} is
   *         greater than {@code b}; or zero if they are equal
   */
  public static int compare(long a, long b) {
    return Longs.compare(flip(a), flip(b));
  }

и Longs.compare(long, long) + flip(long):

  /**
   * A (self-inverse) bijection which converts the ordering on unsigned longs to the ordering on
   * longs, that is, {@code a <= b} as unsigned longs if and only if {@code flip(a) <= flip(b)}
   * as signed longs.
   */
  private static long flip(long a) {
    return a ^ Long.MIN_VALUE;
  }
 
  /**
   * Compares the two specified {@code long} values. The sign of the value
   * returned is the same as that of {@code ((Long) a).compareTo(b)}.
   *
   * @param a the first {@code long} to compare
   * @param b the second {@code long} to compare
   * @return a negative value if {@code a} is less than {@code b}; a positive
   *     value if {@code a} is greater than {@code b}; or zero if they are equal
   */
  public static int compare(long a, long b) {
    return (a < b) ? -1 : ((a > b) ? 1 : 0);
  }

Побитовые сдвиги

Чтобы окончательно покрыть тему о битовых операциях, вспомним также о сдвигах. В x86 ассемблере есть целая пачка различных команд, которые делают побитовые сдвиги – SHL, SHR, SAL, SAR, ROR, ROL, RCR, RCL. Последние 4 осуществляют циклические сдвиги, их эквивалентов в Java нет. А вот логические и арифметические сдвиги присутствуют. Логический сдвиг (не учитывает знака) – SHL (shift left) и SHR (shift right) – реализуется в Java операторами << и >>> соответственно. С помощью логических сдвигов можно быстро выполнять целочисленные умножение и деление на числа степени двойки. Арифметический сдвиг (учитывает знак) вправо – SAR – реализуется оператором >>. Арифметический сдвиг влево эквивалентен логическому, и поэтому специального оператора для него нет. Может показаться странным, что в ассемблере есть специальный опкод для этой операции, но на самом деле он делает то же самое, то есть SAL полностью повторяет поведение SHL, и об этом прямо говорит документация от Intel:

The shift arithmetic left (SAL) and shift logical left (SHL) instructions perform the same operation; they shift the bits in the destination operand to the left (toward more significant bit locations). For each shift count, the most significant bit of the destination operand is shifted into the CF flag, and the least significant bit is cleared (see Figure 7-7 in the Intel®64 and IA-32 Architectures Software Developer’sManual, Volume 1).

То есть SAL добавили просто для симметрии, с учётом того, что для сдвига вправо есть разделение на логический и арифметический. Ну а Гослинг решил не заморачиваться (и, думается, правильно сделал).

Итак, мы имеем следующее:

a << 1; // беззнаковый сдвиг влево, эквивалентно умножению на 2
a >> 1; // сдвиг вправо с учётом знака (эквивалентно делению на 2)
a >>> 1; // сдвиг вправо без учёта знака (эквивалентно беззнаковому делению на 2)

Заключительные рекомендации

  • При выполнении арифметических действий, которые могут привести к переполнению в выбранной разрядной сетке, нужно всегда точно представлять, какая область допустимых значений может быть у переменных, и отслеживать эти инварианты, расставляя утверждения (assertions). Например, очевидно, что при умножении двух произвольных 32-разрядных беззнаковых чисел результат может не поместиться в 32 бита, и если вам нужно избежать переполнения, нужно либо убедиться, что в этом месте никогда не будет ситуации, при которой произведение не влезает в 32 бита, либо необходимо предварительно сконвертировать оба операнда в long (выполнив a & 0xffffffffL). Здесь, кстати, можно легко допустить ошибку, сконвертировав только один из операндов. Нет, нужно сконвертировать в long оба, т.к. если второй операнд окажется отрицательным, он будет неявно преобразован в long с расширением знака, и результат умножения будет неправильным.
  • Щедро расставляйте скобки в выражениях, где используются побитовые операции. Дело в том, что приоритет побитовых операторов в Java несколько странный, и часто ведёт себя неочевидным образом. Лучше добавить пару скобок, чем потом несколько часов искать трудноуловимые ошибки.
  • Если вам нужна константа типа long, не забудьте добавить суффикс L в конец числа. Если этого не сделать, это будет не long, а int, и при неявном приведении к long снова произойдёт неприятное нам расширение со знаком.

Борьба с native queries в EclipseLink 2.5 (Oracle DB)

Written by elwood

JPA Logo

Предыстория: у меня был проект – веб-приложение, работающее в GlassFish. Приложение собиралось в EAR а слой данных был реализован на базе JPA. В качестве реализации JPA использовался EclipseLink 2.5, встроенный в GlassFish 4.0. База данных – Oracle 11g Express Edition.

Вообще, до этого я всегда работал с Hibernate, и EclipseLink заставил немного помучаться с особенностями восприятия JPQL. Например, EclipseLink не хотел понимать count(*) и все используемые таблицы обязывал снабжать алиасами. Но это мелочи, всё было хорошо, пока не понадобилось сделать несложный запрос: получить объекты с условием, в котором было необходимо применение арифметики с датами. Итак, краткая история мучений.

JPQL: OPERATOR()

Вычитал, что в EclipseLink JPQL можно использовать оператор OPERATOR('AddDate') тынц. Как оказалось, в EclipseLink этот оператор реализован не был. Что в принципе удивительно, т.к. вендором EclipseLink сейчас является Oracle, и нормальный человек в этом случае имеет все основания ожидать хорошей поддержки их СУБД.

JPQL: SQL()

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

Пробуем @NamedNativeQuery

Отчаявшись решить проблему в рамках JPQL, решил спуститься на ступеньку ниже и заюзать Native Query. Но запрос мой требовал соединения четырёх сущностей, и некоторые поля в них назывались по умолчанию одинаково (ID). Аннотация @EntityResult позволяет для таких ситуаций задать discriminatorColumn, но мне не удалось заставить его работать. Маппинг работал некорректно, значения пропертей маппились некорректно в любом варианте использования. Более того, выяснилось, что все столбцы должны иметь уникальный префикс. Иначе вложенные объекты могут захватить свойство (например, ID) внешнего объекта, чей столбец ID был без префикса. Всё это выглядит настолько бажным, что становится странным, как такое количество багов может считаться приемлемым.

Работающий маппинг теперь имел вид:

@SqlResultSetMapping( name = "request-department", entities = {
        @EntityResult( entityClass = Request.class, fields = {
                @FieldResult( name = "id", column = "R_ID"),
                @FieldResult( name = "deadlineDate", column = "R_DEADLINE_DATE"),
                @FieldResult( name = "correctedDeadlineDate", column = "R_CORRECTED_DEADLINE_DATE")
        }),
        @EntityResult( entityClass = Department.class, fields = {
                @FieldResult( name = "id", column = "DEP_ID"),
                @FieldResult( name = "description", column = "DEP_DESCRIPTION"),
                @FieldResult( name = "name", column = "DEP_NAME")
        }),
        @EntityResult( entityClass = User.class, fields = {
                @FieldResult( name = "id", column = "U_ID"),
                @FieldResult( name = "email", column = "U_EMAIL"),
                @FieldResult( name = "personalFirstName", column = "U_FIRST_NAME"),
                @FieldResult( name = "personalLastName", column = "U_LAST_NAME"),
                @FieldResult( name = "personalMiddleName", column = "U_MIDDLE_NAME")
        }),
        @EntityResult( entityClass = User.class, fields = {
                @FieldResult( name = "id", column = "ASS_ID"),
                @FieldResult( name = "email", column = "ASS_EMAIL"),
                @FieldResult( name = "personalFirstName", column = "ASS_FIRST_NAME"),
                @FieldResult( name = "personalLastName", column = "ASS_LAST_NAME"),
                @FieldResult( name = "personalMiddleName", column = "ASS_MIDDLE_NAME")
        })
})

Left join и вложенные объекты

Тут я вспомнил, что мне на самом деле нужен не INNER JOIN, а LEFT JOIN. То есть мне нужно сделать один из @EntityResult – nullable. Но этого сделать нельзя ! EclipseLink видит, что User.Id – первичный ключ, и падает на утверждении, что он не может быть Null. Ничего нельзя сделать, кроме того, что подставлять в столбец магическое число, сигнализирующее о том, что на самом деле сущность отсутствует. Вложенные объекты, кстати, тоже подцепить не удаётся. Но можно их зацепить отдельно, а потом вручную присвоить свойствам внешнего объекта.

http://www.eclipse.org/forums/index.php/t/305321/ https://www.java.net/node/675607

Параметры

Ок, после явного прописывания всех столбцов и использования -1 для сигнализации null-объектов, все работало. Теперь я подумал, что неплохо бы добавить в мой Named Native Query параметры. Как оказалось, это невозможно. Параметры в named native queries не работают вообще и никак. Не работает ничего: ни именование с двоеточия, ни вопросиками, ни через решётку (как предлагают в одном из ответов).

http://eclipse.1072660.n5.nabble.com/Params-on-NamedNativeQuery-td3401.html

Пришлось убрать аннотацию @NamedNativeQuery, и сделать всё руками:

Query nativeQuery = entityManager.createNativeQuery(
    String.format( sql, daysBeforeDeadline, RequestStatus.Sent.ordinal(), RequestStatus.Processing.ordinal() ),
    "request-department" );
List&lt;Object[]> resultList = nativeQuery.getResultList();
 
for ( Object[] rowObjects : resultList ) {
    Request request = ( Request ) rowObjects[0];
    Department department = ( Department ) rowObjects[1];
    User user = ( User ) rowObjects[2];
    User assignee = (User) rowObjects[3];
    request.setDepartment( department );
    request.setUser( user );
    if (assignee.getId() != -1)
    request.setAssignee( assignee );
    requests.add( request );
}
return requests;

Прикол в том, что и здесь параметры тоже не работают ! Приходится вставлять их с помощью String.format().

(Тут вроде у мужиков работает, но у меня и так не заработало)

Заключение

В общем, я рад, что таки-получилось заставить это работать, но какой ценой ? Программистам EclipseLink стоит задуматься о качестве своего продукта, если на такие простые вещи за столько лет разработки у них не отработаны работающие сценарии на одной из самых популярных СУБД, тем более что они теперь – вендоры этой ORM. Но если вы желаете использовать в своём проекте native queries, то лучше придерживаться следующих правил:

  • Не использовать @NamedNativeQuery
  • Явно задавать все имена столбцы, они должны быть уникальны и не должны совпадать по названию ни с одним из свойств вложенных сущностей. Лучше всего задать уникальные префиксы для каждой сущности.
  • Там, где ожидаются nullable-столбцы, но которые мапятся на свойства, не допускающие значений null, придётся использовать магические константы или другие столбцы в качестве флагов
  • Параметры не работают – придётся вставлять их вручную