Zoeken…


Invoering

Verschillende misbruik van Java-programmeertaal kan een programma uitvoeren om onjuiste resultaten te genereren, ondanks dat het correct is gecompileerd. Het hoofddoel van dit onderwerp is om veel voorkomende valkuilen met hun oorzaken op te sommen en de juiste manier voor te stellen om te voorkomen dat u in dergelijke problemen terechtkomt.

Opmerkingen

Dit onderwerp gaat over specifieke aspecten van de syntaxis van de Java-taal die foutgevoelig zijn of die niet op bepaalde manieren mogen worden gebruikt.

Valkuil - Zichtbaarheid van methoden negeren

Zelfs ervaren Java-ontwikkelaars hebben de neiging te denken dat Java slechts drie beschermingsmodificatoren heeft. De taal heeft eigenlijk vier! Het private (alias standaard) zichtniveau van het pakket wordt vaak vergeten.

Je moet letten op welke methoden je openbaar maakt. De openbare methoden in een toepassing zijn de zichtbare API van de toepassing. Dit moet zo klein en compact mogelijk zijn, vooral als u een herbruikbare bibliotheek schrijft (zie ook het SOLID- principe). Het is belangrijk om op dezelfde manier rekening te houden met de zichtbaarheid van alle methoden en alleen beveiligde toegang of private toegangspakketten te gebruiken waar nodig.

Wanneer u methoden aangeeft die privé als openbaar moeten zijn, geeft u de interne implementatiegegevens van de klasse weer.

Een gevolg hiervan is dat u alleen unit test het publieke methoden van de klasse - in feite kun je alleen in openbare methoden te testen. Het is een slechte gewoonte om de zichtbaarheid van privé-methoden te vergroten alleen om unit-tests tegen die methoden te kunnen uitvoeren. Het testen van openbare methoden die methoden met beperktere zichtbaarheid oproepen, moet voldoende zijn om een hele API te testen. U moet uw API nooit uitbreiden met meer openbare methoden, alleen voor het testen van eenheden.

Valkuil - Er ontbreekt een 'pauze' in een 'wissel'-zaak

Deze Java-problemen kunnen erg gênant zijn en blijven soms onontdekt totdat ze in productie worden genomen. Doorvalgedrag in schakelinstructies is vaak nuttig; het missen van een 'break'-trefwoord wanneer dergelijk gedrag niet gewenst is, kan echter leiden tot desastreuze resultaten. Als u bent vergeten om een "pauze" in "geval 0" in het onderstaande codevoorbeeld te plaatsen, schrijft het programma "Nul" gevolgd door "Eén", omdat de besturingsstroom hier de hele "schakeloptie" doorloopt totdat het bereikt een "pauze". Bijvoorbeeld:

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");
        }
}

In de meeste gevallen zou de schonere oplossing zijn om interfaces te gebruiken en code met specifiek gedrag te verplaatsen naar afzonderlijke implementaties ( samenstelling overerving )

Als een switch-statement onvermijdelijk is, wordt aanbevolen om "verwachte" doorbraken te documenteren als deze zich voordoen. Op die manier laat je collega-ontwikkelaars zien dat je je bewust bent van de ontbrekende pauze en dat dit verwacht gedrag is.

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

Valkuil - Misplaatste puntkomma's en ontbrekende accolades

Dit is een fout die echte verwarring veroorzaakt voor Java-beginners, althans de eerste keer dat ze het doen. In plaats van dit te schrijven:

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

ze schrijven dit per ongeluk:

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

en zijn verbaasd wanneer de Java-compiler hen vertelt dat de else misplaatst is. De Java-compiler met interpreteert het bovenstaande als volgt:

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");

In andere gevallen zullen er geen compilatiefouten zijn, maar de code zal niet doen wat de programmeur van plan is. Bijvoorbeeld:

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

drukt "Hallo" slechts eenmaal af. Nogmaals, de onechte puntkomma betekent dat het lichaam van de for lus een lege verklaring is. Dat betekent dat de volgende println oproep onvoorwaardelijk is.

Een andere variatie:

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

Dit geeft een foutmelding "Kan symbool niet vinden" voor i . De aanwezigheid van de onechte puntkomma betekent dat de println oproep i buiten zijn bereik probeert te gebruiken.

In die voorbeelden is er een eenvoudige oplossing: verwijder eenvoudig de valse puntkomma. Er kunnen echter een aantal diepere lessen worden getrokken uit deze voorbeelden:

  1. De puntkomma in Java is geen "syntactische ruis". De aanwezigheid of afwezigheid van een puntkomma kan de betekenis van uw programma veranderen. Voeg ze niet alleen toe aan het einde van elke regel.

  2. Vertrouw de inspringing van uw code niet. In de Java-taal wordt extra witruimte aan het begin van een regel genegeerd door de compiler.

  3. Gebruik een automatische indenter. Alle IDE's en veel eenvoudige teksteditors begrijpen hoe ze Java-code correct kunnen laten inspringen.

  4. Dit is de belangrijkste les. Volg de nieuwste richtlijnen in Java-stijl en zet accolades rond de "toen" en "anders" -instructies en de body-instructie van een lus. De open accolade ( { ) mag niet op een nieuwe regel staan.

Als de programmeur de stijlregels volgde, zou het if voorbeeld met een misplaatste puntkomma er als volgt uitzien:

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

Dat ziet er vreemd uit voor een ervaren oog. Als u die code automatisch inspringt, ziet deze er waarschijnlijk als volgt uit:

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

die zelfs voor een beginner als verkeerd zou moeten opvallen.

Valkuil - Beugels weglaten: de problemen met 'bungelen als' en 'bungelen anders'

De nieuwste versie van de Oracle Java-stijlgids verplicht dat de "toen" en "anders" -instructies in een if instructie altijd tussen "accolades" of "accolades" moeten staan. Soortgelijke regels zijn van toepassing op de lichamen van verschillende lusinstructies.

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

Dit is eigenlijk niet vereist voor de syntaxis van de Java-taal. Inderdaad, als het "dan" deel van een if statement een enkele statement is, is het legaal om de accolades weg te laten

if (a)
    doSomething();

of zelfs

if (a) doSomething();

Er zijn echter gevaren aan het negeren van regels in Java-stijl en het weglaten van de accolades. U verhoogt met name het risico dat code met onjuiste inspringing verkeerd wordt gelezen.

Het probleem "bungelend als":

Beschouw de voorbeeldcode van hierboven, herschreven zonder accolades.

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

Deze code lijkt te zeggen dat de gesprekken naar doSomething en doSomeMore beide zal optreden als en slechts als a is true . In feite is de code onjuist ingesprongen. De Java- doSomeMore() die door de doSomeMore() gedaan, is een afzonderlijke instructie na de if instructie. De juiste inspringing is als volgt:

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

Het probleem "bungelend anders"

Een tweede probleem verschijnt wanneer we iets else aan de mix toevoegen. Beschouw het volgende voorbeeld met ontbrekende accolades.

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

De bovenstaande code lijkt te zeggen dat doZ wordt aangeroepen als a false . De inspringing is zelfs opnieuw onjuist. De juiste inspringing voor de code is:

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

Als de code is geschreven volgens de Java-stijlregels, ziet deze er eigenlijk zo uit:

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

Om te illustreren waarom dat beter is, stel je voor dat je de code per ongeluk verkeerd hebt ingesprongen. Je zou kunnen eindigen met zoiets als dit:

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

Maar in beide gevallen ziet de verkeerd ingesprongen code er voor een ervaren Java-programmeur "verkeerd" uit.

Valkuil - Overbelasting in plaats van overschrijven

Overweeg het volgende voorbeeld:

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();
    }
}

Deze code zal zich niet gedragen zoals verwacht. Het probleem is dat de equals en hashcode voor Person niet de standaardmethoden overschrijven die door Object gedefinieerd.

  • De methode is equals de verkeerde handtekening. Het moet worden aangegeven als is equals(Object) niet is equals(String) .
  • De hashcode methode heeft de verkeerde naam. Dit moet hashCode() (let op hoofdletter C ).

Deze fouten betekenen dat we per ongeluk overbelastingen hebben verklaard en deze zullen niet worden gebruikt als Person in een polymorfe context wordt gebruikt.

Er is echter een eenvoudige manier om hiermee om te gaan (vanaf Java 5). Gebruik de @Override annotatie wanneer u uw methode van plan om een override zijn:

