StringUtils это утилитный класс из библиотеки Apache Commons Lang 3 (пакет org.apache.commons.lang3) с готовыми методами для работы со строками. Главное его свойство: методы безопасны для null, то есть не бросают NullPointerException, а возвращают разумный результат. Подключается одной зависимостью Maven, после чего вместо самописных проверок и циклов можно вызывать isEmpty, leftPad, stripStart, contains, defaultString и десятки других методов (javadoc класса).

Кратко

  • Зависимость: org.apache.commons:commons-lang3, импорт org.apache.commons.lang3.StringUtils. Актуальную версию смотрите на сайте проекта Apache Commons Lang.
  • StringUtils.isEmpty(str) возвращает true и для null, и для пустой строки, а isBlank еще и для строки из одних пробелов.
  • leftPad(str, size, padStr) дополняет строку слева до нужной длины: вместо цикла или StringBuilder получается одна строка кода.
  • stripStart(str, stripChars) убирает в начале строки любые перечисленные символы, а не только пробелы.
  • compare, equals, contains, endsWith это null-безопасные двойники методов String: там, где обычный вызов упал бы с NullPointerException, StringUtils вернет false, true или число.
  • defaultString подставляет значение по умолчанию вместо null, а deleteWhitespace удаляет из строки все пробелы.
Всем привет, мои дорогие читатели. Я стараюсь писать о том, что реально мне интересно и что волнует в данный момент. Поэтому сегодня будет легкое чтиво, которое в будущем пригодится вам как справочник: поговорим о StringUtils. Обложка статьи о классе StringUtils из библиотеки Apache Commons Lang 3Так уж случилось, что в свое время я обошел вниманием библиотеку 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} это версия библиотеки: актуальную можно посмотреть на сайте проекта Apache Commons Lang. Далее, чтобы импортировать в каком-то классе, добавляем импорт:

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, добавил класс StripStartExample, в котором два метода. Один с собственным решением:

public static String ownStripStart(String value) {
   int index = 0;
   List<String> 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 apacheCommonStripStart(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.apacheCommonStripStart(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
}
Обратите внимание на условие: StringUtils.isEmpty(value) это именно value == null || value.isEmpty(), то есть метод возвращает true и для null, и для пустой строки. Рядом есть StringUtils.isBlank(value): он считает пустой и строку из одних пробелов. Плюс этих методов в том, что сразу видно, где какой метод используется.

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. Действительно, чтобы проверить такой случай без этого метода, придется перебрать несколько вариантов. А так гармонично будет использован только один метод.

    Несколько примеров из доки:

    
           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
    

  4. 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 и их аналоги в String

Чтобы было к чему возвращаться, вот разобранные методы в одной таблице:
Метод StringUtils Что делает Что будет с null Аналог в String
isEmpty(cs) проверяет, что строка пустая true isEmpty() упадет с NullPointerException
isBlank(cs) проверяет, что строка пустая или состоит из пробелов true isBlank() с Java 11, но без защиты от null
leftPad(str, size, pad) дополняет строку слева до заданной длины вернет null нет
stripStart(str, chars) убирает указанные символы в начале строки вернет null stripLeading() с Java 11, только пробелы
compare(s1, s2) сравнивает строки, возвращает int null считается меньше любой строки compareTo() упадет с NullPointerException
contains(cs, search) ищет подстроку false contains() упадет с NullPointerException
containsIgnoreCase(cs, search) то же без учета регистра false нет
equals(cs1, cs2) сравнивает строки на равенство два null равны equals() на null-ссылке упадет
equalsAny(s, s1, s2, ...) сравнивает строку сразу с несколькими см. примеры выше нет
defaultString(str, default) подставляет значение по умолчанию вернет значение по умолчанию Objects.requireNonNullElse() с Java 9
deleteWhitespace(str) удаляет все пробелы из строки вернет null нет
endsWith(str, suffix) проверяет окончание строки два null дают true endsWith() упадет с NullPointerException

Итог

В итоге мы уходим со знанием того, что такое StringUtils, какие в нем есть полезные методы. Ну и с осознанием, что есть вот такие полезные вещи и не нужно городить каждый раз костыли в местах, где можно было бы закрыть вопрос при помощи готового решения. В целом мы разобрали только часть методов. Если будет желание, я могу продолжить: там их еще много, и они реально заслуживают внимания. Если есть идеи, как это еще можно подать, пожалуйста, пишите: я всегда открыт к новым идеям. Документация к методам написана очень качественно, добавлены тестовые примеры с результатами, что помогает лучше понять работу метода. Поэтому не чураемся чтения документации: она развеет ваши сомнения по поводу функционала утилиты. Справедливости ради: часть пробелов в этом списке Java закрыла сама. С Java 11 у String есть isBlank(), strip(), stripLeading() и repeat(). Но они по-прежнему падают с NullPointerException, если вызвать их у null-ссылки, так что ради null-безопасности StringUtils остается полезным. Чтобы получить новый опыт кодинга, советую посмотреть, как делают и пишут утильные классы. Это будет полезно в будущем, так как обычно на каждом проекте есть свои утильные классы, и опыт их написания пригодится. Традиционно предлагаю подписаться на гитхабе на мой аккаунт) Тех, кто не знает о моем проекте с телеграмм-ботом: вот ссылка на первую статью.

Вопросы и ответы

Что такое StringUtils в Java?

Это утилитный класс из библиотеки Apache Commons Lang 3 (пакет org.apache.commons.lang3), который собирает готовые методы для работы со строками: проверки на пустоту, дополнение и обрезку, поиск подстрок, сравнение. Почти все его методы null-безопасны: вместо NullPointerException они возвращают true, false, null или число, поэтому из кода уходят ручные проверки на null.

Как подключить StringUtils в проект?

Через зависимость org.apache.commons:commons-lang3 в pom.xml (Maven) или в build.gradle (Gradle), а затем импортом import org.apache.commons.lang3.StringUtils;. Актуальную версию библиотеки проще всего посмотреть на сайте проекта Apache Commons Lang: класс лежит в том же артефакте, отдельно ставить ничего не нужно.

Чем StringUtils.isEmpty отличается от String.isEmpty?

str.isEmpty() проверяет только длину строки и падает с NullPointerException, если str равен null. StringUtils.isEmpty(str) возвращает true и для пустой строки, и для null. То есть эта запись равносильна ручной проверке str == null || str.isEmpty(), а не str != null && str.isEmpty().

Чем isEmpty отличается от isBlank?

isEmpty считает непустой любую строку длиной больше нуля, даже из одних пробелов: StringUtils.isEmpty(" ") это false. isBlank проверяет еще и пробелы: StringUtils.isBlank(" ") это true. Для данных, пришедших от пользователя или из внешней системы, обычно нужен именно isBlank.

Нужен ли StringUtils, если в Java 11 появились isBlank, strip и repeat?

Новые методы String закрывают часть задач, но они вызываются у объекта, поэтому у null-ссылки все так же дают NullPointerException. StringUtils принимает строку аргументом и отвечает даже на null, плюс у него есть то, чего в String нет: leftPad, stripStart с произвольным набором символов, defaultString, equalsAny.

Какие методы StringUtils чаще всего нужны на практике?

isEmpty и isBlank для проверок входных данных, defaultString вместо if с подстановкой значения по умолчанию, leftPad и rightPad для форматирования кодов и номеров, stripStart и strip для чистки мусорных символов, equalsIgnoreCase и containsIgnoreCase для сравнения без учета регистра. Полный список с примерами есть в javadoc класса.Всем спасибо за чтение. Внизу добавил несколько полезных ссылок.