Улучшает ли использование ключевое слово final в Java?


347

В Java мы видим много мест, где finalключевое слово можно использовать, но его использование встречается редко.

Например:

String str = "abc";
System.out.println(str);

В приведенном выше случае, strможет быть, finalно это обычно прекращается.

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

Действительно ли использование ключевого слова final в любом из этих случаев действительно повышает производительность? Если так, то как? Пожалуйста, объясни. Если правильное использование finalдействительно имеет значение для производительности, какие привычки должен разработать Java-программист, чтобы наилучшим образом использовать ключевое слово?


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

Если я запускаю свой инструмент PMD (плагин для затмения), используемый для проверки, он предлагает внести изменения для переменной в случае, как показано выше. Но я не поняла его концепцию. Неужели производительность так сильно бьет?
Абхишек Джайн

5
Я думал, что это был типичный экзаменационный вопрос. Я помню, что выпускные действительно влияют на производительность, выпускные классы IIRC могут быть оптимизированы JRE, так как они не могут быть разделены на подклассы.
Каву

Я действительно проверил это. На всех виртуальных машинах я проверил, использование окончательного на локальных переменных сделал улучшить производительность (немного, но тем не менее может быть фактором в методах полезности). Исходный код в моем ответе ниже.
rustyx

1
«преждевременная оптимизация - корень всего зла». Просто дайте компилятору выполнить свою работу. Напишите читаемый и хороший закомментированный код. Это всегда лучший выбор!
Кайзер

Ответы:


285

Обычно нет. Для виртуальных методов, HotSpot отслеживает , есть ли способ на самом деле переопределен, и может выполнять оптимизации, такие как встраивание, в предположении, что метод не был переопределен - до тех пор, пока он не загрузит класс, который переопределяет метод, и в этот момент он может отменить (или частично отменить) эти оптимизации.

(Конечно, это предполагает, что вы используете HotSpot - но это, безусловно, самая распространенная JVM, так что ...)

На мой взгляд, вы должны использовать final на основе четкого дизайна и удобочитаемости, а не по соображениям производительности. Если вы хотите что-то изменить по соображениям производительности, вам следует выполнить соответствующие измерения, прежде чем изгибать наиболее четкий код из формы - таким образом, вы можете решить, стоит ли какая-либо дополнительная достигнутая производительность в худшей читаемости / дизайне. (По моему опыту это почти никогда не стоит; YMMV.)

РЕДАКТИРОВАТЬ: Поскольку последние поля были упомянуты, стоит упомянуть, что они в любом случае являются хорошей идеей с точки зрения четкого дизайна. Они также изменяют гарантированное поведение с точки зрения видимости между потоками: после завершения конструктора все конечные поля гарантированно сразу же становятся видимыми в других потоках. Это, пожалуй, наиболее распространенное использование finalв моем опыте, хотя, как сторонник эмпирического правила Джоша Блоха "я должен использовать его finalчаще для занятий ..."


1
@Abhishek: О чем конкретно? Самым важным моментом является последний - что вы почти наверняка не должны беспокоиться об этом.
Джон Скит

9
@Abishek: finalобычно рекомендуется, потому что это облегчает понимание кода и помогает находить ошибки (потому что это делает намерение программистов явным). PMD, вероятно, рекомендует использоватьfinal из-за этих проблем стиля, а не из-за соображений производительности.
слеске

3
@Abhishek: Многие из них, скорее всего, связаны с JVM и могут опираться на очень тонкие аспекты контекста. Например, я полагаю, что JVM сервера HotSpot будет по-прежнему разрешать встраивание виртуальных методов при переопределении в одном классе, с быстрой проверкой типа, где это уместно. Но детали сложно определить, и они могут меняться в зависимости от выпуска.
Джон Скит

5
Здесь я хотел бы процитировать Effective Java, 2 - е издание, пункт 15, Минимизация переменчивость: Immutable classes are easier to design, implement, and use than mutable classes. They are less prone to error and are more secure.. Кроме того , An immutable object can be in exactly one state, the state in which it was created.против Mutable objects, on the other hand, can have arbitrarily complex state spaces.. Исходя из моего личного опыта, использование ключевого слова finalдолжно подчеркнуть намерение разработчика склоняться к неизменности, а не к «оптимизации» кода. Я призываю вас прочитать эту главу, увлекательно!
Луи Ф.

2
Другие ответы показывают, что использование finalключевого слова для переменных может уменьшить количество байт-кода, что может повлиять на производительность.
Жюльен Кронегг

86

Короткий ответ: не беспокойся об этом!

Длинный ответ:

Когда говорим об окончательных локальных переменных имейте в виду, что использование ключевого слова finalпоможет компилятору статически оптимизировать код , что в конечном итоге может привести к более быстрому коду. Например, последние строки a + bв примере ниже сцепляются статически (во время компиляции).

public class FinalTest {

    public static final int N_ITERATIONS = 1000000;

    public static String testFinal() {
        final String a = "a";
        final String b = "b";
        return a + b;
    }

    public static String testNonFinal() {
        String a = "a";
        String b = "b";
        return a + b;
    }

    public static void main(String[] args) {
        long tStart, tElapsed;

        tStart = System.currentTimeMillis();
        for (int i = 0; i < N_ITERATIONS; i++)
            testFinal();
        tElapsed = System.currentTimeMillis() - tStart;
        System.out.println("Method with finals took " + tElapsed + " ms");

        tStart = System.currentTimeMillis();
        for (int i = 0; i < N_ITERATIONS; i++)
            testNonFinal();
        tElapsed = System.currentTimeMillis() - tStart;
        System.out.println("Method without finals took " + tElapsed + " ms");

    }

}

Результат?

Method with finals took 5 ms
Method without finals took 273 ms

Протестировано на Java Hotspot VM 1.7.0_45-b18.

Так сколько же фактическое улучшение производительности? Я не смею говорить. В большинстве случаев, вероятно, маргинальный (~ 270 наносекунд в этом синтетическом тесте, потому что объединение строк вообще исключается - редкий случай), но в сильно оптимизированном коде утилит это может быть фактором. В любом случае ответ на первоначальный вопрос - да, это может улучшить производительность, но в лучшем случае незначительно .

Помимо преимуществ времени компиляции, я не смог найти никаких доказательств того, что использование ключевого слова finalоказывает какое-либо измеримое влияние на производительность.


2
Я немного переписал ваш код, чтобы протестировать оба случая по 100 раз. В итоге среднее время финала составило 0 мс и 9 мс для нефинала. При увеличении счетчика итераций до 10M установите среднее значение 0 мс и 75 мс. Однако лучший результат для нефинала был 0 мс. Может быть, это результаты обнаружения VM просто выбрасываются или что-то? Я не знаю, но, тем не менее, использование final делает существенную разницу.
Каспер Фергеманд

5
Испорченный тест. Более ранние тесты согреют JVM и пойдут на пользу последующим вызовам теста. Измените порядок ваших тестов и посмотрите, что получится. Вам нужно запустить каждый тест в своем собственном экземпляре JVM.
Стив Куо

16
Нет, тест не ошибочен, разминка была учтена. Второй тест - МЕДЛЕННЫЙ, не быстрее. Без прогрева второй тест был бы ДАЖЕ МЕДЛЕННЫМ.
rustyx

6
В testFinal () все время возвращал один и тот же объект из пула строк, потому что повторное объединение финальных строк и конкатенации строк вычисляется во время компиляции. testNonFinal () каждый раз возвращает новый объект, что объясняет разницу в скорости.
anber

4
Что заставляет вас думать, что сценарий нереалистичен? Stringконкатенация - намного более дорогая операция, чем сложение Integers. Делать это статически (если возможно) более эффективно, это то, что показывает тест.
rustyx

62

Да, оно может. Вот пример, где final может повысить производительность:

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

учтите следующее:

public class ConditionalCompile {

  private final static boolean doSomething= false;

    if (doSomething) {
       // do first part. 
    }

    if (doSomething) {
     // do second part. 
    }

    if (doSomething) {     
      // do third part. 
    }

    if (doSomething) {
    // do finalization part. 
    }
}

Преобразовав атрибут doSomething в последний атрибут, вы сказали компилятору, что всякий раз, когда он видит doSomething, он должен заменить его на false в соответствии с правилами подстановки во время компиляции. Первый проход компилятора изменяет код на что-то вроде этого:

public class ConditionalCompile {

  private final static boolean doSomething= false;

    if (false){
       // do first part. 
    }

    if (false){
     // do second part. 
    }

    if (false){
      // do third part. 
    }

    if (false){
    // do finalization part. 

    }
}

Как только это будет сделано, компилятор еще раз посмотрит на него и обнаружит, что в коде есть недостижимые операторы. Поскольку вы работаете с высококачественным компилятором, ему не нравятся все эти недоступные байтовые коды. Таким образом это удаляет их, и вы в конечном итоге с этим:

public class ConditionalCompile {


  private final static boolean doSomething= false;

  public static void someMethodBetter( ) {

    // do first part. 

    // do second part. 

    // do third part. 

    // do finalization part. 

  }
}

