JavaRush /Java блог /Random UA /Як писати методи ефективно (переклад статті)
pena
41 рівень
Москва

Як писати методи ефективно (переклад статті)

Стаття з групи Random UA
Вихідна стаття лежить за адресаою: 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 } Останнє, але, тим не менш, важливе, як правило, рекомендується (але рідко використовується), відзначити аргументи методу, як кінець. Це допоможе позбавитися практики написання поганого коду, коли аргументи методу призначені різним значенням. Крім того, такі аргументи методу можуть бути використані анонімними класами (докладніше про анонімних класів розглядається в частині 3 підручника, , How to design Classes and Interfaces (Як проектувати Класи та Інтерфейси)), хоча Java 8 полегшила трохи це обмеження шляхом введення ефективних final змінних .
3. Тіло методу
Кожен метод має свою реалізацію та мету існування. Однак, є пара загальних рекомендацій, які реально допомагають написанню ясних і зрозумілих методів. Мабуть, найважливіший принцип - це принцип одиничної відповідальності: потрібно намагатися реалізувати метод таким шляхом, щоб кожен одиничний метод робив щось одне, і робив це добре. Дотримуючись цього принципу, можливе роздування кількості методів класу, і важливо знайти правильний баланс. Інша важлива річ у процесі кодування та проектування - це робити реалізовані методи короткими. Для коротких методів легко зрозуміти причину, через яку вони зроблені, плюс вони зазвичай вміщаються на екран, і таким чином можуть бути дуже швидко зрозумілі читачем вашого коду. Остання по порядку (але не за значенням) рада пов'язана з використанням оборотних операторів.
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 підручника (Як проектувати класи та інтерфейси) У цьому розділі, коли ми вже знаємо про навантаження методів, ми збираємося показати, чому використання @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 *

An exception of type {@code NumberFormatException} is thrown if 08 * string is {@code null} or has length of zero. 09 * 10 *

Examples: 11 *

12	 * parse( "0" ) returns 0
13	 * parse( "+42") returns 42
14	 * parse( "-2" ) returns -2
15	 * parse( "string" ) throws a NumberFormatException
16	 * 
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 < T >), наприклад: 1 public< T > Collection< T > find( String id ) { 2 return Collections.emptyList(); 3 }
12. Метод як точка входу у додаток
Навіть якщо ви простий розробник пише програми у вашій організації або учасник в одній з найпопулярніших Java framework або library, проектні рішення, які ви приймаєте, відіграють дуже важливу роль у тому, як ваш код буде використовуватися. У той час як методичні рекомендації проектування API стоять кількох книг, ця частина підручника стосується багатьох з них (як методи стають точкою входу в API), таким чином, короткий огляд буде корисним: • Використовуйте осмислені імена для методів та їх аргументів (Method signatures ) Намагайтеся, щоб кількість аргументів, щоб бути менше 6-ти (розділ Method signatures) • Зберігайте ваші методи короткими та читабельними (розділ Method body та Inlining) • Завжди документуйте відкриті методи, у тому числі попередніх умов та приклади,
13. Що далі
Ця частина підручника говорить трохи менше про Java як про мову, але більше про те, як використовувати мову Java ефективно, зокрема написання читаних, чистих, задокументованих та ефективних методів. У наступному розділі ми будемо продовжувати ту ж основну ідею та обговорювати загальні принципи програмування, які призначені, щоб допомогти вам як розробнику Java стати кращим.
14. Завантаження вихідного коду
Це був урок, присвячений тому, як ефективно писати методи. Ви можете завантажити вихідний код тут:
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