Подборка лучших практик программирования на Java для экономии времени, оптимизации и улучшения качества кода.
На Java работают более 3 миллиардов устройств и кодят больше 9 миллионов разработчиков. Подсчитать количество приложений просто невозможно. Это мощный и надежный язык, но иногда программирование на Java может быть трудоемким.
На отладку, оптимизацию и повышение производительности кода уходит много времени, поэтому хороший разработчик всегда использует лучшие практики программирования.
В этой статье мы подобрали небольшую коллекцию таких практик, трюков и советов, которые сэкономят вам время.
Если вы начинающий Java-программист, то скачать Java можно на официальном сайте.
☕ Подтянуть свои знания по Java вы можете на нашем телеграм-канале «Библиотека Java для собеса»
Организация работы
Чистый код
В больших проектах на первый план выходит не создание нового кода, а поддержка существующего. Поэтому очень важно его правильно организовывать с самого начала. Проектируя новое приложение, всегда помните о трех основных принципах чистого и поддерживаемого кода:
- правило 10-50-500. В одном пакете не может быть больше 10 классов. Каждый метод должен быть короче 50 строк кода, а каждый класс – короче 500 строк;
- дизайн-принципы SOLID;
- использование паттернов проектирования.
Работа с ошибками
Трассировка стека
Отлов багов – это, возможно, самая трудоемкая составляющая процесса разработки на Java. Трассировка стека позволяет отследить, в каком именно месте проекта было выброшено исключение.
import java.io.*; Exception e = …; java.io.StringWriter sw = new java.io.StringWriter(); e.printStackTrace(new java.io.PrintWriter(sw)); String trace = sw.getBuffer().toString();
NullPointer Exception
Исключения нулевого указателя возникает в Java довольно часто при попытке вызова метода несуществующего объекта.
Возьмем для примера следующую строчку кода:
int noOfStudents = school.listStudents().count;
Если объект school окажется равен Null
или его метод listStudents
вернет Null
, вы получите исключение NullPointerException
.
Хорошей практикой разработки на Java является предварительная проверка на Null
в методах:
private int getListOfStudents(File[] files) { if (files == null) throw new NullPointerException("File list cannot be null"); }
🧩☕ Интересные задачи по Java для практики можно найти на нашем телеграм-канале «Библиотека задач по Java»
Дата и время
System.currentTimeMillis vs System.nanoTime
В Java есть два стандартных способа проведения тайм-операций и не всегда понятно, какой из них следует выбрать.
Метод System.currentTimeMillis
возвращает текущее количество миллисекунд от начала эпохи Unix в формате Long
. Его точность составляет от 1 до 15 тысячных долей секунды в зависимости от системы.
long startTime = System.currentTimeMillis(); long estimatedTime = System.currentTimeMillis() - startTime;
Метод System.nanoTime
имеет точность до одной миллионной доли секунды (наносекунды) и возвращает текущее значение наиболее точного доступного системного таймера.
long startTime = System.nanoTime(); long estimatedTime = System.nanoTime() - startTime;
Таким образом, System.currentTimeMillis
отлично подходит для отображения абсолютного времени и синхронизации с ним, а System.nanoTime
– для измерения относительных интервалов.
Валидность строки с датой
Если вам необходимо получить объект даты из обычной строки на Java, воспользуйтесь этим полезным вспомогательным классом, который возьмет на себя все сложности валидации и конвертации.
Строки
Оптимизация строк
При конкатенации строк на Java с помощью оператора +
, например, в цикле for
, каждый раз создается новый объект String
, что приводит к потере памяти и увеличению времени работы программы.
Также следует избегать создания Java строки с помощью конструктора класса:
// медленное инстанцирование String bad = new String("Yet another string object"); // быстрое инстанцирование String good = "Yet another string object"
Одинарные и двойные кавычки
Что вы ожидаете получить от этого кода?
public class Haha { public static void main(String args[]) { System.out.print("H" + "a"); System.out.print('H' + 'a'); } }
Казалось бы, должна вернуться строка HaHa
, но на деле будет Ha169
.
Двойные кавычки обрабатывают символы как строки, но одинарные ведут себя иначе. Они преобразуют символьные операнды ('H'
и 'a'
) к целочисленным значениям через расширение примитивных типов – получается 169
.
Математика
Float vs Double
Программисты часто не могут выбрать необходимую им точность для чисел с плавающей точкой. Float требует всего 4 байта, но и значащих цифр у него только 7, в то время как Double в два раза точнее (15 цифр), но и в два раза расточительнее.
На самом деле большинство процессоров способны работать с Float и Double одинаково эффективно, поэтому воспользуйтесь рекомендацией Бьёрна Страуструпа:
"Выбор нужной точности в реальных задачах требует хорошего понимания природы машинных вычислений. Если у вас его нет, либо проконсультируйтесь с кем-нибудь, либо изучите проблему сами, либо используйте double и надейтесь на лучшее."
Проверка нечетности
Можно ли использовать этот код для точного определения нечетности числа?
public boolean oddOrNot(int num) { return num % 2 == 1; }
Надеюсь, вы заметили подвох. Если мы решим проверить таким образом отрицательное нечетное число (-5, к примеру), остаток от деления не будет равен единице (а чему он равен?). Поэтому используйте более точный метод:
public boolean oddOrNot(int num) { return (num & 1) != 0; }
Он не только решает проблему отрицательных чисел, но и работает более продуктивно, чем его предшественник. Арифметические и логические операции выполняются гораздо быстрее умножения и деления.
Вычисление степени
Возвести число в степень можно двумя способами:
- простым умножением;
- с помощью функции
Math.pow(double base, double exponent)
.
Использовать библиотечную функцию рекомендуется только при крайней необходимости, например, в случае дробной или отрицательной степени.
double result = Math.pow(625, 0.5); // 25.0
Простое умножение на Java работает в 300-600 раз эффективнее, к тому же его можно дополнительно оптимизировать:
double square = double a * double a; double cube = double a * double a * double a; // не оптимизировано double cube = double a * double square; // оптимизировано double quad = double a * double a * double a * double a; // не оптимизировано double quad = double square * double square; // оптимизировано
JIT-оптимизация
Java-код обрабатывается с помощью JIT-компиляции: сначала транслируется в платформонезависимый байт-код, а уже после этого в машинный код. При этом оптимизируется все, что возможно, и разработчик может помочь компилятору создать максимально эффективную программу.
В качестве примера взглянем на две простые операции:
// 1 n += 2 * i * i; // 2 n += 2 * (i * i);
Измерим время выполнения каждой из них:
// 1 long startTime1 = System.nanoTime(); int n1 = 0; for (int i = 0; i < 1000000000; i++) { n1 += 2 * i * i; } double resultTime1 = (double)(System.nanoTime() - startTime1) / 1000000000; System.out.println(resultTime1 + " s"); // 2 long startTime2 = System.nanoTime(); int n2 = 0; for (int i = 0; i < 1000000000; i++) { n2 += 2 * (i * i); } double resultTime2 = (double)(System.nanoTime() - startTime2) / 1000000000; System.out.println(resultTime2 + " s");
Запустив этот код несколько раз, получим что-то подобное:
2*(i*i) | 2*i*i ----------+---------- 0.5183738 | 0.6246434 0.5298337 | 0.6049722 0.5308647 | 0.6603363 0.5133458 | 0.6243328 0.5003011 | 0.6541802 0.5366181 | 0.6312638 0.515149 | 0.6241105 0.5237389 | 0.627815 0.5249942 | 0.6114252 0.5641624 | 0.6781033 0.538412 | 0.6393969 0.5466744 | 0.6608845 0.531159 | 0.6201077 0.5048032 | 0.6511559 0.5232789 | 0.6544526
Закономерность очевидна: группировка переменных с помощью скобок ускоряет работу программы. Это происходит из-за генерации более эффективного байт-кода при умножении одинаковых значений.
Подробнее об этом эксперименте вы можете почитать здесь. А провести собственное испытание можно с помощью онлайн-компилятора Java.
Структуры данных
Объединение хеш-таблиц
Объединять два хеша, итерируя их значения вручную, весьма неэффективно. Вот альтернативное решение этой задачи, которое точно вам понравится:
import java.util.*; Map m1 = …; Map m2 = …; m2.putAll(m1); // добавляет в m2 все элементы из m1
Array vs ArrayList
Выбор между Array и ArrayList зависит от специфики задачи на Java, которую требуется решить. Помните о следующих особенностях этих типов:
- Array имеет фиксированный размер и память для него выделяется в момент объявления, а размер ArrayLists может динамически изменяться.
- Массивы Java работают гораздо быстрее, а в ArrayList намного проще добавлять/удалять элементы.
- При работе с Array велика вероятность получить ошибку
ArrayIndexOutOfBoundsException
. - У ArrayList только одно измерение, а вот массивы Java могут быть многомерными.
import java.util.ArrayList; public class arrayVsArrayList { public static void main(String[] args) { // объявление Array int[] myArray = new int[6]; // обращение к несуществующему индексу myArray[7]= 10; // ArrayIndexOutOfBoundsException // объявление ArrayList ArrayList<Integer> myArrayList = new ArrayList<>(); // простое добавление и удаление элементов myArrayList.add(1); myArrayList.add(2); myArrayList.add(3); myArrayList.add(4); myArrayList.add(5); myArrayList.remove(0); // получение элементов ArrayList for(int i = 0; i < myArrayList.size(); i++) { System.out.println("Element: " + myArrayList.get(i)); } // многомерный Array int[][][] multiArray = new int [3][3][3]; } }
JSON
Сериализация и десериализация
JSON – невероятно удобный и полезный синтаксис для хранения и обмена данными. Java полностью поддерживает его.
Сериализовать данные можно так:
import org.json.simple.JSONObject; import org.json.simple.JSONArray; public class JsonEncodeDemo { public static void main(String[] args) { JSONObject obj = new JSONObject(); obj.put("Novel Name", "Godaan"); obj.put("Author", "Munshi Premchand"); JSONArray novelDetails = new JSONArray(); novelDetails.add("Language: Hindi"); novelDetails.add("Year of Publication: 1936"); novelDetails.add("Publisher: Lokmanya Press"); obj.put("Novel Details", novelDetails); System.out.print(obj); } }
Получится вот такая JSON-строка:
{"Novel Name":"Godaan","Novel Details":["Language: Hindi","Year of Publication: 1936","Publisher: Lokmanya Press"],"Author":"Munshi Premchand"}
Десериализация на Java выглядит примерно так:
import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.Iterator; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; public class JsonParseTest { private static final String filePath = "//home//user//Documents//jsonDemoFile.json"; public static void main(String[] args) { try { // читаем json-файл FileReader reader = new FileReader(filePath); JSONParser jsonParser = new JSONParser(); JSONObject jsonObject = (JSONObject)jsonParser.parse(reader); // получаем данные из json объекта Long id = (Long) jsonObject.get("id"); System.out.println("The id is: " + id); String type = (String) jsonObject.get("type"); System.out.println("The type is: " + type); String name = (String) jsonObject.get("name"); System.out.println("The name is: " + name); Double ppu = (Double) jsonObject.get("ppu"); System.out.println("The PPU is: " + ppu); // получаем массив System.out.println("Batters:"); JSONArray batterArray= (JSONArray) jsonObject.get("batters"); Iterator i = batterArray.iterator(); // перебираем все элементы массива отдельно while (i.hasNext()) { JSONObject innerObj = (JSONObject) i.next(); System.out.println("ID "+ innerObj.get("id") + " type " + innerObj.get("type")); } System.out.println("Topping:"); JSONArray toppingArray= (JSONArray) jsonObject.get("topping"); Iterator j = toppingArray.iterator(); while (j.hasNext()) { JSONObject innerObj = (JSONObject) j.next(); System.out.println("ID "+ innerObj.get("id") + " type " + innerObj.get("type")); } } catch (FileNotFoundException ex) { ex.printStackTrace(); } catch (IOException ex) { ex.printStackTrace(); } catch (ParseException ex) { ex.printStackTrace(); } catch (NullPointerException ex) { ex.printStackTrace(); } } }
JSON-файл, использованный в примере (jsonDemoFile.json):
{ "id": 0001, "type": "donut", "name": "Cake", "ppu": 0.55, "batters": [ { "id": 1001, "type": "Regular" }, { "id": 1002, "type": "Chocolate" }, { "id": 1003, "type": "Blueberry" }, { "id": 1004, "type": "Devil's Food" } ], "topping": [ { "id": 5001, "type": "None" }, { "id": 5002, "type": "Glazed" }, { "id": 5005, "type": "Sugar" }, { "id": 5007, "type": "Powdered Sugar" }, { "id": 5006, "type": "Chocolate with Sprinkles" }, { "id": 5003, "type": "Chocolate" }, { "id": 5004, "type": "Maple" } ] }
Ввод-вывод
FileOutputStream vs. FileWriter
Запись файлов на Java осуществляется двумя способами: FileOutputStream
и FileWriter
. Какой именно метод выбрать, зависит от конкретной задачи.
FileOutputStream
предназначен для записи потоков необработанных байтов. Это делает его идеальным решением для работы, например, с изображениями.
File foutput = new File(file_location_string); FileOutputStream fos = new FileOutputStream(foutput); BufferedWriter output = new BufferedWriter(new OutputStreamWriter(fos)); output.write("Buffered Content");
У FileWriter
другое призвание: работа с потоками символов. Так что если вы пишете текстовые файлы, выбирайте этот метод.
FileWriter fstream = new FileWriter(file_location_string); BufferedWriter output = new BufferedWriter(fstream); output.write("Buffered Content");
Производительность и лучшие практики
Пустая коллекция вместо Null
Если ваша программа может возвращать коллекцию, которая не содержит ни одного значения, убедитесь, что возвращена именно пустая коллекция, а не Null
. Это сэкономит вам время на различные проверки.
public class getLocationName { return (null==cityName ? "": cityName); }
Создание объектов только при необходимости
Создание объектов – одна из самых затратных операций в Java. Лучшая практика – создавать их только при необходимости, когда они действительно нужны.
import java.util.ArrayList; import java.util.List; public class Employees { private List Employees; public List getEmployees() { // инициализация только при необходимости if(null == Employees) { Employees = new ArrayList(); } return Employees; } }
Взаимоблокировки (deadlocks)
Взаимные блокировки потоков могут возникать по многим причинам, и полностью защититься от них в Java 8 очень сложно. Чаще всего это происходит, когда один синхронизированный объект ожидает ресурсов, которые заблокированы другим синхронизированным объектом.
Вот пример такой взаимоблокировки потоков:
public class DeadlockDemo { public static Object addLock = new Object(); public static Object subLock = new Object(); public static void main(String args[]) { MyAdditionThread add = new MyAdditionThread(); MySubtractionThread sub = new MySubtractionThread(); add.start(); sub.start(); } private static class MyAdditionThread extends Thread { public void run() { synchronized (addLock) { int a = 10, b = 3; int c = a + b; System.out.println("Addition Thread: " + c); System.out.println("Holding First Lock..."); try { Thread.sleep(10); } catch (InterruptedException e) {} System.out.println("Addition Thread: Waiting for AddLock..."); synchronized (subLock) { System.out.println("Threads: Holding Add and Sub Locks..."); } } } } private static class MySubtractionThread extends Thread { public void run() { synchronized (subLock) { int a = 10, b = 3; int c = a - b; System.out.println("Subtraction Thread: " + c); System.out.println("Holding Second Lock..."); try { Thread.sleep(10); } catch (InterruptedException e) {} System.out.println("Subtraction Thread: Waiting for SubLock..."); synchronized (addLock) { System.out.println("Threads: Holding Add and Sub Locks..."); } } } } }
Вывод этой программы:
===== Addition Thread: 13 Subtraction Thread: 7 Holding First Lock... Holding Second Lock... Addition Thread: Waiting for AddLock... Subtraction Thread: Waiting for SubLock...
Взаимоблокировки можно избежать, если изменить порядок вызова потоков:
private static class MySubtractionThread extends Thread { public void run() { synchronized (addLock) { int a = 10, b = 3; int c = a - b; System.out.println("Subtraction Thread: " + c); System.out.println("Holding Second Lock..."); try { Thread.sleep(10); } catch (InterruptedException e) {} System.out.println("Subtraction Thread: Waiting for SubLock..."); synchronized (subLock) { System.out.println("Threads: Holding Add and Sub Locks..."); } } } }
Вывод:
===== Addition Thread: 13 Holding First Lock... Addition Thread: Waiting for AddLock... Threads: Holding Add and Sub Locks... Subtraction Thread: 7 Holding Second Lock... Subtraction Thread: Waiting for SubLock... Threads: Holding Add and Sub Locks...
Резервирование памяти
Некоторые Java-приложения очень требовательны к ресурсам и могут работать медленно. Для повышения производительности можно выделять Java-машине больше оперативной памяти.
export JAVA_OPTS="$JAVA_OPTS -Xms5000m -Xmx6000m -XX:PermSize=1024m -XX:MaxPermSize=2048m"
- Xms – минимальный пул выделения памяти;
- Xmx – максимальный пул выделения памяти;
- XX:PermSize – начальный размер, который будет выделен при запуске JVM;
- XX:MaxPermSize – максимальный размер, который может быть выделен при запуске JVM.
Решение распространенных задач
Содержимое директории
Java позволяет получить имена всех подкаталогов и файлов в папке в виде массива, который затем можно последовательно просмотреть.
import java.io.*; public class ListContents { public static void main(String[] args) { File file = new File("//home//user//Documents/"); String[] files = file.list(); System.out.println("Listing contents of " + file.getPath()); for(int i=0 ; i < files.length ; i++) { System.out.println(files[i]); } } }
Выполнение консольных команд
Java позволяет выполнять консольные команды прямо из кода с помощью класса Runtime
. При этом очень важно не забывать об обработке исключений.
Для примера попробуем открыть PDF-файл через терминал на Java:
import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; public class ShellCommandExec { public static void main(String[] args) { String gnomeOpenCommand = "gnome-open //home//user//Documents//MyDoc.pdf"; try { Runtime rt = Runtime.getRuntime(); Process processObj = rt.exec(gnomeOpenCommand); InputStream stdin = processObj.getErrorStream(); InputStreamReader isr = new InputStreamReader(stdin); BufferedReader br = new BufferedReader(isr); String myoutput = ""; while ((myoutput=br.readLine()) != null) { myoutput = myoutput+"\n"; } System.out.println(myoutput); } catch (Exception e) { e.printStackTrace(); } } }
Воспроизведение звука
Звук – важная составляющая часть многих приложений, например, игр. Язык программирования Java предоставляет средства для работы с ним.
import java.io.*; import java.net.URL; import javax.sound.sampled.*; import javax.swing.*; public class playSoundDemo extends JFrame { // конструктор public playSoundDemo() { this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setTitle("Play Sound Demo"); this.setSize(300, 200); this.setVisible(true); try { URL url = this.getClass().getResource("MyAudio.wav"); AudioInputStream audioIn = AudioSystem.getAudioInputStream(url); Clip clip = AudioSystem.getClip(); clip.open(audioIn); clip.start(); } catch (UnsupportedAudioFileException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (LineUnavailableException e) { e.printStackTrace(); } } public static void main(String[] args) { new playSoundDemo(); } }
Отправка email
Отправка электронной почты на Java очень проста. Нужно лишь установить Java Mail и указать путь к нему в classpath проекта.
import java.util.*; import javax.mail.*; import javax.mail.internet.*; public class SendEmail { public static void main(String [] args) { String to = "recipient@gmail.com"; String from = "sender@gmail.com"; String host = "localhost"; Properties properties = System.getProperties(); properties.setProperty("mail.smtp.host", host); Session session = Session.getDefaultInstance(properties); try{ MimeMessage message = new MimeMessage(session); message.setFrom(new InternetAddress(from)); message.addRecipient(Message.RecipientType.TO,new InternetAddress(to)); message.setSubject("My Email Subject"); message.setText("My Message Body"); Transport.send(message); System.out.println("Sent successfully!"); } catch (MessagingException ex) { ex.printStackTrace(); } } }
Захват координат курсора
Чтобы захватить события мыши, необходимо реализовать интерфейс MouseMotionListener
. Когда курсор попадает в определенную область, срабатывает обработчик события mouseMoved
, из которого можно получить точные координаты.
import java.awt.event.*; import javax.swing.*; public class MouseCaptureDemo extends JFrame implements MouseMotionListener { public JLabel mouseHoverStatus; public static void main(String args[]) { new MouseCaptureDemo(); } MouseCaptureDemo() { setSize(500, 500); setTitle("Frame displaying Coordinates of Mouse Motion"); mouseHoverStatus = new JLabel("No Mouse Hover Detected.", JLabel.CENTER); add(mouseHoverStatus); addMouseMotionListener(this); setVisible(true); } public void mouseMoved(MouseEvent e) { mouseHoverStatus.setText("Mouse Cursor Coordinates => X:"+e.getX()+" | Y:"+e.getY()); } public void mouseDragged(MouseEvent e) {} }
И немного о регулярных выражениях в Java.
Комментарии