JavaRush /Java блог /Random UA /Розбираємо по поличках клас StringUtils
Roman Beekeeper
35 рівень

Розбираємо по поличках клас StringUtils

Стаття з групи Random UA
Всім привіт, мої любі читачі. Я намагаюся писати про те, що реально мені цікаво і що хвилює зараз. Тому сьогодні буде легке чтиво, яке в майбутньому стане вам у нагоді як довідник: поговоримо про StringUtils . Розбираємо по поличках клас StringUtils - 1Так сталося, що свого часу я обійшов увагою бібліотеку Apache Commons Lang 3 . Це бібліотека з допоміжними класами для роботи з різними об'єктами. Така собі збірка корисних методів для роботи з рядками, колекціями, і так далі. На нинішньому проекті, де мені довелося детальніше попрацювати з рядками у перекладі бізнес-логіки 25-річної давності (з COBOL на Java), виявилося, що я мав недостатньо глибокі знання про клас StringUtils. Тому доводилося створювати все самому. Що я маю на увазі? Те, що певні завдання з маніпуляцією з рядками можна не писати самому, а скористатися готовим рішенням. Чим погано писати самому? Як мінімум тим, що це більше за код, який вже давно написаний. Не менш гостро стоїть питання про тестування того коду, який написаний додатково. Коли ми використовуємо бібліотеку, яка зарекомендувала себе як якісну, ми очікуємо, що вона вже протестована і не потрібно писати безліч варіантів тестів, щоб перевірити її. Так вийшло, що набір методів для роботи з рядком у джави не такий вже й великий. Реально не вистачає багатьох методів, які були б корисними для роботи. Також цей клас створений для забезпечення перевірок на NullPointerException. План нашої статті буде таким:
  1. Як підключити?
  2. Приклади з моєї роботи: як не знаючи про такий корисний клас, я створював свій велосипед мабоця.
  3. Розбираємо інші методи, які мені видалися цікавими.
  4. Підбиваємо підсумок.
Всі випадки будуть додані до окремого репозиторію в організації Javarush Community на GitHub. Там будуть окремо записані приклади та тести для них.

0. Як підключити

Ті, хто йде зі мною нога в ногу, вже більш-менш знайомі і з гітом, і з мавеном, так що далі я спиратимуся на ці знання і не повторюватимусь. Тим же, хто пропустив мої попередні статті або тільки приєднався до читання — ось матеріали про мавен і гіт . Звичайно, без системи складання (мавен, гредл) можна також підключити все ручками, але це дико в наш час і так робити не потрібно: краще відразу вчитися робити все правильно. Тому для роботи з Мавен спочатку додаємо відповідну залежність:
<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-lang3</artifactId>
   <version>${apache.common.version}</version>
</dependency>
Де ${apache.common.version} — версія цієї бібліотеки. Далі, щоб імпортувати в якомусь класі, додаємо імпорт:
import org.apache.commons.lang3.StringUtils;
І все, справа в капелюсі))

1. Приклади з реального проекту

  • leftPad метод

