Generics - Zahlentypen

Hi,
zurzeit lege ich ein paar mathematiche Hilfsklassen (hier konkret ein dreidimensionaler Strahl / Ray) an. Da ich allerdings manchmal die Werte als Float, manchmal als Doble und eher selten als Integer oder Long bräuchte, dachte ich mir, dass ich den benötigten Typ doch als Generic angeben könnte. Allerdings kann ich bei public class Ray3<T extends Number> { } Anschließend kann ich von den Objekten des Typs “T” jedoch weder subtrahieren, addieren, noch sonst irgendwas machen, da Number nur verschiedene Methoden für verschiedene Zahlentypen hat (floatValue89; intValue; etc).
Die Klassen Double, Float, Integer, etc erben jedoch alle direkt von Number.

Ich hatte bisher noch nie selbst generische Klassen erstellt, sondern lediglisch die vorhandenen genutzt. Gibt es irgendeine Möglichkeit das ganze zu realisieren ohne für jeden Typ eine eigene Klasse anzulegen ?

Schonmal vielen Dank !

MfG,
~Oneric

Es gibt leider keine Möglichkeit direkt mit einer Number zu rechnen.
Du hättest zwei Möglichkeiten das zu lösen:

  1. Per instanceof den konkreten Typ prüfen und T in ein double bzw. long umwandeln.
  2. Du kannst BigDecimal nutzen (new BigDecimal(number.toString()) und dann mit dem BigDecimal weiterrechnen. Das würde dann für alle Typen funktionieren und du kannst dir das instanceof aus 1) sparen.

Überlege dir mal, wie oft du den gesamten Wertebereich eines longs, also sozusagen rationale Zahlen mit 64 Bit Genauigkeit benötigst. Sofern int, float und double reichen, kannst du alles mit der 53-Bit-Genauigkeit (52 mantisse + 1 implizites) eines doubles berechnen und bei Bedarf nach int oder float casten oder zusätzlich zu einer ohnehin notwendigen “getValue()”- noch eine “getIntValue()”- und eine “getFloatValue()”-Methode implementieren. Number zu erweitern macht wie bereits gesagt, leider eh keinen Sinn, weil man damit eh’ nicht direkt rechnen kann.
Zumindest kann ein double sämtliche rationalen Zahlen von -2^53 bis +2^53 darstellen, was in etwa zwischen den Wertebereichen zwischen long und int angesiedelt ist. Verschiedene Datentypen oder gar Generics sind also evtl. gar nicht nötig und würden den Code nur aufblähen und umständlich machen.

Ich würde dir auf jeden raten, die ganzen Hilfsklassen mit fest codiertem “double” zu implementieren und ggf. im Client-Code zu casten. Das macht den Code sauberer und klarer.

Number als die Mutter aller Zahlen ist in Java eher unbrauchbar, was Berechnungen betrifft.

Du meinst eher ganze Zahlen, also [tex]\mathbb Z[/tex].

Funny Fact

Selbst wenn die Mengen der ganzen und der rationalen Zahlen gleich mächtig sind, so gibt es bei rationalen Zahlen in beliebigen Intervallen unendlich viele Zahlen, wohingegen das auf die ganzen Zahlen offensichtlich nicht zutrifft.

Natürlich ganze Zahlen. Fließkommazahlen nennt man doch reelle Zahlen oder nicht? Verwechsel ich da wieder was?

Reelle Zahlen ([tex]\mathbb R[/tex]) bestehen aus den rationalen Zahlen ([tex]\mathbb Q[/tex]) und den irrationalen Zahlen (der Rest). Rationale Zahlen sind alle als “Bruchzahlen”, also Quotienten aus ganzen Zahlen darstellbare Zahlen.
Wenn man es genau nimmt, dann sind alle Zahlen die als Gleitkommazahl darstellbar sind, rationale Zahlen. Irrationale Zahlen (wie z. B. [tex]\sqrt 2[/tex]) lassen sich nicht darstellen.

