Доступ к локальной переменной или параметру является одношаговой операцией: возьмите переменную, расположенную со смещением N в стеке. Если ваша функция имеет 2 аргумента (упрощенно):
- N = 0 -
this
- N = 1 - первый аргумент
- N = 2 - второй аргумент
- N = 3 - первая локальная переменная
- N = 4 - вторая локальная переменная
- ...
Так, когда вы обращаетесь к локальной переменной, у вас есть один доступ к памяти с фиксированным смещением (N известно во время компиляции). Это байт-код для доступа к первому аргументу метода (int
):
iload 1 //N = 1
Однако, когда вы получаете доступ к полю, вы фактически делаете дополнительный шаг. Сначала вы читаете « локальная переменная » this
просто для определения текущего адреса объекта. Затем вы загружаете поле (getfield
), которое имеет фиксированное смещение от this
. Таким образом, вы выполняете две операции с памятью вместо одной (или одной дополнительной). Bytecode:
aload 0 //N = 0: this reference
getfield total I //int total
Таким образом, технический доступ к локальным переменным и параметрам происходит быстрее, чем к объектным полям. На практике на производительность могут влиять многие другие факторы (в том числе различные уровни оптимизации кэша ЦП и JVM).
final
- это другая история. Это в основном подсказка для компилятора / JIT, что эта ссылка не изменится, поэтому она может сделать более тяжелые оптимизации. Но это гораздо сложнее отследить, как правило, при большом пальце руки final
всегда, когда это возможно.