Статьи

Общие рекомендации по программированию

Эта статья является частью нашего Академического курса под названием Advanced Java .

Этот курс призван помочь вам наиболее эффективно использовать Java. В нем обсуждаются сложные темы, включая создание объектов, параллелизм, сериализацию, рефлексию и многое другое. Он проведет вас через ваше путешествие в мастерство Java! Проверьте это здесь !

1. Введение

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

2. Переменные области

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

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

1
2
3
4
5
6
7
for( final Locale locale: Locale.getAvailableLocales() ) {
    // Some implementation here
}
 
try( final InputStream in = new FileInputStream( "file.txt" ) ) {
    // Some implementation here
}

В обоих фрагментах кода область действия локальных переменных ограничена блоками выполнения, в которых они объявлены. Как только блок заканчивается, локальная переменная выходит из области видимости и больше не отображается. Это выглядит чистым и лаконичным, однако, с выпуском Java 8 и введением лямбд, многие известные идиомы использования локальных переменных становятся устаревшими. Давайте перепишем цикл for-each из предыдущего примера, чтобы использовать вместо него лямбда-выражения:

1
2
3
4
Arrays.stream( Locale.getAvailableLocales() ).forEach( ( locale ) -> {
        // Some implementation here
    }
);

Локальная переменная стала аргументом функции, которая сама передается как аргумент метода forEach .

3. Поля класса и локальные переменные

Каждый метод в Java принадлежит некоторому классу (или некоторому интерфейсу в случае Java 8, и метод объявлен по default ). Таким образом, существует вероятность конфликта имен между локальными переменными, используемыми в реализациях методов, и членами класса. Компилятор Java может выбрать правильную переменную из области видимости, хотя это может быть не тот разработчик, который намеревался использовать. Современные Java IDE проделывают огромную работу, чтобы намекнуть разработчикам, когда происходят такие конфликты (предупреждения, выделения, …), но все же лучше об этом подумать при разработке. Давайте посмотрим на этот пример:

01
02
03
04
05
06
07
08
09
10
11
12
public class LocalVariableAndClassMember {
     private long value;
 
     public long calculateValue( final long initial ) {
         long value = initial;       
 
         value *= 10;
         value += value;
 
         return value;
     }
 }

Пример выглядит довольно просто, однако здесь есть одна загвоздка. Метод calculateValue вводит локальную переменную с именем name и тем самым скрывает члена класса с тем же именем. Строка 08 должна была суммировать член класса и локальную переменную, но вместо этого она делает что-то совсем другое. Правильная версия может выглядеть так (используя ключевое слово this ):

01
02
03
04
05
06
07
08
09
10
11
12
public class LocalVariableAndClassMember {
     private long value;
 
     public long calculateValue( final long initial ) {
         long value = initial;       
 
         value *= 10;
         value += this.value;       
 
         return value;
     }
 }

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

4. Аргументы метода и локальные переменные

Еще одна ловушка, в которую нередко попадают неопытные Java-разработчики, — использование аргументов метода в качестве локальных переменных. Java позволяет переназначать неконечные аргументы метода с другим значением (однако это никак не влияет на исходное значение). Например:

1
2
3
4
5
6
7
8
public String sanitize( String str ) {
    if( !str.isEmpty() ) {
        str = str.trim();
    }
 
    str = str.toLowerCase();
    return str;
}

Это не красивый кусок кода, но он достаточно хорош для выявления проблемы: аргумент метода str переназначается на другое значение (и в основном используется как локальная переменная). Во всех случаях (без исключения) этого паттерна можно и нужно избегать (например, объявив аргументы метода как final ). Например:

01
02
03
04
05
06
07
08
09
10
public String sanitize( final String str ) {
    String sanitized = str;
 
    if( !str.isEmpty() ) {
        sanitized = str.trim();
    }
 
    sanitized = sanitized.toLowerCase();
    return sanitized;
}

Код, который следует этому простому правилу, намного легче следовать и рассуждать, даже ценой введения локальных переменных.

5. Бокс и распаковка

Упаковка и распаковка — это все имена одного и того же метода, используемого в языке Java для преобразования между примитивными типами (например, int , long , double ) в соответствующие оболочки примитивных типов (например, Integer , Long , Double ). В четвертой части руководства « Как и когда использовать Generics» мы уже видели это в действии, когда говорили об обертках примитивного типа в качестве параметров универсального типа.

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

1
2
3
public static void calculate( final long value ) {
    // Some implementation here
}
1
2
final Long value = null;
calculate( value );

