Java Generics, Tightly Bounded Parameter Type
Asked Answered
K

1

9

I wish to have a method which has a signature like method(T1 t1, T2 t2) such that T2 is-a T1 and/or T1 is-a T2. I do not want the case where T1 and T2 are both a T but where neither is-a the other. I wish for the most allowed type to be bounded above by the highest of either T1 or T2 in the inheritance tree. I am using Java 6.

Below is an attempt to show some desired use cases

class Experiment
{
  static List genericList = new ArrayList();
  static ArrayList arrayList = new ArrayList();

  static class Test1 { }
  static class Test2 extends Test1 { }
  static class Test3 extends Test1 { }

  static <T> T experiment0(T expected, T actual)
  {
    return actual;
  }

  /** Almost works, but want arbitrary ordering. Cannot overload due to erasure. */
  static <T, S extends T> S experiment1(T expected, S actual)
  {
    return actual;
  }

  private static void experimentDriver()
  {
    // good, allowed
    List l = experiment0(genericList, arrayList);

    // bad, allowed; want compile-time error
    Object o = experiment0(new String(), new Integer(0));


    // good, allowed
    Test1 test1 = experiment1(new Test1(), new Test3());
    String string = experiment1(new String(), new String());
    List list = experiment1(genericList, new ArrayList());

    // good, disallowed
    experiment1(new Test2(), new Test3());
    experiment1(new Test3(), new Test2());
    experiment1(new Integer(0), new String());
    experiment1(new ArrayList(), new LinkedList());

    // bad, disallowed; want either order
    experiment1(new Test3(), new Test1());
    experiment1(new ArrayList(), genericList);
  }
}

I know that I can achieve something similar with a signature like experiment(Class<T> clazz, T expected, T actual) but that forces the programmer to explicitly mention the highest allowable type, when I'd like it inferred by the language.

Note that the closer solution can't simply be overloaded because their erasures are the same.

I hope I've provided enough information to convey what I want. I realize this may just be impossible in Java, but I hope it's doable. This is also my first question on this forum, so if I've unknowingly violated any rules I apologize in advance and appreciate your patience!

Kantos answered 10/12, 2013 at 18:59 Comment(11)
Nope, not possible; T can always be Object.Fluorometer
I'm pretty sure what you're asking for isn't possible, short of creating two methods with different names. Out of curiosity, why is the order-doesn't-matter requirement significant? What's your use case for this?Romie
@LouisWasserman Apparently java8 can infer T=Object, but java7 isn't that smart.Nipa
I came across this when doing unit tests with a Map<String, Object>. I cast the values as I pull them out, and compare them to constants stored as part of the test. Sometimes things change, and I could save tens of minutes not running tests which are just going to fail sometimes.Kantos
Take a look at the "visitor design pattern". It's definetly not what you want, but does some cating tricks that may inspire you...Faxan
You can't achieve compile-time checks like this, runtime it should be possible using reflection. But if you are doing this to solve tests that occationally fail I'm not sure if this is the way. What exactly are you trying to solve here?Aracelis
The problem I want to solve is compile time prevention of a comparison such as String and Integer. Consider github.com/junit-team/junit/blob/master/src/main/java/junit/… where the .equals() will clearly fail in such a case. In a case like this you already get a notice at run time. I am looking to prevent a developer, who is working on unit tests, from accidentally creating an erroneous test & only finding out after having run the tests, which can take a potentially large amount of time. (In comparing an ArrayList and LinkedList, you would cast one to List.)Kantos
Even without erasure, you'd still need to define two methods. The idea of T extends S, S extends T creates a circular dependency between the types. Clearly both of those constraints can't be true at the same time unless S == T. In C# you could do it by overloading the method (because there's no erasure). In Java, it can't be done at all.Mccorkle
@IanMcLaird your comments are the most helpful so far (though thank you everyone!). I'm not looking for both of those constraints to be true at the same time though, just one at the minimum. So a logical OR instead of a logical AND. I really appreciate the comparison to C#, since it lacks erasure. I still feel like abstractly this should be doable in either language (theoretically; I don't know C#) without overloading, but if not then I am happy to know it and understand it.Kantos
@Sbodd, sorry, I meant to answer this before, and still don't believe I've done so properly: order matters as in the example where you have an assert() where one value is expected and the other the actual value, and you print a helpful message where knowing which is which matters.Kantos
But printing that message happens at run time not compile time, and at run time you could use reflection to determine those types.Mccorkle
M
3

This became too long for a comment.

From the compiler's perspective, a constraint that might be false isn't a constraint at all, and won't help it figure out what types should be allowed in the various parameter slots, or what you're allowed to do with them. Thus, the language (neither C# nor Java, by the way) has no syntax to define things that way. The reason for allowing the type constraints is that it allows you to treat the variable as though it were that type in the body of the method. Thus:

public static <T extends Collection> void foo(T a) {
    System.out.println(a.size());
}

Will compile and run, because all Collection subclasses have a size() method. The trouble is that <T extends S || S extends T> (or whatever other made-up syntax) doesn't help.

For example, suppose we had this:

public static <T extends List || S extends ArrayList> S bar(T a, S b) {
    // details
}

Can we, in this example, call b.removeRange(...)? We have no idea because S might not be an ArrayList. How about a.size()? Again, we don't know, because T might not be a List. So this isn't any better-defined than if we'd just said

public static <T, S> S bar(T a, S b) {...}

Adding that they possibly derive from each other just adds an additional level of complexity to that example.

Mccorkle answered 10/12, 2013 at 23:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.