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