Перегрузка в Java и множественная диспетчеризация - PullRequest
23 голосов
/ 18 марта 2012

У меня есть коллекция (или список или список массивов), в который я хочу поместить как строковые, так и двойные значения.Я решил сделать это коллекцией объектов и использовать перегрузку полиморфизма OND, но я сделал что-то не так.

Я запускаю небольшой тест:

public class OOP {
    void prova(Object o){
        System.out.println("object");
    }

    void prova(Integer i){
    System.out.println("integer");
    }

    void prova(String s){
        System.out.println("string");
    }

    void test(){
        Object o = new String("  ");
        this.prova(o); // Prints 'object'!!! Why?!?!?
    }

    public static void main(String[] args) {
        OOP oop = new OOP();
        oop.test(); // Prints 'object'!!! Why?!?!?
    }
}

В тесте похоже на тип аргументаопределяется во время компиляции, а не во время выполнения.Почему это так?

Этот вопрос относится к:

Полиморфизм против переопределения против перегрузки
Попытайтесь описать полиморфизм как можно проще

РЕДАКТИРОВАТЬ:

Хорошо, метод для вызова определяется во время компиляции.Есть ли способ избежать использования оператора instanceof?

Ответы [ 6 ]

19 голосов
/ 18 марта 2012

Этот пост секунд ответ voo, и дает подробную информацию о / альтернативы позднего связывания.

Общие JVM используют только одну отправку : тип времени выполнения рассматривается только для объекта-получателя; для параметров метода рассматривается статический тип. Эффективная реализация с оптимизацией довольно проста, если использовать таблицы методов (которые похожи на виртуальные таблицы C ++). Вы можете найти подробную информацию, например, в HotSpot Wiki .

Если вы хотите многократная отправка для ваших параметров, взгляните на

  • заводной . Но, насколько мне известно, это устаревшая медленная реализация множественной диспетчеризации (см., Например, это сравнение производительности ), например, без кеширования.
  • clojure , но это сильно отличается от Java.
  • MultiJava , который предлагает множественную диспетчеризацию для Java. Кроме того, вы можете использовать
    • this.resend(...) вместо super(...) для вызова наиболее специфического переопределенного метода включающего метода;
    • диспетчеризация значения (пример кода ниже).

Если вы хотите придерживаться Java , вы можете

  • перепроектируйте ваше приложение, переместив перегруженные методы по более тонкой иерархии классов. Пример приведен в Effective Java Джоша Блоха , Item 41 (Используйте разумную перегрузку);
  • использовать некоторые шаблоны проектирования, такие как Стратегия, Посетитель, Наблюдатель. Они часто могут решить те же проблемы, что и множественная диспетчеризация (т. Е. В таких ситуациях у вас есть тривиальные решения для этих шаблонов с использованием множественной диспетчеризации).

Значение диспетчеризации:

class C {
  static final int INITIALIZED = 0;
  static final int RUNNING = 1;
  static final int STOPPED = 2;
  void m(int i) {
    // the default method
  }
  void m(int@@INITIALIZED i) {
    // handle the case when we're in the initialized `state'
  }
  void m(int@@RUNNING i) {
    // handle the case when we're in the running `state'
  }
  void m(int@@STOPPED i) {
    // handle the case when we're in the stopped `state'
  }
}
12 голосов
/ 18 марта 2012

Что вам нужно, так это двойная или более общая многократная отправка , то, что фактически реализовано на других языках (на ум приходит общий язык)

Предположительно, основная причина, по которой у Java нетэто потому, что это приводит к снижению производительности, потому что разрешение перегрузки должно выполняться во время выполнения, а не во время компиляции.Обычный способ обойти это - шаблон посетителя - довольно уродливо, но это так.

3 голосов
/ 18 марта 2012

При вызове метода, который перегружен, Java выбирает наиболее строгий тип на основе типа переменной , переданной функции.Он не использует тип фактического экземпляра.

2 голосов
/ 25 декабря 2017

Старый вопрос, но нет ответа, предоставляет конкретное решение в Java для четкого решения проблемы.
На самом деле не простой, но очень интересный вопрос. Вот мой вклад.

Хорошо, метод, который нужно вызвать, определяется во время компиляции. Есть ли Обходной путь, чтобы избежать использования оператора instanceof?

Как сказано в превосходном ответе @DaveFar, Java поддерживает только метод единой отправки.
В этом режиме диспетчеризации компилятор ограничивает метод для вызова сразу после компиляции, полагаясь на объявленные типы параметров, а не на их типы времени выполнения.

У меня есть коллекция (или список или список массивов), в который я хочу поместить как строковые, так и двойные значения.

Чтобы правильно решить ответ и использовать двойную диспетчеризацию, мы должны принести абстракцию для манипулируемых данных.
Почему?

Вот наивный подход посетителя, чтобы проиллюстрировать проблему:

public class DisplayVisitor {

    void visit(Object o) {
        System.out.println("object"));
    }

    void visit(Integer i) {
        System.out.println("integer");
    }

    void visit(String s) {
        System.out.println("string"));
    }

}

Теперь вопрос: как посещаемые классы могут вызывать метод visit()?
Вторая диспетчеризация реализации двойной диспетчеризации опирается на контекст «this» класса, который принимает посещение.
Поэтому нам необходим метод accept() в классах Integer, String и Object для выполнения этой второй отправки:

public void accept(DisplayVisitor visitor){
    visitor.visit(this);
}