Juhu… ich hatte immer angenommen, dass rational lateinisch für ganz ist und das Brüche deshalb auch nur aus ganzen Zahlen in Zähler und Nenner bestehen. Rechnungen mit Bruchstrich hingegen, wo reelle Zahlen verwendet werden, waren mir hingegen nur als Division bekannt. Na egal… ist OT

Das ist tatsächlich recht schwierig. Es gibt immer Lösungen und Workarounds, und ggf. solltest du genauer beschreiben, was das Ziel ganz am Ende ist. Also, welche Methoden sollte es in dieser Klasse “Ray” z.B. geben? Bei sowas wie ray.scale(whatever), sollte das whatever da ein T sein? Wann braucht man wirklich mal einen Ray<Long> oder Ray<BigDecimal>? (Aber ich denke, dass das nur EIN Beispiel sein sollte. Sowas wie ein Point3D<Integer> wäre sicher an vielen Stellen sinnvoll!).

Trotzdem: Ich würde auf Basis der bisherigen (spärlichen) Beschreibung in Richtung dessen tendieren, was Bleiglanz gesagt hat: double als supertyp aller primitiven Typen (und ggf. spezielle Klassen wie IntPoint3D oder so…) Aber nochmal: Das hängt vom genauen Ziel ab.

(Kleine Randnotiz: Ich bin kürzlich mit Double und dem Autoboxing ziemlich auf die Fr**** geflogen: Ich dachte, man könnte die neue, praktische, tolle, allgemeine DoubleFunction<T> aus Java8 verwenden, um sämtliche double->double-Funktionen abzubilden, die in dem Programm so vorkamen - eben als DoubleFunction<Double>. Das funktioniert auch, und ist bequem und mächtig und praktisch… bis ich mich darüber wunderte, warum das Programm zwischendrin immer mal 1 bis 5 (!) Sekunden lang komplett anzuhalten schien. Des Rätsels Lösung: Er hing dann immer in einer “Full Garbage Collection”, wo er hauptsächlich damit beschäftigt war, Double-Instanzen aufzuräumen :eek: Laut JVisualVM flogen da halt mal locker 30 Millionen Double-Objekte rum - die meisten waren 0.0 oder 1.0, aber Müll ist Müll. Die Quintessenz: Wenn’s performant (und ohne GC-Pausen) sein soll, sollte man primitive Zahlen verwenden. Jetzt gibt es eine DoubleDoubleFunction, die nur primitive Typen verwendet, und das Problem ist gelöst. Sicher wäre das allgemeine interface “schöner”, aber … manchmal muss man Prioritäten setzen…)

*** Edit ***

Hab’ mal zwei theoretische Lösungsansätze skizziert, die ich NICHT empfehle, sondern die nur andeuten sollen, wie man das theoretisch lösen könnte, wenn man’s drauf anlegen würde.

Der pragmatische: Man erstellt für jeden Number-Typ eine Klasse. Tatsächlich könnte der “Ray” selbt dann nach außen hin hauptsächlich über “Number” kommunizieren, weil intern alles geregelt wird.

Nicht empfohlen
[spoiler]

public class GenericNumbersTest
{
    public static void main(String[] args)
    {
        Ray3<Integer> intRay = new IntRay3();
        Ray3<Double> doubleRay = new DoubleRay3();
        
        doubleRay.add(intRay);
        doubleRay.scale(1.234);
        intRay.add(doubleRay);
        intRay.scale(1.234);
    }
}

interface Ray3<T extends Number> 
{
    void scale(Number number);
    void add(Ray3<?> other);
    T getX();
    T getY();
    T getZ();
}

class IntRay3 implements Ray3<Integer>
{
    int x, y, z;
    
    @Override
    public void scale(Number number)
    {
        x *= number.intValue();
        y *= number.intValue();
        z *= number.intValue();
    }

