Подтвердить что ты не робот

Почему метод Java String.equals() использует две переменные подсчета?

Я просто просматривал реализацию класса Java String, и следующее поразило меня как нечетное:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {               // Here n is being decremented...
                if (v1[i] != v2[i])
                    return false;
                i++;                         // while i is being incremented
            }
            return true;
        }
    }
    return false;
}

Это может быть легко реализовано только с одной переменной подсчета, тогда как n будет эффективно окончательным, например:

while (i != n) {
    if (v1[i] != v2[i])
        return false;
    i++;
}

Это также означает, что он полностью избавляется от i:

while (n-- != 0) {
    if (v1[n] != v2[n])
        return false;
}

Связано ли это с сравнением с 0 (младшим битом) дешевле, чем с другой переменной, или есть ли какая-то другая причина, почему он реализован таким образом?

4b9b3361

Ответ 1

Я думаю, что это должно быть с substring до JDK 7.

В то время размер массива базового символа не обязательно был размером строки. Было два поля: offset и count (i an j соответственно), которые показывали, где строка this в базовом массиве, поэтому метод был:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = count;
        if (n == anotherString.count) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = offset;
            int j = anotherString.offset;
            while (n-- != 0) {
                if (v1[i++] != v2[j++])
                    return false;
            }
            return true;
        }
    }
    return false;
}

После изменения, упомянутого выше, этот метод также должен был быть изменен, поэтому они просто фиксировали n, чтобы теперь быть длиной массива:

int n = value.length;

и избавился от j (потому что больше нет смещения):

int i = 0;

Теперь, поскольку i должен быть увеличен после двух обычаев, он увеличивается в отдельном выражении:

if (v1[i] != v2[i])
    return false;
i++;

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

Бенчмаркинг

Что касается бенчмаркинга String#equals vs Arrays#equals(char[], char[]), я думаю, что мы должны сравнивать яблоки с яблоками, поэтому я поставил два подхода для сравнения 2 массивов символов:

public static void main(String[] args) {
    final Random random = new Random();
    final int arrays = 10000;
    final int chars = 1000;
    // generate the arrays
    char[][] array = new char[arrays][chars];
    for (int i = 0; i < arrays; i++) {
        for (int j = 0; j < chars; j++) {
            array[i][j] = (char)(random.nextInt(94) + 33);
        }
    }
    // compare using Arrays equals
    long before = System.nanoTime();
    for (int i = 0; i < arrays; i++) {
        for (int j = 0; j < chars; j++) {
            equals_Arrays(array[i], array[j]);
        }
    }
    System.out.println(System.nanoTime() - before);
    // compare using String equals
    before = System.nanoTime();
    for (int i = 0; i < arrays; i++) {
        for (int j = 0; j < chars; j++) {
            equals_String(array[i], array[j]);
        }
    }
    System.out.println(System.nanoTime() - before);
}

private static boolean equals_Arrays(char[] a, char[] a2) {
    if (a == a2)
        return true;
    if (a == null || a2 == null)
        return false;

    int length = a.length;
    if (a2.length != length)
        return false;

    for (int i = 0; i < length; i++)
        if (a[i] != a2[i])
            return false;

    return true;
}

private static boolean equals_String(char[] v1, char[] v2) {
    if (v1 == v2)
        return true;
    if (v1 == null || v2 == null)
        return false;

    int length = v1.length;
    if (length == v2.length) {
        int i = 0;
        while (length-- != 0) {
            if (v1[i] != v2[i])
                return false;
            i++;
        }
        return true;
    }
    return false;
}

В моей коробке я не вижу заметной разницы.

Ответ 2

Это не настоящий код для большинства реальных Java VM с HotSpot. String.equals - очень важный метод, и он реализуется по внутреннему. Он имеет встроенную платформу. Вы можете найти полный список здесь src/share/vm/classfile/vmSymbols.hpp (см. Do_intrinsic)

Ответ 3

Это НЕ ОТВЕТ, justa pseudo banchmark:

public class CompString {

    public static void main(String[] args) {

        DecimalFormat df = new DecimalFormat("0.#################");
        int count = 1000000;
        int length = 20;
        int runs = 10;

        String[] strings = new String[count];
        String[] copiedStrings = new String[count];
        char[][] chars = new char[count][];
        char[][] copiedChars = new char[count][];

        for (int i = 0; i < count; i++) {
            String str = RandomStringUtils.random(length);

            strings[i] = new String(str);
            copiedStrings[i] = new String(str);

            chars[i] = Arrays.copyOf(str.toCharArray(), str.length());
            copiedChars[i] = Arrays.copyOf(chars[i], chars[i].length);
        }

        System.out.println("Lets banchmark that !!");
        int loop = 0;
        while (loop++ < runs) {
            System.out.println("Run #" + loop);
            long t = 0;
            long t0 = System.currentTimeMillis();
            for (int i = 0; i < count; i++) {
                strings[i].equals(copiedStrings[i]);
            }
            long t1 = System.currentTimeMillis();
            t = t1 - t0;

            System.out.println("Avg String.equals() duration: " + df.format(t / (double) count));

            t0 = System.currentTimeMillis();
            for (int i = 0; i < count; i++) {
                Arrays.equals(chars[i], copiedChars[i]);
            }
            t1 = System.currentTimeMillis();
            t = t1 - t0;

            System.out.println("Avg Arrays.equals(char[] char[]) duration: " + df.format(t / (double) count));
            System.out.println();
        }
    }
}

И вот результаты:

Run #1
Avg String.equals() duration: 0,000017
Avg Arrays.equals(char[] char[]) duration: 0,000013

Run #2
Avg String.equals() duration: 0,000037
Avg Arrays.equals(char[] char[]) duration: 0

Run #3
Avg String.equals() duration: 0,000002
Avg Arrays.equals(char[] char[]) duration: 0

Run #4
Avg String.equals() duration: 0,000003
Avg Arrays.equals(char[] char[]) duration: 0

Run #5
Avg String.equals() duration: 0,000002
Avg Arrays.equals(char[] char[]) duration: 0

Run #6
Avg String.equals() duration: 0,000003
Avg Arrays.equals(char[] char[]) duration: 0

Run #7
Avg String.equals() duration: 0,000003
Avg Arrays.equals(char[] char[]) duration: 0

Run #8
Avg String.equals() duration: 0,000003
Avg Arrays.equals(char[] char[]) duration: 0

Run #9
Avg String.equals() duration: 0,000002
Avg Arrays.equals(char[] char[]) duration: 0

Run #10
Avg String.equals() duration: 0,000002
Avg Arrays.equals(char[] char[]) duration: 0

Итак, поскольку String.equals занимает больше времени, а затем сравнивает базовые массивы (с единственной переменной итератора), мы можем исключить некоторые проблемы оптимизации JVM здесь (но могли ли они быть в прошлом?).