Параметр метода нескольких типов Java?

Интересно, можно ли потребовать, чтобы параметр java-метода был любого типа из конечного набора типов. Например, я использую библиотеку, в которой два (или более) типа имеют общие методы, но их самый низкий общий предок в иерархии типов - это Object. Что я имею в виду здесь:

   public interface A {
      void myMethod();
   }

   public interface B {
      void myMethod();
   }
...
   public void useMyMethod(A a) {
      // code duplication
   }

   public void useMyMethod(B b) {
      // code duplication
   }

Я хочу избежать дублирования кода. Я думаю примерно так:

   public void useMyMethod(A|B obj){
      obj.myMethod();
   }

Подобный тип синтаксиса уже существует в java. Например:

  try{
     //fail
  } catch (IllegalArgumentException | IllegalStateException e){
     // use e safely here
  }

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


person egelev    schedule 16.12.2014    source источник
comment
Поскольку у вас нет общего интерфейса, как ваш компилятор должен проверять, что два или более классов имеют общий интерфейс? Проблема также в том, что код, который вы определяете как дублирование, не дублируется в соответствии с программным кодом, поскольку у вас есть две разные иерархии классов - код только выглядит одинаково, но работает с совершенно разными структурами данных.   -  person Smutje    schedule 16.12.2014
comment
В Scala это легко, но, вероятно, вам это не поможет. Все, что я могу предложить, - это создать оболочку для одного интерфейса, реализующую другой (или оболочки для обоих, реализующие один и тот же интерфейс).   -  person lmm    schedule 16.12.2014
comment
Поскольку вы пометили его как «шаблоны проектирования», на ум приходит шаблон adapter. Но это требует, чтобы вы написали оболочку для каждого из этих типов. Боюсь, что без этого не будет статически безопасного решения этой проблемы. По крайней мере, я был бы удивлен, если бы были.   -  person 5gon12eder    schedule 16.12.2014
comment
Привет, @egelev, ты нашел какие-нибудь ответы полезными? В таком случае, пожалуйста, дайте автору несколько похвал и отметьте его как принятый! Спасибо.   -  person Yogster    schedule 18.12.2014


Ответы (8)


Вы можете написать interface MyInterface одним методом myMethod. Затем для каждого типа, который вы хотите рассматривать как часть конечного набора, напишите класс-оболочку, например:

class Wrapper1 implements MyInterface {

    private final Type1 type1;

    Wrapper1(Type1 type1) {
        this.type1 = type1;
    }

    @Override
    public void myMethod() {
        type1.method1();
    }
}

Тогда вам просто нужно использовать MyInterface, а не один из конечного набора типов, и всегда будет вызываться соответствующий метод из соответствующего типа.

Обратите внимание, что для фактического использования этих классов-оболочек для вызова метода myMethod вам придется написать

myMethod(new Wrapper1(type1));

Это будет немного некрасиво, так как вам придется запоминать имя класса-оболочки для каждого типа в наборе. По этой причине вы можете предпочесть заменить MyInterface абстрактным классом несколькими статическими фабриками, которые производят типы-оболочки. Нравится:

abstract class MyWrapper {

    static MyWrapper of(Type1 type1) {
        return new Wrapper1(type1);
    }

    static MyWrapper of(Type2 type2) {
        return new Wrapper2(type2);
    }

    abstract void myMethod();
}

тогда вы можете вызвать метод, используя код

myMethod(MyWrapper.of(type1));

Преимущество этого подхода заключается в том, что код остается неизменным независимо от того, какой тип вы используете. Если вы используете этот подход, вам нужно заменить implements MyInterface в объявлении Wrapper1 на extends MyWrapper.

person Paul Boddington    schedule 16.12.2014
comment
Возможно, вы пропустили часть. Как мне создать хорошо разработанный код, используя такой тип нередактируемой иерархии типов? - person Smutje; 16.12.2014
comment
@Smutje, вам не нужно редактировать (исходную) иерархию типов. Вот почему это называется Wrapper. - person Arturo Torres Sánchez; 16.12.2014
comment
Вам, вероятно, следует добавить второй фрагмент кода, как эта оболочка на самом деле используется с двумя объектами methodCall (new Wrapper (t1);) - я бы даже подумал об одном классе Wrapper с двумя конструкторами, по одному для каждого интерфейса - person Falco; 16.12.2014
comment
@Falco Отличный момент. Я улучшил свой ответ, следуя вашему совету, за исключением того, что я выбрал статические фабрики, а не несколько конструкторов. - person Paul Boddington; 16.12.2014
comment
Это мой любимый ответ. Это просто еще один способ реализовать шаблон адаптера, упомянутый в других ответах, с помощью шаблона делегата для реализации самих адаптеров. Однако с точки зрения кода у него довольно много накладных расходов, поскольку вам нужно определить новый общий интерфейс и два класса делегирования. Так что это, вероятно, того стоит, только если у вас много дублированного кода. - person LordOfThePigs; 16.12.2014