Но невозможно! Посещаемые классы - это встроенные классы: String, Integer, Object.
Поэтому у нас нет возможности добавить этот метод.
И вообще, мы не хотим добавлять это.

Таким образом, чтобы реализовать двойную диспетчеризацию, мы должны иметь возможность изменять классы, которые мы хотим передать в качестве параметра во второй диспетчеризации.
Таким образом, вместо манипулирования Object и List<Object> как объявленного типа, мы будем манипулировать Foo и List<Foo>, где класс Foo является оберткой, содержащей пользовательское значение.

Вот интерфейс Foo:

public interface Foo {
    void accept(DisplayVisitor v);
    Object getValue();
}

getValue() возвращает пользовательское значение.
В качестве типа возврата указывается Object, но Java поддерживает ковариационные возвраты (начиная с версии 1.5), поэтому мы могли бы определить более конкретный тип для каждого подкласса, чтобы избежать падений.

ObjectFoo

public class ObjectFoo implements Foo {

    private Object value;

    public ObjectFoo(Object value) {
        this.value = value;
    }

    @Override
    public void accept(DisplayVisitor v) {
        v.visit(this);
    }

    @Override
    public Object getValue() {
        return value;
    }

}

StringFoo

public class StringFoo implements Foo {

    private String value;

    public StringFoo(String string) {
        this.value = string;
    }

    @Override
    public void accept(DisplayVisitor v) {
        v.visit(this);
    }

    @Override
    public String getValue() {
        return value;
    }

}

IntegerFoo

public class IntegerFoo implements Foo {

    private Integer value;

    public IntegerFoo(Integer integer) {
        this.value = integer;
    }

    @Override
    public void accept(DisplayVisitor v) {
        v.visit(this);
    }

    @Override
    public Integer getValue() {
        return value;
    }

}

Вот класс DisplayVisitor , посещающий Foo подклассов:

public class DisplayVisitor {

    void visit(ObjectFoo f) {
        System.out.println("object=" + f.getValue());
    }

    void visit(IntegerFoo f) {
        System.out.println("integer=" + f.getValue());
    }

    void visit(StringFoo f) {
        System.out.println("string=" + f.getValue());
    }

}

А вот пример кода для проверки реализации:

public class OOP {

    void test() {

        List<Foo> foos = Arrays.asList(new StringFoo("a String"),
                                       new StringFoo("another String"),
                                       new IntegerFoo(1),
                                       new ObjectFoo(new AtomicInteger(100)));

        DisplayVisitor visitor = new DisplayVisitor();
        for (Foo foo : foos) {
            foo.accept(visitor);
        }

    }

    public static void main(String[] args) {
        OOP oop = new OOP();
        oop.test();
    }
}

Вывод:

string = a String

строка = другая строка

* * +1091 целое число = 1

объект = 100


Улучшение реализации

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

private Integer value; // or String or Object

@Override
public Object getValue() {
    return value;
}

Мы действительно можем ввести абстрактный обобщенный класс, который содержит пользовательское значение и обеспечивает доступ к:

public abstract class Foo<T> {

    private T value;

    public Foo(T value) {
        this.value = value;
    }

    public abstract void accept(DisplayVisitor v);

    public T getValue() {
        return value;
    }

}

Теперь Foo подклассы легче объявить:

public class IntegerFoo extends Foo<Integer> {

    public IntegerFoo(Integer integer) {
        super(integer);
    }

    @Override
    public void accept(DisplayVisitor v) {
        v.visit(this);
    }

}

public class StringFoo extends Foo<String> {

    public StringFoo(String string) {
        super(string);
    }

    @Override
    public void accept(DisplayVisitor v) {
        v.visit(this);
    }

}

public class ObjectFoo extends Foo<Object> {

    public ObjectFoo(Object value) {
        super(value);
    }

    @Override
    public void accept(DisplayVisitor v) {
        v.visit(this);
    }

}

И метод test() следует изменить, чтобы объявить тип подстановочного знака (?) для типа Foo в объявлении List<Foo>.

void test() {

    List<Foo<?>> foos = Arrays.asList(new StringFoo("a String object"),
                                      new StringFoo("anoter String object"),
                                      new IntegerFoo(1),
                                      new ObjectFoo(new AtomicInteger(100)));

    DisplayVisitor visitor = new DisplayVisitor();
    for (Foo<?> foo : foos) {
        foo.accept(visitor);
    }

}

Фактически, если это действительно необходимо, мы могли бы еще больше упростить подклассы Foo, введя генерацию кода Java.

Объявление этого подкласса:

public class StringFoo extends Foo<String> {

    public StringFoo(String string) {
        super(string);
    }

    @Override
    public void accept(DisplayVisitor v) {
        v.visit(this);
    }

}

можно просто объявить класс и добавить аннотацию:

@Foo(String.class)
public class StringFoo { }

Где Foo - пользовательская аннотация, обрабатываемая во время компиляции.

1 голос
/ 18 марта 2012

Все в Java является Object / объектом (кроме примитивных типов).Вы храните строки и целые числа как объекты, а затем, когда вы вызываете метод prove, они по-прежнему называются объектами.Вы должны взглянуть на ключевое слово instanceof. Проверьте эту ссылку

void prove(Object o){
   if (o instanceof String)
    System.out.println("String");
   ....
}
1 голос
/ 18 марта 2012

это не полиморфизм, вы просто перегрузили метод и вызвали его с параметром типа объекта

...