2

I have code from two companies asoft and bsoft. I cannot change either. This is a simplified version of my situation which I'm pretty sure has enough information to the find what's causing the problem.

bsoft provides IGang, which represents a gang that can battle other gangs.

package bsoft;

public interface IGang {
    /** @return negative, 0, or positive, respectively
     *          if this gang is weaker than, equal to, or stronger
     *          than the other
     */
    public int compareTo(IGang g);
    public int getStrength();
    public String getName();
    public void attack(IGang g);
    public void weaken(int amount);
}

asoft provides GangWar, which allows IGangs to battle:

package asoft;
import java.util.*;
import bsoft.*;
/** An `IGang` ordered by identity (name) */
public interface ComparableGang extends IGang, Comparable<IGang> {}

package asoft;
import java.util.*;

public class GangWar {
    public final Set<ComparableGang> gangs = new TreeSet<ComparableGang>();
    public void add(ComparableGang g) {gangs.add(g);}
    public void doBattle() {
        while (gangs.size() > 1) {
          Iterator<ComparableGang> i = gangs.iterator();
          ComparableGang g1 = i.next();
          ComparableGang g2 = i.next();
          System.out.println(g1.getName() + " attacks " + g2.getName());
          g1.attack(g2);
          if (g2.getStrength() == 0) {
              System.out.println(g1.getName() + " smokes " + g2.getName());
              gangs.remove(g2);
          }
          if (g1.getStrength() == 0) {
              System.out.println(g2.getName() + " repels " + g1.getName());
              gangs.remove(g1);
          }
        }
        for (ComparableGang g : gangs) {
            System.out.println(g.getName() + " now controls the turf!");
        }
    }
}

It requires the additional constraint that the Gangs you supply to it are Comparable, presumably so it can sort by name or avoid duplicates. Each gang (in an arbitrary order, Set order used here for simplicity) attacks another gang, until only one gang is left (or no gangs, if the last two have a tie). I've written a simple implementation of ComparableGang to test it out:

import asoft.*;
import bsoft.*;
import java.util.*;

class Gang implements ComparableGang {
    final String name;
    int strength;

    public Gang(String name, int strength) {
        this.name = name;
        this.strength = strength;
    }

    public String getName() {return name;}
    public int getStrength() {return strength;}

    public int compareTo(IGang g) {
        return strength - g.getStrength();
    }

    public void weaken(int amount) {
        if (strength < amount) strength = 0;
        else strength -= amount;
    }

    public void attack(IGang g) {
        int tmp = strength;
        weaken(g.getStrength());
        g.weaken(tmp);

    }

    public boolean equals(Object o) {
      if (!(o instanceof IGang)) return false;
      return name.equals(((IGang)o).getName());
    }
}

class Main {
   public static void main(String[] args) {
       GangWar gw = new GangWar();
       gw.add(new Gang("ballas", 2));
       gw.add(new Gang("grove street", 9));
       gw.add(new Gang("los santos", 8));
       gw.add(new Gang("triads", 9));
       gw.doBattle();
   }
}

Testing it out...

$ java Main
ballas attacks los santos
los santos repels ballas
los santos attacks grove street
grove street repels los santos
grove street now controls the turf!

The problem is, triads do not show up to the fight. In fact, printing gangs.size() right at the start of doBattle() returns 3 instead of 4. Why? How to fix it?

Dog
  • 7,707
  • 8
  • 40
  • 74
  • @RobWatts, I see now I should have included the javadoc for `ComparableGang`, fixed. – Dog Apr 03 '13 at 18:48
  • 2
    Okay, it seems like there a contradiction in the specifications. The IGang interface's documentation wants them sorted by strength, but the ComparableGang wants them sorted by name. – Rob Watts Apr 03 '13 at 18:49

4 Answers4

5

The problem is, triads do not show up to the fight. In fact, printing gangs.size() right at the start of doBattle() returns 3 instead of 4. Why?

Both triads and grove street have a strength of 9. Therefore they're equal in terms of Gang.compareTo (implementing Comparable). Therefore only one is permitted in a TreeSet.

If you don't want to remove items which are duplicates in terms of their sort order, don't use a TreeSet...

EDIT: The ComparableGang interface description indicates what's expected:

/** An `IGang` ordered by identity (name) */
public interface ComparableGang extends IGang, Comparable<IGang> {}

Your compareTo method does not order "by identity (name)" - it orders by strength. To be honest, it's a pretty stupid interface in the first place, as it would have been perfectly easy for asoft to create a class of public class GangNameComparator : Comparator<IGang>, and then supply that as the comparator to the tree set if they wanted to order by name.

However, as they're suggesting that you should implement the comparison, you need to do so as the interface describes:

public int compareTo(IGang g) {
    return name.compareTo(g.getName());
}

However... as you note in comments (and as noted in Rob's answer), this then contradicts the convention-bustingly-named IGang description:

public interface IGang {
    /** @return negative, 0, or positive, respectively
     *          if this gang is weaker than, equal to, or stronger
     *          than the other
     */
    public int compareTo(IGang g);
}

It's impossible to implement ComparableGang to satisfy both its own documentation and the IGang documentation. This is basically broken by design, on asoft's part.

Any code should be able to use an IGang implementation, knowing only about IGang, and relying on the implementation following the IGang contract. However, asoft broke that assumption by requiring different behaviour in an interface extending IGang.

It would have been reasonable for them to add more requirements in ComparableGang, so long as they didn't violate the existing requirements of IGang.

Note that this is an important difference between C# and Java. In C#, two functions in two different interfaces with the same signature can be combined into one interface that inherits both of them and the two methods remain distinct and accessible. In Java, the two methods, since they are completely abstract and have the same signature, are considered to be the same method and a class implementing the combined interfaces has only one such method. So in Java ComparableGang is invalid because it cannot have an implementation of compareTo() that satisfies the contract of ComparableGang and the contract of IGang.

Community
  • 1
  • 1
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • `GangWar` only takes a `ComparableGang`, which is meant to totally order `IGang`s by identity (name). So I see nothing wrong with the `GangWar` class in itself. – Dog Apr 03 '13 at 18:34
  • @Dog `TreeSet` uses the `compareTo` method to order elements. Your `compareTo` method uses the strengths of gangs. So `9 - 9` for `triads - grove street` equals 0, which means they are equal. `TreeSet` will get rid of one. – Sotirios Delimanolis Apr 03 '13 at 18:39
  • @Dog: Where do you see anything which *orders* by name? You're checking *equality* by name (and not overriding `hashCode`, by the way, which is a bug) but that's not ordering. – Jon Skeet Apr 03 '13 at 18:45
  • @JonSkeet: Sorry about the confusion, I updated the question to include the javadoc for `ComparableGang`. – Dog Apr 03 '13 at 18:50
  • @Dog: Well that just means that you shouldn't be using `TreeSet`. Do you understand that `TreeSet` uses `compareTo` in order to determine ordering *and equality*? – Jon Skeet Apr 03 '13 at 19:05
  • @JonSkeet: I'm not using `TreeSet`, asoft is. I realize now my `Gang` needs to implement `Comparable.compareTo`, but that would violate `IGang.compareTo`. I didn't notice I need to implement `Comparable.compareTo` because the typechecker didn't tell me. I guess I need to implement both somehow? – Dog Apr 03 '13 at 19:13
  • @Dog: It's not clear what you mean, but fundamentally you're violating the interface *description*: "An `IGang` ordered by identity (name)" Your ordering isn't by name. I'll edit this into my answer. – Jon Skeet May 08 '13 at 17:26
  • But if I order by name then it will violate `IGang.compareTo`. – Dog May 08 '13 at 17:53
  • @Dog: Then fundamentally the two interfaces contradict each other. It's conceptually broken. I've explained why you're getting fewer entries than expected... I'm not really sure what else there is to say at this point. – Jon Skeet May 08 '13 at 17:54
  • How is it broken? Which part? Is it asoft's fault for not checking disjointness of each signature of CompareableGang's superinterfaces? If that's the case, is there a way to make the compiler/IDE detect this so people don't have to check by hand? I was thinking there might be a way to look at the stack at runtime to see which method (`Comparable.compareTo` vs `IGang.compareTo`) is being called, or something like this. – Dog May 08 '13 at 18:03
  • 1
    @Dog: It's broken because it's telling you to write a single method which does two different things. And no, there's no way that the compiler can detect this, because the discrepancy is in the *description*. – Jon Skeet May 08 '13 at 18:22
  • Are you talking about `ComparableGang`? How can the compiler not detect this? It just needs to check that there are no 2 superinterfaces `A` and `B` such that `A.f =sig= B.f`, where `=sig=` denotes equivalent signatures. – Dog May 08 '13 at 18:38
  • @Dog: The compiler can detect that `compareTo` is specified by both `ComparableGang` and again in `Comparable` as part of `IGang` - but how is it meant to know that the two descriptions are incompatible? There's nothing inherently wrong with two interfaces specifying the same member, so long as they can be implemented correctly by the same method. – Jon Skeet May 08 '13 at 18:46
  • @JonSkeet, I was assuming it would just assume that they are incompatible... If I have `A implements AI` and `B implements BI`, then `A.f != B.f`, and the compiler would prevent such use. I'm not sure how to tell `asoft` that there interface has a bug. Isn't there a way to check at runtime inside `compareTo` whether it should behave as `IGang` or `Comparable`? – Dog May 08 '13 at 18:51
  • @Dog: No, it doesn't assume they're incompatible, and often they won't be. And no, there's no way to tell whether the method is being invoked "via" one interface or another. (Imagine two callers, both creating a `TreeSet`, but with different expectations. In both cases it would be `TreeSet` calling the code... )You really *can't* work round this - it's broken by design. – Jon Skeet May 08 '13 at 18:58
  • Hmm, is there a way to infer through the bytecode of the caller? Maybe there is an instruction that captures the interface. What's broken by design? Who's fault is this? – Dog May 08 '13 at 19:03
  • @Dog: No, there really isn't a way round this. It's asoft's fault for creating `ComparableGang` with an *unimplementable* interface. What's the point of creating an interface which *cannot* be implemented correctly? – Jon Skeet May 08 '13 at 19:08
  • I was thinking it's asoft's fault, but how was asoft even supposed to know that two of their superinterfaces have clashing methods? They don't even necessarily know `compareTo` exists, since they never call it directly. – Dog May 08 '13 at 19:13
  • @Dog: They *must* know about `IGang` because their interface extends `IGang`. So it's their fault for not reading the documentation for `IGang`. Just because they don't *call* it doesn't mean they don't know it exists. – Jon Skeet May 08 '13 at 19:16
3

TL;DR: Use B) Below