А как насчет передачи функции в качестве параметра вашей функции useMyMethod?

Если вы используете Java ‹8:

public interface A {
    void myMethod();
}

public interface B {
    void myMethod();
}

public void useMyMethod(Callable<Void> myMethod) {
    try {
        myMethod.call();
    } catch(Exception e) {
        // handle exception of callable interface
    }
}

//Use

public void test() {
    interfaceA a = new ClassImplementingA();
    useMyMethod(new Callable<Void>() {
        public call() {
            a.myMethod();
            return null;
        }
    });

    interfaceB b = new ClassImplementingB();
    useMyMethod(new Callable<Void>() {
        public call() {
            b.myMethod();
            return null;
        }
    });
}

Для Java> = 8 вы можете использовать лямбда-выражения:

public interface IMyMethod {
    void myMethod();
}

public void useMyMethod(IMyMethod theMethod) {
    theMethod.myMethod();
}

//Use

public void test() {
    interfaceA a = new ClassImplementingA();
    useMyMethod(() -> a.myMethod());

    interfaceB b = new ClassImplementingB();
    useMyMethod(() -> b.myMethod());
}
person Yogster    schedule 16.12.2014
comment
Неплохо, но это заставляет нас показать вызывающей стороне, какие методы мы будем вызывать для аргумента. Изменение реализации (простой вызов другого метода) может заставить нас редактировать каждое использование функции. А если мы хотим вызвать более одного метода, это быстро становится утомительным. - person 5gon12eder; 16.12.2014
comment
Привет @ 5gon12ender, твои баллы действительны. Преимущество использования адаптера заключается в инкапсуляции вызова метода, поэтому, если метод необходимо изменить, нужно изменить только класс адаптера. С другой стороны, вам нужно будет создать адаптер для каждого интерфейса, что может оказаться даже более утомительным, чем функциональное решение. Это действительно зависит от объема кода (внутренний код небольшого класса или широко используемой функции?) И от того, насколько вероятно изменение реализации, я бы сказал. - person Yogster; 16.12.2014

Попробуйте использовать шаблон проектирования Адаптер.

Или, если возможно, добавьте базовый интерфейс:

public interface Base {
    void myMethod();
}

public interface A extends Base {}
public interface B extends Base {}
...
public void useMyMethod(Base b) {
    b.myMethod()
}

Кроме того, вы можете использовать что-то похожее на это

person pbespechnyi    schedule 16.12.2014
comment
Возможно, вы пропустили часть. Как мне создать хорошо разработанный код, используя такой тип нередактируемой иерархии типов? - person Smutje; 16.12.2014
comment
Не работает, как показано, потому что у Base нет необходимого метода. - person BarrySW19; 16.12.2014
comment
Я все еще не вижу картины. ответ pbabcdefp делает его намного понятнее. - person 5gon12eder; 16.12.2014
comment
@ 5gon12eder, я думаю, Adapter - это хорошо известный шаблон, и его легко можно найти в Google. Также я добавил ссылку на страницу Википедии. - person pbespechnyi; 16.12.2014
comment
Я думаю, что Adapter + Delegate - это путь, как в ответе @pbespechnyi. Таким образом, нет необходимости изменять существующую иерархию типов. - person LordOfThePigs; 16.12.2014

Что ж, правильный способ смоделировать ваше требование - объявить myMethod () в интерфейсе супертипа C, который расширяется как A, так и B; затем ваш метод принимает тип C в качестве параметра. Тот факт, что у вас возникли проблемы с выполнением этого в описанной вами ситуации, указывает на то, что вы не моделируете иерархию классов таким образом, который фактически отражает их поведение.

Конечно, если вы не можете изменить структуру интерфейса, вы всегда можете сделать это с помощью отражений.

public static void useMyMethod(Object classAorB) throws Exception {
    classAorB.getClass().getMethod("myMethod").invoke(classAorB);
}
person BarrySW19    schedule 16.12.2014
comment
Чтобы сделать это статически типобезопасным, по крайней мере, на сайте вызывающего абонента, я бы подумал о создании метода, который принимает Object private и предоставляет public методы, перегруженные для каждого поддерживаемого типа. Они просто делегируют показанный вами метод. Это требует небольшого дублирования кода, но только технических однострочников, без бизнес-логики. - person 5gon12eder; 16.12.2014
comment
@ 5gon12eder это именно тот подход, который я выбрал бы - общедоступные методы, которые обеспечивают безопасность типов и вызывают лежащий в основе частный метод! - person Falco; 16.12.2014

