Статьи

Как эффективно писать методы

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

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

1. Введение

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

2. Метод подписи

Как мы уже хорошо знаем, Java является объектно-ориентированным языком. Таким образом, каждый метод в Java принадлежит некоторому экземпляру класса (или самому классу в случае static методов), имеет правила видимости (или доступности), может быть объявлен abstract или final и т. Д. Однако, пожалуй, наиболее важной частью метода является его сигнатура: тип возвращаемого значения и аргументы, а также список проверенных исключений, которые может генерировать реализация метода (но в настоящее время эта часть используется все реже и реже). Вот небольшой пример для начала:

1
2
3
public static void main( String[] args ) {
    // Some implementation here
}

Метод main принимает массив строк как единственный аргумент args и ничего не возвращает. Было бы очень хорошо, чтобы все методы были такими же простыми, как и main . Но в действительности подпись метода может стать нечитаемой. Давайте посмотрим на следующий пример:

1
2
3
public void setTitleVisible( int lenght, String title, boolean visible ) {
    // Some implementation here
}

Первое, на что следует обратить внимание, это то, что по соглашению имена методов в Java пишутся в случае верблюда, например: setTitleVisible . Имя выбрано правильно и пытается описать, что должен делать метод.

Во-вторых, название каждого аргумента говорит (или хотя бы намекает) о его назначении. Очень важно найти правильные, пояснительные имена для аргументов метода вместо int i , String s , boolean f (однако в очень редких случаях это имеет смысл).

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

Начиная с версии Java 5, методы могут иметь переменный список аргументов одного типа (называемых varargs) с использованием специального синтаксиса, например:

1
2
3
public void find( String ... elements ) {
    // Some implementation here
}

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

Интересно, что Java также позволяет объявлять аргумент varargs с помощью параметра универсального типа. Однако, поскольку тип аргумента неизвестен, компилятор Java хочет быть уверенным в том, что varargs используются ответственно, и советует методу быть final и аннотироваться аннотацией @SafeVarargs (более подробно об аннотациях рассказано в части 5. учебника, как и когда использовать перечисления и аннотации ). Например:

1
2
3
4
@SafeVarargs
final public< T > void find( T ... elements ) {
    // Some implementation here
}

Другой способ — использовать аннотацию @SuppressWarnings , например:

1
2
3
4
@SuppressWarnings( "unchecked" )
public< T > void findSuppressed( T ... elements ) {
    // Some implementation here
}

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

1
2
3
public void write( File file ) throws IOException {
    // Some implementation here
}

И последнее, но не менее важное: обычно рекомендуется (но редко используется) пометить аргументы метода как final . Это помогает избавиться от плохой практики кода, когда аргументы метода переназначаются с другими значениями. Кроме того, такие аргументы метода могут использоваться анонимными классами (более подробная информация об анонимных классах описана в части 3 учебного пособия « Как проектировать классы и интерфейсы» ), хотя Java 8 немного ослабила это ограничение, введя эффективно final переменные.

3. Метод тела

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

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

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

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

4. Метод перегрузки

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

1
2
3
4
5
6
7
public String numberToString( Long number ) {
    return Long.toString( number );
}
 
public String numberToString( BigDecimal number ) {
    return number.toString();
}

Перегрузка методов в некоторой степени близка к универсальным (более подробная информация об универсальных схемах описана в части 4 учебника « Как и когда использовать универсальные шаблоны» ), однако она используется в тех случаях, когда универсальный подход не работает должным образом и каждый (или большинство) универсальных Аргументы типа требуют своих собственных специализированных реализаций. Тем не менее, объединение как обобщенных, так и перегрузочных функций может быть очень мощным, но часто невозможным в Java из-за стирания типов (более подробную информацию см. В части 4 руководства « Как и когда использовать обобщенные элементы» ). Давайте посмотрим на этот пример:

1
2
3
4
5
6
7
public< T extends Number > String numberToString( T number ) {
    return number.toString();
}
 
public String numberToString( BigDecimal number ) {
    return number.toPlainString();
}

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

5. Метод переопределения

