Best practices från ”Effective Java (3rd Edition)”

Utdrag ur ”Effective Java (3rd Edition) ” av Joshua Bloch

1. Returnera tomma samlingar eller arrayer, inte null

Om metoden returnerar ett Set ska du inte skriva return null; för ett tomt resultat – skriv return Set.of(); i stället.

2. Överväg statiska fabriksmetoder i stället för konstruktorer

public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}

Inte alltid bättre, men i vissa fall har de många fördelar:

Nackdelar:

3. Överväg en builder när du har många konstruktorparametrar

Statiska fabriker och konstruktorer har en gemensam begränsning: de skalar dåligt när det finns många valfria parametrar. Det här har drabbat några projekt jag har arbetat i väldigt hårt.

4. Säkerställ singleton-egenskapen med en privat konstruktor eller en enum-typ

Allt som inte behöver flera instanser bör bara instansieras en gång – oavsett om det sker med @ApplicationScoped eller ett annat singleton-mönster.

5. Förhindra instansiering med en privat konstruktor

Ibland vill man skriva en klass som bara är en samling statiska metoder och statiska fält. Sådana klasser har fått dåligt rykte eftersom en del använder dem för att slippa tänka i termer av objekt, men de har sina berättigade användningsområden.

private MyStaticClass() {
    // private constructor to prevent instantiation
}

6. Undvik att skapa onödiga objekt

String s = new String("bikini");  // DON'T DO THIS!

Eller:

// Hideously slow! Can you spot the object creation?
private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

Lösning: Programmet ger rätt svar, men det är mycket långsammare än det borde vara, på grund av ett skrivfel på ett enda tecken. Variabeln sum är deklarerad som Long i stället för long, vilket innebär att programmet skapar ungefär 2^31 onödiga Long-instanser (ungefär en för varje gång long i adderas till Long sum).

7. Undvik finalizers och cleaners

Finalizers är oförutsägbara, ofta farliga och i regel onödiga. Att använda dem kan leda till oberäkneligt beteende, dålig prestanda och portabilitetsproblem.

8. Föredra try-with-resources framför try-finally

// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
}

9. Överväg att implementera Comparable

Genom att implementera Comparable låter du din klass samverka med alla de många generiska algoritmer och collection-implementationer som bygger på det här interfacet.

10. Minimera åtkomligheten för klasser och medlemmar

Den enskilt viktigaste faktorn som skiljer en väldesignad komponent från en dåligt designad är i vilken grad komponenten döljer sina interna data och andra implementationsdetaljer för andra komponenter. En väldesignad komponent döljer alla sina implementationsdetaljer och skiljer tydligt sitt API från sin implementation. Komponenterna kommunicerar då bara via sina API:er och är omedvetna om varandras inre funktion. Det här konceptet, känt som informationsdöljande eller inkapsling, är en grundprincip inom mjukvarudesign.

11. Använd åtkomstmetoder i publika klasser, inte publika fält

Ibland kan man frestas att skriva degenererade klasser som inte fyller något annat syfte än att gruppera instansfält:

// Degenerate classes like this should not be public!
class Point {
    public double x;
    public double y;
}

Eftersom datafälten i sådana klasser nås direkt ger klasserna inte fördelarna med inkapsling. Du kan inte ändra representationen utan att ändra API:et, du kan inte upprätthålla invarianter och du kan inte vidta extra åtgärder när ett fält läses. Renläriga objektorienterade programmerare anser att sådana klasser är förkastliga och alltid bör ersättas med klasser med privata fält och publika åtkomstmetoder (getters) och, för föränderliga klasser, mutatorer (setters):

// Encapsulation of data by accessor methods and mutators
class Point {

    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { return x; }
    public double getY() { return y; }

    public void setX(double x) { this.x = x; }
    public void setY(double y) { this.y = y; }
}

Visst har de renläriga rätt när det gäller publika klasser: Om en klass är åtkomlig utanför sitt paket, tillhandahåll åtkomstmetoder för att bevara möjligheten att ändra klassens interna representation. Om en publik klass exponerar sina datafält är allt hopp om att ändra representationen ute, eftersom klientkoden kan vara spridd vida omkring.

Om en klass däremot är paketprivat eller en privat nästlad klass är det inget principiellt fel med att exponera dess datafält – förutsatt att de på ett bra sätt beskriver den abstraktion som klassen erbjuder.

12. Minimera föränderlighet

En oföränderlig (immutable) klass är helt enkelt en klass vars instanser inte kan ändras. All information i varje instans är fast under objektets hela livstid, så inga ändringar kan någonsin observeras. Javaplattformens bibliotek innehåller många oföränderliga klasser, bland annat String, de boxade primitiva klasserna samt BigInteger och BigDecimal. Det finns många goda skäl till det: Oföränderliga klasser är lättare att designa, implementera och använda än föränderliga klasser. De är mindre felbenägna och säkrare. Följ dessa fem regler för att göra en klass oföränderlig:

  1. Tillhandahåll inga metoder som ändrar objektets tillstånd (så kallade mutatorer).
  2. Se till att klassen inte kan utökas. Det hindrar slarviga eller illvilliga subklasser från att undergräva klassens oföränderliga beteende genom att bete sig som om objektets tillstånd hade ändrats. Att förhindra subklassning åstadkoms i regel genom att göra klassen final, till exempel.
  3. Gör alla fält final. Det uttrycker tydligt din avsikt på ett sätt som systemet upprätthåller. Det är också nödvändigt för att garantera korrekt beteende om en referens till en nyskapad instans skickas från en tråd till en annan utan synkronisering, så som det beskrivs i minnesmodellen.
  4. Gör alla fält privata. Det hindrar klienter från att få åtkomst till föränderliga objekt som fälten refererar till och ändra dem direkt. Även om det tekniskt sett är tillåtet för oföränderliga klasser att ha publika final-fält med primitiva värden eller referenser till oföränderliga objekt, rekommenderas det inte, eftersom det omöjliggör att den interna representationen ändras i en senare version.
  5. Säkerställ exklusiv åtkomst till alla föränderliga komponenter. Om din klass har fält som refererar till föränderliga objekt, se till att klassens klienter inte kan få referenser till dessa objekt. Initiera aldrig ett sådant fält med en objektreferens från klienten och returnera aldrig fältet från en åtkomstmetod. Gör defensiva kopior i konstruktorer, åtkomstmetoder och readObject-metoder.

13. Föredra komposition framför arv

Arv är ett kraftfullt sätt att återanvända kod, men det är inte alltid det bästa verktyget för uppgiften. Använt på fel sätt leder det till ömtålig mjukvara. Det är säkert att använda arv inom ett paket, där subklassens och superklassens implementationer kontrolleras av samma programmerare. Det är också säkert att använda arv när man utökar klasser som är särskilt designade och dokumenterade för att utökas. Att ärva från vanliga konkreta klasser över paketgränser är däremot farligt.

Till skillnad från metodanrop bryter arv inkapslingen. Man kan tro att det är säkert att utöka en klass om man bara lägger till nya metoder och avstår från att överskugga befintliga metoder. Även om den sortens utökning är mycket säkrare är den inte riskfri. Om superklassen får en ny metod i en senare version och du har haft oturen att ge subklassen en metod med samma signatur men en annan returtyp, kommer din subklass inte längre att kompilera. Om du har gett subklassen en metod med samma signatur och returtyp som den nya superklassmetoden, överskuggar du den nu och drabbas av problemen som beskrevs tidigare. Dessutom är det tveksamt om din metod uppfyller kontraktet för den nya superklassmetoden, eftersom det kontraktet ännu inte var skrivet när du skrev subklassmetoden.

Lyckligtvis finns det ett sätt att undvika alla problem som beskrivs ovan. I stället för att utöka en befintlig klass ger du din nya klass ett privat fält som refererar till en instans av den befintliga klassen. Den här designen kallas komposition, eftersom den befintliga klassen blir en komponent i den nya. Varje instansmetod i den nya klassen anropar motsvarande metod på den inneslutna instansen av den befintliga klassen och returnerar resultatet. Det kallas vidarebefordran (forwarding), och metoderna i den nya klassen kallas vidarebefordrande metoder. Den resulterande klassen blir bergfast, utan beroenden till den befintliga klassens implementationsdetaljer. Inte ens nya metoder i den befintliga klassen påverkar den nya klassen.

14. Föredra interface framför abstrakta klasser

Java har två mekanismer för att definiera en typ som tillåter flera implementationer: interface och abstrakta klasser. Sedan default-metoder för interface infördes i Java 8 låter båda mekanismerna dig tillhandahålla implementationer för vissa instansmetoder.

En viktig skillnad är att en klass måste vara en subklass till den abstrakta klassen för att implementera typen som den abstrakta klassen definierar. Eftersom Java bara tillåter enkelarv begränsar den här restriktionen kraftigt hur abstrakta klasser kan användas som typdefinitioner. Varje klass som definierar alla obligatoriska metoder och följer det allmänna kontraktet får implementera ett interface, oavsett var i klasshierarkin klassen befinner sig. Befintliga klasser kan enkelt byggas om för att implementera ett nytt interface.

15. Generics: Använd inte råa typer

Du upptäcker inte felet förrän vid körning, långt efter att det har inträffat, och i kod som kan ligga långt från koden som innehåller felet. När du väl ser ClassCastException måste du leta igenom kodbasen efter metodanropet som lade myntet i frimärkssamlingen. Kompilatorn kan inte hjälpa dig.

16. Generics: Eliminera unchecked-varningar

När du programmerar med generics får du många kompilatorvarningar:

Ju mer erfarenhet du får av generics, desto färre varningar får du, men räkna inte med att nyskriven kod kompilerar utan varningar. Många unchecked-varningar är lätta att eliminera. Anta till exempel att du av misstag skriver den här deklarationen:

Set<Lark> exaltation = new HashSet();

Ändra den till:

Set<Lark> exaltation = new HashSet<>();

Vissa varningar är betydligt svårare att eliminera. När du får varningar som kräver eftertanke – ge inte upp! Eliminera varje unchecked-varning som du kan.

Om du eliminerar alla varningar kan du vara säker på att din kod är typsäker, vilket är väldigt bra. Det betyder att du inte får någon ClassCastException vid körning, och det ökar din tilltro till att programmet beter sig som du avsåg. Om du inte kan eliminera en varning men kan bevisa att koden som orsakade varningen är typsäker, så (och bara då) undertryck varningen med en @SuppressWarnings("unchecked")-annotering. Om du undertrycker varningar utan att först ha bevisat att koden är typsäker invaggar du dig själv i en falsk trygghet. Koden kanske kompilerar utan varningar, men den kan ändå kasta en ClassCastException vid körning.

Varje gång du använder en @SuppressWarnings("unchecked")-annotering, lägg till en kommentar som förklarar varför det är säkert.

17. Generics: När det är meningsfullt

Aj – en klass som verkligen skulle behöva generics:

Exempel på en klass som behöver generics

Något sådant här borde fångas upp senast vid merge requesten!

18. Generics: Så ska man inte göra

Förlåt, inga ord behövs. Merga ALDRIG det här:

public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
        LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
        AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
    extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
    // ...
}

En enkel klassdeklaration ska aldrig ta 5 minuter att förstå.

19. Föredra listor framför arrayer

Arrayer skiljer sig från generiska typer på två viktiga sätt. För det första är arrayer kovarianta. Det skrämmande ordet betyder helt enkelt att om Sub är en subtyp till Super, så är arraytypen Sub[] en subtyp till arraytypen Super[]. Generics är däremot invarianta: för två olika typer Type1 och Type2 är List<Type1> varken en subtyp eller en supertyp till List<Type2>.

20. Föredra lambdor framför anonyma klasser

// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

Gör i stället så här:

// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

21. Minimera räckvidden för lokala variabler

Den mest effektiva tekniken för att minimera räckvidden (scope) för en lokal variabel är att deklarera den där den först används …

22. Föredra for-each-loopar framför traditionella for-loopar

// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
    ... // Do something with e
}

23. Känn till och använd biblioteken

Anta att du vill generera slumpmässiga heltal mellan noll och en övre gräns. Inför den vanliga uppgiften skulle många programmerare skriva en liten metod som ser ut ungefär så här:

// Common but deeply flawed!
static Random rnd = new Random();

static int random(int n) {
    return Math.abs(rnd.nextInt()) % n;
}

Metoden kan se bra ut, men den har tre brister. Den första är att om n är en liten tvåpotens upprepar sig sekvensen av slumptal efter en ganska kort period. Den andra bristen är att om n inte är en tvåpotens kommer vissa tal i genomsnitt att returneras oftare än andra. Om n är stort kan den effekten bli ganska påtaglig.

Den tredje bristen i random-metoden är att den i sällsynta fall kan misslyckas katastrofalt och returnera ett tal utanför det angivna intervallet. Det beror på att metoden försöker mappa värdet som returneras av rnd.nextInt() till en icke-negativ int genom att anropa Math.abs. Om nextInt() returnerar Integer.MIN_VALUE returnerar även Math.abs Integer.MIN_VALUE, och restoperatorn (%) returnerar ett negativt tal, förutsatt att n inte är en tvåpotens. Det får nästan säkert ditt program att fallera, och felet kan vara svårt att återskapa.

24. Undvik float och double om exakta svar krävs

Använd BigDecimal för penningbelopp. float och double ger avrundningsfel.

Anta till exempel att du har 1,03 dollar i fickan och spenderar 42 cent. Hur mycket pengar har du kvar? Här är ett naivt programfragment som försöker besvara frågan:

System.out.println(1.03 - 0.42);

Tyvärr skriver det ut 0.6100000000000001. Det är inget enstaka fall. Anta att du har en dollar i fickan och köper nio brickor (washers) som kostar tio cent styck. Hur mycket växel får du tillbaka?

System.out.println(1.00 - 9 * 0.10);

Enligt det här programfragmentet får du $0.09999999999999998.

Man kan tro att problemet går att lösa genom att bara avrunda resultaten innan de skrivs ut, men tyvärr fungerar det inte alltid. Anta till exempel att du har en dollar i fickan och ser en hylla med en rad läckra godisbitar som kostar 10 cent, 20 cent, 30 cent och så vidare, upp till en dollar. Du köper en av varje, med början på den som kostar 10 cent, tills du inte har råd med nästa godisbit på hyllan.

25. Föredra primitiva typer framför boxade primitiver

// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

26. Referera till objekt via deras interface

Bra exempel:

Set<Son> sonSet = new LinkedHashSet<>();

Dåligt exempel:

LinkedHashSet<Son> sonSet = new LinkedHashSet<>();

27. Föredra interface framför reflection

Annars gäller:

28. Följ allmänt vedertagna namngivningskonventioner

Javaplattformen har en väletablerad uppsättning namngivningskonventioner, varav många finns i The Java Language Specification [JLS, 6.1].

29. Använd undantag endast i undantagsfall

// Horrible abuse of exceptions. Don't ever do this!
try {
    int i = 0;
    while (true) {
        range[i++].climb();
    }
} catch (ArrayIndexOutOfBoundsException e) {
    // catch it
}

Vad gör den här koden? Det är inte alls uppenbart vid en granskning, och det räcker som skäl att inte använda den. Det visar sig vara ett fruktansvärt illa genomtänkt idiom för att loopa igenom elementen i en array. Den oändliga loopen avslutas genom att en ArrayIndexOutOfBoundsException kastas, fångas och ignoreras när koden försöker komma åt det första elementet utanför arrayens gränser. Den ska motsvara standardidiomet för att loopa igenom en array, som varje Java-programmerare känner igen direkt:

for (Mountain m : range) {
    m.climb();
}

Undantag ska, som namnet antyder, bara användas i undantagsfall; de ska aldrig användas för vanligt kontrollflöde.

