Особенности Java с точки зрения Android-разработчика

Что же такое Java и откуда она к нам пришла? А пришла она к нам с далёкого 1995. Поначалу язык назывался Oak («дуб»), разрабатывал его бородатый Джеймсон Гослинг для программирования бытовых электронных устройств. В дальнейшем получил язык название Java, которое, по одной из версий, происходит от марки элитного кофе. Помните логотип?
Приложения Java обычно транслируются в специальный байт-код, поэтому они могут работать на любой виртуальной Java-машине вне зависимости от компьютерной архитектуры.
Моё изучение Java началось с разработки приложения под Android. Разработчиков, которые специализировались в этой сфере, поблизости не было, потому многое оставалось без внимания просто по причине незнания о существовании тех или иных вещей.
Думаю, каждый оказывался в ситуации, когда ты понимаешь, что что-то в твоем коде не так. Имея огромное желание это исправить, ты начинаешь искать ответ на вопрос, который не можешь сформулировать, да еще и подсказать некому...
В этой статье я попробую собрать все особенности программирования на Java для Android, которые в свое время мне пришлось выискивать в безграничной сети. Возможно, кому-то они покажутся очевидными, но мне в свое время такая подборка фишек Java очень бы помогла. Надеюсь, все же найдутся те, кому это пригодится :).
Immutable class и разница между String, StringBuffer/StringBuilder
Класс String
Класс String является immutable — вы не можете модифицировать объект String, но можете заменить его созданием нового экземпляра. Создание нового экземпляра обходится дороговато:
//Inefficient version using immutable String String output = "Some text"; int count = 100; for(int i =0; i<count; i++) { output += i; } return output;
Кусок кода в примере выше создаст 99 новых объектов String, 98 из которых будут тут же откинуты. Создание новых объектов неэффективно.
StringBuffer/StringBuilder
Класс StringBuffer является mutable — использовать StringBuffer или StringBuilder следует тогда, когда вы хотите модифицировать содержимое. StringBuilder был добавлен в пятой джаве, и он во всем идентичен классу StringBuffer, за исключением того, что он не синхронизирован, что делает его значительно шустрее. Но цена скорости — небезопасное поведение в мультипоточной среде.
//More efficient version using mutable StringBuffer // set an initial size of 110 StringBuffer output = new StringBuffer(110); output.append("Some text"); for(int i =0; i<count; i++) { output.append(i); } return output.toString();
Вышеуказанный код создает только два новых объекта, StringBuffer и строковую константу, которая возвращается. StringBuffer расширяется по мере надобности, что, в свою очередь, дороговато, так что лучше инициализировать StringBuffer корректным значением размера.
Другой важный момент заключается в том, что создание дополнительных строк не ограничено математическим оператором "+", но существует некоторое количество методов, таких как concat(), trim(), substring(), replace() в классах String, которые генерируют новые объекты.
Почему не стоит использовать AsyncTask при работе с сетью
Вот типичный пример кода, который обычно пишут начинающие разработчики для реализации каких либо действий в сети. В данном примере это загрузка файлов с определенного списка адресов:
private class DownloadFilesTask extends AsyncTask < URL, Integer, Long > { protected Long doInBackground(URL... urls) { int count = urls.length; long totalSize = 0; for (int i = 0; i < count; i++) { totalSize += Downloader.downloadFile(urls[i]); publishProgress((int) ((i / (float) count) * 100)); // Ранний выход, если был вызван cancel(). if (isCancelled()) break; } return totalSize; } protected void onProgressUpdate(Integer... progress) { setProgressPercent(progress[0]); } protected void onPostExecute(Long result) { showDialog("Downloaded " + result + " bytes"); } }
После создания выполнить задачу очень просто:
new DownloadFilesTask().execute(url1, url2, url3);
Ну, правда ведь, как и пишет документация, класс AsyncTask используется для реализации параллельных задач, создавая дочерний поток в главном потоке, и имеет возможность обновлять UI по завершению работы. С этим не поспоришь. Есть, конечно, нюанс с реализацией большого количества параллельных задач, но если читать внимательно документацию, становится понятно — чтобы эти задачи не попадали в очередь, а выполнялись параллельно, нужно выполнять их в специальном ThreadPoolExecutor.
А вот чего не пишет документация, так это о толерантности работы с данными — если это можно так назвать. Представьте себе ситуацию, у пользователя медленное соединение, в таких условиях даже самый минимальный запрос может осуществляться 3-5 секунд, не говоря уже о загрузке каких либо файлов. Естественно, в этот момент пользователю может наскучить смотреть на ваш прелоадер, и он уйдёт на другой экран в поисках развлечения, а активность, которая породила AsyncTask, прощается с жизненным циклом под катком Garbage collector-a. Дочерние потоки прекращают существовать, и все труды превращаются в пару красных строчек в логе... Ни данных, ни результата... Пользователь возвращается в активность с надеждой увидеть уже подгруженные обновления, и все начинается заново.
Как этого избежать? Использовать для таких кейсов IntentService, который будет реализовывать всю работу с сетью. По запуску запроса он будет корректно завершен вне зависимости от того, существует активность в данный момент или нет. Данные можно сохранить в кеш и отобразить пользователю при следующем запросе вместе с прелоадером. Таким образом, мы еще и избавимся от скучных экранов загрузки.
Нестатические блоки инициализации
В Java существуют статические блоки инициализации — class initializers, код которых выполняется при первой загрузке класса.
class Foo { static List<Character> abc; static { abc = new LinkedList<Character>(); for (char c = 'A'; c <= 'Z'; ++c) { abc.add( c ); } } }
Но существуют также и нестатические блоки инициализации — instance initializers. Они позволяют проводить инициализацию объектов вне зависимости от того, какой конструктор был вызван или, например, вести журналирование:
class Bar { { System.out.println("Bar: новый экземпляр"); } }
Такой метод инициализации весьма полезен для анонимных внутренних классов, которые конструкторов иметь не могут. Кроме того, вопреки ограничению синтаксиса Java, используя их, мы можем элегантно инициализировать коллекцию:
Map<String, String> mapPatriot = new HashMap<String, String>() {{ put("Слава Україні!", "Героям Слава!"); put("Слава нації!", "Смерть ворогам!"); put("Україна!", "Понад усе!"); }};
Вложенные в интерфейсы классы
Вложенный (nested) в интерфейс класс является открытым (public) и статическим (static) даже без явного указания этих модификаторов. Помещая класс внутрь интерфейса, мы показываем, что он является неотъемлемой частью API этого интерфейса и более нигде не используется.
interface Colorable { public Color getColor(); public static class Color { private int red, green, blue; Color(int red, int green, int blue) { this.red = red; this.green = green; this.blue = blue; } int getRed() { return red; } int getGreen() { return green; } int getBlue() { return blue; } } } class Triangle implements Colorable { private Color color; // ... @Override public Color getColor() { return color; } }
Поскольку такой класс является статическим, мы можем создать его экземпляр, не ссылаясь на объект объемлющего класса, а лишь указывая тип внешнего интерфейса или реализующего его класса.
Colorable.Color color = new Colorable.Color(0, 0, 0); color = new Triangle.Color(255, 255, 255);
Самым, наверное, известным примером этой идиомы является класс Map.Entry<K, V>, содержащий пары ключ-значение ассоциативного словаря.
Модификация данных из внутренних классов
Хотя в Java и предусмотрено ключевое слово final, однако на деле отсутствует возможность задать неизменяемость самого объекта, а не указывающей на него ссылки (не относится к примитивам). Ну, в принципе, можно спроектировать неизменяемый (immutable) класс, предоставив только геттеры и чистые функции, но нельзя, к примеру, создать неизменяемый массив. Это, как мне кажется, существенное упущение в дизайне языка. Тут бы пригодилось зарезервированное, но запрещённое ключевое слово const. Ждём в следующих версиях?
final int[] array = {1, 2, 3, 4, 5}; new Object() { void twice() { for (int i = 0; i < array.length; ++i) { array[i] *= 2; } } }.twice();
Таким образом, мы можем модифицировать хоть и финализированные, но фактически изменяемые данные, будь то массивы либо другие объекты, даже из контекста внутренних (inner) классов. Со строками и оболочками примитивных типов, к сожалению, такой фокус не пройдёт. Пусть вас ключевое слово final не вводит в заблуждение.
Конфликт имён
Если импортированы несколько классов с одним и тем же именем из разных пакетов, возникает конфликт имён. В таком случае при обращении к классу следует указывать его полное имя, включая и имя пакета, например, java.lang.String.
Неужели ничего нельзя с этим поделать? Оказывается, можно. Следующий код скомпилируется без проблем, несмотря на то, что класс List присутствует и в пакете java.awt, и в пакете java.util:
import java.awt.*; import java.util.*; import java.util.List; public class Класс { public static void main(String... аргументы) { List простоСписок = Collections.emptyList(); System.out.println(простоСписок); } }
Достаточно дополнительно импортировать необходимый в данном примере класс java.util.List.
Тут, как вы заметили, используются кириллические идентификаторы. Да! Для кого-то это станет откровением, но Java такая Java. Идентификатор может состоять из совершенно любых букв, помимо цифр, знаков подчёркивания и валюты США (однако последний знак ($) использовать не рекомендуется, он предназначен для системных нужд). Но оно нам надо? Только представьте себе, сколько разных идентификаторов можно сгенерировать всего-то из символов «А» английского, русского и греческого алфавитов…
Инициализация коллекций
К каким только хитростям не приходится прибегать, чтобы упростить инициализацию коллекций и облегчить восприятие кода. Благодаря переменному числу аргументов в методе, которое появилось в пятой версии SDK, а также заботливому обновлению разработчиками стандартного API, ситуация стала немного лучше:
List<Integer> theNumbers = new LinkedList<Integer>(); Collections.addAll(theNumbers, 4, 8, 15, 16, 23, 42);
Но этот код занимает две строки вместо одной и не кажется логически связанным. Можно использовать сторонние библиотеки, такие как Google Collections, или изобрести свой велосипед, но есть и более опрятный вариант:
List<Integer> theNumbers = new LinkedList<Integer>(Arrays.asList(4, 8, 15, 16, 23, 42));
А с появлением статического импорта во всё той же версии Java можно укоротить эту конструкцию ещё на одно слово:
import static java.util.Arrays.*; // ... List<Integer> theNumbers = new LinkedList<Integer>(asList(4, 8, 15, 16, 23, 42));
Впрочем, если число элементов в коллекции изменяться не будет, мы можем написать совсем просто:
import static java.util.Arrays.*; // ... List<Integer> theNumbers = asList(4, 8, 15, 16, 23, 42);
К сожалению, с картами так не получится.
Выход из любого блока операторов
Хотя goto и является зарезервированным ключевым словом Java, использовать его в своих программах нельзя. Временно? На смену ему пришли операторы break и continue, позволяющие прерывать и продолжать не только текущий цикл, но также и любой обрамляющий цикл, обозначенный меткой:
String a = "quadratic", b = "complexity"; boolean hasSame = false; outer: for (int i = 0; i < a.length(); ++i) { for (int j = 0; j < b.length(); ++j) { if (a.charAt(i) == b.charAt(j)) { hasSame = true; break outer; } } } System.out.println(hasSame);
Но многие даже не догадываются, что в Java мы всё же можем при помощи оператора break не только прервать цикл, но и покинуть совершенно любой блок операторов. Чем не оператор goto, правда, односторонний? Как говорится, вперёд и ни шагу назад.
long factorial(int n) { long result = 1; scope: { if (n == 0) { break scope; } result = n * factorial(n - 1); } return result; }
Практическая ценность от таких прыжков весьма сомнительна и нарушает принципы структурного программирования, но знать о такой возможности, я думаю, стоит.
Подсписки
Интерфейс java.util.List, от которого наследуются в частности ArrayList и LinkedList, обладает замечательным методом List.subList(). Он возвращает не новый список, как может показаться, а вид (view) списка, для которого этот метод был вызван, да таким образом, что оба списка станут разделять хранимые элементы. Из этого вытекают прекрасные свойства:
someList.subList(3, 7).clear();
В данном примере из списка someList будут удалены четыре элемента, с третьего по седьмой (не включительно).
Подсписки можно использовать в качестве диапазонов (ranges). Как часто вам требовалось обойти коллекцию, исключая первый или последний элемент, например? Теперь foreach становится ещё мощнее:
import static java.util.Arrays.*; // ... List<Integer> theNumbers = asList(4, 8, 15, 16, 23, 42); int size = theNumbers.size(); for (Integer number : theNumbers.subList(0, size - 1)) { System.out.print(number + ", "); } System.out.println(theNumbers.get(size - 1));
Подсписки следует использовать с осторожностью из-за особенностей, вытекающих из их сути (для подробностей смотрите документацию).
Cafe babe
Все скомпилированные классы и интерфейсы хранятся в специальных файлах с расширением .class. В них содержится байт-код, интерпретируемый виртуальной машиной Java. Чтобы быстро распознавать эти файлы, в них, в первых четырёх байтах, содержится метка, которая в шестнадцатеричном виде выглядит так: 0xCAFEBABE.
Ну, с первым словом всё ясно — Java, как уже напоминалось, названа была не в честь тропического острова, а одноимённого сорта кофе, и среди знаков, используемых в шестнадцатеричной системе счисления, литер «J» и «V» не нашлось. А вот чем руководствовались разработчики, выдумывая второе слово, остаётся только догадываться.