Мы много говорили о переопределении методов в третьей части руководства « Как проектировать классы и интерфейсы» . В этом разделе, когда мы уже знаем о перегрузке методов, мы собираемся показать, почему использование аннотации @Override так важно. Наш пример продемонстрирует тонкую разницу между переопределением метода и перегрузкой в ​​простой иерархии классов.

1
2
3
4
5
public class Parent {
    public Object toObject( Number number ) {
        return number.toString();
    }
}

Класс Parent имеет только один метод toObject . Давайте создадим подкласс этого класса и попробуем найти версию метода для преобразования чисел в строки (вместо необработанных объектов).

1
2
3
4
5
6
public class Child extends Parent {
    @Override
    public String toObject( Number number ) {
        return number.toString();
    }
}

Тем не менее, сигнатура метода toObject в классе Child немного отличается (см. Подробности в типах возвращаемых данных метода Covariant ), он переопределяет сигнатуру из суперкласса, и компилятор Java не имеет к этому претензий. Теперь давайте добавим еще один метод в класс Child .

1
2
3
4
5
public class Child extends Parent {
    public String toObject( Double number ) {
        return number.toString();
    }
}

Опять же, есть небольшая разница в сигнатуре метода ( Double вместо Number ), но в этом случае это перегруженная версия метода, она не переопределяет родительскую. Вот когда справка от Java-компилятора и аннотации @Override окупаются: аннотирование метода из последнего примера с помощью @Override вызывает ошибку компилятора.

6. Встраивание

Встраивание — это оптимизация, выполняемая компилятором Java JIT (Just-in-Time), чтобы исключить конкретный вызов метода и заменить его непосредственно реализацией метода. Использование JIT-компилятора эвристики зависит как от того, как часто вызывается метод, так и от его размера. Слишком большие методы нельзя эффективно использовать. Встраивание может значительно повысить производительность вашего кода и является еще одним преимуществом сокращения методов, как мы уже обсуждали в разделе «Методы».

7. Рекурсия

Рекурсия в Java — это метод, при котором метод вызывает себя при выполнении вычислений. Например, давайте взглянем на следующий пример, который суммирует числа в массиве:

1
2
3
4
5
6
7
8
9
public int sum( int[] numbers ) {
    if( numbers.length == 0 ) {
        return 0;
    } if( numbers.length == 1 ) {
        return numbers[ 0 ];
    } else {
        return numbers[ 0 ] + sum( Arrays.copyOfRange( numbers, 1, numbers.length ) );
    }
}

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

01
02
03
04
05
06
07
08
09
10
public int sum( int initial, int[] numbers ) {
    if( numbers.length == 0 ) {
        return initial;
    } if( numbers.length == 1 ) {
        return initial + numbers[ 0 ];
    } else {
        return sum( initial + numbers[ 0 ],
            Arrays.copyOfRange( numbers, 1, numbers.length ) );
    }
}

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

8. Ссылки на метод

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

Тип ссылки пример
Ссылка на статический метод SomeClass::staticMethodName
Ссылка на метод экземпляра определенного объекта someInstance::instanceMethodName
Ссылка на метод экземпляра произвольного объекта определенного типа SomeType::methodName
Ссылка на конструктор SomeClass::new

Таблица 1

Давайте посмотрим на быстрый пример того, как методы могут передаваться в качестве аргументов другим методам.

01
02
03
04
05
06
07
08
09
10
public class MethodReference {
    public static void println( String s ) {
        System.out.println( s );
    }
 
    public static void main( String[] args ) {
        final Collection< String > strings = Arrays.asList( "s1", "s2", "s3" );
        strings.stream().forEach( MethodReference::println );
    }
}

Последняя строка метода main использует ссылку на метод println для печати каждого элемента из коллекции строк на консоль, и он передается в качестве аргумента другому методу forEach .

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

В наши дни большое внимание уделяется неизменности, и Java не является исключением. Общеизвестно, что неизменность в Java сложна, но это не значит, что ее следует игнорировать.

