Sök…


Introduktion

Flera missbruk av Java-programmeringsspråk kan utföra ett program för att generera felaktiga resultat trots att de har komponerats korrekt. Huvudsyftet med detta ämne är att lista vanliga fallgropar med sina orsaker och att föreslå rätt sätt att undvika att falla i sådana problem.

Anmärkningar

Det här ämnet handlar om specifika aspekter av Java-språksyntaxen som antingen är felutsatta eller som inte bör användas på vissa sätt.

Fallgrop - Ignorering av metodens synlighet

Även erfarna Java-utvecklare tror att Java bara har tre skyddsmodifierare. Språket har faktiskt fyra! Paketets privata (alias standard) synlighet glömmer ofta.

Du bör vara uppmärksam på vilka metoder du offentliggör. De offentliga metoderna i en applikation är applikationens synliga API. Detta bör vara så litet och kompakt som möjligt, särskilt om du skriver ett återanvändbart bibliotek (se även SOLID- principen). Det är viktigt att på liknande sätt överväga synligheten för alla metoder och att endast använda skyddad eller paketad privat åtkomst där så är lämpligt.

När du förklarar metoder som bör vara privata som offentliga avslöjar du klassens interna implementeringsdetaljer.

En följd av detta är att du bara testar enhetens offentliga metoder - du kan faktiskt bara testa offentliga metoder. Det är dålig praxis att öka synligheten för privata metoder bara för att kunna köra enhetstester mot dessa metoder. Testa offentliga metoder som kallar metoderna med mer restriktiv synlighet bör vara tillräckliga för att testa ett helt API. Du bör aldrig utöka ditt API med mer offentliga metoder bara för att tillåta enhetstestning.

Fallgrop - Saknar en "paus" i ett "switch" fall

Dessa Java-problem kan vara mycket pinsamma och förblir ibland oupptäckta tills de körs i produktion. Ofullständigt beteende i switch-uttalanden är ofta användbart; att sakna ett "break" -sökord när sådant beteende inte önskas kan leda till katastrofala resultat. Om du har glömt att sätta en "paus" i "fall 0" i kodexemplet nedan, kommer programmet att skriva "Zero" följt av "One", eftersom kontrollflödet inuti här kommer att gå igenom hela "switch" -satsen tills det når en "paus". Till exempel:

public static void switchCasePrimer() {
        int caseIndex = 0;
        switch (caseIndex) {
            case 0:
                System.out.println("Zero");
            case 1:
                System.out.println("One");
                break;
            case 2:
                System.out.println("Two");
                break;
            default:
                System.out.println("Default");
        }
}

I de flesta fall skulle den renare lösningen vara att använda gränssnitt och flytta kod med specifikt beteende till separata implementationer ( sammansättning över arv )

Om ett switch-statement inte kan undvikas, rekommenderas det att dokumentera "förväntade" fall när de inträffar. På så sätt visar du andra utvecklare att du är medveten om det saknade avbrottet och att detta förväntas beteende.

switch(caseIndex) {
    [...]
    case 2:
        System.out.println("Two");
        // fallthrough
    default:
        System.out.println("Default");

Fallgrop - Misplacerade semikolon och saknade hängslen

Detta är ett misstag som orsakar verklig förvirring för Java-nybörjare, åtminstone första gången de gör det. Istället för att skriva detta:

if (feeling == HAPPY)
    System.out.println("Smile");
else
    System.out.println("Frown");

de skriver av misstag:

if (feeling == HAPPY);
    System.out.println("Smile");
else
    System.out.println("Frown");

och blir förbryllade när Java-kompilatorn säger till dem att den else är placerad. Java-kompilatorn med tolkar ovanstående enligt följande:

if (feeling == HAPPY)
    /*empty statement*/ ;
System.out.println("Smile");   // This is unconditional
else                           // This is misplaced.  A statement cannot
                               // start with 'else'
System.out.println("Frown");

I andra fall kommer det inte att finnas några kompilationsfel, men koden gör inte det som programmeraren avser. Till exempel:

for (int i = 0; i < 5; i++);
    System.out.println("Hello");

skriver bara "Hej" en gång. Återigen betyder den falska semikolon att kroppen på for loopen är ett tomt uttalande. Det betyder att det println samtalet som följer är ovillkorligt.

En annan variant:

for (int i = 0; i < 5; i++);
    System.out.println("The number is " + i);

Detta ger ett "Kan inte hitta symbol" -fel för i . Närvaron av den falska semikolon innebär att det println samtalet försöker använda i utanför dess omfattning.

I de exemplen finns det en rak lösning: radera helt enkelt den falska semikolon. Det finns emellertid några djupare lärdomar som kan dras av dessa exempel:

  1. Semikolonet i Java är inte "syntaktiskt brus". Närvaron eller frånvaron av en semikolon kan ändra innebörden av ditt program. Lägg inte bara till dem i slutet av varje rad.

  2. Lita inte på kodens intryck. På Java-språket ignoreras extra vitrum i början av en rad av kompilatorn.

  3. Använd en automatisk indenter. Alla IDE: er och många enkla textredigerare förstår hur man korrekt indragar Java-kod.

  4. Detta är den viktigaste lektionen. Följ de senaste riktlinjerna i Java-stil och lägg hängslen runt "då" och "annat" uttalanden och kroppsuttalandet om en slinga. Den öppna stången ( { ) bör inte vara på en ny linje.

Om programmeraren följde stilreglerna så skulle if exemplet med fel placerade semikoloner se ut så här:

if (feeling == HAPPY); {
    System.out.println("Smile");
} else {
    System.out.println("Frown");
}

Det ser konstigt ut för ett erfaret öga. Om du automatiskt indragit den koden skulle den förmodligen se ut så här:

if (feeling == HAPPY); {
                           System.out.println("Smile");
                       } else {
                           System.out.println("Frown");
                       }

vilket bör framstå som fel för även en nybörjare.

Fallgrop - Utelämnande av hängslen: problemen med "dinglande om" och "dinglande annat"

Den senaste versionen av Oracle Java-stilguiden kräver att "då" och "annat" -satserna i ett if uttalande alltid ska bifogas i "hängslen" eller "lockiga parenteser". Liknande regler gäller för organen i olika slinganalyser.

if (a) {           // <- open brace
    doSomething();
    doSomeMore();
}                  // <- close brace

Detta krävs inte faktiskt av Java-språksyntax. Om "den" delen av ett if uttalande är ett enda uttalande är det faktiskt lagligt att utelämna hängslen

if (a)
    doSomething();

eller ens

if (a) doSomething();

Men det finns faror när man ignorerar Java-stilregler och lämnar hängslen. Specifikt ökar du risken för att kod med felaktig intryck blir felaktig.

Problemet "dinglande om":

Tänk på exempelkoden ovanifrån, omskrivna utan hängslen.

if (a)
   doSomething();
   doSomeMore();

Denna kod verkar säga att doSomething till doSomething och doSomeMore båda kommer att ske om och bara om a är true . I själva verket är koden felaktigt intryckt. Java-språkspecifikationen som doSomeMore() ringer är ett separat uttalande efter if uttalandet. Rätt intryck är som följer:

if (a)
   doSomething();
doSomeMore();

Problemet med "dinglande annat"

Ett andra problem visas när vi lägger till else i mixen. Tänk på följande exempel med saknade hängslen.

if (a)
   if (b)
      doX();
   else if (c)
      doY(); 
else
   doZ();

Koden ovan verkar säga att doZ kommer att doZ när a är false . Faktum är att indragningen är felaktig igen. Rätt intryck för koden är:

if (a)
   if (b)
      doX();
   else if (c)
      doY(); 
   else
      doZ();

Om koden skrivits enligt Java-stilreglerna skulle den verkligen se ut så här:

if (a) {
   if (b) {
      doX();
   } else if (c) {
      doY(); 
   } else {
      doZ();
   }
}

För att illustrera varför det är bättre, anta att du av misstag hade indragit koden. Du kan sluta med något liknande:

if (a) {                         if (a) {
   if (b) {                          if (b) {
      doX();                            doX();
   } else if (c) {                   } else if (c) {
      doY();                            doY();
} else {                         } else {
   doZ();                            doZ();
}                                    }
}                                }

Men i båda fallen ser den felindragna koden "fel" ut för en erfaren Java-programmerare.

Fallgrop - Överbelastning istället för att åsidosätta

Tänk på följande exempel:

public final class Person {
    private final String firstName;
    private final String lastName;
   
    public Person(String firstName, String lastName) {
        this.firstName = (firstName == null) ? "" : firstName;
        this.lastName = (lastName == null) ? "" : lastName;
    }

    public boolean equals(String other) {
        if (!(other instanceof Person)) {
            return false;
        }
        Person p = (Person) other;
        return firstName.equals(p.firstName) &&
                lastName.equals(p.lastName);
    }

    public int hashcode() {
        return firstName.hashCode() + 31 * lastName.hashCode();
    }
}

Den här koden kommer inte att bete sig som förväntat. Problemet är att equals och hashcode för Person inte åsidosätter de standardmetoder som definieras av Object .

  • Metoden equals har fel signatur. Det ska förklaras som equals(Object) inte equals(String) .
  • hashcode metoden har fel namn. Det ska vara hashCode() (notera kapital C ).

Dessa misstag innebär att vi har förklarat oavsiktliga överbelastningar, och dessa kommer inte att användas om Person används i ett polymorft sammanhang.

Det finns emellertid ett enkelt sätt att hantera detta (från Java 5 och framåt). Använd @Override anteckningen när du tänker att din metod ska vara en åsidosättning:

Java SE 5
public final class Person {
    ...

    @Override
    public boolean equals(String other) {
        ....
    }

    @Override
    public hashcode() {
        ....
    }
}

När vi lägger till en @Override anteckning till en metod deklaration kommer kompilatorn kontrollera att metoden gör override (eller införa) en metod som deklareras i en superklass eller gränssnitt. Så i exemplet ovan kommer kompilatorn att ge oss två sammanställningsfel, vilket borde vara tillräckligt för att varna oss för misstaget.

Fallgrop - oktala bokstäver

Tänk på följande kodavsnitt:

// Print the sum of the numbers 1 to 10
int count = 0;
for (int i = 1; i < 010; i++) {    // Mistake here ....
    count = count + i;
}
System.out.println("The sum of 1 to 10 is " + count);

En Java-nybörjare kan bli förvånad över att veta att ovanstående program skriver ut fel svar. Den skriver faktiskt ut summan av siffrorna 1 till 8.

Anledningen är att ett heltal bokstavligt som börjar med siffran noll ('0') tolkas av Java-kompilatorn som en oktal bokstav, inte en decimal bokstav som du kan förvänta dig. Således är 010 det oktala talet 10, som är 8 i decimal.

Fallgrop - Förklarande klasser med samma namn som standardklasser

Ibland gör programmerare som är nya i Java misstaget att definiera en klass med ett namn som är samma som en allmänt använt klass. Till exempel:

package com.example;

/**
 * My string utilities
 */
public class String {
    ....
}

Då undrar de varför de får oväntade fel. Till exempel:

package com.example;

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

Om du sammanställer och sedan försöker köra ovanstående klasser får du ett fel:

$ javac com/example/*.java
$ java com.example.Test
Error: Main method not found in class test.Test, please define the main method as:
   public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

Någon som tittar på koden för Test klass skulle se deklarationen av main och titta på sin underskrift och undrar vad java kommandot klagar. Men i själva verket säger java kommandot sanningen.

När vi förklarar en version av String i samma paket som Test , har denna version företräde framför den automatiska importen av java.lang.String . Således är signaturen för Test.main metoden faktiskt

void main(com.example.String[] args) 

istället för

void main(java.lang.String[] args)

och java kommandot kommer inte att känna igen det som en entrypoint-metod.

Lektion: Definiera inte klasser som har samma namn som befintliga klasser i java.lang eller andra vanliga klasser i Java SE-biblioteket. Om du gör det ställer du dig öppen för alla slags otydliga fel.

Fallgrop - Använd '==' för att testa en boolean

Ibland skriver en ny Java-programmerare kod som denna:

public void check(boolean ok) {
    if (ok == true) {           // Note 'ok == true'
        System.out.println("It is OK");
    }
}

En erfaren programmerare skulle upptäcka det som klumpigt och vill skriva om det som:

public void check(boolean ok) {
    if (ok) {
       System.out.println("It is OK");
    }
}

Men det är mer fel med ok == true än enkel klumpighet. Tänk på denna variation:

public void check(boolean ok) {
    if (ok = true) {           // Oooops!
        System.out.println("It is OK");
    }
}

Här har programmeraren skrivit fel == som = ... och nu har koden ett subtilt fel. Uttrycket x = true villkorslöst tilldelar true till x och utvärderas sedan till true . Med andra ord kommer check nu att skriva ut "Det är OK" oavsett vad parametern var.

Lektionen här är att komma ur vanan att använda == false och == true . Förutom att de är ordlista gör de din kodning mer felaktig.


Obs: Ett möjligt alternativ till ok == true som undviker fallgropen är att använda Yoda-villkor ; dvs lägg den bokstavliga på vänster sida av den relationella operatören, som i true == ok . Detta fungerar, men de flesta programmerare skulle förmodligen komma överens om att Yoda-förhållandena ser udda ut. Visst ok (eller !ok ) är mer kortfattat och mer naturligt.

Fallgrop - Import av jokertecken kan göra din kod ömtålig

Tänk på följande delvisa exempel:

import com.example.somelib.*;
import com.acme.otherlib.*;

public class Test {
    private Context x = new Context();   // from com.example.somelib
    ...
}

Anta att när du först utvecklade koden mot version 1.0 av somelib och version 1.0 av otherlib . På något senare tillfälle måste du uppgradera dina beroenden till en senare version och du bestämmer dig för att använda otherlib version 2.0. Anta också att en av ändringarna som de gjorde till otherlib mellan 1.0 och 2.0 var att lägga till en Context .

När du kompilerar igen Test får du ett sammanställningsfel som säger att Context är en tvetydig import.

Om du känner till kodbasen är det förmodligen bara en mindre besvär. Om inte, har du lite arbete att göra för att lösa problemet, här och eventuellt på andra håll.

Problemet här är jokardimporten. Å ena sidan kan du använda jokertecken göra dina klasser några rader kortare. Å andra sidan:

  • Uppåt kompatibla ändringar av andra delar av din kodbas, till Java-standardbibliotek eller till tredje parts bibliotek kan leda till sammanställningsfel.

  • Läsbarheten lider. Om du inte använder en IDE kan det vara svårt att räkna ut vilken av jokardimporten som drar i en klass.

Lektionen är att det är en dålig idé att använda jokerteckenimport i kod som måste vara långlivad. Speciell (icke-jokerteknisk) import är inte mycket ansträngning att upprätthålla om du använder en IDE, och ansträngningen är värt.

Fallgrop: Använda "påstå" för argument eller validering av användarinmatning

En fråga som ibland på StackOverflow är om det är lämpligt att använda assert att validera argument som tillhandahålls till en metod, eller till och med input som tillhandahålls av användaren.

Det enkla svaret är att det inte är lämpligt.

Bättre alternativ inkluderar:

  • Kasta en IllegalArgumentException med anpassad kod.
  • Använda Preconditions tillgängliga i Google Guava-biblioteket.
  • Använda de Validate som finns tillgängliga i Apache Commons Lang3-biblioteket.

Det här är vad Java Language Specification (JLS 14.10, för Java 8) rekommenderar i denna fråga:

Vanligtvis är påståendekontroll aktiverad under programutveckling och testning och inaktiverad för distribution för att förbättra prestanda.

Eftersom påståenden kan vara inaktiverade, får program inte anta att uttryck som finns i påståenden kommer att utvärderas. Således bör dessa booleska uttryck i allmänhet vara fria från biverkningar. Utvärdering av ett sådant booleskt uttryck bör inte påverka något tillstånd som är synligt efter att utvärderingen är klar. Det är inte olagligt att ett booleskt uttryck i en påstående har en biverkning, men det är i allmänhet olämpligt, eftersom det kan orsaka programbeteende att variera beroende på om påståenden var aktiverade eller inaktiverade.

Mot bakgrund av detta bör påståenden inte användas för argumentkontroll i offentliga metoder. Argumentkontroll är vanligtvis en del av kontraktet för en metod, och detta kontrakt måste upprätthållas oavsett om påståenden är aktiverade eller inaktiverade.

Ett sekundärt problem med att använda påståenden för argumentkontroll är att felaktiga argument bör resultera i ett lämpligt undantag för körning (t.ex. IllegalArgumentException , ArrayIndexOutOfBoundsException eller NullPointerException ). Ett påståendefel kommer inte att kasta ett lämpligt undantag. Återigen är det inte olagligt att använda påståenden för argumentkontroll av offentliga metoder, men det är i allmänhet olämpligt. Det är avsett att AssertionError aldrig fångas, men det är möjligt att göra det. Reglerna för försök ska därför behandla påståenden som uppträder i ett försöksblock på samma sätt som den nuvarande behandlingen av uttalanden.

Fallgrop av auto-unboxing nollobjekt till primitiv

public class Foobar {
    public static void main(String[] args) {

        // example: 
        Boolean ignore = null;
        if (ignore == false) {
            System.out.println("Do not ignore!");
        }
    }
}

Fallgropen här är att null jämförs med false . Eftersom vi jämför en primitiv boolean en Boolean försöker Java att ta bort det Boolean Object i ett primitivt ekvivalent, redo för jämförelse. Men eftersom detta värde är null , NullPointerException en NullPointerException .

Java kan inte jämföra primitiva typer med null , vilket orsakar en NullPointerException vid körning. Betrakta det primitiva fallet med villkoret false == null ; detta skulle generera en kompileringstid fel incomparable types: int and <null> .



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow