Porównywanie obiektów niejednorodnych

0

Mam taką trochę męczącą potrzebę zaimplementowania metod compareTo oraz equal dla obiektów różnych klas implementujących wspólny interfejs Comparable<Base>. Obie metody muszą oczywiście spełniać wszystkie warunki dla poprawnie napisanych metod equal i compare (np. przechodniość, zwrotność itd.) oraz muszą być ze sobą spójne (zachowane równości i nierówności). Metoda compareTo (inaczej niż zwykle) nie może nigdy rzucić wyjątkiem ClassCastException - nawet jeżeli porównane muszą by dwa obiekty, które mają wspólną bazę, ale same pochodzą z różnych gałęzi dziedziczenia. W ostateczności najwyższą wspólną bazą jest klasa Base i wtedy obiekty takie muszą być porównane za pomocą jej porządku (lub pierwszej wcześniejszej wspólnej bazy). Gdyby zdarzyło się porównywać z nullem, to zamiast rzucania wyjątkiem każdy null musi być z definicji największy, więc sortowany na końcu przy porządku rosnącym (ale to szczegół).

Trudność polega na tym, że tylko obiekt klasy potomnej wie jak porównać siebie z obiektem swojej klasy i nadrzędnych. Oznacza to, że obiekt nadklasy musi delegować porównania i równości do metod equals i compareTo obiektu klasy potomnej oraz wywołać jej metodę compareTo (lub podobną) z pierwszej znalezionej wspólnej klasy bazowej dla obiektów z różnych drzew dziedziczenia. Poszczególne klasy mogą porównywać po swoich polach, wykorzystywać porównania z klas nadrzędnych lub nawet trywialnie odwoływać się do porównania z nadklasy (kiedy żadne nowe właściwości zmieniające porównanie/równość nie doszły), więc przy okazji trzeba systemowo ominąć rekurencję gdy nadklasa odwoła się z porównaniem do podklasy, a ta do porównania odwoła się do swojej nadklasy, czyli z powrotem do tej samej samej klasy.

Jestem niemal pewien, że był na to jakiś wzorzec, ale jego nazwa kompletnie wyleciała mi z głowy (o ile był), więc kombinuję właśnie nad implementacją tego. Byłoby miło gdyby ktoś przypomniał mi jego nazwę (patterna, wtedy wygoogluję) lub ewentualnie zaproponował szablon metod compareTo i equals, który spełniałyby powyższe warunki.

ps. Byłoby dobrze, żeby ominąć refleksję przy wywoływaniu porównywania właściwego dla klasy nadrzędnej (ale nie wiem czy to możliwe inaczej niż przez serię else-if z isAssignableFrom i getSuperClass).

ps2. Każda klasa z łańcucha jest niezmienna (nie ma mutatorów) za wyjątkiem faktu dziedziczenia i konieczności użycia metod wirtualnych.

0
  1. W metodach masz zawsze dostęp do metod nadklasy za pomocą słowa kluczowego super, np możesz wywołać super.metoda2() w metoda1().
  2. Aby uniknąć isAssignableFrom itp możesz spróbować dodać metodę do każdego obiektu:
boolean canEqual(final Object that) {
  return that instanceof KlasaWKtórejSięZnajduję;
}

I teraz możesz robić w equals(final Object that) oprócz rzeczy typu:

that instanceof KlasaWKtórejSięZnajduję

Także rzeczy typu:

that.canEqual(this);

Ciekawostka: Metoda canEqual jest generowana z automatu w Scali, ale nie wiem czy dla wszystkich obiektów.

0
  1. To cały czas wykorzystuję, a także takie rzeczy jak B.this.metoda() czy C.super.metoda(). Brakuje mi trochę możliwości aby super stało się zmienną i mogło sobie wędrować przez całe drzewko parentów tak jak Class<?>. Albo przy klasach A->B->C->D choćby coś takiego jak wołana z D super.super.super.metoda() zamiast konstrukcji A.super.metoda() czy B.super.metoda(). Mogę to zrobić poprzez Class, ale potem muszę chyba użyć refleksji, żeby wywołać konkretną metodę.
  2. Ciekawe - muszę sobie przemyśleć czy i jak tego użyć.

Na razie wymodziłem coś takiego, które daje się skompilować:

	private static class Base implements Comparable<Base>
	{
		//...
		@Override public int compareTo(Base other)
		{	//deleguje wywołanie tej metody z klasy pochodnej jeżeli
			//nie jest identycznego typu
			return other == null ? 1 : other.getClass() == this.getClass() ?
				Base.this.objectCompareTo(other) : other.compareTo(this);
		}

		protected int objectCompareTo(Base other)
		{
			return name.compareTo(other.name);
		}

		private Cut name;
	}

	private static class Derived1 extends Base
	{
		//...
		@Override public int compareTo(Base other)
		{
			if(other == null) return 1; //nulle na koniec
			Class<?> thisClass = this.getClass(), otherClass = other.getClass();

			if(!(other instanceof Derived1)) //klasa nadrzędna lub inna
			{	//klasa z innego drzewa dziedziczenia
				if(!otherClass.isAssignableFrom(thisClass))
				{
					do thisClass = thisClass.getSuperclass();
					while(!thisClass.isAssignableFrom(otherClass));
					int result = 0;
					try
					{
						Method objectCompareTo =
							thisClass.getMethod("objectCompareTo", otherClass);
						result = (int)objectCompareTo.invoke(this, other);
					}
					catch(NoSuchMethodException | SecurityException
						| IllegalAccessException | InvocationTargetException
						ignored) {}
					return result;
				}
				else //klasa nadrzędna
					//Base najpierw, bez dodatkowego sprawdzania
					return -1; //Base.super.objectCompareTo(inna);
			}
			else if(thisClass == otherClass) //ta sama klasa
				return Derived1.this.objectCompareTo(other);
			else //klasa potomna
				return other.compareTo(this); //delegacja do klasy potomnej
		}

		@Override protected int objectCompareTo(Base inna)
		{	//inna jest typu Derived1 lub potomną
			final Derived1 other = (Derived1)inna; //bezpieczna konwersja?
			int result = this.nrGrupy - other.nrGrupy;
			if(result != 0)
				return result;
			return this.nrIndeksu - other.nrIndeksu;
		}

		private short nrGrupy, nrIndeksu; //dane pochodzą z uszczegółowienia super.name
	}

