Die 10 häufigsten Fehler in Java-Projekten im Jahr 2019


2019 neigt sich dem Ende zu und das PVS-Studio-Team blickt auf die Erfolge dieses Jahres zurück. Anfang 2019 erweiterten wir die Diagnosefunktionen unseres Analysators um Java-Unterstützung, mit der wir auch Java-Projekte überprüfen und überprüfen konnten. Wir haben in diesem Jahr viele Fehler gefunden, und hier sind unsere 10 häufigsten Fehler, die in Java-Projekten gefunden wurden.


Nr 10: Byte mit Vorzeichen


Quelle: Analyse des Apache Dubbo RPC Frameworks durch den PVS-Studio Static Code Analyzer

V6007 Ausdruck 'endKey [i] <0xff' ist immer wahr. OptionUtil.java (32)

public static final ByteSequence prefixEndOf(ByteSequence prefix) { byte[] endKey = prefix.getBytes().clone(); for (int i = endKey.length - 1; i >= 0; i--) { if (endKey[i] < 0xff) { // <= endKey[i] = (byte) (endKey[i] + 1); return ByteSequence.from(Arrays.copyOf(endKey, i + 1)); } } return ByteSequence.from(NO_PREFIX_END); } 

Viele Programmierer glauben, dass der Bytetyp nicht vorzeichenbehaftet ist. Dies ist in der Tat in vielen Programmiersprachen der Fall. Dies gilt beispielsweise für C #. Bei Java ist das nicht der Fall.

In der Bedingung endKey [i] <0xff wird eine Variable vom Typ Byte mit der hexadezimal dargestellten Zahl 255 (0xff) verglichen. Der Entwickler hat wahrscheinlich vergessen, dass der Bereich des Java- Bytetyps [-128, 127] ist. Diese Bedingung ist immer wahr, und die for- Schleife verarbeitet immer nur das letzte Element des endKey- Arrays.

Nr 9: Zwei in einem


Quelle: PVS-Studio für Java ist auf dem Vormarsch. Nächste Station ist Elasticsearch

V6007 Ausdruck '(int) x <0' ist immer falsch. BCrypt.java (429)

V6025 Möglicherweise liegt der Index '(int) x' außerhalb des zulässigen Bereichs . BCrypt.java (431)

 private static byte char64(char x) { if ((int)x < 0 || (int)x > index_64.length) return -1; return index_64[(int)x]; } 

Rabatt! Eine Methode - zwei Bugs! Das erste hat mit dem char- Typ zu tun: Da er in Java nicht signiert ist, ist die (int) x <0- Bedingung immer falsch. Der zweite Fehler ist die gewöhnliche Indizierung außerhalb der Grenzen des Arrays index_64, wenn (int) x == index_64.length ist . Dies geschieht aufgrund der Bedingung (int) x> index_64.length . Der Fehler kann behoben werden, indem der Operator '>' in '> =' geändert wird : (int) x> = index_64.length .

Nr 8: Eine Lösung und ihre Auswirkungen


Quelle: Analyse des Codes der CUBA-Plattform mit PVS-Studio

V6007 Ausdruck 'previousMenuItemFlatIndex> = 0' ist immer wahr. CubaSideMenuWidget.java (328)

 protected MenuItemWidget findNextMenuItem(MenuItemWidget currentItem) { List<MenuTreeNode> menuTree = buildVisibleTree(this); List<MenuItemWidget> menuItemWidgets = menuTreeToList(menuTree); int menuItemFlatIndex = menuItemWidgets.indexOf(currentItem); int previousMenuItemFlatIndex = menuItemFlatIndex + 1; if (previousMenuItemFlatIndex >= 0) { // <= return menuItemWidgets.get(previousMenuItemFlatIndex); } return null; } 

Der Autor der findNextMenuItem- Methode wollte den von der indexOf- Methode zurückgegebenen Wert -1 entfernen , wenn die menuItemWidgets- Liste kein currentItem enthält. Dazu addiert der Programmierer 1 zum Ergebnis von indexOf (der Variable menuItemFlatIndex ) und schreibt den resultierenden Wert in die Variable previousMenuItemFlatIndex , die dann in der Methode verwendet wird. Das Hinzufügen von -1 erweist sich als schlechte Lösung, da es gleichzeitig zu mehreren Fehlern führt:

  • Die Anweisung return null wird niemals ausgeführt, da der Ausdruck previousMenuItemFlatIndex> = 0 immer true ist. Daher gibt die findNextMenuItem- Methode immer aus der if- Anweisung zurück.
  • Eine IndexOutOfBoundsException wird ausgelöst , wenn die menuItemWidgets- Liste leer ist, da das Programm versucht, auf das erste Element der leeren Liste zuzugreifen.
  • Eine IndexOutOfBoundsException wird ausgelöst , wenn das Argument currentItem das letzte Element der menuItemWidget- Liste ist.

Nr 7: Aus nichts eine Datei machen


Quelle: Huawei Cloud: Heute ist es im PVS-Studio bewölkt

V6008 Mögliche Null-Dereferenzierung von 'dataTmpFile'. CacheManager.java (91)

 @Override public void putToCache(PutRecordsRequest putRecordsRequest) { .... if (dataTmpFile == null || !dataTmpFile.exists()) { try { dataTmpFile.createNewFile(); // <= } catch (IOException e) { LOGGER.error("Failed to create cache tmp file, return.", e); return; } } .... } 

Beim Schreiben der putToCache- Methode machte der Programmierer einen Tippfehler in der Bedingung dataTmpFile == null || ! dataTmpFile.exists () vor dem Erstellen einer neuen Datei dataTmpFile.createNewFile () : Sie haben den Operator '==' anstelle von '! =' geschrieben. Dieser Tippfehler führt beim Aufrufen der createNewFile- Methode zum Auslösen einer NullPointerException . So sieht die Bedingung mit dem korrigierten Tippfehler aus:

 if (dataTmpFile != null || !dataTmpFile.exists()) 

Sie könnten denken: "Nun, okay, wir können uns jetzt entspannen." Noch nicht!

Nachdem ein Fehler behoben wurde, tauchte ein weiterer auf. Beim Aufrufen der Methode dataTmpFile.exists () wird möglicherweise eine NullPointerException ausgelöst. Um dies zu beheben, müssen wir das '||' ersetzen Operator mit '&&'. Dies ist die endgültige Version der Bedingung, nach allen Korrekturen:

 if (dataTmpFile != null && !dataTmpFile.exists()) 

Nr 6: Ein sehr seltsamer Logikfehler


Quelle: PVS-Studio für Java

V6007 [CWE-570] Der Ausdruck "0" .equals (text) "ist immer falsch. ConvertIntegerToDecimalPredicate.java 46

 public boolean satisfiedBy(@NotNull PsiElement element) { .... @NonNls final String text = expression.getText().replaceAll("_", ""); if (text == null || text.length() < 2) { return false; } if ("0".equals(text) || "0L".equals(text) || "0l".equals(text)) {// <= return false; } return text.charAt(0) == '0'; } 

Das Interessante an dieser Methode ist, dass sie einen offensichtlichen logischen Fehler enthält. Wenn die satisfiedBy- Methode nach der ersten if- Anweisung nicht zurückgegeben wird, bedeutet dies, dass die Textzeichenfolge mindestens zwei Zeichen lang ist. Dies bedeutet auch, dass die allererste Prüfung von "0" .equals (text) in der nächsten if- Anweisung bedeutungslos ist. Was der Programmierer damit eigentlich gemeint hat, bleibt ein Rätsel.

Nr 5: Was für eine Wendung!


Quelle: PVS-Studio besucht Apache Hive

V6034 Die Verschiebung um den Wert 'bitShiftsInWord - 1' kann mit der Größe des Typs 'bitShiftsInWord - 1' = [-1 ... 30] inkonsistent sein. UnsignedInt128.java (1791)

 private void shiftRightDestructive(int wordShifts, int bitShiftsInWord, boolean roundUp) { if (wordShifts == 0 && bitShiftsInWord == 0) { return; } assert (wordShifts >= 0); assert (bitShiftsInWord >= 0); assert (bitShiftsInWord < 32); if (wordShifts >= 4) { zeroClear(); return; } final int shiftRestore = 32 - bitShiftsInWord; // check this because "123 << 32" will be 123. final boolean noRestore = bitShiftsInWord == 0; final int roundCarryNoRestoreMask = 1 << 31; final int roundCarryMask = (1 << (bitShiftsInWord - 1)); // <= .... } 

Mit den Eingabeargumenten wordShifts = 3 und bitShiftsInWord = 0 wird die Variable roundCarryMask , die das Ergebnis der bitweisen Verschiebung (1 << (bitShiftsInWord - 1)) speichert , zu einer negativen Zahl. Der Entwickler hat das wahrscheinlich nicht erwartet.

Nr 4: Können wir bitte die Ausnahmen sehen?


Quelle: PVS-Studio besucht Apache Hive

V6051 Die Verwendung der Anweisung 'return' im Block 'finally' kann zum Verlust nicht behandelter Ausnahmen führen. ObjectStore.java (9080)

 private List<MPartitionColumnStatistics> getMPartitionColumnStatistics(....) throws NoSuchObjectException, MetaException { boolean committed = false; try { .... /*some actions*/ committed = commitTransaction(); return result; } catch (Exception ex) { LOG.error("Error retrieving statistics via jdo", ex); if (ex instanceof MetaException) { throw (MetaException) ex; } throw new MetaException(ex.getMessage()); } finally { if (!committed) { rollbackTransaction(); return Lists.newArrayList(); } } } 

Die Deklaration der Methode getMPartitionColumnStatistics ist irreführend, da sie besagt, dass sie eine Ausnahme auslösen könnte. Unabhängig davon, welche Ausnahme im try- Block generiert wird, ist die festgeschriebene Variable immer false , sodass die return- Anweisung im finally- Block einen Wert zurückgibt, während alle ausgelösten Ausnahmen verloren gehen und außerhalb der Methode nicht verarbeitet werden können. Daher kann keine der in dieser Methode ausgelösten Ausnahmen sie verlassen.

Nr 3: Hokuspokus oder Versuch, eine neue Maske zu bekommen


Quelle: PVS-Studio besucht Apache Hive

V6034 Die Verschiebung um den Wert 'j' kann mit der Größe des Typs inkonsistent sein: 'j' = [0 ... 63]. IoTrace.java (272)

 public void logSargResult(int stripeIx, boolean[] rgsToRead) { .... for (int i = 0, valOffset = 0; i < elements; ++i, valOffset += 64) { long val = 0; for (int j = 0; j < 64; ++j) { int ix = valOffset + j; if (rgsToRead.length == ix) break; if (!rgsToRead[ix]) continue; val = val | (1 << j); // <= } .... } .... } 

Auch dieser Fehler hat mit einer bitweisen Verschiebung zu tun, aber nicht nur das. Die Variable j wird als Zähler über den Bereich [0 ... 63] in der inneren for- Schleife verwendet. Dieser Zähler nimmt an einer bitweisen Verschiebung 1 << j teil . Alles scheint in Ordnung zu sein, aber hier kommt das Integer-Literal '1' vom Typ int (ein 32-Bit-Wert) ins Spiel. Aus diesem Grund beginnt die bitweise Verschiebung, die zuvor zurückgegebenen Werte zurückzugeben, wenn der Wert der Variablen j 31 überschritten hat. Wenn dieses Verhalten nicht dem vom Programmierer gewünschten entspricht, muss der Wert 1 als long dargestellt werden : 1L << j oder (long) 1 << j .

Nr 2: Initialisierungsreihenfolge


Quelle: Huawei Cloud: Heute ist es im PVS-Studio bewölkt

V6050 Klasseninitialisierungszyklus ist vorhanden. Die Initialisierung von 'INSTANCE' erscheint vor der Initialisierung von 'LOG'. UntrustedSSL.java (32), UntrustedSSL.java (59), UntrustedSSL.java (33)

 public class UntrustedSSL { private static final UntrustedSSL INSTANCE = new UntrustedSSL(); private static final Logger LOG = LoggerFactory.getLogger(UntrustedSSL.class); .... private UntrustedSSL() { try { .... } catch (Throwable t) { LOG.error(t.getMessage(), t); // <= } } } 

Die Reihenfolge, in der Felder in einer Klasse deklariert werden, macht einen Unterschied, da sie in derselben Reihenfolge initialisiert werden, in der sie deklariert wurden. Das Vergessen dieser Tatsache führt zu schwer fassbaren Fehlern wie den oben genannten.

Der Analysator weist darauf hin, dass das statische Feld LOG zum Zeitpunkt der Initialisierung auf den Wert null dereferenziert wird, was dazu führt, dass eine Reihe von Ausnahmen ausgelöst wird : NullPointerException -> ExceptionInInitializerError .

Aber warum hat das statische Feld LOG beim Aufruf des Konstruktors den Wert null ?

Der ExceptionInInitializerError ist der Hinweis. Der Konstruktor initialisiert das statische Feld INSTANCE , das vor dem Feld LOG deklariert ist. Aus diesem Grund wird das LOG- Feld beim Aufruf des Konstruktors noch nicht initialisiert. Damit dieser Code ordnungsgemäß funktioniert, muss das LOG- Feld vor dem Aufruf initialisiert werden.

Erstens: Kopieren-Einfügen-orientierte Programmierung


Quelle: Apache Hadoop Code Qualität: Production VS Test

V6072 Es wurden zwei ähnliche Codefragmente gefunden. Möglicherweise ist dies ein Tippfehler, und die Variable 'localFiles' sollte anstelle von 'localArchives' verwendet werden. LocalDistributedCacheManager.java (183), LocalDistributedCacheManager.java (178), LocalDistributedCacheManager.java (176), LocalDistributedCacheManager.java (181)

 public synchronized void setup(JobConf conf, JobID jobId) throws IOException { .... // Update the configuration object with localized data. if (!localArchives.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALARCHIVES, StringUtils .arrayToString(localArchives.toArray(new String[localArchives // <= .size()]))); } if (!localFiles.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALFILES, StringUtils .arrayToString(localFiles.toArray(new String[localArchives // <= .size()]))); } .... } 

Der erste Platz in unserer Top-10-Liste geht an das Kopieren und Einfügen bzw. an einen Fehler, der auf die nachlässige Verwendung dieser Technik zurückzuführen ist. Die zweite if- Anweisung ähnelt einer Kopie der ersten, wobei einige der Variablen geändert wurden:

  • localArchives wurde in localFiles geändert.
  • MRJobConfig.CACHE_LOCALARCHIVES wurde in MRJobConfig.CACHE_LOCALFILES geändert.

Aber der Programmierer hat es geschafft, auch in dieser einfachen Operation einen Fehler zu machen: Die if- Anweisung in der Zeile, auf die der Analysator hinweist , verwendet weiterhin die Variable localArchives anstelle der scheinbar beabsichtigten Variablen localFiles .

Fazit


Die Behebung von Fehlern, die in späteren Entwicklungsphasen oder nach der Veröffentlichung auftreten, erfordert eine Menge Ressourcen. Mit dem statischen Analysator PVS-Studio können Sie Fehler in der Codierungsphase erkennen, was die Arbeit erheblich vereinfacht und verbilligt. Viele Unternehmen haben bereits das Leben ihrer Entwickler erleichtert, indem sie den Analysator regelmäßig einsetzen. Wenn Sie Ihren Job wirklich genießen möchten, probieren Sie PVS-Studio aus .

Wir werden nicht damit aufhören und planen, unser Tool weiter zu verbessern und zu verbessern. Seien Sie gespannt auf neue Diagnosen und Artikel mit noch interessanteren Fehlern im nächsten Jahr.

Ich sehe dich wie Abenteuer! Zuerst haben Sie 2019 die Top 10 Bugs in C # besiegt, und jetzt haben Sie auch Java gemeistert! Willkommen auf der nächsten Ebene - dem Artikel über die besten Fehler in C ++ - Projekten im Jahr 2019 .

Source: https://habr.com/ru/post/de481184/


All Articles