• ,

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

Исходная статья лежит по адресу:
http://www.javacodegeeks.com/2015/09/how-to-write-methods-efficiently.html#download

учебник

Опубликовано: Andrey Redko ( Андреем Редько) в Core Java (Java Ядро) 18 сентября 2015г

Эта заметка — часть курса Advanced Java (Продвинутый Java.) нашей академии
Этот курс создан, чтобы помочь вам сделать использование Java более эффективным. Здесь обсуждаются более сложные темы, как создание объектов, распараллеливание, сериализация, рефлексия и многое другое. Эти знания будут гидом для вашего путешествия к вершинам мастерства Java.

Оглавление курса
1. Введение
2. Сигнатура методов
3. Тело метода
4. Перегрузка метода
5. Переопределение метода
6. Встраивание
7. Рекурсия
8. Ссылки метода
9. Неизменность
10. Документирование метода
11. Параметры метода и возвращаемые значения
12. Метод как точка входа в приложение
13. Что дальше
14. Загрузка исходного кода

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

2. Сигнатуры методов
Как вы уже знаете, Java — это объектно-ориентированный язык. По существу, каждый метод Java относится к какой-то части класса (или к самому классу в случае статистического метода). Он имеет правила видимости (или доступности), может быть объявлен абстрактным или финальным и так далее. Однако возможно наиболее важная часть метода — это его сигнатура: тип возвращаемого значения и аргументов, плюс список проверяемых исключений реализации каждого метода, который может быть выброшен (но эта часть раньше использовалась не часто, и еще менее часто в наши дни). Начнем с маленького примера.
1	public static void main( String[] args ) {
2	    // Some implementation here
3	}

Метод main принимает массив строк только как аргумент args и ничего не возвращает. Это могло бы быть очень приятно — делать все методы такими простыми как main. Но в реальности сигнатура метода может стать нечитаемой. Давайте взглянем на следующий пример:
1	public void setTitleVisible( int lenght, String title, boolean visible ) {
2	    // Some implementation here
3	}

Первое что здесь заметно, что условные обозначения изначально используются в названиях методов Java, например setTitleVisible.Имя хорошо подобрано и пытается описать, что в методе полагается сделать.
Второе, имена аргументов говорят (или по крайней мере намекают) насчет их цели. Это очень важно найти правильные, толковые имена для аргументов метода, вместо int i, String s, boolean f(в очень редких случаях это, однако, имеет смысл).
Третье, метод имеет только три аргумента. Хотя Java имеет гораздо больший предел разрешенного числа аргументов, настоятельно рекомендовано не превышать количество аргументов больше 6. Выход за эти рамки делает сигнатуру тяжело понимаемой.
С тех пор как была выпущена Java 5, методы могут иметь различный список аргументов одинакового типа (названный varargs — переменные аргументы) и использовать специальный синтаксис, например:
1	public void find( String … elements ) {
2	    // Some implementation here
3	}

