ТОП-25 трюков, советов и лучших практик программирования на Java

Подборка лучших практик программирования на Java для экономии времени, оптимизации и улучшения качества кода.

На Java работают более 3 миллиардов устройств и кодят больше 9 миллионов разработчиков. Подсчитать количество приложений просто невозможно. Это мощный и надежный язык, но иногда программирование на Java может быть трудоемким.

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

В этой статье мы подобрали небольшую коллекцию таких практик, трюков и советов, которые сэкономят вам время.

Если вы начинающий Java-программист, то скачать Java можно на официальном сайте.

☕ Подтянуть свои знания по Java вы можете на нашем телеграм-канале «Библиотека Java для собеса»


Организация работы

Чистый код

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

Работа с ошибками

Трассировка стека

Отлов багов – это, возможно, самая трудоемкая составляющая процесса разработки на 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.

Лучшие ресурсы и книги по Java:

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
16 апреля 2019

ТОП-6 алгоритмов сортировки на Java для новичков

Изучение алгоритмов сортировки на языке Java поможет не изобретать велосипе...
admin
11 января 2019

ТОП-10 лучших книг по Java для программистов

Не имеет значения, хотите вы улучшить скилл или только собираетесь начать и...
admin
05 апреля 2017

6 книг по Java для программистов любого уровня

Подборка материалов по Java. Если вы изучаете его, то обязательно найдете д...