Generics und Vererbung


#1

Moin,

ich bastle gerade an einer Demo und habe Schwierigkeiten mit Generics und Vererbung.
Ich habe ein baumartiges Datenmodel, das später in einem JTree abgebildet werden soll.

Ein stark vereinfachtes KSBK soll das verdeutlichen:
Ich habe einen Schrank, der Schubladen hat. In den Schubladen sind Boxen mit Fotos.
Ziel des Ganzen ist es in einer Box nicht nur Fotos zu sammeln, sondern auch weitere Boxen, in denen wiederum Fotos liegen können. Das heißt: eine Box kann nicht nur Fotos enthalten, sondern selbst auch Boxen (mit Fotos). Wie bildet man das in Klassen ab? Wäre toll, wenn mir da jemand weiterhelfen könnte.

Hier noch, was ich bisher habe:
Der Schrank:


/**
 * Cabinet repräsentiert einen Schrank o.ä. der Schubladen und Kartons mit Fotos enthält.
 * Cabinet hat eine baumartige Struktur, die in einem TreeModel abgebildet werden kann.
 */
public class Cabinet extends Element<Drawer> {

    public Cabinet() {
        this("Neue Sammlung");
    }

    public Cabinet(String name) {
        super(name);
        elementList = new ArrayList<>();
    }

}

Die Schublade:


/**
 * Drawer repräsentiert eine Schublade in einer Box/Schrank.
 */
public class Drawer extends Element<Box> {

    public Drawer() {
        this(null, null);
    }

    public Drawer(Cabinet parent, String name) {
        super(parent, name);
        elementList = new ArrayList<>();
    }

}

Die Box:

import java.util.ArrayList;

/**
 * Box repräsentiert eine/n Schachtel/Karton, die/der Fotos enthält.
 */
public class Box extends Element<Photo> {

    public Box() {

    }

    public Box(Drawer parent, String name) {
        super(parent, name);
        elementList = new ArrayList<>();
    }        
   
}

Das Foto:


/**
 * Photo repräsentiert ein Foto.
 */
public class Photo {

    private String name;
    private Box parent;
    private String imagePath;

    public Photo() {
        this(null, null);
    }

    public Photo(Box parent, String name) {
        this(parent, name, null);
    }

    public Photo(Box parent, String name, String imagePath) {
        this.parent = parent;
        this.name = name;
        this.imagePath = imagePath;
    }

    /**
     * Gibt die String-Repräsentation des Pfades des Fotos zurück.
     *
     * @return der Pfad zum Foto
     */
    public String getImagePath() {
        return imagePath;
    }

    /**
     * Setzt den Pfad zum zum eingebetteten Foto.
     *
     * @param imagePath der Pfad zum Foto.
     */
    public void setImagePath(String imagePath) {
        this.imagePath = imagePath;
    }

    @Override
    public String toString() {
        return name;
    }    

}

Die Basisklasse:


/**
 *
 * <code>
 * Schrank
 *   +-Schublade_1
 *   |   +-Karton_1
 *   |   |   +-Foto_1
 *   |   |   +-Foto_2
 *   |   |   +-Karton_1
 *   |   |   |   +-Foto_1
 *   |   |   |   +-Foto_2
 *   |   |   |   +-Foto_3
 *   |   |   |   +-[...]
 *   |   |   +-Foto_3
 *   |   |   +-[...]
 *   |   +-Karton_2
 *   |   +-[...]
 *   +-Schublade_2
 *   +-[...]
 * </code>
 *
 */
public class Element<T> {

    protected List<T> elementList; //Liste von enthaltenen Elementen
    private Element parent; //das Eltern-Objekt
    private String name; //die Bezeichnung dieses Elements

    /**
     * Erzeugt ein neues Element.
     */
    public Element() {
        this(null);
    }

    /**
     * Erzeugt ein neues Element mit der angegebenen Bezeichnung.
     *
     * @param name die Bezeichnung des Elements
     */
    public Element(String name) {
        this(null, name);
    }

    /**
     * Erzeugt ein neues Element mit einem Verweis auf sein Eltern-Element
     * und der angegebenen Bezeichnung.
     *
     * @param parent das Eltern-Element
     * @param name die Bezeichnung des Elements
     */
    public Element(Element parent, String name) {
        this.parent = parent;
        this.name = name;
    }

    /**
     * Gibt das Eltern-Element zurück.
     *
     * @return das Eltern-Element
     */
    public Element getParent() {
        return parent;
    }

    /**
     * Gibt eine Liste der im Element gespeicherten Elemente zurück.
     *
     * @return Liste der im Element gespeicherten Elemente
     */
    public List<T> getElementList() {
        return elementList;
    }

    /**
     * Fügt ein neues Element diesem Element hinzu.
     *
     * @param t Das Element, das diesem Element hinzugefügt werden soll.
     */
    public void addElement(T t) {
        elementList.add(t);
    }


    /**
     * @see java.lang.Object#toString()
     * @return Ein String mit Name oder Zustandsdaten des Elements.
     */
    @Override
    public String toString() {
        return name;
    }
}

Eine Klasse zum Testen:

import java.util.Random;

public class Test {

   public Test() {
      Cabinet cabinet = createCabinet();
      System.out.println(showCabinetTree(cabinet));
   }

   private Cabinet createCabinet() {
      Cabinet cabinet = new Cabinet("Fotos");
      for(int i = 0, j = random(4); i < j; i++) {
         cabinet.addElement(createDrawer(cabinet));
      }      
      return cabinet;
   }

   private Drawer createDrawer(Cabinet parent) {
      Drawer drawer = new Drawer(parent, "Drawer_" + (parent.getElementList().size()+1));
      for(int i = 0, j = random(6); i < j; i++) {
         drawer.addElement(createBox(drawer));
      }       
      return drawer;
   }

   private Box createBox(Drawer drawer) {
      Box box = new Box(drawer, "Box_" + (drawer.getElementList().size()+1));
      for(int i = 0, j = random(8); i < j; i++) {
         box.addElement(createPhoto(box)); //Fotos hinzufügen
      }
      return box;    
   }  

   private Photo createPhoto(Box parent) {
      Photo photo = new Photo(parent, "Foto_" + (parent.getElementList().size()+1));
      return photo;       
   }

   private int random(int number) {
      Random r = new Random();
      int n = r.nextInt(number);
      if(n < 2) {
         return 2;
      }
      return n;
   }

   private String showCabinetTree(Cabinet cabinet) {
      StringBuilder sb = new StringBuilder();
      sb.append(cabinet).append("\n\r");
      for (Drawer drawer : cabinet.getElementList()) {
         sb.append("   +-").append(drawer).append("\n\r");
         for (Box box : drawer.getElementList()) {
            sb.append("      +-").append(box).append("\n\r");
            for (Photo photo : box.getElementList()) {
               sb.append("         +-").append(photo).append("\n\r");
            }            
         }
      }
      return sb.toString();
   }

   public static void main(String[] args) {
      new Test();
   }
}

#2

generics-technische Maßnahme, die mir dazu einfällt, wäre gemeinsames Interface für Box + Photo und anderere solche Gegenstände ‘Boxable’ oder ähnliches,

Box extends Element<Boxable>

aber wird nicht schön, jeglichen Content darin zu unterscheiden, instanceof auf jedes Element?
wie stellst du dir Zugriff/ Verarbeitung/ Verwendung vor?

humaner mag doch ‘Box extends Element < Photo >’ sein mit zusätzlich noch einer List < Box < Photo> > in der Box-Klasse,

der Zugriff auf Box ist mit diesen Möglichkeiten eh etwas anders als auf andere Elemente des Baums,
es sei denn diese Funktion kommt in die Basisklasse Element, hat neben List < T > auch eine List < Element < T > >, evtl. mit boolean-Methode ob die jeweilige Stufe solche Zusätze erlaubt oder nicht,


Element als Name klingt mehr nach dem Photo als nach den höheren Stufen,
zumal auch ‘elementList; //Liste von enthaltenen Elementen’, obwohl das ja eine List < T > ist, T unabhängig von Element, eben Photo

wie wäre es mit Namen Container?


ob Code nur Beispiel oder nicht,

elementList = new ArrayList<>();

muss nicht in jeder erbenden Klasse im Konstruktor stehen, das kann nun gut in die Basisklasse, in Konstruktoren dort oder gleich direkt beim Attribut


#3

Vermutlich ist da in dem “Dummybeispiel” irgendeine fachliche Anforderung untergangen. Oder ich sollte um 4:00 keine Beiträge mehr schreiben :roll_eyes: aber im Moment erschließt sich mir nicht ganz, welche Rolle oder welchen Zweck die Generics da erfüllen (sollen).

Wenn ich das richtig verstanden habe, sind die Stufen Schrank und Schublade ja fix. Und darunter gibt es dann Box, was Photos enthalten kann, oder weitere Boxes. Natürlich kann man immer versuchen, gemeinsame Elemente und Strukturen zu identifizieren. Und vielleicht können die Klassen auch ein gemeinsames interface implementieren. Aber um das besser zu verstehen: Welchen Zweck erfüllen die Generics und die Verallgemeinerungen, hinaus gehend über sowas wie

class Cabinet {
    List<Drawer> getDrawers() { ... }
}
class Drawer{
    List<Box> getBoxes() { ... }
}
class Box{
    List<Photo> getPhotos() { ... }
    List<Box> getBoxes() { ... }
}

?

(die letzte Ebene erinnert ja an “File” und “Folder”…)

Sollen da später noch weitere (getypte aber von Element abgeleitete) “Stufen” dazukommen können?


#4

Danke SlaterB, deine beiden Vorschläge hatte ich auch bereits im Editor, bin aber mangels Erfahrung nicht so richtig weiter gekommen. Mir hat das rumgecaste nicht so gefallen, bzw. war ich mir nicht sicher, ob meine Lösung nichts taugt.
Ansonsten ist das, wie Marco13 schon bemerkte ein Dummy-Beispiel. Dahinter stehen im Prinzip Klassen mit einem ähnlichen Zweck, aber in anderem Zusammenhang.

Danke Marco13, es stimmt, die ganze Sache geht irgendwie nicht so richtig auf. Ich bin von meiner Lösung auch nicht vollends überzeugt, daher wende ich mich an Leute, die damit täglich zu tun haben.
Ich habe mir ein kleines Projekt auferlegt und möchte das unbedingt durchziehen und dabei auch noch was lernen. Wie bekannt, verdiene ich meine Brötchen nicht mit der Softwareentwicklung…

Hauptsächlich versuche ich mit möglichst wenigen Methoden und soweit es geht ohne Casten oder instanceof Objekte aus Listen zu lesen. Die Objekte sind sich aber nur teilweise ähnlich, daher das Erben von einer gemeinsamen Basisklasse. Die Datenstruktur ist aber baumartig, wird in einem JTree dargestellt und außerdem in zerlegter Form in einem Panel, das hauptsächlich Buttons darstellt. Ein Klick auf einen Button bedeutet die Änderung der Darstellung (auslesen einer tiefergelegenen Liste), ähnlich einem Menü.

Im “echten” Programm wäre Photo nicht die tiefste Ebene des Datenbaums. Prinzipiell geht es mir, sofern meine Überlegungen brauchbar sind, darum, dass ich Boxen habe, die Fotos und weitere Boxen mit Fotos enthalten können. Das ist die einzige wiederkehrende Objekt-Beziehung. Diesen Teil des Baums würde ich gerne auch als Kontext-Menü darstellen können.


#5

Wow, das hats in sich…

import java.util.ArrayList;

public class BoxesDrawersCabinets {

}

class Photo {
	// dummy
}

interface PhotoCollection {
	boolean add(Photo p);
	boolean remove(Photo p);
}

class Box extends ArrayList<Photo> implements PhotoCollection {
	@Override
	public boolean remove(Photo p) {
		return super.remove(p);
	}
}

class Element<T extends PhotoCollection> extends ArrayList<Element<? extends T>> {
}

class Drawer extends Element<Box> implements PhotoCollection {
	private final PhotoCollection pc = new Box();

	@Override
	public boolean add(Photo p) {
		return pc.add(p);
	}

	@Override
	public boolean remove(Photo p) {
		return pc.remove(p);
	}
	
}

class Cabinet extends Element<Drawer> {
	
}

Hmm… nö… eigentlich doch nicht. :wink:
Wahlweise kann Cabinet ja auch noch PhotoCollection implementieren, aber wer will schon eine derartige Unordnung im Schrank? :smile:


#6

Moin Spacerat. Danke für deine Hilfe. Ich probiere das mal aus, was du vorgeschlagen hast.


#7

Das hört sich gut an. Aber vorweg: Lass dir nicht einfallen aus PhotoCollection eine echte java.util.Collection zu machen, es sei denn du stehst auf “blaue Wunder”… Interfaces können nämlich leider nur einmal implementiert werden, also entweder in der List oder in der List<Element>. Ich habe Letzteres bevorzugt, weil sich dadurch die Baumstruktur der Behältnisse (JTree) automatisch ergibt.
Ich wüsste noch gerne, ob Cabinets auch Boxes hinzugefügt werden können. Kannst du mir ja mal mitteilen, wenn du es getestet hat.


#8

Ich habe jetzt nicht alle Details durchgelesen, aber das Box-Problem sieht sehr nach dem Composite-Pattern aus:

Kurz gesagt sollten Box und Photo eine gemeinsame Elternklasse/interface haben.


#9

Danke Landei, ich ziehe mir das mal rein. Ich habe nach dem Tipp von Marco13 die Klassen von einem gemeinsamen Interface erben lassen. Danach konnte ich in Boxen weitere Boxen mit Fotos speichern.

Ich will aber auch noch mal den Vorschlag von Spacerat ausprobieren.