1. Обзор
Преимущество использования ключевого слова final
в производительности является очень популярной темой для дебатов среди разработчиков Java. В зависимости от того, где мы его применяем, ключевое слово final
может иметь разное назначение и разные последствия для производительности .
В этом руководстве мы рассмотрим, есть ли какие-либо преимущества в производительности от использования ключевого слова final
в нашем коде. Мы рассмотрим влияние на производительность использования final
на уровне переменной, метода и класса.
Наряду с производительностью мы также упомянем аспекты дизайна использования ключевого слова final
. Наконец, мы порекомендуем, следует ли нам его использовать и по какой причине.
2. Локальные переменные
Когда final
применяется к локальной переменной, ее значение должно быть присвоено ровно один раз .
Мы можем присвоить значение в окончательном объявлении переменной или в конструкторе класса. Если мы позже попытаемся изменить конечное значение переменной, компилятор выдаст ошибку.
2.1. Тест производительности
Давайте посмотрим, может ли применение ключевого слова final
к нашим локальным переменным повысить производительность.
Мы воспользуемся инструментом JMH , чтобы измерить среднее время выполнения тестового метода. В нашем тестовом методе мы выполним простую конкатенацию строк неконечных локальных переменных:
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public static String concatNonFinalStrings() {
String x = "x";
String y = "y";
return x + y;
}
Далее мы повторим тот же тест производительности, но на этот раз с окончательными локальными переменными:
@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public static String concatFinalStrings() {
final String x = "x";
final String y = "y";
return x + y;
}
JMH позаботится о запуске итераций прогрева, чтобы позволить оптимизации JIT-компилятора вступить в силу. Наконец, давайте взглянем на измеренную среднюю производительность в наносекундах:
Benchmark Mode Cnt Score Error Units
BenchmarkRunner.concatFinalStrings avgt 200 2,976 ± 0,035 ns/op
BenchmarkRunner.concatNonFinalStrings avgt 200 7,375 ± 0,119 ns/op
В нашем примере использование конечных локальных переменных позволило ускорить выполнение в 2,5 раза.
2.2. Статическая оптимизация кода
Пример объединения строк демонстрирует, как ключевое слово final
может помочь компилятору статически оптимизировать код .
Используя неконечные локальные переменные, компилятор сгенерировал следующий байт-код для объединения двух строк:
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ARETURN
Добавив ключевое слово final
, мы помогли компилятору сделать вывод, что результат конкатенации строк никогда не изменится. Таким образом, компилятор смог вообще избежать конкатенации строк и статически оптимизировать сгенерированный байт-код:
LDC "xy"
ARETURN
Следует отметить, что в большинстве случаев добавление final
к нашим локальным переменным не приводит к значительному повышению производительности, как в этом примере.
3. Переменные экземпляра и класса
Мы можем применить ключевое слово final
к переменным экземпляра или класса. Таким образом, мы гарантируем, что их присвоение значений может быть выполнено только один раз. Мы можем присвоить значение при окончательном объявлении переменной экземпляра, в блоке инициализатора экземпляра или в конструкторе.
Переменная класса объявляется путем добавления ключевого слова static
к переменной-члену класса. Кроме того, применяя ключевое слово final
к переменной класса, мы определяем константу . Мы можем присвоить значение при объявлении константы или в статическом блоке инициализатора:
static final boolean doX = false;
static final boolean doY = true;
Давайте напишем простой метод с условиями, использующими эти логические
константы:
Console console = System.console();
if (doX) {
console.writer().println("x");
} else if (doY) {
console.writer().println("y");
}
Затем давайте удалим ключевое слово final
из логических
переменных класса и сравним сгенерированный классом байт-код:
- Пример использования неконечных переменных класса — 76 строк байт-кода
- Пример использования конечных переменных класса (констант) — 39 строк байт-кода
Добавив ключевое слово final
в переменную класса, мы снова помогли компилятору выполнить статическую оптимизацию кода . Компилятор просто заменит все ссылки на конечные переменные класса их фактическими значениями.
Однако следует отметить, что подобный пример редко используется в реальных Java-приложениях. Объявление переменных как final
может оказать лишь незначительное положительное влияние на производительность реальных приложений.
4. Эффективно окончательный
Термин эффективно финальная переменная был введен в Java 8. Переменная является эффективно финальной, если она явно не объявлена как final, но ее значение никогда не изменяется после инициализации.
Основная цель эффективных конечных переменных — позволить лямбда-выражениям использовать локальные переменные, которые явно не объявлены как final. Однако компилятор Java не будет выполнять статическую оптимизацию кода для эффективных переменных final так, как он это делает для переменных final.
5. Классы и методы
Ключевое слово final
имеет другое назначение применительно к классам и методам. Когда мы применяем ключевое слово final
к классу, этот класс не может быть подклассом. Когда мы применяем его к методу, этот метод нельзя переопределить.
О преимуществах производительности применения final
к классам и методам не сообщается. Более того, финальные классы и методы могут доставлять большие неудобства разработчикам, поскольку ограничивают наши возможности повторного использования существующего кода. Таким образом, безрассудное использование final
может поставить под угрозу хорошие принципы объектно-ориентированного проектирования.
Есть несколько веских причин для создания окончательных классов или методов, таких как обеспечение неизменяемости. Однако выигрыш в производительности не является веской причиной для использования final
на уровнях классов и методов .
6. Производительность против чистого дизайна
Помимо производительности, мы могли бы рассмотреть и другие причины использования final
. Ключевое слово final
может помочь улучшить читаемость и понятность кода. Давайте рассмотрим несколько примеров того, как final
может сообщать о выборе дизайна:
- final классы — это проектное решение для предотвращения расширения — это может быть путем к неизменяемым объектам .
- методы объявлены окончательными, чтобы предотвратить несовместимость дочерних классов
- аргументы метода объявляются окончательными, чтобы предотвратить побочные эффекты
- конечные переменные предназначены только для чтения
Таким образом, мы должны использовать final
для информирования других разработчиков о выборе дизайна . Кроме того, ключевое слово final
, применяемое к переменным, может служить подсказкой компилятору для выполнения незначительных оптимизаций производительности.
7. Заключение
В этой статье мы рассмотрели преимущества использования ключевого слова final
в производительности . В примерах мы показали, что применение ключевого слова final
к переменным может оказать незначительное положительное влияние на производительность . Тем не менее, применение ключевого слова final
к классам и методам не приведет к повышению производительности.
Мы продемонстрировали, что, в отличие от переменных final, фактически переменные final не используются компилятором для выполнения статической оптимизации кода. Наконец, помимо производительности, мы рассмотрели последствия для дизайна применения ключевого слова final
на разных уровнях.
Как всегда, исходный код доступен на GitHub .