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…)