Перший приклад взагалі зараз здається настільки дурним, що дуже добре, що мої колеги знали про StringUtils.leftPad та підказали мені. Яке було завдання: код був побудований так, що потрібно було зробити трансформацію даних, якщо вони прийшли не зовсім коректно. Очікувалося, що рядкове поле має складатися лише із цифр, тобто. якщо його довжина його 3, а значення - 1, то запис має бути "001". Тобто спочатку потрібно видалити всі прогалини, а потім замостити вже це нулями. Ще прикладів, щоб було зрозуміло суть завдання: з “12” -> “012” з “1” -> “001” І таке інше. Що я зробив? Описав це у класі LeftPadExample . Я написав метод, який це все зробить:
public static String ownLeftPad(String value) {
   String trimmedValue = value.trim();

   if(trimmedValue.length() == value.length()) {
       return value;
   }

   StringBuilder newValue = new StringBuilder(trimmedValue);

   IntStream.rangeClosed(1, value.length() - trimmedValue.length())
           .forEach(it -> newValue.insert(0, "0"));
   return newValue.toString();
}
За основу взяв ідею, що ми можемо просто отримати різницю між оригінальним та обрізаним значенням та заповнити попереду нулями. Для цього я використовував IntStream , щоб n раз зробити одну й ту саму операцію. І це точно потрібно тестувати. А ось що можна було зробити, якби я заздалегідь знав про метод StringUtils.leftPad :
public static String apacheCommonLeftPad(String value) {
   return StringUtils.leftPad(value.trim(), value.length(), "0");
}
Як бачите, коду набагато менше, при цьому ще й використовується всіма підтверджена бібліотека. Для цієї справи я створив два тести в класі LeftPadExampleTest (зазвичай, коли планують тестувати якийсь клас, створюють у такому ж пакеті, тільки в src/test/java, клас з таким же ім'ям+Test). Ці тести перевіряють один метод, щоб він правильно трансформував значення, то інший. Звичайно, тестів потрібно написати набагато більше, але тема тестування в нашому випадку не головна:
package com.github.codegymcommunity.stringutilsdemo;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Unit-level testing for LeftPadExample")
class LeftPadExampleTest {

   @DisplayName("Should transform by using ownLeftPad method as expected")
   @Test
   public void shouldTransformOwnLeftPadAsExpected() {
       //given
       String value = "1   ";
       String expectedTransformedValue = "0001";

       //when
       String transformedValue = LeftPadExample.ownLeftPad(value);

       //then
       Assertions.assertEquals(expectedTransformedValue, transformedValue);
   }

   @DisplayName("Should transform by using StringUtils method as expected")
   @Test
   public void shouldTransformStringUtilsLeftPadAsExpected() {
       //given
       String value = "1   ";
       String expectedTransformedValue = "0001";

       //when
       String transformedValue = LeftPadExample.apacheCommonLeftPad(value);

       //then
       Assertions.assertEquals(expectedTransformedValue, transformedValue);
   }

}
Можу поки що зробити кілька коментарів щодо тестів. Написані вони по JUnit 5:
  1. Тест сприйматиметься як тест, якщо він має відповідну інструкцію — @Test.
  2. Якщо в імені складно описати роботу тесту або опис довгий і його незручно читати, можна додати анотацію @DisplayName і зробити в ній нормальний опис, який буде видно при запуску тестів.
  3. При написанні тестів я використовую підхід BDD, в якому розділяю тести на логічні частини:
    1. //given - блок налаштування даних перед тестом;
    2. //when - Блок, де запускається та частина коду, яку ми тестуємо;
    3. //then - Блок, в якому проходять перевірки результатів роботи блоку when.
Якщо їх запустити, вони підтвердять, що все працює, як очікується.

  • stripStart метод

Тут мені потрібно було вирішити питання з рядком, на початку якого могли бути прогалини та коми. Після трансформації їх мало бути у новому значенні. Постановка завдання зрозуміла як ніколи. Декілька прикладів закріпить наше розуміння: ", , books" -> "books" ",,,books" -> "books" b , books" -> "b , books" Як і для випадку з leftPad, додав клас StrimStartExample , в якому два методи. Один — із власним рішенням:
public static String ownStripStart(String value) {
   int index = 0;
   List commaSpace = asList(" ", ",");
   for (int i = 0; i < value.length(); i++) {
       if (commaSpace.contains(String.valueOf(value.charAt(i)))) {
           index++;
       } else {
           break;
       }
   }
   return value.substring(index);
}
Тут ідея полягала в тому, щоб знайти той індекс, починаючи з якого вже немає прогалин і ком. Якщо їх зовсім не було на початку, індекс буде нуль. І другий - з рішенням через StringUtils :
public static String apacheCommonLeftPad(String value) {
   return StringUtils.stripStart(value, StringUtils.SPACE + COMMA);
}
Тут ми передаємо першим аргументом інформацію про те, з яким рядком працюємо, а в другому передаємо рядок, що складається із символів, які потрібно пропустити. Так само створюємо StripStartExampleTest клас:
package com.github.codegymcommunity.stringutilsdemo;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("Unit-level testing for StripStartExample")
class StripStartExampleTest {

   @DisplayName("Should transform by using stripStart method as expected")
   @Test
   public void shouldTransformOwnStripStartAsExpected() {
       //given
       String value = ", , books";
       String expectedTransformedValue = "books";

       //when
       String transformedValue = StripStartExample.ownStripStart(value);

       //then
       Assertions.assertEquals(expectedTransformedValue, transformedValue);
   }

   @DisplayName("Should transform by using StringUtils method as expected")
   @Test
   public void shouldTransformStringUtilsStripStartAsExpected() {
       //given
       String value = ", , books";
       String expectedTransformedValue = "books";

       //when
       String transformedValue = StripStartExample.apacheCommonLeftPad(value);

       //then
       Assertions.assertEquals(expectedTransformedValue, transformedValue);
   }
}

  • isEmpty метод

Цей метод, звичайно, набагато простіший, але від цього він не менш корисний. Він розширює можливості методу String.isEmpty() , який ще додається перевірка на null. Навіщо? Щоб не було NullPointerException, тобто, щоб уникнути виклику методів змінної, яка є null . Тому щоб не писати:
if(value != null && value.isEmpty()) {
   //doing something
}
Можна просто зробити так:
if(StringUtils.isEmpty(value)) {
   //doing something
}
Плюс цього в тому, що відразу видно де який метод використовуються.

2. Розбір інших методів класу StringUtils

Тепер поговоримо про ті методи, які на мій погляд теж заслуговують на увагу. Говорячи загалом про StringUtils , варто сказати, що він надає null безпечні методи-аналоги тих, що є в класі String (як у випадку з методом isEmpty ). Пройдемося ними:

  • compare метод

Такий метод є в String і буде NullPointerException, якщо в порівнянні двох рядків один з них буде null. Щоб уникнути потворних перевірок у нашому коді, можемо використовувати метод StringUtils.compare (String str1, String str2) : він повертає int як результат порівняння. Що означають ці значення? int = 0, якщо вони однакові (або обидва null). int < 0, if str1 менше, ніж str2. int > 0, якщо str1 більше, ніж str2. Також якщо подивитися на їхню документацію, то в Javadoc цього методу представлені наступні варіанти розвитку подій:
StringUtils.compare(null, null)   = 0
StringUtils.compare(null , "a")   < 0
StringUtils.compare("a", null)    > 0
StringUtils.compare("abc", "abc") = 0
StringUtils.compare("a", "b")     < 0
StringUtils.compare("b", "a")     > 0
StringUtils.compare("a", "B")     > 0
StringUtils.compare("ab", "abc")  < 0

  • contains... методи

Тут розробники утиліти розгулялися на славу. Який метод хочеш є. Я вирішив їх зібрати докупи:
  1. contains - метод, що перевіряє, чи є передбачуваний рядок всередині іншого рядка. Чим це корисно? Можна використовувати цей метод, якщо потрібно переконатися, що є слово в тексті.

    Приклади:

    StringUtils.contains(null, *)     = false
    StringUtils.contains(*, null)     = false
    StringUtils.contains("", "")      = true
    StringUtils.contains("abc", "")   = true
    StringUtils.contains("abc", "a")  = true
    StringUtils.contains("abc", "z")  = false

    Знов-таки, NPE (Null Pointer Exception) безпека є.

  2. containsAny — метод, що перевіряє, чи є хоч якийсь символ із представлених у рядку. Також корисна річ: часто доводиться таке виконувати.

    Приклади документації:

    StringUtils.containsAny(null, *)                  = false
    StringUtils.containsAny("", *)                    = false
    StringUtils.containsAny(*, null)                  = false
    StringUtils.containsAny(*, [])                    = false
    StringUtils.containsAny("zzabyycdxx", ['z', 'a']) = true
    StringUtils.containsAny("zzabyycdxx", ['b', 'y']) = true
    StringUtils.containsAny("zzabyycdxx", ['z', 'y']) = true
    StringUtils.containsAny("aba", ['z'])             = false

  3. containsIgnoreCase - корисне розширення для методу contains . Справді, щоб перевірити такий випадок без цього, доведеться перебрати кілька варіантів. А так гармонійно буде використано лише один метод.

  4. Декілька прикладів з доки:

    StringUtils.containsIgnoreCase(null, *) = false
    StringUtils.containsIgnoreCase(*, null) = false
    StringUtils.containsIgnoreCase("", "") = true
    StringUtils.containsIgnoreCase("abc", "") = true
    StringUtils.containsIgnoreCase("abc", "a") = true
    StringUtils.containsIgnoreCase("abc", "z") = false
    StringUtils.containsIgnoreCase("abc", "A") = true
    StringUtils.containsIgnoreCase("abc", "Z") = false

  5. containsNone - вже судячи з назви можна зрозуміти, що перевіряється. Рядок усередині не повинно бути. Корисна річ, безумовно. Швидкий пошук якихось неугодних символів;). У нашому телеграм-боті фільтруватимемо мати, не пройдемо повз ці кумедні методи.

    І приклади, куди ж без них:

    StringUtils.containsNone(null, *)       = true
    StringUtils.containsNone(*, null)       = true
    StringUtils.containsNone("", *)         = true
    StringUtils.containsNone("ab", '')      = true
    StringUtils.containsNone("abab", 'xyz') = true
    StringUtils.containsNone("ab1", 'xyz')  = true
    StringUtils.containsNone("abz", 'xyz')  = false

  • defaultString метод

