String vs StringBuilder
Сегодня я протестирую скорость добавления строки, при использовании двух классов: класса String и класса StringBuilder.
Сразу начну с примера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
import java.util.Date; public class Main { public static void main(String[] args) { String [] charsArray = {"Q","W","E","R","T","Y","U","I","O", "P","A","S","D","F","G","H","J","K", "L","Z","X","C","V","B","N","M" }; int charsArrayLength = charsArray.length - 1; int charsIterator = 0; String destString = ""; StringBuilder destStringBuilder = new StringBuilder(""); Timer timer = new Timer(); System.out.println("Test String concatenate.."); timer.start(); for (int i=0; i<100000; i++) { destString += charsArray[charsIterator]; if (++charsIterator>charsArrayLength) charsIterator=0; } timer.stop(); System.out.println("Length of destString: "+destString.length()); System.out.println("Time: "+timer.getWorkTimeInMS()+" ms"); System.out.println("-----------------------------"); System.out.println("Test StringBuilder concatenate.."); charsIterator = 0; timer.start(); for (int i=0; i<100000; i++) { destStringBuilder.append(charsArray[charsIterator]); if (++charsIterator>charsArrayLength) charsIterator=0; } timer.stop(); System.out.println("Length of StringBuilder: "+destStringBuilder.length()); System.out.println("Time: "+timer.getWorkTimeInMS()+" ms"); } } class Timer { private long start=0; private long end=0; private long getCurrentTimeInMS() { return new Date().getTime(); } public void start() { reset(); start = getCurrentTimeInMS(); } public void stop() { end = getCurrentTimeInMS(); } public void reset() { start = end = 0; } public long getWorkTimeInMS() { return end-start; } } |
Результат работы:
1 2 3 4 5 6 7 8 9 |
Test String concatenate.. Length of destString: 100000 Time: 2039 ms ----------------------------- Test StringBuilder concatenate.. Length of StringBuilder: 100000 Time: 3 ms |
Как видите String работает во много раз медленнее чем StringBuilder. Как же так? Давайте разбираться!
Что происходит в программе?
Мы создаем длинную строку из 100.000 букв, по очереди добавляя их к 1 длинной строке. Соответственно засекаем время до начала и после окончания работы. И выводим разницу во времени.
Почему медленно работает String?
Дело в том, что объекты класса String (его экземпляры), являются неизменяемыми (на англ. immutable). Это означает, что эти объекты нельзя изменить, при их изменении они пересоздаются заново. Как же это реализовано, давайте посмотрим исходник класса String:
1 2 3 4 5 6 7 |
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; |
Как видим, класс объявлен final = это означает, что мы не можем отнаследовать этот класс, но это не главное. Главное, что массив char-ов который используется как хранилище (storage) объявлен так же как final, а это значит, что этот массив, после внесения в него данных уже нельзя поменять! Вот и вся хитрость.
Вернемся к нашей программе, что же происходит при добавлении буквы? Создается новая строка, туда копируется старая строка + новая буква. Т.е. по сути мы 100.000 раз создаем новую строку и копируем туда прошлый результат. Когда строка состоит из пары букв, это не особо критично, на представьте что нужно копировать строку из 99.999 символов, только для добавления одного символа. Это очень накладно, поэтому и медленно.
Почему быстро работает StringBuilder?
Потому что, в отличии от String, объекты StringBuilder являются изменяемыми (mutable), это значит, что при добавлении буквы, массив представляющий хранилище не копируется заново. Давайте так же глянем как он реализован:
1 2 3 4 5 6 7 |
abstract class AbstractStringBuilder implements Appendable, CharSequence { /** * The value is used for character storage. */ char[] value; |
Как видим, в этом случае, хранилище не объявлено как final. А это значит, что при каждом добавлении, не происходит копирование всей старой строки в новую переменную. Но можно ли утверждать это? На самом деле нет! Давайте взглянем на метод добавления:
1 2 3 4 5 6 7 8 9 10 11 12 |
// Documentation in subclasses because of synchro difference public AbstractStringBuilder append(StringBuffer sb) { if (sb == null) return append("null"); int len = sb.length(); ensureCapacityInternal(count + len); sb.getChars(0, len, value, count); count += len; return this; } |
Как видите, тут присутствует вызов такого метода: ensureCapacityInternal. Именно он и помогает оптимизировать работу всего класса. А работает это так:
- Создается например заранее готовый массив в 100 символов
- Пока вы добавляете поочередно 100 символов, массив не изменяется
- Как только вы станете добавлять 101 символ, массив будет создан заново, но уже размером в 100+100 символов, и в него будет скопирован старый массив, именно за эту проверку и увеличение размера отвечает ensureCapacityInternal
Обратите внимание, что размер именно в 100 символов, я выбрал просто для примера. Точное значение можно посмотреть в конструкторе класса StringBuilder, в моем случае это был массив из 16 символов.
Итак копирование все же происходит, но класс хитрит и выделяет размерность массива, как бы наперед. Как видите особых сложностей нет.
Как это работает думаю понятно.
Почему String неизменяемый (immutable)?
Это сделано, в моем понимании, для использования кеширования строк. Компилятор, чтобы оптимизировать работу со строками, одинаковые строки, хранит в одной и той же ячейке памяти (не буду вдаваться сейчас в подробности, возможно позже напишу отдельный пост), сделать он может это только с константами, поэтому String пришлось сделать final. Отсюда и растут ноги.
Когда использовать String, а когда StringBuilder?
Думаю это очевидно: String используем всегда, когда не требуется частых изменений строк. StringBuilder - как и следует из названия, когда нужно "строить" строки, т.е. менять часто их значения. Это может быть добавление, вставка или удаление символа - посмотрите методы StringBuilder'а и поймете где его ещё лучше использовать.
Что еще интересного в этом примере?
Я создал класс Timer, с помощью которого засекаю время. Возможно он Вам пригодится, если вы захотите измерить скорость работы Вашей программы или какого-то метода.
Вопросы для собеседования:
- В чем отличие работы со строками с помощью классов String и StringBuilder?
- Когда использовать String, а когда StringBuilder
Ответы:
- Объекты класса String является неизменяемым(immutable), а объекты класса StringBuilder являются изменяемыми (mutable)
- String - когда нет или мало изменений со строкой хранящейся в экземпляре класса. StringBuilder - когда надо делать изменения со строкой хранящейся в экземпляре класса.
Author: | Tags: /
| Rating:
Leave a Reply