Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add (experimental) support of java.lang.Comparable #512

Closed
lombokissues opened this issue Jul 14, 2015 · 21 comments
Closed

Add (experimental) support of java.lang.Comparable #512

lombokissues opened this issue Jul 14, 2015 · 21 comments
Labels
accepted The issue/enhancement is valid, sensible, and explained in sufficient detail

Comments

@lombokissues
Copy link

Migrated from Google Code (issue 439)

@lombokissues
Copy link
Author

👤 maxxyme   🕗 Dec 20, 2012 at 16:14 UTC

What steps will reproduce the problem?

Implementing java.lang.Comparable for standard cases can be fastidious, because the calculation of the int return value might follow the same scheme as the ones used for the equals and hashCode (using each field, comparing, so on...)

What is the expected output? What do you see instead?

Provide an annotation to auto-generate the code for a basic appropriate compareTo() method in order to implement the java.lang.Comparable interface.
Provide this Lombok annotation with the same kind of attributes as the @ EqualsAndHashCode one (esp. the "exclude" one, maybe "callSuper" and "doNotUseGetters" too)

@lombokissues
Copy link
Author

👤 reinierz   🕗 Mar 11, 2013 at 23:33 UTC

It's on our todo-list, but we want to take a somewhat wider view of this problem and introduce a more general annotation that indicates a complete strict relationship (so, it would also create compatible equals and hashCode methods and the name of the annotation will be a bit less unwieldy than @ EqualsAndHashCode). The biggest issue is ordering the fields, and being able to supply small comparator properties for each field, such as 'reverse direction on this'. We can't go overboard with that obviously (at that point, it's not really boilerplate anymore), and we also need to worry about what we do if any given field is not of a type we implicitly know how to order, and also isn't Comparable. More to the point perhaps to know if the field is comparable in the first place we need to resolve type on it and that's very difficult to do in a handler that needs to create methods. Not impossible (@ Delegate does it!) but it always leads to lots of complications.

Generics also don't help.

@lombokissues
Copy link
Author

👤 Maaartinus   🕗 Dec 07, 2013 at 23:09 UTC

I'd suggest to look for the uses of Guava's ComparisonChain. I'm afraid there's no easy solution for anything which is neither primitive nor Comparable; not even for arrays (IIRC my most recent Comparator ordered int[] lexicographically as unsigned with the exception of ordering shorter arrays first - surely not something you want to create a syntax for).

Concerning the fields order, I believe that processing them as they appear in the source code is nearly sufficient except for you often want to skip a field (e.g. id) entirely or process it later. So maybe something like @ Comparator.Postpone and @ Comparator.Exclude would do (all the postponed fields would get processed after all normal fields, again in their appearance order).

Maybe you should assume that all non-primitive fields are Comparable and get a compile-time error if they're not. The user could annotate the field with @ Comparator.UseComparator(MyComparator.class) to prevent this or to specify a special handling for Comparables.

It gets a bit complicated concerning the number of annotations, but it's fairly regular and need no hacks and no resolution. Finally, I'd also add @ Comparator.Reverse and maybe @ Comparator.NullFirst and @ Comparator.NullLast.

@lombokissues
Copy link
Author

End of migration

@rzwitserloot
Copy link
Collaborator

Proposal:

  • We deprecate @EqualsAndHashCode (but it'll never be removed, really, or at least not for a long while).
  • Using both @EqAHC and @Ordered at the same time is invalid.
  • @Data and @Value switch over from implying @EqAHC to implying @Ordered (without comparable=true!)
  • We create a new annotation, @Ordered. It broadly fills the same role as @EqAHC.
  • of and exclude go away. comparable = true/false (default = depends on whether you added implements Comparable to your implements list) is added. onParam is renamed to onEqualsParam, and onCompareToParam is added (oCTP without comparable=(default) true is an error).
  • the same batch of fields is used for all 4 methods (canEqual, equals, hashCode, and compareTo).
  • For comparable=true, any field so included must either be a primitive (we know how to compare this intrinsically), or must implement Comparable. We don't check this; we just call .compareTo(o), and if that's not a valid action the compiler will complain about generated code. This means we do not have to do resolution which is a good thing.
  • Trying to compare arrays is a lombok-generated error; you must specify a comparison method/comparator class or exclude the field.
  • If comparable=true, we don't just generate compareTo, we also add implements Comparable<SelfType> to the implements list if it isn't already there.
  • To configure individual fields, add sub-annotations of ordered:
    • @Ordered.Include. If present, any use of @Ordered.Exclude anywhere in this class is invalid. All fields without an @Ordered.Include are excluded if any field is so annotated.
    • @Ordered.Exclude. Nuff said.
    • @Ordered.NullOrder(NullOrder.NULL_IS_MAX -or- NullOrder.NULL_IS_MIN -or- NullOrder.NULL_IS_ERR). Should the default behaviour be NULL_IS_MIN, or NULL_IS_ERR?
    • @Ordered.DoNotUseGetter; overrides the doNotUseGetters of the @ordered annotation.
    • @Ordered.Use(method = "methodName") - this is unfortunately still stringly typed, but there's just no way to tie a method to a field without stringly typing one of them. Alternatives are magic names (if the field is named foobar, then a method named foobar_compare is assumed to be the comparer method); this sounds worse than specifying it in the annotation. At least there we can error if there's no method with that name. The method can return either boolean or int; if int, negative = first arg is lower, 0 = equal, positive = first arg is larger. If boolean, an error is generated unless comparable=false. The method must take 2 arguments.
    • @Ordered.Use(comparator = ComparatorClass.class) - this will instantiate ComparatorClass with the no-args constructor and then call .compare(a, b) on it, assuming that it is a Comparator implementation.
    • @Ordered.Reverse. Nuff said.
    • @Ordered.Order(int). By default all fields have the '0' order. All the 4 generated methods will start out with the fields that have the lowest order number. If multiple fields have the same order, they are handled in the order they show up in the source file, as now.
  • Like @EqAHC now, static fields, transient fields, and fields that start with $ are skipped.
  • primitives get special treatment. arrays also get special treatment, but for compareTo that's just lombok generating an error; you must exclude these or specify an explicit comparator with @Ordered.Use.

OPEN ISSUES:

  • default NULL_IS_MIN or NULL_IS_ERR?
  • Go with all these sub-annotations, or have one @Ordered.Conf annotation that has all that stuff as params instead?
  • Is this the right way to go about specifying custom equality / comparator methods?
  • How should we handle the process of deprecating @EqAHC?
  • Is the general concept of 'always treat equals, hashCode, canEqual, and compareTo as sides of the same coin' okay? There are some indications that java code out there uses different fields, but this doesn't sound like it can adhere to the rule that if a.equals(b), then a.compareTo(b) == 0 and vice versa.
  • You now can't refer to expressions that are a comparator; you can't use this to specify String.CASE_INSENSITIVE_COMPARATOR for example. Kind of annoying. If only fields or methods could be referenced in annotations!

@Condor70
Copy link

  • I would recommend NULL_IS_MIN as default to lower the chances of accidental exceptions.
  • To improve readability I would go for a single annotation. However, I would prefer this annotation to be at field level instead of a sub-annotation.
  • I would leave @EqualsAndHashCode for now. Even deprecating it would cause problems for long-time users.
  • For equals/compareTo see below.
  • There should be an option to specify a class that implements Comparator (instead of a method name). Unfortunately you can't specify class instances like String.CASE_INSENSITIVE_COMPARATOR, so for strings I recommend also adding @Ordered.CaseInsensitive / caseInsensitive="true".

Should equals and compareTo == 0 be identical? e.g.
A. When a.equals(b) then a.compareTo(b) == 0
B. When a.compareTo(b) == 0 then a.equals(b)

The Java API does not have this restriction AFAIK.

Classes like HashMap and HashSet use equals() and hashCode() exclusively and classes like TreeMap and TreeSet only use compareTo. I can't find any class that uses both compareTo and equals.

My classes adhere to (A) but not to (B) and I have never experienced any problems with this!

@rzwitserloot rzwitserloot added the accepted The issue/enhancement is valid, sensible, and explained in sufficient detail label Aug 28, 2015
@Verdoso
Copy link

Verdoso commented Aug 28, 2015

My 2ec:
.- I'm not a big fan of having so many different annotations, I'd rather have less annotations (one if possible) with multiple parameters.
.- From the usubility point of view, I think some people might miss the name Ordered when looking on how to configure the equals/hashcode behaviour. For example, I have many entities I might use as keys in maps or just in sets, but they don't require any order, so I simply configure the real keys in EqAHC. Having an annotation named Ordered for that does not sound intuitive to me. Maybe something lilke @Identity? Not sure if being so similar to javax.persistence.Id that has similar goals is good or bad :D

Apart of that: +1.
Cheers!

@Maaartinus
Copy link
Contributor

Just some random and partly conflicting ideas. Please let me know which ones you like.

How should we handle the process of deprecating @EqAHC?

Maybe not at all? Later, there may be additional options for @EqAHC (e.g. precomputed or cached), which would not fit that well. Maybe we should keep it:

  • Add @Ordered as stated above and let it by default use the same fields as @EqAHC.
  • Deprecate include and exclude, add @EqAHC.Include and @EqAHC.Exclude (independent feature).
  • Use @Ordered.Include and @Ordered.Exclude for the cases when they use different fields (independent feature).

A big advantage of independent features is that someone could rather easily contribute them (generating methods and other magic is much harder than such modifications).


@Ordered.NullOrder - this should be allowed on the whole class and also in the configuration. Then the default doesn't really matter.

Ideally, combining NullOrder.NULL_IS_ERR with @Nullable should give a compile time error, but this is probably impossible, because of the fuzziness of @Nullable.

IMHO NULL_IS_ERR without lombok.@Nonnull should generate a warning, unless given explicitly on the field (then the user is supposed to know what they're doing).


Maybe there should be @Ordered.Special(booleans=FALSE_FIRST, strings=CASE_INSENSITIVE, arrays=LEXICOGRAPHICAL), or something like this making the common cases simpler.


@Ordered.Use(method = "methodName")

I guess, you want to use the boolean to tell if the fields are equal or not; true means "equal", but it's not obvious.

This could get cleaner with something like compareTo="methodName" and/or equals="methodName" and/or hashCode="methodName".

@Ordered.Use(comparator = ComparatorClass.class)

I'd maybe replace comparator by valuein order to make the more common (and nicer) case simpler to use.


default NULL_IS_MIN or NULL_IS_ERR?

I'd suggest the latter, overridable with a configuration key and a class-wide option.


When useGuava=true is on, use Guava ComparisonChain to make the generated class small.

@Maaartinus
Copy link
Contributor

I couldn't help myself and tried to make my ideas into a complete proposal.

Keeping @EqAHC leads IMHO to a clearer design. I added some options to it for completeness.

I'd use @CompareTo instead of @Ordered as it's more obvious for people used to @EqAHC and avoids the rather confusing @Ordered.Order(int) and a clash with org.springframework.core.Ordered. I'm sticking with @Ordered below.

Summary

  • @EqAHC
    • keep @EqAHC
    • (optional) add @EqAHC.Include and .Exclude, deprecate include and of
    • (more optional) add @EqAHC.Use(SomeEquivalence.class) where SomeEquivalence
      • has a no-args constructor
      • implements the two abstract methods doEquivalent(T, T) and doHash(T) of com.google.common.base.Equivalence (without necessarily extending it)
    • (more optional) add @EqAHC(precomputed=boolean, cached=boolean) where the options are mutually exclusive
  • @Ordered (or better @CompareTo)
    • add @Ordered using by default the same fields as @EqAHC uses (or would use) in the source file order
    • (optional) add @Ordered.Reverse
      • on fields meaning to reverse the sign of the single comparison
      • on classes meaning to reverse the sign of the result
    • (optional) add @Ordered.Order(int)
    • (optional) add @Ordered.Use(value=SomeComparator.class, compareTo="someMethodName") where always exactly one option must be used
    • (optional) add @Ordered.Special(value=Ordered.SpecialPolicy.XXX) allowing to specify multiple enum members

Details

@EqAHC

Out of the four possibilities for specifying fields, at most one can be used per class.

@EqAHC.Use(SomeEquivalence.class) is compatible with Guava without depending on it. The user has to implement the two methods as if they were extending Equivalence, but they may or may not extend it. IMHO linking to doEquivalent and doHash is way better than any own specification.

Obviosly, equivalent and hash sound better, but doEquivalent and doHash are the core methods freed of boilerplate (identity and null checks).

@Ordered.Special

This would allow the following options (names partly taken from Guava Ordering)

  • important
    • NULLS_FIRST
    • NULLS_LAST
    • NULLS_THROW
  • rather important
    • FALSE_FIRST
    • TRUE_FIRST
    • CASE_INSENSITIVE
    • CASE_SENSITIVE
  • fancy
    • LEXICOGRAPHICAL

It's a bit unstructured, but rather practical. Some combinations make sense, many do not, but for a String[] you can use

@Ordered.Special(specials={NULLS_FIRST, CASE_INSENSITIVE, LEXICOGRAPHICAL})

and the meaning is rather obvious (apart from NULLS_FIRST being applied to both the array and its members).

All nonsensical or not yet implemented combinations must raise an error.

The target should be fields and classes.

Comments

default NULL_IS_MIN or NULL_IS_ERR?

Doesn't matter much, when a corresponding configuration key and a class-wide option exist (and a warning is given for fields throwing on null and having neither @NonNull or NULLS_THROW on them).

Go with all these sub-annotations, or have one @Ordered.Conf annotation that has all that stuff as params instead?

A single annotation wouldn't work well because of its target (some make sense on fields only, others on both).

Even with all the fancy and optional stuff, the number of annotation is not that high and you hardly ever need more of them. Sure, there could be

@Ordered(reverse=true, nulls=NULLS_FIRST, strings=CASE_INSENSITIVE, booleans=FALSE_FIRST)

on a class and

@Ordered.Conf(exclude=false, order=2, use=SomeComparator.class)

on a field and maybe it's a good idea. I'd start with plain dumb @Ordered() and watch the demand.

Is the general concept of 'always treat equals, hashCode, canEqual, and compareTo as sides of the same coin' okay? There are some indications that java code out there uses different fields, but this doesn't sound like it can adhere to the rule that if a.equals(b), then a.compareTo(b) == 0 and vice versa.

This rule is "strongly recommended, but not strictly required", so a possibility to violate it should not be needlessly rejected. In such cases, I prefer to implement a Comparator instead, but if I could use a generated compareTo, I'd do it. By not conflating @EqAHC and @Ordered, the possibility remains open with an obvious syntax.

@Condor70
Copy link

Two remarks:

  1. I really don't like the annotation name "@Ordered.Conf". I would recommend simply naming it @Compare (and the one at class level @CompareTo as suggested).
  2. We don't need a "booleans" attribute. I would recommend using FALSE_FIRST as the default and using reverse="true" if you want TRUE_FIRST.

@Condor70
Copy link

There is something to be said about allowing both a nested class-level annotation and a field-level annotation for @Ordered.Conf. Some users like a clutter free class body and some like annotations to be as closely to the object it is related to (I prefer the latter).
This would mean that @Ordered.Conf needs a default "value" attribute to specify the field name (not needed when specified on a field).

It should also be possible to specify @Ordered.Conf on a getter method instead of a field (in case some extra calculations are required on the value).

@rzwitserloot
Copy link
Collaborator

Roel and I had a long talk on this and we did not reach any concensus. The major issues we just can't fix are:

  • We REALLY want to combine compareTo and equals/hashcode because they are almost always built the same way. NOT combining them results in serious issues when trying to include/exclude fields. Should @Ordered piggyback on @EqAHC's includes and excludes lists UNLESS @ordered has it's own? That's just not intuitive AT ALL, it's a vetoed non-starter here. If we combine them, what's the name? Identity was shot down because it often means the inverse (as in IdentityHashMap), Ordered is shot down because it is too suggestive of comparables. The final winners of the evening were some random placeholder name (@Ecco) or @Eq. This would be the annotation that does both.
  • Use in general shot down because it needs to know how to calculate hashCode too. That means we're indeed stuck with needing a class that implements / looks like guava's Equivalence, except we can't use that either, because we need a comparable result (negative/zero/positive), and that's not what Equivalence does. So now it's a huge mess that's stuck together with nonsense and structural typing, none of which is very 'java'. We currently have no non-vetoed plan to do anything for the concept of 'use', which is obviously bad news. Best bet is to specify a class that implements a lombok-specific class that MUST be local, at which point we can strip out the 'implements' part there so as to ensure that lombok is not needed at runtime. This doesn't sound right either.
  • An alternate plan of sticking with 2 annotations (@Eq and @Ordered most likely) runs into trouble with specifying details. We want a single exclusion/inclusion system, how do we do this? There are a number of settings that are only relevant to just one of those two, and others that are relevant to both. How do we handle that? One massive top-level @FieldSettings might work, but this becomes the clowncar of settings, as it would also have to contain @ToString's exclusions and settings (such as 'skipNulls' which optimally should be per-field configurable). This clown car plan doesn't sound very scalable or pretty either.

We ran around in circles and didn't get anywhere with a design that felt anywhere even close to right.

This proposal seems to be sinking for want of a name :/

@Condor70
Copy link

Condor70 commented Sep 1, 2015

What is the reason you want to include compareTo in @EqualsAndHashCode? Not every class that has a equals/hashCode needs a compareTo, so I would not have a problem with a separate @CompareTo annotation.

A @FieldSetting annotation also doesn't seem a good idea to me. Do you really need a single exclusion/inclusion system? I agree that the goal should be to be as concise as possible, but separating the settings into several annotations (e.g. @EqualsAndHashCode.Field, @tostring.Field and @CompareTo.Field) would make the concept easier to understand to first-time users and lower the learning curve.

@rzwitserloot
Copy link
Collaborator

We want to combine them because, IF want compareTo, you also want equals/hashCode, and almost always, you want the same batch of fields to define the behaviour of all those methods.

We want one annotation; that doesn't mean the annotation always generates compareTo. That'd be a choice. But the same annotation powers both of it (you can choose between generating equals/hashCode/canEqual, or equals/hashCode/canEqual/compareTo. Those are the only 2 choices).

Yes, we need a single exclusion/inclusion system, for all the reasons explained earlier. @EqAHC.Field and @Ordered.Field, while easy to understand, is therefore not feasible without an expansive analysis of how to marry the inclusion/exclusion lists.

@Condor70
Copy link

Condor70 commented Sep 1, 2015

I understand why you would want to combine equals/hashCode and compareTo, but you concluded yourself that this would cause almost too much trouble with the nested field configuration.

Maybe @CompareTo could borrow the field includes/excludes from @EqualsAndHashCode by adding an attribute like useEqualsAndHashCodeFields (with "true" being the default value).
That way you could easily use the same includes/excludes, but specifying a different set of includes (like I would be doing) would also be very easy.

@Condor70
Copy link

Condor70 commented Sep 1, 2015

Here you have some examples of what I'm suggesting:

Using nested annotations:

@EqualsAndHashCode(
    callSuper = true,
    fields = {
        @EqField(name = "id", doNotUseGetter = true),
        @EqField(name = "extra", exclude = true),
    }
)
@CompareTo(
    fields = {
        @CompField(name = "name", caseInsensitive = true)
    }
)
public class Test {
    private int id;
    private String name;
    private Object extra;
}

and

@EqualsAndHashCode(
    exclude = {"extra"}
)
@CompareTo(
    useEqualsAndHashCodeFields = false,
    fields = {
        @CompField(name = "id"),
        @CompField(name = "extra", comparator = CompareExtra.class),
    }
)
public class Test {
    private int id;
    private String name;
    private Object extra;
}

and/or using field-level annotations:

@EqualsAndHashCode(callSuper = true)
@CompareTo()
public class Test {

    @EqField(doNotUseGetter = true)
    private int id;

    @CompField(caseInsensitive = true)
    private String name;

    @EqField(exclude = true)
    private Object extra;
}

and

@EqualsAndHashCode()
@CompareTo(useEqualsAndHashCodeFields = false)
public class Test {

    @CompField()
    private int id;

    private String name;

    @EqField(exclude = true)
    @CompField(comparator = CompareExtra.class)
    private Object extra;
}

@ckorakidis
Copy link

What if you provide just one annotation (EqualsAndHashCodeAndCompareTo) which implements a compareTo method based on the fields configuration of the annotation and this compareTo is just used in equals as well (return 0 == this.compareTo(other))

@Maaartinus
Copy link
Contributor

Some more ideas...

Should @Ordered piggyback on @EqAHC's includes and excludes lists UNLESS @Ordered has it's own? That's just not intuitive AT ALL

Then forget it and use 3 annotations: @EqAHC, @Ordered, and @EqOrdered. Let the last one govern common options and let it be overridable by the specific ones. Use also corresponding field level annotations. This is a bunch of annotations, but their number does not make anything worse.

Ordered is shot down because it is too suggestive of comparables.

Sure, @Ordered generating no compareTo by default sounds wrong, so what about @EqOrdered?

Yes, we need a single exclusion/inclusion system, for all the reasons explained earlier. @EqAHC.Field and @Ordered.Field, while easy to understand, is therefore not feasible without an expansive analysis of how to marry the inclusion/exclusion lists.

Maybe our includes/excludes thinking is a part of the problem. The two things are symmetrical, but only as long as you ignore configuration. When you exclude a field, then there's nothing more to configure. So maybe there should be nested @*.Exclude (parameterless) and @*.Include (with some parameters) annotation. Of course, you can use @_.Field(exclude=true)and@_.Field(option1=..., option2=....) instead of the two annotations. Combining them is trivial:

  • a field level annotation wins over class level annotation
  • otherwise, @EqAHC and @Ordered win over @EqOrdered
  • combining @X.Include and @X.Exclude with the same X is nonsensical and therefore forbidden (in case of sticking with @*.Field: combining exclude=true with any other option is forbidden).

Also add includeNone, which excludes all fields by default (so only those included on field level get in).

Let me try:

  • keep @EqAHC, deprecate include and of, add includeNone=false
  • add @Ordered(reverse=false, includeNone=false) (which just generates compareTo)
  • add @EqOrdered(includeNone=false) as a shortcut for both @EqAHC and @Ordered
  • add @EqAHC.Exclude, @Ordered.Exclude, and @EqOrdered.Exclude
  • add nested *.Include field level annotations allowing to specify details:
    • @EqAHC.Include(equivalence=..., order=...)
    • @Ordered.Include(reverse=..., comparator=..., order=...)
    • @EqOrdered(order=...)

@eaaltonen
Copy link

Somewhat related. I dream of someday being able to write

public @NamedTuple class Foo {
    private int bar;
    private String baz;
}

and get a Java class that I would resemble a python namedtuple. In addition to the current @Data, a @NamedTuple would provide map like access to the data elements.

Rough sketch

Foo foo = Foo(1, "str")
foo.entrySet() == [ "bar", "baz" ] 
foo.get("bar") == 1
for (Object value : foo.values()) {
    ...
}
for (Entry e : foo.entrySet()) {
    ...
}

More to the point

We can look at how other languages deal with this. In Python tuples are sorted lexicographically (first `bar, then baz above). If some other ordering is required, the general practice seems to be to define a new Comparator as required.

As for the name of the new annotation, I offer @CompareEqualsAndHashCode, with an optional comparator class argument.

@svenhaag
Copy link

svenhaag commented Aug 9, 2017

Hi, I'd like to see this in future versions. Please add a milestone ;).

@rzwitserloot
Copy link
Collaborator

lianhaijun pushed a commit to lianhaijun/lombok that referenced this issue May 8, 2020
* added example for issue projectlombok#512

* fix removing of field annotations during delombok

* cleanup code

* added to changelog projectlombok#530

* added support for ImplicitResourceCloser extension point for @cleanup projectlombok#287

* master is now for: up from 2018.1 IntelliJ builds
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted The issue/enhancement is valid, sensible, and explained in sufficient detail
Projects
None yet
Development

No branches or pull requests

8 participants