Java SE 5
public final class Person {
    ...

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

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

Als we een bijkomstigheid @Override annotatie een methodedeclaratie, zal de compiler controleren of de methode doet override (of uitvoering) een werkwijze superklasse of grensvlak verklaard. In het bovenstaande voorbeeld geeft de compiler ons twee compilatiefouten, wat voldoende zou moeten zijn om ons op de fout te attenderen.

Valkuil - Octale literalen

Overweeg het volgende codefragment:

// 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);

Een Java-beginner kan verrast zijn om te weten dat het bovenstaande programma het verkeerde antwoord afdrukt. Het drukt eigenlijk de som van de getallen 1 tot 8 af.

De reden is dat een geheel getal letterlijk dat begint met het cijfer nul ('0') door de Java-compiler wordt geïnterpreteerd als een octaal letterlijk, niet als een decimaal letterlijk zoals je zou verwachten. Dus 010 is het octale getal 10, dat 8 in decimalen is.

Valkuil - Klassen met dezelfde namen als standaardklassen declareren

Soms maken programmeurs die nieuw zijn op Java de fout een klasse te definiëren met een naam die hetzelfde is als een veelgebruikte klasse. Bijvoorbeeld:

package com.example;

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

Dan vragen ze zich af waarom ze onverwachte fouten krijgen. Bijvoorbeeld:

package com.example;

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

Als u compileert en vervolgens probeert de bovenstaande klassen uit te voeren, krijgt u een foutmelding:

$ 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

Iemand op zoek naar de code voor de Test klasse zou de verklaring te zien van main en de blik op de ondertekening en vraag me af wat de java commando klaagt over. Maar in feite vertelt het java commando de waarheid.

Wanneer we een versie van String in hetzelfde pakket als Test declareren, heeft deze versie voorrang op de automatische import van java.lang.String . De handtekening van de methode Test.main is dus eigenlijk

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

in plaats van

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

en de java opdracht niet zal erkennen dat als een entrypoint methode.

Les: definieer geen klassen met dezelfde naam als bestaande klassen in java.lang of andere veelgebruikte klassen in de Java SE-bibliotheek. Als u dat doet, stelt u zich open voor allerlei obscure fouten.

Valkuil - Gebruik '==' om een Boolean te testen

Soms schrijft een nieuwe Java-programmeur code zoals deze:

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

Een ervaren programmeur ziet dat als onhandig en wil het herschrijven als:

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

Er is echter meer mis met ok == true dan eenvoudige onhandigheid. Overweeg deze variatie:

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

Hier heeft de programmeur == as = ... verkeerd == en nu heeft de code een subtiele bug. De uitdrukking x = true wijst onvoorwaardelijk true aan x en evalueert vervolgens true . Met andere woorden, de check zal nu afdrukken "Het is OK", ongeacht wat de parameter was.

De les hier is om af te komen van de gewoonte om == false en == true . Ze zijn niet alleen uitgebreid, ze maken uw codering ook foutgevoeliger.


Opmerking: een mogelijk alternatief voor ok == true dat de valkuil vermijdt, is het gebruik van Yoda-omstandigheden ; dwz plaats de letterlijke letter aan de linkerkant van de relationele operator, zoals in true == ok . Dit werkt, maar de meeste programmeurs zijn het er waarschijnlijk mee eens dat de omstandigheden in Yoda er vreemd uitzien. Zeker ok (of !ok ) is beknopter en natuurlijker.

Valkuil - Import van jokertekens kan uw code kwetsbaar maken

Overweeg het volgende gedeeltelijke voorbeeld:

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

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

Stel dat toen u voor het eerst de code ontwikkelde tegen versie 1.0 van somelib en versie 1.0 van otherlib . Vervolgens moet u op een later moment uw afhankelijkheden upgraden naar een latere versie en besluit u otherlib versie 2.0 te gebruiken. Stel ook dat een van de wijzigingen die ze in otherlib tussen 1.0 en 2.0 hebben aangebracht, het toevoegen van een Context klasse was.

Wanneer u nu Test opnieuw compileert, krijgt u een compilatiefout die u vertelt dat Context een dubbelzinnige import is.

Als u bekend bent met de codebase, is dit waarschijnlijk slechts een klein ongemak. Als dit niet het geval is, moet u dit probleem hier en mogelijk elders aanpakken.

Het probleem hier is de invoer met jokertekens. Enerzijds kan het gebruik van jokertekens uw klassen een paar regels korter maken. Aan de andere kant:

  • Naar boven compatibele wijzigingen in andere delen van uw codebase, in standaard Java-bibliotheken of in externe bibliotheken kunnen tot compilatiefouten leiden.

  • Leesbaarheid lijdt. Tenzij u een IDE gebruikt, kan het moeilijk zijn om erachter te komen welke van de wildcard-invoer in een genoemde klasse trekt.

De les is dat het een slecht idee is om jokertekens te importeren in code die lang moet meegaan. Specifieke (niet-wildcard) import is niet veel moeite om te handhaven als u een IDE gebruikt, en de moeite is de moeite waard.

Valkuil: 'assert' gebruiken voor validatie van argument of gebruikersinvoer

Een vraag die af en toe op StackOverflow is of het passend is om assert te gebruiken assert het valideren van argumenten die aan een methode zijn geleverd, of zelfs invoer die door de gebruiker is verstrekt.

Het eenvoudige antwoord is dat het niet gepast is.

Betere alternatieven zijn onder meer:

  • Een IllegalArgumentException gooien met aangepaste code.
  • Met behulp van de Preconditions methoden die beschikbaar zijn in de Google Guava-bibliotheek.
  • Met behulp van de Validate methoden die beschikbaar zijn in de Apache Commons Lang3-bibliotheek.

Dit is wat de Java Language Specification (JLS 14.10, voor Java 8) hierover adviseert:

Doorgaans wordt bewering van beweringen ingeschakeld tijdens het ontwikkelen en testen van programma's en uitgeschakeld voor implementatie om de prestaties te verbeteren.

Omdat beweringen zijn uitgeschakeld, mogen programma's er niet van uitgaan dat de uitdrukkingen in beweringen worden geëvalueerd. Aldus zouden deze booleaanse uitdrukkingen in het algemeen vrij moeten zijn van bijwerkingen. Het evalueren van een dergelijke Booleaanse uitdrukking mag geen invloed hebben op een toestand die zichtbaar is nadat de evaluatie is voltooid. Het is niet illegaal dat een booleaanse uitdrukking in een bewering een bijwerking heeft, maar het is over het algemeen ongepast, omdat dit ertoe kan leiden dat het programmagebruik varieert, afhankelijk van of beweringen zijn ingeschakeld of uitgeschakeld.

In het licht hiervan mogen beweringen niet worden gebruikt voor argumentcontrole bij openbare methoden. Controle van argumenten maakt meestal deel uit van het contract van een methode en dit contract moet worden bevestigd, ongeacht of beweringen zijn ingeschakeld of uitgeschakeld.

Een bijkomend probleem met het gebruik van beweringen voor het controleren van argumenten is dat foutieve argumenten moeten resulteren in een geschikte runtime-uitzondering (zoals IllegalArgumentException , ArrayIndexOutOfBoundsException of NullPointerException ). Een bewering mislukt geen passende uitzondering. Nogmaals, het is niet illegaal om beweringen te gebruiken voor argumentcontrole op openbare methoden, maar het is over het algemeen ongepast. Het is de bedoeling dat AssertionError nooit wordt betrapt, maar het is mogelijk om dit te doen, dus de regels voor try-statements moeten beweringen die in een try-blok voorkomen op dezelfde manier behandelen als de huidige behandeling van throw-statements.

Valkuil van het automatisch uitpakken van null-objecten in primitieven

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

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

De valkuil hier is dat null wordt vergeleken met false . Omdat we een primitieve boolean vergelijken met een Boolean , probeert Java het Boolean Object te unboxen in een primitief equivalent, klaar voor vergelijking. Omdat deze waarde echter null , wordt een NullPointerException gegenereerd.

Java is niet in staat primitieve typen te vergelijken met null waarden, wat tijdens runtime een NullPointerException veroorzaakt. Beschouw het primitieve geval van de voorwaarde false == null ; dit zou een compilatie tijd fout incomparable types: int and <null> genereren incomparable types: int and <null> .



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow