JavaRush /Java блог /Архив info.javarush /Не давал покоя мне substring(..)
IgorBrest
33 уровень

Не давал покоя мне substring(..)

Статья из группы Архив info.javarush
Собственно, по причине сабжа позволил себе покопаться в String.substring(..). И пришел, наверное, к неожиданным результатам, которыми решил поделиться с Вами, дорогие Джаворашовцы. Представить на Ваш суд, так сказать. Так вот. Есть такое утверждение, что строка, созданная с помощью метода substring(..) использует массив символов исходной строки. Вот, в частности, выдержка из недавно прочтенной статьи "Справочник по java. Статические строки" всеми уважаемого articles:
Есть замечание относительно метода substring — возвращаемая строка использует тот же байтовый массив, что и исходная
Ну и конечно Лекции джавараш. Вот цитаты из Декции 22:
Когда мы создаем подстроку с помощью метода substring, то создается новый объект String. Но вместо того, чтобы хранить ссылку на массив с новым набором символов, этот объект хранит ссылку на старый массив символов и вместе с этим хранит две переменные, с помощью которых определяет – какая часть оригинального массива символов относится к нему. ... Когда создается подстрока, массив символов не копируется в новый объект String. Вместо этого оба объекта хранят ссылку на один и тот же массив символов. Но! Второй объект хранит еще две переменных, в который записано с какого и сколько символов этого массива – его. ... Поэтому, если ты возьмешь строку длинной 10,000 символов и наделаешь из нее 10,000 подстрок любой длинны, то эти «подстроки» будут занимать очень мало памяти, т.к. массив символов не дублируется. Строки, которые должны занимать кучу места, будут занимать буквально пару байт.
все понятно расписано, даже разжевано. Но, так как я пытаюсь повысить знание английского, то часто обращаюсь к официальной документации, и вот там я как то не смог найти подтверждение сему факту...Списав это на свою невнимательность, я все таки заглянул в исходник substring() (благо IDEA позволяет это сделать одним нажатием кнопки). public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > value.length) { throw new StringIndexOutOfBoundsException(endIndex); } int subLen = endIndex - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen); } заинтригованный, я пошел дальше: * Allocates a new {@code String} that contains characters from a subarray * of the character array argument. The {@code offset} argument is the * index of the first character of the subarray and the {@code count} * argument specifies the length of the subarray. The contents of the * subarray are copied; subsequent modification of the character array does * not affect the newly created string. public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count < 0) { throw new StringIndexOutOfBoundsException(count); } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); } где Arrays.copyOfRange - нативный метод, который возвращает копию массива из char... Вполне себе тривиальный код, и мне показалось очевидным, что просто создается новая строка с новым набором chars. или я что то не учел... Так до конца и не поверив в свои выводы, я решил как нибудь потестировать этот substring(), опираясь на фразу из лекции:
Поэтому, если ты возьмешь строку длинной 10,000 символов и наделаешь из нее 10,000 подстрок любой длинны, то эти «подстроки» будут занимать очень мало памяти...
только вместо 10_000 сделаем сразу 100_000_000, чего мелочиться. Накидал побыстрому такой код: public class Test { public static void main(String[] args) { System.out.println("Начинаем:"); print(); System.out.println("********************************"); char[]big=new char[100_000_000];//создаем нормальный такой массив int j=0;//и заполняем этот массив всякой ерундой for (int k=0;klist=new ArrayList<>();//здесь будут ссылки на строки, что бы сборщик мусора не удалял //не используемые, по его мнению, строки. System.out.println("************************************"); System.out.println("Теперь будем создавть подстроки с помощью substring(..) и наблюдать," + "что же происходит с памятью"); for (int i = 2; i <10; i++) { //создаем подстроку, используя метод String.substring(..) String sub= bigString.substring(1,bigString.length()-1); //если этот метод не создает полностью новый массив символов, а только пользуется //исходным из bigString // то при создании новой строки sub мы не будем наблюдать ощутипый расход памяти list.add(sub);//эти ссылки мы должны где нибудь хранить, иначе сборщик мусора //избавится от неипользуемых объктов String System.out.print(String.format("Создаем %d-ую подстроку, при этом ", i - 1)); print(); } System.out.println("***************************************"); print(); } static void print(){ System.out.println("Памяти используется "+(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())/1024/1024 + " mb"); } } и вот, что получилось: Начинаем: Памяти используется 0 mb ******************************** создал большую строку bigString на основе массива big. Теперь: Памяти используется 382 mb ************************************ Теперь будем создавть подстроки с помощью substring(..) и наблюдать,что же происходит с памятью Добавляем 1-ую подстроку, при этом Памяти используется 573 mb Добавляем 2-ую подстроку, при этом Памяти используется 763 mb Добавляем 3-ую подстроку, при этом Памяти используется 954 mb Добавляем 4-ую подстроку, при этом Памяти используется 1145 mb Добавляем 5-ую подстроку, при этом Памяти используется 1336 mb Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOfRange(Arrays.java:3658) at java.lang.String.(String.java:201) at java.lang.String.substring(String.java:1956) at com.javarush.test.tests.Test.main(Test.java:42) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:483) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134) Process finished with exit code 1 т.е. каждый раз при создании новой строки sub при помощи bigString.substring(..) массив символов, как раз ДУБЛИРУЕТСЯ. Иначе как объяснить такой рост расхода памяти?. После этого лично у меня отпали всякие сомнения относительно работы метода String.substsring() А у Вас?
Комментарии (4)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Joysi Уровень 41
26 апреля 2016
Уху, просто до JDK 7u6 подстроки хранились 32 байта (+ содержали String.offset и String.count, которые позволяли и ссылаться,
не создавая в памяти подстроки как написано в статье JavaRush)
OFFSET    SIZE  TYPE   DESCRIPTION
0          12          (object header)
1           4   char[] String.value
16          4   int    String.offset
20          4   int    String.count
24          4   int    String.hash
28          4          (alignment loss)


а после 7u6 — занимают 24 байта:
OFFSET    SIZE  TYPE   DESCRIPTION
0          12          (object header)
12          4   char[] String.value
16          4   int    String.hash
20          4          (alignment loss)


Сделали это не для экономии памяти :), к тому же сильно проиграли в скорости работы substring() — теперь надо выделять память и копировать(затратно),
а не просто инициировать начало(String.offset) и смещение(String.count).
Цель — избавится от утечек и расхода памяти. Так как создав подстроку:
1) трудно контролировать необходимость надобности удаления исходной строки (зависимость)
2) если исходная строка огромная, а мы сабстрингаем 2-3 символа, то и исходная строка вынуждена оставаться и занимать память.

Кому интересны подробности на русском языке, потратьте час на:
видео: www.youtube.com/watch?v=SZFe3m1DV1A и
слайды: shipilev.net/talks/jpoint-April2015-string-catechism.pdf
ferasinka Уровень 32
26 апреля 2016
Судя по комментариям отсюда:
stackoverflow.com/questions/20260140/how-to-detect-whether-string-substring-copies-the-character-data
таакая штука имела место в версиях Java ниже седьмой.
In older Java versions, String.substring(..) will use the same char array as the original, with a different offset and count.
In the latest Java versions (according to the comment by Thomas Mueller: since 1.7 Update 6), this has changed, and substrings are now be created with a new char array.