- Как Java 8 поддерживается в Android
- Лямбды
- История десахаризации
- Преобразование исходников — Source Transformation
- Нативная поддержка лямбд
- Method References
- Default и static методы в интерфейсах
- Может, просто использовать Kotlin?
- Desugaring APIs
- Эпилог
- Это должен знать каждый
- 1 ноября 2014 г. Особенности Java с точки зрения Android-разработчика
- Immutable class и разница между String, StringBuffer/StringBuilder
- Класс String
- StringBuffer/StringBuilder
- Почему не стоит использовать AsyncTask при работе с сетью
- Нестатические блоки инициализации
- Вложенные в интерфейсы классы
- Модификация данных из внутренних классов
- Конфликт имён
- Инициализация коллекций
- Выход из любого блока операторов
- Подсписки
- Cafe babe
Как Java 8 поддерживается в Android
Привет, Хабр! Предлагаю вашему вниманию перевод замечательной статьи из цикла статей небезызвестного Джейка Вортона о том, как происходит поддержка Андроидом Java 8.
Оригинал статьи лежит тут
Несколько лет я работал из дома, и мне часто приходилось слышать, как мои коллеги жалуются на поддержку Андроидом разных версий Java.
Это довольно сложная тема. Для начала нужно определиться, что мы вообще подразумеваем под «поддержкой Java в Android», ведь в одной версии языка может быть много всего: фичи (лямбды, например), байткод, тулзы, APIs, JVM и так далее.
Когда говорят о поддержке Java 8 в Android, обычно подразумевают поддержку фичей языка. Итак, начнем с них.
Лямбды
Одним из главных нововведений Java 8 были лямбды.
Код стал более лаконичным и простым, лямбды избавили нас от необходимости писать громоздкие анонимные классы, используя интерфейс с единственным методом внутри.
После компиляции этого, используя javac и легаси dx tool , мы получим следующую ошибку:
Эта ошибка происходит из-за того, что лямбды используют новую инструкцию в байткоде — invokedynamic , которая была добавлена в Java 7. Из текста ошибки можно увидеть, что Android поддерживает ее только начиная с 26 API (Android 8).
Звучит не очень, ведь вряд ли кто-то будет выпускать приложение с 26 minApi. Чтобы это обойти, используется так называемый процесс десахаризации (desugaring), который делает возможным поддержку лямбд на всех версиях API.
История десахаризации
Она довольно красочна в мире Android. Цель десахаризации всегда одна и та же — позволить новым языковым фичам работать на всех устройствах.
Изначально, например, для поддержки лямбд в Android разработчики подключали плагин Retrolambda. Он использовал тот же встроенный механизм, что и JVM, конвертируя лямбды в классы, но делал это в рантайме, а не во время компиляции. Сгенерированные классы были очень дорогими с точки зрения количества методов, но со временем, после доработок и улучшений, этот показатель снизился до чего-то более-менее разумного.
Затем команда Android анонсировала новый компилятор, который поддерживал все фичи Java 8 и был более производительным. Он был построен поверх Eclipse Java компилятора, но вместо генерации Java-байткода генерировал Dalvik-байткод. Однако его производительность все равно оставляла желать лучшего.
Когда новый компилятор (к счастью) забросили, трансформатор Java байткода в Java байткод, который и выполнял дешугаринг, был интегрирован в Android Gradle Plugin из Bazel — системы сборки Google. И его производительность все равно была невелика, поэтому параллельно продолжался поиск более хорошего решения.
И вот нам представили новый dexer — D8, который должен был заменить dx tool . Десахаризация теперь выполнялась во время конвертации скомпилированных JAR-файлов в .dex (dexing). D8 сильно выигрывает в производительности по сравнению с dx , и, начиная с Android Gradle Plugin 3.1 он стал dexer’ом по умолчанию.
Теперь, используя D8, у нас получится скомпилировать приведенный выше код.
Чтобы посмотреть, как D8 преобразовал лямбду, можно использовать dexdump tool , который входит в Android SDK. Она выведет довольно много всего, но мы заострим внимание только на этом:
Если вы до этого еще не читали байткод, не волнуйтесь: многое из того, что здесь написано, можно понять интуитивно.
В первом блоке наш main метод с индексом 0000 получает ссылку от поля INSTANCE на класс Java8$1 . Этот класс был сгенерирован во время десахаризации . Байткод метода main тоже нигде не содержит упоминаний о теле нашей лямбды, поэтому, скорее всего, она связана с классом Java8$1 . Индекс 0002 затем вызывает static-метод sayHi , используя ссылку на INSTANCE . Методу sayHi требуется Java8$Logger , поэтому, похоже, Java8$1 имплементирует этот интерфейс. Мы можем убедиться в этом тут:
Флаг SYNTHETIC означает, что класс Java8$1 был сгенерирован и список интерфейсов, которые он включает, содержит Java8$Logger .
Этот класс и представляет собой нашу лямбду. Если вы посмотрите на реализацию метода log , то не увидите тело лямбды.
Вместо этого внутри вызывается static метод класса Java8 — lambda$main$0 . Повторюсь, этот метод представлен только в байткоде.
Флаг SYNTHETIC снова говорит нам, что этот метод был сгенерирован, и его байткод как раз содержит тело лямбды: вызов System.out.println . Причина, по которой тело лямбды находится внутри Java8.class, простая — ей может понадобиться доступ к private членам класса, к которым сгенерированный класс иметь доступа не будет.
Все, что нужно для понимания того, как работает десахаризация, описано выше. Однако, взглянув на это в байткоде Dalvik, можно увидеть, что там все намного более сложно и пугающе.
Преобразование исходников — Source Transformation
Чтобы лучше понимать, как происходит десахаризация, давайте попробуем шаг за шагом преобразовывать наш класс в то, что будет работать на всех версиях API.
Возьмем за основу тот же класс с лямбдой:
Сначала тело лямбды перемещается в package private метод.
Затем генерируется класс, имплементирующий интерфейс Logger , внутри которого выполняется блок кода из тела лямбды.
Далее создается синглтон инстанс Java8$1 , который хранится в static переменной INSTANCE .
Вот итоговый задешугаренный класс, который может использоваться на всех версиях API:
Если вы посмотрите на сгенерированный класс в байткоде Dalvik, то не найдете имен по типу Java8$1 — там будет что-то вроде -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY . Причина, по которой для класса генерируется такой нейминг, и в чем его плюсы, тянет на отдельную статью.
Нативная поддержка лямбд
Когда мы использовали dx tool , чтобы скомпилировать класс, содержащий лямбды, сообщение об ошибке говорило, что это будет работать только с 26 API.
Поэтому кажется логичным, что если мы попробуем скомпилировать это с флагом —min-api 26 , то десахаризации происходить не будет.
Однако если мы сдампим .dex файл, то в нем все равно можно будет обнаружить -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY . Почему так? Это баг D8?
Чтобы ответить на этот вопрос, а также почему десахаризация происходит всегда, нам нужно заглянуть внутрь Java-байткода класса Java8 .
Внутри метода main мы снова видим invokedynamic по индексу 0 . Второй аргумент в вызове — 0 — индекс ассоциируемого с ним bootstrap метода.
Вот список bootstrap методов:
Здесь bootstrap метод назван metafactory в классе java.lang.invoke.LambdaMetafactory . Он живет в JDK и занимается созданием анонимных классов налету (on-the-fly) в рантайме для лямбд так же, как и D8 генерит их в компайлтайме.
Если взглянуть на документацию Android к java.lang.invoke
или на исходники AOSP к java.lang.invoke , увидим, что в рантайме этого класса нет. Вот поэтому дешугаринг всегда происходит во время компиляции, независимо от того, какой у вас minApi. VM поддерживает байткод инструкцию, похожую на invokedynamic , но встроенный в JDK LambdaMetafactory недоступен для использования.
Method References
Вместе с лямбдами в Java 8 добавили ссылки на методы — это эффективный способ создать лямбду, тело которой ссылается на уже существующий метод.
Наш интерфейс Logger как раз является таким примером. Тело лямбды ссылалось на System.out.println . Давайте превратим лямбду в метод референc:
Когда мы это скомпилируем и взглянем на байткод, то увидим одно различие с предыдущей версией:
Вместо вызова сгенерированного Java8.lambda$main$0 , который содержит вызов System.out.println , теперь System.out.println вызывается напрямую.
Класс с лямбдой больше не static синглтон, а по индексу 0000 в байткоде видно, что мы получаем ссылку на PrintStream — System.out , который затем используется для того, чтобы вызвать на нем println .
В итоге наш класс превратился в это:
Default и static методы в интерфейсах
Еще одним важным и серьезным изменением, которое принесла Java 8, стала возможность объявлять default и static методы в интерфейсах.
Все это тоже поддерживается D8. Используя те же инструменты, что и ранее, несложно увидеть задешугаренную версию Logger’a с default и static методами. Одно из различий с лямбдами и method references в том, что дефолтные и статик методы реализованы в Android VM и, начиная с 24 API, D8 не будет дешугарить их.
Может, просто использовать Kotlin?
Читая статью, большинство из вас, наверное, подумали о Kotlin. Да, он поддерживает все фичи Java 8, но реализованы они kotlinc точно так же, как и D8, за исключением некоторых деталей.
Поэтому поддержка Андроидом новых версий Java до сих пор очень важна, даже если ваш проект на 100% написан на Kotlin.
Не исключено, что в будущем Kotlin перестанет поддерживать байткод Java 6 и Java 7. IntelliJ IDEA, Gradle 5.0 перешли на Java 8. Количество платформ, работающих на более старых JVM, сокращается.
Desugaring APIs
Все это время я рассказывал про фичи Java 8, но ничего не говорил о новых API — стримы, CompletableFuture , date/time и так далее.
Возвращаясь к примеру с Logger’ом, мы можем использовать новый API даты/времени, чтобы узнать, когда сообщения были отправлены.
Снова компилируем это с помощью javac и преобразуем его в байткод Dalvik с D8, который дешугарит его для поддержки на всех версиях API.
Можете даже запушить это на свой девайс, чтобы убедиться, что оно работает.
Если на этом устройстве API 26 и выше, появится месседж Hello. Если нет — увидим следующее:
D8 справился с лямбдами, метод референсами, но не сделал ничего для работы с LocalDateTime , и это очень печально.
Разработчикам приходится использовать свои собственные реализации или обертки над date/time api, либо использовать библиотеки по типу ThreeTenBP для работы со временем, но почему то, что ты можешь написать руками, не может сделать D8?
Эпилог
Отсутствие поддержки всех новых API Java 8 остается большой проблемой в экосистеме Android. Ведь вряд ли каждый из нас может позволить указать 26 min API в своем проекте. Библиотеки, поддерживающие и Android и JVM, не могут позволить себе использовать API, представленный нам 5 лет назад!
И даже несмотря на то, что саппорт Java 8 теперь является частью D8, каждый разработчик все равно должен явно указывать source и target compatibility на Java 8. Если вы пишете собственные библиотеки, то можете усилить эту тенденцию, выкладывая библиотеки, которые используют Java 8 байткод (даже если вы не используете новые фичи языка).
Над D8 ведется очень много работ, поэтому, кажется, в будущем с поддержкой фичей языка все будет ок. Даже если вы пишете только на Kotlin, очень важно заставлять команду разработки Android поддерживать все новые версии Java, улучшать байткод и новые API.
Этот пост — письменная версия моего выступления Digging into D8 and R8.
Источник
Это должен знать каждый
Спрос на Android-разработчиков весьма велик сейчас. Я решил подготовить список того, что нужно знать каждому разработчику под эту платформу. Это не только то, что вас могут спросить на собседовании, а весь спектр знаний, который скорее всего пригодится в работе. Бонусом идет пара интерсных вопросов про платформу.
Платформа Android:
В качестве бонуса полезно знать:
- Механизмы подписи приложений для публикации в Android Market.
- Android NDK: вызов нативного кода
- Фреймворки для разработки под различные мобильные OS: Titanium, PhoneGap и прочие.
Core java. Все то, что нужно обычному java-разработчику.
- Наследование в java, интерфейсы, абстрактные классы, классы, внутренние и анонимные. Множественное наследование интерфейсов, когда применяется. Замыкания.
- Модификаторы в java: доступа, синхронизации, прочие(static и final). Влияние final на производительность.
- Collections: типы коллекций( List , Set , Map ). Различные реализации, применимость коллекций в тех или иных случаях. Сложность вставки, чтения и поиска в различных реализациях.
- Многопоточность: потоки, способы синхронизации, методы wait и notify. Ключевое слово synchronized , когда используется, что означает.
- Отличия библиотеки классов Java SE и платформы Android.
Несколько интересных вопросов про платформу:
1. Есть Task 1, Task 2. В первом есть Activity A, на вершине — Activity B. Из Task 2 посылается intent на запуск Activity A, при этом у A в манифесте указан тип запуска = «singleTask». Что произойдет с Task 1? Как будет выглядеть стек Activity в Task 1? Что произойдет при нескольких нажатиях на кнопку Back?
2. Есть сервис, который был запущен через binding с Activity. Что произойдет с сервисом, когда пользователь покинет соответствующую Activity? Что нужно сделать, чтобы изменить это поведение?
3. Внутри Activity определен BroadcastReceiver как нестатический внутренний класс. Сможет ли такой ресивер получать интенты? Почему?
Источник
1 ноября 2014 г. Особенности Java с точки зрения Android-разработчика
Что же такое Java и откуда она к нам пришла? А пришла она к нам с далёкого 1995. Поначалу язык назывался Oak («дуб»), разрабатывал его бородатый Джеймсон Гослинг для программирования бытовых электронных устройств. В дальнейшем получил язык название Java, которое, по одной из версий, происходит от марки элитного кофе. Помните логотип?
Приложения Java обычно транслируются в специальный байт-код, поэтому они могут работать на любой виртуальной Java-машине вне зависимости от компьютерной архитектуры.
Моё изучение Java началось с разработки приложения под Android. Разработчиков, которые специализировались в этой сфере, поблизости не было, потому многое оставалось без внимания просто по причине незнания о существовании тех или иных вещей.
Думаю, каждый оказывался в ситуации, когда ты понимаешь, что что-то в твоем коде не так. Имея огромное желание это исправить, ты начинаешь искать ответ на вопрос, который не можешь сформулировать, да еще и подсказать некому.
В этой статье я попробую собрать все особенности программирования на Java для Android, которые в свое время мне пришлось выискивать в безграничной сети. Возможно, кому-то они покажутся очевидными, но мне в свое время такая подборка фишек Java очень бы помогла. Надеюсь, все же найдутся те, кому это пригодится :).
Immutable class и разница между String, StringBuffer/StringBuilder
Класс String
Класс String является immutable — вы не можете модифицировать объект String, но можете заменить его созданием нового экземпляра. Создание нового экземпляра обходится дороговато:
Кусок кода в примере выше создаст 99 новых объектов String, 98 из которых будут тут же откинуты. Создание новых объектов неэффективно.
StringBuffer/StringBuilder
Класс StringBuffer является mutable — использовать StringBuffer или StringBuilder следует тогда, когда вы хотите модифицировать содержимое. StringBuilder был добавлен в пятой джаве, и он во всем идентичен классу StringBuffer, за исключением того, что он не синхронизирован, что делает его значительно шустрее. Но цена скорости — небезопасное поведение в мультипоточной среде.
Вышеуказанный код создает только два новых объекта, StringBuffer и строковую константу, которая возвращается. StringBuffer расширяется по мере надобности, что, в свою очередь, дороговато, так что лучше инициализировать StringBuffer корректным значением размера.
Другой важный момент заключается в том, что создание дополнительных строк не ограничено математическим оператором «+», но существует некоторое количество методов, таких как concat(), trim(), substring(), replace() в классах String, которые генерируют новые объекты.
Почему не стоит использовать AsyncTask при работе с сетью
Вот типичный пример кода, который обычно пишут начинающие разработчики для реализации каких либо действий в сети. В данном примере это загрузка файлов с определенного списка адресов:
После создания выполнить задачу очень просто:
Ну, правда ведь, как и пишет документация, класс AsyncTask используется для реализации параллельных задач, создавая дочерний поток в главном потоке, и имеет возможность обновлять UI по завершению работы. С этим не поспоришь. Есть, конечно, нюанс с реализацией большого количества параллельных задач, но если читать внимательно документацию, становится понятно — чтобы эти задачи не попадали в очередь, а выполнялись параллельно, нужно выполнять их в специальном ThreadPoolExecutor.
А вот чего не пишет документация, так это о толерантности работы с данными — если это можно так назвать. Представьте себе ситуацию, у пользователя медленное соединение, в таких условиях даже самый минимальный запрос может осуществляться 3-5 секунд, не говоря уже о загрузке каких либо файлов. Естественно, в этот момент пользователю может наскучить смотреть на ваш прелоадер, и он уйдёт на другой экран в поисках развлечения, а активность, которая породила AsyncTask, прощается с жизненным циклом под катком Garbage collector-a. Дочерние потоки прекращают существовать, и все труды превращаются в пару красных строчек в логе. Ни данных, ни результата. Пользователь возвращается в активность с надеждой увидеть уже подгруженные обновления, и все начинается заново.
Как этого избежать? Использовать для таких кейсов IntentService, который будет реализовывать всю работу с сетью. По запуску запроса он будет корректно завершен вне зависимости от того, существует активность в данный момент или нет. Данные можно сохранить в кеш и отобразить пользователю при следующем запросе вместе с прелоадером. Таким образом, мы еще и избавимся от скучных экранов загрузки.
Нестатические блоки инициализации
В Java существуют статические блоки инициализации — class initializers, код которых выполняется при первой загрузке класса.
Но существуют также и нестатические блоки инициализации — instance initializers. Они позволяют проводить инициализацию объектов вне зависимости от того, какой конструктор был вызван или, например, вести журналирование:
Такой метод инициализации весьма полезен для анонимных внутренних классов, которые конструкторов иметь не могут. Кроме того, вопреки ограничению синтаксиса Java, используя их, мы можем элегантно инициализировать коллекцию:
Вложенные в интерфейсы классы
Вложенный (nested) в интерфейс класс является открытым (public) и статическим (static) даже без явного указания этих модификаторов. Помещая класс внутрь интерфейса, мы показываем, что он является неотъемлемой частью API этого интерфейса и более нигде не используется.
Поскольку такой класс является статическим, мы можем создать его экземпляр, не ссылаясь на объект объемлющего класса, а лишь указывая тип внешнего интерфейса или реализующего его класса.
Самым, наверное, известным примером этой идиомы является класс Map.Entry , содержащий пары ключ-значение ассоциативного словаря.
Модификация данных из внутренних классов
Хотя в Java и предусмотрено ключевое слово final, однако на деле отсутствует возможность задать неизменяемость самого объекта, а не указывающей на него ссылки (не относится к примитивам). Ну, в принципе, можно спроектировать неизменяемый (immutable) класс, предоставив только геттеры и чистые функции, но нельзя, к примеру, создать неизменяемый массив. Это, как мне кажется, существенное упущение в дизайне языка. Тут бы пригодилось зарезервированное, но запрещённое ключевое слово const. Ждём в следующих версиях?
Таким образом, мы можем модифицировать хоть и финализированные, но фактически изменяемые данные, будь то массивы либо другие объекты, даже из контекста внутренних (inner) классов. Со строками и оболочками примитивных типов, к сожалению, такой фокус не пройдёт. Пусть вас ключевое слово final не вводит в заблуждение.
Конфликт имён
Если импортированы несколько классов с одним и тем же именем из разных пакетов, возникает конфликт имён. В таком случае при обращении к классу следует указывать его полное имя, включая и имя пакета, например, java.lang.String.
Неужели ничего нельзя с этим поделать? Оказывается, можно. Следующий код скомпилируется без проблем, несмотря на то, что класс List присутствует и в пакете java.awt, и в пакете java.util:
Достаточно дополнительно импортировать необходимый в данном примере класс java.util.List.
Тут, как вы заметили, используются кириллические идентификаторы. Да! Для кого-то это станет откровением, но Java такая Java. Идентификатор может состоять из совершенно любых букв, помимо цифр, знаков подчёркивания и валюты США (однако последний знак ($) использовать не рекомендуется, он предназначен для системных нужд). Но оно нам надо? Только представьте себе, сколько разных идентификаторов можно сгенерировать всего-то из символов «А» английского, русского и греческого алфавитов…
Инициализация коллекций
К каким только хитростям не приходится прибегать, чтобы упростить инициализацию коллекций и облегчить восприятие кода. Благодаря переменному числу аргументов в методе, которое появилось в пятой версии SDK, а также заботливому обновлению разработчиками стандартного API, ситуация стала немного лучше:
Но этот код занимает две строки вместо одной и не кажется логически связанным. Можно использовать сторонние библиотеки, такие как Google Collections, или изобрести свой велосипед, но есть и более опрятный вариант:
А с появлением статического импорта во всё той же версии Java можно укоротить эту конструкцию ещё на одно слово:
Впрочем, если число элементов в коллекции изменяться не будет, мы можем написать совсем просто:
К сожалению, с картами так не получится.
Выход из любого блока операторов
Хотя goto и является зарезервированным ключевым словом Java, использовать его в своих программах нельзя. Временно? На смену ему пришли операторы break и continue, позволяющие прерывать и продолжать не только текущий цикл, но также и любой обрамляющий цикл, обозначенный меткой:
Но многие даже не догадываются, что в Java мы всё же можем при помощи оператора break не только прервать цикл, но и покинуть совершенно любой блок операторов. Чем не оператор goto, правда, односторонний? Как говорится, вперёд и ни шагу назад.
Практическая ценность от таких прыжков весьма сомнительна и нарушает принципы структурного программирования, но знать о такой возможности, я думаю, стоит.
Подсписки
Интерфейс java.util.List, от которого наследуются в частности ArrayList и LinkedList, обладает замечательным методом List.subList(). Он возвращает не новый список, как может показаться, а вид (view) списка, для которого этот метод был вызван, да таким образом, что оба списка станут разделять хранимые элементы. Из этого вытекают прекрасные свойства:
В данном примере из списка someList будут удалены четыре элемента, с третьего по седьмой (не включительно).
Подсписки можно использовать в качестве диапазонов (ranges). Как часто вам требовалось обойти коллекцию, исключая первый или последний элемент, например? Теперь foreach становится ещё мощнее:
Подсписки следует использовать с осторожностью из-за особенностей, вытекающих из их сути (для подробностей смотрите документацию).
Cafe babe
Все скомпилированные классы и интерфейсы хранятся в специальных файлах с расширением .class. В них содержится байт-код, интерпретируемый виртуальной машиной Java. Чтобы быстро распознавать эти файлы, в них, в первых четырёх байтах, содержится метка, которая в шестнадцатеричном виде выглядит так: 0xCAFEBABE.
Ну, с первым словом всё ясно — Java, как уже напоминалось, названа была не в честь тропического острова, а одноимённого сорта кофе, и среди знаков, используемых в шестнадцатеричной системе счисления, литер «J» и «V» не нашлось. А вот чем руководствовались разработчики, выдумывая второе слово, остаётся только догадываться.
Источник