From the javadoc for Comparable (and same for Comparator too!):

The natural ordering for a class C is said to be consistent with equals if and only if e1.compareTo(e2) == 0 has the same boolean value as e1.equals(e2) for every e1 and e2 of class C. Note that null is not an instance of any class, and e.compareTo(null) should throw a NullPointerException even though e.equals(null) returns false.

In your case (simplified),

  • equals is defined as equality of name
  • compareTo is defined as comparison of strength

This fails the above condition:

  • when the two strengths are equal, but the two names are different
  • when the two names are equal, but the two strengths are different (probably a condition avoided by your application logic)

Answer

How to correct?

A) If your requirements allow you to have the set sorted by name (consistent with comment in asoft code):

 // will only return 0 if the strengths are equal AND the names are equal
 public int compareTo(ComparableGang g) {
     return name.compareTo(g.getName());
 }

B) If your requirements force you to have the set sorted by strength (and then name) (consistent with comment in bsoft code).

 // will return 0 if & only if the strengths are equal AND the names are equal
 public int compareTo(ComparableGang g) {
     int result = strength - g.getStrength();
     if (result == 0) result =  name.compareTo(g.getName());
     return result;
 }

 // will return true if & only if the strengths are equal AND the names are equal
 public boolean equals(Object o) {
     if (!(o instanceof ComparableGang)) return false;
     ComparableGang gang2 = (ComparableGang)o;
     return name.equals(gang2.getName()) && strength == gang2.getStrength();
 }

 // For this case, if it's illegal to have two gangs of same name but different 
 // strength (it should be illegal!), then app logic must enforce this - the Set 
 // no longer will.

