воскресенье, 13 ноября 2011 г.

Java String.substring() и расход памяти


Недавно в треде на Хабре (http://habrahabr.ru/blogs/java/132500) было обсуждение того, насколько накладен метод String.substring. Мол, каждый раз при вызове этого метода создается новый объект, это жеж кошмар и все такое. Однако, давайте посмотрим чуть внимательнее.

Любой Java-программист знает, как устроен внутри метод String (до определенных пределов :) - никто не знает всех деталей этого процесса, и то, насколько много вы знаете о строках в Java, и определяет вашу зрелость как Java-программиста, ха!).

Итак, у объекта класса String есть три поля. Offset, length, value. Последнее хранит массив символов, которые и представляют из себя строку, первый и второй - смещение от начала строки в массиве value[],  и длину региона в этом массиве, которые и позволяют нам "вырезать" из массива символов value[]  то, что можно назвать "срез текущей строки". Зачем мы храним в памяти эти два дополнительных свойства, смещение и длину строки? По разным причинам, одна из них - следование паттерну Flightweight, который позволяет нам использовать тот факт, что массив value[] - неизменяем, и расшаривать один общий экземпляр этого массива при вызове методов, которые возвращают нам часть исходной строки, например, substring.

Посмотрим внимательнее на метод String.substring:


public String substring(int beginIndex, int endIndex) {

 if (beginIndex < 0) {

 throw new StringIndexOutOfBoundsException(beginIndex);

 }

 if (endIndex > count) {

 throw new StringIndexOutOfBoundsException(endIndex);

 }

 if (beginIndex > endIndex) {

 throw new StringIndexOutOfBoundsException(endIndex - beginIndex);

 }

 return ((beginIndex == 0) && (endIndex == count)) ? this :

 new String(offset + beginIndex, endIndex - beginIndex, value);

 }



Смотрим теперь по коду конструктор с такой же сигнатурой, видим:

// Package private constructor which shares value array for speed.

 String(int offset, int count, char value[]) {

 this.value = value;

 this.offset = offset;

 this.count = count;

 }

Вывод — при вызове метода Substring, новый объект String конечно создается, но массив символов, используемый для хранения данных, не копируется. В новом объекте String будут просто использованы другие значения для offset/lenght, и ссылка на тот же самый массив символов, относительно которых эти offset/length и берутся.


Небольшая тонкость относительно копирования строк, о которой не все знают. Иногда об этом не знают даже те, кто пишет об оптимизации использования памяти на Хабре ;)

4 комментария:

  1. Да, про него я забыл упомянуть. Спасибо за напоминание!

    ОтветитьУдалить
  2. Дык, в той статье говорится о ситуации, когда мы прочитали длинную-предлинную строку, а работаем с небольшим кусочком этой строки. В такой ситуации был как раз таки выгодно скопировать маленький кусочек массива, чтобы большой массив был убран сборщиком мусора. И чтобы это произошло дядечка с хабрахабра предлагает делать

    ss = new String(longString.substring(3))

    вместо


    ss = longString.substring(3)

    Так мы заставляем создать новый маленький массив символов, чтобы старый был убран gc.


    Так что слив не засчитан:)

    ОтветитьУдалить
  3. P.S.

    А вообще, лучше использовать apache-commons StringUtils - там гораздо оптимальней ф-ии написаны, чем в стандартном классе.

    Стандартный split, кстати, вообще редко когда стоит использовать: внутри этой ф-ии при каждом вызове компилится регексп. Это ужос!

    ОтветитьУдалить