Кратко
- Зависимость: 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 удаляет из строки все пробелы.
Так уж случилось, что в свое время я обошел вниманием библиотеку Apache Commons Lang 3. Это библиотека со вспомогательными классами для работы с разными объектами. Такой себе сборник полезных методов для работы со строками, коллекциями, и так далее.
На нынешнем проекте, где мне пришлось более детально поработать со строками в переводе бизнес-логики 25-летней давности (из COBOL на Java), обнаружилось, что у меня были недостаточно глубокие знания о классе StringUtils. Поэтому приходилось создавать все самому. Что я имею в виду? То, что определенные задачи с манипуляцией со строками можно не писать самому, а воспользоваться уже готовым решением.
Чем плохо писать самому? Как минимум тем, что это больше кода, который уже давно написан. Не в меньшей степени остро стоит вопрос о тестировании того кода, который написан дополнительно. Когда мы используем библиотеку, которая зарекомендовала себя как качественную, мы ожидаем, что она уже протестирована и не нужно писать множество вариантов тестов, чтобы проверить ее.
Так уж вышло, что набор методов для работы со строкой у джавы не такой уж и большой. Реально не хватает многих методов, которые были бы полезны для работы.
Также этот класс создан для обеспечения проверок на NullPointerException.
План нашей статьи будет такой:- Как подключить?
- Примеры из моей работы: как не зная о таком полезном классе я создавал свой
велосипедкостыль. - Разбираем другие методы, которые мне показались интересными.
- Подводим итог.
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:- Тест будет восприниматься как тест, если у него есть соответствующая аннотация @Test.
- Если в имени сложно описать работу теста или же описание длинное и его неудобно читать, можно добавить аннотацию @DisplayName и сделать в ней нормальное описание, которое будет видно при запуске тестов.
- При написании тестов я использую BDD подход, в котором разделяю тесты на логические части:
- //given: блок настройки данных перед тестом;
- //when: блок, где запускается та часть кода, которую мы тестируем;
- //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... методы
Здесь разработчики утилиты разгулялись на славу. Какой хочешь метод есть. Я решил их собрать воедино: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) безопасность присутствует.
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']) = falsecontainsIgnoreCase это полезное расширение для метода 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") = falsecontainsNone: уже судя по названию можно понять, что проверяется. Строк внутри не должно быть. Полезная вещь, определенно. Быстрый поиск каких-то неугодных символов ;). В нашем телеграм-боте будем фильтровать маты, не пройдем мимо этих забавных методов.
И примеры, куда ж без них:
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 класса.Всем спасибо за чтение. Внизу добавил несколько полезных ссылок.| Полезные ссылки |
|---|
|
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