Чистый код с Google Guava

в 11:30, , рубрики: guava, java, чистый код

Наверное, любому программисту доводилось видеть код, пестрящий большим количеством повторов и реализации «низкоуровневых» действий прямо посреди бизнес-логики. Например, посреди метода, печатающего отчёт, может оказаться такой фрагмент кода, конкатерирующий строки:

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js