Серія методів, які допомагають уникнути додавання зайвого іфчика у випадку, якщо рядок null і потрібно поставити якесь значення за промовчанням. Варіантів є багато на будь-який смак. Головний серед них - StringUtils.defaultString(final String str, final String defaultStr) - у випадку, якщо str дорівнює null, ми просто передамо значення defaultStr . Приклади документації:
StringUtils.defaultString(null, "NULL")  = "NULL"
StringUtils.defaultString("", "NULL")    = ""
StringUtils.defaultString("bat", "NULL") = "bat"
Його дуже зручно використовувати, коли створюєш клас POJO з даними.

  • deleteWhitespace метод

Це цікавий метод, хоч і варіантів його застосування не так уже й багато. Разом з тим, якщо такий випадок представиться, метод точно буде дуже корисним. Він видаляє всі прогалини з рядка. Де б цей пробіл не був, від нього не залишиться і сліду))) Приклади з доки:
StringUtils.deleteWhitespace(null)         = null
StringUtils.deleteWhitespace("")           = ""
StringUtils.deleteWhitespace("abc")        = "abc"
StringUtils.deleteWhitespace("   ab  c  ") = "abc"

  • endsWith метод

Говорить сам за себе. Це дуже корисний метод: він перевіряє, чи закінчується рядок пропонованим рядком чи ні. Часто таке таке потрібне. Звичайно, можна написати перевірку і самому, але використовувати вже готовий метод явно зручніше і краще. Приклади:
StringUtils.endsWith(null, null)      = true
StringUtils.endsWith(null, "def")     = false
StringUtils.endsWith("abcdef", null)  = false
StringUtils.endsWith("abcdef", "def") = true
StringUtils.endsWith("ABCDEF", "def") = false
StringUtils.endsWith("ABCDEF", "cde") = false
StringUtils.endsWith("ABCDEF", "")    = true
Як бачимо, все закінчується на порожній рядок))) Думаю, що цей приклад (StringUtils.endsWith("ABCDEF", "") = true) просто йде як бонус, адже це абсурд) Також є там і метод, який ігнорує регістр .

  • equals метод

