Наверное, любому программисту доводилось видеть код, пестрящий большим количеством повторов и реализации «низкоуровневых» действий прямо посреди бизнес-логики. Например, посреди метода, печатающего отчёт, может оказаться такой фрагмент кода, конкатерирующий строки:
StringBuilder sb = new StringBuilder();
for (Iterator<String> i = debtors.iterator(); i.hasNext();) {
if (sb.length() != 0) {
sb.append(", ");
}
sb.append(i.next());
}
out.println("Debtors: " + sb.toString());
Понятно, что этот код мог бы быть более прямолинейным, например, в Java 8 можно написать так:
out.println("Debtors: " + String.join(", ", debtors));
Вот так сразу гораздо понятнее, что происходит. Google Guava – это набор open-source библиотек для Java, помогающий избавиться от подобных часто встречающихся шаблонов кода. Поскольку Guava появилась задолго до Java 8, в Guava тоже есть способ конкатенации строк: Joiner.on(", ").join(debtors).
Очень базовые полезности
Давайте рассмотрим простой класс, реализующий стандартный набор базовых методов Java. Предлагаю не вникать особо в реализацию методов hashCode, equals, toString и compareTo (первые три из них я просто сгенерировал в Eclipse) дабы не тратить время впустую, а просто посмотреть на объём кода.
class Person implements Comparable<Person> {
private String lastName;
private String middleName;
private String firstName;
private int zipCode;
// constructor, getters and setters are omitted
@Override
public int compareTo(Person other) {
int cmp = lastName.compareTo(other.lastName);
if (cmp != 0) {
return cmp;
}
cmp = middleName.compareTo(other.middleName);
if (cmp != 0) {
return cmp;
}
cmp = firstName.compareTo(other.firstName);
if (cmp != 0) {
return cmp;
}
return Integer.compare(zipCode, other.zipCode);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (firstName == null) {
if (other.firstName != null)
return false;
} else if (!firstName.equals(other.firstName))
return false;
if (lastName == null) {
if (other.lastName != null)
return false;
} else if (!lastName.equals(other.lastName))
return false;
if (middleName == null) {
if (other.middleName != null)
return false;
} else if (!middleName.equals(other.middleName))
return false;
if (zipCode != other.zipCode)
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
result = prime * result + ((middleName == null) ? 0 : middleName.hashCode());
result = prime * result + zipCode;
return result;
}
@Override
public String toString() {
return "Person [lastName=" + lastName + ", middleName=" + middleName
+ ", firstName=" + firstName + ", zipCode=" + zipCode + "]";
}
}
Теперь посмотрим на похожий код, использующий Guava и новые методы из Java 8:
class Person implements Comparable<Person> {
private String lastName;
private String middleName;
private String firstName;
private int zipCode;
// constructor, getters and setters are omitted
@Override
public int compareTo(Person other) {
return ComparisonChain.start()
.compare(lastName, other.lastName)
.compare(firstName, other.firstName)
.compare(middleName, other.middleName, Ordering.natural().nullsLast())
.compare(zipCode, other.zipCode)
.result();
}
@Override
public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Person other = (Person) obj;
return Objects.equals(lastName, other.lastName)
&& Objects.equals(middleName, other.middleName)
&& Objects.equals(firstName, other.firstName)
&& zipCode == other.zipCode;
}
@Override
public int hashCode() {
return Objects.hash(lastName, middleName, firstName, zipCode);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.omitNullValues()
.add("lastName", lastName)
.add("middleName", middleName)
.add("firstName", firstName)
.add("zipCode", zipCode)
.toString();
}
}
Как видим, код стал чище и лаконичнее. Здесь используются MoreObjects и ComparisonChain из Guava и класс Objects из Java 8. Если вы используете Java 7 или более старую версию, то можете воспользоваться классом Objects из Guava – в нём есть методы hashCode и equal, аналогичные использованным методам hash и equals из класса java.lang.Objects. Раньше toStringHelper тоже находился в классе Objects, но с появлением Java 8 в Guava 18 в классе Objects навесили меточку @Deprecated на все методы, а те методы, аналогов которым которых нет в Java 8, перенесли в MoreObjects, чтобы не было конфликта имён – Guava развивается, а её разработчики не стесняются избавляться от устаревшего кода.
Замечу, что эта версия класса немного отличается от изначальной: я предположил, что отчество может быть не заполнено, в таком случае в результате toString мы его не увидим, а compareTo будет считать, что личности без отчества должны идти после тех, у кого есть отчество (при этом упорядочение происходит сначала по фамили и имени, а только потом по отчеству).
Другим примером весьма базовых полезностей могут служить предусловия. По какой-то причине в Java есть только Objects.requireNotNull (начиная с Java 7).
Кратко о предусловиях:
Имя метода в классе Preconditions | Генерируемое исключение |
---|---|
checkArgument(boolean) | IllegalArgumentException |
checkNotNull(T) | NullPointerException |
checkState(boolean) | IllegalStateException |
checkElementIndex(int index, int size) | IndexOutOfBoundException |
checkPositionIndex(int index,int size) | IndexOutOfBoundException |
Зачем они нужны, можно прочитать на сайте Oracle.
Новые коллекции
Частенько бывает, что можно увидеть подобного рода код посреди бизнес-логики:
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
Integer count = counts.get(word);
if (count == null) {
counts.put(word, 1);
} else {
counts.put(word, count + 1);
}
}
Или такой код:
List<String> values = map.get(key);
if (values == null) {
values = new ArrayList<>();
map.put(key, values);
}
values.add(value);
(в последнем отрывке входными данными являются map, key и value). Эти два примера демонстрируют работу с коллекциями, когда в коллекциях содержатся изменяеные данные (в данном случае числа и списки соответственно). В первом случае отображение (map) по-сути описывает мультимножество, т.е. множество с повторяющимися элементами, а во втором случае отображение является мультиотображением. Такие абстракии есть в Guava. Давайте перепишем примеры с использованием этих абстракций:
Multiset<String> counts = HashMultiset.create();
for (String word : words) {
counts.add(word);
}
и
map.put(key, value);
(здесь map – это Multimap<String, String>). Замечу, что Guava позволяет настраивать поведение таких мультиотображений – например, мы можем хотеть, чтобы наборы значений хранились как множества, а можем захотеть списки, для самого же отображения мы можем захотеть связанный список, хэш или дерево – все нужные реализации в Guava имеются. Table – коллекция, избавляющая от аналогичного дублированая кода, но уже на случай хранения отображений внутри отображений. Вот примеры новых коллекций, упрощающих жизнь:
Multiset | “Множество”, которое может иметь дубликаты |
Multimap | “Отображение”, которое может иметь дубликаты |
BiMap | Поддерживает “обратное отображение” |
Table | Связывает упорядоченную пару ключей со значением |
ClassToInstanceMap | Отображает тип на экземпляр этого типа (избавляет от приведений типов) |
RangeSet | Набор диапазонов |
RangeMap | Набор отображений непересекающихся диапазонов на ненулевые значения |
Декораторы для коллекций
Для создания декораторов к коллекциям – и к тем, что уже есть в Java Collections Framework, и к тем, что определены в Guava – имеются соответствующие классы, например ForwardingList, ForwardingMap, ForwardingMiltiset.
Неизменяемые коллекции
Также в Guava имеются неизменяемые коллекции; возможно, они не связаны напрямую с чистым кодом, но заметно упрощают отладку и взаимодействие между разными частями приложения. Они:
- безопасны для использования в “недружественном коде”;
- могут сохранить время и память, поскольку не ориентируются на возможность изменения (анализ показал, что все неизенные коллекции эффективнее своих аналогов);
- могут быть использованными как константы, и можно ожидать, что они точно не будут изменены.
Здесь есть положительные отличия по сравнению с методами Collections.unmodifiableКонкретнаяКоллекция, которые создают обёртки, благодаря чему можно ожидать, что коллекция неизменна только если на неё больше нет ссылок; коллекция оставляет накладные расходы на возможность изменения как по скорости, так и по памяти.
Пара простых примеров:
public static final ImmutableSet<String> COLOR_NAMES = ImmutableSet.of(
"red",
"green",
"blue");
class Foo {
final ImmutableSet<Bar> bars;
Foo(Set<Bar> bars) {
this.bars = ImmutableSet.copyOf(bars); // defensive copy!
}
}
Реализация итераторов
PeekingIterator | Просто оборачивает итератор, добавляя к нему метод peek() для получения значения следующего элемента. Создаётся с помощью вызова Iterators.peekingIterator(Iterator) |
AbstractIterator | Избавляет от необходимости реализовывать все методы итератора – достаточно только реализовать protected T computeNext() |
AbstractSequentialIterator | Аналогичен предыдущему, но вычисляет следующий элемент на основе предыдущего: нужно реализовать метод protected T computeNext(T previous) |
Функциональные и утилиты для коллекций
Guava предоставляет такие интерфейсы, как Function<A, R> и Predicate, и утилитные классы Functions, Predicates, FluentIterable, Iterables, Lists, Sets и другие. Напоминаю, что Guava появилась задолго до Java 8, и поэтому в ней неизбежно появились Optional и интерфейсы Function<A, R> и Predicate, которые, впрочем, полезны только в ограниченных случаях, потому что без лямбд функциональный код с предикатами и функциями в большинстве случаев будет гораздо более грамоздким, нежели обычный императивный, но в некоторых случаях он позволяет сохранить лаконичность. Простой пример:
Predicate<MyClass> nonDefault = not(equalTo(DEFAULT_VALUE));
Iterable<String> strings1 = transform(filter(iterable, nonDefault), toStringFunction());
Iterable<String> strings2 = from(iterable).filter(nonDefault).transform(toStringFunction());
Здесь импортированы статические методы из Functions (toStringFunction), Predicates (not, equalTo), Iterables (transform, filter) и FluentIterable (from). В первом случае используются статические методы Iterable, чтобы сконструировать результат, во втором – FluentIterable.
Ввод/вывод
Для абстрагирования байтовых и символьных потоков определены такие абстрактные классы, как ByteSource, ByteSink, CharSoure и CharSink. Создаются они как правило с помощью фасадов Resources и Files. Также имеется немалый набор методов для работы с потоками ввода и вывода, такие как преобразование, считывание, копирование и конкатенация (см. классы CharSource, ByteSource, ByteSink). Примеры:
// Read the lines of a UTF-8 text file
ImmutableList<String> lines = Files.asCharSource(file, Charsets.UTF_8).readLines();
// Count distinct word occurrences in a file
Multiset<String> wordOccurrences = HashMultiset.create(
Splitter.on(CharMatcher.WHITESPACE)
.trimResults()
.omitEmptyStrings()
.split(Files.asCharSource(file, Charsets.UTF_8).read()));
// SHA-1 a file
HashCode hash = Files.asByteSource(file).hash(Hashing.sha1());
// Copy the data from a URL to a file
Resources.asByteSource(url).copyTo(Files.asByteSink(file));
Обо всём помаленьку
Lists | Создание различный видов списков, в т.ч. Lists.newCopyOnWriteArrayList(iterable), Lists.reverse(list) /* view! /, Lists.transform(fromList, function) /* lazy view! */ |
Sets | Преобразование из Map<SomeClass, Boolean> в Set<SomeClass> (view!), работа со множествами в математическом смысле (пересечение, объединение, разность) |
Iterables | Простые методы типа any, all, contains, concat, filter, find, limit, isEmpty, size, toArray, transform. По какой-то причине в Java 8 многие подобные методы относятся только к коллекциям, но не к Iterable в общем. |
Bytes, Ints, UnsignedInteger и т.д. | Работа с беззнаковыми числами и массивами примитивных типов (соответствующие утилитные классы есть для каждого примитивного типа). |
ObjectArrays | По-сути только два вида методов – конкатенация массивов (по какой-то причине её нет в стандартной библиотеке Java) и создание массовов по заданному классу или классу массива (почему-то в библиотеке Java есть только аналогичный метод для копирования). |
Joiner, Splitter | Гибкие классы для объединения или нарезация строк из или в Iterable, List или Map. |
Strings, MoreObjects | Из неупомянутых – крайне частоиспользуемые методы Strings.emptyToNull(String), Strings.isNullOrEmpty(String), Strings.nullToEmpty(String) и MoreObjects.firstNonNull(T, T) |
Closer, Throwables | Эмуляция try-with-resources, multi-catch (полезно только для Java 6 и старее), работа с трассировкой стека и перекидывание исключений. |
com.google.common.net | Названия классов говорят сами за себя: InternetDomainName, InetAddresses, HttpHeaders, MediaType, UrlEscapers |
com.google.common.html и com.google.common.xml | HtmlEscapers и XmlEscapers |
Range | Диапазон. |
EventBus | Мощная реализация паттерна издатель-подписчик. В EventBus регистрируются подписчики, “реагирующие” методы которых помечены аннотацией, а при вызове какого-либо события EventBus находит подписчиков, способных воспринимать данный вид событий, и уведомляет их о событии. |
IntMath, LongMath, BigIntegerMath, DoubleMath | Множество полезных функций для работы с числами. |
ClassPath | В Java нет кроссплатформенного способа просматривать классы на classpath. А Guava предоставляет возможность пройтись по классам пакета или проекта. |
TypeToken | Благодаря стиранию типов мы не можем манипулировать обобщёнными типами во время исполнения программы. TypeToken позволяет манипулировать такими типами. |
Ещё примеры
Хеширование:
HashFunction hf = Hashing.md5();
HashCode hc = hf.newHasher()
.putLong(id)
.putString(name, Charsets.UTF_8)
.putObject(person, personFunnel)
.hash();
Кэширование:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(MY_LISTENER)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
});
Динамический прокси:
Foo foo = Reflection.newProxy(Foo.class, invocationHandler)
Для создания динамического прокси без Guava обычно пишется такой код:
Foo foo = (Foo) Proxy.newProxyInstance(
Foo.class.getClassLoader(),
new Class<?>[] {Foo.class},
invocationHandler);
На этом всё, читаемого вам кода.
Автор: B1-66ER