Памятка: перегрузка и переопределение

Overriding — переопределение — когда подкласс подсовывает свою реализацию метода вместо реализации в суперклассе. При вызове метода выбирается наиболее «специфическая» реализация, то есть реализация в подклассе, если она есть, приоритетнее реализации в суперклассе. В Java выбор выполняемого метода при переопределении происходит динамически (в рантайме) и не зависит от того, каким типом объявлена ссылка на объект:

public class TestOverriding {
    
    private static class Parent {
        String getName() { return "Parent"; }
    }

    private static class Child extends Parent {
        @Override
        String getName() { return "Child"; }
    }

    public static void main(String[] args) {
        Parent child = new Child();
        // Prints "Child"
        System.out.println(child.getName()); 
    }
}

Overloading — перегрузка — объявление методов (или конструкторов) с одинаковыми именами, но разными сигнатурами. Пример — все конструкторы любого класса, если их больше одного. В Java выбор имплементации при перегрузке происходит статически (во время компиляции) и не зависит от типа объекта в рантайме:

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class TestOverloading {

    static void classifyCollection(Collection<?> c) {
        System.out.println("Collection");
    }

    static void classifyCollection(Set<?> s) {
        System.out.println("Set");
    }

    public static void main(String[] args) {
        Collection<?> c = new HashSet<Object>();
        // prints "Collection"
        classifyCollection(c);
    }
}

Среды разработки

Работая в предыдущей команде (GCM), я писал на Java. Я пользовался Intellij IDEA (не люблю Eclipse) с плагинами для Protobuf и чего-то ещё. Всё было хорошо, и я был доволен.

В новой команде (Spanner/Test Infrastructure) я пишу на C++, Python, Go, Java — у нас тут несколько компонентов, написанных на разных языках и обильно склеенных Protobuf-ом. Я начал с Vim, мне не понравилось, попробовал нашу собственную IDE, тоже не особо. Потом подумал, что если мне нравилась Intellij, то может понравиться и CLion, среда разработки на C++ от тех же Jetbrains. Так оно и получилось, в общем. Конечно, с CLion у меня всё не идеально, и я продолжаю разбираться и подстраивать его под себя, но ничего удобнее мне пока не известно.

Как и Intellij, CLion имеет плагины для Python, Go и Protobuf. Однако у CLion нет плагина для Java, ровно как и у Intellij нет плагина для C/C++. Поэтому я, к большому своему сожалению, не могу вести всю свою разработку ни в одном из них. И я не могу понять, почему нельзя было сделать общую IDE платформу с плагинами под разные языки (чтобы некоторые плагины, например C++ и Java, были платными). Меня лично это бы очень устроило. Кажется, что сделать такую среду было бы вполне возможно, и она даже могла быть удобной и приятной в использовании (как умеют сделать в Intellij). Я не проверял, но есть подозрение, что Eclipse как раз таким комбайном и является, что, впрочем, не делает его более привлекательным для меня.

Исключения без стек трейса в Java

Бывает, видишь в логе исключение, а стек трейса у него нет (он пустой):

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException

Одна строка и всё.

Это происходит, когда программа кидает очень много таких исключений. Сначала они генерируются как надо, с заполненным стек трейсом, и полностью попадают в лог. Потом виртуальная машина решает, что раз такие ошибки происходят очень часто, имеет смысл оптимизировать генерацию этих исключений (см. JIT). Тогда-то у них и пропадает стек трейс.

Поэтому когда вы у себя такое видите и не можете понять, откуда эти исключения вылетают, имеет смысл промотать лог назад (очень далеко назад, как правило) и поискать в том месте, где они впервые появились. И/или можно выключить эту оптимизацию с помощью следующего аргумента JVM

-XX:-OmitStackTraceInFastThrow

Если интересно, можно сымитировать подобную ситуацию простой программой:

public class Main2 {
 public static void main(String... args) {
  for (int i = 0; i < 1000000; i++) {
   try {
    int x = 0 / 0;
   } catch (Exception e) {
    if (e.getStackTrace().length == 0) {
     System.out.println("i=" + i);
     throw e;
    }
   }
  }
 }
}

У меня, например, цикл заканчивается на 12288-й итерации.

Мне не совсем понятна философия этой оптимизации. Да, исключения действительно “тяжёлые”. Но на то они и исключения, что должны происходить редко. И если в вашей программе они выкидываются очень часто, вы явно что-то делаете не так, и плохая производительность — это не главная ваша проблема. Ваша проблема в том, что вы не читали Item 57: Use exceptions only for exceptional conditions из Effective Java Блоха.

Может быть, кто-нибудь из читателей может объяснить?

Java puzzler

Отлично:

  1. Вставляем следующий код без изменений в B.java:
    class B extends A{B(Long i){
    new B(i/Long.compare(i,i));System.out.println("Win");}}
  2. Пишем что угодно в A.java
  3. Запускаем так: java -Djava.security.manager A . Security manager нужен, чтобы запретить читерство типа рефлексии.
  4. Программа должна достичь System.out.println(“Win”) в B и напечатать “Win”.

Источник. Туда пока что можно отправить своё решение взакрытую, но со временем комментарии с ответами откроются, поэтому будьте осторожны.

@VisibleForTesting не нужна

В Guava есть аннотация VisibleForTesting. Её описание из кода библиотеки:

Annotates a program element that exists, or is more widely visible than otherwise necessary, only for use in test code.

То есть предполагается, что этой аннотацией мы будем отмечать методы и поля, которые, по идее, должны были быть private, но для тестов были сделаны package-private или public.

По поводу этой аннотации я хотел бы сказать, что она, ИМХО, не нужна.

В случае, когда член нужен тесту из другого пакета, и его поэтому пришлось сделать public, это совсем другая проблема и никакие аннотации тут уже не помогут. Так что рассуждать будем о private полях, которые были сделаны package-private для тестов.

Так вот, нет ничего плохого в package-private членах. Они по-прежнему остаются недоступными для клиентов из других пакетов (это самое главное, потому что их обычно большинство). Для клиентов из того же пакета: сам факт того, что некоторое поле, метод или конструктор имеет ограниченную видимость, должен настораживать разработчика не хуже аннотации. Конечно, наверняка найдётся кто-нибудь, кого это не остановит от доступа к члену или методу не по назначению. Но, ей-богу, оно того не стоит:

До:

@VisibleForTesting
static final double AVOGADROS_NUMBER = 6.02214199e23;
@VisibleForTesting
static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
@VisibleForTesting
static final double ELECTRON_MASS = 9.10938188e-31;

После:

static final double AVOGADROS_NUMBER = 6.02214199e23;
static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
static final double ELECTRON_MASS = 9.10938188e-31;

И ещё. Описание к аннотации намекает, что некоторые члены могут быть не только раскрыты, но и созданы исключительно для тестирования. Так вот, я не Кент Бек, конечно, но вроде бы:

  1. продакшн код не должен испытывать никаких структурных изменений, сделанных исключительно для тестирования;

  2. если некоторый класс ну никак не протестировать без введения туда новых членов, то скорее всего беда с качеством кода. За подробностями можно обратиться к «Чистому коду».

А ещё я иногда вижу такое:

