JavaRush /Blog Java /Random-PL /Przerwa kawowa #56. Krótki przewodnik po najlepszych prak...

Przerwa kawowa #56. Krótki przewodnik po najlepszych praktykach w Javie

Opublikowano w grupie Random-PL
Źródło: DZone Ten przewodnik zawiera najlepsze praktyki Java i odniesienia, które poprawiają czytelność i niezawodność kodu. Na programistach spoczywa duża odpowiedzialność za codzienne podejmowanie właściwych decyzji, a najlepszą rzeczą, która może im pomóc w podejmowaniu właściwych decyzji, jest doświadczenie. I choć nie wszyscy mają duże doświadczenie w tworzeniu oprogramowania, każdy może skorzystać z doświadczeń innych. Przygotowałem dla Ciebie kilka rekomendacji, które wyniosłem z moich doświadczeń z Javą. Mam nadzieję, że pomogą Ci poprawić czytelność i niezawodność kodu Java.Przerwa kawowa #56.  Krótki przewodnik po najlepszych praktykach w Javie — 1

Zasady programowania

Nie pisz kodu, który po prostu działa . Staraj się pisać kod, który będzie możliwy do utrzymania — nie tylko przez Ciebie, ale przez kogokolwiek innego, kto może w przyszłości pracować nad oprogramowaniem. Programista spędza 80% swojego czasu na czytaniu kodu, a 20% na pisaniu i testowaniu kodu. Skup się więc na pisaniu czytelnego kodu. Twój kod nie powinien wymagać komentarzy, aby ktokolwiek mógł zrozumieć, co robi. Aby napisać dobry kod, istnieje wiele zasad programowania, które możemy wykorzystać jako wytyczne. Poniżej wymienię te najważniejsze.
  • • KISS – oznacza „Keep It Simple, Stupid”. Możesz zauważyć, że programiści na początku swojej drogi starają się wdrażać złożone, niejednoznaczne projekty.
  • • SUCHE – „Nie powtarzaj się”. Staraj się unikać duplikatów, zamiast tego umieszczaj je w jednej części systemu lub metody.
  • YAGNI – „Nie będziesz tego potrzebować”. Jeśli nagle zaczniesz zadawać sobie pytanie: „A co powiesz na dodanie kolejnych elementów (funkcji, kodu itp.)?”, prawdopodobnie będziesz musiał zastanowić się, czy w ogóle warto je dodawać.
  • Czysty kod zamiast inteligentnego kodu – mówiąc najprościej, zostaw swoje ego za drzwiami i zapomnij o pisaniu inteligentnego kodu. Chcesz czystego kodu, a nie inteligentnego kodu.
  • Unikaj przedwczesnej optymalizacji — problem z przedwczesną optymalizacją polega na tym, że nigdy nie wiadomo, gdzie w programie będą wąskie gardła, dopóki się nie pojawią.
  • Pojedyncza odpowiedzialność – każda klasa lub moduł w programie powinna troszczyć się tylko o zapewnienie jednego fragmentu określonej funkcjonalności.
  • Dziedziczenie kompozycji , a nie implementacji — obiekty o złożonym zachowaniu powinny zawierać instancje obiektów o indywidualnym zachowaniu, zamiast dziedziczyć klasę i dodawać nowe zachowania.
  • Gimnastyka obiektowa to ćwiczenia programistyczne zaprojektowane jako zbiór 9 zasad .
  • Fail fast, stop fast – zasada ta oznacza zatrzymanie bieżącej operacji w przypadku wystąpienia nieoczekiwanego błędu. Przestrzeganie tej zasady prowadzi do bardziej stabilnej pracy.

Pakiety

  1. Nadaj priorytet pakietom strukturalnym według obszaru tematycznego , a nie poziomu technicznego.
  2. Preferuj układy promujące hermetyzację i ukrywanie informacji w celu ochrony przed niewłaściwym użyciem, zamiast organizować zajęcia ze względów technicznych.
  3. Traktuj pakiety tak, jakby miały niezmienny interfejs API - nie ujawniaj wewnętrznych mechanizmów (klas) przeznaczonych wyłącznie do przetwarzania wewnętrznego.
  4. Nie ujawniaj klas, które są przeznaczone do użycia wyłącznie w pakiecie.

Zajęcia

Statyczny

  1. Nie zezwalaj na tworzenie klasy statycznej. Zawsze twórz prywatnego konstruktora.
  2. Klasy statyczne powinny pozostać niezmienne, nie pozwalać na podklasy ani klasy wielowątkowe.
  3. Klasy statyczne powinny być chronione przed zmianami orientacji i powinny być udostępniane jako narzędzia, takie jak filtrowanie list.

Dziedzictwo

  1. Wybierz kompozycję zamiast dziedziczenia.
  2. Nie ustawiaj pól chronionych . Zamiast tego określ bezpieczną metodę dostępu.
  3. Jeśli zmienną klasy można oznaczyć jako final , zrób to.
  4. Jeśli nie oczekuje się dziedziczenia, uczyń klasę ostateczną .
  5. Oznacz metodę jako ostateczną , jeśli nie oczekuje się, że podklasy będą mogły ją zastąpić.
  6. Jeśli konstruktor nie jest wymagany, nie twórz konstruktora domyślnego bez logiki implementacji. Java automatycznie udostępni domyślny konstruktor, jeśli go nie określono.

Interfejsy

  1. Nie używaj wzorca interfejsu stałych, ponieważ umożliwia on klasom implementację i zanieczyszczanie interfejsu API. Zamiast tego użyj klasy statycznej. Ma to tę dodatkową zaletę, że pozwala na bardziej złożoną inicjalizację obiektu w bloku statycznym (np. zapełnianie kolekcji).
  2. Unikaj nadmiernego używania interfejsu .
  3. Posiadanie jednej i tylko jednej klasy implementującej interfejs prawdopodobnie doprowadzi do jego nadużywania i wyrządzi więcej szkody niż pożytku.
  4. „Program dla interfejsu, a nie implementacja” nie oznacza, że ​​powinieneś łączyć każdą klasę domeny z mniej więcej identycznym interfejsem, robiąc to, łamiesz YAGNI .
  5. Zawsze staraj się, aby interfejsy były małe i specyficzne, aby klienci wiedzieli tylko o metodach, które ich interesują. Sprawdź dostawcę usług internetowych firmy SOLID.

Finalizatorzy

  1. Obiektu #finalize() należy używać rozsądnie i wyłącznie w celu ochrony przed awariami podczas czyszczenia zasobów (takich jak zamykanie pliku). Zawsze podawaj jawną metodę czyszczenia (taką jak close() ).
  2. W hierarchii dziedziczenia zawsze wywołuj funkcję finalize() elementu nadrzędnego w bloku try . Oczyszczanie klasy powinno odbywać się w bloku Final .
  3. Jeśli nie została wywołana jawna metoda czyszczenia, a finalizator zamknął zasoby, zarejestruj ten błąd.
  4. Jeśli program rejestrujący nie jest dostępny, użyj procedury obsługi wyjątków wątku (co kończy się przekazaniem standardowego błędu przechwyconego w dziennikach).

Główne zasady

Sprawozdania

Twierdzenie, zwykle w formie sprawdzenia warunków wstępnych, wymusza umowę „szybko zakończ awarię, szybko zatrzymaj”. Powinny być szeroko stosowane w celu identyfikacji błędów programistycznych możliwie najbliżej przyczyny. Stan obiektu:
  • • Obiekt nigdy nie powinien być tworzony ani wprowadzany w nieprawidłowy stan.
  • • W konstruktorach i metodach zawsze opisz i egzekwuj kontrakt za pomocą testów.
  • • Należy unikać słowa kluczowego Java , gdyż można je wyłączyć i zwykle jest to krucha konstrukcja.
  • • Użyj klasy użytkowej Assertions , aby uniknąć szczegółowych warunków if-else podczas sprawdzania warunków wstępnych.

Genetyki

Pełne, niezwykle szczegółowe wyjaśnienie jest dostępne w często zadawanych pytaniach dotyczących Java Generics . Poniżej znajdują się typowe scenariusze, o których powinni wiedzieć programiści.
  1. O ile to możliwe, lepiej używać wnioskowania o typie niż zwracać klasę bazową/interfejs:

    // MySpecialObject o = MyObjectFactory.getMyObject();
    public  T getMyObject(int type) {
    return (T) factory.create(type);
    }

  2. Jeśli typ nie może zostać określony automatycznie, wstaw go.

    public class MySpecialObject extends MyObject {
     public MySpecialObject() {
      super(Collections.emptyList());   // This is ugly, as we loose type
      super(Collections.EMPTY_LIST();    // This is just dumb
      // But this is beauty
      super(new ArrayList());
      super(Collections.emptyList());
     }
    }

  3. Symbole wieloznaczne:

    Użyj rozszerzonego symbolu wieloznacznego, gdy otrzymujesz tylko wartości ze struktury, użyj super wieloznacznika , gdy wstawisz do struktury tylko wartości i nie używaj symbolu wieloznacznego, gdy robisz jedno i drugie.

    1. Wszyscy kochają PECS ! ( Producent-przedłuża, Konsument-super )
    2. Użyj Foo dla producenta T.
    3. Użyj Foo dla konsumenta T.

Singletony

Singletona nigdy nie należy pisać w klasycznym stylu wzorca projektowego , który jest dobry w C++, ale nieodpowiedni w Javie. Nawet jeśli jest odpowiednio bezpieczny dla wątków, nigdy nie wdrażaj następujących czynności (byłoby to wąskim gardłem wydajności!):
public final class MySingleton {
  private static MySingleton instance;
  private MySingleton() {
    // singleton
  }
  public static synchronized MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
    return instance;
  }
}
Jeśli naprawdę pożądana jest leniwa inicjalizacja, wówczas zadziała kombinacja tych dwóch podejść.
public final class MySingleton {
  private MySingleton() {
   // singleton
  }
  private static final class MySingletonHolder {
    static final MySingleton instance = new MySingleton();
  }
  public static MySingleton getInstance() {
    return MySingletonHolder.instance;
  }
}
Wiosna: Domyślnie komponent bean jest zarejestrowany w zakresie singletonowym, co oznacza, że ​​kontener utworzy tylko jedną instancję i połączy ją ze wszystkimi konsumentami. Zapewnia to tę samą semantykę, co zwykły singleton, bez żadnych ograniczeń wydajności i wiązania.

Wyjątki

  1. Użyj sprawdzonych wyjątków dla możliwych do naprawienia warunków i wyjątków czasu wykonania dla błędów programistycznych. Przykład: pobieranie liczby całkowitej z ciągu znaków.

    Źle: NumberFormatException rozszerza wyjątek RuntimeException, dlatego ma wskazywać błędy programistyczne.

  2. Nie wykonuj następujących czynności:

    // String str = input string
    Integer value = null;
    try {
       value = Integer.valueOf(str);
    } catch (NumberFormatException e) {
    // non-numeric string
    }
    if (value == null) {
    // handle bad string
    } else {
    // business logic
    }

    Prawidłowe użycie:

    // String str = input string
    // Numeric string with at least one digit and optional leading negative sign
    if ( (str != null) && str.matches("-?\\d++") ) {
       Integer value = Integer.valueOf(str);
      // business logic
    } else {
      // handle bad string
    }
  3. Musisz obsługiwać wyjątki we właściwym miejscu, we właściwym miejscu na poziomie domeny.

    ZŁY SPOSÓB - Warstwa obiektu danych nie wie, co zrobić, gdy wystąpi wyjątek bazy danych.

    class UserDAO{
        public List getUsers(){
            try{
                ps = conn.prepareStatement("SELECT * from users");
                rs = ps.executeQuery();
                //return result
            }catch(Exception e){
                log.error("exception")
                return null
            }finally{
                //release resources
            }
        }}
    

    ZALECANY SPOSÓB - Warstwa danych powinna po prostu ponownie zgłosić wyjątek i przekazać odpowiedzialność za obsługę wyjątku lub nie właściwej warstwie.

    === RECOMMENDED WAY ===
    Data layer should just retrow the exception and transfer the responsability to handle the exception or not to the right layer.
    class UserDAO{
       public List getUsers(){
          try{
             ps = conn.prepareStatement("SELECT * from users");
             rs = ps.executeQuery();
             //return result
          }catch(Exception e){
           throw new DataLayerException(e);
          }finally{
             //release resources
          }
      }
    }

  4. Wyjątki na ogół NIE powinny być rejestrowane w momencie ich wystawienia, ale raczej w momencie ich faktycznego przetwarzania. Wyjątki rejestrowania, gdy są zgłaszane lub zgłaszane ponownie, mają tendencję do wypełniania plików dziennika szumem. Należy również pamiętać, że ślad stosu wyjątków nadal rejestruje, gdzie został zgłoszony wyjątek.

  5. Wspieraj użycie standardowych wyjątków.

  6. Używaj wyjątków zamiast kodów powrotu.

Równa się i HashCode

Podczas pisania odpowiednich metod równoważności obiektów i kodów skrótu należy wziąć pod uwagę wiele kwestii. Aby ułatwić użycie, użyj równań i hash java.util.Objects .
public final class User {
 private final String firstName;
 private final String lastName;
 private final int age;
 ...
 public boolean equals(Object o) {
   if (this == o) {
     return true;
   } else if (!(o instanceof User)) {
     return false;
   }
   User user = (User) o;
   return Objects.equals(getFirstName(), user.getFirstName()) &&
    Objects.equals(getLastName(),user.getLastName()) &&
    Objects.equals(getAge(), user.getAge());
 }
 public int hashCode() {
   return Objects.hash(getFirstName(),getLastName(),getAge());
 }
}

Zarządzanie zasobami

Sposoby bezpiecznego zwalniania zasobów: Instrukcja try-with-resources gwarantuje, że każdy zasób zostanie zamknięty na końcu instrukcji. Dowolny obiekt implementujący java.lang.AutoCloseable, który obejmuje wszystkie obiekty implementujące java.io.Closeable , może zostać użyty jako zasób.
private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path)))
 try {
   // business logic
 }
}

Użyj haków zamykających

Użyj haka zamykającego , który jest wywoływany, gdy JVM zamyka się bezpiecznie. (Ale nie będzie w stanie obsłużyć nagłych przerw, takich jak przerwa w dostawie prądu). Jest to zalecana alternatywa zamiast deklarowania metody finalize() , która będzie działać tylko wtedy, gdy System.runFinalizersOnExit() ma wartość true (wartość domyślna to false) .
public final class SomeObject {
 var distributedLock = new ExpiringGeneralLock ("SomeObject", "shared");
 public SomeObject() {
   Runtime
     .getRuntime()
     .addShutdownHook(new Thread(new LockShutdown(distributedLock)));
 }
 /** Code may have acquired lock across servers */
 ...
 /** Safely releases the distributed lock. */
 private static final class LockShutdown implements Runnable {
   private final ExpiringGeneralLock distributedLock;
   public LockShutdown(ExpiringGeneralLock distributedLock) {
     if (distributedLock == null) {
       throw new IllegalArgumentException("ExpiringGeneralLock is null");
     }
     this.distributedLock = distributedLock;
   }
   public void run() {
     if (isLockAlive()) {
       distributedLock.release();
     }
   }
   /** @return True if the lock is acquired and has not expired yet. */
   private boolean isLockAlive() {
     return distributedLock.getExpirationTimeMillis() > System.currentTimeMillis();
   }
 }
}
Pozwól, aby zasoby stały się kompletne (a także odnawialne), dystrybuując je między serwerami. (Umożliwi to przywrócenie pracy po nagłej przerwie, takiej jak przerwa w dostawie prądu.) Zobacz powyższy przykładowy kod, który używa ExpiringGeneralLock (blokada wspólna dla wszystkich systemów).

Data i godzina

W Javie 8 wprowadzono nowy interfejs API Date-Time w pakiecie java.time. W Javie 8 wprowadzono nowy interfejs API daty i godziny, aby wyeliminować następujące niedociągnięcia starego interfejsu API daty i godziny: brak wątków, zły projekt, złożona obsługa stref czasowych itp.

Równoległość

Główne zasady

  1. Uważaj na następujące biblioteki, które nie są bezpieczne dla wątków. Zawsze synchronizuj z obiektami, jeśli są używane przez wiele wątków.
  2. Data ( niezmienna ) — użyj nowego interfejsu API daty i godziny, który jest bezpieczny dla wątków.
  3. SimpleDateFormat — użyj nowego interfejsu API daty i godziny, który jest bezpieczny dla wątków.
  4. Wolę używać klas Java.util.concurrent.atomic niż czynić zmienne nietrwałymi .
  5. Zachowanie klas atomowych jest bardziej oczywiste dla przeciętnego programisty, podczas gdy volatile wymaga zrozumienia modelu pamięci Java.
  6. Klasy atomowe otaczają zmienne lotne w wygodniejszy interfejs.
  7. Zrozumienie przypadków użycia, w których odpowiedni jest lotny . (zobacz artykuł )
  8. Użyj opcji Callable gdy wymagany jest sprawdzony wyjątek, ale nie ma typu zwracanego. Ponieważ nie można utworzyć instancji Void, przekazuje ona intencję i może bezpiecznie zwrócić null .

Strumienie

  1. Java.lang.Thread powinien być przestarzały. Chociaż nie jest to oficjalnie prawdą, w prawie wszystkich przypadkach pakiet java.util.concurrent zapewnia jaśniejsze rozwiązanie problemu.
  2. Rozszerzanie Java.lang.Thread jest uważane za złą praktykę - zamiast tego zaimplementuj Runnable i utwórz nowy wątek z instancją w konstruktorze (reguła kompozycji nad dziedziczeniem).
  3. Preferuj executory i wątki, gdy wymagane jest przetwarzanie równoległe.
  4. Zawsze zaleca się określenie własnej fabryki wątków w celu zarządzania konfiguracją tworzonych wątków ( więcej szczegółów tutaj ).
  5. Użyj DaemonThreadFactory w Executorach dla niekrytycznych wątków, aby pula wątków mogła zostać zamknięta natychmiast po wyłączeniu serwera ( więcej szczegółów tutaj ).
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
   Thread thread = Executors.defaultThreadFactory().newThread(runnable);
   thread.setDaemon(true);
   return thread;
});
  1. Synchronizacja Java nie jest już tak powolna (55–110 ns). Nie unikaj tego, stosując sztuczki, takie jak podwójne sprawdzanie blokowania .
  2. Preferuj synchronizację z obiektem wewnętrznym zamiast klasą, ponieważ użytkownicy mogą synchronizować się z Twoją klasą/instancją.
  3. Zawsze synchronizuj wiele obiektów w tej samej kolejności, aby uniknąć zakleszczeń.
  4. Synchronizacja z klasą nie blokuje z natury dostępu do jej obiektów wewnętrznych. Zawsze używaj tych samych blokad podczas uzyskiwania dostępu do zasobu.
  5. Pamiętaj, że słowo kluczowe synchronized nie jest uważane za część sygnatury metody i dlatego nie będzie dziedziczone.
  6. Unikaj nadmiernej synchronizacji, może to prowadzić do słabej wydajności i zakleszczenia. Użyj słowa kluczowego synchronized wyłącznie dla części kodu wymagającej synchronizacji.

Kolekcje

  1. Jeśli to możliwe, używaj kolekcji równoległych Java-5 w kodzie wielowątkowym. Są bezpieczne i mają doskonałe właściwości.
  2. Jeśli to konieczne, użyj CopyOnWriteArrayList zamiast synchronizedList.
  3. Użyj Collections.unmodible list(...) lub skopiuj kolekcję po otrzymaniu jej jako parametr do new ArrayList(list) . Unikaj modyfikowania lokalnych kolekcji spoza klasy.
  4. Zawsze zwracaj kopię swojej kolekcji, unikając modyfikowania listy zewnętrznie za pomocą nowej ArrayList (list) .
  5. Każda kolekcja musi być opakowana w osobną klasę, dzięki czemu zachowanie powiązane z kolekcją ma teraz swój początek (np. metody filtrowania, stosowanie reguły do ​​każdego elementu).

Różnorodny

  1. Wybierz lambdy zamiast klas anonimowych.
  2. Wybierz odwołania do metod zamiast lambd.
  3. Używaj wyliczeń zamiast stałych int.
  4. Unikaj używania float i double, jeśli wymagane są dokładne odpowiedzi, zamiast tego użyj BigDecimal, takiego jak Money.
  5. Wybierz typy pierwotne zamiast prymitywów pudełkowych.
  6. Powinieneś unikać używania magicznych liczb w swoim kodzie. Użyj stałych.
  7. Nie zwracaj wartości Null. Komunikuj się z klientem metody, korzystając z opcji „Opcjonalne”. To samo dotyczy kolekcji - zwracaj puste tablice lub kolekcje, a nie wartości null.
  8. Unikaj tworzenia niepotrzebnych obiektów, używaj obiektów ponownie i unikaj niepotrzebnego czyszczenia GC.

Leniwa inicjalizacja

Leniwa inicjalizacja to optymalizacja wydajności. Stosuje się go, gdy z jakiegoś powodu dane są uważane za „drogie”. W Javie 8 musimy do tego użyć funkcjonalnego interfejsu dostawcy.
== Thread safe Lazy initialization ===
public final class Lazy {
   private volatile T value;
   public T getOrCompute(Supplier supplier) {
       final T result = value; // Just one volatile read
       return result == null ? maybeCompute(supplier) : result;
   }
   private synchronized T maybeCompute(Supplier supplier) {
       if (value == null) {
           value = supplier.get();
       }
       return value;
   }
}
Lazy lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
To wszystko na teraz. Mam nadzieję, że to było pomocne!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION