Collectors.joining против StringBuilder.append

Какой из них лучше по производительности?

finalWords.stream().forEach(word -> stringBuilder.append(word).append(”,“)); 
String finalResult = stringBuilder.toString();

ПРОТИВ

String finalResult = finalWords.stream().collect(Collectors.joining(","));

Честно говоря, я не думаю, что вы когда-нибудь соедините 1 миллион струн. Так что отдайте предпочтение максимально читаемому, вне зависимости от производительности.

Arnaud Denoyelle 04.05.2018 16:34

Лучший способ - это, вероятно, использовать String.join. Но вам, вероятно, стоит ориентир.

Bernhard Barker 04.05.2018 16:40

@JaroslawPawlak Да, я посмотрел код и до сих пор не уверен в производительности, поэтому и поднял вопрос. К тому же я экономлю время многих разработчиков, у которых в будущем может возникнуть такой же вопрос. Надеюсь, это оправдывает вопрос. И я уверен, что вы тоже узнаете что-то новое в ходе обсуждения, которое здесь происходит!

Sagar 04.05.2018 16:41

в StringBuilder вы можете указать емкость, в StringJoiner вы не можете, он будет увеличивать *2 каждый раз, массив строк 8 -> 16 -> 32, эти изменения размера используют Array.copyOf. Я бы сказал, что при должной емкости StringBuilder выиграет в тесте скорости.

Eugene 04.05.2018 16:43

@Sagar Оба подхода используют StringBuilder, поэтому разница будет незначительной. Вы можете увидеть некоторый шаблонный код в joining - все эти новые созданные объекты (например, StringBuilder) и все операторы if, поэтому он определенно будет немного медленнее. Оба подхода имеют сложность O(n), поэтому все сводится к тому, что @ArnaudDenoyelle уже сказал выше - «предпочитать наиболее читаемый».

Jaroslaw Pawlak 04.05.2018 18:01

Поскольку код не делает то же самое, бессмысленно обсуждать различия в производительности. Первый вариант добавляет завершающий ",", чего не делает соединитель. Кроме того, первый вариант не является потокобезопасным. Помимо этих различий, они делают то же самое под поверхностью. Таким образом, любая разница в производительности возникает из-за того, что не делается одно и то же. По словам Дюклинга, вы также можете использовать String.join(",", finalWords), который попроще (но делает то же самое)…

Holger 04.05.2018 18:54
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
6
6
2 879
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я собрал небольшой тест, чтобы проверить это, потому что мне было любопытно. Он инициализирует List со случайно сгенерированными size в нижнем регистре, каждый из которых имеет длину String:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Fork(3)
public class MyBenchmark {

    @Param({"10", "100", "1000", "10000", "100000"})
    private int size;

    private List<String> finalWords;

    @Setup(Level.Invocation)
    public void initialize() {
        finalWords = IntStream.range(0, size)
                              .mapToObj(i -> {
                                  return ThreadLocalRandom.current()
                                                          .ints(10, 'a', 'z' + 1)
                                                          .mapToObj(c -> Character.toString((char) c))
                                                          .collect(Collectors.joining());
                              }).collect(Collectors.toList());
    }

    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }

    @Benchmark
    public String stringBuilder() {
        StringBuilder sb = new StringBuilder();
        finalWords.forEach(word -> sb.append(word).append(","));
        return sb.toString();
    }

    @Benchmark
    public String stream() {
        return finalWords.stream().collect(Collectors.joining(","));
    }
}

Вот результаты:

Benchmark                  (size)  Mode  Cnt        Score        Error  Units
MyBenchmark.stream             10  avgt   30      242.330 ±      5.177  ns/op
MyBenchmark.stream            100  avgt   30     1426.333 ±     20.183  ns/op
MyBenchmark.stream           1000  avgt   30    30779.509 ±   1114.992  ns/op
MyBenchmark.stream          10000  avgt   30   720944.424 ±  27845.997  ns/op
MyBenchmark.stream         100000  avgt   30  7701294.456 ± 648084.759  ns/op
MyBenchmark.stringBuilder      10  avgt   30      170.566 ±      1.833  ns/op
MyBenchmark.stringBuilder     100  avgt   30     1166.153 ±     21.162  ns/op
MyBenchmark.stringBuilder    1000  avgt   30    32374.567 ±    979.288  ns/op
MyBenchmark.stringBuilder   10000  avgt   30   473022.229 ±   8982.260  ns/op
MyBenchmark.stringBuilder  100000  avgt   30  4524267.849 ± 242801.008  ns/op

Как видите, метод 10 в этом случае работает быстрее, даже если я не указываю начальную емкость.

это не честный тест, первый ничего не инициирует от Stream API, жрет ваше время

Eugene 04.05.2018 16:47

Я также сомневаюсь, что вам здесь нужен Level.Invocation, поскольку все, что вам нужно сделать, это собрать их в строку в конечном итоге

Eugene 04.05.2018 16:48

@Eugene Вы рекомендуете использовать stream().forEach(?

Jacob G. 04.05.2018 16:49

Сам вопрос, вероятно, в том, что быстрее StringJoiner или StringBuilder, и это то, о чем должен быть тест, таким образом, ну, вы сравниваете разные вещи. Еще бы добавил тест с начальной емкостью StringBuilder

Eugene 04.05.2018 16:51

поскольку OP в конечном итоге хочет String, это может пойти еще дальше, как насчет java-9 concat + java-9 with a different strategy, что-то вроде @Fork(jvmArgsAppend = "-Djava.lang.invoke.stringConcat=BC_SB") или даже guava Joiner, это сделало бы тест, который я хотел бы

Eugene 04.05.2018 16:56

Было бы интересно также заменить и протестировать ненужный цикл Stream.forEach() обычным циклом for.

Lukas Eder 09.05.2018 12:24

@JacobG. Что вы использовали для этого теста? Являются ли аннотации частью API какой-либо платформы тестирования?

TMG 18.12.2019 14:52

@TMG Скорее всего, это был JMH, Java Microbenchmark Harness.

Jacob G. 18.12.2019 14:53

@JacobG. Вау, это был невероятно быстрый ответ! Спасибо :-)

TMG 18.12.2019 14:55

Другие вопросы по теме