30. Använd checked exceptions för situationer som går att hantera och runtime exceptions för programmeringsfel

Använd checked exceptions för situationer som anroparen rimligen kan förväntas återhämta sig från. Genom att konfrontera användaren med en checked exception ger API-designern ett uppdrag att hantera situationen. Användaren kan strunta i uppdraget genom att fånga undantaget och ignorera det, men det är oftast en dålig idé.

Använd runtime exceptions för att signalera programmeringsfel. De allra flesta runtime exceptions signalerar brott mot förvillkor. Ett brott mot ett förvillkor är helt enkelt att en klient till ett API inte följer det kontrakt som API-specifikationen fastställer. Kontraktet för arrayåtkomst anger till exempel att arrayindexet måste ligga mellan noll och arrayens längd minus ett, inklusive. ArrayIndexOutOfBoundsException signalerar att detta förvillkor har brutits.

31. Undvik onödig användning av checked exceptions

Sammanfattningsvis: Använda sparsamt kan checked exceptions öka programmens tillförlitlighet; överanvända gör de API:er plågsamma att använda.

Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
                                                      LearningCardAssignment learningCardAssignment,
                                                      @NotNull Collection<AnswerOption> answers)
      throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
             ConsecutiveCorrectCountExceededException,
             PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
             LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
     // this is a mistake seen a lot with junior devs
     // exceptions are exceptionally slow and should be avoided for mere validation
     // the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions

32. Använd helst standardundantag

En egenskap som skiljer erfarna programmerare från mindre erfarna är att experterna strävar efter, och oftast uppnår, en hög grad av kodåteranvändning.

Undantag är inget undantag från regeln att kodåteranvändning är bra. Javas bibliotek erbjuder en uppsättning undantag som täcker de flesta behov av att kasta undantag i de flesta API:er. Att återanvända standardundantag har flera fördelar. Den främsta är att ditt API blir lättare att lära sig och använda, eftersom det följer etablerade konventioner som programmerare redan känner till. En nära tvåa är att program som använder ditt API blir lättare att läsa, eftersom de inte är belamrade med obekanta undantag. Sist (och minst) innebär färre undantagsklasser ett mindre minnesavtryck och mindre tid åt att ladda klasser.

Den oftast återanvända undantagstypen är IllegalArgumentException. Det är i regel undantaget att kasta när anroparen skickar in ett argument med ett olämpligt värde. Det skulle till exempel vara undantaget att kasta om anroparen skickade ett negativt tal i en parameter som anger hur många gånger en åtgärd ska upprepas. Ett annat ofta återanvänt undantag är IllegalStateException. Det är i regel undantaget att kasta om anropet är otillåtet på grund av det mottagande objektets tillstånd. Det skulle till exempel vara undantaget att kasta om anroparen försökte använda ett objekt innan det hade initierats korrekt.

Man kan hävda att varje felaktigt metodanrop i grunden handlar om ett otillåtet argument eller tillstånd, men andra undantag används som standard för vissa typer av otillåtna argument och tillstånd. Om en anropare skickar null i en parameter där null-värden är förbjudna, föreskriver konventionen att NullPointerException kastas i stället för IllegalArgumentException. På samma sätt ska IndexOutOfBoundsException kastas i stället för IllegalArgumentException om en anropare skickar ett värde utanför intervallet i en parameter som representerar ett index i en sekvens.

Återanvänd inte Exception, RuntimeException, Throwable eller Error direkt. Behandla dessa klasser som om de vore abstrakta. RuntimeException kan dock användas om du vill omvandla en checked exception till en runtime exception och verkligen vet vad du gör.

33. Ignorera inte undantag

// Empty catch block ignores exception - Highly suspect!
try {
    ...
} catch (SomeException e) {
}

Ett tomt catch-block motverkar syftet med undantag, som är att tvinga dig att hantera exceptionella situationer. Att ignorera ett undantag är som att ignorera ett brandlarm – och stänga av det så att ingen annan får chansen att se om det faktiskt brinner.