Java Language
Java Pitfalls - Taalsyntaxis
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:
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.
Vertrouw de inspringing van uw code niet. In de Java-taal wordt extra witruimte aan het begin van een regel genegeerd door de compiler.
Gebruik een automatische indenter. Alle IDE's en veel eenvoudige teksteditors begrijpen hoe ze Java-code correct kunnen laten inspringen.
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 isequals(Object)
niet isequals(String)
. - De
hashcode
methode heeft de verkeerde naam. Dit moethashCode()
(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:
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
ofNullPointerException
). 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 datAssertionError
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>
.