Наверное, любому программисту доводилось видеть код, пестрящий большим количеством повторов и реализации «низкоуровневых» действий прямо посреди бизнес-логики. Например, посреди метода, печатающего отчёт, может оказаться такой фрагмент кода, конкатерирующий строки:
Понятно, что этот код мог бы быть более прямолинейным, например, в Java 8 можно написать так:
Вот так сразу гораздо понятнее, что происходит. Google Guava – это набор open-source библиотек для Java, помогающий избавиться от подобных часто встречающихся шаблонов кода. Поскольку Guava появилась задолго до Java 8, в Guava тоже есть способ конкатенации строк: Joiner.on(", ").join(debtors).
Давайте рассмотрим простой класс, реализующий стандартный набор базовых методов Java. Предлагаю не вникать особо в реализацию методов hashCode, equals, toString и compareTo (первые три из них я просто сгенерировал в Eclipse) дабы не тратить время впустую, а просто посмотреть на объём кода.
Теперь посмотрим на похожий код, использующий Guava и новые методы из Java 8:
Как видим, код стал чище и лаконичнее. Здесь используются 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).
Кратко о предусловиях:
Зачем они нужны, можно прочитать на сайте Oracle.
Частенько бывает, что можно увидеть подобного рода код посреди бизнес-логики:
Или такой код:
(в последнем отрывке входными данными являются map, key и value). Эти два примера демонстрируют работу с коллекциями, когда в коллекциях содержатся изменяеные данные (в данном случае числа и списки соответственно). В первом случае отображение (map) по-сути описывает мультимножество, т.е. множество с повторяющимися элементами, а во втором случае отображение является мультиотображением. Такие абстракии есть в Guava. Давайте перепишем примеры с использованием этих абстракций:
и
(здесь map – это Multimap<String, String>). Замечу, что Guava позволяет настраивать поведение таких мультиотображений – например, мы можем хотеть, чтобы наборы значений хранились как множества, а можем захотеть списки, для самого же отображения мы можем захотеть связанный список, хэш или дерево – все нужные реализации в Guava имеются. Table – коллекция, избавляющая от аналогичного дублированая кода, но уже на случай хранения отображений внутри отображений. Вот примеры новых коллекций, упрощающих жизнь:
Для создания декораторов к коллекциям – и к тем, что уже есть в Java Collections Framework, и к тем, что определены в Guava – имеются соответствующие классы, например ForwardingList, ForwardingMap, ForwardingMiltiset.
Также в Guava имеются неизменяемые коллекции; возможно, они не связаны напрямую с чистым кодом, но заметно упрощают отладку и взаимодействие между разными частями приложения. Они:
Здесь есть положительные отличия по сравнению с методами Collections.unmodifiableКонкретнаяКоллекция, которые создают обёртки, благодаря чему можно ожидать, что коллекция неизменна только если на неё больше нет ссылок; коллекция оставляет накладные расходы на возможность изменения как по скорости, так и по памяти.
Пара простых примеров:
Guava предоставляет такие интерфейсы, как Function<A, R> и Predicate, и утилитные классы Functions, Predicates, FluentIterable, Iterables, Lists, Sets и другие. Напоминаю, что Guava появилась задолго до Java 8, и поэтому в ней неизбежно появились Optional и интерфейсы Function<A, R> и Predicate, которые, впрочем, полезны только в ограниченных случаях, потому что без лямбд функциональный код с предикатами и функциями в большинстве случаев будет гораздо более грамоздким, нежели обычный императивный, но в некоторых случаях он позволяет сохранить лаконичность. Простой пример:
Здесь импортированы статические методы из Functions (toStringFunction), Predicates (not, equalTo), Iterables (transform, filter) и FluentIterable (from). В первом случае используются статические методы Iterable, чтобы сконструировать результат, во втором – FluentIterable.
Для абстрагирования байтовых и символьных потоков определены такие абстрактные классы, как ByteSource, ByteSink, CharSoure и CharSink. Создаются они как правило с помощью фасадов Resources и Files. Также имеется немалый набор методов для работы с потоками ввода и вывода, такие как преобразование, считывание, копирование и конкатенация (см. классы CharSource, ByteSource, ByteSink). Примеры:
Хеширование:
Кэширование:
Динамический прокси:
Для создания динамического прокси без Guava обычно пишется такой код:
На этом всё, читаемого вам кода.
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);
На этом всё, читаемого вам кода.
