Особенности 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 при работе с сетью
Вот типичный пример кода, который обычно пишут начинающие разработчики для реализации каких либо действий в сети. В данном примере это загрузка файлов с определенного списка адресов:
privateclass DownloadFilesTask extends AsyncTask <URL, Integer, Long>{protectedLong 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;} protectedvoid onProgressUpdate(Integer... progress){ setProgressPercent(progress[0]);} protectedvoid 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 { publicColor getColor(); publicstaticclassColor{privateint 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 { privateColor color;// ... @Override publicColor 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. Ждём в следующих версиях?
finalint[] array ={1, 2, 3, 4, 5};newObject(){void twice(){for(int i =0; i < array.length;++i){ array[i]*=2;}}}.twice();
Таким образом, мы можем модифицировать хоть и финализированные, но фактически изменяемые данные, будь то массивы либо другие объекты, даже из контекста внутренних (inner) классов. Со строками и оболочками примитивных типов, к сожалению, такой фокус не пройдёт. Пусть вас ключевое слово final не вводит в заблуждение.
Конфликт имён
Если импортированы несколько классов с одним и тем же именем из разных пакетов, возникает конфликт имён. В таком случае при обращении к классу следует указывать его полное имя, включая и имя пакета, например, java.lang.String.
Неужели ничего нельзя с этим поделать? Оказывается, можно. Следующий код скомпилируется без проблем, несмотря на то, что класс List присутствует и в пакете java.awt, и в пакете java.util:
importjava.awt.*;importjava.util.*;importjava.util.List; publicclass Класс {publicstaticvoid 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 можно укоротить эту конструкцию ещё на одно слово:
importstatic java.util.Arrays.*;// ... List<Integer> theNumbers =new LinkedList<Integer>(asList(4, 8, 15, 16, 23, 42));
Впрочем, если число элементов в коллекции изменяться не будет, мы можем написать совсем просто:
importstatic 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 становится ещё мощнее:
importstatic 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» не нашлось. А вот чем руководствовались разработчики, выдумывая второе слово, остаётся только догадываться.