Приёмы и хитрости начинающего Java-программиста

Kate

Administrator
Команда форума

Дмитрий Зайцев​

эксперт по разработке ПО компании «Рексофт»​

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

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

Чистый код​

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

  • Правило 10-50-500. В одном пакете не может быть более 10 классов. Каждый метод должен быть короче 50 строк кода, а каждый класс — короче 500 строк.
  • SOLID принципы.
  • Использование паттернов проектирования.

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

Stack Trace (Трассировка стека)​

Выявление ошибок — это, пожалуй, самая трудоемкая часть процесса разработки на Java. Трассировка стека позволяет вам точно отслеживать, где именно в проекте возникла ошибка или исключение (exception).

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();

NullPointerException​

Исключения, возникающие из-за null значений (NullPointerException), довольно часто появляются при попытке вызвать метод у несущестующего объекта.

Возьмем для примера следующий код:

int noOfStudents = school.listStudents().count;
private int getListOfStudents(File[] files) {
if (files == null)
throw new NullPointerException("File list cannot be null");
}
Прим. перев. А вот пример от меня как переводчика материала:

String anyString = null;

if (anyString.equals("some string")) { // здесь будет null pointer exception, т.к. anyString == null

// ...

}

Дата и Время​

System.currentTimeMillis или 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() для измерения относительных интервалов времени.

Валидация Даты из строки​

Если необходимо достать объект Date из обычной строки в Java, можете использовать небольшой утилитный класс, который приведен ниже. Он позаботится обо всех сложностях валидации и преобразовании строки в объект Date.

package net.viralpatel.java;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class DateUtil {

// List of all date formats that we want to parse.
// Add your own format here.
private static List;
dateFormats = new ArrayList() {{
add(new SimpleDateFormat("M/dd/yyyy"));
add(new SimpleDateFormat("dd.M.yyyy"));
add(new SimpleDateFormat("M/dd/yyyy hh:mm:ss a"));
add(new SimpleDateFormat("dd.M.yyyy hh:mm:ss a"));
add(new SimpleDateFormat("dd.MMM.yyyy"));
add(new SimpleDateFormat("dd-MMM-yyyy"));
}
};

/**
* Convert String with various formats into java.util.Date
*
* @param input
* Date as a string
* @return java.util.Date object if input string is parsed
* successfully else returns null
*/
public static Date convertToDate(String input) {
Date date = null;
if(null == input) {
return null;
}
for (SimpleDateFormat format : dateFormats) {
try {
format.setLenient(false);
date = format.parse(input);
} catch (ParseException e) {
//Shhh.. try other formats
}
if (date != null) {
break;
}
}

return date;
}
}

Пример его использования:

package net.viralpatel.java;

public class TestDateUtil {
public static void main(String[] args) {
System.out.println("10/14/2012" + " = " + DateUtil.convertToDate("10/14/2012"));
System.out.println("10-Jan-2012" + " = " + DateUtil.convertToDate("10-Jan-2012"));
System.out.println("01.03.2002" + " = " + DateUtil.convertToDate("01.03.2002"));
System.out.println("12/03/2010" + " = " + DateUtil.convertToDate("12/03/2010"));
System.out.println("19.Feb.2011" + " = " + DateUtil.convertToDate("19.Feb.2011" ));
System.out.println("4/20/2012" + " = " + DateUtil.convertToDate("4/20/2012"));
System.out.println("some string" + " = " + DateUtil.convertToDate("some string"));
System.out.println("123456" + " = " + DateUtil.convertToDate("123456"));
System.out.println("null" + " = " + DateUtil.convertToDate(null));
}
}

Результат:

10/14/2012 = Sun Oct 14 00:00:00 CEST 2012
10-Jan-2012 = Tue Jan 10 00:00:00 CET 2012
01.03.2002 = Fri Mar 01 00:00:00 CET 2002
12/03/2010 = Fri Dec 03 00:00:00 CET 2010
19.Feb.2011 = Sat Feb 19 00:00:00 CET 2011
4/20/2012 = Fri Apr 20 00:00:00 CEST 2012
some string = null
123456 = null
null = null

Строки​

Оптимизация строки​