Metoda objectCompareTo służy mi do wywołania porównania na danych tylko dla bieżącej klasy i ew. nadklas poprzez X.super.objectCompareTo. Kolejne Derived2, Derived3 są podobnie skonstruowane jeżeli chodzi o compareTo i objectCompareTo.
Wywołań z refleksji wolałbym uniknąć z powodu sporej degradacji wydajności bo porównań będą miliony. A już samo delegowanie zrobi widoczny narzut.

1

Czy przypadkiem w twoim kodzie...

return Derived1.this.objectCompareTo(other);

...nie redukuje się do...

return this.objectCompareTo(other);

...a więc do nieskończonej rekurencji?

Na szybko pomyślałem i hmm może coś takiego:

class C extends Base (lub pochodna) implements Comparable<Base> {
  boolean canEqual(final Object that) {
    return that instanceof C;
  }

  int compareTo(final Base other) {
    final boolean jestemNadklasąLubRówny = other instanceof C;
    final boolean jestemPodklasąLubRówny = other.canEqual(this);
    if (nadklasa && podklasa) {
      // ta sama klasa, porównujemy tutaj bezpośrednio
    } else if (nadklasa) {
      return -other.compareTo(this);
    } else if (podklasa) {
      // other jest wyżej w hierarchii, więc porównujemy tutaj bezpośrednio
    } else {
      return super.commonCompare(other);
    }
  }

  int commonCompare(final Base other) {
    if (other instanceof C) {
      // jesteśmy w najniższym wspólnym przodku this i other, porównujemy tutaj
    } else {
      return super.commonCompare(other);
    }
  }
}

więc przy okazji trzeba systemowo ominąć rekurencję gdy nadklasa odwoła się z porównaniem do podklasy

Nadklasa ma rzutować obiekt na podklasę? To mi wygląda na złamanie obiektowości. Klasa nadrzędna nie może nic wiedzieć o klasach podrzędnych.

0
Wibowit napisał(a):

Czy przypadkiem w twoim kodzie...

return Derived1.this.objectCompareTo(other);

...nie redukuje się do...

return this.objectCompareTo(other);

...a więc do nieskończonej rekurencji?

Te wywołania są tożsame tylko w obiekcie klasy Derived1. Gdyby ten kod (podobny) wykonano w jakimś obiekcie klasy potomnej od Derived1, wtedy nadal wywołana zostanie wersja z Derived1, a nie jakaś hipotetyczna przeładowana nowa wersja. Poza tym objectCompareTo nie ma żadnych wywołań do metod polimorficznych - zwraca tylko porównanie z bieżącego typu obiektu, ewentualnie mogłaby wywołać te same porównania z klas bazowych, ale one też nie przekażą nigdzie dalej sterowania. tą metodę pomyślałem jako porównanie specyficzne dla konkretnej klasy w przeciwieństwie do compareTo, która przekazuje sterowanie gdzie indziej jeżeli sama nie potrafi porównać.

Na szybko pomyślałem i hmm może coś takiego:

Dzięki. Zawsze przyda się inny punkt widzenia. Szczególnie commonCompare rozwiązuje mój problem znalezienia wspólnego przodka - chodziło mi coś podobnego po głowie, ale ubzdurało mi się, że nie potrzebuję innej metody polimorficznej, aby to zrobić. Muszę sprawdzić jak wychodzi z warunkami bo może symetryczność tutaj padnie, ale muszę i tak przetestować wszystkie 5 warunków dla compareTo (i analogicznie equals).

więc przy okazji trzeba systemowo ominąć rekurencję gdy nadklasa odwoła się z porównaniem do podklasy

Nadklasa ma rzutować obiekt na podklasę? To mi wygląda na złamanie obiektowości. Klasa nadrzędna nie może nic wiedzieć o klasach podrzędnych.

Nie musi rzutować. Ponieważ w przypadku porównania nadklasy i podklasy tylko podklasa wie jak prawidłowo porównać, więc wywołanie z nadklasy this.compareTo(potencjalnaPodklasa) można z zachowania symetrii zamienić na -potencjalnaPodklasa.compareTo(this). A to wywołanie będzie już potrafiło prawidłowo porównać. Nadklasa dalej nic nie wie o obiekcie potencjalnaPodklasa poza tym, że ma (prawdopodobnie przesłoniętą) metodę compareTo. Problem pojawiłby się gdyby podklasa do prawidłowego porównania musiała wykorzystać porównanie z jednej z nadklas bo skoro one delegują w dół, to wykorzystanie do takich celów publicznej compareTo da rekurencję. Stąd pomyślałem, że osobna metoda do porównywania dla konkretnej klasy i to dostępna tylko z dołu (a więc protected) byłaby niezbędna. Dlatego objectCompareTo, której założeniem jest brak jakichkolwiek dalszych odwołań do porównań z innych poziomów. Stąd nazwa klasy przed this - wtedy obiekt nie wywoła innej wersji niż tej z jawnie określonej klasy (np. macierzystej).

1 użytkowników online, w tym zalogowanych: 0, gości: 1