/* package */ class SomeClass implements SomeInterface  {

Для кого этот комментарий вообще?

Несогласные призываются в комменты.

Публичность и опубликованность

Статья Мартина Фаулера Public versus Published Interfaces. В целом ничего очень нового или исключительно интересного, но заставила задуматься, почему различие public-published на уровне языка реализовано далеко не во всех языках.

Суть:

Публичный интерфейс, public interface — методы класса, которые могут быть использованы любыми другими объектами.

Опубликованный интерфейс, published interface — то, чем пользуются внешние клиенты нашей, например, библиотеки. Они вызывают методы и наследуются своими классами.

В Java нет способа явно отличить одно от другого. То есть если наша библиотека состоит из нескольких пакетов (что почти всегда), то публичные интерфейсы будут опубликованы и доступны внешним пользователям, даже если мы их делали только для внутреннего применения. Избежать этого никак, разве что в документации указывать пользователям, какие именно компоненты можно использовать.

Опубликованный интерфейс изменить трудно, если код внешних пользователей, который, очевидно, вне нашего контроля, его использует. Иногда можно упростить себе и другим жизнь:

  • опубликовывать как можно позже и как можно меньше;
  • действительно опубликовывать интерфейсы только когда они уже достаточно отточены;
  • не изменять сигнатуры методов, а добавлять новые методы.

Об анализе хеш-функций

Тут относительно недавно пробегали две статьи, в которых авторы затрагивали тему анализа хеш-функций с целью определить, какая лучше: Заметки о реализации hashCode() в Java и Changes to String internal representation made in Java 1.7.0_06 (перевод). В обеих статьях анализ проводился очень простой: генерировалось большое множество объектов нужного типа, для них вычислялись хеш-коды и среди хеш-кодов искалось количество коллизий (совпадений).

Я считаю, что такой анализ недостаточно хорош. Главное применение хеш-функций в Java — это в основанных на хешировании коллекциях, куда объекты складываются, распределяясь по ячейкам (бакетам, bucket) согласно своим хеш-кодам. Количество ячеек ограниченно, поэтому чтобы определить, в какой из них место объекту, берётся остаток от деления хеш-кода этого объекта на количество ячеек, которое из соображений производительности выбирается из степеней двойки. Так вот, равномерность распределения объектов по бакетам играет решающую роль в производительности коллекции, а значит очень важно, помимо количества коллизий, анализировать и её.

Легко показать, что малое число коллизий хеш-функции не всегда коррелирует с равномерным распределением объектов внутри хеш-таблицы. Достаточно взять любую хорошую хеш-функцию f(x) и получить из неё новую: h(x)=2*f(x). Количество коллизий у новой функции будет ровно таким же (для простоты забудем про переполнение), но при этом все её значения будут чётными, и распределение получится очень неравномерным.

Чистый код

Дочитал наконец «Чистый код» Роберта Мартина (Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin). Книга является сборником советов и правил написания хорошего и улучшения плохого кода. Она может быть интересна в первую очередь программистам на Java.

Миша про эту книгу говорил, что она какая-то капитанская (то есть в ней написаны очевидные вещи — от Капитана Очевидность). Так оно на самом деле и есть, но это не делает её плохой или бесполезной. По мере прочтения я несколько раз осознавал, что некоторые правила хорошего кода я нарушал и продолжаю нарушать. Поэтому считаю, что, несмотря на малость полученных знаний, перечитывать что-нибудь подобное время от времени очень даже полезно.

Ещё хотел заметить, что Appendix A: Concurrency II совсем не раскрывает темы чистого многопоточного кода. Такое ощущение, что это выдранная глава про concurrency из учебника Java для начинающих.

Понравилось заключение к одной из глав:

It is not enough for code to work. Code that works is often badly broken. Programmers who satisfy themselves with merely working code are behaving unprofessionally. They may fear that they don’t have time to improve the structure and design of their code, but I disagree. Nothing has a more profound and long-term degrading effect upon a develop- ment project than bad code. Bad schedules can be redone, bad requirements can be rede- fined. Bad team dynamics can be repaired. But bad code rots and ferments, becoming an inexorable weight that drags the team down. Time and time again I have seen teams grind to a crawl because, in their haste, they created a malignant morass of code that forever thereafter dominated their destiny.

Of course bad code can be cleaned up. But it’s very expensive. As code rots, the mod- ules insinuate themselves into each other, creating lots of hidden and tangled dependen- cies. Finding and breaking old dependencies is a long and arduous task. On the other hand, keeping code clean is relatively easy. If you made a mess in a module in the morning, it is easy to clean it up in the afternoon. Better yet, if you made a mess five minutes ago, it’s very easy to clean it up right now.

So the solution is to continuously keep your code as clean and simple as it can be. Never let the rot get started.