Для конкатенации (сложения) строк в Java используется оператор «+», для примера, в цикле for новый объект может создаваться для каждой новой строки, что приводит к потере памяти и увеличению времени работы программы.

https://tproger.ru/quiz/test-your-java/
Необходимо избегать создания 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».
https://tproger.ru/jobs/android-developer/?utm_source=in_text
Двойные кавычки обрабатывают символы как строки, но одинарные кавычки ведут себя иначе. Они преобразуют символьные операнды ('H' и 'a') в целые значения посредством расширения примитивных типов — получается 169.

Математика​

Float или 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;
}

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

Возведение в степень​

Возвести число в степень можно двумя способами:

  1. простое умножение;
  2. используя метод Math.pow() (двойное основание, двойной показатель степени).
Использование библиотечной функции рекомендуется только в случае крайней необходимости, например, в случае дробной или отрицательной степени.

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.

https://tproger.ru/blogs/jvm-insides/

Структуры данных​

Комбинирование хеш-таблиц​

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

import java.util.*;
Map m1 = …;
Map m2 = …;
m2.putAll(m1); // добавить к m2 все элементы из m1

Array или ArrayList?​

Выбор между Array и ArrayList зависит от специфики задачи Java, которую вы хотите решить. Запомните следующие особенности этих типов:

  • Массив имеет фиксированный размер, и память для него выделяется во время объявления, а размер ArrayList может динамически меняться.
  • Массивы Java работают намного быстрее, а в ArrayList намного проще добавлять и удалять элементы.
  • При работе с Array скорее всего возникнет ошибка ArrayIndexOutOfBoundsException.
  • ArrayList может быть только одномерным, когда массивы Java могут быть многомерными.
import java.util.*;

public class ArrayVsArrayList {
public static void main(String[] args) {
// создаем массив
int[] myArray = new int[6];

// ссылаемся на несуществующий индекс
myArray[7]= 10; // ArrayIndexOutOfBoundsException

// создаем ArrayList
List 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));
}

// многомерный массив
int[][][] multiArray = new int[3][3][3];
}
}

JSON​

Сериализация и Десериализация​

JSON — невероятно удобный и полезный синтаксис для хранения и обмена данными. Java полностью поддерживает это.

Прим. перев. Для использования JSON из примера необходимо подключить библиотеку JSON Simple.

Вы можете сериализовать данные следующим образом:

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|IOException|ParseException|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" }
]
}

Прим. перев. В Java проектах очень часто для работы с JSON используют библиотеки Gson от Google или Jackson. Обе библиотеки очень популярны и хорошо поддерживаются. Попробуйте и их.

Ввод и Вывод​

FileOutputStream или 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. Это сэкономит вам время на различные проверки и избавит от многих ошибок.

import java.util.*

public List getLocations() {
List result = someService.getLocationsFromDB();
return result == null ? Collections.emptyList() : result;
}

Создание объектов только когда необходимо​

Создание объектов — одна из самых затратных операций в Java. Лучше всего создавать их только тогда, когда они действительно нужны.

import java.util.ArrayList;
import java.util.List;

public class Employees {
private List Employees;

public List getEmployees() {
// initialization only if necessary
if(null == Employees) {
Employees = new ArrayList();
}

return Employees;
}
}

Deadlocks (Дедлоки)​

Взаимная блокировка (Deadlock) потоков может происходить по многим причинам, и полностью защититься от них в 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.
Прим. перев. PermSize и MaxPermSize, начиная с Java 8 уже больше не используются.

Решение распространенных проблем​

Содержимое директории​

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);
}
}
}

Выполнение консольных команд​

Java позволяет выполнять консольные команды прямо из кода, используя класс Runtime. Очень важно не забывать об обработке исключений.

Например, давайте попробуем открыть файл PDF через терминал Java (на Linux’e):

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 {

// constructor
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|IOException|LineUnavailableException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
new PlaySoundDemo();
}
}

Отправка email​

Отправить электронную почту на Java очень просто. Вам просто нужно установить Java Mail и указать путь к нему в пути к классам проекта.

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, из которого вы можете получить точные координаты (используя Swing для UI)

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) {
}
}


Источник статьи: https://tproger.ru/translations/java-tips-and-tricks-for-begginer/
 
Сверху