Особливості 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 був доданий у п'ятій версії Java, і він у всьому ідентичний класу 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("Завантажено "+ result +" байт");}}

Після створення виконати задачу дуже просто:

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» не знайшлося місця. А от чим керувалися розробники, вигадуючи друге слово, залишається тільки здогадуватися.