Определить, является ли String константой времени компиляции

Учитывая ссылку на любой String, можно ли программно определить, является ли это ссылкой на константу времени компиляции?
А если нет, то хранится ли она во внутреннем пуле без выполнения s.intern() == s?

isConst("foo")                       -> true
isConst("foo" + "bar")               -> true   // 2 literals, 1 compile time string
isConst(SomeClass.SOME_CONST_STRING) -> true
isConst(readFromFile())              -> false
isConst(readFromFile().intern())     -> false  // true would be acceptable too

(контекст для комментариев ниже: изначально задан вопрос о литералах)


person Bart van Heukelom    schedule 10.07.2019    source источник
comment
Почему readFromFile().intern() возвращает false в вашем примере? Это может быть тот же самый объект "foo", на который ссылаются где-то еще как на строковый литерал.   -  person apangin    schedule 10.07.2019
comment
Похоже, ваш вопрос исходит из неправильного предположения. Записи в пуле строк НЕ создаются во время загрузки класса. Они создаются лениво при первом разрешении инструкции ldc. Таким образом, вполне возможно, что литерал "foo" вернет тот же объект, который был создан ранее readFromFile().intern().   -  person apangin    schedule 10.07.2019
comment
Строковый литерал — это синтаксическая конструкция языка программирования Java. Во время выполнения нет такой вещи, как строковый литерал. Обратите внимание, что 5+6+"" не является строковым литералом, но тем не менее, 5+6+"" == "11" оценивается как true, фактически выражение будет заменено постоянным результатом true даже во время компиляции. Другими словами, «строковые литералы», «константы времени компиляции» и «строки, содержащиеся в пуле времени выполнения» связаны, но все же совершенно разные вещи. Такой метод, как isLiteral("foo"), сам по себе является противоречием, так как внутри метода ссылка на параметр никогда не является литералом.   -  person Holger    schedule 10.07.2019
comment
@apangin Итак, вполне возможно, что литерал foo вернет тот же объект, который был создан ранее с помощью readFromFile ().   -  person Bart van Heukelom    schedule 11.07.2019
comment
@Holger Ну, я полагаю, что константа времени компиляции - это то, что я имел в виду в конце концов. Чтобы было ясно, это академический вопрос, однако теоретический вариант использования, который заставил меня задать этот вопрос, будет чем-то вроде запрета критически важному для производительности методу каждый раз получать новую выделенную строку.   -  person Bart van Heukelom    schedule 11.07.2019
comment
Что ж, особенно для академических вопросов важно правильно различать строковые литералы, константы времени компиляции (типа String) и экземпляры строк, содержащиеся (на которые ссылается) пул времени выполнения.   -  person Holger    schedule 11.07.2019
comment
Теоретически можно оснастить вашу JVM агентом JVMTI, который перехватывает вызов isConst(), проверяет кадр стека, находит сайт вызова, загружает байт-код и проверяет, была ли инструкция LDC непосредственно перед вызовом. Есть еще несколько сложных случаев: например. должен ли isConst(flag ? "foo" : "bar") возвращать true. У меня нет опыта работы с агентами JVMTI, но я считаю, что @apangin может это сделать. Хотя я не уверен, подходит ли такое решение для OP.   -  person Tagir Valeev    schedule 12.07.2019


Ответы (1)


Чтобы прояснить исходный вопрос, каждый строковый литерал является константой времени компиляции, но не каждая константа времени компиляции должна происходить из строкового литерала.

Во время выполнения нет никакой разницы между объектом String, созданным для константы времени компиляции или созданным другими способами. Строки, созданные для констант времени компиляции, автоматически добавляются в пул, но другие строки могут быть добавлены в тот же пул вручную через intern(). Поскольку строки создаются и добавляются лениво, можно даже создать и добавить строку вручную, чтобы константы времени компиляции с тем же значением позже разрешались в эту строку. Этот ответ использует эту возможность, чтобы определить, когда фактически разрешается экземпляр String для константы времени компиляции.

Из этого ответа можно вывести метод, чтобы просто определить, находится ли строка в пуле или нет:

public static boolean isInPool(String s) {
    return s == new String(s.toCharArray()).intern();
}

new String(s.toCharArray()) создает строку с тем же содержимым, которой нет в пуле, и вызов intern() для нее должен разрешаться в ту же ссылку, что и s, если s ссылается на экземпляр в пуле. В противном случае intern() может разрешаться в другой существующий объект или добавлять нашу строку или вновь созданную строку и возвращать ссылку на нее, в зависимости от реализации, но в любом случае возвращаемая ссылка будет отличаться от s.

Обратите внимание, что этот метод имеет побочный эффект добавления строки в пул, если ее там не было раньше, которая останется там, по крайней мере, до следующего цикла сборки мусора, возможно, до следующего полного сбора мусора, в зависимости от реализации.

Метод test может быть удобен для отладки или удовлетворения любопытства, но нет никакого смысла использовать его в рабочем коде. Код приложения не должен зависеть от этого свойства, и вариант использования, предложенный в комментарии, принудительное использование объединенных строк в критически важном для производительности коде, не является хорошей идеей.

Помимо того, что сам тест является дорогостоящим и противодействует цели повышения производительности, базовое предположение о том, что объединенные в пул строки лучше, чем не объединенные в пул, ошибочно. Отсутствие в пуле не означает, что приложение будет выполнять дорогостоящую реконструкцию каждый раз, когда оно вызывает критически важный для производительности код. Он может просто хранить ссылку в переменной или использовать HashMap, оба подхода более эффективны, чем вызов intern(). На самом деле даже временные строки в некоторых случаях могут быть наиболее эффективным решением.

person Holger    schedule 11.07.2019
comment
отличный комментарий: Метод test может быть хорош для отладки или удовлетворения любопытства, но нет никакого смысла использовать его в рабочем коде. Также интересно, что если я ищу использование String::intern, я получаю 281 использование, некоторые из них в самом jdk; хотя сам не могу объяснить почему. - person Eugene; 11.07.2019
comment
@Eugene большинство из них можно проследить до связанного с XML кода от Apache Foundation, что меня не удивляет. Им действительно нравится писать static final String SOME_CONSTANT = "some constant".intern();, чтобы константы времени компиляции не были константами времени компиляции, поэтому поле должно быть прочитано во время выполнения, не знаю, какое эзотерическое преимущество, по их мнению, оно имеет. Затем Swing использует антипаттерн вызова intern(), так что впоследствии они могут использовать == для сравнения, что меня тоже не удивляет. Остальные ~40 случаев связаны с Reflection и Serialization, где это оправдано. - person Holger; 11.07.2019