    @Override
    public void add(Ray3<?> other)
    {
        x += other.getX().intValue();
        y += other.getY().intValue();
        z += other.getZ().intValue();
    }

    @Override public Integer getX() { return x; }
    @Override public Integer getY() { return y; }
    @Override public Integer getZ() { return z; }
    
}

class DoubleRay3 implements Ray3<Double>
{
    double x, y, z;
    
    @Override
    public void scale(Number number)
    {
        x *= number.doubleValue();
        y *= number.doubleValue();
        z *= number.doubleValue();
    }

    @Override
    public void add(Ray3<?> other)
    {
        x += other.getX().doubleValue();
        y += other.getY().doubleValue();
        z += other.getZ().doubleValue();
    }

    @Override public Double getX() { return x; }
    @Override public Double getY() { return y; }
    @Override public Double getZ() { return z; }
}

[/spoiler]

Die Fragen, die du dir selbst sowieso schon stellen musst, werden da nochmal offensichtlicher: Kann man zu einem Ray<Integer> einen Ray<Double> addieren, und wenn ja, was kommt da raus? Usw.

Das wäre aber so natürlich ein Krampf, und ist vollkommen unpraktikabel, wenn man den ganzen Raum abbilden will, der durch Basisklassen wie Ray, Point2D, Point3D, Line... und alle Parameter wie Double, Float, BigDecimal... usw. aufgespannt wird.

Aus rein theoretisch-objektorientierter Sicht wäre es da Sinnvoller, die Unterschiede in einer Klasse zusammenzufassen, die genau das beschreibt, was die Unterschiede ausmacht - nämlich die Arithmetik.

Nicht empfohlen
[spoiler]

class Ray3<T extends Number>
{
    static Ray3<Double> createDouble() 
    {
        return new Ray3<Double>(new DoubleArithmetic());
    }
    
    private final Arithmetic<T> arithmetic;
    private T x;
    private T y;
    private T z;
    
    private Ray3(Arithmetic<T> arithmetic)
    {
        this.arithmetic = arithmetic;
        this.x = arithmetic.zero();
        this.y = arithmetic.zero();
        this.z = arithmetic.zero();
    }
    
    void add(Ray3<T> other)
    {
        this.x = arithmetic.add(x, other.x);
        this.y = arithmetic.add(y, other.y);
        this.z = arithmetic.add(z, other.z);
    }
}

interface Arithmetic<T extends Number>
{
    T add(T t0, T t1);
    T mul(T t0, T t1);
    T zero();
    T one();
}

class DoubleArithmetic implements Arithmetic<Double>
{
    @Override public Double add(Double t0, Double t1) { return t0+t1; }
    @Override public Double mul(Double t0, Double t1) { return t0*t1; }
    @Override public Double zero() { return 0.0; }
    @Override public Double one()  { return 1.0; }
}

[/spoiler]

Dann gäbe es nur noch jeweils EINE Klasse für Ray, Point2D, Point3D, Line... und jeweils EINE Klasse für Arithmetiken mit Double, Float, BigDecimal..., und das Aufspannen des daraus entstehenden Raumes wäre gelöst, indem jeweils die passende Arithmetik-Klasse im Konstruktor übergeben wird.

ABER auch das wäre ein Krampf, und es wäre schwierig, da eine “gute”, “praktische”, “effiziente” und “intuitiv verwendbare” Bibliothek drauf aufzubauen (nicht unmöglich, und es könnte Ziele geben, die damit gut erreicht werden könnten, aber auf Basis der bisherigen Beschreibung klingt es, als gäbe es sinnvollere Ansätze…)

Ich hatte da ja auch mal ein Problem mit, also mit diversen Matrizen, Points oder Vectoren in diversen Zahlenformaten. Ich bin dann zu dem Schluss gekommen, dass meine dafür ersonnene “BufferData-API” vollkommen ausreicht (siehe Anhang).

Die Hauptklasse hat Konstruktoren mit int, float oder double als VarArgs und einen der eine Anzahl gewünschter Elemente und einen TransferType (ein Enum) benötigt. Darüber hinaus stellt sie Getter und Setter zum setzen oder lesen einzelner Elemente zur Verfügung. Konstruktoren, sowie Getter und Setter sind geschützt, können aber durch erweiternde Klassen öffentlich gemacht werden. Aus dieser Hauptklasse kann man alle weiteren ableiten (z.B. wie im Paket schon vorhanden die Prototypen von Matrix und Point). Darüber hinaus verwende ich in der BufferData-Klasse ByteBuffer statt Arrays, ganz einfach deswegen, weil diese mit den Methoden “put/getInt()”, “put/getFloat()” und “put/getDouble()” intern leichter zu handlen sind.

Anfangs dachte ich auch, dass ich dort Generics und verschiedene Ausführungen von Gettern und Settern bräuchte, dem ist aber nicht so. Setter, die ein double erwarten, akzeptieren auch ints, floats und longs (wobei longs über ±2^53 natürlich verfälscht werden) und Getter, die nur doubles zurück geben, kann man lokal besser casten, als mit einer ganzen langen Staffel meist inkompatibler Klassen (z.B. Point3F oder Point3D, Point4F oder Point4D usw.) zu hantieren.

P.S.: Ich habe das Ganze ein wenig für das Forum angepasst und Synchronisation und Observierungsmöglichkeiten entfernt. Kann sein, dass dadurch das ein oder andere nicht mehr funktioniert, aber das Prinzip sollte deutich werden. Über Performance- und/oder Speicherprobleme kann ich beim Verwenden dieser “API” nicht klagen, aber das kann ich bei meiner “High-End-Kiste” ohnehin nie.

Geheimtipp: Auf einer Rotz-alten Dreckskiste entwickeln, dann schreibt man performanteren Code :smiley: (Werd’ mir die API ggf. mal ansehen…)

Vielen Dank für die ganzen Antworten.
Ich werde mir die Buffer basierte APi mal auf jeden Fall ansehen.
Momentan tendiere ich dazu für jeden benötigten Typ (double, float, int, etc) eine eigene Vektor/Punkt-Klasse zu erstellen und durch Methoden wie “.toDoubleVector()” die Kompatibilität untereinander zu gewähren.
Andererseits hat mich der Post von Spacerat mit der Buffer API auf die Idee gebracht, das Ganze folgend zu lösen. Man hat eine Vektorklasse. Wenn diese erstellt wird übergibt man einen Enum der den primitiven Typ bestimmt. Je nachdem wird dann ein verschieden großer ByteBuffer angelegt. In diesem werden dann die Werte abgelegt. Problem dabei: Als Methodenparameter müsste ich mich für einen primitiven Typ entscheiden, dass wäre dann wohl double. Theoretisch mögliche große Longs, würden so verfälscht werden.
Ich würde zudem vermuten, dass es besser wäre alle 3 Koordinaten (x, y, z) in einem Buffer zu speichern und nicht drei getrennte zu benutzen. Stimmt das so ?

In der Rayklasse würden dann die Vektorklasse(n) benutzt werden.

MfG,
~Oneric

[QUOTE=Oneric]Andererseits hat mich der Post von Spacerat mit der Buffer API auf die Idee gebracht, das Ganze folgend zu lösen. Man hat eine Vektorklasse. Wenn diese erstellt wird übergibt man einen Enum der den primitiven Typ bestimmt. Je nachdem wird dann ein verschieden großer ByteBuffer angelegt. In diesem werden dann die Werte abgelegt. Problem dabei: Als Methodenparameter müsste ich mich für einen primitiven Typ entscheiden, dass wäre dann wohl double. Theoretisch mögliche große Longs, würden so verfälscht werden.
Ich würde zudem vermuten, dass es besser wäre alle 3 Koordinaten (x, y, z) in einem Buffer zu speichern und nicht drei getrennte zu benutzen. Stimmt das so ?[/QUOTE]Ja das stimmt so und du müsstest dazu auch gar nicht mehr viel programmieren, denn genau so funktioniert die BufferData-Klasse. Eigentlich bräuchtest du nur noch den TransferType.LONG zu definieren. Ich persönlich habe longs aber noch nie gebraucht. Aus der vorhandenen Point-Klasse habe ich Point2, Point3 und Point4 und dazu passende Vektor-Klassen abgeleitet und aus Matrix Matrix3x3, Matrix4x4 und GaussMatrix. Darüber hinaus habe ich direkt aud BufferData noch Rectangle, Area, Insets, Dimension2 und BoundinCirle für 2D sowie Vertex, Normal, BoundingBox, BoundingCircle und Dimension3 für den 3D-Bereich abgeleitet. Nun kann man z.B. Point2 und Point3 auf Point casten und die beiden miteinander addieren, wobei natürlich nur die ersten beiden Elemente von Point3 beachtet werden. Der dadurch etstehende Code sieht augenscheinlich n mal besser aus als entsprechender mit dem VecMath-Paket, selbst die Tatsache, dass einzelne Elemente per Getter und Setter manipuliert werden müssen, macht ihn nicht schlimmer.

ich würde nochmal darüber nachdenken: da +,-,*,/ nun einmal nicht überladen werden können, wirst du mit JEDEM solchen Ansatz gewaltige Mengen an dupliziertem Code produzieren, du musst ja die Mathematik für die einzelnen primitiven Typen zweimal schreiben und/oder viel casten.

wenn du alles mit double machst, dann übernehmen das Autoboxing und die erweiternden casts der primitiven Typen doch eine Menge Arbeit automatisch, ggf. muss man dann gelegentlich down-casten.

Ich verstehe nicht ganz die Intention: Mal angenommen du hast schon alles mit double als primitivem Typ vorliegen. Welchen Grund gibt es, das ganze für long, float, usw. nochmal zu erzeugen: Performance (ist das wirklich so?), Genauigkeit (warum?) oder was?

Wenn das ganze z.B. Richtung OpenGL oder so geht - dann ist das doch völlig überflüssig?

Wenn es wirklich darum geht, viel Performance rauszuquetschen, wäre z.B. die Verwendung von float anstatt double durchaus sinnvoll. (Und wenn es darum geht, dann auch weil OpenGL selbst - wenn überhaupt - dann höchstens mit float-Werten etwas anfangen kann. Spätestens in Buffern liegen höchstens floats rum). Auch ein int-Punkt kann sinnvoll sein: Wenn man z.B. Koordinaten in einem (3D)-Gitter beschreiben will, oder schlicht (2D)-Pixelkoordinaten.

Paradoxerweise steht aber der Wunsch, möglichst hohe Performance zu erreichen, bis zu einem gewissen Grad im Widerspruch zur Abstraktion. Zum Glück ist es heute durch JIT-Inlining, On-Stack-Replacement und Escape Analysis nicht mehr so extrem, wie damals, als man sich z.B. in der Vecmath Tuple3f dazu entschieden hat, die fields public zu machen. Und es gibt ja auch die Empfehlung Write Dumb Code! (von jemandem, der es wissen muss). Aber es gibt Bereiche, in denen man von sonst üblichen “Best practices” abweichen muss. Sowas wie polymorphe Aufrufe können auch heute noch ein Performancekiller sein, wenn man z.B. ein Mesh mit 10000 “Point3D”-Objekten hat, und das nur ein Interface ist, für das 10 Implementierungen rumfliegen. Vom (nicht zu vernachlässigenden) Problem des Mülls, der durch Auto(un)boxing von float und double anfallen kann, mal ganz abgesehen.

Tatsächlich habe ich schon mehrfach darüber nachgedacht, wie man Punkte und Vektoren modellieren kann. Nein, es ging nicht um eine “allgemeine, unverselle 3D-Geometry-Lib”. (Ja, daraum auch, aber (noch) nicht speziell). Und schon die Frage, wie die geeignetste Modellierung einer einzigen Klasse ist - nämlich eines Punktes - hat mich im Endeffekt überfordert.

Gibt es einen Unterschied zwischen einem Punkt und einem Vektor? Meistens nicht. Außer wenn man ein “3D-Ding” mit einer 4x4-Matrix transformiert: Für den Vektor muss die letzte Komponente als 0 angenommen werden, und für den Punkt als 1. Von Details, wie dass ein Punkt keine “length()” methode hat, und ein Vektor keine “distance(Vector other)”-Methode mal abgesehen. Sollen also beide von “Tuple” erben, wie bei Vecmath? Die Frage, ob Point3D von Point2D erben soll wäre für mich persönlich aber klar. (Auch wenn viele das anders sehen). Das sorgt für zu viele Uneindeutigkeiten und subtile Fehlermöglichkeiten.
Sind die Koordinaten float oder double, und gibt es auch int und long? Oder gibe es dafür unterschiedliche Klassen? (Das, worum es hier eigentlich geht).
Public fields, wie in Vecmath? Nee. Setter und getter?

class Point3D {
    private float x,y,z;
    public float getX() { return x; }
    public void setX(float x) { this.x = x; }
    ...
}

Vielleicht. Oder doch ein interface?

interface Point3D {
    float getX();
    void setX(float x);
    ...
}

Getrennt nach Mutable/Immutable?

interface Point3D {
    float getX();
    ...
}
interface MutablePoint3D extends Point3D {
    float getX();
    ...
}

Ja, das wäre an vielen Stellen sicher sinnvoll. Der Punkt, der “Origin” heißt, soll bei (0,0,0) sein und dort auch bleiben. Bei den Implementierungen muss man sich dann natürlich noch über den Unterschied zwischen “Immutable” und “Unmodifiable” Gedanken machen…
Und die spannend(st)e Frage: Wie macht man dann die “üblichen” Operatoren?

interface Point3D {
    ...
    // Wird ein neuer Punkt zurückgeben? Wenn ja, wer erstellt den,
    // und von welcher Implementierung ist der dann?
    Point3D add(Point3D other); 

    // Wird das Ergebnis in ein Argument geschrieben?
    void add(Point3D other, MutablePoint3D result); 
}
interface MutablePoint3D extends Point3D {
    ...
    // Der hier kann verändert werden .... 
    void add(Point3D other); 
    // ... aber die Methode gibt es aber schon, mit anderem Rückgabetyp 
    // im superinterface. Hier würde man also eher Kovarianz erwarten
    @Override
    MutablePoint3D add(Point3D other); 
}

Die Liste könnte noch lange fortgesetzt werden. Und immer geht es nur um …

@Marco13 : Jo, überfordert hat es mich auch eine Zeit lang. Ich habe mir jedoch irgendwann für andere Zwecke eine Observable-/Lockable-API ersonnen und siehe da, meine Mutables wurden durch Setzen diverser Locks sozusagen zu Schein-Immutables. Zumindest hatte ich nun eine Möglichkeit, viel mehr durch viel weniger (also mehr Funktion mit weniger Klassen) zu erreichen und wagte mich dann noch mal an diesen VecMath-Krams. Ich jedenfalls verwende nur noch das. Evtl. kann man hier da und dort noch etwas verbessern, aber viel bestimmt nicht mehr.

BTW.: Von der Verwendung von Wrapper-Klassen oder Number, kann man nur abraten, ebenso von SingleElement-Arrays. Das sind Objekte, und die belasten (im Zweifelsfalle immer) den GC! Alles in double berechnen und am Schluss einmal casten ist nicht nur viel genauer, sondern auch wirtschaftlicher, um nicht zu sagen performanter. Die primitiven doubles existieren nur innerhalb der Methoden, wo sie verwendet werden und dort belasten sie nur den User-Stack der Methode, nicht aber den Objectpool des GCs. Darüber hinaus wird der User-Stack (Speicher) beim verlassen der Methode sofort wieder an den Heap entlassen (freigegeben).

Wäre das auch performanter als die Buffer-Lösung ?

Gena das ist der Punkt. In OpenGl werden eher floats verwendet, wenn man aber doubles für die Koordinaten, etc verwendet verbraucht das im Speicher deutlich mehr Platz, und zusätzlich benötigt der double mit seiner höheren Genauigkeit mehr Rechenzeit.
Bei wenigen Werten/Berechnungen ist das vernachlässigbar, wenn man jetzt aber eine Menge Models mit vielen Polygonen hat summiert sich das. Und ich will eben maximale Performance, soweit möglich.
Innerhalb meines Logik-Threads(Kollisionserkennung, etc) rechne ich eben mit doubles, da dort die höhere Genauigkeit im Endeffekt auch genutzt wird und erwünscht ist.

MfG,
~Oneric

BufferData beruht darauf. Es verwaltet die Daten in DirectByteBuffern, die ausserhalb des Heaps (im Hauptspeicher des Rechners) angelegt werden. Je nach dem mit welchem Konstruktor BufferData durch Kindklassen instanziert wird, wird auch nur Speicher verwendet. Unabhängig davon werden von Settern doubles erwartet, also kann man auch alle anderen primitiven Typen übergeben, gespeichert wird aner dennoch im zuvor festgelegten TransferType. Ferner geben Getter auch nur doubles zurück und dabei ist es egal, od da nur float oder int in den Buffern steckt, die beiden Typen werden ja von double vollständig abgedeckt. Mit diesen doubles kann man alles in double Genauigkeit innerhalb einer Methode berechnen und ganz zum Schluss, wenn man es an OpenGL oder Graphics2D übergeben will in float, int oder was auch immer (Hauptsache Primitivtyp) casten.
Kannst ja mal versuchen, eine ähnliche Überschaubarkeit und Performance mit der VecMathLib hinzubekommen. Marco13 war sicher nicht der einzige, der sich bei diesem Thema die Zähne ausbeisst, mir ging es ja genau so, weshalb ich mal einen komplett anderen Weg gegangen bin. Durch ObjectReuse-Support ist das auch für OpenGL performant genug (also geringfügig weniger als VecMath, aber viel überschaubarer, was die Klassen angeht). ObjectReuse wird z.B. bei Morphing wichtig, damit z.B. bei 300FPS und einem MorphObjekt mit 1000 Punkten nicht 300000 Objekte pro Sekunde den GC belasten, was die Performance sehr stark drückt. mit 300000 mal “new” pro Sekunde schaft man in der Realität auch keine 300FPS, da kann man sich auf den Kopf stellen bzw. können die JVM-Entwickler an JIT-Optimierungen implementieren was sie wollen.

Von der Idee die Number-Klassen zu benutzen habe ich ja schon verworfen.
Allerdings würde es mich interessieren ob es performanter/schneller ist für ca. 3 (double, float und int würden wohl reichen) primitive Type eine jeweils eigene Klasse zu haben, die untereinander kompatibel sind, oder aber einen ByteBuffer zu benutzen.
Vielleicht weiß jemand etwas dazu, so dass ich nicht erst beide Konzepte realisieren und vergleichen muss, nur um eines davon wieder zu verwerfen.

Wenn man sich jetzt auf, sagen wir mal float, double und int beschränkt, musss man für jeden Typ eine eigene Klasse anlegen und dann noch auf die Kompatibilität achten. Dass ist verglichen mit dem Buffer deutlcih mehr Arbeit un es entsteht dabei jede Menge duplizierter Code.

