2
votes

Existe-t-il un moyen plus rapide d'analyser une chaîne pour les entiers valides en Java?

Mon application attend des requêtes json contenant un tableau non trié (possible multidimensionnel) avec uniquement des entiers et des valeurs nulles possibles. Quelque chose comme [6, 2, [4, 3], [[[5], nil], 1]]

Comme je ne peux pas analyser le json invalide, j'ai dû recourir à l'utilisation d'une expression régulière pour faire le sale boulot, et c'est super lent.

Le cas de test ci-dessus, par exemple, prend environ 1.xx seconde pour se terminer, tandis qu'un tableau plat avec 10000 éléments prend moins de 1 seconde

Actuellement, je reçois le corps de la requête sous forme de chaîne, puis j'applique l'expression régulière.

static ArrayList<Integer> getIntegers(String requestData) {
    // Apply a regex to the request body
    final String regularExpression = "([^\\d])+";
    // to get all the nested arrays
    Pattern pattern = Pattern.compile(regularExpression);
    String[] results = pattern.split(requestData);
    ArrayList<Integer> numbers = new ArrayList<>();
    // loop over the results and add to numbers array
    for (String result : results) {
        try {
            numbers.add(Integer.valueOf(result));
        } catch (NumberFormatException e) {
            // Catch and skip any non integers
        }

    }
    return numbers;
}

}

Est-ce que je peux de toute façon accélérer cela ou y a-t-il peut-être une approche alternative avec de meilleures performances? Si je dois traiter un tableau multidimensionnel avec 20000 éléments, ce sera beaucoup trop lent.


6 commentaires

Bonne question!! Si c'était du javascript, je vous aurais demandé d'essayer .flatmap (Infinite) puis de vérifier. Peut-être que cela donnera quelques idées.


@AdityaGupta merci pour le tuyau! Je rechercherai certainement votre suggestion pour quelques idées


Pourquoi essayez-vous d'analyser JSON invalide au lieu de simplement rejeter de telles demandes?


petite note latérale, l'expression régulière [^ \ d] est égale à \ D , les deux correspondront à tout sauf les nombres


@SalmanA mon exigence est de supprimer les valeurs nil et d'obtenir uniquement des entiers valides. Si je pouvais simplement rejeter la demande, ce serait formidable. Ce n'est pas un système de production, si c'était le cas, votre question et vos conseils seraient parfaits (et exactement ce que je pensais moi-même)


@krankit si c'est juste le nil alors remplacez-les par null en utilisant la chaîne replace. Vous vous retrouverez avec un JSON valide que vous pourrez analyser avec n'importe quel analyseur JSON.


5 Réponses :


0
votes

Si les performances sont le problème dans votre cas, je ne pense pas que l'API Stream soit une bonne solution.

static ArrayList<Integer> getIntegers(String requestData) {
            char[] charArray = requestData.toCharArray();
             ArrayList<Integer> numbers = new ArrayList<>();
            for(char c : charArray) {

                if(Character.isDigit(c)) {
                    numbers.add(Integer.valueOf(c) - 48);
                }
            }
            return numbers;
        }


3 commentaires

Merci! Il semble que la solution ne fonctionne pas avec des nombres à deux (ou trois) chiffres car elle est formatée en un seul char comme vous pouvez le voir dans mon scénario de test SEVERE: java.lang.AssertionError: Not égale: [0,2,4,5,5,9,9]! = [0,2,4,55,99]


De plus, l'encapsulation de Integer.valueOf (c) semble inutile puisque vous vérifiez déjà si le caractère est un chiffre avec Character.isDigit (c) . Jouera avec cela plus tard merci!


De plus, comme expliqué dans ce commentaire , mélangeant Character.isDigit (…) avec le traitement du code uniquement les caractères de la plage '0' .. '9' est pas une bonne idée. Il n'y a pas non plus de raison d'écrire 48 au lieu de la constante de caractère '0' . De plus, créer une copie complète du contenu de la chaîne via toCharArray () juste pour écrire une boucle for-each, n'ajoute pas aux performances.



0
votes

Que diriez-vous d'utiliser une pile?

Nous pouvons mettre à jour le problème des accolades équilibrées .

Lors de l'itération de la chaîne, si le caractère est notBracket () , alors ce doit être un nombre. Inutile de dire que vous ignorez toutes les virgules. Simultanément, il vérifiera également la structure du tableau.

Cela a une complexité amortie de O (n) .


0 commentaires

0
votes

Vous pouvez obtenir de meilleures performances en analysant des modèles positifs (par exemple \ d + ) au lieu de négatifs ( [^ \ d] + ).

List<Integer> extractNumbersStreamTokenizer(String str) throws IOException {
    StreamTokenizer s = new StreamTokenizer(new StringReader(str));
    ArrayList<Integer> numbers = new ArrayList<>();
    int token;
    while ((token = s.nextToken()) != StreamTokenizer.TT_EOF) {
        if (token == StreamTokenizer.TT_NUMBER) {
            numbers.add((int) s.nval);
        }
    }
    return numbers;
}


1 commentaires

Gardez à l'esprit que Character.isDigit (…) peut renvoyer true pour les caractères en dehors de la plage '0' .. '9' . Lorsque vous utilisez Character.isDigit (…) pour tester, vous devez également utiliser Character.digit (…) pour extraire. Sinon, utilisez un test explicite pour la plage '0' .. '9' lorsque vous ne souhaitez gérer que les valeurs de cette plage. De plus, votre code peut déborder silencieusement lorsque la magnitude du nombre est trop grande.



3
votes

J'ai bricolé un peu et créé la classe suivante:

List<Integer> integers = new JsonNumberParser(jsonRequest).parse();

