Sök…


Introduktion

Detta ämne beskriver några av de vanliga misstag som nybörjare gjort i Java.

Detta inkluderar alla vanliga misstag i användningen av Java-språket eller förståelse av körmiljön.

Fel kopplade till specifika API: er kan beskrivas i ämnen specifika för dessa API: er. Strängar är ett speciellt fall; de omfattas av Java Language Specification. Andra detaljer än vanliga misstag kan beskrivas i detta ämne på Strängar .

Fallgrop: med == för att jämföra primitiva omslagsobjekt som heltal

(Denna fallgrop gäller lika för alla primitiva omslagstyper, men vi illustrerar den för Integer och int .)

När du arbetar med Integer är det frestande att använda == att jämföra värden, eftersom det är vad du skulle göra med int värden. Och i vissa fall verkar detta fungera:

Integer int1_1 = Integer.valueOf("1");
Integer int1_2 = Integer.valueOf(1);

System.out.println("int1_1 == int1_2: " + (int1_1 == int1_2));          // true
System.out.println("int1_1 equals int1_2: " + int1_1.equals(int1_2));   // true

Här skapade vi två Integer med värdet 1 och jämför dem (I detta fall skapade vi ett från en String och ett från ett int bokstav. Det finns andra alternativ). Vi observerar också att de två jämförelsemetoderna ( == och equals ) båda ger true .

Detta beteende förändras när vi väljer olika värden:

Integer int2_1 = Integer.valueOf("1000");
Integer int2_2 = Integer.valueOf(1000);

System.out.println("int2_1 == int2_2: " + (int2_1 == int2_2));          // false
System.out.println("int2_1 equals int2_2: " + int2_1.equals(int2_2));   // true

I detta fall ger endast equals jämförelse rätt resultat.

Anledningen till denna skillnad i beteende är att JVM upprätthåller en cache av Integer för intervallet -128 till 127. (Det övre värdet kan åsidosättas med systemegenskapen "java.lang.Integer.IntegerCache.high" eller JVM-argument "-XX: AutoBoxCacheMax = storlek"). För värden inom detta område kommer Integer.valueOf() att returnera det cachade värdet istället för att skapa ett nytt.

I det första exemplet Integer.valueOf(1) och Integer.valueOf("1") samma cache- Integer instans. Däremot Integer.valueOf(1000) och Integer.valueOf("1000") i det andra exemplet båda och returnerade nya Integer .

Operatören == för referenstyptester för referenslikhet (dvs. samma objekt). I det första exemplet är därför int1_1 == int1_2 true eftersom referenserna är desamma. I det andra exemplet är int2_1 == int2_2 falsk eftersom referenserna är olika.

Fallgrop: glömmer att frigöra resurser

Varje gång ett program öppnar en resurs, till exempel en fil eller nätverksanslutning, är det viktigt att frigöra resursen när du är klar med att använda den. Liknande försiktighet bör iakttas om något undantag skulle kastas under operationer på sådana resurser. Man kan hävda att FileInputStream har en finaliserare som åberopar den close() metoden på en soporhändelseshändelse; eftersom vi emellertid inte kan vara säkra på när en skräppostcykel kommer att starta kan ingångsströmmen konsumera datorresurser under en obestämd tidsperiod. Resursen måste vara stängd i ett finally avsnitt i ett trygghetsblock:

Java SE 7
private static void printFileJava6() throws IOException {
    FileInputStream input;
    try {
        input = new FileInputStream("file.txt");
        int data = input.read();
        while (data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    } finally {
        if (input != null) {
            input.close();
        }
    }
}

Sedan Java 7 finns det ett riktigt användbart och snyggt uttalande som introducerats i Java 7 särskilt för detta fall, kallat try-with-resources:

Java SE 7
private static void printFileJava7() throws IOException {
    try (FileInputStream input = new FileInputStream("file.txt")) {
        int data = input.read();
        while (data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    }
}

Try-with-resources- uttalandet kan användas med alla objekt som implementerar Closeable eller AutoCloseable . Det säkerställer att varje resurs stängs i slutet av uttalandet. Skillnaden mellan de två gränssnitten är att den close() metoden för Closeable kastar en IOException som måste hanteras på något sätt.

I de fall resursen redan har öppnats men bör stängas säkert efter användning kan man tilldela den till en lokal variabel i try-with-resurserna

Java SE 7
private static void printFileJava7(InputStream extResource) throws IOException {
    try (InputStream input = extResource) {
        ... //access resource
    }
}

Den lokala resursvariabeln som skapats i konstruktören med försök med resurser är effektiv slutlig.

Fallgrop: minne läcker

Java hanterar minnet automatiskt. Du behöver inte frigöra minne manuellt. Ett objekts minne på högen kan frigöras av en sopor när objektet inte längre kan nås med en levande tråd.

Du kan dock förhindra att minnet frigörs genom att låta objekt nås som inte längre behövs. Oavsett om du kallar detta för ett minnesläckage eller minnespaket är resultatet detsamma - en onödig ökning av tilldelat minne.

Minnesläckor i Java kan hända på olika sätt, men det vanligaste skälet är eviga objektreferenser, eftersom skräppassaren inte kan ta bort föremål från högen medan det fortfarande finns referenser till dem.

Statiska fält

Man kan skapa en sådan referens genom att definiera klass med ett static fält som innehåller viss samling av objekt och glömmer att ställa in det static fältet till null efter samlingen behövs inte längre. static fält betraktas som GC-rötter och samlas aldrig in. En annan fråga är läckor i minnet utan heap när JNI används.

Klassläsare läcker

Överlägset är dock den mest lumviga typen av minnessläcka klassläckarläckan . En klasslastare har en referens till varje klass den har laddat, och varje klass har en referens till sin klasslastare. Varje objekt har också en referens till sin klass. Om till och med ett enda objekt i en klass som laddas av en klasslastare inte är skräp, kan inte en enda klass som den klasslastaren har laddat samlas in. Eftersom varje klass också hänvisar till sina statiska fält, kan de inte heller samlas in.

Ackumuleringsläcka Exempel på ackumuleringsläcka kan se ut som följande:

final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    BigDecimal number = numbers.peekLast();
    if (number != null && number.remainder(divisor).byteValue() == 0) {
        System.out.println("Number: " + number);
        System.out.println("Deque size: " + numbers.size());
    }
}, 10, 10, TimeUnit.MILLISECONDS);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    numbers.add(new BigDecimal(System.currentTimeMillis()));
}, 10, 10, TimeUnit.MILLISECONDS);

try {
    scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
    e.printStackTrace();
}

Detta exempel skapar två schemalagda uppgifter. Den första uppgiften tar det sista numret från ett tecken som kallas numbers , och om antalet är delbart med 51 skriver det ut numret och teckenets storlek. Den andra uppgiften sätter siffror i täcken. Båda uppgifterna är schemalagda med en fast takt, och de körs var 10 ms.

Om koden körs kommer du att se att storleken på tavlan stiger permanent. Detta kommer så småningom att täcken fylls med föremål som konsumerar allt tillgängligt högminne.

För att förhindra detta medan vi bevarar semantiken i detta program, kan vi använda en annan metod för att ta nummer från deque: pollLast . I motsats till metoden peekLast , returnerar pollLast elementet och tar bort det från torkningen medan peekLast bara returnerar det sista elementet.

Fallgrop: använder == för att jämföra strängar

Ett vanligt misstag för Java-nybörjare är att använda operatören == att testa om två strängar är lika. Till exempel:

public class Hello {
    public static void main(String[] args) {
        if (args.length > 0) {
            if (args[0] == "hello") {
                System.out.println("Hello back to you");
            } else {
                System.out.println("Are you feeling grumpy today?");
            }
        }
    }
}

Ovanstående program är tänkt att testa det första kommandoradsargumentet och skriva ut olika meddelanden när det inte är ordet "hej". Men problemet är att det inte fungerar. Det programmet kommer att mata ut "Känner du dig grinig idag?" oavsett vad det första kommandoradsargumentet är.

I detta speciella fall String "hello" sätts i strängen poolen medan String args [0] finns på högen. Detta betyder att det finns två objekt som representerar samma bokstav, var och en med sin referens. Eftersom == tester för referenser, inte faktisk jämlikhet, kommer jämförelsen att ge en falsk de flesta gånger. Det betyder inte att det alltid kommer att göra det.

När du använder == att testa strängar är det du testar faktiskt om två String är samma Java-objekt. Tyvärr är det inte vad sträng jämställdhet betyder i Java. Faktum är att det korrekta sättet att testa strängar är att använda metoden equals(Object) . För ett par strängar vill vi oftast testa om de består av samma tecken i samma ordning.

public class Hello2 {
    public static void main(String[] args) {
        if (args.length > 0) {
            if (args[0].equals("hello")) {
                System.out.println("Hello back to you");
            } else {
                System.out.println("Are you feeling grumpy today?");
            }
        }
    }
}

Men det blir faktiskt värre. Problemet är att == kommer att ge det förväntade svaret under vissa omständigheter. Till exempel

public class Test1 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        if (s1 == s2) {
            System.out.println("same");
        } else {
            System.out.println("different");
        }
    }
}

Intressant nog kommer detta att skriva ut "samma", även om vi testar strängarna på fel sätt. Varför är det så? Eftersom Java Language Specification (avsnitt 3.10.5: String Literals) föreskriver att alla två strängar >> literals << som består av samma tecken faktiskt kommer att representeras av samma Java-objekt. Därför kommer testet == att gälla för lika stora bokstäver. (Strängbokstäverna "interneras" och läggs till i en delad "strängpool" när din kod laddas, men det är faktiskt en implementeringsdetalj.)

För att lägga till förvirringen föreskriver Java Language Specification också att när du har en konstant-tidskonstantuttryck som sammanlänker två strängbokstäver, motsvarar det en enda bokstav. Således:

    public class Test1 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        String s3 = " mum";
        if (s1 == s2) {
            System.out.println("1. same");
        } else {
            System.out.println("1. different");
        }
        if (s1 + s3 == "hello mum") {
            System.out.println("2. same");
        } else {
            System.out.println("2. different");
        }
    }
}

Detta kommer att mata ut "1. samma" och "2. annorlunda". I det första fallet utvärderas + -uttrycket vid sammanställningstiden och vi jämför ett String med sig själv. I det andra fallet utvärderas det vid körtid och vi jämför två olika String

Sammanfattningsvis är det att använda == att testa strängar i Java nästan alltid fel, men det är inte garanterat att du får fel svar.

Fallgrop: testa en fil innan du försöker öppna den.

Vissa människor rekommenderar att du använder olika tester på en fil innan du försöker öppna den antingen för att ge bättre diagnostik eller undvika att hantera undantag. Till exempel försöker den här metoden kontrollera om path motsvarar en läsbar fil:

public static File getValidatedFile(String path) throws IOException {
    File f = new File(path);
    if (!f.exists()) throw new IOException("Error: not found: " + path);
    if (!f.isFile()) throw new IOException("Error: Is a directory: " + path);
    if (!f.canRead()) throw new IOException("Error: cannot read file: " + path);
    return f;
}

Du kan använda ovanstående metod så här:

File f = null;
try {
    f = getValidatedFile("somefile");
} catch (IOException ex) {
    System.err.println(ex.getMessage());
    return;
}
try (InputStream is = new FileInputStream(file)) {
    // Read data etc.
}

Det första problemet är i signaturen för FileInputStream(File) eftersom kompilatorn fortfarande insisterar på att vi fångar IOException här, eller längre upp i bunten.

Det andra problemet är att kontroller som utförs av getValidatedFile inte garanterar att FileInputStream lyckas.

  • Rasförhållanden: en annan tråd eller en separat process kan byta namn på filen, ta bort filen eller ta bort getValidatedFile efter att getValidatedFile kommer tillbaka. Det skulle leda till en "vanlig" IOException utan det anpassade meddelandet.

  • Det finns kantfall som inte omfattas av dessa tester. Till exempel på ett system med SELinux i "verkställande" -läge kan ett försök att läsa en fil misslyckas trots att canRead() returnerar true .

Det tredje problemet är att testen är ineffektiva. Exempelvis exists , isFile och canRead samtal var och en gör ett syscall för att utföra den nödvändiga kontrollen. En annan syscall görs sedan för att öppna filen, som upprepar samma kontroller bakom kulisserna.

Kort sagt, metoder som getValidatedFile är felaktiga. Det är bättre att helt enkelt försöka öppna filen och hantera undantaget:

try (InputStream is = new FileInputStream("somefile")) {
    // Read data etc.
} catch (IOException ex) {
    System.err.println("IO Error processing 'somefile': " + ex.getMessage());
    return;
}

Om du ville skilja på IO-fel som kastades när du öppnade och läste, kan du använda en kapslad försök / fångst. Om du ville producera bättre diagnostik för öppna fel kan du utföra exists , isFile och canRead kontroller i hanteraren.

Fallgrop: att tänka på variabler som objekt

Ingen Java-variabel representerar ett objekt.

String foo;   // NOT AN OBJECT

Inga Java-array innehåller heller inte objekt.

String bar[] = new String[100];  // No member is an object.

Om du felaktigt tänker på variabler som objekt kommer Java-språket verkligen att överraska dig.

  • För Java-variabler som har en primitiv typ (som int eller float ) har variabeln en kopia av värdet. Alla kopior av ett primitivt värde kan inte skiljas; dvs det finns bara ett int värde för nummer ett. Primitiva värden är inte objekt och de uppför sig inte som objekt.

  • För Java-variabler som har en referenstyp (antingen en klass eller en array-typ) har variabeln en referens. Alla kopior av en referens kan inte skiljas. Hänvisningar kan peka på objekt, eller de kan vara null vilket betyder att de pekar på inget objekt. Men de är inte föremål och de uppför sig inte som föremål.

Variabler är inte objekt i båda fallen, och de innehåller inte objekt i båda fallen. De kan innehålla referenser till objekt , men det säger något annat.

Exempel klass

Exemplen som följer använder denna klass, som representerar en punkt i 2D-rymden.

public final class MutableLocation {
   public int x;
   public int y;

   public MutableLocation(int x, int y) {
       this.x = x;
       this.y = y;
   }

   public boolean equals(Object other) {
       if (!(other instanceof MutableLocation) {
           return false;
       }
       MutableLocation that = (MutableLocation) other;
       return this.x == that.x && this.y == that.y;
   }
}

Ett exempel på denna klass är ett objekt som har två fält x och y som har typen int .

Vi kan ha många fall av klassen MutableLocation . Vissa kommer att representera samma platser i 2D-rymden; dvs. de respektive värdena för x och y kommer att matcha. Andra kommer att representera olika platser.

Flera variabler kan peka på samma objekt

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

I det ovanstående har vi förklarat tre variabler here , there och på elsewhere som kan innehålla referenser till MutableLocation objekt.

Om du (felaktigt) tänker på dessa variabler som objekt är det troligt att du läser felaktiga uttalanden som att säga:

  1. Kopiera platsen "[1, 2]" here
  2. Kopiera platsen "[1, 2]" there
  3. Kopiera platsen "[1, 2]" till elsewhere

Från det kommer du sannolikt att dra slutsatsen att vi har tre oberoende objekt i de tre variablerna. I själva verket finns det bara två objekt som skapats av ovanstående. Variablerna here och there hänvisar faktiskt till samma objekt.

Vi kan visa detta. Antagande av variabla deklarationer som ovan:

System.out.println("BEFORE: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);
here.x = 42;
System.out.println("AFTER: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);

Detta kommer att producera följande:

BEFORE: here.x is 1, there.x is 1, elsewhere.x is 1
AFTER: here.x is 42, there.x is 42, elsewhere.x is 1

Vi tilldelade ett nytt värde till here.x och det ändrade värdet som vi ser via there.x . De avser samma objekt. Men värdet som vi ser via elsewhere.x har inte förändrats, så elsewhere måste hänvisa till ett annat objekt.

Om en variabel var ett objekt, skulle tilldelningen here.x = 42 inte ändras there.x

Jämställdhetsoperatören testar INTE att två objekt är lika

Tillämpa jämställdhetsoperatören ( == ) på referensvärdenstest om värdena avser samma objekt. Det är inte testa om två (olika) objekt är "lika" i intuitiv känsla.

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

 if (here == there) {
     System.out.println("here is there");
 }
 if (here == elsewhere) {
     System.out.println("here is elsewhere");
 }

Detta kommer att skriva ut "här är där", men det kommer inte att skriva ut "här är någon annanstans". (Hänvisningarna here och på elsewhere är för två distinkta objekt.)

Däremot, om vi kallar metoden equals(Object) som vi implementerade ovan, kommer vi att testa om två MutableLocation instanser har en lika plats.

 if (here.equals(there)) {
     System.out.println("here equals there");
 }
 if (here.equals(elsewhere)) {
     System.out.println("here equals elsewhere");
 }

Detta kommer att skriva ut båda meddelandena. I synnerhet here.equals(elsewhere) true eftersom de semantiska kriterierna vi valde för jämlikhet mellan två MutableLocation objekt har uppfyllts.

Metodsamtal passerar INTE objekt alls

Java-metodsamtal använder pass by value 1 för att skicka argument och returnera ett resultat.

När du skickar ett referensvärde till en metod överför du faktiskt en referens till ett objekt efter värde , vilket innebär att det skapar en kopia av objektreferensen.

Så länge båda objektreferenser fortfarande pekar på samma objekt kan du ändra objektet från endera referensen, och det är detta som orsakar förvirring för vissa.

Men är du inte passerar ett objekt genom hänvisning 2. Skillnaden är att om objektreferenskopian ändras för att peka på ett annat objekt, kommer den ursprungliga objektreferensen fortfarande att peka på det ursprungliga objektet.

void f(MutableLocation foo) {  
    foo = new MutableLocation(3, 4);   // Point local foo at a different object.
}

void g() {
    MutableLocation foo = MutableLocation(1, 2);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 1".
}

Inte heller skickar du en kopia av objektet.

void f(MutableLocation foo) {  
    foo.x = 42;
}

void g() {
    MutableLocation foo = new MutableLocation(0, 0);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 42"
}

1 - På språk som Python och Ruby föredras termen "pass by sharing" för "pass by value" för ett objekt / referens.

2 - Termen "pass by reference" eller "call by reference" har en mycket specifik betydelse i programmeringsspråkets terminologi. I själva verket betyder det att du passerar adressen till en variabel eller ett arrayelement , så att när den anropade metoden tilldelar ett nytt värde till det formella argumentet, ändrar det värdet i den ursprungliga variabeln. Java stöder inte detta. För en mer fullständig beskrivning av olika mekanismer för att skicka parametrar, se https://sv.wikipedia.org/wiki/Evaluation_strategy .

Fallgrop: att kombinera uppdrag och biverkningar

Ibland ser vi StackOverflow Java-frågor (och C- eller C ++ -frågor) som ställer vad något liknande:

i += a[i++] + b[i--];

utvärderar till ... för några kända initialtillstånd i , a och b .

Generellt:

  • för Java är svaret alltid specificerat 1 , men inte uppenbart, och ofta svårt att räkna ut
  • för C och C ++ är svaret ofta ospecificerat.

Sådana exempel används ofta i tentor eller jobbintervjuer som ett försök att se om eleven eller intervjuaren förstår hur uttrycksutvärdering verkligen fungerar på Java-programmeringsspråket. Detta är utan tvekan legitimt som ett "kunskapstest", men det betyder inte att du någonsin ska göra detta i ett riktigt program.

För att illustrera har följande till synes enkla exempel dykt upp några gånger i StackOverflow-frågor (som det här ). I vissa fall visas det som ett äkta misstag i någons kod.

int a = 1;
a = a++;
System.out.println(a);    // What does this print.

De flesta programmerare (inklusive Java-experter) som läser dessa uttalanden snabbt skulle säga att det matar ut 2 . I själva verket matar det ut 1 . För en detaljerad förklaring av varför, läs detta svar .

Men den verkliga takeawayen från detta och liknande exempel är att alla Java-uttalanden som både tilldelar och biverkningar samma variabel kommer att vara i bästa fall svåra att förstå och i värsta fall helt vilseledande. Du bör undvika att skriva kod som den här.


1 - moduloproblem med Java Memory Model om variabler eller objekt är synliga för andra trådar.

Fallgrop: Inte förstå att String är en oföränderlig klass

Nya Java-programmerare glömmer ofta eller misslyckas helt med att Java String klassen är oföränderlig. Detta leder till problem som i följande exempel:

public class Shout {
    public static void main(String[] args) {
        for (String s : args) {
            s.toUpperCase();
            System.out.print(s);
            System.out.print(" ");
        }
        System.out.println();
    }
}

Ovanstående kod är tänkt att skriva ut kommandoradsargument med versaler. Tyvärr fungerar det inte, argumentets fall ändras inte. Problemet är detta uttalande:

s.toUpperCase();

Du kanske tror att att ringa till toUpperCase() kommer att ändra s till en versalsträng. Det gör det inte. Det kan inte! String är oföränderliga. De kan inte ändras.

I verkligheten toUpperCase() metoden returnerar en String föremål som är en versal version av String som du kallar den. Detta kommer förmodligen att vara ett nytt String , men om s redan var alla stora versaler kan resultatet bli den befintliga strängen.

Så för att använda den här metoden effektivt måste du använda objektet som returneras av metodsamtalet; till exempel:

s = s.toUpperCase();

Faktum är att "strängar ändras aldrig" -regeln gäller alla String . Om du kommer ihåg det kan du undvika en hel kategori av nybörjarens misstag.



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