Внутренне, компилятор Java конвертирует переменные аргументы в массив соответствующих типов и, таким образом, переменные аргументы могут быть приняты для реализации метода.
Интересно, Java также разрешает декларировать varargs используя параметры типа generic. Однако, потому что тип аргумента неизвестен, компилятор Java хочет быть уверенным что varargs используются правильно и советует методы final снабжать комментариями с пометкой @SafeVarargs(более детальная информация содержится в части 5 учебника, How and when to use Enums and Annotations (как и когда мы используем Перечисления и Комментарии). Например:
1	@SafeVarargs
2	final public< T > void find( T ... elements ) {
3	    // Some implementation here
4	}

Другой ближайший путь это использовать @SuppressWarnings комментарии, например
1	@SuppressWarnings( "unchecked" )
2	public< T > void findSuppressed( T ... elements ) {
3	    // Some implementation here
4	}

Следующий пример демонстрирует использование проверки исключений как части сигнатуры метода. В недалеком прошлом проверка исключений показала себя не настолько полезной, какой она предполагалась быть, в результате шаблонный код был использован скорее для записи, чем для решения проблем.
1	public void write( File file ) throws IOException {
2	    // Some implementation here
3	}

Последнее, но, тем не менее, важное, как правило, рекомендуется (но редко используется), отметить аргументы метода, как final. Это поможет избавиться от практики написания плохого кода, когда аргументы метода предназначены различным значениям. Кроме того, такие аргументы метода могут быть использованы анонимными классами (подробнее об анонимных классов рассматривается в части 3 учебника,, How to design Classes and Interfaces (Как проектировать Классы и Интерфейсы)), хотя Java 8 облегчила немного это ограничение путем введения эффективных final переменных.

3. Тело метода
Каждый метод имеет свою реализацию и цель существования. Однако, имеется пара общих рекомендаций которые реально помогают написанию ясных и понятных методов.
Вероятно, наиболее важный принцип — это принцип единичной ответственности: нужно пытаться реализовать метод таким путем, чтобы каждый единичный метод делал что-то одно, и делал это хорошо. Следуя этому принципу возможно раздувание количества методов класса, и важно найти правильный баланс.
Другая важная вещь в процессе кодирования и проектирования — это делать реализуемые методы короткими. Для коротких методов легко понять причину, по которой они сделаны, плюс они обычно умещаются на экран, и таким образом могут быть очень быстро поняты читателем вашего кода.
Последний по порядку (но не по значению) совет связан с использованием return операторов. Если метод возвращает некоторое значение, пытайтесь минимизировать число мест, где return значение было бы вызвано (некоторые люди идут даже дальше и рекомендуют использовать лишь единичное return значение во всех случаях. Чем больше return значений имеет метод, тем более тяжело становится следовать его логике и модифицировать (или оптимизировать) реализацию.

4. Перегрузка метода
Техника перегрузки методов часто используется, чтобы обеспечить специализацию версий метода для различных типов аргументов или их комбинаций. Хотя имя метода одинаковое компьютер выбирает правильную альтернативу, углубляясь в текущие значения аргументов в точке вызова (лучший пример перегрузки это конструкторы Java: имя всегда одинаковое, но аргументы разные) или вызывает ошибку компилятора, если такой вариант метода не найден. Например:
1	public String numberToString( Long number ) {
2	    return Long.toString( number );
3	}
4	 
5	public String numberToString( BigDecimal number ) {
6	    return number.toString();
7	}

Перегрузка метода отчасти близка к дженерикам (больше информации о дженериках можно найти в части 4 учебника How and when to use Generics (Как и когда использовать дженерики)), однако перегрузка используется в случае, где подход с использованием дженериков не работает хорошо и каждый или большинство типов аргументов, которые являются дженериками, требуют своих собственных специализированных реализаций. Тем не менее, комбинируя оба способа дженерики и перегрузку можно быть очень производительным, но часто это невозможно в Java, потому что тип стирается (больше информации в части 4 учебника How and when to use Generics (Как и когда использовать дженерики)). Давайте взглянем на пример:
1	public< T extends Number > String numberToString( T number ) {
2	    return number.toString();
3	}
4	 
5	public String numberToString( BigDecimal number ) {
6	    return number.toPlainString();
7	}

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

5. Переопределение метода
Мы много говорили о переопределении методов в части 3 учебника (How to design Classes and Interfaces (Как проектировать классы и интерфейсы). В этом разделе, когда мы уже знаем о перегрузке методов, мы собираемся показать, почему использование @Override аннотации так важно. Наш пример продемонстрирует тонкое различие между переопределением метода и перегрузкой метода в простой иерархии классов.
1	public class Parent {
2	    public Object toObject( Number number ) {
3	        return number.toString();
4	    }
5	}

Родительский класс имеет только один метод toObject. Давайте создадим подкласс этого класса и попытаемся придумать версию метода преобразования чисел в строки (вместо необработанных объектов).
1	public class Child extends Parent {
2	    @Override
3	    public String toObject( Number number ) {
4	        return number.toString();
5	    }
6	}

Тем не менее, сигнатура метода toObject в дочернем классе немногим отличается (см Covariant method return types (Ковариантные типы возвращаемые методами) для более подробной информации), и это делает переопределение его из суперкласса в свой класс, при этом компилятор Java не выдает никаких ошибок и предупреждений. Теперь, давайте добавим еще один метод к дочернему классу.
1	public class Child extends Parent {
2	    public String toObject( Double number ) {
3	        return number.toString();
4	    }
5	}

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

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

7. Рекурсия

Рекурсия в Java — это техника, где метод вызывает сам себя, выполняя расчеты. Например, давайте взглянем на следующий пример, который суммирует число массива:
1	public int sum( int[] numbers ) {
2	    if( numbers.length == 0 ) {
3	        return 0;
4	    } if( numbers.length == 1 ) {
5	        return numbers[ 0 ];
6	    } else {
7	        return numbers[ 0 ] + sum( Arrays.copyOfRange( numbers, 1, numbers.length ) );
8	    }
9	}

Это очень неэффективная реализация, однако она демонстрирует рекурсию достаточно хорошо. Существует одна хорошо известная проблема с рекурсивными методами: в зависимости, насколько глубока цепь вызовов, они могут переполнить стек и вызвать исключение StackOverflowError. Но не все так плохо, как кажется, потому что есть техника, которая может устранить переполнение стека, называемая tail call optimization (оптимизация хвоста вызова). Она может быть применена, если метод с хвостовой рекурсией (методы с хвостовой рекурсией это методы, в которых все рекурсивные вызовы это хвостовые вызовы). Например, давайте перепишем предыдущий алгоритм с использованием в хвостовой рекурсии:
01	public int sum( int initial, int[] numbers ) {
02	    if( numbers.length == 0 ) {
03	        return initial;
04	    } if( numbers.length == 1 ) {
05	        return initial + numbers[ 0 ];
06	    } else {
07	        return sum( initial + numbers[ 0 ],
08	            Arrays.copyOfRange( numbers, 1, numbers.length ) );
09	    }
10	}

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

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

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

Давайте взглянем на небольшой пример того, как методы могут быть использованы в качестве аргументов других методов.
01	public class MethodReference {
02	    public static void println( String s ) {
03	        System.out.println( s );
04	    }
05	 
06	    public static void main( String[] args ) {
07	        final Collection< String > strings = Arrays.asList( "s1", "s2", "s3" );
08	        strings.stream().forEach( MethodReference::println );
09	    }
10	}

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

9. Неизменность
Неизменность обращает на себя много внимания в эти дни, и Java не является исключением. Хорошо известно, что неизменности трудно добиться в Java, но это не значит, что это должно быть проигнорировано.
В Java, неизменность — это все знания об изменении внутреннего состояния. В качестве примера, давайте взглянем на спецификации JavaBeans (http://docs.oracle.com/javase/tutorial/javabeans/). В ней говорится, очень ясно, что сеттеры могут изменить состояние объекта, что- то до этого содержащего, и это то, что ожидает каждый разработчик Java.
Тем не менее, альтернативный подход мог бы не менять состояние, а возвращать новый объект (new) каждый раз. Это не так страшно, как кажется, и новый Java 8 Date/Time API ( разработан под JSR 310: Date and Time API прикрытием) является отличным примером этого. Давайте взглянем на следующий фрагмент кода:
1	final LocalDateTime now = LocalDateTime.now();
2	final LocalDateTime tomorrow = now.plusHours( 24 );
3	 
4	final LocalDateTime midnight = now
5	    .withHour( 0 )
6	    .withMinute( 0 )
7	    .withSecond( 0 )
8	    .withNano( 0 );

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

10. Документирование метода
В Java, в частности, если вы разрабатываете какую-то библиотеку или framework, все публичные методы должны быть задокументированы с помощью инструмента Javadoc (http://www.oracle.com/technetwork/articles/java/index-jsp-135444.html). Строго говоря, ничего не заставляет вас делать это, но хорошая документация помогает другим разработчикам понять, что конкретный метод делает, какие аргументы он требует, каковы предположения или ограничения его реализации, какие типы исключений он вызывает и когда они возникают, какое может быть возвращаемое значение (если таковые имеются), а также многие другие вещи.
Давайте взглянем на следующий пример:
01	/**
02	 * The method parses the string argument as a signed decimal integer.
03	 * The characters in the string must all be decimal digits, except
04	 * that the first character may be a minus sign {@code '-'} or plus
05	 * sign {@code '+'}.
06	 *
07	 * <p>An exception of type {@code NumberFormatException} is thrown if
08	 * string is {@code null} or has length of zero.
09	 *
10	 * <p>Examples:
11	 * <blockquote><pre>
12	 * parse( "0" ) returns 0
13	 * parse( "+42") returns 42
14	 * parse( "-2" ) returns -2
15	 * parse( "string" ) throws a NumberFormatException
16	 * </pre></blockquote>
17	 *
18	 * @param str a {@code String} containing the {@code int} representation to be parsed
19	 * @return the integer value represented by the string
20	 * @exception NumberFormatException if the string does not contain a valid integer value
21	 */
22	public int parse( String str ) throws NumberFormatException {
23	    return Integer.parseInt( str );
24	}

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

11. Параметры метода и возвращаемые значения
Документирование ваших методов — это великая вещь, но, к сожалению, это не предупреждает случаи, когда метод называют, используя неправильные или неожиданные значения аргументов. Из-за этого, как правило, все публичные методы должны подтвердить свои аргументы и никогда не должны быть уверены, что все время при вызове будут указаны правильные значения (паттерн более известный как sanity checks (санитарная проверка)).
Возвращаясь к нашему примеру из предыдущего раздела, метод parse должен выполнить проверку своего единственного аргумента, прежде чем делать что-нибудь с ним:
1	public int parse( String str ) throws NumberFormatException {
2	    if( str == null ) {
3	        throw new IllegalArgumentException( "String should not be null" );
4	    }
5	 
6	    return Integer.parseInt( str );
7	}

Java имеет другой вариант выполнения проверки и sanity checks, используя assert операторы. Однако, те, которые могли быть выключены во время выполнения и могут быть не выполнены. Предпочтительно, всегда выполнять такие проверки и вызывать соответствующие исключения.
Даже имея документированные методы и проверку их аргументов, хочу сделать еще пару замечаний связанных с возвращаемыми значениями. До того как вышла Java 8, самым простым способом сказать что метод в данное время не имеет значения чтобы его возвратить было просто вернуть нуль. Вот почему Java так плохо получить исключение NullPointerException. Java 8 пытается решить этот вопрос с введением Optional class. Давайте взглянем на этот пример:
1	public< T > Optional< T > find( String id ) {
2	    // Some implementation here
3	}

Optional предоставляет много полезных методов, и полностью устраняет необходимость возвращать в методе null и загрязнять везде ваш код проверками на null. Единственное исключение, вероятно, это коллекции. Всякий раз, когда метод возвращает коллекцию, всегда лучше вернуть null вместо null (и даже Optional ), например:
1	public< T > Collection< T > find( String id ) {
2	return Collections.emptyList();
3	}

12. Метод как точка входа в приложение
Даже если вы простой разработчик пишущий приложения в вашей организации или участник в одной из самых популярных Java framework or library, проектные решения, которые вы принимаете, играют очень важную роль в том, как ваш код будет использоваться.
В то время как методические рекомендации проектирования API стоят нескольких книг, эта часть учебника касается многих из них (как методы становятся точкой входа в API), таким образом, краткий обзор будет очень полезен:
• Используйте осмысленные имена для методов и их аргументов (Method signatures)
Старайтесь, чтобы количество аргументов, чтобы быть меньше 6-ти (раздел Method signatures)
• Сохраняйте ваши методы короткими и читабельными (раздел Method body и Inlining)
• Всегда документируйте открытые методы, в том числе предварительных условий и примеры, если это имеет смысл (раздел Method Documentation)
• Всегда выполняйте проверку аргументов и sanity checks (section Method Parameters and Return Values)
• Старайтесь избежать null, как возвращаемое значения (раздел Method Parameters and Return Values)
• Всякий раз, когда это имеет смысл, попробуйте проектировать неизменные методы (которые не влияют на внутреннее состояние, раздел Immutability)
• Используйте правила видимости и доступности, чтобы скрыть методы, которые не должны быть публичными (часть 3 учебника, How to design Classes and Interfaces)

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

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

1 комментарий

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