8. Réutilisation (3)

Résumé du chapitre

  • mot-clé final
    • données finales
    • finale vierge
    • paramètres finaux
    • méthode finale
    • final et privé
    • cours final
    • dernier conseil
  • Initialisation et chargement de la classe
    • Héritage et initialisation

mot-clé final

Le mot-clé Java final a des significations légèrement différentes selon le contexte, mais il signifie généralement « cela ne peut pas être modifié ». Il y a deux raisons d’empêcher le changement : la conception ou l’efficacité. Ces deux raisons étant très différentes, il est possible d’utiliser à mauvais escient le mot-clé final .

Les sections suivantes abordent trois endroits où final peut être utilisé : les données, les méthodes et les classes.

données finales

De nombreux langages de programmation disposent d'un moyen d'indiquer au compilateur qu'une donnée est constante. Les constantes sont utiles comme :

  1. Une constante de compilation qui ne change jamais.
  2. Une valeur qui ne change pas lors de l'initialisation au moment de l'exécution.

Dans le cas de constantes au moment de la compilation, le compilateur peut intégrer les constantes dans le calcul ; c'est-à-dire qu'elles peuvent être calculées au moment de la compilation, réduisant ainsi la charge d'exécution. En Java, ces constantes doivent être de types basiques et modifiées avec le mot-clé final . Vous devez attribuer la valeur lors de la définition de la constante.

Une propriété modifiée à la fois par static et final n'occupera qu'un espace de stockage non modifiable.

Lorsque final est utilisé pour modifier une référence d'objet plutôt qu'un type primitif, les implications peuvent être un peu déroutantes. Pour les types primitifs, final rend la valeur immuable, tandis que pour les références d'objet, final rend la référence immuable. Une fois qu'une référence est initialisée pour pointer vers un objet, elle ne peut pas être modifiée pour pointer vers un autre objet. Cependant, l'objet lui-même peut être modifié et Java ne permet pas de faire d'un objet une constante. (Vous pouvez écrire votre propre classe pour rendre l'objet immuable.) Cette restriction s'applique également aux tableaux, qui sont également des objets.

L'exemple suivant montre l'utilisation des attributs finaux :

// reuse/FinalData.java
// The effect of final on fields
import java.util.*;

class Value {
    
    
    int i; // package access
    
    Value(int i) {
    
    
        this.i = i;
    }
}

public class FinalData {
    
    
    private static Random rand = new Random(47);
    private String id;
    
    public FinalData(String id) {
    
    
        this.id = id;
    }
    // Can be compile-time constants:
    private final int valueOne = 9;
    private static final int VALUE_TWO = 99;
    // Typical public constant:
    public static final int VALUE_THREE = 39;
    // Cannot be compile-time constants:
    private final int i4 = rand.nextInt(20);
    static final int INT_5 = rand.nextInt(20);
    private Value v1 = new Value(11);
    private final Value v2 = new Value(22);
    private static final Value VAL_3 = new Value(33);
    // Arrays:
    private final int[] a = {
    
    1, 2, 3, 4, 5, 6};
    
    @Override
    public String toString() {
    
    
        return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
    }
    
    public static void main(String[] args) {
    
    
        FinalData fd1 = new FinalData("fd1");
        //- fd1.valueOne++; // Error: can't change value
        fd1.v2.i++; // Object isn't constant
        fd1.v1 = new Value(9); // OK -- not final
        for (int i = 0; i < fd1.a.length; i++) {
    
    
            fd1.a[i]++; // Object isn't constant
        }
        //- fd1.v2 = new Value(0); // Error: Can't
        //- fd1.VAL_3 = new Value(1); // change reference
        //- fd1.a = new int[3];
        System.out.println(fd1);
        System.out.println("Creating new FinalData");
        FinalData fd2 = new FinalData("fd2");
        System.out.println(fd1);
        System.out.println(fd2);
    }
}

sortir:

fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18

Étant donné que valueOne et VALUE_TWO sont des types primitifs finaux avec des valeurs au moment de la compilation , ils peuvent tous deux être utilisés comme constantes au moment de la compilation sans grande différence. VALUE_THREE est une manière plus typique de définir des constantes : public signifie qu'elle est accessible en dehors du package, static signifie qu'il n'y en a qu'une et final signifie qu'il s'agit d'une constante.

Par convention, les variables de base statiques finales avec des valeurs initiales constantes (c'est-à-dire des constantes au moment de la compilation) sont nommées en lettres majuscules, avec des mots séparés par des traits de soulignement. (Dérivé de la façon dont les constantes sont définies dans le langage C.)

Nous ne pouvons pas penser que simplement parce qu'une certaine donnée est modifiée par final , sa valeur peut être connue au moment de la compilation. Comme le montrent i4 et INT_5 dans l'exemple ci-dessus , des nombres aléatoires leur sont attribués au moment de l'exécution. La section d'exemple montre également la différence entre définir des valeurs finales comme statiques et non statiques . Cette différence n'apparaît que lorsque la valeur est initialisée au moment de l'exécution, car le compilateur traite les valeurs au moment de la compilation de la même manière. (Les valeurs au moment de la compilation peuvent également disparaître en raison de l'optimisation.) Cette différence peut être constatée lors de l'exécution du programme. Notez que les valeurs i4 de fd1 et fd2 sont différentes, mais la valeur de INT_5 n'a pas changé car le deuxième objet FinalData a été créé. En effet, il est statique et a été initialisé lors du chargement, pas à chaque fois qu'un nouvel objet est créé. créé. Tous initialisés.

Les variables v1 à VAL_3 illustrent la signification de la référence finale . Comme vous main()pouvez le voir dans , ce n'est pas parce que la v2 est finale que vous ne pouvez pas modifier sa valeur. Puisqu’il s’agit d’une référence, cela signifie simplement qu’elle ne peut pas pointer vers un nouvel objet. Cela a la même signification pour les tableaux, qui ne sont qu’un autre type de référence. (Je ne connais aucun moyen de rendre une référence de tableau elle-même final .) Il semble que déclarer une référence final soit moins utile que de déclarer un type de base final .

finale vierge

Blank final fait référence à une propriété finale qui n'a pas de valeur d'initialisation. Le compilateur garantit que les espaces finaux doivent être initialisés avant utilisation. Cela peut non seulement rendre différente la valeur de propriété finale de chaque objet d'une classe , mais également maintenir son invariance.

// reuse/BlankFinal.java
// "Blank" final fields
class Poppet {
    
    
    private int i;
    
    Poppet(int ii) {
    
    
        i = ii;
    }
}

public class BlankFinal {
    
    
    private final int i = 0; // Initialized final
    private final int j; // Blank final
    private final Poppet p; // Blank final reference
    // Blank finals MUST be initialized in constructor
    public BlankFinal() {
    
    
        j = 1; // Initialize blank final
        p = new Poppet(1); // Init blank final reference
    }
    
    public BlankFinal(int x) {
    
    
        j = x; // Initialize blank final
        p = new Poppet(x); // Init blank final reference
    }
    
    public static void main(String[] args) {
    
    
        new BlankFinal();
        new BlankFinal(47);
    }
}

Vous devez effectuer l'affectation aux variables finales au moment de la définition ou dans chaque constructeur. Cela garantit que les propriétés finales sont initialisées avant utilisation.

paramètres finaux

Dans la liste des paramètres, déclarer le paramètre comme final signifie que l'objet ou la variable de base pointée par le paramètre ne peut pas être modifié dans la méthode :

// reuse/FinalArguments.java
// Using "final" with method arguments
class Gizmo {
    
    
    public void spin() {
    
    
        
    }
}

public class FinalArguments {
    
    
    void with(final Gizmo g) {
    
    
        //-g = new Gizmo(); // Illegal -- g is final
    }
    
    void without(Gizmo g) {
    
    
        g = new Gizmo(); // OK -- g is not final
        g.spin();
    }
    
    //void f(final int i) { i++; } // Can't change
    // You can only read from a final primitive
    int g(final int i) {
    
    
        return i + 1;
    }
    
    public static void main(String[] args) {
    
    
        FinalArguments bf = new FinalArguments();
        bf.without(null);
        bf.with(null);
    }
}

Méthodes f()et g()montrent l’utilisation des paramètres de type de base finaux . Vous pouvez uniquement lire mais pas modifier les paramètres. Cette fonctionnalité est principalement utilisée pour transmettre des données à des classes internes anonymes. Ceci sera expliqué en détail dans le chapitre « Classes internes ».

méthode finale

Il y a deux raisons d’ utiliser les méthodes finales . La première raison est de verrouiller la méthode pour empêcher les sous-classes de modifier le comportement de la méthode en la remplaçant. Ceci est fait pour des raisons d'héritage afin de garantir que le comportement de la méthode ne change pas en raison de l'héritage.

La deuxième raison pour laquelle les méthodes finales ont été recommandées dans le passé est l’efficacité. Dans les premières implémentations Java, si vous désigniez une méthode comme final , vous acceptiez que le compilateur convertisse les appels à la méthode en appels en ligne. Lorsque le compilateur rencontre un appel à une méthode finale , il fera très attention à ignorer le code d'insertion ordinaire pour exécuter le mécanisme d'appel de méthode (en poussant les paramètres sur la pile, en passant au code de la méthode pour l'exécution, puis en revenant en arrière et en effaçant les paramètres). sur la pile), la valeur de retour est finalement traitée) et l'appel de méthode est remplacé par une copie du code réel dans le corps de la méthode. Cela élimine la surcharge des appels de méthode. Mais si une méthode présente une surcharge de code importante, vous ne verrez peut-être pas l'amélioration des performances apportée par l'inline, car l'amélioration des performances apportée par les appels en inline est compensée par le temps passé dans la méthode.

Dans les versions Java récentes, la machine virtuelle peut détecter ces situations (notamment la technologie hotspot ) et optimiser et supprimer ces méthodes d'appel en ligne qui réduisent l'efficacité. Pendant longtemps, l’utilisation de final pour améliorer l’efficacité a été déconseillée. Vous devez laisser le compilateur et la JVM gérer les problèmes de performances et n'utiliser final que lorsque vous souhaitez désactiver explicitement le remplacement d'une méthode .

final et privé

Toutes les méthodes privées d'une classe sont implicitement désignées comme final . Étant donné que la méthode privée n’est pas accessible, elle ne peut pas être remplacée. Vous pouvez ajouter une modification finale à une méthode privée , mais cela n'apporte aucune signification supplémentaire à la méthode.

La situation suivante peut prêter à confusion, lorsque vous essayez de surcharger une méthode privée (qui est implicitement final ), cela semble fonctionner et le compilateur ne donne pas de message d'erreur :

// reuse/FinalOverridingIllusion.java
// It only looks like you can override
// a private or private final method
class WithFinals {
    
    
    // Identical to "private" alone:
    private final void f() {
    
    
        System.out.println("WithFinals.f()");
    }
    // Also automatically "final":
    private void g() {
    
    
        System.out.println("WithFinals.g()");
    }
}

class OverridingPrivate extends WithFinals {
    
    
    private final void f() {
    
    
        System.out.println("OverridingPrivate.f()");
    }
    
    private void g() {
    
    
        System.out.println("OverridingPrivate.g()");
    }
}

class OverridingPrivate2 extends OverridingPrivate {
    
    
    public final void f() {
    
    
        System.out.println("OverridingPrivate2.f()");
    } 
    
    public void g() {
    
    
        System.out.println("OverridingPrivate2.g()");
    }
}

public class FinalOverridingIllusion {
    
    
    public static void main(String[] args) {
    
    
        OverridingPrivate2 op2 = new OverridingPrivate2();
        op2.f();
        op2.g();
        // You can upcast:
        OverridingPrivate op = op2;
        // But you can't call the methods:
        //- op.f();
        //- op.g();
        // Same here:
        WithFinals wf = op2;
        //- wf.f();
        //- wf.g();
    }
}

sortir:

OverridingPrivate2.f()
OverridingPrivate2.g()

Le "remplacement" ne se produit que lorsque la méthode est une interface de la classe de base. Autrement dit, il doit être possible de transtyper un objet vers une classe de base et d'appeler la même méthode (ceci est clarifié dans le chapitre suivant). Si une méthode est private , elle ne fait pas partie de l’interface de la classe de base. C'est juste du code caché dans une classe qui porte le même nom. Mais si vous créez des méthodes public , protected ou package-access dans une classe dérivée portant le même nom, ces méthodes n'ont aucun lien avec les méthodes de la classe de base. Vous ne remplacez pas la méthode, vous créez simplement une nouvelle méthode . Puisque la méthode privée est inaccessible et effectivement cachée, rien d’autre n’a besoin de la considérer sauf comme faisant partie de la classe.

cours final

Lorsqu'une classe est dite finale ( le mot-clé final précède la définition de la classe), cela signifie qu'elle ne peut pas être héritée. Cela est dû au fait que la classe est conçue pour ne jamais avoir besoin d'être modifiée ou parce que vous ne souhaitez pas qu'elle ait de sous-classes pour des raisons de sécurité.

// reuse/Jurassic.java
// Making an entire class final
class SmallBrain {
    
    }

final class Dinosaur {
    
    
    int i = 7;
    int j = 1;
    SmallBrain x = new SmallBrain();
    
    void f() {
    
    }
}

//- class Further extends Dinosaur {}
// error: Cannot extend final class 'Dinosaur'
public class Jurassic {
    
    
    public static void main(String[] args) {
    
    
        Dinosaur n = new Dinosaur();
        n.f();
        n.i = 40;
        n.j++;
    }
}

Les attributs des cours finaux peuvent être définitifs ou non selon choix personnel . De même, les propriétés des classes non finales peuvent également être définitives ou non selon un choix personnel . Cependant, comme les classes final interdisent l'héritage, toutes les méthodes de la classe sont implicitement désignées comme final , il n'y a donc aucun moyen de les remplacer. Vous pouvez ajouter le modificateur final à une méthode dans une classe finale , mais cela n'ajoutera aucune signification.

dernier conseil

Il semble judicieux de désigner une méthode comme finale lors de la conception d’une classe. Vous pourriez penser que personne ne remplacerait cette méthode. Parfois, c'est vrai.

Mais faites attention à vos hypothèses. De manière générale, il est difficile de prévoir comment une classe sera réutilisée, notamment une classe générique. Désigner une méthode comme finale peut empêcher d'autres programmeurs de réutiliser votre classe par héritage dans leurs projets simplement parce que vous ne vous attendiez pas à ce qu'elle soit utilisée de cette manière.

La bibliothèque de classes standard Java en est un bon exemple. En particulier, la classe Vector de Java 1.0/1.1 est largement utilisée. Cependant, toutes ses méthodes sont désignées comme finales pour des raisons d'"efficacité" (cependant, cela n'améliore pas l'efficacité, ce n'est qu'une illusion). Si final n'est pas spécifié , cela peut être encore plus déroutant. Il est facile d'imaginer que vous puissiez hériter et remplacer une telle classe de base, mais les concepteurs n'ont pas pensé que cela était approprié. Ironique pour deux raisons. Premièrement, Stack hérite de Vector , ce qui signifie que Stack est un Vector , mais c'est logiquement faux. Pourtant, les concepteurs Java l'ont fait, et en créant des Stacks de cette manière , ils auraient dû se rendre compte que les méthodes finales étaient trop restrictives.

Deuxièmement, de nombreuses méthodes importantes dans VectoraddElement() , telles que les méthodes et elementAt(), sont synchronisées. Comme vous le verrez dans le chapitre « Programmation simultanée », la synchronisation peut entraîner une surcharge d'exécution importante, ce qui peut annuler les avantages de final . Cela renforce l’idée selon laquelle les programmeurs ne peuvent jamais deviner correctement où l’optimisation doit avoir lieu. C'est dommage qu'une conception aussi maladroite apparaisse dans la bibliothèque standard que tout le monde utilise. Heureusement, les conteneurs Java modernes utilisent ArrayList au lieu de Vector , qui se comporte de manière beaucoup plus rationnelle. Malheureusement, il existe encore beaucoup de nouveau code qui utilise les anciennes bibliothèques de collections, notamment Vector .

Une autre classe importante de la bibliothèque de classes standard Java 1.0/1.1 est Hashtable (remplacée plus tard par HashMap ), qui ne contient aucune méthode finale . Comme mentionné ailleurs dans ce livre, il est clair que différentes classes ont été conçues par différentes personnes. Hashtable est beaucoup plus concis que les noms de méthodes dans Vector , ce qui est un autre élément de preuve. Pour les utilisateurs de la bibliothèque de classes, cela ne devrait pas être fait à la hâte. Cette irrégularité crée plus de travail pour l'utilisateur - une autre ironie d'une conception et d'un code médiocres.

Initialisation et chargement de la classe

Dans de nombreux langages traditionnels, les programmes sont chargés en même temps au démarrage. Ensuite, initialisez-le, puis le programme commence à s'exécuter. Le processus d'initialisation dans ces langages doit être soigneusement contrôlé pour garantir que l'ordre dans lequel les statiques sont initialisées ne pose pas de problèmes. En C++, des problèmes peuvent survenir si un statique s'attend à ce qu'un autre statique soit utilisé et que l'autre statique n'a pas encore été initialisé.

Il n'y a pas de problème de ce type en Java car il se charge d'une manière différente. Puisque tout en Java est un objet, le chargement des activités est beaucoup plus facile. N'oubliez pas que le code compilé pour chaque classe existe dans son propre fichier distinct. Ce fichier n'est chargé que lorsque le code du programme est utilisé. En général, on peut dire que "le code d'une classe est chargé lors de sa première utilisation". Cela fait généralement référence à la création du premier objet de la classe ou à l'accès à une propriété ou à une méthode statique de la classe. Un constructeur est également une méthode statique même si son mot-clé static est implicite. Par conséquent, pour être précis, une classe sera chargée lors de l’accès à l’un de ses membres statiques .

La première fois qu'il est utilisé, c'est lors de l'initialisation statique . Tous les objets statiques et blocs de code statiques sont initialisés séquentiellement dans l'ordre du texte (l'ordre défini dans la classe) lors du chargement. Les variables statiques ne sont initialisées qu'une seule fois.

Héritage et initialisation

Il est utile de comprendre l'ensemble du processus d'initialisation, y compris l'héritage, afin d'avoir une compréhension globale de ce qui se passe. Prenons l'exemple suivant :

// reuse/Beetle.java
// The full process of initialization
class Insect {
    
    
    private int i = 9;
    protected int j;
    
    Insect() {
    
    
        System.out.println("i = " + i + ", j = " + j);
        j = 39;
    }
    
    private static int x1 = printInit("static Insect.x1 initialized");
    
    static int printInit(String s) {
    
    
        System.out.println(s);
        return 47;
    }
}

public class Beetle extends Insect {
    
    
    private int k = printInit("Beetle.k.initialized");
    
    public Beetle() {
    
    
        System.out.println("k = " + k);
        System.out.println("j = " + j);
    }
    
    private static int x2 = printInit("static Beetle.x2 initialized");
    
    public static void main(String[] args) {
    
    
        System.out.println("Beetle constructor");
        Beetle b = new Beetle();
    }
}

sortir:

static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39

Lorsque Java Beetle est exécuté , il va d'abord tenter d'accéder à la méthode de la classe Beetle main()(une méthode statique), le chargeur démarre et trouve le code compilé de la classe Beetle (dans un fichier nommé Beetle.class ). Pendant le processus de chargement, le compilateur remarque qu’il existe une classe de base et procède au chargement de la classe de base. La classe de base sera chargée indépendamment du fait qu'un objet de la classe de base soit créé ou non. (Vous pouvez essayer de commenter le code qui crée les objets de classe de base pour le prouver.)

Si la classe de base possède également sa propre classe de base, la deuxième classe de base sera également chargée, et ainsi de suite. Ensuite, l'initialisation statique de la classe de base racine (la classe de base dans l'exemple est Insect ) est exécutée, suivie de la classe dérivée, et ainsi de suite. Ceci est important car l’initialisation statique dans une classe dérivée peut dépendre de l’initialisation correcte des membres de la classe de base.

À ce stade, les classes nécessaires sont chargées et les objets peuvent être créés. Tout d'abord, toutes les variables primitives de l'objet sont définies sur leurs valeurs par défaut et les références d'objet sont définies sur null - ceci est créé d'un seul coup en définissant la mémoire de l'objet sur la valeur binaire zéro. Ensuite, le constructeur de la classe de base est appelé. Dans ce cas, il est appelé automatiquement, mais vous pouvez également utiliser super pour appeler le constructeur de classe de base spécifié ( la première opération du constructeur Beetle ). Les constructeurs de classes de base suivent le même processus dans le même ordre que les constructeurs de classes dérivées. Une fois le constructeur de la classe de base terminé, les variables d'instance sont initialisées dans l'ordre textuel. Finalement, le reste du constructeur est exécuté.

おすすめ

転載: blog.csdn.net/GXL_1012/article/details/132238488