В Java неизменность заключается в изменении внутреннего состояния. В качестве примера давайте рассмотрим спецификацию JavaBeans ( http://docs.oracle.com/javase/tutorial/javabeans/ ). В нем очень четко говорится, что сеттеры могут изменять состояние содержащего объекта, и именно этого ожидает каждый разработчик Java.

Однако альтернативный подход заключается не в изменении состояния, а в возврате нового каждый раз. Это не так страшно, как кажется, и новый Java 8 Date / Time API (разработанный в рамках JSR 310: зонтик API Date и Time ) является отличным примером этого. Давайте посмотрим на следующий фрагмент кода:

1
2
3
4
5
6
7
8
final LocalDateTime now = LocalDateTime.now();
final LocalDateTime tomorrow = now.plusHours( 24 );
 
final LocalDateTime midnight = now
    .withHour( 0 )
    .withMinute( 0 )
    .withSecond( 0 )
    .withNano( 0 );

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

10. Методика Документация

В Java, особенно если вы разрабатываете какую-то библиотеку или фреймворк, все публичные методы должны быть документированы с использованием инструмента Javadoc ( http://www.oracle.com/technetwork/articles/java/index-jsp-135444.html ) , Строго говоря, ничто не заставляет вас делать это, но хорошая документация помогает другим разработчикам понять, что делает конкретный метод, какие аргументы он требует, какие допущения или ограничения имеет его реализация, какие типы исключений и когда могут возникать, и что возвращаемое значение (если есть) может быть (плюс еще много вещей).

Давайте посмотрим на следующий пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * The method parses the string argument as a signed decimal integer.
 * The characters in the string must all be decimal digits, except
 * that the first character may be a minus sign {@code '-'} or plus
 * sign {@code '+'}.
 *
 * <p>An exception of type {@code NumberFormatException} is thrown if
 * string is {@code null} or has length of zero.
 *
 * <p>Examples:
 * <blockquote><pre>
 * parse( "0" ) returns 0
 * parse( "+42") returns 42
 * parse( "-2" ) returns -2
 * parse( "string" ) throws a NumberFormatException
 * </pre></blockquote>
 *
 * @param str a {@code String} containing the {@code int} representation to be parsed
 * @return the integer value represented by the string
 * @exception NumberFormatException if the string does not contain a valid integer value
 */
public int parse( String str ) throws NumberFormatException {
    return Integer.parseInt( str );
}

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

6.Javadoc.Eclipse

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

11. Параметры метода и возвращаемые значения

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

Возвращаясь к нашему примеру из предыдущего раздела, метод parse должен выполнить проверку своего единственного аргумента, прежде чем что-либо делать с ним:

1
2
3
4
5
6
7
public int parse( String str ) throws NumberFormatException {
    if( str == null ) {
        throw new IllegalArgumentException( "String should not be null" );
    }
 
    return Integer.parseInt( str );
}

У Java есть еще один вариант для проверки и проверки работоспособности с использованием операторов assert . Тем не менее, они могут быть отключены во время выполнения и не могут быть выполнены. Желательно всегда выполнять такие проверки и выдвигать соответствующие исключения.

Даже имея документированные методы и проверяя их аргументы, нужно упомянуть пару замечаний, касающихся значений, которые они могут вернуть. До Java 8 самый простой способ сказать «у меня нет значения для возврата в это время» — это просто вернуть null . Вот почему Java так печально известна исключениями NullPointerException . Java 8 пытается решить эту проблему с введением Optional < T > класса Optional < T > . Давайте посмотрим на этот пример:

1
2
3
public< T > Optional< T > find( String id ) {
    // Some implementation here
}

Optional < T > предоставляет множество полезных методов и полностью исключает необходимость для метода возвращать null и загрязнять ваш код проверками на ноль везде. Единственное исключение, вероятно, коллекции. Всякий раз, когда метод возвращает коллекцию, всегда лучше возвращать пустой, а не null (и даже Optional < T > ), например:

1
2
3
public&lt; T &gt; Collection&lt; T &gt; find( String id ) {
return Collections.emptyList();
}

12. Методы как точки входа API

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

Хотя руководство по разработке API стоит нескольких книг, эта часть учебника затрагивает многие из них (так как методы становятся точками входа API), поэтому краткое резюме будет очень полезно:

13. Что дальше

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

14. Загрузите исходный код

Это был урок о том, как эффективно писать методы. Вы можете скачать исходный код здесь: advanced-java-part-6