Java Language
Pièges Java communs
Recherche…
Introduction
Cette rubrique présente certaines des erreurs courantes commises par les débutants en Java.
Cela inclut toutes les erreurs courantes dans l'utilisation du langage Java ou la compréhension de l'environnement d'exécution.
Les erreurs associées à des API spécifiques peuvent être décrites dans des rubriques spécifiques à ces API. Les cordes sont un cas particulier; ils sont couverts dans la spécification de langage Java. Les détails autres que les erreurs courantes peuvent être décrits dans cette rubrique sur les chaînes .
Piège: utiliser == pour comparer des objets d’emballage primitifs tels que Entier
(Ce piège s'applique également à tous les types d’emballages primitifs, mais nous allons l’illustrer pour Integer
et int
.)
Lorsque vous travaillez avec des objets Integer
, il est tentant d'utiliser ==
pour comparer les valeurs, car c'est ce que vous feriez avec les valeurs int
. Et dans certains cas, cela semble fonctionner:
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
Ici, nous avons créé deux objets Integer
avec la valeur 1
et nous les avons Integer
(dans ce cas, nous en avons créé un à partir d'un String
et un à partir d'un littéral int
. Il existe d'autres alternatives). En outre, nous observons que les deux méthodes de comparaison ( ==
et equals
) produisent toutes deux une valeur true
.
Ce comportement change lorsque nous choisissons des valeurs différentes:
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
Dans ce cas, seule la comparaison equals
donne le résultat correct.
La raison de cette différence de comportement est que la machine virtuelle Java conserve un cache d’objets Integer
compris entre -128 et 127. (La valeur supérieure peut être remplacée par la propriété système "java.lang.Integer.IntegerCache.high" ou la propriété Argument JVM "-XX: AutoBoxCacheMax = size"). Pour les valeurs de cette plage, Integer.valueOf()
retournera la valeur mise en cache plutôt que d'en créer une nouvelle.
Ainsi, dans le premier exemple, les Integer.valueOf(1)
et Integer.valueOf("1")
renvoyé la même instance Integer
cache. En revanche, dans le deuxième exemple, Integer.valueOf(1000)
et Integer.valueOf("1000")
tous deux créé et renvoyé de nouveaux objets Integer
.
L'opérateur ==
pour les types de référence teste l'égalité de référence (c'est-à-dire le même objet). Par conséquent, dans le premier exemple, int1_1 == int1_2
est true
car les références sont les mêmes. Dans le deuxième exemple, int2_1 == int2_2
est faux car les références sont différentes.
Piège: oublier les ressources gratuites
Chaque fois qu'un programme ouvre une ressource, telle qu'une connexion de fichier ou de réseau, il est important de libérer la ressource une fois que vous l'utilisez. Des précautions similaires devraient être prises si une exception devait être levée pendant des opérations sur de telles ressources. On pourrait soutenir que FileInputStream
a un finaliseur qui appelle la méthode close()
sur un événement de récupération de place; Cependant, comme nous ne pouvons pas être sûrs du démarrage d'un cycle de récupération de place, le flux d'entrée peut consommer des ressources informatiques pour une durée indéterminée. La ressource doit être fermée dans une finally
partie d'un bloc try-catch:
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();
}
}
}
Depuis Java 7, Java 7 contient une instruction particulièrement utile, particulièrement pour ce cas, appelée try-with-resources:
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();
}
}
}
L'instruction try-with-resources peut être utilisée avec n'importe quel objet qui implémente l'interface Closeable
ou AutoCloseable
. Il s'assure que chaque ressource est fermée à la fin de l'instruction. La différence entre les deux interfaces est que la méthode close()
de Closeable
lance une Closeable
IOException
qui doit être gérée d’une certaine manière.
Dans les cas où la ressource a déjà été ouverte mais doit être fermée en toute sécurité après l’utilisation, on peut l’attribuer à une variable locale à l’intérieur de try-with-resources.
private static void printFileJava7(InputStream extResource) throws IOException {
try (InputStream input = extResource) {
... //access resource
}
}
La variable de ressource locale créée dans le constructeur try-with-resources est effectivement finale.
Piège: fuites de mémoire
Java gère la mémoire automatiquement. Vous n'êtes pas obligé de libérer de la mémoire manuellement. La mémoire d'un objet sur le tas peut être libérée par un garbage collector lorsque l'objet n'est plus accessible par un thread en direct.
Cependant, vous pouvez empêcher la libération de la mémoire, en permettant aux objets d'être accessibles qui ne sont plus nécessaires. Que vous appeliez cela une fuite de mémoire ou un empaquetage de mémoire, le résultat est le même: une augmentation inutile de la mémoire allouée.
Les fuites de mémoire dans Java peuvent se produire de différentes manières, mais la raison la plus courante est la référence permanente aux objets, car le ramasse-miettes ne peut pas supprimer les objets du tas alors qu'il y a encore des références.
Champs statiques
On peut créer une telle référence en définissant la classe avec un champ static
contenant une collection d'objets et en oubliant de définir ce champ static
sur null
après que la collection ne soit plus nécessaire. static
champs static
sont considérés comme des racines GC et ne sont jamais collectés. Un autre problème concerne les fuites dans la mémoire non-tas lorsque JNI est utilisé.
Fuite de classloader
De loin, le type de fuite de mémoire le plus insidieux est la fuite du chargeur de classe . Un classloader contient une référence à chaque classe qu'il a chargée, et chaque classe contient une référence à son classloader. Chaque objet contient également une référence à sa classe. Par conséquent, même si un seul objet d'une classe chargée par un chargeur de classe n'est pas un déchet, aucune classe chargée par ce chargeur de classes ne peut être collectée. Comme chaque classe fait également référence à ses champs statiques, ils ne peuvent pas non plus être collectés.
Fuite d' accumulation L'exemple de fuite d'accumulation pourrait ressembler à ceci:
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();
}
Cet exemple crée deux tâches planifiées. La première tâche prend le dernier numéro d'un numbers
appelé deque et, si le nombre est divisible par 51, elle imprime le nombre et la taille du deque. La deuxième tâche met des chiffres dans le deque. Les deux tâches sont planifiées à un taux fixe et elles s'exécutent toutes les 10 ms.
Si le code est exécuté, vous verrez que la taille de la police augmente en permanence. Cela entraînera éventuellement le remplissage de deque avec des objets consommant toute la mémoire de tas disponible.
Pour éviter cela tout en préservant la sémantique de ce programme, nous pouvons utiliser une méthode différente pour prendre des nombres à partir de deque: pollLast
. Contrairement à la méthode peekLast
, pollLast
renvoie l'élément et le supprime de deque tandis que peekLast
ne renvoie que le dernier élément.
Piège: utiliser == pour comparer des chaînes
Une erreur courante pour les débutants Java est d'utiliser l'opérateur ==
pour tester si deux chaînes sont égales. Par exemple:
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?");
}
}
}
}
Le programme ci-dessus est censé tester le premier argument de la ligne de commande et imprimer différents messages lorsqu'il ne s'agit pas du mot "bonjour". Mais le problème est que cela ne fonctionnera pas. Ce programme produira "Êtes-vous grincheux aujourd'hui?" quel que soit le premier argument de la ligne de commande.
Dans ce cas particulier, la String
"hello" est placée dans le pool de chaînes pendant que les arguments String
[0] résident sur le tas. Cela signifie qu'il y a deux objets représentant le même littéral, chacun avec sa référence. Étant donné que ==
teste les références et non l’égalité réelle, la comparaison produira un faux la plupart du temps. Cela ne signifie pas qu'il le fera toujours.
Lorsque vous utilisez ==
pour tester des chaînes, ce que vous testez en réalité est si deux objets String
sont le même objet Java. Malheureusement, cela ne signifie pas l’égalité des chaînes en Java. En fait, la méthode correcte pour tester les chaînes consiste à utiliser la méthode equals(Object)
. Pour une paire de chaînes, nous voulons généralement tester si elles sont composées des mêmes caractères dans le même ordre.
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?");
}
}
}
}
Mais en réalité, ça empire. Le problème est que ==
donnera la réponse attendue dans certaines circonstances. Par exemple
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");
}
}
}
Il est intéressant de noter que cela affichera "identique", même si nous testons les chaînes dans le mauvais sens. Pourquoi donc? Parce que la spécification de langage Java (Section 3.10.5: Littéraux de chaîne) stipule que deux chaînes >> littérales << composées des mêmes caractères seront effectivement représentées par le même objet Java. Par conséquent, le test ==
sera vrai pour les littéraux égaux. (Les littéraux de chaîne sont "internés" et ajoutés à un "pool de chaînes" partagé lorsque votre code est chargé, mais il s'agit en fait d'un détail d'implémentation.)
Pour ajouter à la confusion, la spécification de langage Java stipule également que lorsque vous avez une expression constante de compilation qui concatène deux littéraux de chaîne, cela équivaut à un seul littéral. Ainsi:
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");
}
}
}
Cela produira "1. mêmes" et "2. différents". Dans le premier cas, l'expression +
est évaluée au moment de la compilation et nous comparons un objet String
avec lui-même. Dans le second cas, il est évalué à l'exécution et nous comparons deux objets String
différents
En résumé, l'utilisation de ==
pour tester des chaînes en Java est presque toujours incorrecte, mais il n'est pas certain que la réponse soit incorrecte.
Piège: tester un fichier avant d'essayer de l'ouvrir.
Certaines personnes recommandent d’appliquer divers tests à un fichier avant de tenter de l’ouvrir pour fournir de meilleurs diagnostics ou pour éviter de traiter des exceptions. Par exemple, cette méthode tente de vérifier si le path
correspond à un fichier lisible:
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;
}
Vous pourriez utiliser la méthode ci-dessus comme ceci:
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.
}
Le premier problème réside dans la signature de FileInputStream(File)
car le compilateur insistera toujours pour intercepter IOException
ici ou plus haut dans la pile.
Le second problème est que les vérifications effectuées par getValidatedFile
ne garantissent pas la FileInputStream
.
Conditions de course: un autre thread ou un processus séparé peut renommer le fichier, supprimer le fichier ou supprimer l'accès en lecture après le retour de
getValidatedFile
. Cela conduirait à uneIOException
"simple" sans le message personnalisé.Il existe des cas marginaux non couverts par ces tests. Par exemple, sur un système avec SELinux en mode "Forçage", une tentative de lecture d'un fichier peut échouer malgré le retour de
true
canRead()
.
Le troisième problème est que les tests sont inefficaces. Par exemple, le exists
, isFile
et canRead
appels feront chacun un syscall pour effectuer le contrôle nécessaire. Un autre appel système est alors effectué pour ouvrir le fichier, qui répète les mêmes vérifications dans les coulisses.
En bref, les méthodes telles que getValidatedFile
sont erronées. Il est préférable d'essayer d'ouvrir le fichier et de gérer l'exception:
try (InputStream is = new FileInputStream("somefile")) {
// Read data etc.
} catch (IOException ex) {
System.err.println("IO Error processing 'somefile': " + ex.getMessage());
return;
}
Si vous voulez distinguer les erreurs IO générées lors de l'ouverture et de la lecture, vous pouvez utiliser un try / catch imbriqué. Si vous voulez produire de meilleurs diagnostics pour les échecs ouverts, vous pouvez effectuer les exists
, isFile
et canRead
contrôles dans le gestionnaire.
Piège: penser les variables comme des objets
Aucune variable Java ne représente un objet.
String foo; // NOT AN OBJECT
Aucun tableau Java ne contient non plus d'objets.
String bar[] = new String[100]; // No member is an object.
Si vous pensez à tort que les variables sont des objets, le comportement réel du langage Java vous surprendra.
Pour les variables Java qui ont un type primitif (tel que
int
oufloat
), la variable contient une copie de la valeur. Toutes les copies d'une valeur primitive sont indiscernables. c'est-à-dire qu'il n'y a qu'une seule valeurint
pour le numéro un. Les valeurs primitives ne sont pas des objets et ne se comportent pas comme des objets.Pour les variables Java qui ont un type de référence (soit un type de classe, soit un type de tableau), la variable contient une référence. Toutes les copies d'une référence sont indiscernables. Les références peuvent pointer sur des objets ou être
null
ce qui signifie qu'elles ne pointent vers aucun objet. Cependant, ils ne sont pas des objets et ils ne se comportent pas comme des objets.
Les variables ne sont pas des objets dans les deux cas et elles ne contiennent aucun objet dans les deux cas. Ils peuvent contenir des références à des objets , mais cela dit quelque chose de différent.
Exemple classe
Les exemples suivants utilisent cette classe, qui représente un point dans un espace 2D.
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;
}
}
Une instance de cette classe est un objet qui a deux champs x
et y
qui ont le type int
.
Nous pouvons avoir plusieurs instances de la classe MutableLocation
. Certains représenteront les mêmes emplacements dans un espace 2D; c'est-à-dire que les valeurs respectives de x
et y
correspondent. D'autres représenteront des endroits différents.
Plusieurs variables peuvent pointer vers le même objet
MutableLocation here = new MutableLocation(1, 2);
MutableLocation there = here;
MutableLocation elsewhere = new MutableLocation(1, 2);
Dans ce qui précède, nous avons déclaré trois variables here
, there
et elsewhere
pouvant contenir des références aux objets MutableLocation
.
Si vous pensez (à tort) que ces variables sont des objets, vous risquez de ne pas les interpréter comme suit:
- Copiez l'emplacement "[1, 2]"
here
- Copiez l'emplacement « [1, 2] » pour
there
- Copier l'emplacement "[1, 2]" vers
elsewhere
À partir de cela, vous pouvez en déduire que nous avons trois objets indépendants dans les trois variables. En fait, il n'y a que deux objets créés par ce qui précède. Les variables here
et there
se réfèrent effectivement au même objet.
Nous pouvons le démontrer. En supposant les déclarations de variable comme ci-dessus:
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);
Cela va afficher les éléments suivants:
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
Nous avons assigné une nouvelle valeur à here.x
et cela a changé la valeur que nous voyons there.x
Ils font référence au même objet. Mais la valeur que nous voyons via elsewhere.x
n'a pas changé, donc elsewhere
doit faire référence à un objet différent.
Si une variable était un objet, l'affectation here.x = 42
ne changerait pas there.x
. there.x
L'opérateur d'égalité ne teste PAS que deux objets sont égaux
L'application de l'opérateur d'égalité ( ==
) pour référencer les valeurs teste si les valeurs font référence au même objet. Il ne teste pas si deux objets (différents) sont "égaux" au sens intuitif.
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");
}
Ceci imprimera "ici est là", mais il n'imprimera pas "ici est ailleurs". (Les références here
et elsewhere
concernent deux objets distincts.)
En revanche, si nous appelons la méthode equals(Object)
que nous avons implémentée ci-dessus, nous allons tester si deux instances de MutableLocation
ont un emplacement égal.
if (here.equals(there)) {
System.out.println("here equals there");
}
if (here.equals(elsewhere)) {
System.out.println("here equals elsewhere");
}
Cela imprimera les deux messages. En particulier, here.equals(elsewhere)
renvoie true
car les critères sémantiques que nous avons choisis pour l'égalité de deux objets MutableLocation
ont été satisfaits.
Les appels de méthode ne transmettent PAS d'objets du tout
Les appels de méthode Java utilisent la valeur par défaut 1 pour transmettre les arguments et renvoyer un résultat.
Lorsque vous transmettez une valeur de référence à une méthode, vous transmettez en réalité une référence à un objet par valeur , ce qui signifie qu'il crée une copie de la référence d'objet.
Tant que les deux références d'objet pointent toujours vers le même objet, vous pouvez modifier cet objet à partir de l'une ou l'autre référence, ce qui est source de confusion pour certaines.
Cependant, vous ne passez pas d'objet par référence 2 . La distinction est que si la copie de référence d'objet est modifiée pour pointer vers un autre objet, la référence d'objet d'origine pointe toujours vers l'objet d'origine.
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".
}
Vous ne transmettez pas non plus une copie de l'objet.
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 - Dans les langages comme Python et Ruby, le terme "pass by sharing" est préféré pour "passer par valeur" d'un objet / référence.
2 - Le terme "passer par référence" ou "appeler par référence" a une signification très spécifique dans la terminologie des langages de programmation. En effet, cela signifie que vous passez l'adresse d'une variable ou d'un élément de tableau , de sorte que lorsque la méthode appelée assigne une nouvelle valeur à l'argument formel, elle modifie la valeur de la variable d'origine. Java ne supporte pas cela. Pour une description plus complète des différents mécanismes de transmission des paramètres, reportez-vous à https://en.wikipedia.org/wiki/Evaluation_strategy .
Piège: combiner affectation et effets secondaires
Occasionnellement, nous voyons des questions sur StackOverflow Java (et des questions sur C ou C ++) qui demandent ce qui suit:
i += a[i++] + b[i--];
évalue à ... pour certains états initiaux connus de i
, a
et b
.
En général:
- pour Java la réponse est toujours spécifiée 1 , mais non évidente et souvent difficile à comprendre
- pour C et C ++, la réponse est souvent non spécifiée.
Ces exemples sont souvent utilisés dans les examens ou les entretiens d'embauche pour tenter de voir si l'étudiant ou la personne interrogée comprend comment l'évaluation de l'expression fonctionne réellement dans le langage de programmation Java. Cela est sans doute légitime en tant que "test de connaissance", mais cela ne signifie pas que vous devriez le faire dans un vrai programme.
Pour illustrer cela, l'exemple apparemment simple suivant est apparu à quelques reprises dans les questions de StackOverflow (comme celle-ci ). Dans certains cas, il apparaît comme une véritable erreur dans le code de quelqu'un.
int a = 1;
a = a++;
System.out.println(a); // What does this print.
La plupart des programmeurs (y compris les experts Java) lisant ces déclarations rapidement diraient qu’ils produisent 2
. En fait, il produit 1
. Pour une explication détaillée des raisons, veuillez lire cette réponse .
Cependant , la véritable plats à emporter à partir de ce et des exemples similaires est que toute déclaration Java qui affecte à la fois et les effets secondaires de la même variable va être au mieux difficile à comprendre, et au pire carrément trompeur. Vous devriez éviter d'écrire du code comme celui-ci.
1 - problèmes potentiels modulo avec le modèle de mémoire Java si les variables ou objets sont visibles par les autres threads.
Piège: ne pas comprendre que String est une classe immuable
Les nouveaux programmeurs Java oublient souvent, ou échouent à comprendre, que la classe Java String
est immuable. Cela conduit à des problèmes comme celui de l'exemple suivant:
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();
}
}
Le code ci-dessus est censé imprimer les arguments de ligne de commande en majuscule. Malheureusement, cela ne fonctionne pas, la casse des arguments n'est pas modifiée. Le problème est cette déclaration:
s.toUpperCase();
Vous pourriez penser que l’appel à toUpperCase()
va changer s
en une chaîne majuscule. Ce n'est pas le cas. Ça ne peut pas! String
objets String
sont immuables. Ils ne peuvent pas être changés.
En réalité, la méthode toUpperCase()
renvoie un objet String
qui est une version majuscule de la String
laquelle vous l'appelez. Ce sera probablement un nouvel objet String
, mais si s
était déjà en majuscule, le résultat pourrait être la chaîne existante.
Donc, pour utiliser cette méthode efficacement, vous devez utiliser l'objet renvoyé par l'appel de la méthode. par exemple:
s = s.toUpperCase();
En fait, la règle "strings never change" s'applique à toutes les méthodes String
. Si vous vous en souvenez, vous pouvez éviter toute une catégorie d'erreurs du débutant.