Разница между if (a - b <0) и if (a <b)

Я читал исходный код Java ArrayList и замечал некоторые сравнения в if-statement.

В Java 7 метод grow(int) использует

if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;

В Java 6, grow не существовало. Однако метод ensureCapacity(int) использует

if (newCapacity < minCapacity)
    newCapacity = minCapacity;

В чем причина изменения? Это проблема производительности или просто стиль?

Я мог представить себе, что сравнение с нолем происходит быстрее, но выполнение полного вычитания просто для проверки того, кажется ли это негативным для меня немного излишним. Также в терминах байт-кода это будет включать две команды (ISUB и IF_ICMPGE) вместо одного (IFGE).

Ответ 1

a < b и a - b < 0 могут означать две разные вещи. Рассмотрим следующий код:

int a = Integer.MAX_VALUE;
int b = Integer.MIN_VALUE;
if (a < b) {
    System.out.println("a < b");
}
if (a - b < 0) {
    System.out.println("a - b < 0");
}

При запуске будет напечатано только a - b < 0. Случается, что a < b явно ложно, но a - b переполняется и становится -1, что отрицательно.

Теперь, сказав это, учтите, что массив имеет длину, которая действительно близка к Integer.MAX_VALUE. Код в ArrayList выглядит следующим образом:

int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

oldCapacity действительно близок к Integer.MAX_VALUE, поэтому newCapacity (который oldCapacity + 0.5 * oldCapacity) может переполняться и стать Integer.MIN_VALUE (т.е. отрицательным). Затем вычитание minCapacity возвращается обратно в положительное число.

Эта проверка гарантирует, что if не будет выполнен. Если код был записан как if (newCapacity < minCapacity), в этом случае он был бы true (так как newCapacity отрицательный), поэтому newCapacity будет принудительно minCapacity независимо от oldCapacity.

Этот случай переполнения обрабатывается следующим if. Когда newCapacity переполнится, это будет true: MAX_ARRAY_SIZE определяется как Integer.MAX_VALUE - 8 и Integer.MIN_VALUE - (Integer.MAX_VALUE - 8) > 0 is true. Поэтому newCapacity корректно обрабатывается: метод hugeCapacity возвращает MAX_ARRAY_SIZE или Integer.MAX_VALUE.

NB: это комментарий // overflow-conscious code в этом методе.

Ответ 2

Я нашел это объяснение:

Вторник, 9 мар 2010 в 03:02, Кевин Л. Стерн писал (а):

Я сделал быстрый поиск, и кажется, что Java действительно два дополнения   исходя из. Тем не менее, позвольте мне указать, что в целом это   тип кода беспокоит меня, так как я полностью ожидаю, что в какой-то момент кто-то будет   придите и сделайте то, что предложил Дмитрий. то есть кто-то будет   изменить:

if (a - b > 0)
     

к

if (a > b)
     

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

if (oldCapacity > RESIZE_OVERFLOW_THRESHOLD) {
   // Do something
} else {
  // Do something else
}

Это хороший момент.

В ArrayList мы не можем сделать это (или, по крайней мере, несовместимо), потому что ensureCapacity является общедоступным API и фактически уже принимает отрицательные числа в качестве запросов на положительную способность, которые не могут быть удовлетворены.

Текущий API используется следующим образом:

int newcount = count + len;
ensureCapacity(newcount);

Если вы хотите избежать переполнения, вам нужно будет что-то изменить менее естественным, чем

ensureCapacity(count, len);
int newcount = count + len;

В любом случае, я сохраняю код с переполнением, но добавляю больше предупреждающие комментарии и создание "огромного массива" с выделением, чтобы Теперь код ArrayList выглядит следующим образом:

/**
 * Increases the capacity of this <tt>ArrayList</tt> instance, if
 * necessary, to ensure that it can hold at least the number of elements
 * specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
public void ensureCapacity(int minCapacity) {
    modCount++;

    // Overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // Overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

Обновлен Webrev.

Martin

В Java 6, если вы используете API как:

int newcount = count + len;
ensureCapacity(newcount);

И newCount переполнения (это становится отрицательным), if (minCapacity > oldCapacity) вернет false, и вы можете ошибочно предположить, что значение ArrayList было увеличено на len.

Ответ 3

Взглянув на код:

int newCapacity = oldCapacity + (oldCapacity >> 1);

Если oldCapacity достаточно велико, это будет переполняться, а newCapacity будет отрицательным числом. Сравнение типа newCapacity < oldCapacity будет неправильно оценивать true, а ArrayList не будет расти.

Вместо этого код, записанный (newCapacity - minCapacity < 0 возвращает false), позволит продолжить оценку отрицательного значения newCapacity в следующей строке, что приведет к пересчету newCapacity путем вызова hugeCapacity (newCapacity = hugeCapacity(minCapacity);) чтобы ArrayList вырасти до MAX_ARRAY_SIZE.

Это то, что комментарий // overflow-conscious code пытается связываться, хотя и наклонно.

Итак, в нижней строке новое сравнение защищает от выделения ArrayList больше, чем предопределенный MAX_ARRAY_SIZE, позволяя ему расти вплоть до этого предела, если это необходимо.

Ответ 4

Две формы ведут себя точно так же, если выражение a - b не переполняется, и в этом случае они противоположны. Если a является большим отрицательным, а b является большим положительным, то (a < b) явно истинно, но a - b будет переполняться, чтобы стать положительным, поэтому (a - b < 0) является ложным.

Если вы знакомы с ассемблером x86, считайте, что (a < b) реализуется jge, который веткится вокруг тела оператора if, когда SF = OF. С другой стороны, (a - b < 0) будет действовать как a jns, который веткится, когда SF = 0. Следовательно, они ведут себя по-разному точно, когда OF = 1.