Die Buffer-Lösung hat den Vorteil, dass es nicht 6 verschiedene Klassen gibt, aber trotzdem jeder primitive Typ mit einer Klasse abgedeckt ist. Aber ich könnte mir durchaus vorstellen, dass es etwas Rechenzeit kostet jedesmal aus dem Buffer zu lesen oder hineinzuschreiben.
Wenn dies allerdings keinen Unterschied in der Rechenzeit machen sollte wäre es einfacher und eleganter den Buffer zu benutzen.

Hier mal zwei Pseudo-Klassen mit einer grundlegenden Funktion zum Vergleichen

public class Point3Buffer {
ByteBuffer data;
EnumPrimitiveType primitive;

public Point3Buffer(EnumPrimitiveType type)
{
this.primitive = type;
this.data = ByteBuffer.allocate(3*type.byteSize);
this.setDefault(); // Setzt die Kooridante zu (1, 1, 1)
}


public double getDoubleXValue()
{
 if(primitive == DOUBLE)
    return this.data.getDouble(0);
 return this.getXValue();
}

public float getFloatXValue() {
if(primitive == FLOAT)
   return this.data.getFloat(0);
return (float)this.getXValue();
}

//+Andere getXXXValue Methoden

public double getXValue() { // X steht an erster stelle im Buffer, das auslesen beginnt also bei 0
switch(primitive)
{
case INT:
   return this.data.getInt(0);
case FLOAT:
   return this.data.getFloat(0);
case LONG:
   return (double)this.data.getLong(0);
//usw
}
}

public double distance(Point3Buffer point)
{
// beide Male einfach getX/Y/ZValue aufrufen (double als Typ) und dann die Distanz berechenen
}

}
public class Point3f {
 private float x, y, z;

 public Point3f() {
  this.setDefault();
 }

//+andere Konsruktoren

public float getX(){
return this.x;
}

public Point3d toPoint3D() {
 return new Point3D(x, y, z);
}

public static Point3d toPoint3D(Point3f point) {
 return new Point3D(point.x, point.y, point.z);
}

public float distance(Point3d point) {
  //berechne distance mit (float)point.getX(); Aufrufen
}

public float distance(Point3f point) {
//Einfach distanz berechen
}



}

MfG,
~Oneric

Das Problem ist doch (und das zeigt die VecMathLib ganz deutlich), dass es zwischen solchen Klassen zu Kompatibilitätsproblemen kommt, weil ein Point3D weder ein Point3F noch ein Point3I ist, dann noch sämtliche Unterklassen, Vector3D, Vector3F und Vector3I… alle ebenso inkompatibel. Bei meiner Version gibt es nur Point3, Vector3 usw. welche ein wenig mehr Speicher (trotzdem sehr viel weniger Heapspeicher!) benötigen und nach aussen hin nur doubles anbieten (obwohl Int, Float und Double gespeichert werden), bei ungefähr gleichbleibender, größtenteils aber verbesserter Performance. Der Performancegewinn resultiert daraus, dass man viel weniger zwischen inkompatiblen Klassen konvertieren muss, wobei stets GC-Abfall entsteht. Ich jedenfalls nutze die VecMathLib nicht mehr.

Ich hab mit meiner BufferData ungefähr so angefangen wie du es mit deinem Beispiel gebracht hast, nur sind bei mir inzwischen die Methoden “getInt()” und “getFloat()” rausgeflogen, weil unnötig. Der TransferType wird erst wieder interessant, wenn man eine BuffaDataKlasse in ein Array oder einen anderen Buffer transferieren will. Ausserhalb eines BufferDataObjekts genügt ein “(int) get()” oder ein “(float) get()”, besser ist aber, wenn man sich die Daten in doubles holt, damit rechnet und erst wenn man mit den Berechnungen fertig ist, castet.