De façon générale, je vous invite quand vous vous posez des questions sur un mécanisme tel que l'héritage, à concevoir des petits exemple tels que ceux proposés.
  1. La classe B démarre dans la vie avec toutes les méthodes de la classe parente A. L'objet b possède donc une méthode g et une méthode f. Le code compile et affichera :
    Je suis f de de A
    
  2. La méthode f est redéfinie (override), tout objet de la classe B possède la nouvelle méthode f. Le code compile et affichera :
    Je suis f de de B
    
    On note que l'occurence de f dans le corps de g n'est pas résolue statiquement, c'est à dire en considérant la définition de f de la classe A. On parle de liaison tardive.

  3. Le code ne compile pas. En effet, si la conversion de b (de type B) vers A peut rester implicite, (Java confond héritage et sous-typage, c'est à dire qu'une classe construite par héritage appartient naturellement au type de sa classe parente), la conversion inverse (de a de type A vers B) ne va pas de soit. En effet cette conversion doit être explicitée et elle entraîne une vérification à l'exécution (qui ici n'échoue pas car la variable a contient bien un objet de classe B). On doit donc écrire :
     public static void main (String [] argv) {
       
    A a = new B () ;
       a.g() ; a.h() ;
       
    B b = (B)a ; // ICI
       b.g() ; b.h() ;
     }
    }
    Le code ne compile pas encore, car pour le compilateur, un objet de type A (c'est-à-dire dont le compilateur pense qu'il est de classe A), n'a pas de méthode h, on doit donc supprimer l'appel de méthode a.h()

    Le code compile enfin, et son exécution affiche :
    Je suis f de de B
    Je suis f de de B
    Je suis f de de B
    
    En effet les conversions de types sont justes des vérifications et ne changent pas la classe de l'objet.

    Résumons nous. Note : La classe exacte de l'objet contenu dans a est bien B, mais le compilateur ne veut pas le savoir. Pour comprendre que le compilateur n'est pas si crétin. Il faut noter que parfois le compilateur ne peut pas connaître la classe exacte d'un objet :
      static void appelerH(A a) {
        a.h() ;
      }
    Il faut écrire :
      static void appelerH(A a) {
        ((
    B)a).h() ;
      }
    Et se préparer à voir surgir l'exception idoine ClassCastException, que bizarrement on est pas obligé de déclarer.

  4. On défnit la nouvelle classe HInstit par héritage :
    import java.io.* ;

    class HInstit extends Instit {

      HInstit(
    Lexer lexer) {
        
    super(lexer) ;
      }

      
    void parseF() { … }

      
    void parseL() { … }

      
    void parse() { … }

      
    public static void main(String [] argv) {
        
    Lexer lexer = new Lexer (new StringReader (argv[0])) ;
        HInstit i = 
    new HInstit (lexer) ;
        i.parse() ;
      }
    }
    Le code des méthodes a déjà été donné. Il est remarquable que, par la le mécanisme de liaison tardive, les méthodes parseE et parseP appellent nouvelles définitions des méthodes redéfines, bien que ces premières soient définies dans la classe parente Instit.

    On notera l'appel au constructeur super(lexer) dans le constructeur de la classe héritante.

    Enfin, il n'est plus possible de qualifier par exemple tok ou step par private, car alors la classe héritante n'y aurait plus accès. On notera que l'absence de qualificateur signifie « visible de toutes les classes du même package », que protected signifie « visible de toutes les classes du même package et des classes héritantes », et que public signifie « visible de partout ».