Улучшает ли использование ключевое слово final в Java?
В Java мы видим много мест, где final
ключевое слово может быть использовано, но его использование редко.
Например:
String str = "abc";
System.out.println(str);
В приведенном выше случае str
может быть final
но это обычно прекращается.
Когда метод никогда не будет переопределен, мы можем использовать ключевое слово final. Аналогично в случае класса, который не будет наследоваться.
Действительно ли использование ключевого слова final в любом или во всех этих случаях действительно повышает производительность? Если так, то как? Пожалуйста, объясни. Если правильное использование final
действительно важно для производительности, какие привычки должен развивать Java-программист, чтобы наилучшим образом использовать ключевое слово?
16 ответов
Обычно нет. Для виртуальных методов HotSpot отслеживает, был ли метод на самом деле переопределен, и может выполнять оптимизацию, такую как встраивание, в предположении, что метод не был переопределен - до тех пор, пока он не загрузит класс, который переопределяет метод, и в этот момент он может отменить (или частично отменить) эти оптимизации.
(Конечно, это предполагает, что вы используете HotSpot - но это, безусловно, самая распространенная JVM, так что...)
На мой взгляд, вы должны использовать final
основанный на ясном дизайне и удобочитаемости, а не по соображениям производительности. Если вы хотите что-то изменить по соображениям производительности, вы должны выполнить соответствующие измерения, прежде чем изгибать самый чистый код из формы - таким образом, вы можете решить, стоит ли какая-либо дополнительная достигнутая производительность в худшей читаемости / дизайне. (По моему опыту это почти никогда не стоит; YMMV.)
РЕДАКТИРОВАТЬ: Поскольку последние поля были упомянуты, стоит упомянуть, что они в любом случае являются хорошей идеей с точки зрения четкого дизайна. Они также изменяют гарантированное поведение с точки зрения видимости между потоками: после завершения конструктора все конечные поля гарантированно сразу же становятся видимыми в других потоках. Это, вероятно, наиболее распространенное использование final
по моему опыту, хотя в качестве сторонника эмпирического правила Джоша Блоха "дизайн для наследования или запретить его", я, вероятно, должен использовать final
чаще для занятий...
Короткий ответ: не беспокойся об этом!
Длинный ответ:
Говоря об окончательных локальных переменных, имейте в виду, что используя ключевое слово 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
оказывает какое-либо измеримое влияние на производительность.
Да, оно может. Вот пример, где 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
, Это доказывает, что конечные переменные влияют на производительность, по крайней мере, для этого простого случая.
По словам IBM - это не для классов или методов.
http://www.ibm.com/developerworks/java/library/j-jtp04223.html
Я поражен тем, что на самом деле никто не опубликовал какой-то реальный код, который был декомпилирован, чтобы доказать, что есть хоть какая-то незначительная разница.
Для справки это было проверено против 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
С этим нужно бороться. Во-первых, когда методfinal
(из JIT
в перспективе), такой метод мономорфен - и это самые любимые JVM
,
Тогда естьfinal
переменные экземпляра (которые должны быть установлены в каждом конструкторе); они важны, так как они гарантируют правильно опубликованную ссылку, о чем здесь немного говорится, а также указанную в JLS
,
Вы действительно спрашиваете о двух (как минимум) разных случаях:
final
для локальных переменныхfinal
для методов / классов
Джон Скит уже ответил 2). О 1):
Я не думаю, что это имеет значение; для локальных переменных компилятор может определить, является ли переменная конечной или нет (просто проверяя, назначена ли она более одного раза). Поэтому, если компилятор хочет оптимизировать переменные, которые назначаются только один раз, он может сделать это независимо от того, объявлена ли переменная на самом деле final
или нет.
final
может иметь значение для полей защищенного / публичного класса; там компилятору очень трудно узнать, задано ли поле более одного раза, как это может происходить из другого класса (который, возможно, даже не был загружен). Но даже тогда JVM могла бы использовать технику, описанную Джоном (оптимистично оптимизировать, вернуться, если загружен класс, который меняет поле).
Таким образом, я не вижу причин, почему это должно помочь производительности. Так что этот вид микрооптимизации вряд ли поможет. Вы можете попробовать сравнить его, чтобы убедиться, но я сомневаюсь, что это будет иметь значение.
Редактировать:
На самом деле, согласно ответу Тимо Весткемпера, final
может улучшить производительность для полей класса в некоторых случаях. Я стою исправлено.
Примечание: не эксперт по Java
Если я правильно помню мой Java, было бы очень мало способов улучшить производительность, используя ключевое слово final. Я всегда знал, что он существует для "хорошего кода" - дизайна и читабельности.
В Java мы делаем вещи неизменяемыми с помощью ключевого слова, и есть по крайней мере 3 способа, которыми неизменяемость может реально повлиять на производительность кода. Эти 3 пункта имеют общее происхождение в том, что компилятор или разработчик делают лучшие предположения:
- Более надежный код
- Более производительный код
- Более эффективное выделение памяти и сборка мусора
Более надежный код
Как указано во многих других ответах и комментариях, неизменяемость классов приводит к более чистому и удобному в сопровождении коду, а неизменяемость объектов упрощает их обработку, поскольку они могут находиться ровно в одном состоянии, поэтому это приводит к упрощению параллелизма и оптимизации. времени, необходимого для выполнения задач.
Кроме того, компилятор предупредит вас об использовании неинициализированной переменной и не позволит вам переназначить ее с новым значением.
Если мы говорим о параметрах метода, их объявление вызовет жалобу компилятора, если вы случайно используете одно и то же имя для переменной или переназначаете ее значение (делая параметр недоступным).
Более производительный код
Простой анализ сгенерированного байт-кода должен поставить последний мир перед вопросом о производительности: используя минимально измененную версию кода, опубликованную @rustyx в его ответе, вы можете увидеть, что сгенерированный байт-код отличается, когда компилятор знает, что объекты победят. t мутируют их значение.
Это код:
public class FinalTest {
private static final int N_ITERATIONS = 1000000;
private static String testFinal() {
final String a = "a";
final String b = "b";
return a + b;
}
private static String testNonFinal() {
String a = "a";
String b = "b";
return a + b;
}
private static String testSomeFinal() {
final String a = "a";
String b = "b";
return a + b;
}
public static void main(String[] args) {
measure("testFinal", FinalTest::testFinal);
measure("testSomeFinal", FinalTest::testSomeFinal);
measure("testNonFinal", FinalTest::testNonFinal);
}
private static void measure(String testName, Runnable singleTest){
final long tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
singleTest.run();
final long tElapsed = System.currentTimeMillis() - tStart;
System.out.printf("Method %s took %d ms%n", testName, tElapsed);
}
}
компиляция с помощью openjdk17:javac FinalTest.java
затем декомпиляция:javap -c -p FinalTest.class
привести к этому байт-коду:
private static java.lang.String testFinal();
Code:
0: ldc #7 // String ab
2: areturn
private static java.lang.String testNonFinal();
Code:
0: ldc #9 // String a
2: astore_0
3: ldc #11 // String b
5: astore_1
6: aload_0
7: aload_1
8: invokedynamic #13, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: areturn
private static java.lang.String testSomeFinal();
Code:
0: ldc #11 // String b
2: astore_0
3: aload_0
4: invokedynamic #17, 0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
9: areturn
// omitted bytecode for the measure() method, which is not interesting
Как видите, бывают случаи, когдаfinal
ключевое слово имеет значение.
Для полноты это измеренное время:
Метод testFinal занял 5 мс
Метод testSomeFinal занял 13 мс
Метод testNonFinal занял 20 мс
Эти времена кажутся несущественными (учитывая, что мы выполнили 1 миллион задач), но я думаю, что через некоторое время JIT-оптимизация творит чудеса и сглаживает различия, но даже с учетом сказанного, 4x не так уж и незначительна, учитывая что, когда дело доходит доtestNonFinal
В свою очередь, JVM разогрета предыдущими тестами, а общий код еще предстоит оптимизировать.
Упрощенное встраивание
Меньшее количество байт-кода также приводит к более простому и короткому встраиванию, а значит, к лучшему использованию ресурсов и повышению производительности.
Встроенные устройства
Разработчики Java потенциально могут писать код, который работает на серверах, настольных компьютерах и небольших или встроенных устройствах, поэтому повышение эффективности кода во время компиляции (и не полностью полагаться на оптимизацию JVM) может сэкономить память, время и энергию во всех средах выполнения и привести к меньше проблем с параллелизмом и ошибок.
Более эффективное выделение памяти и сборка мусора
Если у объектов есть окончательные или неизменяемые поля, их состояние не может измениться, а требуемая им память легче оценивается при их создании (что приводит к меньшему количеству перемещений) и требует меньше защитных копий: в геттере я могу напрямую поделиться неизменяемым объект, не создавая защитную копию.
Наконец, есть еще один момент о будущих возможностях: когда проект Valhalla увидит солнечный свет и станут доступны «классы значений», применение неизменности к полям объектов будет значительным упрощением для тех, кто хочет их использовать, и воспользоваться преимуществами многочисленных JIT. -оптимизация компилятора, которая могла выйти.
Личное примечание о неизменности
Если бы переменные, свойства объектов и параметры методов были неизменяемыми по умолчанию в Java (как в Rust), разработчикам пришлось бы писать более чистый и более производительный код и явно объявлятьmutable
все объекты, которые могут изменять свое значение, сделали бы разработчиков более осведомленными о возможных ошибках.
Я не знаю, если дляfinal class
es было бы то же самое, потому чтоmutable class
не звучит для меня так осмысленно
Как упоминалось в другом месте, "final" для локальной переменной и, в несколько меньшей степени, для переменной-члена - это скорее вопрос стиля.
"final" - это утверждение, что вы не намерены изменять переменную (т.е. переменная не будет изменяться!). Затем компилятор может помочь вам, пожаловавшись, если вы нарушите собственное ограничение.
Я разделяю мнение, что Java была бы лучшим языком, если бы идентификаторы (извините, я просто не могу назвать неизменяющуюся вещь "переменной") по умолчанию окончательными и потребовали бы, чтобы вы явно указали, что они были переменными. Но, сказав это, я обычно не использую final для локальных переменных, которые инициализируются и никогда не назначаются; это кажется слишком шумным.
(Я использую final для переменных-членов)
Final (по крайней мере, для переменных-членов и параметров) больше подходит для людей, чем для машины.
По возможности рекомендуется делать переменные final. Я бы хотел, чтобы Java по умолчанию сделала "переменные" final и имела ключевое слово "Mutable", разрешающее изменения. Неизменяемые классы приводят к гораздо лучшему многопоточному коду, и простой взгляд на класс с "final" перед каждым членом быстро покажет, что он неизменяемый.
Другой случай - я преобразовывал много кода для использования аннотаций @NonNull/@Nullable (вы можете сказать, что параметр метода не должен быть нулевым, тогда IDE может предупреждать вас каждый раз, когда вы передаете переменную, которая не помечена @NonNull- все дело до смешного разносится). Гораздо проще доказать, что переменная-член или параметр не могут иметь значение null, если они помечены как final, поскольку вы знаете, что они не переназначаются где-либо еще.
Мое предложение - выработать привычку применять final для членов и параметров по умолчанию. Это всего лишь несколько символов, но это подтолкнет вас к улучшению вашего стиля кодирования, если ничего больше.
Final для методов или классов - это еще одна концепция, поскольку она запрещает очень допустимую форму повторного использования и мало что говорит читателю. Лучшее использование, вероятно, заключается в том, как они сделали String и другие встроенные типы final, чтобы вы могли полагаться на согласованное поведение повсюду - это предотвратило множество ошибок (хотя бывают случаи, когда мне бы очень понравилось расширять строку.... возможности)
На самом деле, во время тестирования некоторого кода, связанного с OpenGL, я обнаружил, что использование финального модификатора в закрытом поле может снизить производительность. Вот начало класса, который я тестировал:
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();
}
}
После 3-й итерации с разогретой виртуальной машиной я постоянно получал эти цифры без последнего ключевого слова:
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, не показанный здесь) работает ужасно.
Я не эксперт, но я полагаю, вы должны добавить final
ключевое слово для класса или метода, если оно не будет перезаписано и оставит переменные в покое. Если будет какой-либо способ оптимизировать такие вещи, компилятор сделает это за вас.
Определенно да, если переменная преобразуется в константу,
как мы знаем, java-компилятор конвертирует такие конечные переменные в константу
как концепция константного компилятора Java напрямую заменить значение его ссылкой во время компиляции
в Java переменные идут как константа, если это строка или примитивный тип, который предоставляется без какого-либо процесса выполнения
в противном случае это просто окончательная (неизменяемая) переменная,
& Постоянное использование всегда быстрее, чем ссылка.
поэтому, если возможно, используйте константы в любом языке программирования для повышения производительности
Я не могу сказать, что это редкость, потому что, насколько я знаю, это единственный способ объявить константу в java. Как разработчик JavaScript, я знаю, насколько важна константа ключевого слова. Если вы работаете на производственном уровне, и у вас есть значения, которые никогда не могут быть изменены даже случайно другими кодировщиками, такие значения, как номер SSN или даже имена. Затем вы должны использовать ключевое слово final для объявления переменных. Иногда это может быть очень проблематично, если определенные классы могут быть унаследованы. Потому что, если в команде работает много людей, кто-то может наследовать класс, расширить его и даже внести изменения в переменные и методы родительского класса. Это можно остановить с помощью ключевого слова final, потому что даже статические переменные могут быть изменены, если не используется ключевое слово final.
final
Ключевое слово может быть использовано в Java пятью способами.
- Урок окончательный
- Ссылочная переменная является окончательной
- Локальная переменная является окончательной
- Метод является окончательным
Класс final: класс final означает, что мы не можем быть расширены, или наследование означает, что наследование невозможно.
Точно так же - объект является конечным: некоторое время мы не изменяли внутреннее состояние объекта, поэтому в этом случае мы можем указать, что объект является конечным объектом. Объект final означает не переменную, а также final.
Как только ссылочная переменная становится окончательной, она не может быть переназначена другому объекту. Но может изменить содержимое объекта, пока его поля не являются окончательными
Члены, объявленные с помощью final, будут доступны по всей программе, поскольку, в отличие от нефинальных, если эти члены не использовались в программе, сборщик мусора не позаботится о них, что может привести к проблемам с производительностью из-за плохого управления памятью.