Sprawdź, czy ciąg jest numeryczny w Javie

1. Wstęp

Często działając na String s, musimy dowiedzieć się, czy String jest prawidłową liczbą, czy nie.

W tym samouczku zbadamy wiele sposobów wykrywania, czy dany ciąg jest liczbowy , najpierw przy użyciu zwykłej Javy, następnie wyrażeń regularnych, a na końcu przy użyciu bibliotek zewnętrznych.

Gdy skończymy omawiać różne implementacje, użyjemy testów porównawczych, aby zorientować się, które metody są optymalne.

2. Wymagania wstępne

Zacznijmy od pewnych wymagań wstępnych, zanim przejdziemy do głównej zawartości.

W dalszej części tego artykułu będziemy używać zewnętrznej biblioteki Apache Commons, dla której dodamy jej zależność w naszym pom.xml :

 org.apache.commons commons-lang3 3.9 

Najnowszą wersję tej biblioteki można znaleźć w Maven Central.

3. Korzystanie ze zwykłej Java

Być może najłatwiejszym i najbardziej niezawodnym sposobem sprawdzenia, czy łańcuch jest numeryczny, czy nie, jest jego analiza za pomocą wbudowanych metod Javy:

  1. Integer.parseInt (ciąg)
  2. Float.parseFloat (ciąg)
  3. Double.parseDouble (ciąg)
  4. Long.parseLong (ciąg)
  5. nowy BigInteger (String)

Jeśli te metody nie zgłaszają wyjątku NumberFormatException , oznacza to, że parsowanie zakończyło się pomyślnie, a ciąg znaków jest numeryczny:

public static boolean isNumeric(String strNum) { if (strNum == null) { return false; } try { double d = Double.parseDouble(strNum); } catch (NumberFormatException nfe) { return false; } return true; }

Zobaczmy tę metodę w akcji:

assertThat(isNumeric("22")).isTrue(); assertThat(isNumeric("5.05")).isTrue(); assertThat(isNumeric("-200")).isTrue(); assertThat(isNumeric("10.0d")).isTrue(); assertThat(isNumeric(" 22 ")).isTrue(); assertThat(isNumeric(null)).isFalse(); assertThat(isNumeric("")).isFalse(); assertThat(isNumeric("abc")).isFalse();

W naszej metodzie isNumeric () po prostu sprawdzamy wartości typu Double , ale tę metodę można również zmodyfikować, aby sprawdzać liczby całkowite , zmiennoprzecinkowe , długie i duże przy użyciu dowolnej z metod analizy, które wymieniliśmy wcześniej .

Te metody są również omówione w artykule Konwersje ciągów Java.

4. Używanie wyrażeń regularnych

Teraz użyjmy wyrażenia regularnego -? \ D + (\. \ D +)? aby dopasować ciągi liczbowe składające się z dodatniej lub ujemnej liczby całkowitej i liczb zmiennoprzecinkowych.

Jest to jednak oczywiste, że zdecydowanie możemy zmodyfikować to wyrażenie regularne, aby identyfikować i obsługiwać szeroki zakres reguł. Tutaj będziemy to proste.

Podzielmy to wyrażenie regularne i zobaczmy, jak to działa:

  • -? - ta część określa, czy podana liczba jest ujemna, myślnik „ - ” wyszukuje myślnik dosłownie, a znak zapytania „ ? ”Zaznacza swoją obecność jako opcjonalną
  • \ d + - wyszukuje jedną lub więcej cyfr
  • (\. \ d +)? - ta część wyrażenia regularnego służy do identyfikacji liczb zmiennoprzecinkowych. Tutaj szukamy jednej lub więcej cyfr, po których następuje kropka. Znak zapytania w końcu oznacza, że ​​ta pełna grupa jest opcjonalna

Wyrażenia regularne to bardzo szeroki temat. Aby uzyskać krótkie omówienie, zapoznaj się z naszym samouczkiem dotyczącym interfejsu API wyrażeń regularnych języka Java.

Na razie stwórzmy metodę przy użyciu powyższego wyrażenia regularnego:

private Pattern pattern = Pattern.compile("-?\\d+(\\.\\d+)?"); public boolean isNumeric(String strNum) { if (strNum == null) { return false; } return pattern.matcher(strNum).matches(); }

Spójrzmy teraz na kilka stwierdzeń dotyczących powyższej metody:

assertThat(isNumeric("22")).isTrue(); assertThat(isNumeric("5.05")).isTrue(); assertThat(isNumeric("-200")).isTrue(); assertThat(isNumeric(null)).isFalse(); assertThat(isNumeric("abc")).isFalse();

5. Korzystanie z Apache Commons