Возможно, это не лучшая практика, но не могли бы вы создать новый класс (назовите его C), который содержит дублированные части из A и B, и создать новый метод, который принимает C, пусть ваши методы принимают A и B создать экземпляр C и вызвать новый метод?

Так что у вас есть

class C {
    // Stuff from both A and B
}

public void useMyMethod(A a) {
    // Make a C
    useMyMethod(c);
}

public void useMyMethod(B b) {
    // Make a C
    useMyMethod(c);
}

public void useMyMethod(C c) {
    // previously duplicated code
}

Это также позволит вам сохранить любой не дублированный код в методах для A и B (если он есть).

person zenzizenzizenzic    schedule 16.12.2014

Мне это очень похоже на шаблон шаблона:

public interface A {

    void myMethod();
}

public interface B {

    void myMethod();
}

public class C {

    private abstract class AorBCaller {

        abstract void myMethod();

    }

    public void useMyMethod(A a) {
        commonAndUseMyMethod(new AorBCaller() {

            @Override
            void myMethod() {
                a.myMethod();
            }
        });
    }

    public void useMyMethod(B b) {
        commonAndUseMyMethod(new AorBCaller() {

            @Override
            void myMethod() {
                b.myMethod();
            }
        });
    }

    private void commonAndUseMyMethod(AorBCaller aOrB) {
        // ... Loads of stuff.
        aOrB.myMethod();
        // ... Loads more stuff
    }
}

В Java 8 это намного лаконичнее:

public class C {

    // Expose an "A" form of the method.
    public void useMyMethod(A a) {
        commonAndUseMyMethod(() -> a.myMethod());
    }

    // And a "B" form.
    public void useMyMethod(B b) {
        commonAndUseMyMethod(() -> b.myMethod());
    }

    private void commonAndUseMyMethod(Runnable aOrB) {
        // ... Loads of stuff -- no longer duplicated.
        aOrB.run();
        // ... Loads more stuff
    }
}
person OldCurmudgeon    schedule 16.12.2014

Динамический прокси-сервер можно использовать для создания моста между общим интерфейсом, который вы определяете, и объектами, реализующими другие интерфейсы, которые соответствуют новому интерфейсу. Затем вы можете сделать так, чтобы ваши useMyMethods преобразовали параметр в новый интерфейс (в качестве динамического прокси) и ваш общий код был написан только в терминах нового интерфейса.

Это будет новый интерфейс:

interface Common {
  void myMethod();
}

Затем с помощью этого обработчика вызова:

class ForwardInvocationHandler implements InvocationHandler {
  private final Object wrapped;
  public ForwardInvocationHandler(Object wrapped) {
    this.wrapped = wrapped;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable {
    Method match = wrapped.getClass().getMethod(method.getName(), method.getParameterTypes());
    return match.invoke(wrapped, args);
  }
}

У вас могут быть такие методы:

public void useMyMethod(A a) {
  useMyMethod(toCommon(a));
}

public void useMyMethod(B b) {
  useMyMethod(toCommon(b));
}

public void useMyMethod(Common common) {
  // ...
}

private Common toCommon(Object o) {
  return (Common)Proxy.newProxyInstance(
    Common.class.getClassLoader(), 
    new Class[] { Common.class }, 
    new ForwardInvocationHandler(o));   
}

Обратите внимание, что для упрощения вы даже можете выбрать один из существующих интерфейсов (A или B), который будет использоваться в качестве общего интерфейса.

(См. Другой пример здесь, а также другие идеи по этой теме)

person Jordão    schedule 16.12.2014

Правильный способ - использовать Java Generics.

См. http://docs.oracle.com/javase/tutorial/java/generics/bounded.html

person StarShine    schedule 16.12.2014
comment
Как бы вы ограничили параметр универсального типа конечным набором типов? - person dcastro; 16.12.2014
comment
Очевидно, у вас есть базовый интерфейс или тип объекта, скажем, тип AorB, который имеет определение или реализацию метода. Итак, вы определяете открытый интерфейс Functor ‹T extends AorB› {}. Вы можете несколько раз связать T с серией типов: ‹T расширяет B1, B2 и B3› - person StarShine; 16.12.2014
comment
OP не может редактировать существующие интерфейсы, чтобы они расширяли общий интерфейс. Как я могу создать хорошо спроектированный код, используя такой тип нередактируемой иерархии типов? - person dcastro; 16.12.2014
comment
Что ж, использование расширений с несколькими привязками все еще возможно. - person StarShine; 16.12.2014
comment
Я не использую Java, но смотрю документацию , кажется, вы можете заставить параметр универсального типа расширять несколько типов (T extends A & B), но вы не можете заставить его расширять один из нескольких типов. - person dcastro; 16.12.2014