Все о String.intern()

http://weblogs.java.net/blog/2006/06/26/all-about-intern
  • Перевод
Думаю, что многие Java-разработчики знают или хотя бы слышали о методе String.intern(). Но далеко не каждый использует его в своих приложениях или представляет в каких случаях он полезен и полезен ли вообще. Так было и со мной до тех пор пока я не столкнулся с этим методом в одном из проектов. В тот момент я захотел узнать смысл и особенности его использования и набрел на одну весьма интересную статью ведущего разработчика Yahoo! по имени Ethan Nicholas, переводом которой теперь хочу поделиться с той частью Хабра-сообщества, которая не безразлична к языку Java.

Тем, кто знает об этом методе лишь понаслышке, добро пожаловать под кат.


Строки являются фундаментальной частью любого современного языка программирования и так же важны, как и числа. Поэтому можно предположить, что Java программисты должны иметь свое твердое представление о них, но к сожалению, это не всегда так.

Сегодня я просматривал исходный код Xerces (XML-парсер, включенных в Java) и наткнулся на строку, которая меня очень удивила:

com.sun.org.apache.xerces.internal.impl.XMLScanner:395
protected final static String fVersionSymbol = "version".intern();

Далее я нашел еще несколько строк, определенных как эта, и каждая из них была интернирована. Так что же такое intern()? Ну, как вы, несомненно, знаете, существует два различных способа для сравнения объектов в Java. Вы можете использовать оператор ==, или же вы можете использовать метод equals(). Оператор == сравнивает ссылаются ли две ссылки на один и тот же объект, в то время как equals() сравнивает содержат ли два объекта одни и те же данные.

Одним из первых уроков, который вы усваиваете при изучении Java является то, что обычно для сравнения двух строк вы должны использовать equals(), а не ==. Если сравнить, скажем, new String("Hello") == new String("Hello"), то в результате получится false, потому что это два разных экземпляра класса. Если же вы используете equals(), то получите true, как и ожидаете. К сожалению, equals() может оказаться довольно медленным, поскольку он выполняет посимвольное сравнение строк.

Т.к. оператор == проверяет идентичность (identity), все, что он должен сделать — это сравнить два указателя, и, очевидно, это будет гораздо быстрее, чем equals(). Так что если вы собираетесь сравнивать одни и те же строки многократно, вы можете получить значительное преимущество в производительности за счет использования проверки идентичности объектов вместо сравнения символов.

Основной алгоритм:

1) Создать множество (hash set) строк
2) Проверить, что строка (как последовательность символов), с которой вы имеете дело, уже в множестве
3) Если да, то использовать строку из множества
4) В противном случае, добавить эту строку в множество и затем использовать ее

При использовании этого алгоритма гарантируется, что если две строки являются идентичными последовательностями символов, они являются одним экземпляром класса. Это означает, что вы можете спокойно сравнивать строки, используя == вместо equals(), получая при этом значительные преимущества производительности при многократно повторяющихся сравнениях.

К счастью Java уже включает в себя реализацию этого алгоритма. Это метод intern() в классе java.lang.String. Выражение new String("Hello").intern() == new String("Hello").intern() возвращает true, в то время как без использования intern() возвращается false.

Так почему же я так удивился, увидев
protected final static String fVersionSymbol = "version".intern();
в исходном коде Xerces? Очевидно, что эта строка будет использоваться для многократных сравнений. Имеет ли смысл интернировать ее?

Конечно, имеет. Вот почему Java уже это делает. Все строки-константы, которые встречаются в классе автоматически интернированы. Сюда входят как собственные константы (например, приведенная выше строка "version"), так и другие строки, которые являются частью формата файла класса — имена классов, сигнатуры методов и так далее. Это распространяется даже на выражения: "Hel" + "lo" обрабатывается javac точно так же, как "Hello", поэтому "Hel" + "lo" == "Hello" возвращает true.

Таким образом, результатом вызова intern() для строки-константы типа "version", по определению, будет точно тот же объект, который вы объявили. Другими словами "version" == "version".intern() всегда истинно. Вам нужно интернировать строки тогда, когда они не являются константами, и вы хотите иметь возможность быстро сравнить их с другими интернированными строками.

Также при интернировании строк можно получить преимущество в использовании памяти, т.к. вы храните в ней лишь один экземпляр последовательности символов строки, независимо от того, сколько раз вы на эту последовательность ссылаетесь. Это основная причина того, почему строковые константы файла класса интернированы: подумайте о том, сколько классов ссылаются, например, на java.lang.Object. Имя класса java.lang.Object должно появиться в каждом из этих классов, но, благодаря магии intern(), оно появляется в памяти лишь в одном экземпляре.

Вывод? intern() является полезным методом и может сделать жизнь легче — но убедитесь, что вы используете его должным образом.

От переводчика
Прошу простить за то, что пару раз исказил исходный текст, чтобы сделать его более понятным (как мне казалось).

Большое спасибо хабраюзеру nolled, который пригласил меня в Хабрасообщество.

Update
Думаю, что следующая информация, которую я узнал из других источников будет здесь не лишней:

1. Пул строк хранится в области «Perm Gen», которая зарезервирована для non-user объектов JVM (классы и пр). Если этого не учитывать, вы можете неожиданно получить OutOfMemory Error.
2. Интернированные строки не хранятся вечно. Строки, на которых нет ссылок, также удаляются сборщиком мусора.
3. В большинстве случаев вы не получите существенного прироста производительности от использования intern() — если сравнение строк не является основной (или очень частой) операцией вашего приложения и сравниваемые строки разные по длине.
Поделиться публикацией
Комментарии 33
    0
    Укажите, пожалуйста, где Вы исказили исходный текст, раз уж признались.

    Получается, Java содержит ещё и скрытую хэш-таблицу для интернирования строк?
      +1
      Она(хэш-таблица) называется пулом стрингов.
      +15
      Немного вас поправлю. Метод intern просто перед созданием объекта String смотрит есть ли этот объект в пуле стрингов и возвращает его. Иначе создается новый объект в пуле.
        +13
        Суть целого поста поместилась в одном предложении. Кр. с. т.
          0
          По поводу краткости я, конечно, согласен. Однако мне кажется, что одного предложения все же недостаточно, чтобы рассказать о смысле и особенностях интернирования строк, а также случаях, когда его использование оправданно.
          0
          Только наверно метод intern вызывается уже после создания объекта String и в случае чего возвращает объект из пула.
            0
            When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

            JavaDoc
              0
              А разве я не это же сказал?
              Сначало делается new String() потом у этого стринга вызывается intern который может вернуть указатель на другой объект, но главное что вызывается это уже после создания того первого экземпляра строки, который правда соберет сборщик мусора.
          0
          Раз уж признался, укажу: добавил пояснение в основной алгоритм для большей ясности, в каком случае говорится об одинаковых строках, а в каком об идентичных объектах, а также еще где-то, где это уже не так существенно.
          Так же хочу уточнить, что 'основной алгоритм' — это обобщенная идея интернирования, а сама реализация String.intern() нативна и о ней мне сложно что-либо сказать со 100% уверенностью.
            +1
            Правильно ли я понимаю, что s.intern()==const по сравнению с s.equals(const) не имеет смысла, а вот если сравнений с константами несколько — то это уже быстрее?
              +19
              К сожалению, наездом на Xerces автор статьи показал не крутое знание intern, а плохое знание того, как работают константы в Java.

              Вопрос о том, зачем в Xerces так написали, задавался неоднократно. И на него есть простой ответ:

              It prevents the field being a compile time constant.

              References to final String fields which are compile time constants are compiled as a literal, not as an access to the field.

              In the example, if there was no .intern(), anything referencing the library would have to be recompiled when the value of the schema namespace is updated. So the authors added 'intern()', making the value not a compile time constant, so the client code desn't have to be rebuilt whhen the library is updated.


              Недостаток данной строки в Xerces состоит в том, что не написан комментарий, объясняющий смысл этой операции. А сам код — правильный.

              В течении прошлого года я 4 раза убеждался, что даже Senior Java Developer с опытом работы 5-6 лет может не подозревать о механизме инлайнинга констант и о том, какой геморрой это создает в продакшене при выпуске обновлений.
                0
                Вот оно что… Но можно по-подробнее о проблемах в продакшене вызванных инлайнингом констант?
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    может быть правильней хотя бы для сборки продакшн пользоваться мавеном?
                      0
                      Безусловно для сборки — да. Но IRL есть еще такое понятие, как hotpatch :)
                    +8
                    Если в классе объявлена final-константа, компилятор может в других классах, ссылающихся на нее поставить не ссылку, а непосредственно ее значение. Поэтому, казалось бы изменили константу в одном классе, перекомпилировали только этот класс и залили на продакшн. И как-будто бы приложение не видит изменений.
                      0
                      Проблема проявляется, например, для IDE при вычислении зависимостей между классами. Если бы константы не инлайнились, то по пулу строк, который живет в начале каждого класса, можно было бы вычислить множество всех классов, на которые ссылается данный.

                      IDE должна знать зависимости, например, для того, чтобы при изменении одного класса корректно определять множество классов, которое следует перекомпилировать. Из-за инлайна констант в байт-коде теряется информация compile-time зависимости.
                      +1
                      Хм… отличное пояснение. Хотя я не уверен, что константа «version» может со временем измениться в отличии от URI, рассмотренного на форуме… Или это пропаганда того, чтобы везде избегать инлайнинга констант?
                        0
                        Если упрощенно: следует избегать инлайнинга констант с видимостью public и protected.
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Вообще-то это можно сказать про любые public или protected final переменные. Но такое как раз сейчас используется не часто, благодаря тому же IOC, а в примере действительно такое, что изменяться вряд ли будет.
                            0
                            Если упрощенно — да, про любые. Частота использования зависит от coding style.

                            Что же до «вряд ли будет изменяться» — это из серии «фразы, которые не должен говорить вслух вменяемый архитектор».
                              0
                              А если не упрощенно, то не про любые, так что ли?

                              Вменяемый архитектор (и невменяемый тоже) отвечает за продукт и делает то, что считает нужным в конкретной ситуации.
                                –1
                                Если не упрощенно, то ответ такой: как и везде при проектировании все определяется тем, как планируется эту константу использовать.

                                Если НУЖНО, чтобы она была заинлайнена — тогда делаем литералом. И пишем комментарий с причиной. Это может иметь смысл, когда мы намеренно требуем обеспечить синхронную пересборку нескольких отдельных единиц деплоймента. Либо если мы ТОЧНО знаем, что эта константа НИКОГДА не может измениться. К примеру, нет причин запрещать инлайнинг константы MONTHS_PER_YEAR.

                                Во всех остальных случаях безопаснее блокировать инлайнинг.

                                Т.е. мы имеем два случая:

                                1. private и package private — пусть инлайнятся.
                                2. public и protected — разрешаем инлайнинг только если к тому есть явные показания.

                                (далее — отход от основной темы)

                                Что до ответственности за продукт — архитектор не должен мыслить категориями «вряд ли». Именно из соображений ответственности.

                                К примеру, если мы используем как уникальный идентификатор домен сайта компании, то обоснование «он вряд ли изменится» является признаком безответственности. Поскольку после ребрендинга или слияния домен может поменяться, а старый — освободиться и достаться другой конторе. Или его могут тупо продать за стопиццот тыщщ баксов.

                                Соответственно, вменяемый архитектор хотя бы опишет неизменность домена как assumption в проектных документах. А совсем умный (таких пока не встречал даже в зеркале) не забудет про раздел «управление рисками».

                                А вот всякие «вряд ли» есть грех.
                                  +1
                                  Ну и как это противоречит тому, что я сказал? Намного больше слов?

                                  И как «эта константа НИКОГДА не может измениться» соотносит с «Что же до «вряд ли будет изменяться» — это из серии «фразы, которые не должен говорить вслух вменяемый архитектор»»?

                                  «Вряд ли» на математическом языке означает с вероятностью 0.99 (или 0.95, или 0.8 — в зависимости от сложности переделки) и бывает такое часто. А НИКОГДА — вероятность 1, что как раз редкость.

                                  Все это словоблудие, рассчитанное на заказчика. Суть в том, что есть конкретная ситуация, которая решается конкретными методами. И если у нас сейчас миллирднооборотный клиент, который принимает Visa, Amex и Master Card, то, соответственно, никто не станет включать названия этих карт в конфигураторы и ни в какие «управления рисками» тоже, ибо все прекрасно понимают — чтобы включить в список, скажем, Diners Club, потребуется год переговоров, одной пицы съедят на миллион, так что добавление еще одного типа карты на этом фоне — полная фигня, поэтому нет смысла марать бумагу всякими глупостями.
                          0
                          >>Очевидно, что эта строка будет использоваться для многократных сравнений. Имеет ли смысл интернировать ее?

                          >>Конечно, имеет. Вот почему Java уже это делает.

                          Тут не все так однозначно. Не знаю, как насчет Java, но в CLR по умолчанию задается атрибут, ограничивающий «массовое интернирование».
                          • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            Я в Java совсем новичок, но тут столкнулся со следующей задачей. Имеется коллекция, где объекты создаются по запросу примерно так:
                            Object get(String name) {
                              Object res = cache.get(name);
                              if(res == null) {
                                res = createElement(name);
                                if(res != null) cache.put(name);
                              }
                              return res;
                            }

                            Возникла такая проблема, что создание некоторых объектов (вызов createElement) долгое и ресурсоёмкое и при этом сразу несколько тредов могут запросить создание одного и того же объекта одновременно (то есть пока один тред обрабатывает createElement, другой снова заходит в этот get и тоже приступает к createElement, потому что объекта ещё нет в кэше). Добавил в объявление метода get волшебное слово synchronized, но стало ещё хуже, потому что если один тред создаёт долгий элемент, то другие ждут его, хотя они запросили совсем другой быстрый элемент. То есть синхронизация нужна только при запросе одного и того же элемента. Обернул тело функции в блок synchronized(name) {}, но это не сработало. Тут я вспомнил, что что-то такое недавно читал на Хабре :-) Изменил на synchronized(name.intern()) {}. После этого как будто бы стало всё прекрасно, так что автору спасибо за подсказку :-) Интересно, это правильное решение задачи или есть подводные камни, о которых я не подумал?
                              0
                              Если ключи длинные и их большое количество (разных), то возможно будет большой расход памяти.
                                0
                                Думаю что действительно далеко не всё предусмотрено, завит от того что за структура этот ваш cache. Если например, туда поставить HashMap, то уже неправильно, так как put операция не атомарная и вполне возможны ситуации, когда добавляются разные элементы и какой-то из них теряется.

                                Про память уже сказали.
                                  +2

                                  Если кто забредёт сюда, не читайте этот бред выше, который я восемь лет назад написал. Был маленький, глупый.

                                  0
                                  Отличная статья.
                                    0
                                    Извините, но Шипилёву я верю больше, чем автору статьи. И он говорит, что использовать intern() как правило не стоит.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                    Самое читаемое