таким образом уменьшая любые избыточные коды или любую ненужную условную проверку.

Изменить: В качестве примера, давайте возьмем следующий код:

public class Test {
    public static final void main(String[] args) {
        boolean x = false;
        if (x) {
            System.out.println("x");
        }
        final boolean y = false;
        if (y) {
            System.out.println("y");
        }
        if (false) {
            System.out.println("z");
        }
    }
}

При компиляции этого кода с Java 8 и декомпиляции с помощью javap -c Test.classмы получаем:

public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static final void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: ifeq          14
       6: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       9: ldc           #22                 // String x
      11: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      14: iconst_0
      15: istore_2
      16: return
}

Мы можем заметить, что скомпилированный код включает только не финальную переменную x. Это доказывает, что конечные переменные влияют на производительность, по крайней мере, для этого простого случая.


1
@ ŁukaszLech Я узнал об этом из книги Орейли: Hardcore Java, в их главе о последнем ключевом слове.
mel3kings

15
Это говорит об оптимизации во время компиляции, означает, что разработчик знает ЗНАЧЕНИЕ окончательной логической переменной во время компиляции, в чем весь смысл записи, если блоки на первом месте, тогда для этого сценария, где IF-условия не нужны и не делают любое чувство? На мой взгляд, даже если это повышает производительность, это неправильный код, в первую очередь, и может быть оптимизирован самим разработчиком, а не передавать ответственность компилятору, и вопрос в основном намеревается задать вопрос об улучшении производительности в обычных кодах, где используется final, который имеет программный смысл.
Бхавеш Агарвал

7
Смысл этого в том, чтобы добавить отладочные операторы, как заявили mel3kings. Вы можете перевернуть переменную перед производственной сборкой (или настроить ее в ваших скриптах сборки) и автоматически удалить весь этот код при создании дистрибутива.
Адам

37

Согласно IBM - это не для классов или методов.

http://www.ibm.com/developerworks/java/library/j-jtp04223.html


1
... и, по словам IBM, это относится к полям : ibm.com/developerworks/java/library/j-jtp1029/… - и также рекламируется как лучшая практика.
Филзен

2
Статья "04223" написана в 2003 году. Сейчас ей почти семнадцать лет. Это была ... Java 1.4?
dmatej

16

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

Для справки это было проверено на соответствие javacверсии 8, 9и10 .

Предположим, этот метод:

public static int test() {
    /* final */ Object left = new Object();
    Object right = new Object();

    return left.hashCode() + right.hashCode();
}

Компиляция этого кода , как она есть, производит точно такой же байт - код , как когда finalбы присутствовал (final Object left = new Object(); ).

Но этот:

public static int test() {
    /* final */ int left = 11;
    int right = 12;
    return left + right;
}

Производит:

   0: bipush        11
   2: istore_0
   3: bipush        12
   5: istore_1
   6: iload_0
   7: iload_1
   8: iadd
   9: ireturn

Уход, finalчтобы присутствовать, производит:

   0: bipush        12
   2: istore_1
   3: bipush        11
   5: iload_1
   6: iadd
   7: ireturn

Код в значительной степени не требует пояснений, в случае, если есть постоянная времени компиляции, он будет загружен непосредственно в стек операндов (он не будет сохранен в массив локальных переменных, как в предыдущем примере через bipush 12; istore_0; iload_0 ), что имеет смысл так как никто не может изменить это.

С другой стороны, почему во втором случае компилятор не выдает, istore_0 ... iload_0это за мной, это не похоже на этот слот0 используется каким-либо образом (он может уменьшить массив переменных таким образом, но может быть, я пропускаю некоторые внутренние детали, не могу расскажу наверняка)

Я был удивлен, увидев такую ​​оптимизацию, учитывая, как это javacделают маленькие . Что мы должны всегда использовать final? Я даже не собираюсь писать JMHтест (который я хотел изначально), я уверен, что различия в порядке ns(если это вообще возможно). Единственное место, где это может быть проблемой, - это когда метод не может быть встроен из-за его размера (и объявленияfinal уменьшит этот размер на несколько байтов).

Есть еще два finalвопроса, которые необходимо решить. Во-первых, когда метод finalJITточки зрения), такой метод является мономорфным - и это самые любимые изJVM .

Тогда есть finalпеременные экземпляра (которые должны быть установлены в каждом конструкторе); они важны, так как они гарантируют правильно опубликованную ссылку, о которой мы немного поговорили, а также указано именно JLS.


