Подтвердить что ты не робот

Как ссылаться на общий тип возврата с несколькими ограничениями

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

public class Foo {
    public String getFoo() { ... }
}

public interface Bar {
    public void setBar(String bar);
}

Я могу объявить возвращаемый тип следующим образом:

public class FooBar {

    public static <T extends Foo & Bar> T getFooBar() {
        //some implementation that returns a Foo object,
        //which is forced to implement Bar
    }
}

Если я вызываю этот метод откуда-то, eclipse сообщает мне, что тип возвращаемого объекта имеет метод String getFoo(), а также setBar(String), но только если я нахожу точку за функцией, подобной этой:

FooBar.getFooBar(). // here eclipse is showing the available methods.

Есть ли способ получить ссылку на такой объект? Я имею в виду, если бы я сделал что-то вроде этого:

//bar only has the method setBar(String)
Bar bar = FooBar.getFooBar();
//foo only has the getFoo():String method
Foo foo = FooBar.getFooBar();

Я хотел бы иметь ссылку вроде этого (псевдокод):

<T extents Foo & Bar> fooBar = FooBar.getFooBar();
//or maybe
$1Bar bar = FooBar.getFooBar();
//or else maybe
Foo&Bar bar = FooBar.getFooBar();

Возможно ли это как-то в Java, или я могу только объявить типы возвращаемых данных, как это? Я думаю, что Java так или иначе набирает его. Я бы предпочел не прибегать к обертке вроде этого, так как это похоже на обман:

public class FooBarWrapper<T extends Foo & Bar> {
    private Foo mFoo;
    private Bar mBar;

    public FooBarWrapper(T val) {
        mFoo = val;
        mBar = val;
    }

    public Foo getFoo() {
        return mFoo;
    }

    public Bar getBar() {
        return mBar;
    }
}

Действительно ли Java придумала такую ​​приятную особенность, но забывайте, что хотелось бы иметь ссылку на нее?

4b9b3361

Ответ 1

Пока параметры типа общего метода могут быть ограничены границами, такими как extends Foo & Bar, они в конечном итоге решаются вызывающим. Когда вы вызываете getFooBar(), сайт вызова уже знает, к чему решается T. Часто эти параметры типа будут выведены компилятором, поэтому вам обычно не нужно указывать их, например:

FooBar.<FooAndBar>getFooBar();

Но даже когда T выведено как FooAndBar, это действительно происходит за кулисами.

Итак, чтобы ответить на ваш вопрос, такой синтаксис выглядит следующим образом:

Foo&Bar bothFooAndBar = FooBar.getFooBar();

Никогда не будет полезен на практике. Причина в том, что вызывающий абонент должен знать, что такое T. Либо T - это конкретный тип:

FooAndBar bothFooAndBar = FooBar.<FooAndBar>getFooBar(); // T is FooAndBar

Или, T - параметр неразрешенного типа, и мы находимся в его области:

<U extends Foo & Bar> void someGenericMethod() {
    U bothFooAndBar = FooBar.<U>getFooBar(); // T is U
}

Другой пример:

class SomeGenericClass<V extends Foo & Bar> {
    void someMethod() {
        V bothFooAndBar = FooBar.<V>getFooBar(); // T is V
    }
}

Технически это завершает ответ. Но я также хотел бы указать, что ваш метод example getFooBar по своей сути является небезопасным. Помните, что вызывающий решает, что T будет, а не метод. Поскольку getFooBar не принимает никаких параметров, связанных с T, и из-за типа erasure его единственными параметрами будут возвращать null или "лгать", сделав непроверенный бросок, рискуя загрязнением кучи. Типичным обходным решением было бы для getFooBar взять аргумент Class<T>, или, например, FooFactory<T>.

Update

Оказывается, я был не прав, когда утверждал, что вызывающий getFooBar должен всегда знать, что такое T. Как указывает @MiserableVariable, есть ситуации, когда аргумент типа общего метода определяется как групповой захват, а не конкретный тип или переменная типа. Посмотрите его ответ за отличный пример реализации getFooBar, который использует прокси-сервер для вывода своей домашней страницы о том, что T неизвестен.

Как мы обсуждали в комментариях, пример с использованием getFooBar создал путаницу, потому что он не принимает аргументов для вывода T from. Некоторые компиляторы выдают ошибку по бесконтактному вызову getFooBar(), а другие в порядке с ним. Я думал, что несогласованные ошибки компиляции вместе с тем, что вызов FooBar.<?>getFooBar() является незаконным, подтвердил мою точку зрения, но они оказались красными сельдевыми.

Основываясь на ответе @MiserableVariable, я собрал новый пример, который использует общий метод с аргументом, чтобы удалить путаницу. Предположим, что у нас есть интерфейсы Foo и Bar и реализация FooBarImpl:

interface Foo { }
interface Bar { }
static class FooBarImpl implements Foo, Bar { }

У нас также есть простой класс контейнера, который обертывает экземпляр некоторого типа, реализующего Foo и Bar. Он объявляет глупый статический метод unwrap, который принимает FooBarContainer и возвращает его референт:

static class FooBarContainer<T extends Foo & Bar> {

    private final T fooBar;

    public FooBarContainer(T fooBar) {
        this.fooBar = fooBar;
    }

    public T get() {
        return fooBar;
    }

    static <T extends Foo & Bar> T unwrap(FooBarContainer<T> fooBarContainer) {
        return fooBarContainer.get();
    }
}

Теперь скажем, что у нас есть подстановочный параметр с параметрами FooBarContainer:

FooBarContainer<?> unknownFooBarContainer = ...;

Нам разрешено передавать unknownFooBarContainer в unwrap. Это показывает, что мое предыдущее утверждение было неправильным, потому что сайт вызова не знает, что T - только то, что это некоторый тип в пределах extends Foo & Bar.

FooBarContainer.unwrap(unknownFooBarContainer); // T is a wildcard capture, ?

Как я уже говорил, вызов unwrap с подстановочным знаком является незаконным:

FooBarContainer.<?>unwrap(unknownFooBarContainer); // compiler error

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

Итак, здесь используется прецедент для синтаксиса, о котором спрашивает OP. Вызов unwrap на unknownFooBarContainer возвращает ссылку типа ? extends Foo & Bar. Мы можем назначить эту ссылку Foo или Bar, но не обе:

Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = FooBarContainer.unwrap(unknownFooBarContainer);

Если по какой-то причине unwrap были дорогими, и мы только хотели называть их один раз, мы были бы вынуждены использовать:

Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = (Bar)foo;

Итак, вот где гипотетический синтаксис пригодится:

Foo&Bar fooBar = FooBarContainer.unwrap(unknownFooBarContainer);

Это всего лишь один довольно непонятный случай использования. Было бы очень далеко идущие последствия для разрешения такого синтаксиса, как хорошего, так и плохого. Это откроет место для злоупотреблений там, где это не было необходимо, и совершенно понятно, почему разработчики языка не реализовали такую ​​вещь. Но я все еще думаю, что интересно подумать.


Заметка о загрязнении кучи

(В основном для @MiserableVariable) Здесь описано, как небезопасный метод, например getFooBar, вызывает загрязнение кучи и его последствия. Учитывая следующий интерфейс и реализации:

interface Foo { }

static class Foo1 implements Foo {
    public void foo1Method() { }
}

static class Foo2 implements Foo { }

Позвольте реализовать небезопасный метод getFoo, аналогичный getFooBar, но упрощенный для этого примера:

@SuppressWarnings("unchecked")
static <T extends Foo> T getFoo() {
    //unchecked cast - ClassCastException is not thrown here if T is wrong
    return (T)new Foo2();
}

public static void main(String[] args) {
    Foo1 foo1 = getFoo(); //ClassCastException is thrown here
}

Здесь, когда новый Foo2 отбрасывается до T, он "не отмечен", что означает, что из-за стирания типа среда выполнения не знает, что она должна потерпеть неудачу, хотя в этом случае это должно быть, так как T было Foo1. Вместо этого куча "загрязнена", что означает, что ссылки указывают на объекты, которым они не должны были разрешаться.

Сбой происходит после возврата метода, когда экземпляр Foo2 пытается получить ссылку на ссылку Foo1, которая имеет тип reifiable Foo1.

Вероятно, вы думаете: "Ладно, так что это взорвалось на сайте вызова, а не в методе, большое дело". Но это может легко усложниться, когда задействовано больше дженериков. Например:

static <T extends Foo> List<T> getFooList(int size) {
    List<T> fooList = new ArrayList<T>(size);
    for (int i = 0; i < size; i++) {
        T foo = getFoo();
        fooList.add(foo);
    }
    return fooList;
}

public static void main(String[] args) {

    List<Foo1> foo1List = getFooList(5);

    // a bunch of things happen

    //sometime later maybe, depending on state
    foo1List.get(0).foo1Method(); //ClassCastException is thrown here
}

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

Это становится еще более сложным, когда вызывающий абонент находится в общем объеме. Представьте себе, что вместо List<Foo1> мы получаем List<T>, помещая его в Map<K, List<T>> и возвращаем его еще одному методу. Надеюсь, вы надеетесь.

Ответ 2

Что касается вопроса о щедрости,

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

import java.lang.reflect.*;

interface Foo {}
interface Bar {}

class FooBar1 implements Foo, Bar {public String toString() { return "FooBar1"; }}
class FooBar2 implements Foo, Bar {public String toString() { return "FooBar2"; }}   

class FooBar {
    static <T extends Foo & Bar> T getFooBar1() { return (T) new FooBar1(); }
    static <T extends Foo & Bar> T getFooBar2() { return (T) new FooBar2(); }
    static <T extends Foo & Bar> T getFooBar() { 
        return (T) 
        Proxy.newProxyInstance(
            Foo.class.getClassLoader(),
            new Class[] { Foo.class, Bar.class },
            new InvocationHandler() {
                public Object invoke(Object proxy, Method method, Object[] args) {
                    return "PROXY!!!";}});
    }

    static <U extends Foo & Bar> void show(U u) { System.out.println(u); }

    public static void main(String[] args) {
        show(getFooBar1());
        show(getFooBar2());
        show(getFooBar());      
    }

}

Оба FooBar1 и FooBar2 реализуют Foo и Bar. В main вызовы на getFooBar1 и getFooBar2 могут быть назначены переменной, хотя для нее не существует серьезной причины знать IMHO.

Но getFooBar - интересный случай, который использует прокси. На практике это может быть единственный экземпляр объекта, реализующего два интерфейса. Другой метод (show здесь) можно использовать с временным типом более безопасным способом, но он не может быть назначен переменной без FooBarWrapper хака, описанного в вопросе. Невозможно создать общую оболочку, class Wrapper<T extends U & V> не разрешено.

Единственная проблема, похоже, заключается в определении синтаксиса, механизмы проверки других типов, по-видимому, существуют, по крайней мере, в Oracle javac 1.7.0.

Ответ 3

Подобно @Paul Bellora, упомянутому в его ответе, тип get разрешен вызывающим, так как по существу он теперь будет тем, что он вызывает. Я просто хотел бы добавить к его ответу вариант использования, где, я думаю, использование синтаксиса может принести пользу.

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

Case

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

 public interface CoordinateView extends View
 {
      Coordinate getCoordinate();

      //Maybe more stuff
 } 


У меня есть несколько окон в моем приложении, которые реализуют этот интерфейс. Теперь скажем, что по какой-то причине я хочу сохранить в модели последнюю координату, представленную в окне, и закрыть окно сразу после. Для этого я могу прикрепить обработчик к кнопке окна, которая отправит форму, обработчик начнет срабатывать, когда пользователь закроет окно. Я мог бы добиться этого, просто добавив обработчик анонимно в каждое окно, например:

  public MyWindow extends Window implements CoordinateView, OtherInterface
  {
       private Button submitButton;

       public MyWindow()
       {
          super();
          //Create all the elements

          submitButton.addClickHandler(
              new ClickHandler()
              {
                 @Override
                 onCLick(ClickEvent e)
                 {
                   getModel().add(getCoordinate());
                   destroy();
                 }
              });  
       }
  }

Однако этот дизайн мне не нужен, он недостаточно модульный. Учитывая, что у меня есть приличное количество окон с таким поведением, его изменение может стать довольно утомительным. Поэтому я скорее извлечу анонимный метод в классе, чтобы было легче изменять и поддерживать. Но проблема в том, что метод destroy() не определен в каком-либо интерфейсе, является лишь частью окна, а метод getCoordinate() определен в определенном интерфейсе.

Использование

В этом случае я мог бы использовать несколько ограничений, например:

  public class MyController <T extends Window & CoordinateView> implements ClickHandler
  {
      private T windowWithCoordinates;

      public MyController (T window)
      {
         windowWithCoordinates = window;
      }

      @Override
      onClick(ClickEvent e)
      {
          getModel().add(windowWithCoordinates.getCoordinate());
          windowWithCoordinate.destroy();
      }
  }

Тогда код в окнах будет теперь:

 public MyWindow extends Window implements CoordinateView, OtherInterface
  {
       private Button submitButton;

       public MyWindow()
       {
          super();
          //Create all the elements

          submitButton.addClickHandler(new MyController<MyWindow>(this));

       }
  }

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

Альтернативные

В качестве альтернативы я мог бы определить дополнительный интерфейс, расширяющий CoordinateView и определяющий метод закрытия окна.

public interface CoordinateWindow extends CoordinateView
{
    void destroy();
}

Наличие окна реализует этот более конкретный интерфейс вместо ненужного использования общих параметров в извлеченном контроллере:

  public class MyController implements ClickHandler
  {
      private CoordinateWindow windowWithCoordinates;

      public MyController (CoordinateWindow window)
      {
         windowWithCoordinates = window;
      }

      @Override
      onClick(ClickEvent e)
      {
          getModel().add(windowWithCoordinates.getCoordinate());
          windowWithCoordinate.destroy();
      }
  }



    public MyWindow extends Window implements CoordinateWindow
   {
       private Button submitButton;

       public MyWindow()
       {
          super();
          //Create all the elements  
          submitButton.addClickHandler(new MyController(this));                  
       }

       @Override
       void destroy(){
          this.destroy();
       }
  }

Этот подход для некоторых можно рассматривать как гораздо более чистый, чем предыдущий, и даже более многократно используемый, поскольку теперь он может быть добавлен в другие "окна" за пределами указанной иерархии. Лично я предпочитаю этот подход. Однако это может привести к немного более кодированию, поскольку новый интерфейс должен быть определен только ради получения доступа к желаемому методу.

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

Ответ 4

Не уверен, что делает Eclipse для вас, но большая часть кода выше не подходит для компиляции....

Я сделал соответствующие изменения, чтобы заставить его скомпилировать как можно больше, и вот что я получаю:

public class Foo
{
  public String getFoo() { return ""; }  // must have a body
}
public interface Bar // no ()
{
  public void setBar(String bar);
}
public class FooBar<T>
{
  public static <T extends Foo & Bar> T getFooBar()
  {
    return null;
  }
}
public class FB
{
  private FooBar<Object> fb = new FooBar<Object>();

  public static void main(String args[])
  {
    new FB();
  }

  public FB()
  {
    System.out.println(fb.getFooBar());
  }
}

FB.java:12: type parameters of <T>T cannot be determined; no unique maximal instance exists for type variable T with upper bounds java.lang.Object,Foo,Bar
    System.out.println(fb.getFooBar());
                                   ^
1 error