← Назад к вопросам
Как оптимизировать работу со строкой в цикле
1.0 Junior🔥 121 комментариев
#Основы Java
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как оптимизировать работу со строкой в цикле
Это классический вопрос о производительности. Покажу почему String конкатенация в цикле медленная и как оптимизировать.
1. Проблема: неоптимизированная работа со строками в цикле
public class BadStringLoopExample {
public static void main(String[] args) {
// ❌ НЕПРАВИЛЬНО: String конкатенация в цикле
long start = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 10000; i++) {
result += "Item " + i + ", "; // Очень медленно!
}
long end = System.currentTimeMillis();
System.out.println("Time: " + (end - start) + "ms"); // ~500-1000ms
}
}
Почему это медленно?
String в Java immutable (неизменяемый). Каждая операция `result += ...` создаёт новый String объект:
Итерация 1: result = "" + "Item 0, "
→ создаётся новый String (length=7)
→ старый String удаляется (garbage collection)
Итерация 2: result = "Item 0, " + "Item 1, "
→ создаётся новый String (length=14)
→ старый String (length=7) удаляется
Итерация 3: result = "Item 0, Item 1, " + "Item 2, "
→ создаётся новый String (length=21)
→ старый String (length=14) удаляется
...
Итерация 10000: создаётся String длиной ~70000
→ всего 10000 объектов String создано и удалено!
→ очень медленно из-за GC и копирования памяти
Сложность: O(n²) — квадратичная!
Операция += заключает:
1. Копирование старой строки в новый буфер: O(n)
2. Добавление новых символов: O(m)
3. Удаление старого объекта: O(n)
Для цикла из n итераций:
Итерация 1: O(1)
Итерация 2: O(2)
Итерация 3: O(3)
...
Итерация n: O(n)
Всего: O(1+2+3+...+n) = O(n²)
2. Решение 1: StringBuilder (РЕКОМЕНДУЕТСЯ)
StringBuilder — это mutable (изменяемый) буфер для эффективного построения строк:
public class GoodStringLoopExample {
public static void main(String[] args) {
// ✅ ПРАВИЛЬНО: StringBuilder в цикле
long start = System.currentTimeMillis();
StringBuilder result = new StringBuilder();
for (int i = 0; i < 10000; i++) {
result.append("Item ").append(i).append(", ");
}
String finalResult = result.toString();
long end = System.currentTimeMillis();
System.out.println("Time: " + (end - start) + "ms"); // ~1-5ms (в 100-500 раз быстрее!)
}
}
Как работает StringBuilder:
StringBuilder внутри имеет growable char array:
Capacity = 16 (начальный размер)
┌─────────────────────────────────────────┐
│ 'I' 't' 'e' 'm' ' ' '0' ',' ' ' _ _ _ _ │
└─────────────────────────────────────────┘
Length = 8
При append("Item 1, "):
Длина станет 16, capacity хватает → просто добавляем
┌─────────────────────────────────────────┐
│ 'I' 't' 'e' 'm' ' ' '0' ',' ' ' 'I' 't' 'e' 'm' ' ' '1' ',' ' ' │
└─────────────────────────────────────────┘
Length = 16
При append("Item 2, ") (нужно 8 символов, capacity=16):
Length станет 24, capacity недостаточно → растягиваем буфер
capacity *= 2 → новый capacity = 32
┌──────────────────────────────────────────────────────────────────┐
│ 'I' 't' 'e' 'm' ' ' '0' ',' ' ' 'I' 't' 'e' 'm' ' ' '1' ',' ' ' 'I' 't' 'e' 'm' ' ' '2' ',' ' ' _ _ _ _ _ _ │
└──────────────────────────────────────────────────────────────────┘
Length = 24, Capacity = 32
Сложность: O(n) — линейная!
StringBuilder.append() в среднем O(1) амортизированная сложность
(копирование буфера происходит редко, только при увеличении capacity)
Для цикла из n итераций:
Всего: O(n)
3. StringBuilder vs StringBuffer
public class StringBuilderVsBuffer {
// StringBuilder — НЕ потокобезопасен (но быстрее)
StringBuilder sb = new StringBuilder();
// StringBuffer — потокобезопасен (все методы synchronized)
StringBuffer sf = new StringBuffer();
}
Сравнение:
┌──────────────────┬────────────┬──────────┬──────────────┐
│ Класс │ Потокобезо │ Скорость │ Использование│
├──────────────────┼────────────┼──────────┼──────────────┤
│ String │ ДА │ ~1ms │ Readonly │
│ StringBuilder │ НЕТ │ ~3ms │ Single-thread│
│ StringBuffer │ ДА │ ~10ms │ Multi-thread │
└──────────────────┴────────────┴──────────┴──────────────┘
Для цикла: ВСЕГДА используй StringBuilder (выполняется в одном потоке)
4. Практические примеры оптимизации
Пример 1: CSV построение
public class CsvBuilderExample {
// ❌ НЕПРАВИЛЬНО
public String buildCsvBad(List<String> items) {
String csv = "";
for (String item : items) {
csv += item + ",";
}
return csv;
}
// ✅ ПРАВИЛЬНО: StringBuilder
public String buildCsvGood(List<String> items) {
StringBuilder csv = new StringBuilder();
for (int i = 0; i < items.size(); i++) {
csv.append(items.get(i));
if (i < items.size() - 1) {
csv.append(",");
}
}
return csv.toString();
}
// ✅ ИЛИ с помощью String.join (Java 8+)
public String buildCsvBest(List<String> items) {
return String.join(",", items);
}
}
Пример 2: SQL query построение
public class SqlQueryBuilder {
// ❌ НЕПРАВИЛЬНО
public String buildQueryBad(List<Long> ids) {
String query = "SELECT * FROM users WHERE id IN (";
for (int i = 0; i < ids.size(); i++) {
query += ids.get(i);
if (i < ids.size() - 1) {
query += ",";
}
}
query += ")";
return query;
}
// ✅ ПРАВИЛЬНО: StringBuilder
public String buildQueryGood(List<Long> ids) {
StringBuilder query = new StringBuilder("SELECT * FROM users WHERE id IN (");
for (int i = 0; i < ids.size(); i++) {
query.append(ids.get(i));
if (i < ids.size() - 1) {
query.append(",");
}
}
query.append(")");
return query.toString();
}
// ✅ ИЛИ с помощью stream (Java 8+)
public String buildQueryBest(List<Long> ids) {
return "SELECT * FROM users WHERE id IN (" +
ids.stream()
.map(String::valueOf)
.collect(java.util.stream.Collectors.joining(",")) +
")";
}
}
Пример 3: JSON построение
public class JsonBuilder {
// ❌ НЕПРАВИЛЬНО
public String buildJsonBad(List<User> users) {
String json = "[";
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
json += "{\"id\":" + user.getId() + ",\"name\":\"" + user.getName() + "\"}";
if (i < users.size() - 1) {
json += ",";
}
}
json += "]";
return json;
}
// ✅ ПРАВИЛЬНО: StringBuilder
public String buildJsonGood(List<User> users) {
StringBuilder json = new StringBuilder("[");
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
json.append("{\"id\":")
.append(user.getId())
.append(",\"name\":\"")
.append(user.getName())
.append("\"}");
if (i < users.size() - 1) {
json.append(",");
}
}
json.append("]");
return json.toString();
}
// ✅ ИЛИ используй JSON library (Jackson, Gson)
public String buildJsonBest(List<User> users) {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
return mapper.writeValueAsString(users);
}
}
5. Инициализация StringBuilder с нужным capacity
public class StringBuilderCapacity {
public static void main(String[] args) {
// ❌ ПЛОХО: StringBuilder растёт несколько раз
StringBuilder sb1 = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb1.append("Item ").append(i).append(", ");
}
// Рост буфера: 16 → 32 → 64 → 128 → 256 → 512 → 1024 → 2048 → 4096 → 8192 → 16384
// ✅ ХОРОШО: указываем начальный capacity
StringBuilder sb2 = new StringBuilder(100000);
for (int i = 0; i < 10000; i++) {
sb2.append("Item ").append(i).append(", ");
}
// Буфер не растёт → быстрее
System.out.println("sb1 length: " + sb1.length());
System.out.println("sb1 capacity: " + sb1.capacity());
System.out.println("sb2 capacity: " + sb2.capacity());
}
}
6. Есть ещё методы для специальных случаев
// Повторение строки
String repeated = "a".repeat(1000); // Java 11+
// Join для коллекций
String csv = String.join(",", "a", "b", "c"); // "a,b,c"
// Stream collectors
String result = items.stream()
.map(Object::toString)
.collect(java.util.stream.Collectors.joining(","));
// Formatting (только если нужна гибкость)
String formatted = String.format("%s, %d, %.2f", "Item", 10, 3.14);
7. Benchmark: сравнение производительности
public class StringPerformanceBenchmark {
public static void main(String[] args) {
int iterations = 10000;
// Test 1: String конкатенация
long start = System.nanoTime();
String result1 = "";
for (int i = 0; i < iterations; i++) {
result1 += "Item " + i + ", ";
}
long time1 = System.nanoTime() - start;
// Test 2: StringBuilder
start = System.nanoTime();
StringBuilder result2 = new StringBuilder();
for (int i = 0; i < iterations; i++) {
result2.append("Item ").append(i).append(", ");
}
long time2 = System.nanoTime() - start;
// Test 3: StringBuilder с capacity
start = System.nanoTime();
StringBuilder result3 = new StringBuilder(100000);
for (int i = 0; i < iterations; i++) {
result3.append("Item ").append(i).append(", ");
}
long time3 = System.nanoTime() - start;
System.out.println("String concatenation: " + (time1 / 1_000_000) + "ms");
System.out.println("StringBuilder: " + (time2 / 1_000_000) + "ms");
System.out.println("StringBuilder with capacity: " + (time3 / 1_000_000) + "ms");
System.out.println("Speedup (1 vs 2): " + (double) time1 / time2 + "x");
System.out.println("Speedup (1 vs 3): " + (double) time1 / time3 + "x");
}
}
// Типичный вывод:
// String concatenation: 850ms
// StringBuilder: 2ms
// StringBuilder with capacity: 1ms
// Speedup (1 vs 2): 425x
// Speedup (1 vs 3): 850x
Best Practices
// ✅ ПРАВИЛЬНО
1. StringBuilder для построения строк в цикле
2. String.join() для простых случаев (Java 8+)
3. Инициализируй capacity если знаешь размер
4. Используй append() вместо конкатенации
5. Вызови toString() один раз в конце
// ❌ НЕПРАВИЛЬНО
1. String += в цикле (квадратичная сложность)
2. StringBuffer в single-thread коде (медленнее)
3. Не инициализируй capacity → дополнительные копирования
4. Не используй String.format() в тайтных циклах
Вывод
Для оптимизации работы со строками в цикле:
- Никогда не используй String конкатенацию (
+=) в цикле → O(n²) сложность - Всегда используй StringBuilder → O(n) сложность
- Инициализируй capacity если знаешь примерный размер
- Вызови toString() один раз в конце
Это даёт ускорение в 100-1000 раз для больших объёмов данных.