Я скомпилировал код с использованием Java 8 (JDK 1.8.0_162) с параметрами отладки ( javac -g FinalTest.java), декомпилировал код с использованием javap -c FinalTest.classи не получил те же результаты (с final int left=12, я получил bipush 11; istore_0; bipush 12; istore_1; bipush 11; iload_1; iadd; ireturn). Так что да, сгенерированный байт-код зависит от множества факторов, и трудно сказать, оказывает ли finalэто влияние на производительность или нет. Но так как байт-код отличается, может существовать некоторая разница в производительности.
Жюльен Кронегг


13

Вы действительно спрашиваете о двух (как минимум) разных случаях:

  1. final для локальных переменных
  2. final для методов / классов

Джон Скит уже ответил 2). О 1):

Я не думаю, что это имеет значение; для локальных переменных компилятор может определить, является ли переменная конечной или нет (просто проверяя, назначена ли она более одного раза). Поэтому, если компилятор хочет оптимизировать переменные, которые назначаются только один раз, он может это сделать независимо от того, объявлена ​​ли переменная на самом деле finalили нет.

final мощь иметь значение для полей защищенного / публичного класса; там компилятору очень трудно узнать, задано ли поле более одного раза, как это может происходить из другого класса (который, возможно, даже не был загружен). Но даже тогда JVM могла бы использовать технику, описанную Джоном (оптимистично оптимизировать, вернуться, если загружен класс, который меняет поле).

Таким образом, я не вижу никаких причин, почему это должно помочь производительности. Так что этот вид микрооптимизации вряд ли поможет. Вы можете попробовать сравнить его, чтобы убедиться, но я сомневаюсь, что это будет иметь значение.

Редактировать:

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


Я не думаю, что компилятор может правильно проверить, сколько раз назначается локальная переменная: как насчет if-then-else структур с многочисленными присваиваниями?
gyorgyabraham

1
@gyabraham: компилятор уже проверяет эти случаи, если вы объявляете локальную переменную как final, чтобы убедиться, что вы не назначаете ее дважды. Насколько я вижу, ту же логику проверки можно (и, вероятно, использовать) использовать для проверки, может ли быть переменная final.
слеське

Конечность локальной переменной не выражается в байт-коде, поэтому JVM даже не знает, что она окончательная
Стив Куо,

1
@SteveKuo: Даже если это не выражено в байт-коде, это может помочь javacлучше оптимизировать. Но это всего лишь домыслы.
слеске

1
Компилятор может выяснить, была ли локальная переменная назначена ровно один раз, но на практике это не так (за исключением проверки ошибок). С другой стороны, если finalпеременная имеет примитивный тип или тип Stringи ей немедленно присваивается константа времени компиляции, как в примере вопроса, компилятор должен встроить ее, поскольку переменная является константой времени компиляции для спецификации. Но для большинства случаев использования код может выглядеть иначе, но все равно не имеет значения, является ли константа встроенной или считываемой из локальной переменной с точки зрения производительности.
Хольгер

6

Примечание: не эксперт по Java

Если я правильно запомнил java, было бы очень мало способов улучшить производительность, используя ключевое слово final. Я всегда знал, что он существует для «хорошего кода» - дизайна и читабельности.


1

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


1

На самом деле, во время тестирования некоторого кода, связанного с OpenGL, я обнаружил, что использование модификатора final в приватном поле может снизить производительность. Вот начало класса, который я тестировал:

public class ShaderInput {

    private /* final */ float[] input;
    private /* final */ int[] strides;


    public ShaderInput()
    {
        this.input = new float[10];
        this.strides = new int[] { 0, 4, 8 };
    }


    public ShaderInput x(int stride, float val)
    {
        input[strides[stride] + 0] = val;
        return this;
    }

    // more stuff ...

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

public static void test4()
{
    int arraySize = 10;
    float[] fb = new float[arraySize];
    for (int i = 0; i < arraySize; i++) {
        fb[i] = random.nextFloat();
    }
    int times = 1000000000;
    for (int i = 0; i < 10; ++i) {
        floatVectorTest(times, fb);
        arrayCopyTest(times, fb);
        shaderInputTest(times, fb);
        directFloatArrayTest(times, fb);
        System.out.println();
        System.gc();
    }
}

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

Simple array copy took   : 02.64
System.arrayCopy took    : 03.20
ShaderInput took         : 00.77
Unsafe float array took  : 05.47

С последним ключевым словом:

Simple array copy took   : 02.66
System.arrayCopy took    : 03.20
ShaderInput took         : 02.59
Unsafe float array took  : 06.24

Обратите внимание на цифры для теста ShaderInput.

Не имело значения, сделал ли я поля открытыми или закрытыми.

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

Также System.arrayCopy, по-видимому, на самом деле несколько медленнее для небольших массивов, чем простое копирование элементов из одного массива в другой в цикле for. И использование sun.misc.Unsafe (а также прямой java.nio.FloatBuffer, не показанный здесь) работает ужасно.


1
Вы забыли сделать параметры окончательными. <pre> <code> public ShaderInput x (final int stride, final float val) {input [strides [stride] + 0] = val; верни это; } </ code> </ pre> По моему опыту, выполнение любой переменной или финала поля действительно может повысить производительность.
Anticro

1
Да, а также сделайте остальные тоже финальными: <pre> <code> final int arraySize = 10; final float [] fb = new float [arraySize]; for (int i = 0; i <arraySize; i ++) {fb [i] = random.nextFloat (); } final int times = 1000000000; for (int i = 0; i <10; ++ i) {floatVectorTest (times, fb); arrayCopyTest (times, fb); shaderInputTest (times, fb); directFloatArrayTest (times, fb); System.out.println (); System.gc (); } </ code> </ pre>
Anticro

1

Final (по крайней мере, для переменных и параметров членов) больше для людей, чем для машины.

Хорошей практикой является сделать переменные окончательными, где это возможно. Я бы хотел, чтобы Java по умолчанию делала final с переменными и имела ключевое слово Mutable, чтобы разрешить изменения. Неизменяемые классы приводят к гораздо лучшему многопоточному коду, и просто взгляд на класс с «final» перед каждым членом быстро покажет его неизменность.

Другой случай - я конвертировал много кода для использования аннотаций @ NonNull / @ Nullable (Вы можете сказать, что параметр метода не должен быть нулевым, тогда IDE может предупреждать вас всякий раз, когда вы передаете переменную, которая не помечена @ NonNull - все это распространяется до нелепой степени). Гораздо проще доказать, что переменная-член или параметр не могут иметь значение null, если они помечены как final, поскольку вы знаете, что они не будут переназначаться где-либо еще.

Мое предложение состоит в том, чтобы привыкнуть применять final для элементов и параметров по умолчанию. Это всего лишь несколько символов, но это подтолкнет вас к улучшению стиля кодирования, если ничего больше.

Final для методов или классов - это еще одна концепция, поскольку она запрещает очень правильную форму повторного использования и на самом деле мало что говорит читателю. Лучшее использование, вероятно, - это то, как они сделали String и другие встроенные типы окончательными, чтобы вы могли повсеместно полагаться на согласованное поведение - это предотвратило много ошибок (хотя бывали случаи, когда я ЛЮБИЛ, чтобы расширять строку .... о возможности)


0

Как упоминалось в другом месте, 'final' для локальной переменной и, в несколько меньшей степени, переменной-члена, больше зависит от стиля.

'final' - это утверждение, что вы намерены изменить переменную (т. е. переменная не будет меняться!). Затем компилятор может помочь вам, подав жалобу, если вы нарушаете собственное ограничение.

Я разделяю мнение, что Java был бы лучшим языком, если бы идентификаторы (извините, я просто не могу назвать неизменную вещь «переменная») были окончательными по умолчанию и требовали от вас явного указания, что они являются переменными. Но, сказав это, я обычно не использую 'final' для локальных переменных, которые инициализируются и никогда не назначаются; это просто кажется слишком шумным.

(Я использую final для переменных-членов)


-3

Члены, объявленные с помощью final, будут доступны по всей программе, потому что, в отличие от нефинальных, если эти члены не использовались в программе, сборщик мусора не позаботится о них, что может вызвать проблемы с производительностью из-за плохого управления памятью.


2
Что это за ерунда? У вас есть источник, почему вы думаете, что это так?
Дж. Доу

-4

final Ключевое слово может быть использовано в Java пятью способами.

  1. Урок окончательный
  2. Ссылочная переменная является окончательной
  3. Локальная переменная является окончательной
  4. Метод является окончательным

Класс final: класс final означает, что мы не можем расширяться или наследование означает, что наследование невозможно.

Точно так же - объект является конечным: некоторое время мы не изменяли внутреннее состояние объекта, поэтому в таком случае мы можем указать, что объект является конечным объектом. Объект final означает, что переменная также не окончательная.

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


2
Выражение «объект окончательный» означает «объект неизменен».
gyorgyabraham

6
Вы правы, но вы не отвечаете на вопрос. ОП не спрашивал, что finalзначит, но влияет ли использование finalна производительность.
Хонза Зидек
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.