Java-как бороться с стиранием типов в конструкторах?

предположим, у меня есть два конструктора в моем классе:

public User (List<Source1> source){
...
}

public User (List<Source2> source) {
...
}

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

в Java вы не можете сделать это из-за типа erasure -- Java не будет принимать два конструктора, которые имеют в качестве параметров List.

Итак,как это обойти? Что такое решение, которое не является излишним, но все еще уважает базовый OO? Кажется неправильным создавать заводской метод или другой интерфейс вокруг этого только потому, что Java не имеет сильной поддержки дженериков.

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

1) принять List<?> в качестве параметра для конструктора и проанализируйте в конструкторе, какая логика вам нужна, или создайте исключение, если это не какой-либо из принятых типов.

2) Создайте класс, который принимает любой список, создает соответствующего пользователя объект и возвращает его.

3) Создайте обертки вокруг List<Source1> и List<Source2> который может быть передан конструктору пользователя вместо этого.

4) подкласс этого парня с двумя классами, где все функции наследуются, за исключением конструктора. Конструктор одного принимает Source1, другой принимает Source2.

5) оберните этого парня строителем, где есть два разных метода строителя для двух разных источников данных для экземпляр.

мои вопросы:

1) является ли необходимость делать это недостатком Java или преднамеренным дизайнерским решением? Что такое интуиция?

2) Какое решение является самым сильным с точки зрения поддержания хорошего кода, не внося ненужные сложности? Почему?

этот вопрос сродни: проектирование конструкторов вокруг стирания типа в Java но не вдается в детали,он просто предлагает различные обходные пути.

5 ответов


обычный подход заключается в использовании методы фабрики:

public static User createFromSource1(List<Source1> source) {
    User user = new User();
    // build your User object knowing you have Source1 data
    return user;
}

public static User createFromSource2(List<Source2> source) {
    User user = new User();
    // build your User object knowing you have Source2 data
    return user;
}

если вы только хотите строительство с помощью Source1 или Source2 (т. е. у вас нет конструктора по умолчанию), вы просто скрываете свой конструктор, заставляя клиентов использовать ваши заводские методы:

private User () {
    // Hide the constructor
}

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


1: поддержание обратной совместимости со стиранием.

2: Может ли ваш класс использовать дженерики? Что-то вроде этого:--2-->

public class User<T> {
    private List<T> whatever;
    public User(List<T> source){
       ....
    }
}

Я не уверен, что это то, что вы имели в виду под (2)


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

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

public User(List<Source1>, Source1 instance) {
    // ignore instance
}

public User(List<Source2>, Source2 instance) {
    // ignore instance
}

Это немного хромает, хотя, как вы могли бы заменить эти дополнительные параметры с чем угодно (например,Integer и String, или просто один из них опустить второй параметр) и он будет работать. Кроме того, дополнительный параметр игнорируется - он существует только для различения конструкторов. Тем не менее, это позволяет коду работать без добавления каких-либо дополнительных методов или специального кода.


public class User<T> {

    public User(List<T> source){

    }

}

или, наверное, лучше:

public class User<T extends SomeTypeOfYours> {

    public User(List<T> source){

    }
}

Wheer SomeTypeOfYours супер-типа Source1 и Source2.


мне нравится идея фабрики в целом или обобщающий пользователь (как предложил @Markus Mikkolainen), но одной из возможных альтернатив было бы передать класс списка в качестве 2-го аргумента и включить его, например

public User<List<?> source, Class<?> clazz) {
   switch(clazz) {
      case Source1.class: initForSource1(); break;
      case Source2.class: initForSource2(); break;
      default: throw new IllegalArgumentException();
   }
}

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