← Назад к вопросам
Как решить проблему с потреблением большого количества памяти при множественной конкатенации в цикле?
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 везде, где конкатенируешь строки в цикле.