Статьи

Ссылки на методы в Java 8 накладывают дополнительные ограничения на перегрузку

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

Есть две основные причины, почему перегрузка полезна:

  1. Чтобы учесть аргументы по умолчанию
  2. Чтобы разрешить дизъюнктные альтернативные типы аргументов

Обе причины мотивированы просто для удобства пользователей API. Хорошие примеры легко найти в JDK:

Значения по умолчанию


public class Integer {
    public static int parseInt(String s) {
        return parseInt(s,10);
    }

    public static int parseInt(String s, int radix) {}
}

В приведенном выше примере первый parseInt()метод является просто вспомогательным методом для вызова второго с наиболее часто используемым основанием.

Разъединить альтернативы типа аргумента

Иногда подобное поведение может быть достигнуто с использованием различных типов параметров, которые означают схожие вещи, но не совместимы с системой типов Java. Например, при построении String:


public class String {
    public static String valueOf(char c) {
        char data[] = {c};
        return new String(data, true);
    }

    public static String valueOf(boolean b) {
        return b ? "true" : "false";
    }

    // and many more...
}

Как видите, поведение одного и того же метода оптимизируется в зависимости от типа аргумента. Это не влияет на «ощущение» метода при чтении или написании исходного кода, так как семантика двух valueOf()методов одинакова.

Другим вариантом использования этого метода является то, когда обычно используются похожие, но несовместимые типы, которые требуют удобного преобразования между собой. Как разработчик API, вы не хотите, чтобы ваш потребитель API бездействовал с такими утомительными преобразованиями. Вместо этого вы предлагаете:


public class IOUtils {
    public static void copy(InputStream input, OutputStream output);
    public static void copy(InputStream input, Writer output);
    public static void copy(InputStream input, Writer output, String encoding);
    public static void copy(InputStream input, Writer output, Charset encoding);
}

Это хороший пример, показывающий как параметры по умолчанию (необязательное кодирование), так и альтернативы типа аргумента ( OutputStreamпротив Writerили Stringпротив Charsetпредставления кодирования).

Примечание

I suspect that the union type and defaulted argument ships have sailed for Java a long time ago – while union types might be implemented as syntax sugar, defaulted arguments would be a beast to introduce into the JVM as it would depend on the JVM’s missing support for named arguments.

As displayed by the Ceylon language, these two features cover about 99% of all method overloading use-cases, which is why Ceylon can do completely without overloading – on top of the JVM!

Overloading is dangerous and unnececssary

The above examples show that overloading is essentially just a means to help humans interact with an API. For the runtime, there is no such thing as overloading. There are only different, unique method signatures to which calls are linked “statically” in byte code (give or take more recent opcodes like invokedynamic). But the point is, there’s no difference for the computer if the above methods are all called copy(), or if they had been called unambiguously m1(), m2(), m3(), and m4().

On the other hand, overloading is real in Java source code, and the compiler has to do a lot of work to find the most specific method, and otherwise apply the JLS’s complex overload resolution algorithm. Things get worse with each new Java language version. In Java 8, for instance, method references will add additional pain to API consumers, and require additional care from API designers. Consider the following example by Josh Bloch:

// Spot the bug static void pfc(List<Integer> x) { x.stream().map(Integer::toString).forEach( s -> System.out.println(s.charAt(0))); }

— Joshua Bloch (@joshbloch) July 20, 2015

You can copy-paste the above code into Eclipse to verify the compilation error (note that not-up-to-date compilers may report type inference side-effects instead of the actual error). The compilation error reported by Eclipse for the following simplification:


static void pfc(List<Integer> x) {
    Stream<?> s = x.stream().map(Integer::toString);
}

… is


Ambiguous method reference: both toString() and 
toString(int) from the type Integer are eligible

Oops!

The above expression is ambiguous. It can mean any of the following two expressions:


// Instance method:
x.stream().map(i -> i.toString());

// Static method:
x.stream().map(i -> Integer.toString(i));

As can be seen, the ambiguity is immediately resolved by using lambda expressions rather than method references. Another way to resolve this ambiguity (towards the instance method) would be to use the super-type declaration of toString() instead, which is no longer ambiguous:


// Instance method:
x.stream().map(Object::toString);

Conclusion

The conclusion here for API designers is very clear:

Method overloading has become an even more dangerous tool for API designers since Java 8

While the above isn’t really “severe”, API consumers will waste a lot of time overcoming this cognitive friction when their compilers reject seemingly correct code. One big faux-pas that is a takeaway from this example is to:

Never mix similar instance and static method overloads

And in fact, this amplifies when your static method overload overloads a name from java.lang.Object, as we’ve explained in a previous blog post.

There’s a simple reason for the above rule. Because there are only two valid reasons for overloading (defaulted parameters and incompatible parameter alternatives), there is no point in providing a static overload for a method in the same class. A much better design (as exposed by the JDK) is to have “companion classes” – similar to Scala’s companion objects. For instance:


// Instance logic
public interface Collection<E> {}
public class Object {}

// Utilities
public class Collections {}
public final class Objects {}

By changing the namespace for methods, overloading has been circumvented somewhat elegantly, and the previous problems would not have appeared.

TL;DR: Avoid overloading unless the added convenience really adds value!