NodeList est live !

Jean-François Perrot


  1. Le problème

  2. Que faire ?
    1. Léger mais risqué
    2. Sûr et simple mais pesant
    3. Optimal mais compliqué



Le problème

Dans la plupart des traitements fondés sur DOM interviennent des listes de sommets de type org.w3c.dom.NodeList.
Exemple : dans un document
Noms_notes, on veut supprimer les éléments <eleve> dont la note est inférieure à 5/20.
Pour cela, on est tenté de modifier la classe Lire_1 (vue en cours) comme suit :

fichier Suppr.java

/* Noms-Notes en format 1 */

import org.w3c.dom.NodeList;
import org.w3c.dom.Element;
import org.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

public class Suppr{
   
    public static void suppr(Document doc, int barre) throws Exception {
   
        Element depart = doc.getDocumentElement();
       
        int k = 0;  //nombre de notes
        NodeList les_eleves = depart.getElementsByTagName("eleve");
        for( int i = 0; i < les_eleves.getLength(); i++ ){
            Element l_eleve = (Element) les_eleves.item(i); // coercion !
            String le_nom = l_eleve.getAttribute("nom");
            String la_note = l_eleve.getAttribute("note");

            if( Integer.parseInt(la_note) < barre ){
                l_eleve.getParentNode().removeChild(l_eleve);
                System.out.println(le_nom+" a pour note "+la_note+ " on le supprime de la liste.");
                k++;
            }//if
        }// for
       
         System.out.println("\nBarre = "+barre+" : suppression de "+k+" noms-notes.");
    }// suppr;
       
    public static void main(String[] args) throws Exception {
       
        DocumentBuilder parseur = DocumentBuilderFactory.
                                    newInstance().newDocumentBuilder();
        Document doc =  parseur.parse("NN1.xml");
       
        suppr(doc, 5);
    }//main
}// class Suppr


Essai : fichier NN1.xml
<?xml version="1.0" encoding="UTF-8"?>
<liste>
  <eleve nom="Danièle" note="15"/>
  <eleve nom="Max" note="07"/>
  <eleve nom="Pierre" note="03"/>    <!-- 1 Pierre-->
  <eleve nom="Héloïse" note="18"/>
  <eleve nom="Kevin" note="09"/>
  <eleve nom="Joëlle" note="16"/>
  <eleve nom="Christine" note="12"/>
  <eleve nom="Joseph" note="03"/>    <!-- 2 Joseph-->
  <eleve nom="Elisabeth" note="02"/><!-- 3 Elisabeth-->
  <eleve nom="Elsa" note="02"/>        <!-- 4 Elsa-->
  <eleve nom="Frédéric" note="03"/>    <!-- Frédéric-->
  <eleve nom="Amélie" note="04"/>    <!-- 6 Amélie-->
  <eleve nom="Jules" note="11"/>
  <eleve nom="Étienne" note="13"/>
  <eleve nom="Gaëlle" note="05"/>
  <eleve nom="Jean-Pierre" note="01"/><!-- 7 Jean-Pierre-->
  <eleve nom="Franck" note="12"/>
  <eleve nom="Francine" note="13"/>
  <eleve nom="Aline" note="12"/>
</liste
>

Exécution:

jfp% java Suppr
Pierre a pour note 03 on le supprime de la liste.
Joseph a pour note 03 on le supprime de la liste.
Elsa a pour note 02 on le supprime de la liste.
Am?lie a pour note 04 on le supprime de la liste.
Jean-Pierre a pour note 01 on le supprime de la liste.

Barre = 5 : suppression de 5 noms-notes.


Et pourtant il y en avait 7 à supprimer !

Où sont passés Elisabeth et Frédéric ?
La suppression de Joseph a fait avancer Elisabeth à sa place,
et au tour de boucle suivant on a trouvé Elsa au lieu d'Elisabeth...
Idem pour Frédéric !

Car une NodeList est un objet vivant ...


Ref. W3C
The DOM also specifies a NodeList interface to handle ordered lists of Nodes, such as the children of a Node, or the elements returned by the Element.getElementsByTagNameNS(namespaceURI, localName) method, and also a NamedNodeMap interface to handle unordered sets of nodes referenced by their name attribute, such as the attributes of an Element. NodeList and NamedNodeMap objects in the DOM are live; that is, changes to the underlying document structure are reflected in all relevant NodeList and NamedNodeMap objects. For example, if a DOM user gets a NodeList object containing the children of an Element, then subsequently adds more children to that element (or removes children, or modifies them), those changes are automatically reflected in the NodeList, without further action on the user's part. Likewise, changes to a Node in the tree are reflected in all references to that Node in NodeList and NamedNodeMap objects.

Que faire ?

Voici 3 démarches possibles :
  1. Léger mais risqué

    puisque notre problème vient d'une affaire d'indices, on peut tout bonnement annuler l'effet non souhaité :
    fichier
    SupprA.java
                if( Integer.parseInt(la_note) < barre ){
                    l_eleve.getParentNode().removeChild(l_eleve);
                    System.out.println(le_nom+" a pour note "+la_note+ " on le supprime de la liste.");
                    k++;
                    i--; // contrarie l'effet de l'effacement sur la NodeList
                }//if

    mais ce patch repose sur une interprétation du fonctionnement de NodeList qui peut dépendre de l'implémentation...

  2. Sûr et simple mais pesant

    on recopie la liste dans une collection stable, et on travaille sur cette collection.
    fichier
    supprP.java
            NodeList eleves = depart.getElementsByTagName("eleve");
           
            ArrayList<Element> les_eleves = new ArrayList<Element>();
            for( int i = 0; i < eleves.getLength(); i++ ){
                les_eleves.add( (Element) eleves.item(i) );
            }// for i
           
            for( Iterator<Element> itr = les_eleves.iterator(); itr.hasNext(); ){
                Element l_eleve = itr.next();
                String le_nom = l_eleve.getAttribute("nom");
                String la_note = l_eleve.getAttribute("note");
                ...le reste sans changement...

  3. Optimal mais compliqué

    on ne recopie que les éléments à supprimer, et on les supprime dans une seconde passe.
    fichier
    Suppr0.java
            NodeList eleves = depart.getElementsByTagName("eleve");
           
            ArrayList<Element> les_eleves = new ArrayList<Element>();
            for( int i = 0; i < eleves.getLength(); i++ ){
                Element lel = (Element) eleves.item(i);
                if( Integer.parseInt(lel.getAttribute("note")) < barre ){
                    les_eleves.add(lel);
                }
            }// for i
           
            for( Iterator<Element> itr = les_eleves.iterator(); itr.hasNext(); ){
                Element l_eleve = itr.next();
                String le_nom = l_eleve.getAttribute("nom");
                String la_note = l_eleve.getAttribute("note");

                l_eleve.getParentNode().removeChild(l_eleve);
                System.out.println(le_nom+" a pour note "+la_note+ " on le supprime de la liste.");
                k++;
            }// for itr


    Àvous le choix !