Методы удобства Java 9 collection factory как альтернатива сборникам

Рассмотрим этот метод (только для иллюстрации):

boolean isSmallNumber(String s) {
    return (n in ["one", "two", "three", "four"]);
}

Это, конечно, не Java, но это может быть на вашем любимом альтернативном языке, который поддерживает литералы коллекции, такие как Groovy или Kotlin. Выражение лаконично и, подобно строковым литералам, компилятору разрешено помещать литерал коллекции в некоторую статическую область хранения (возможно, даже "intern()").

Теперь введите Java 9:

boolean isSmallNumber(String s) {
    return Set.of("one", "two", "three", "four").contains(s);
}

Это также красноречиво, но, к сожалению, каждый раз, когда вы его вызываете, он выделяет новый набор в кучу, а затем сразу же делает его доступным для сбора мусора.

Вы можете, конечно, определить константу коллекции:

private static final Set<String> SMALL_NUMBERS = Set.of(...);

Но тогда это определение может быть в тысячах строк от определения метода в большом классе, и вы, возможно, не сможете думать о хорошем описательном имени для него, тогда как литерал может быть более ясным (в этом гипотетическом случае).

Итак, если я использую Set.of(...) внутри метода, компилятор JIT оптимизирует создание нового объекта каждый раз при вызове метода?

Ответ 1

Я разработал простой тест JMH:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class Temp {

    private Object value;

    @Setup
    public void setUp() {
        value = 50;
    }

    @Benchmark
    public boolean list1() {
        return List.of("one").contains(value);
    }

    @Benchmark
    public boolean list2() {
        return List.of("one", "two").contains(value);
    }

    @Benchmark
    public boolean list3() {
        return List.of("one", "two", "three").contains(value);
    }

    @Benchmark
    public boolean list4() {
        return List.of("one", "two", "three", "four").contains(value);
    }

    @Benchmark
    public boolean set1() {
        return Set.of("one").contains(value);
    }

    @Benchmark
    public boolean set2() {
        return Set.of("one", "two").contains(value);
    }

    @Benchmark
    public boolean set3() {
        return Set.of("one", "two", "three").contains(value);
    }

    @Benchmark
    public boolean set4() {
        return Set.of("one", "two", "three", "four").contains(value);
    }
}

После запуска теста с -prof gc я могу сделать следующий вывод: JIT оптимизирует list1, list2, set1, set2, но не list3, list4, set3, set4 [1]

Это кажется вполне разумным, потому что для N >= 3 listN/setN создавать более сложные реализации List/Set, чем для N <= 2.

List реализация для 2 элементов:

static final class List2<E> extends AbstractImmutableList<E> {
    private final E e0;
    private final E e1;
    ...
}

List реализация для 3 или более элементов:

static final class ListN<E> extends AbstractImmutableList<E> {
    private final E[] elements;
    ...
}

listN содержит еще один уровень косвенности (массив), который, судя по всему, делает анализ перехода намного сложнее.


Выход JMH (слегка измененный для соответствия странице):

Benchmark                  Mode  Cnt     Score      Error   Units
list1                      avgt    5     3,075 ?    1,165   ns/op
list1:·gc.alloc.rate       avgt    5     0,131 ?    1,117  MB/sec
list1:·gc.alloc.rate.norm  avgt    5    ? 10??               B/op
list1:·gc.count            avgt    5       ? 0             counts

list2                      avgt    5     3,161 ?    0,543   ns/op
list2:·gc.alloc.rate       avgt    5     0,494 ?    3,065  MB/sec
list2:·gc.alloc.rate.norm  avgt    5     0,001 ?    0,003    B/op
list2:·gc.count            avgt    5       ? 0             counts

list3                      avgt    5    33,094 ?    4,402   ns/op
list3:·gc.alloc.rate       avgt    5  6316,970 ?  750,240  MB/sec
list3:·gc.alloc.rate.norm  avgt    5    64,016 ?    0,089    B/op
list3:·gc.count            avgt    5   169,000             counts
list3:·gc.time             avgt    5   154,000                 ms

list4                      avgt    5    32,718 ?    3,657   ns/op
list4:·gc.alloc.rate       avgt    5  6403,487 ?  729,235  MB/sec
list4:·gc.alloc.rate.norm  avgt    5    64,004 ?    0,017    B/op
list4:·gc.count            avgt    5   165,000             counts
list4:·gc.time             avgt    5   146,000                 ms

set1                       avgt    5     3,218 ?    0,822   ns/op
set1:·gc.alloc.rate        avgt    5     0,237 ?    1,973  MB/sec
set1:·gc.alloc.rate.norm   avgt    5    ? 10??               B/op
set1:·gc.count             avgt    5       ? 0             counts

set2                       avgt    5     7,087 ?    2,029   ns/op
set2:·gc.alloc.rate        avgt    5     0,647 ?    4,755  MB/sec
set2:·gc.alloc.rate.norm   avgt    5     0,001 ?    0,010    B/op
set2:·gc.count             avgt    5       ? 0             counts

set3                       avgt    5    88,460 ?   16,834   ns/op
set3:·gc.alloc.rate        avgt    5  3565,506 ?  687,900  MB/sec
set3:·gc.alloc.rate.norm   avgt    5    96,000 ?    0,001    B/op
set3:·gc.count             avgt    5   143,000             counts
set3:·gc.time              avgt    5   108,000                 ms

set4                       avgt    5   118,652 ?   41,035   ns/op
set4:·gc.alloc.rate        avgt    5  2887,359 ?  920,180  MB/sec
set4:·gc.alloc.rate.norm   avgt    5   104,000 ?    0,001    B/op
set4:·gc.count             avgt    5   136,000             counts
set4:·gc.time              avgt    5    94,000                 ms

[1] Java HotSpot (TM) 64-разрядная серверная VM (сборка 9 + 181, смешанный режим)