Более чистый способ проверить, является ли строка страной ISO языка ISO в Java

Предположим, у вас есть двухсимвольный символ String, который должен представлять ISO 639 название страны или языка.

Вы знаете, что класс Locale имеет две функции < a href="http://docs.oracle.com/javase/7/docs/api/java/util/Locale.html#getISOLanguages%28%29" rel="noreferrer">getISOLanguages и getISOCountries, которые возвращают массив из String со всеми языками ISO и странами ISO соответственно.

Чтобы проверить, является ли конкретный объект String допустимым языком ISO или страной ISO, я должен просмотреть эти массивы на предмет соответствия String. Хорошо, я могу сделать это с помощью бинарного поиска (например, Arrays.binarySearch или ApacheCommons ArrayUtils.contains) .

Вопрос в следующем: существует какая-либо утилита (например, из библиотек Guava или Apache Commons), которая обеспечивает более чистый способ, например. функция, которая возвращает boolean для проверки String как допустимого языка ISO 639 или страны ISO 639?

Например:

public static boolean isValidISOLanguage(String s)
public static boolean isValidISOCountry(String s)

person mat_boy    schedule 10.04.2013    source источник
comment
Не забудьте проверить длину строки перед поиском в массиве (так или иначе)   -  person Dariusz    schedule 10.04.2013
comment
Да конечно... спасибо!   -  person mat_boy    schedule 10.04.2013
comment
@Dariusz: я не уверен, что стал бы беспокоиться - по крайней мере, если бы выполнял поиск хэша. Если вы не ожидаете, что вам будут даны огромные строки, для хеширования которых потребуется много времени, это похоже на сложность без доказанной значительной пользы.   -  person Jon Skeet    schedule 10.04.2013
comment
@JonSkeet Пожалуйста, не могли бы вы уточнить?   -  person mat_boy    schedule 10.04.2013
comment
@mat_boy: Уточните, что именно? Какой бит неясен?   -  person Jon Skeet    schedule 10.04.2013
comment
@JonSkeet Почему, по вашему мнению, это кажется сложностью без доказанной существенной пользы...   -  person mat_boy    schedule 10.04.2013
comment
@mat_boy: Ну, именно так: это делает код более сложным, и значительная выгода будет только в том случае, если вам дадут много недопустимых строк, поиск которых займет много времени. Я подозреваю, что для большинства приложений это не так.   -  person Jon Skeet    schedule 10.04.2013
comment
Что ж, возможно, вы правы! Однако я добавил в ваши функции проверку на Pattern.matches("[a-z]+", s) и Pattern.matches("[A-Z]+", s) просто для того, чтобы быть уверенным, что строки — это, соответственно, только альфа-символы в нижнем и верхнем регистре. Я хочу создать исключение, чтобы предоставить отзыв об отсутствующей достоверности предоставленной строки.   -  person mat_boy    schedule 10.04.2013
comment
@mat_boy Сопоставление этих строк с регулярным выражением может занять больше времени, чем поиск HashSet. Если есть вероятность, что ваши строки будут длиннее 2 символов, проверьте длину. Затем выполните поиск по хешу.   -  person Dariusz    schedule 10.04.2013
comment
@Dariusz Спасибо! Теперь у меня есть метод, который принимает строку, сначала проверяя isValidISO...(). Если это неверно, я проверяю длину, а затем тип шаблона, чтобы в конечном итоге выдать исключение, чтобы дать отзыв пользователю. Я прав?   -  person mat_boy    schedule 10.04.2013
comment
Что произойдет после вызова isValidISO(), зависит от вас — все, что вы хотите сообщить пользователю, — ваш выбор. Я бы, наверное, просто сказал неверный код страны, но больше информации, как правило, лучше :) Просто убедитесь, что сообщение понятно.   -  person Dariusz    schedule 10.04.2013


Ответы (2)


Я бы не стал использовать ни бинарный поиск, ни какие-либо сторонние библиотеки — для этого подойдет HashSet:

public final class IsoUtil {
    private static final Set<String> ISO_LANGUAGES = new HashSet<String>
        (Arrays.asList(Locale.getISOLanguages()));
    private static final Set<String> ISO_COUNTRIES = new HashSet<String>
        (Arrays.asList(Locale.getISOCountries()));

    private IsoUtil() {}

    public static boolean isValidISOLanguage(String s) {
        return ISO_LANGUAGES.contains(s);
    }

    public static boolean isValidISOCountry(String s) {
        return ISO_COUNTRIES.contains(s);
    }
}

Вы можете сначала проверить длину строки, но я не уверен, что стал бы заморачиваться — по крайней мере, если вы не хотите защитить себя от атак на производительность, когда вам даются огромные строки, которые потребовали бы долго хешировать.

РЕДАКТИРОВАТЬ: Если вы действительно хотите использовать стороннюю библиотеку, ICU4J является наиболее вероятный претендент, но у него вполне может быть более актуальный список, чем те, которые поддерживаются Locale, поэтому, вероятно, вы захотите перейти на использование ICU4J везде.

person Jon Skeet    schedule 10.04.2013
comment
Обычно я предпочитаю сторонние библиотеки (такие как Guava и ApacheCommons), потому что они часто улучшаются, а я не могу постоянно проверять свой код: лучше поменять версию библиотеки, чем читать тысячи кодов. Тем не менее, я очень ценю ваш ответ. Благодарю вас! - person mat_boy; 10.04.2013
comment
@mat_boy: Как вы ожидаете, что этот код изменится со временем? Он уже делегирует JDK поиск актуального списка стран и языков... - person Jon Skeet; 10.04.2013
comment
Ну, это не про этот код, это в принципе :) Более того, если я уже делал импорт какой-то библиотеки, то обычно предпочитаю использовать методы из этих библиотек, чтобы сделать код более читабельным. - person mat_boy; 10.04.2013
comment
@mat_boy: Хорошо, в таком случае я подозреваю, что ответ просто нет, по крайней мере, на стороне гуавы. Возможно, в Apache Commons что-то есть, но учитывая, что это будет довольно тонкая оболочка, я не ожидал этого. Если здесь уместна какая-либо сторонняя библиотека, это будет icu4j - person Jon Skeet; 10.04.2013
comment
Пожалуйста, добавьте один ) перед ; в третьей и пятой строках. Благодарю вас! - person mat_boy; 10.04.2013
comment
@mat_boy Если вы уже используете Guava, вы можете использовать ImmutableSet, что является идеальным вариантом использования статических констант final, плюс код менее загроможден: private static final Set<String> ISO_LANGUAGES = ImmutableSet.copyOf(Locale.getISOLanguages()); - person Xaerxess; 10.04.2013
comment
@Xaerxess Да, я использую его! Благодарю вас! - person mat_boy; 10.04.2013
comment
Это будет работать медленнее, чем binarySearch(), и будет использовать много памяти. - person Sergey Ponomarev; 14.12.2018
comment
@stokito: Это хэш-набор - почему вы ожидаете, что это будет медленно? - person Jon Skeet; 14.12.2018
comment
Потому что массив более дружелюбен к кешу процессора. В большинстве ситуаций даже линейный поиск может работать быстрее, особенно если страна ближе к началу. Сортировка стран по населению может быть весьма эффективной оптимизацией, но это будет очень спекулятивная оптимизация. Вы можете написать бенчмарк JMH, но я уверен, что здесь теория сложности не согласуется с аппаратным обеспечением. - person Sergey Ponomarev; 15.12.2018
comment
Кстати, в jdk 9 в Locale был добавлен метод, который возвращает Set - person Sergey Ponomarev; 15.12.2018
comment
@stokito: я думаю, нам придется согласиться не соглашаться. Вместо того, чтобы полагаться на недокументированное поведение, я бы предпочел просто использовать набор (сейчас используя вызов Java 9). Я, конечно, не стал бы пытаться микрооптимизировать на основе предположений, даже не зная, имеет ли это значение с точки зрения производительности. Использование памяти будет крошечным по сравнению с остальной частью почти любого реалистичного приложения, и я был бы удивлен, если бы это было достаточно медленным, чтобы вообще быть заметным, если вы ничего не делаете но проверка кодов стран ISO - что мне кажется маловероятным. - person Jon Skeet; 15.12.2018

Насколько я знаю, такого метода нет ни в одной библиотеке, но, по крайней мере, вы можете объявить его самостоятельно, например:

import static java.util.Arrays.binarySearch;
import java.util.Locale;

/**
 * Validator of country code.
 * Uses binary search over array of sorted country codes.
 * Country code has two ASCII letters so we need at least two bytes to represent the code.
 * Two bytes are represented in Java by short type. This is useful for us because we can use Arrays.binarySearch(short[] a, short needle)
 * Each country code is converted to short via countryCodeNeedle() function.
 *
 * Average speed of the method is 246.058 ops/ms which is twice slower than lookup over HashSet (523.678 ops/ms).
 * Complexity is O(log(N)) instead of O(1) for HashSet.
 * But it consumes only 520 bytes of RAM to keep the list of country codes instead of 22064 (> 21 Kb) to hold HashSet of country codes.
 */
public class CountryValidator {
  /** Sorted array of country codes converted to short */
  private static final short[] COUNTRIES_SHORT = initShortArray(Locale.getISOCountries());

  public static boolean isValidCountryCode(String countryCode) {
    if (countryCode == null || countryCode.length() != 2 || countryCodeIsNotAlphaUppercase(countryCode)) {
      return false;
    }
    short needle = countryCodeNeedle(countryCode);
    return binarySearch(COUNTRIES_SHORT, needle) >= 0;
  }

  private static boolean countryCodeIsNotAlphaUppercase(String countryCode) {
    char c1 = countryCode.charAt(0);
    if (c1 < 'A' || c1 > 'Z') {
      return true;
    }
    char c2 = countryCode.charAt(1);
    return c2 < 'A' || c2 > 'Z';
  }

  /**
   * Country code has two ASCII letters so we need at least two bytes to represent the code.
   * Two bytes are represented in Java by short type. So we should convert two bytes of country code to short.
   * We can use something like:
   * short val = (short)((hi << 8) | lo);
   * But in fact very similar logic is done inside of String.hashCode() function.
   * And what is even more important is that each string object already has cached hash code.
   * So for us the conversion of two letter country code to short can be immediately.
   * We can relay on String's hash code because it's specified in JLS
   **/
  private static short countryCodeNeedle(String countryCode) {
    return (short) countryCode.hashCode();
  }

  private static short[] initShortArray(String[] isoCountries) {
    short[] countriesShortArray = new short[isoCountries.length];
    for (int i = 0; i < isoCountries.length; i++) {
      String isoCountry = isoCountries[i];
      countriesShortArray[i] = countryCodeNeedle(isoCountry);
    }
    return countriesShortArray;
  }
}

Locale.getISOCountries() всегда будет создавать новый массив, поэтому мы должны хранить его в статическом поле, чтобы избежать ненужных распределений. В то же время HashSet или TreeSet потребляют много памяти, поэтому этот валидатор будет использовать бинарный поиск по массиву. Это компромисс между скоростью и памятью.

person Sergey Ponomarev    schedule 14.12.2018
comment
Я не вижу в документации никаких гарантий, что значение, возвращаемое Locale.getISOCountries(), отсортировано, что необходимо для работы бинарного поиска. Конечно, вы могли бы сначала отсортировать его, но это должно быть частью ответа. - person Jon Skeet; 14.12.2018
comment
Довольно хороший момент, но мы можем быть уверены, что он всегда будет отсортирован. И да, в javadoc это должно быть четко указано. Это хороший кандидат для отправки запроса на включение в JDK. - person Sergey Ponomarev; 15.12.2018
comment
Вау, я бы не стал так просто доверять существующему поведению. Я бы точно разобрался. Но тогда я бы все равно использовал HashSet в соответствии с моим ответом, и в этот момент это не имеет значения. - person Jon Skeet; 15.12.2018