Приведенный выше фрагмент кода прекрасно компилируется, однако он генерирует NullPointerException в строке 02 когда происходит преобразование между Long и Long . Здесь совет будет предпочесть использование примитивного типа (однако, как мы уже знаем, это не всегда возможно).

6. Интерфейсы

В третьей части руководства « Как проектировать классы и интерфейсы» мы обсуждали интерфейсы и разработку на основе контрактов, уделяя особое внимание тому факту, что интерфейсы должны быть предпочтительнее конкретных классов, где это возможно. Цель этого раздела — убедить вас еще раз рассмотреть интерфейсы, демонстрируя реальные примеры.

Интерфейсы не привязаны к какой-либо конкретной реализации (исключение составляют методы по умолчанию). Это просто контракты, и как таковые они предоставляют большую свободу и гибкость в способах выполнения контрактов. Эта гибкость становится все более важной, когда реализация включает внешние системы или службы. Давайте посмотрим на следующий простой интерфейс и его возможную реализацию:

1
2
3
public interface TimezoneService {
    TimeZone getTimeZone( final double lat, final double lon ) throws IOException;
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TimezoneServiceImpl implements TimezoneService {
    @Override
    public TimeZone getTimeZone(final double lat, final double lon) throws IOException {
        final URL url = new URL( String.format(
            lat, lon
        ) );
        final HttpURLConnection connection = ( HttpURLConnection )url.openConnection();
 
        connection.setRequestMethod( "GET" );
        connection.setConnectTimeout( 1000 );
        connection.setReadTimeout( 1000 );
        connection.connect();
 
        int status = connection.getResponseCode();
        if (status == 200) {
            // Do something here
        }
 
        return TimeZone.getDefault();
    }
}

Приведенный выше фрагмент кода демонстрирует типичный шаблон интерфейса / реализации. В реализации используется внешний HTTP-сервис ( http://api.geonames.org/ ) для извлечения часового пояса для определенного местоположения. Однако, поскольку контакт управляется интерфейсом, очень легко представить еще одну реализацию, используя, например, базу данных или даже плоский файл. При этом интерфейсы очень помогают в разработке тестируемого кода. Например, не всегда практично вызывать внешнюю службу при каждом запуске теста, поэтому имеет смысл вместо этого предоставить альтернативную, фиктивную реализацию (также известную как заглушка или макет):

1
2
3
4
5
6
public class TimezoneServiceTestImpl implements TimezoneService {
    @Override
    public TimeZone getTimeZone(final double lat, final double lon) throws IOException {
        return TimeZone.getDefault();
    }
}

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

Многие превосходные примеры правильного использования интерфейсов инкапсулированы в стандартной библиотеке коллекции Java. Collection , List , Set , все эти интерфейсы поддерживаются несколькими реализациями, которые могут быть заменены беспрепятственно и взаимозаменяемо при предпочтении контрактов, например:

1
2
3
4
5
public static< T > void print( final Collection< T > collection ) {
    for( final T element: collection ) {
        System.out.println( element );
    }
}
1
2
3
4
print( new HashSet< Object >( /* ... */ ) );
print( new ArrayList< Integer >( /* ... */ ) );
print( new TreeSet< String >( /* ... */ ) );
print( new Vector< Long >( /* ... */ ) );

7. Струны

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

В Java строки являются неизменяемыми объектами, представленными в формате UTF-16. Каждый раз, когда вы объединяете строки (или выполняете любую операцию, которая изменяет исходную строку), создается новый экземпляр класса String . Из-за этого факта операции конкатенации могут стать очень неэффективными, что приведет к созданию множества экземпляров промежуточных строк (вообще говоря, генерации мусора).

Но стандартная библиотека Java предоставляет два очень полезных класса, которые призваны облегчить манипуляции со строками: StringBuilder и StringBuffer (единственное различие между ними заключается в том, что StringBuffer является поточно- StringBuilder а StringBuilder — нет). Давайте посмотрим на пару примеров, используя один из этих классов:

01
02
03
04
05
06
07
08
09
10
final StringBuilder sb = new StringBuilder();
 
for( int i = 1; i <= 10; ++i ) {
    sb.append( " " );
    sb.append( i );
}
 
sb.deleteCharAt( 0 );
sb.insert( 0, "[" );
sb.replace( sb.length() - 3, sb.length(), "]" );

Хотя использование StringBuilder / StringBuffer является рекомендуемым способом манипулирования строками, в простом сценарии объединения двух или трех строк он может показаться чрезмерным, поэтому вместо него можно использовать обычный оператор +, например:

1
String userId = "user:" + new Random().nextInt( 100 );

Часто лучшей альтернативой прямой конкатенации является использование строкового форматирования, и стандартная библиотека Java также здесь, чтобы помочь, предоставив статический вспомогательный метод String.format . Он поддерживает широкий набор спецификаторов формата, включая цифры, символы, даты / время и т. Д. (Для полной ссылки, пожалуйста, посетите официальную документацию ). Давайте рассмотрим возможности форматирования на примере:

1
2
3
4
5
String.format( "%04d", 1 );                      -> 0001
String.format( "%.2f", 12.324234d );             -> 12.32
String.format( "%tR", new Date() );              -> 21:11
String.format( "%tF", new Date() );              -> 2014-11-11
String.format( "%d%%", 12 );                     -> 12%

Метод String.format обеспечивает простой и String.format подход к построению строк из разных типов данных. Стоит отметить, что некоторые современные Java IDE могут анализировать спецификацию формата по аргументам, переданным методу String.format и предупреждать разработчиков в случае обнаружения любых несоответствий.

8. Соглашения об именах

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

  • имена пакетов печатаются в нижнем регистре : org.junit , com.fasterxml.jackson , javax.json
  • имена классов, перечислений, интерфейсов или аннотаций набираются заглавными буквами : StringBuilder , Runnable , @Override
  • имена методов или полей (кроме static final ) набираются в случае верблюда : isEmpty , format , addAll
  • Имена статических конечных полей или констант перечисления набираются в верхнем регистре , с разделителями подчеркиванием ‘_’: LOG , MIN_RADIX , INSTANCE
  • имена аргументов локальной переменной и метода набираются в случае верблюда : str , newLength , minimumCapacity
  • имена параметров универсального типа обычно представлены одним символом в верхнем регистре : T , U , E

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

9. Стандартные библиотеки

Неважно, над какими проектами Java вы работаете, стандартные библиотеки Java — ваши лучшие друзья. Да, трудно не согласиться с тем, что у них есть некоторые острые углы и странные дизайнерские решения, тем не менее, на 99% это высококачественный код, написанный экспертами. Это стоит учиться.

Каждый выпуск Java добавляет много новых функций к существующим библиотекам (с некоторым возможным устареванием старых), а также добавляет много новых библиотек. В Java 5 появилась новая библиотека параллелизма, согласованная с пакетом java.util.concurrent . Java 6 поставила (немного менее известную) поддержку сценариев (пакет javax.script ) и API компилятора Java (в пакете javax.tools ). Java 7 внесла множество улучшений в java.util.concurrent , представила новую библиотеку ввода / вывода в пакете java.nio.file и поддержку динамических языков с пакетом java.lang.invoke . И наконец, Java 8 предоставила долгожданный API даты / времени, размещенный в пакете java.time .

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

10. Неизменность

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

11. Тестирование

Практика разработки через тестирование (TDD) чрезвычайно популярна в сообществе Java, что повышает планку качества написанного кода. Со всеми этими преимуществами, которые TDD приносит на стол, печально наблюдать, что стандартная библиотека Java не включает в себя какие-либо тестовые среды или леса на сегодняшний день.

Тем не менее, тестирование становится необходимой частью современной разработки Java, и в этом разделе мы собираемся охватить некоторые основы, используя отличную среду JUnit . По сути, в JUnit каждый тест представляет собой набор утверждений о состоянии или поведении ожидаемого объекта.

Секрет написания отличных тестов состоит в том, чтобы сделать их короткими и простыми, проверяя по одной вещи за раз. В качестве упражнения напишем набор тестов, чтобы убедиться, что функция String.format из раздела Strings возвращает желаемые результаты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package com.javacodegeeks.advanced.generic;
 
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.equalTo;
 
import org.junit.Test;
 
public class StringFormatTestCase {
    @Test
    public void testNumberFormattingWithLeadingZeros() {
        final String formatted = String.format( "%04d", 1 );
        assertThat( formatted, equalTo( "0001" ) );
    }
 
    @Test
    public void testDoubleFormattingWithTwoDecimalPoints() {
        final String formatted = String.format( "%.2f", 12.324234d );
        assertThat( formatted, equalTo( "12.32" ) );
    }
}

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

12. Что дальше

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

13. Скачать исходный код

Это был урок по общим правилам программирования, урок курса Advanced Java. Вы можете скачать исходный код здесь: AdvancedJavaPart7