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

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

Статья из группы Random
Всем привет, мои дорогие читатели. Я стараюсь писать о том, что реально мне интересно и что волнует в данный момент. Поэтому сегодня будет легкое чтиво, которое в будущем пригодится вам как справочник: поговорим о 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.javarushcommunity.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.javarushcommunity.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, if 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, какие в нем есть полезные методы. Ну и с осознанием, что есть вот такие полезные вещи и не нужно городить каждый раз костыли в местах, где можно было бы закрыть вопрос при помощи готового решения. В целом мы разобрали только часть методов. Если будет желание, я могу продолжить: там их еще много, и они реально заслуживают внимания. Если есть идеи, как это еще можно подать, пожалуйста, пишите — я всегда открыт к новым идеям. Документация к методам написана очень качественно, добавлены тестовые примеры с результатами, что помогает лучше понять работу метода. Поэтому не чураемся чтения документации: она развеет ваши сомнения по поводу функционала утилиты. Чтобы получить новый опыт кодинга, советую посмотреть, как делают и пишут утильные классы. Это будет полезно в будущем, так как обычно на каждом проекте есть свои утильные классы, и опыт их написания пригодится. Традиционно предлагаю подписаться на гитхабе на мой аккаунт) Тех, кто не знает о моем проекте с телеграмм-ботом — вот ссылка на первую статью. Всем спасибо за чтение. Внизу добавил несколько полезных ссылок.
Комментарии (5)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #3196841 Уровень 30
22 апреля 2023
Очень странный класс, 95% можно заменить на тернарные операторы, стримы или регулярные выражения. И там и там это займет 1 строчку кода. Я чего-то не понимаю?
fFamous Уровень 51
27 ноября 2021
А почему вместо stripStart было не сделать так?:

value.replaceAll("[, ]", "");
Сэм Фишер Уровень 27
9 мая 2021
классные материалы пишете. большое спасибо за труд!
Roman Beekeeper Уровень 35
11 марта 2021
⚡️UPDATE⚡️ Друзья, создал телеграм-канал 🤓, в котором освещаю свою писательскую деятельность и свою open-source разработку в целом. Не хотите пропустить новые статьи? Присоединяйтесь ✌️
Василий Бабин Уровень 28 Expert
13 февраля 2021
Спасибо большое, Роман! Как всегда качественный материал.