Comment 1: Whilst it's an issue for you to modify asoft's GangWar class, it would be better if you could place above two methods of B) into:

 class ComparableGangComparator implements Comparator<ComparableGang> {
 }

and then modify how GangWar constructs the Set:

 public final Set<ComparableGang> gangs = new TreeSet<ComparableGang>(
                                                  new ComparableGangComparator());

That way, you can leave the two methods of A) within class Gang - leaving the class with it's "true" equals & compareTo from an object identity POV.

Comment 2: Contradicting comments on asoft's & bsoft's compareTo methods

From a theoretical POV: If asoft's comment is not a typo, then not only has asoft extended bsoft's interface, but they've changed the required behaviour of one of the methods. This is not actually a contradiction at all - it's an override: asoft's comment "wins".

From a practical POV: You need your fingers crossed asoft did this on purpose & the comment is correct. If it's a typo from asoft, then bsoft's comment is better & bsoft "wins". You could send a query to asoft or check their doc/examples to confirm.

Glen Best
  • 22,769
  • 3
  • 58
  • 74
  • I don't see how this is *not* a contradiction. Any code which is expecting to work with a value of type `IGang` *should* be able to rely on the implementation obeying the documentation. Anything implementing `ComparableGang` "correctly" is *not* implementing `IGang` correctly. The idea that asoft's comment "wins" is hardly useful to any code which only knows about bsoft's interface. This breaks Liskov's Substitution Principle completely. – Jon Skeet May 09 '13 at 06:25
  • Calm there :) :). Agreed. In good design, any subtype *should* be able to work in place of the type. That means you're following the strongly-typed-behaviour principle. But, unfortunately, that's not mandatory for code to run. But vendor spec is mandatory to follow, if correct. – Glen Best May 09 '13 at 06:39
  • These are both vendors, as far as I can see - and while yes, the code will run, I still can't see how you can claim this is "not actually a contradiction at all". asoft's design is simply broken, by contradicting bsoft's documentation. – Jon Skeet May 09 '13 at 07:11
  • Respect your opinion. I assume asoft's product is basically functional. Their product incorporates bsoft's and they're responsible for implementing & spec'ing the integration. – Glen Best May 09 '13 at 07:28
  • And they've proved that they're no good at it, by specifying contradictory requirements, in my view. It's simply bad API design - it means that anyone using an `IGang` can no longer rely on the documented behaviour. – Jon Skeet May 09 '13 at 07:37
  • I thought of combining the specs of each `compareTo` into one like you say in **B**, but this will break because users of `IGang` expect two gangs of the same strength to have `compareTo` returning `0`. – Dog May 09 '13 at 14:31
  • @JonSkeet, I think you're forgetting something about interfaces. Two methods in two interfaces with the same name+sig really shouldn't be considered the same method as far as I can tell. The name of the methods (modulo ad-hoc polymorphism) should really be a tuple `(interface, name)`. This is probably how asoft sees it, they just don't know that Java merges the methods. Apparently C# lets you give the full name: http://stackoverflow.com/questions/4103300/c-sharp-why-implement-interface-explicitly Is C# fundamentally different than Java or is Java just missing this feature? – Dog May 09 '13 at 14:36
  • I see you edited the answer adding, "But in asoft's GangWar.doBattle(), final loop needs sorting by strength". That final loop is just shorthand for getting the _one_ element in the set, or doing nothing if it's empty. It cannot be reached if there are multiple items in the set. I don't see how it needs sorting by strength, can you elaborate on this? – Dog May 10 '13 at 13:19
  • Thanks! I see condition: gangs.size() == 1 by the end. OK, I'll remove edit... Either sort will work as far as that algorithm goes. – Glen Best May 10 '13 at 13:54
  • @Dog: (I've only just seen this for some reason.) You say they shouldn't, but they are as far as Java is concerned. If asoft is writing Java without understanding the language, that's a failure on asoft's part as far as I'm concerned. And yes, C# is fundamentally different here. – Jon Skeet May 12 '13 at 17:01
  • Yeah, agreed. Asoft should've defined a better interface. They didn't, giving rise to the Q. The real world oft throws up non-ideal constraints. Component/systems integration is often a challenge - much of it can be due to semantic inconsistencies, overlaps and gaps. Common in v large scale heterogeneous enterprise integration envs where java often used. I like this trail on (basic, 25 y.o.) topic of extension v composition: http://programmers.stackexchange.com/questions/65179/where-does-this-concept-of-favor-composition-over-inheritance-come-from. Steven A. Lowe gives a worthy rant, too. – Glen Best May 14 '13 at 05:29
1

The Gang.compareTo method is based on their strengths, so since the triads and grove street have the same strength, the TreeSet thinks they are equal and then removes them.

Based on how the ComparableGang expects to sort them, I'd say ignore the IGang interface's request for the behavior of compareTo and change it to this.

public int compareTo(IGang g) {
    return name.compareTo(g.getName());
}
Rob Watts
  • 6,866
  • 3
  • 39
  • 58
  • This violates the `IGang` interface. `IGang.compareTo` compares the strengths. Presumably, this is so one gang can check out the strength of the other before deceiding to blindly attack it. – Dog Apr 03 '13 at 18:36
  • Since you can't modify `GangWar`, you're going to have to violate the `IGang` interface. It's too bad that asoft and bsoft put you in the position where either way you're going to violate someone's assumptions. – Rob Watts Apr 03 '13 at 18:57
0

Fundamentally the problem is that a ComparableGang is an IGang, but IGangs sort by strength and ComparableGangs sort by name, so a ComparableGang is not an IGang.

TL;DR The right way to fix this is to fix the interfaces and library code. The workaround for writing code to work with both libraries is explained in this answer.


The best solution is to fix both interfaces. bsoft only needs to change one line of their code to:

public interface IGang extends Comparable<IGang> {

Nothing else in the interface or any other code needs to change, but asoft would then be on notice that IGang's are already comparable. (EDIT: On second thought, since IGang.compareTo() is not consistent with equals(), which is the root of why the triads are not showing up to the fight, bsoft probably did the right thing in not extending Comparable. What they did wrong was declaring compareTo() instead of, say, compareStrengthTo().)

asoft doesn't need to wait for bsoft to change anything, though. It's really their fault to begin with that they created a broken-by-design interface. They should have just picked a way to get a Comparator<IGang> that sorts by name. So if I worked at asoft, GangWar would look more like this:

public class GangWar {
    public final Set<IGang> gangs;
    public GangWar(Comparator<IGang> gangNameComparator) {
        gangs = new TreeSet<IGang>(gangNameComparator);
    }
    public void add(IGang g) {gangs.add(g);}
    public void doBattle() {
        while (gangs.size() > 1) {
          Iterator<IGang> i = gangs.iterator();
          IGang g1 = i.next();
          IGang g2 = i.next();
          System.out.println(g1.getName() + " attacks " + g2.getName());
          g1.attack(g2);
          if (g2.getStrength() == 0) {
              System.out.println(g1.getName() + " smokes " + g2.getName());
              gangs.remove(g2);
          }
          if (g1.getStrength() == 0) {
              System.out.println(g2.getName() + " repels " + g1.getName());
              gangs.remove(g1);
          }
        }
        for (IGang g : gangs) {
            System.out.println(g.getName() + " now controls the turf!");
        }
    }
}

So rather than completely breaking IGang, they just ask for what they need (that IGang doesn't have) when they need it. With this change (and a Comparator implementation), the program outputs:

ballas attacks grove street
grove street repels ballas
grove street attacks los santos
los santos repels grove street
los santos attacks triads
triads repels los santos
triads now controls the turf!

Of course, if you're stuck with the libraries as is, you can read this answer for the general approach to living with the breakage without touching the library.

Community
  • 1
  • 1
Old Pro
  • 24,624
  • 7
  • 58
  • 106
  • "What they did wrong was declaring compareTo() instead of, say, compareStrengthTo()" Are you saying that bsoft should have looked at every other Java class ever made before creating `IGang` to make sure that none of the method's coincide? – Dog May 19 '13 at 00:17
  • @Dog Mostly I'm blaming asoft for this mess and no, I'm not saying bsoft should have looked at _every_ class ever made. But bsoft is a _commercial vendor_ of a _Java software library_. I'm saying that bsoft should be familiar enough with the **Java standard library** and how it does comparisons to know that the standard way to do comparisons is to extend `Comparable` and implement `compareTo()`. They should also know enough to either implement `Comparable` or not implement `compareTo()`. – Old Pro May 19 '13 at 00:48
  • bsoft didn't want to implement `Comparable` or they would have. It just so happens that they named the method `compareTo`, should they have spent hours thinking up the ultimate method name? – Dog May 19 '13 at 01:16
  • @Dog, bsoft _did_ implement `Comparable`. That's the source of the conflict. We've spent hours trying to undo the resulting mess, so yes, they should have thought about the problems created by implementing `compareTo()` this way. Designing clean APIs is one of the major tasks of a library developer. – Old Pro May 19 '13 at 03:47
  • By bsoft do you mean asoft? look at the question again, bsoft is the first code snippet right at the top, bsoft doesn't implement `Comparable` anywhere. bsoft created `IGang`, then years later, asoft created their code that uses bsoft's `IGang`. asoft and bsoft did not collaborate at all, so bsoft didn't know that asoft would make a `Comparable` `IGang`. – Dog May 19 '13 at 04:04
  • I meant bsoft forces anyone implementing IGang to implement everything required of the `Comparable` interface without telling anyone that they are, in fact, implementing `Comparable`. That's why you can add the `extends Comparable` to the existing interface without changing any other code. But seriously, enough about bsoft. asoft is the real culprit. They should have noticed that IGang already declared `compareTo()`. That only requires them to read the two interfaces they are extending in `ComparableGang` and see that the same function appears in each of them. – Old Pro May 19 '13 at 04:13
  • `IGang` does not implement `Comparable`. bsoft clearly had no intention of implementing `Comparable`, they just so happened to use the same name. First you say bsoft doesn't have to look at all classes ever made to make sure they aren't conflicting names, then you say they shouldn't have used `compareTo` because the name conflicts with some other interface they don't even care about. Java does not use duck typing. If it helps, pretend that `compareTo` in `IGang` is really called `compareStrength`. Yes, if anything asoft should have avoided making the problem. – Dog May 19 '13 at 14:51
  • @Dog, It very much helps if `compareTo` in `IGang` is really called `compareStrength`. It ***completely*** eliminates the problems!!! Try it yourself. If you don't understand that this change fixes the problem then study Java until you do and quit wasting our time. – Old Pro May 19 '13 at 17:44