简体   繁体   中英

Java — How to deal with type erasure in constructors?

Let's say I have two constructors in my class:

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

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

Let's say that both of these constructors provide the same information about a User and are equally valid ways to construct a user for different use cases.

In Java, you can't do this because of type erasure -- Java won't accept two constructors that have as parameters List< ? >.

So, what is the way to get around this? What is a solution that is not overkill but still respects basic OO? It seems wrong to have to construct a factory method or other interface around this just because Java doesn't have strong generics support.

Here are the possibilities I can think of:

1) Accept a List<?> as a parameter for the constructor and parse in the constructor which kind of logic you need, or throw an exception if it's not any of the accepted types.

2) Create a class that accepts either List, constructs the appropriate User object, and returns it.

3) Create wrappers around List<Source1> and List<Source2> that can be passed to the User constructor instead.

4) Subclass this guy with two classes, where all of the functionality is inherited except for the constructor. The constructor of one accepts Source1, the other accepts Source2.

5) Wrap this guy with a builder where are two different builder methods for the two different sources of data for instantiation.

My questions are these:

1) Is the need to do this a flaw with Java, or an intentional design decision? What is the intuition?

2) Which solution is strongest in terms of maintaining good code without introducing unneeded complexity? Why?

This question is similar: Designing constructors around type erasure in Java but does not go into specifics, it just suggests various work-arounds.

The usual approach is to use factory methods :

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;
}

If you only want construction using Source1 or Source2 (ie you don't have a default constructor), you simply hide your constructor, forcing clients to use your factory methods:

private User () {
    // Hide the constructor
}

This problem arises because you can't name constructors differently, which would be how you'd overcome this if these were normal methods. Because constructor names are fixed as the class name, this code pattern is only way to distinguish then give the same type erasure.

1: Maintaining backward compatibility with erasure.

2: Can your class use generics? Something like this:

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

I am not sure if this is what you meant by (2)

The basic problem is the language was designed (with the constructor name being fixed) before generics existed, so it can't handle collisions due to type erasure, which would normlaly be handled by renaming methods to distinguish them.

One "workaround", without resorting to factory methods, is to add another non-typed parameter to enable the compiler to distinguish them:

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

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

This is a little lame though, as you could replace those extra parameters with anything (eg Integer and String , or simply have one of them omit the second parameter) and it would still work. Further, the extra parameter is ignored - it exists only to distinguish the constructors. Nevertheless, it does allow the code to work without adding any extra methods or special code.

public class User<T> {

    public User(List<T> source){

    }

}

or probably better:

public class User<T extends SomeTypeOfYours> {

    public User(List<T> source){

    }
}

Wheer SomeTypeOfYours is a super-type of Source1 and Source2 .

I like the Factory idea in general, or genericizing User (as suggested by @Markus Mikkolainen), but one possible alternative would be to pass the class of the list as a 2nd argument and switch on that, eg

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

The <?> might be something else if there is some common ancestor class. I can imagine many cases where this is a poor idea, but a few where it might be acceptable.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM