Dans Java, nous voyons de nombreux endroits où le mot clé final
peut être utilisé, mais son utilisation est rare.
Par exemple:
String str = "abc";
System.out.println(str);
Dans le cas ci-dessus, str
peut être final
mais ceci est généralement laissé de côté.
Lorsqu'une méthode ne sera jamais remplacée, nous pouvons utiliser le mot-clé final. De même dans le cas d'une classe qui ne sera pas héritée.
L'utilisation du mot-clé final dans l'un ou l'autre de ces cas améliore-t-elle vraiment les performances? Si oui, alors comment? S'il vous plaît, expliquez. Si l'utilisation correcte de final
importe vraiment pour les performances, quelles habitudes un programmeur Java doit-il développer pour utiliser au mieux le mot clé?
Généralement pas. Pour les méthodes virtuelles, HotSpot vérifie si la méthode a été réellement remplacée et peut effectuer des optimisations telles que l’alignement sur hypothèse si une méthode n’a pas été surchargée. - jusqu'à ce qu'il charge une classe qui remplace la méthode, auquel cas il peut annuler (ou annuler partiellement) ces optimisations.
(Bien sûr, cela suppose que vous utilisiez HotSpot - mais c'est de loin la JVM la plus courante, alors ...)
À mon avis, vous devriez utiliser final
sur la base d'une conception et d'une lisibilité claires plutôt que pour des raisons de performances. Si vous souhaitez modifier quoi que ce soit pour des raisons de performances, vous devez effectuer les mesures appropriées avant de déformer le code le plus clair - de cette façon, vous pourrez décider si les performances supplémentaires obtenues valent moins la lisibilité/conception. (D'après mon expérience, cela ne vaut presque jamais la peine; YMMV.)
EDIT: Comme les derniers champs ont été mentionnés, il est intéressant de noter qu’ils sont souvent une bonne idée de toute façon, en termes de conception claire. Ils modifient également le comportement garanti en termes de visibilité multithread: une fois qu'un constructeur est terminé, tous les champs finaux sont garantis visibles immédiatement dans les autres threads. C’est probablement l’utilisation la plus courante de final
dans mon expérience, bien qu’en tant que partisan de la "conception pour l’héritage ou l’interdire" de Josh Bloch, je devrais probablement utiliser final
plus souvent pour les cours. ..
Réponse courte: ne vous inquiétez pas!
Longue réponse:
Lorsque vous parlez de variables locales finales, gardez à l'esprit que l'utilisation du mot clé final
aidera le compilateur à optimiser le code de manière statique, ce qui peut aboutir à un code plus rapide . Par exemple, les chaînes finales a + b
dans l'exemple ci-dessous sont concaténées statiquement (au moment de la compilation).
public class FinalTest {
public static final int N_ITERATIONS = 1000000;
public static String testFinal() {
final String a = "a";
final String b = "b";
return a + b;
}
public static String testNonFinal() {
String a = "a";
String b = "b";
return a + b;
}
public static void main(String[] args) {
long tStart, tElapsed;
tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
testFinal();
tElapsed = System.currentTimeMillis() - tStart;
System.out.println("Method with finals took " + tElapsed + " ms");
tStart = System.currentTimeMillis();
for (int i = 0; i < N_ITERATIONS; i++)
testNonFinal();
tElapsed = System.currentTimeMillis() - tStart;
System.out.println("Method without finals took " + tElapsed + " ms");
}
}
Le résultat?
Method with finals took 5 ms
Method without finals took 273 ms
Testé sur Java Hotspot VM 1.7.0_45-b18.
Alors, quelle est l’amélioration de la performance réelle? Je n'ose pas dire. Dans la plupart des cas, probablement marginal (environ 270 nanosecondes dans ce test de synthèse, car la concaténation de chaînes est totalement évitée - un cas rare), mais dans un code utilitaire hautement optimisé, le facteur peut-être est un facteur. Dans tous les cas, la réponse à la question initiale est oui, cela pourrait améliorer les performances, mais de manière marginale, au mieux.
Mis à part les avantages liés à la compilation, je n'ai trouvé aucune preuve que l'utilisation du mot clé final
ait un effet mesurable sur les performances.
OUI, c'est possible. Voici un exemple où final peut améliorer les performances:
Compilation conditionnelle est une technique dans laquelle les lignes de code ne sont pas compilées dans le fichier de classe en fonction d'une condition particulière. Cela peut être utilisé pour supprimer des tonnes de code de débogage dans une version de production.
considérer ce qui suit:
public class ConditionalCompile {
private final static boolean doSomething= false;
if (doSomething) {
// do first part.
}
if (doSomething) {
// do second part.
}
if (doSomething) {
// do third part.
}
if (doSomething) {
// do finalization part.
}
}
En convertissant l'attribut doSomething en un attribut final, vous avez indiqué au compilateur qu'il devrait le remplacer chaque fois qu'il le ferait par false, conformément aux règles de substitution de la compilation. La première passe du compilateur change le code en quelque chose comme ceci:
public class ConditionalCompile {
private final static boolean doSomething= false;
if (false){
// do first part.
}
if (false){
// do second part.
}
if (false){
// do third part.
}
if (false){
// do finalization part.
}
}
Une fois que cela est fait, le compilateur l'examine à nouveau et voit qu'il y a des instructions inaccessibles dans le code. Puisque vous travaillez avec un compilateur de haute qualité, il n’aime pas tous ces codes d’octets inaccessibles. Donc, cela les supprime, et vous vous retrouvez avec ceci:
public class ConditionalCompile {
private final static boolean doSomething= false;
public static void someMethodBetter( ) {
// do first part.
// do second part.
// do third part.
// do finalization part.
}
}
réduisant ainsi tout code excessif ou toute vérification conditionnelle inutile.
Edit: prenons comme exemple le code suivant:
public class Test {
public static final void main(String[] args) {
boolean x = false;
if (x) {
System.out.println("x");
}
final boolean y = false;
if (y) {
System.out.println("y");
}
if (false) {
System.out.println("z");
}
}
}
Lors de la compilation de ce code avec Java 8 et de sa décompilation avec javap -c Test.class
, nous obtenons:
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #8 // Method Java/lang/Object."<init>":()V
4: return
public static final void main(Java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: ifeq 14
6: getstatic #16 // Field Java/lang/System.out:Ljava/io/PrintStream;
9: ldc #22 // String x
11: invokevirtual #24 // Method Java/io/PrintStream.println:(Ljava/lang/String;)V
14: iconst_0
15: istore_2
16: return
}
Nous pouvons noter que le code compilé inclut uniquement la variable non finale x
. Cela prouve que les variables finales ont un impact sur les performances, du moins dans ce cas simple.
Selon IBM, ce n'est pas le cas pour les classes ou les méthodes.
http://www.ibm.com/developerworks/Java/library/j-jtp04223.html
Vous vous interrogez vraiment sur deux cas (au moins) différents:
final
pour les variables localesfinal
pour les méthodes/classesJon Skeet a déjà répondu 2). Environ 1):
Je ne pense pas que cela fasse une différence. pour les variables locales, le compilateur peut déduire si la variable est finale ou non (simplement en vérifiant si elle est affectée plus d'une fois). Donc, si le compilateur voulait optimiser des variables qui ne sont assignées qu'une seule fois, il peut le faire, que la variable soit réellement déclarée final
ou non.
final
pourrait faire une différence pour les champs protégés/de classe publique; là, il est très difficile pour le compilateur de savoir si le champ est défini plus d'une fois, car cela pourrait provenir d'une autre classe (qui n'a même pas été chargée). Mais même dans ce cas, la machine virtuelle Java pourrait utiliser la technique décrite par Jon (optimisation optimiste, rétablissement si une classe est chargée, ce qui modifie le champ).
En résumé, je ne vois aucune raison pour que cela améliore la performance. Il est donc peu probable que ce type de micro-optimisation soit efficace. Vous pouvez essayer de le comparer pour vous en assurer, mais je doute que cela fasse une différence.
Edit:
En fait, selon la réponse de Timo Westkämper, final
peut améliorer les performances pour les champs de classe dans certains cas. Je me suis trompé.
Je suis stupéfait que personne n’a publié de code réel décompilé pour prouver qu’il existe au moins une petite différence.
Pour référence, cela a été testé sur javac
version 8
, 9
et 10
.
Supposons que cette méthode:
public static int test() {
/* final */ Object left = new Object();
Object right = new Object();
return left.hashCode() + right.hashCode();
}
La compilation de ce code en l'état produit le exact même code octet que lorsque final
aurait été présent (final Object left = new Object();
).
Mais celui-ci:
public static int test() {
/* final */ int left = 11;
int right = 12;
return left + right;
}
Produit:
0: bipush 11
2: istore_0
3: bipush 12
5: istore_1
6: iload_0
7: iload_1
8: iadd
9: ireturn
Quitter final
être présent produit:
0: bipush 12
2: istore_1
3: bipush 11
5: iload_1
6: iadd
7: ireturn
Le code est assez explicite, dans le cas où il existe une constante de temps de compilation, il sera chargé directement dans la pile d'opérandes (il ne sera pas stocké dans le tableau de variables locales comme le fait l'exemple précédent via bipush 12; istore_0; iload_0
) - ce qui a du sens puisque personne ne peut le changer.
D'autre part, pourquoi dans le second cas, le compilateur ne produit pas istore_0 ... iload_0
est au-delà de moi, ce n'est pas comme si l'emplacement 0
était utilisé de quelque manière que ce soit (il pourrait réduire le tableau de variables de cette façon, be Im manque quelques détails internes, ne peut pas dire à coup sûr)
J'ai été surpris de voir une telle optimisation, compte tenu de la petite javac
. Quant à devrions-nous toujours utiliser final
? Je ne vais même pas écrire un test JMH
(ce que je voulais faire dans un premier temps), je suis sûr que le diff est de l'ordre de ns
(si possible pour être capturé du tout). Le seul endroit où cela pourrait poser problème est lorsqu'une méthode ne peut pas être en ligne à cause de sa taille (et déclarer final
réduirait cette taille de quelques octets).
Il y a deux autres final
s qui doivent être résolus. La première est quand une méthode est final
(d'une perspective JIT
), une telle méthode est monomorphe - et ce sont - les plus aimés ceux du JVM
.
Ensuite, il y a final
variables d'instance (qui doivent être définies dans chaque constructeur); Celles-ci sont importantes car elles garantiront une référence correctement publiée, comme un peu touché ici et également spécifié exactement par le JLS
.
Remarque: ce n'est pas un expert Java
Si je me souviens bien de mon Java, il n'y aurait guère de moyen d'améliorer les performances à l'aide du mot-clé final. J'ai toujours su qu'il existait un "bon code" - conception et lisibilité.
Je ne suis pas un expert, mais je suppose que vous devriez ajouter le mot clé final
à la classe ou à la méthode, si celle-ci ne sera pas écrasée et si vous laissez les variables seules. S'il y a moyen d'optimiser de telles choses, le compilateur le fera pour vous.
En fait, tout en testant du code lié à OpenGL, j'ai constaté que l'utilisation du dernier modificateur sur un champ privé peut dégrader la performance. Voici le début du cours que j'ai testé:
public class ShaderInput {
private /* final */ float[] input;
private /* final */ int[] strides;
public ShaderInput()
{
this.input = new float[10];
this.strides = new int[] { 0, 4, 8 };
}
public ShaderInput x(int stride, float val)
{
input[strides[stride] + 0] = val;
return this;
}
// more stuff ...
Et voici la méthode que j'ai utilisée pour tester les performances de différentes alternatives, parmi lesquelles la classe ShaderInput:
public static void test4()
{
int arraySize = 10;
float[] fb = new float[arraySize];
for (int i = 0; i < arraySize; i++) {
fb[i] = random.nextFloat();
}
int times = 1000000000;
for (int i = 0; i < 10; ++i) {
floatVectorTest(times, fb);
arrayCopyTest(times, fb);
shaderInputTest(times, fb);
directFloatArrayTest(times, fb);
System.out.println();
System.gc();
}
}
Après la 3ème itération, avec le VM réchauffé, j'ai toujours obtenu ces chiffres sans le mot clé final:
Simple array copy took : 02.64
System.arrayCopy took : 03.20
ShaderInput took : 00.77
Unsafe float array took : 05.47
Avec le mot-clé final:
Simple array copy took : 02.66
System.arrayCopy took : 03.20
ShaderInput took : 02.59
Unsafe float array took : 06.24
Notez les chiffres pour le test ShaderInput.
Peu importait que je rende les champs publics ou privés.
Incidemment, il y a quelques choses plus déconcertantes. La classe ShaderInput surpasse toutes les autres variantes, même avec le mot-clé final. C’est remarquable car c’est une classe qui enveloppe un tableau float, alors que les autres tests directement manipulent le tableau. Je dois comprendre celui-ci. Peut avoir quelque chose à voir avec l'interface fluide de ShaderInput.
De plus, System.arrayCopy est apparemment apparemment un peu plus lent pour les petits tableaux que de simplement copier des éléments d’un tableau à l’autre dans une boucle for. Et l’utilisation de Sun.misc.Unsafe (ainsi que l’utilisation directe de Java.nio.FloatBuffer, non illustrée ici) est catastrophique.
Les membres déclarés avec final seront disponibles tout au long du programme car, contrairement aux membres non finaux, s'ils ne sont pas utilisés dans le programme, ils ne seront toujours pas pris en charge par Garbage Collector, ce qui peut entraîner des problèmes de performances en raison d'une mauvaise gestion de la mémoire.