Відмінний приклад null безпечного методу, який порівнює два рядки. Що б ми туди не поклали, відповідь буде і буде без помилок. Приклади:
StringUtils.equals(null, null)   = true
StringUtils.equals(null, "abc")  = false
StringUtils.equals("abc", null)  = false
StringUtils.equals("abc", "abc") = true
StringUtils.equals("abc", "ABC") = false
Зрозуміло, є і equalsIgnoreCase - виконується так само, тільки ігноруємо регістр. Подивимося?
StringUtils.equalsIgnoreCase(null, null)   = true
StringUtils.equalsIgnoreCase(null, "abc")  = false
StringUtils.equalsIgnoreCase("abc", null)  = false
StringUtils.equalsIgnoreCase("abc", "abc") = true
StringUtils.equalsIgnoreCase("abc", "ABC") = true

  • equalsAny метод

Ідемо далі і розширюємо метод equals . Допустимо, замість кількох перевірок на рівність, ми хочемо виконати одну. Ось для цього ми можемо передати рядок, з яким порівнюватимуть і набір рядків, якщо якийсь із них дорівнюватиме запропонованому — то буде TRUE. Передаємо рядок та колекцію рядків, щоб порівняти їх між собою (перший рядок із рядками з колекції). Важко? Ось приклади з доки, які допоможуть зрозуміти, що мають на увазі:
StringUtils.equalsAny(null, (CharSequence[]) null) = false
StringUtils.equalsAny(null, null, null)    = true
StringUtils.equalsAny(null, "abc", "def")  = false
StringUtils.equalsAny("abc", null, "def")  = false
StringUtils.equalsAny("abc", "abc", "def") = true
StringUtils.equalsAny("abc", "ABC", "DEF") = false
Також є equalsAnyIgnoreCase . І приклади для нього:
StringUtils.equalsAnyIgnoreCase(null, (CharSequence[]) null) = false
StringUtils.equalsAnyIgnoreCase(null, null, null)    = true
StringUtils.equalsAnyIgnoreCase(null, "abc", "def")  = false
StringUtils.equalsAnyIgnoreCase("abc", null, "def")  = false
StringUtils.equalsAnyIgnoreCase("abc", "abc", "def") = true
StringUtils.equalsAnyIgnoreCase("abc", "ABC", "DEF") = true

Підсумок

У результаті ми йдемо зі знанням того, що таке StringUtilsякі в ньому є корисні методи. Ну і з усвідомленням, що є такі корисні речі і не потрібно городити щоразу мабоці в місцях, де можна було б закрити питання за допомогою готового рішення. Загалом ми розібрали лише частину методів. Якщо буде бажання, я можу продовжити: там їх ще багато, і вони реально заслуговують на увагу. Якщо є ідеї, як це ще можна подати, будь ласка, пишіть, я завжди відкритий до нових ідей. Документацію до методів написано дуже якісно, ​​додано тестові приклади з результатами, що допомагає краще зрозуміти роботу методу. Тому не цураємось читання документації: вона розвіє ваші сумніви щодо функціоналу утиліти. Щоб отримати новий досвід кодингу, раджу подивитись, як роблять та пишуть утильні класи. Це буде корисно в майбутньому, тому що зазвичай на кожному проекті є свої практичні класи, і досвід їх написання стане в нагоді. Традиційно пропоную підписатися на гітхабі намій аккаунт ) Тих, хто не знає про мій проект з телеграм-ботом - ось посилання на першу статтю . Дякую всім за читання. Внизу додав кілька корисних посилань.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