11 November 2011

Маленькие хитрости Java. Часть 2

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

Buffered Streams

//медленно
InputStream is = new FileInputStream(file);
int val;
while ((val = is.read()) != -1) {
}
//быстро
InputStream is = new BufferedInputStream(new FileInputStream(file));
int val;
while ((val = is.read()) != -1) {
}

Казалось бы — очевидная истина, неправда ли? Но как показал чужой код и опыт собеседования кандидатов, часть разработчиков определенно не понимает в чем преимущество буферизованных стримов. Кто до сих пор не разобрался — метод read() класса FileInputStream:
public native int read() throws IOException;

Согласитесь, каждый раз делать системный вызов, чтобы считать один байт несколько расточительно. Собственно для того, чтобы избежать этой проблемы и были созданы оболочки-буферы. Все что они делают — при первом вызове системного read() считывают несколько больше (в зависимости от указанного размера буфера, котрый по умолчанию равен 8 кб) и при следующем вызове read() считывают данные уже из буфера. Прирост производительности — на порядок. Системные вызовы, на самом деле, это не всегда плохо, например:
System.arraycopy(src, srcPos, dest, destPos, length);

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

Enum vs String

//медленно
String status = plan.getStatus();
if (status.equals("draft")) {
//do stuff
} else if (status.equals("submitted")) {
//do stuff
}
//быстро
PlanStatus status = plan.getStatus();
if (status == PlanStatus.DRAFT) {
//do stuff
} else if (status == PlanStatus.SUBMITTED) {
//do stuff
}

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

ArrayList vs HashSet

Очень внимательно подходите к вопросу хранения данных в коллекциях. Очень часто Set используется там, где можно обойтись списком, а список там, где можна обойтись массивом. Например в классах, которые мапятся на таблицы баз данных. Избегайте этого, стройте правильные зависимости и корректные запросы. Это позволит секономить Вам немного памяти, ведь не для кого не секрет, что HashSet реализован на HashMap, а HashMap в свою очередь использует внутренний класс Entry, который помимо самого ключа и значения хранит также хеш и ссылку на следующий обьект Entry. В дополнение — постоянно при добавлении вычисляется хеш, который для сложных обьектов может стоить дороговато.

Data reading

//плохо
byte[] fileBinaryData = readFile(filepath);
//получше
InputStream fileStream = readFile(filepath);

Что плохого в этих невинных строках? Да в общем-то ничего, кроме того факта, что мы не знаем размер файла который считываем. Вместо файла может быть что угодно — открытый сокет, который принимает данные, данные из post запроса, все… В общем случае старайтесь не считывать всё в массив байтов, у вас попросту может не хватить памяти. Поэтому будьте осторожны с размером любых данных. Старайтесь обрабатывать и пересылать данные по частям. И всегда следите за размером пользовательских данных и по возможности ограничивайте их.

Log field

//плохо
private Logger logger = Logger.getLogger(Plan.class);
//хорошо
private static final Logger logger = Logger.getLogger(Plan.class);

Используете логированние в классе? Всегда определяйте переменную лога как static final. Во-первых: у вас никогда не возникнет проблем при попытках сериализации и десериализации обьекта. Во-вторых: инициализация происходит только 1 раз, вместо постоянной инициализации при создании обьектов класса.

Fields initialization

//плохо
class Plan {
	private String name = null;
	private Boolean isNew = false;
	private Set<Document> documents = new HashSet<Document>();
}
//хорошо
class Plan {
	private String name;
	private Boolean isNew;
	private Set<Document> documents;
}

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

Empty String

//медленно
if (name.equals("")) {
//быстро
if (name.isEmpty()) {

Если вам нужно проверить, содержит ли строка пустое значение — используйте метод isEmpty(). Почему не equals()? Он банально медленней. Если вы просмотрите его реализацию для строки, то сразу все поймете. Не удивляйтесь, многие разработчики до сих пор не знают про этот метод.

Object[] vs custom Class

//плохо
Object[] data = new Object[3];
data[0] = row.getUserCount();
data[1] = row.getOwnerCount();
data[2] = row.getLocations();
return data;
//получше
RowResult data = new RowResult();
data.setUserCount(row.getUserCount());
data.setOwnerCount(row.getOwnerCount());
data.setLocations(row.getLocations());
return data;

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

Anonymous Classes

Аннонимные классы — это отличная фича, но не пихайте их во все места Вашего приложения только потому, что это сэкономит Вам минуту времени. Код с анонимными классами читается гораздо трудней и сложен для восприятия. Лучше же, конечно, выносить в отдельные классы, кроме случаев, когда аннонимный класс очень уместен.

Comparison order

//плохо
if (name.equals("broadcast")) {
//хорошо
if ("broadcast".equals(name)) {

Этот способ позволяет избавится от лишней null проверки. Но если придерживаться правил из первой статьи, то в этом способе нету необходимости.

Выводы

Да, многое из написанного — очевиднее очевидного. Но если так, то откуда же это все берется от проекта к проекту? Эта статья ни в коем случае не пособие по улучшению производительности вашего приложения. Это просто хороший, правильный стиль. Надеюсь это поможет Вам.
Tags:javaтрюкихитростихороший код
Hubs: Java
+75
105.8k 608
Comments 91
Popular right now
Java QA Engineer
December 21, 202060,000 ₽OTUS
Java Developer. Professional
December 22, 202060,000 ₽OTUS
Рефакторинг кода .NET
December 7, 202030,200 ₽Luxoft Training
Java Junior Developer
February 17, 202123,990 ₽Level UP
Top of the last 24 hours