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

Особенности 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 при работе с сетью

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

Вот типичный пример кода, который обычно пишут начинающие разработчики для реализации каких либо действий в сети. В данном примере это загрузка файлов с определенного списка адресов:

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 с точки зрения Android-разработчика

В 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 &lt; 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&lt;Integer&gt; theNumbers =new LinkedList&lt;Integer&gt;();Collections.addAll(theNumbers, 4, 8, 15, 16, 23, 42);

Но этот код занимает две строки вместо одной и не кажется логически связанным. Можно использовать сторонние библиотеки, такие как Google Collections, или изобрести свой велосипед, но есть и более опрятный вариант:

List&lt;Integer&gt; theNumbers =new LinkedList&lt;Integer&gt;(Arrays.asList(4, 8, 15, 16, 23, 42));

А с появлением статического импорта во всё той же версии Java можно укоротить эту конструкцию ещё на одно слово:

importstatic java.util.Arrays.*;// ...
List&lt;Integer&gt; theNumbers =new LinkedList&lt;Integer&gt;(asList(4, 8, 15, 16, 23, 42));

Впрочем, если число элементов в коллекции изменяться не будет, мы можем написать совсем просто:

importstatic java.util.Arrays.*;// ...
List&lt;Integer&gt; 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 &lt; a.length();++i){for(int j =0; j &lt; 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&lt;Integer&gt; 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» не нашлось. А вот чем руководствовались разработчики, выдумывая второе слово, остаётся только догадываться.