← Назад к вопросам

Как решить проблему с потреблением большого количества памяти при множественной конкатенации в цикле?

2.2 Middle🔥 121 комментариев
#JVM и управление памятью#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Оптимизация конкатенации строк

Это типичная проблема performance, которая может привести к OutOfMemoryError на больших данных.

Проблема: String конкатенация в цикле

// ❌ НЕПРАВИЛЬНО — создаёт много промежуточных объектов
public String buildStringBadly(int n) {
    String result = "";
    for (int i = 0; i < n; i++) {
        result = result + "Item " + i + "\n";
        // Каждая итерация создаёт НОВЫЙ String объект!
        // Старый выбрасывается в GC
        // Сложность: O(n²), память: O(n²)
    }
    return result;
}

// Почему это плохо?
// Итерация 1: result = "" + "Item 0\n"  → новый String
// Итерация 2: "Item 0\n" + "Item 1\n" → новый String (старый на GC)
// Итерация 3: "Item 0\nItem 1\n" + "Item 2\n" → новый String
// ...
// На n=1000 создаём ~1000 временных объектов

Решение 1: StringBuilder (самое распространённое)

// ✅ ПРАВИЛЬНО — используем StringBuilder
public String buildStringRight(int n) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < n; i++) {
        sb.append("Item ").append(i).append("\n");
        // Один объект, переиспользуется
        // Сложность: O(n), память: O(n)
    }
    return sb.toString(); // Один финальный String
}

// Тест производительности
public static void performanceTest() {
    int n = 10000;
    
    // Неправильно: ~1 сек, много GC
    long start = System.nanoTime();
    String bad = buildStringBadly(n);
    long badTime = System.nanoTime() - start;
    
    // Правильно: ~1 мс
    start = System.nanoTime();
    String good = buildStringRight(n);
    long goodTime = System.nanoTime() - start;
    
    System.out.println("Bad: " + (badTime / 1_000_000) + " ms");
    System.out.println("Good: " + (goodTime / 1_000_000) + " ms");
    System.out.println("Разница: " + (badTime / goodTime) + "x раз медленнее");
}

Решение 2: StringBuffer (потокобезопасный вариант)

// Если нужна многопоточность
public class ThreadSafeStringBuilder {
    private StringBuffer buffer = new StringBuffer();
    
    public synchronized void append(String text) {
        buffer.append(text);
    }
    
    public String build() {
        return buffer.toString();
    }
}

// Но StringBuffer медленнее из-за synchronization
// В однопоточном коде всегда используй StringBuilder!

Решение 3: String.join() для списков

// Если данные в коллекции
public String joinStrings(List<String> items) {
    return String.join(", ", items);
    // Эффективнее, чем цикл со StringBuilder
}

// Пример
List<String> items = List.of("apple", "banana", "cherry");
String result = String.join(", ", items);
System.out.println(result); // apple, banana, cherry

// С форматированием
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
String csv = String.join(",", 
    numbers.stream().map(String::valueOf).collect(Collectors.toList())
);

Решение 4: StringBuilder для сложного форматирования

public String buildCSVRow(String name, int age, String city) {
    StringBuilder sb = new StringBuilder();
    sb.append('"').append(name).append('"');
    sb.append(',');
    sb.append('"').append(age).append('"');
    sb.append(',');
    sb.append('"').append(city).append('"');
    return sb.toString();
    // Альтернатива: String.format(
    //     "\"%s\",\"%d\",\"%s\", name, age, city
    // );
}

Решение 5: Stream с StringBuilder для больших данных

import java.util.stream.Stream;

public class LargeDataProcessor {
    // Обработка миллионов строк
    public String processManyLines(Stream<String> lines) {
        return lines.collect(
            StringBuilder::new,      // Supplier — создаёт StringBuilder
            (sb, line) -> sb.append(line).append("\n"), // Accumulator
            (sb1, sb2) -> sb1.append(sb2)              // Combiner для паралл.
        ).toString();
    }
    
    // Или с помощью StringJoiner
    public String processManyLinesJoiner(Stream<String> lines) {
        return lines.collect(
            java.util.StringJoiner::new,
            (joiner, line) -> joiner.add(line),
            (j1, j2) -> j1.merge(j2)
        ).toString();
    }
}

Решение 6: StringJoiner для разделителей

import java.util.StringJoiner;

public String buildWithJoiner(String... items) {
    StringJoiner joiner = new StringJoiner(", ", "[", "]");
    for (String item : items) {
        joiner.add(item);
    }
    return joiner.toString();
    // [item1, item2, item3]
}

// С условиями
public String buildCSV(List<String> values) {
    StringJoiner joiner = new StringJoiner(",");
    for (String value : values) {
        if (value != null && !value.isEmpty()) {
            joiner.add('"' + value.replace("\"", "\\\\\"") + '"');
        }
    }
    return joiner.toString();
}

Решение 7: Оптимизация для очень больших объёмов

public class OptimizedLargeFileBuilder {
    private final int BUFFER_SIZE = 65536; // 64KB
    
    public void buildLargeFile(String outputPath, Iterator<String> lines) throws IOException {
        try (BufferedWriter writer = new BufferedWriter(
            new FileWriter(outputPath), BUFFER_SIZE)) {
            
            StringBuilder sb = new StringBuilder(BUFFER_SIZE);
            while (lines.hasNext()) {
                sb.append(lines.next()).append('\n');
                
                // Пишем на диск, когда буфер почти полон
                if (sb.length() > BUFFER_SIZE - 1024) {
                    writer.write(sb.toString());
                    sb.setLength(0); // Очищаем, но переиспользуем буфер
                }
            }
            
            // Пишем остаток
            if (sb.length() > 0) {
                writer.write(sb.toString());
            }
        }
    }
}

Сравнение методов

МетодПотокобезопасноБыстротаПрименение
String +Да❌ Очень медленноНе используй в циклах
StringBuilderНет✅ Очень быстроСтандартный выбор
StringBufferДа⚠️ МедленнееМногопоточность (редко)
String.join()Да✅ БыстроСписки с разделителем
StringJoinerДа✅ БыстроСложный формат
Streams collectЗависит✅ БыстроПараллельная обработка
BufferedWriter-✅ БыстроФайлы (большие объёмы)

Правило памяти:

// Для n итераций цикла:
// String +      → O(n²) временная память (плохо!)
// StringBuilder → O(n)  финальная память (хорошо!)

Старое правило: используй StringBuilder везде, где конкатенируешь строки в цикле.