W tej sekcji omówimy różne metody dostępne w bibliotece Apache Commons.

5.1. NumberUtils.isCreatable (ciąg)

NumberUtils z Apache Commons udostępnia statyczną metodę NumberUtils.isCreatable (String), która sprawdza, czy String jest prawidłowym numerem Java, czy nie.

Ta metoda akceptuje:

  1. Liczby szesnastkowe zaczynające się od 0x lub 0X
  2. Liczby ósemkowe rozpoczynające się od 0 na początku
  3. Notacja naukowa (na przykład 1.05e-10)
  4. Liczby oznaczone kwalifikatorem typu (na przykład 1L lub 2,2d)

Jeśli podany ciąg ma wartość null lub jest pusty / pusty , nie jest uważany za liczbę i metoda zwróci wartość false .

Przeprowadźmy kilka testów przy użyciu tej metody:

assertThat(NumberUtils.isCreatable("22")).isTrue(); assertThat(NumberUtils.isCreatable("5.05")).isTrue(); assertThat(NumberUtils.isCreatable("-200")).isTrue(); assertThat(NumberUtils.isCreatable("10.0d")).isTrue(); assertThat(NumberUtils.isCreatable("1000L")).isTrue(); assertThat(NumberUtils.isCreatable("0xFF")).isTrue(); assertThat(NumberUtils.isCreatable("07")).isTrue(); assertThat(NumberUtils.isCreatable("2.99e+8")).isTrue(); assertThat(NumberUtils.isCreatable(null)).isFalse(); assertThat(NumberUtils.isCreatable("")).isFalse(); assertThat(NumberUtils.isCreatable("abc")).isFalse(); assertThat(NumberUtils.isCreatable(" 22 ")).isFalse(); assertThat(NumberUtils.isCreatable("09")).isFalse();

Zwróć uwagę, jak otrzymujemy prawdziwe stwierdzenia dla liczb szesnastkowych, ósemkowych i notacji naukowych odpowiednio w wierszach 6, 7 i 8.

Ponadto w linii 14. ciąg „09” zwraca fałsz, ponieważ poprzedzające go „0” wskazuje, że jest to liczba ósemkowa, a „09” nie jest prawidłową liczbą ósemkową.

Dla każdego wejścia, które zwraca true przy użyciu tej metody, możemy użyć NumberUtils.createNumber (String), co da nam prawidłową liczbę.

5.2. NumberUtils.isParsable (ciąg)

Metoda NumberUtils.isParsable (String) sprawdza, czy dany ciąg String jest przetwarzalny, czy nie.

Parsable numbers are those that are parsed successfully by any parse method like Integer.parseInt(String), Long.parseLong(String), Float.parseFloat(String) or Double.parseDouble(String).

Unlike NumberUtils.isCreatable(), this method won't accept hexadecimal numbers, scientific notations or strings ending with any type qualifier, that is, ‘f', ‘F', ‘d' ,'D' ,'l'or‘L'.

Let's look at some affirmations:

assertThat(NumberUtils.isParsable("22")).isTrue(); assertThat(NumberUtils.isParsable("-23")).isTrue(); assertThat(NumberUtils.isParsable("2.2")).isTrue(); assertThat(NumberUtils.isParsable("09")).isTrue(); assertThat(NumberUtils.isParsable(null)).isFalse(); assertThat(NumberUtils.isParsable("")).isFalse(); assertThat(NumberUtils.isParsable("6.2f")).isFalse(); assertThat(NumberUtils.isParsable("9.8d")).isFalse(); assertThat(NumberUtils.isParsable("22L")).isFalse(); assertThat(NumberUtils.isParsable("0xFF")).isFalse(); assertThat(NumberUtils.isParsable("2.99e+8")).isFalse();

On line 4, unlike NumberUtils.isCreatable(), the number starting with string “0” isn't considered as an octal number, but a normal decimal number and hence it returns true.

We can use this method as a replacement for what we did in section 3, where we’re trying to parse a number and checking for an error.

5.3. StringUtils.isNumeric(CharSequence)

The method StringUtils.isNumeric(CharSequence) checks strictly for Unicode digits. This means:

  1. Any digits from any language that is a Unicode digit is acceptable
  2. Since a decimal point is not considered as a Unicode digit, it's not valid
  3. Leading signs (either positive or negative) are also not acceptable

Let's now see this method in action:

assertThat(StringUtils.isNumeric("123")).isTrue(); assertThat(StringUtils.isNumeric("١٢٣")).isTrue(); assertThat(StringUtils.isNumeric("१२३")).isTrue(); assertThat(StringUtils.isNumeric(null)).isFalse(); assertThat(StringUtils.isNumeric("")).isFalse(); assertThat(StringUtils.isNumeric(" ")).isFalse(); assertThat(StringUtils.isNumeric("12 3")).isFalse(); assertThat(StringUtils.isNumeric("ab2c")).isFalse(); assertThat(StringUtils.isNumeric("12.3")).isFalse(); assertThat(StringUtils.isNumeric("-123")).isFalse();

Note that the input parameters in lines 2 and 3 are representing numbers 123 in Arabic and Devanagari respectively. Since they're valid Unicode digits, this method returns true on them.

5.4. StringUtils.isNumericSpace(CharSequence)

The StringUtils.isNumericSpace(CharSequence) checks strictly for Unicode digits and/or space. This is same as StringUtils.isNumeric() with the only difference being that it accepts spaces as well, not only leading and trailing spaces but also if they're in between numbers:

assertThat(StringUtils.isNumericSpace("123")).isTrue(); assertThat(StringUtils.isNumericSpace("١٢٣")).isTrue(); assertThat(StringUtils.isNumericSpace("")).isTrue(); assertThat(StringUtils.isNumericSpace(" ")).isTrue(); assertThat(StringUtils.isNumericSpace("12 3")).isTrue(); assertThat(StringUtils.isNumericSpace(null)).isFalse(); assertThat(StringUtils.isNumericSpace("ab2c")).isFalse(); assertThat(StringUtils.isNumericSpace("12.3")).isFalse(); assertThat(StringUtils.isNumericSpace("-123")).isFalse();

6. Benchmarks

Before we conclude this article, let's go through some benchmark results to help us to analyze which of the above-mentioned methods are best for our use-case.

6.1. Simple Benchmark

First, we take a simple approach. We pick one string value – for our test we use Integer.MAX_VALUE. Then, that value will be tested against all our implementations:

Benchmark Mode Cnt Score Error Units Benchmarking.usingCoreJava avgt 20 57.241 ± 0.792 ns/op Benchmarking.usingNumberUtils_isCreatable avgt 20 26.711 ± 1.110 ns/op Benchmarking.usingNumberUtils_isParsable avgt 20 46.577 ± 1.973 ns/op Benchmarking.usingRegularExpressions avgt 20 101.580 ± 4.244 ns/op Benchmarking.usingStringUtils_isNumeric avgt 20 35.885 ± 1.691 ns/op Benchmarking.usingStringUtils_isNumericSpace avgt 20 31.979 ± 1.393 ns/op

As we see, the most costly operations are regular expressions. After that is our core Java-based solution.

Moreover, note that the operations using the Apache Commons library are by-and-large the same.

6.2. Enhanced Benchmark

Let's use a more diverse set of tests, for a more representative benchmark:

  • 95 values are numeric (0-94 and Integer.MAX_VALUE)
  • 3 contain numbers but are still malformatted — ‘x0‘, ‘0..005′, and ‘–11
  • 1 contains only text
  • 1 is a null

Upon executing the same tests, we'll see the results:

Benchmark Mode Cnt Score Error Units Benchmarking.usingCoreJava avgt 20 10162.872 ± 798.387 ns/op Benchmarking.usingNumberUtils_isCreatable avgt 20 1703.243 ± 108.244 ns/op Benchmarking.usingNumberUtils_isParsable avgt 20 1589.915 ± 203.052 ns/op Benchmarking.usingRegularExpressions avgt 20 7168.761 ± 344.597 ns/op Benchmarking.usingStringUtils_isNumeric avgt 20 1071.753 ± 8.657 ns/op Benchmarking.usingStringUtils_isNumericSpace avgt 20 1157.722 ± 24.139 ns/op

The most important difference is that two of our tests – the regular expressions solution and the core Java-based solution – have traded places.

Z tego wyniku dowiadujemy się, że zgłaszanie i obsługa wyjątku NumberFormatException , który występuje tylko w 5% przypadków, ma stosunkowo duży wpływ na ogólną wydajność. Zatem dochodzimy do wniosku, że optymalne rozwiązanie zależy od naszych oczekiwanych danych wejściowych.

Możemy również bezpiecznie stwierdzić, że powinniśmy użyć metod z biblioteki Commons lub metody zaimplementowanej w podobny sposób, aby uzyskać optymalną wydajność.

7. Wnioski

W tym artykule zbadaliśmy różne sposoby sprawdzania, czy ciąg jest liczbowy, czy nie. Przyjrzeliśmy się obu rozwiązaniom - metodom wbudowanym, a także bibliotekom zewnętrznym.

Jak zawsze, implementację wszystkich przykładów i fragmentów kodu podanych powyżej, w tym kodu używanego do wykonywania testów porównawczych, można znaleźć na GitHub.