web-dev-qa-db-fra.com

Java: System.out.println et System.err.println hors d'usage

Mes appels System.out.println() et System.err.println() ne sont pas imprimés sur la console dans l'ordre que je passe.

public static void main(String[] args) {
    for (int i = 0; i < 5; i++) {
        System.out.println("out");
        System.err.println("err");
    }
}

Cela produit:

out
out
out
out
out
err
err
err
err
err

Au lieu d'alterner out et err. Pourquoi est-ce?

45
Nick Heiner

Ils sont différents flux et sont vidés à des moments différents.

Si vous mettez 

System.out.flush();
System.err.flush();

dans votre boucle, cela fonctionnera comme prévu.

Pour clarifier, les flux de sortie sont mis en cache de sorte que toute l'écriture est placée dans cette mémoire tampon. Après une période de silence, ils sont en réalité écrits.

Vous écrivez dans deux mémoires tampons, puis après une période d'inactivité, elles sont toutes les deux vidées (l'une après l'autre).

37
Bill K

Cela est dû à une fonctionnalité de la machine virtuelle Java et à moins que vous ne fassiez un piratage tel que celui fourni par Marcus A. ce n’est pas vraiment facile à contourner. La .flush() fonctionne dans ce cas, mais la raison en est beaucoup plus compliquée.

Que se passe-t-il ici?

Lorsque vous programmez en Java, vous ne dites pas directement à l'ordinateur quoi faire, vous indiquez à la JVM (machine virtuelle Java) ce que vous voulez qu'elle fasse. Et cela le fera, mais de manière plus efficace. Votre code n'est pas une instruction exacte et détaillée. Dans ce cas, vous n'avez besoin que d'un compilateur, comme en C et C++. La JVM utilise votre code comme liste de spécification de ce qu'elle est censée optimiser puis faire. C'est ce qui se passe ici . Java voit que vous insérez des chaînes dans deux flux de mémoire tampon différents. La méthode la plus efficace consiste à mettre en mémoire tampon toutes les chaînes que vous souhaitez que les flux produisent en sortie, puis en sortie. Cela se produit un flux à la fois, transformant essentiellement votre code en faisant quelque chose comme ceci (attention: pseudo code) :

for(int i = 0; i < 5; i++) {
    out.add();
    err.add();
}
out.flush();
err.flush();

Comme c'est plus efficace, c'est ce que la machine virtuelle Java fera à la place. L'ajout de .flush() dans la boucle indiquera à la machine virtuelle Java qu'un vidage doit être effectué dans chaque boucle, ce qui ne peut pas être amélioré avec la méthode ci-dessus. Mais si vous voulez expliquer comment cela fonctionne, la JVM réorganisera votre code pour que l'impression soit effectuée en dernier, car cela est plus efficace.

System.out.println("out");
System.out.flush();
System.err.println("err");
System.err.flush();
System.out.println("out");
System.out.flush();
System.err.println("err");
System.err.flush();

Ce code sera toujours réorganisé pour ressembler à ceci:

System.out.println("out");*
System.err.println("err");*
System.out.println("out");*
System.err.println("err");*
System.out.flush();
System.err.flush();

Parce que la mise en mémoire tampon de nombreux tampons uniquement pour les vider immédiatement après prend beaucoup plus de temps que pour mettre en mémoire tampon tout le code à mettre en mémoire tampon puis tout vider en même temps.

Comment le résoudre

C’est là que la conception de code et l’architecture pourraient entrer en jeu; vous ne résolvez pas ça un peu. Pour contourner ce problème, vous devez rendre plus efficace la mise en tampon d’impression/vidage, d’impression/rinçage en tampon à l’aide de tampons puis de rinçage. Ce sera probablement vous attirer dans une mauvaise conception. S'il est important pour vous de savoir comment l'afficher de manière ordonnée, je vous suggère d'essayer une approche différente. Forer en boucle avec .flush() est un moyen de le pirater, mais vous continuez de pirater la fonctionnalité de la machine virtuelle Java pour réorganiser et optimiser votre code pour vous.

* Je ne peux pas vérifier que le tampon auquel vous avez ajouté en premier sera toujours imprimé en premier, mais très probablement.

27
Gemtastic

Si vous utilisez la console Eclipse, il semble y avoir deux phénomènes différents au travail:
L'une, décrite par @Gemtastic , concerne la gestion des flux par la JVM et l'autre est la façon dont Eclipse lit ces flux, comme indiqué par @DraganBozanovic . Depuis que j'utilise Eclipse, l'élégante solution flush()- publiée par @BillK , qui ne concerne que le problème de la machine virtuelle Java, n'est pas suffisante.

J'ai fini par écrire moi-même une classe d'assistance appelée EclipseTools avec le contenu suivant (ainsi que la déclaration de package requise et les importations). C'est un peu un bidouillage mais corrige les deux problèmes:

public class EclipseTools {

    private static List<OutputStream> streams = null;
    private static OutputStream lastStream = null;

    private static class FixedStream extends OutputStream {

        private final OutputStream target;

        public FixedStream(OutputStream originalStream) {
            target = originalStream;
            streams.add(this);
        }

        @Override
        public void write(int b) throws IOException {
            if (lastStream!=this) swap();
            target.write(b);
        }

        @Override
        public void write(byte[] b) throws IOException {
            if (lastStream!=this) swap();
            target.write(b);
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            if (lastStream!=this) swap();
            target.write(b, off, len);
        }

        private void swap() throws IOException {
            if (lastStream!=null) {
                lastStream.flush();
                try { Thread.sleep(200); } catch (InterruptedException e) {}
            }
            lastStream = this;
        }

        @Override public void close() throws IOException { target.close(); }
        @Override public void flush() throws IOException { target.flush(); }
    }

    /**
     * Inserts a 200ms delay into the System.err or System.out OutputStreams
     * every time the output switches from one to the other. This prevents
     * the Eclipse console from showing the output of the two streams out of
     * order. This function only needs to be called once.
     */
    public static void fixConsole() {
        if (streams!=null) return;
        streams = new ArrayList<OutputStream>();
        System.setErr(new PrintStream(new FixedStream(System.err)));
        System.setOut(new PrintStream(new FixedStream(System.out)));
    }
}

Pour l'utiliser, il suffit d'appeler EclipseTools.fixConsole() une fois au début de votre code.

Fondamentalement, cela remplace les deux flux System.err et System.out par un ensemble personnalisé de flux qui transfère simplement leurs données aux flux d'origine, tout en gardant une trace du dernier flux écrit. Si le flux écrit est modifié, par exemple une System.err.something(...) suivie d'une System.out.something(...), elle vide la sortie du dernier flux et attend 200 ms pour laisser à la console Eclipse le temps de terminer son impression.

Remarque: les 200 ms ne sont qu'une valeur initiale approximative. Si ce code réduit, mais n'élimine pas le problème pour vous, augmentez le délai dans Thread.sleep de 200 à un niveau supérieur jusqu'à ce qu'il fonctionne. Sinon, si ce délai fonctionne, mais affecte les performances de votre code (si vous alternez souvent des flux), vous pouvez essayer de le réduire progressivement jusqu'à ce que vous obteniez des erreurs.

9
Markus A.

Les deux instructions println sont gérées par deux threads différents. La sortie dépend à nouveau de l’environnement dans lequel vous exécutez le code. Par exemple, j’ai exécuté le code suivant dans IntelliJ et la ligne de commande 5 fois. 

public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.print("OUT ");
            System.err.print("ERR ");
        }
    }
}

Cela a pour résultat la sortie suivante:
Ligne de commande 

OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR
OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR
OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR
OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR
OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR OUT ERR

IntelliJ: 

ERR ERR ERR ERR ERR ERR ERR ERR ERR ERR OUT OUT OUT OUT OUT OUT OUT OUT OUT OUT 
OUT OUT OUT OUT OUT OUT OUT OUT OUT OUT ERR ERR ERR ERR ERR ERR ERR ERR ERR ERR
ERR ERR ERR ERR ERR ERR ERR ERR ERR ERR OUT OUT OUT OUT OUT OUT OUT OUT OUT OUT 
ERR ERR ERR ERR ERR ERR ERR ERR ERR ERR OUT OUT OUT OUT OUT OUT OUT OUT OUT OUT
OUT OUT OUT OUT OUT OUT OUT OUT OUT OUT ERR ERR ERR ERR ERR ERR ERR ERR ERR ERR 

Je suppose que différents environnements traitent les tampons différemment.
Une façon de vérifier que ces flux sont gérés par différents threads consiste à ajouter une instruction sleep dans la boucle. Vous pouvez essayer de faire varier la valeur que vous avez définie pour le sommeil et vérifier que celles-ci sont en réalité gérées par différents threads.

public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.print("OUT ");
            System.err.print("ERR ");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

La sortie dans ce cas s'est avéré être

OUT ERR ERR OUT ERR OUT OUT ERR OUT ERR ERR OUT OUT ERR ERR OUT OUT ERR OUT ERR
OUT ERR ERR OUT ERR OUT OUT ERR OUT ERR ERR OUT OUT ERR ERR OUT OUT ERR OUT ERR
ERR OUT ERR OUT OUT ERR ERR OUT OUT ERR ERR OUT OUT ERR OUT ERR ERR OUT ERR OUT 
ERR OUT OUT ERR ERR OUT OUT ERR ERR OUT ERR OUT OUT ERR ERR OUT ERR OUT OUT ERR
OUT ERR OUT ERR ERR OUT OUT ERR ERR OUT OUT ERR ERR OUT ERR OUT OUT ERR OUT ERR 

Une façon de le forcer à l’imprimer dans le même ordre serait d’utiliser la .flush(), qui a fonctionné pour moi. Mais il semble que tout le monde n’obtient pas les bons résultats.

La raison pour laquelle nous voyons parfois le message ERROR imprimé par certaines bibliothèques que nous utilisons est probablement due aux deux flux traités par deux threads différents. Elle est imprimée avant certaines instructions d'impression que nous étions censés voir en fonction de l'ordre d'exécution.

2
Rakesh

C'est un bug dans Eclipse . Il semble qu'Eclipse utilise des threads distincts pour lire le contenu des flux out et err sans aucune synchronisation.

Si vous compilez la classe et l'exécutez dans la console (avec le Java <main class name> classique), l'ordre est comme prévu.

1
Dragan Bozanovic

J'ai utilisé thread pour imprimer la sortie de System.out et System.err séquentiellement en tant que:

    for(int i = 0; i< 5; i++){
        try {
            Thread.sleep(100);
            System.out.print("OUT");
            Thread.sleep(100);
            System.err.print("ERR");
        }catch (InterruptedException ex){
            System.out.println(ex.getMessage());
        }
    }
0
Milan Paudyal