#java #final
Без всяких лишних слов напишу код: public class A { static Object f() { String str = "hello"; str = "world"; // (1) ошибка компиляции! class X extends Object { public String toString() { return str; } } return new X(); } static Object g() { String[] s = { "hello", "silence" }; s[0] = "World"; // (2) НЕТ ошибки компиляции. class X extends Object{ public String toString() { return s[0]; } } return new X(); } public static void main(String[] args) { } } Закомментирование строки (1) даст компилятору понять, что str в f() есть effectively final, код скомпилируется. Закомментирование строки (2) не нужно. Ведь s -- ссылка на массив, которая не изменяется. Взятие s[0] затем законно. Вопрос: Зачем от нас требую использование только final или effectively final локальных переменных в локальном классе? Я могу работать с первой ячейкой массива s и всё у меня будет замечательно. Мои мысли и непонятки: как вообще будет выглядеть код объекта класса f().X ? Это будет объект с кодом из Object, но его метод toString() будет таким (псевдокод): public String toString() { return 0x47e9c2dd; } Где, как понимаю, 0x47e9c2dd адрес той самой строки str? Но если бы оно работало так, то какая разница какой адрес мы бы положили в этот метод, когда создавали экземпляр класса f().X ... Так же не понимаю: мы определяем метод toString() класса f().X ссылаясь на локальную переменную str, которая (как ссылка) исчезнет по завершению работы f(), но при том объект возвращённый из f() содержит в своём toString() ссылку на str (как объект). Значит у нас тратится RAM на то, чтобы держать метод f().X.toString() определённым?.... В общем я очень запутался, совершенно не понимаю происходящего с этими локальными классами в том свете, что на нас возложено это странное требование на final или effectively final. Объясните происходящее, пожалуйста...
Ответы
Ответ 1
В Java замыкания захватывают значения, а не переменные. При компиляции внутреннего класса, Java создаст в нём поле для хранения значения переменной, "захваченной" из внешней области видимости: $ javac A.java $ javap A$1X Compiled from "A.java" class A$1X { final java.lang.String val$str; // <-- "hello" A$1X(); public java.lang.String toString(); } У требования к неизменяемости обеих переменных есть несколько причин. Уверен, самым главным было то, что Java позиционируется как язык, в корне пресекающий возможность совершения множества ошибок и вынуждающий программистов писать правильный код. А классические замыкания - это не самая простая и интуитивно понятная тема. Поэтому в Java замыкания сознательно ограничили и сделали более простыми. Кроме того, если реализовывать классические замыкания, то необходимо было бы каким-то образом сохранять захваченную переменную. Например, выделять место в куче. Во-первых, это потребовало бы доработки виртуальной машины и, возможно, изменения байт-кода. Во-вторых, это повод для утечек памяти. Можно было бы сделать поле val$str изменяемым, но это усложнило бы и замедлило использование анонимных классов и лямбда-выражений в многопоточной среде. Можно было бы дать программисту возможность выбирать, когда это поле финальное, а когда нет, но тогда оно перестаёт быть неявным и мы опять приходит к необходимости доработки виртуальной машины, усложнению языка и поводам для ошибок. Можно было бы не ограничивать изменяемость переменной из внешней области видимости, но на уровне языка переменная str одна, вероятность того, что в разных областях видимости она может иметь разные значения - это ещё больший повод для ошибок, чем классические замыкания. Подытоживая, ограничение на неизменяемость переменных одновременно сделало и язык лучше, и техническую реализацию замыканий проще. Кое-что об этом можно почитать у Брайана Гетца в "State of the Lambda: Variable capture".
Комментариев нет:
Отправить комментарий