Bien sûr, vous pouvez mettre tout cela dans une seule méthode, mais vous obtiendrez alors une duplication de code concernant le ajout de Integers.

Le fonctionnement de cet analyseur est qu'il utilise un tampon, pour tamponner les chiffres, jusqu'à ce que nous rencontrions une virgule. De cette façon, nous pouvons avoir de grands nombres (jusqu'à 64 chiffres dans cette implémentation) dans le json.

Vous pouvez utiliser ceci comme indiqué dans l'exemple suivant:

class JsonNumberParser {
    private final String json;
    private final int length;
    private final List<Integer> result;
    private final char[] buffer = new char[64];
    private int bufferIndex = 0;

    public JsonNumberParser(String json) {
        this.json = json;
        length = json.length();
        result = new ArrayList<>(length);
    }

    public List<Integer> parse() {
        char c;
        for (int i = 0; i < length; i++) {
            c = json.charAt(i);
            // if we encounter a comma and the buffer contains data
            if (c == ',' && bufferIndex > 0) {
                // then we add the new number
                addBuffer();
                // and reset the buffer
                while (bufferIndex > 0) {
                    buffer[--bufferIndex] = '\0';
                }
            } else if (c == '-' || (c >= '0' && c <= '9')) {
                buffer[bufferIndex++] = c;
            }
        }
        // add the last possible number, if there was any
        if (bufferIndex > 0) {
            addBuffer();
        }

        // return the result
        return result;
    }

    private void addBuffer() {
        result.add(Integer.valueOf(new String(buffer, 0, bufferIndex)));
    }
}

En ce qui concerne les performances, je m'attends à ce que ce soit beaucoup plus rapide que d'utiliser un Regex . Mais je n'ai malheureusement pas de configuration de référence sous la main


Gardez à l'esprit que ce n'est pas un validateur, donc une chaîne json: [[]}] produirait simplement une Liste p >


(Peut-être) Améliorations : J'ai réfléchi et cherché un peu plus. Voici quelques améliorations qui pourraient améliorer les performances:

1. On pourrait simplement réinitialiser le buffer en l'attribuant avec un new int [64] , ce qui produirait plus de déchets, mais à la fin peut être plus rapide.

2. L'analyse du nombre pourrait être amélioré en utilisant la réponse suggérée ici . Ce qui utilise simplement de vieilles mathématiques et aucune création de chaînes et analyse d'entiers.


1 commentaires

Pourquoi réinitialiser le tableau? Vous n'utilisez que la partie que vous avez écrasée par des valeurs réelles, les valeurs char pendantes (contrairement aux références d'objet) ne sont donc pas pertinentes. Mettez simplement bufferIndex à zéro. Mais encore mieux, au lieu de copier les valeurs char dans votre propre tableau de tampons, vous pouvez simplement vous souvenir de la position du premier caractère correspondant et utiliser sous-chaîne sur la chaîne d'origine lors de la rencontre soit, le premier caractère non correspondant ou la fin de la chaîne. Ou utilisez la méthode parseInt (CharSequence cs, int start, int end) de ma réponse.



3
votes

Cette réponse va déjà dans la bonne direction. La première étape importante est de déplacer l'opération coûteuse Pattern.compile hors de la méthode, car l'instance de Pattern peut être réutilisée.

De plus, l'itération sur le nombre correspond enregistre vous dès la création du tableau de split . Maintenant, vous pouvez également ignorer la création de sous- String :

static final Pattern NUMBER = Pattern.compile("-?\\d+");
static ArrayList<Integer> getIntegers(String requestData) {
    ArrayList<Integer> numbers = new ArrayList<>();
    Matcher m = NUMBER.matcher(requestData);
    while(m.find()) numbers.add(parseInt(requestData, m.start(), m.end()));
    return numbers;
}

static int parseInt(CharSequence cs, int start, int end) {
    int pos = start;
    if(pos >= end) throw format(cs, start, end);
    boolean negative = cs.charAt(pos) == '-';
    if((negative || cs.charAt(pos) == '+') && ++pos==end)
        throw format(cs, start, end);
    int value = 0;
    for(; pos < end; pos++) {
        int next = cs.charAt(pos) - '0';
        if(next < 0 || next > 9) throw format(cs, start, end);
        if(value < Integer.MIN_VALUE/10) throw size(cs, start, pos, end);
        value = value * 10 - next;
    }
    if(value > 0 || !negative && value == Integer.MIN_VALUE)
        throw size(cs, start, pos, end);
    return negative? value: -value;
}
private static RuntimeException format(CharSequence cs, int start, int end) {
    return start > end? new IndexOutOfBoundsException(end+" < "+start):
        new NumberFormatException(start == end?
            "empty string": cs.subSequence(start, end).toString());
}
private static RuntimeException size(CharSequence cs, int start, int pos, int end) {
    for(; pos < end; pos++) 
        if(cs.charAt(pos) < '0' || cs.charAt(pos) > '9') return format(cs, start, end);
    return new NumberFormatException(cs.subSequence(start, end)+" outside the int range");
}

Integer.parseInt (CharSequence s, int beginIndex, int endIndex, int radix) a été ajouté dans Java 9. Si vous utilisez une version plus ancienne, vous pouvez en créer votre propre variante. Pour simplifier, ne prend désormais en charge qu'une base de 10:

static final Pattern NUMBER = Pattern.compile("\\d+");
static ArrayList<Integer> getIntegers(String requestData) {
    ArrayList<Integer> numbers = new ArrayList<>();
    Matcher m = NUMBER.matcher(requestData);
    while(m.find()) numbers.add(Integer.parseInt(requestData, m.start(), m.end(), 10));
    return numbers;
}


0 commentaires