Pakete, Klassen und Methoden

Nachdem den Lesern nun die Grundlagen des Programmierens bekannt sein sollten, ist es nun an der Zeit einige fortgeschrittenere Konzepte zu lernen.

Methoden

Bis jetzt haben wir sämtlichen Code in die main-Methode geschrieben:

package serlo;

/**
 * Testklasse um diverses Zeug zu testen
 *
 * @author anonymous
 * @since 17.03.2015
 */
public class Testklasse {


    /**
     * MAIN
     * 
     * @param args
     * @since 06.04.2015
     */
    public static void main(String[] args) {
        System.out.println("ABC");
    }

}

Java Klassen können jedoch mehrere Methoden besitzen. Wir erweitern unsere Testklasse um eine zweite Methode zur Potenzberechnung.

static double potenz(double x, int a) {
}

static bedeutet, dass auf die Methode direkt zugegriffen werden kann, dazu später noch mehr.

double ist der Rückgabetyp, es kann alles Mögliche zurückgegeben werden, wenn nichts zurückgegeben werden soll, so schreibt man void.

potenz(double x, int a) In den Klammern schreibt man so genannte Parameter, das sind Übergabewerte.

Methoden werden i.d.R. kleingeschrieben.
Wer eine Entwicklungsumgebung verwendet, bekommt eine Fehlermeldung angezeigt. Das liegt daran, dass eine Methode mit einem Rückgabewert ungleich void, auch etwas zurückgeben muss. Das Schlüsselwort für die Rückgabe lautet return.

Wir erweitern die Methode:

/**
 * Berechnet die Potenz.
 *
 * @param x Die Basis
 * @param a Der Exponent als Ganze Zahl
 * @return
 * @since 06.04.2015
 */
static double potenz(double x, int a) {
    double potenz = 1;
    if(a > 0) {
        for(int i = 0; i < a; i++) {
            potenz *= x;
        }
    }else {
        for(int i = 0; i > a; i--) {
            potenz /= x;
        }
    }
    return potenz;
}

Bei a > 0 multiplizieren wir die Zahl a-mal mit sich selbst. Im Falle eines negativen Exponenten dividieren wir iterativ. Wen das Vorgehen irritiert, der sollte sich den Artikel zur Potenz ansehen.

Wir können die neu erstellte Methode nun mit Hilfe von potenz(14, 2); aufrufen, zu beachten ist, dass die zweite Zahl eine ganze Zahl sein muss. Der Methodenaufruf wird wie eine Zahl bzw. Variable behandelt, wir schreiben in die main()-Methode:

double zahl = potenz(10, -3) + 15;
System.out.println(zahl);

Damit wird klar, dass es sich bei System.out.print() und println() auch um Methoden handelt. Die print()-Methoden sind überladen, das heißt es gibt mehrere Methoden mit dem gleichen Namen, die jedoch unterschiedliche Übergabeparameter besitzen. Aus diesem Grund kann man den print()-Methoden auch diverse Datentypen wie int oder double übergeben und muss nicht zuerst alles in Strings konvertieren. Die einfachste Möglichkeit Zahlen in Strings zu konvertieren wäre übrigens "" + zahl.

Das Programm beginnt mit der Ausführung in der main()-Methode und führt nur Befehle aus, die in dieser stehen. Wenn man also Code in Methoden auslagert, müssen diese explizit aufgerufen werden. Methoden sind vor allem dann besonders nützlich, wenn man Codefragmente hat, die wiederholt ausgeführt werden müssen.

Die Standardbibliotheken (was eine Bibliothek in diesem Kontext genau ist, wird später genauer erläutert) bieten viele Methoden an, die meisten einfacheren Aufgaben sind durch diese schon abgedeckt, für die Berechnung von Potenzen gibt es Beispielsweise die pow()-Methode aus der Klasse Math: double zahl = Math.pow(10, -3) +15; wäre somit eine effizientere Lösung für unsere Aufgabe.

Es kann also viel Zeit sparen, die Standardbibliotheken zu kennen, das Internet ist auch hier sehr nützlich, wenn es darum geht Methoden für eine spezielle Aufgabe zu finden.

Klassen

Es gibt grundsätzlich zwei Hauptgründe für eine weitere Klasse neben der Klasse zu verwenden, in der die main()-Methode steht.

Sammelklassen Dies ist kein Begriff aus der Informatik, wir nennen diese Art von Klassen jetzt einfach mal so. Sammelklassen sind eine Ansammlung aus nützlichen Methoden, ein Beispiel wäre die oben erwähnte Math Klasse.

Objektklassen Eigentlich kann man aus allen Klassen Objekte erstellen, diese Klassen sind allerdings dafür gedacht.

Objekte

Ein Objekt ist eine Instanz einer Klasse. Hier mal ein kleines Beispiel, um zu veranschaulichen, worum es geht.

Zuerst die Klasse Vektor2d:

package serlo;

/**
 * Repräsentiert einen 2-dimensionalen Vektor
 * 
 * @author anonymous
 * @since 15.11.2015
 */
public class Vektor2d {
    public int x;
    public int y;
}

Die Klasse VektorTest mit der main()-Methode:

package serlo;

/**
 * Vektoren testen
 *
 * @author anonymous
 * @since 15.11.2015
 */
public class VektorTest {

    public static void main(String[] args) {
        Vektor2d vec1 = new Vektor2d();
    }

}

Mit der Zeile Vektor2d vec1 = new Vektor2d(); erzeugen wir eine Instanz von Vektor2d, ein sogenanntes Objekt.
Objekte haben Methoden und Attribute, vec1 hat die beiden Attribute x und y. Natürlich könnten wir statt dessen auch ein Array verwenden: int[] vec2 = new int[]{0, 0};.

Der Vorteil eines Objektes liegt darin, dass die Daten besser strukturiert werden können und dass Methoden verwendet werden können, wir erweitern unsere Vektor2d-Klasse um folgende Methode:

/**
 * Liefert die Länge dieses Vektors.
 * 
 * @return Die absolute Länge.
 * @since 15.11.2015
 */
public double getLength() {
    return Math.sqrt(x*x + y*y);
}

Wenn wir nun System.out.println(vec1.getLength()); in der main()-Methode aufrufen, wird die Länge des Voktors ausgegeben.

Um eine Fehlermeldung zu vermeiden, müssen wir noch sicher gehen, dass x und y initialisiert wurden, wir fügen folgende Methode zur Vektor2d-Klasse hinzu:

public Vektor2d() {
    x = 0;
    y = 0;
}

Diese Methode hat keinen Rückgabewert und heißt gleich wie die Klasse, sie wird Konstruktor genannt und sie wird beim erzeugen eines neuen Objekts aufgerufen Vektor2d vec1 = new Vektor2d();.
Wenn eine Klasse keinen Konstruktor hat, wird vom Compiler ein leerer Standard-Konstruktor zugewiesen, daher würde Vektor2d vec1 = new Vektor2d(); auch ohne eigenen Konstruktor klappen.

Wenn wir das Programm nun ausführen, sollte 0.0 als Ausgabe erscheinen.

Wir ergänzen unser Programm:

public static void main(String[] args) {
    Vektor2d vec1 = new Vektor2d();
    vec1.x = -4;
    vec1.y = 3;
    System.out.println(vec1.getLength());
}

Die Ausgabe ist nun korrekter Weise 5.0.
Es lässt sich also sehr einfach auf die Variablen eines Objekts zugreifen, wenn diese public sind.

Eine Klasse kann auch mehrere Konstruktoren besitzen, diese müssen sich lediglich in ihren Parametern unterscheiden, dies wird Überladen genannt, wir haben dies oben mit dem Beispiel der print()-Methode bereits erwähnt.

Aufgabe 1:

Überlade den Konstruktor der Vektor2d-Klasse, sodass beim Initialisieren von Objekten die x- und y-Werte gleich zugewiesen werden können.

Lösung Aufgabe 1:
public Vektor2d(int x, int y) {
    this.x = x;
    this.y = y;
}

Damit der Kompiler weiß, welche Variablen wir meinen, müssen wir die Objektvariablen mit this. aufrufen, sonst geht er davon aus, dass wir die Parameter meinen. Dies ist nur notwendig, wenn die Parameter gleich wie die Klassenvariablen heißen.

Wenn wir jetzt den folgenden Code ausführen lassen,

Vektor2d vec2 = new Vektor2d(1, 1);
System.out.println(vec2.getLength());

Sollte die Ausgabe 1.41421… sein.

Aufgabe 2:

Erweitere die Vektor2d-Klasse um eine Methode add(Vektor2d vec), welche den übergebenen Vektor aufaddiert.

Lösung Aufgabe 2:
/**
 * Addiert den übergebenen Vektor auf.
 * 
 * @param vec Der Vektor, der auf diesen aufaddiert werden soll.
 * @since 15.11.2015
 */
public void add(Vektor2d vec) {     
    x += vec.x;
    y += vec.y;
}

Wenn wir nun

vec1.add(vec2);
System.out.println(vec1.getLength());

ausführen, wissen wir jedoch nicht, ob es geklappt hat, da die Ausgabe 5.0 bleibt.

Wir fügen also eine weitere Methode hinzu:

/**
 * Normale toString() Methode
 * 
 * @return Alle relevanten Informationen zu diesem Objekt in einen String verpackt.
 * @since 15.11.2015
 */
@Override
public String toString() {
    return "Vektor2d-Objekt: x: " + x + ", y: " + y;
}

Mit System.out.println(vec1.toString()); können wir uns nun überzeugen, dass die add()-Methode funktioniert.

Auf das @Override werden wir später zurückkommen. Die toString()-Methode hat eine besondere Funktion in Java, und zwar wird sie immer dann aufgerufen, wenn ein Objekt übergeben wurde, aber ein String erwartet wird. Die beiden Befehle

System.out.println(vec1.toString());
System.out.println(vec1);

sind also äquivalent.

Ähnlich wie beim Konstruktor, kann diese Methode auch aufgerufen werden, wenn sie nicht im Quellcode steht, dazu aber später beim Thema Vererbung mehr.

Aufgabe 3:

Modifiziere die Vektor2d-Klasse so, dass die x- und y-Werte minimal -100 und maximal 100 sind.

Lösung Aufgabe 3:

Am besten lässt sich dies realisieren, indem man x und y als private deklariert und Getter- und Setter-Methoden verwendet:

package serlo;

/**
 * Repräsentiert einen 2-dimensionalen Vektor
 *
 * @author anonymous
 * @since 15.11.2015
 */
public class Vektor2d {
    private int x;
    private int y;


    /**
     * Konstruktor
     * Initialisiert x und y mit 0.
     *
     * @since 15.11.2015
     */
    public Vektor2d() {
        setX(0);
        setY(0);
    }


    /**
     * Konstruktor
     *
     * @param x Der gewünschte x-Wert des Vektors
     * @param y Der gewünschte y-Wert des Vektors
     * @since 15.11.2015
     */
    public Vektor2d(int x, int y) {
        setX(x);
        setY(y);
    }


    /**
     * Addiert den übergebenen Vektor auf.
     *
     * @param vec Der Vektor, der auf diesen aufaddiert werden soll.
     * @since 15.11.2015
     */
    public void add(Vektor2d vec) {
        setX(getX() + vec.getX());
        setY(getY() + vec.getY());
    }


    /**
     * Liefert die Länge dieses Vektors.
     *
     * @return Die absolute Länge.
     * @since 15.11.2015
     */
    public double getLength() {
        return Math.sqrt((getX() * getX()) + (getY() * getY()));
    }


    /**
     * Getter x
     *
     * @return Der x-Wert dieses Vektors.
     * @since 15.11.2015
     */
    public int getX() {
        return x;
    }


    /**
     * Getter y
     *
     * @return Der y-Wert dieses Vektors.
     * @since 15.11.2015
     */
    public int getY() {
        return y;
    }


    /**
     * Setter für x
     * Maximaler Wert: 100, minimaler Wert: -100.
     *
     * @param x Der gewünschte Wert für x.
     * @since 15.11.2015
     */
    public void setX(int x) {
        if(x > 100) {
            x = 100;
        }else if(x < -100) {
            x = -100;
        }
        this.x = x;
    }


    /**
     * Setter für y
     * Maximaler Wert: 100, minimaler Wert: -100.
     *
     * @param y Der gewünschte Wert für y.
     * @since 15.11.2015
     */
    public void setY(int y) {
        if(y > 100) {
            y = 100;
        }else if(y < -100) {
            y = -100;
        }
        this.y = y;
    }


    /**
     * Normale toString() Methode
     *
     * @return Alle relevanten Informationen zu diesem Objekt in einen String verpackt.
     * @since 15.11.2015
     */
    @Override
    public String toString() {
        return "Vektor2d-Objekt: x: " + getX() + ", y: " + getY();
    }

}

Es wäre nicht notwendig z.B. in der getLength()-Methode den Getter zu verwenden.

Wer z.B. setX() kopiert und die Kopie zu setY() umändert, sollte jedoch vorsichtig sein, da ein übersehenes x eine ärgerliche Fehlerquelle darstellt, im Idealfall schreibt man die setY()-Methode komplett und kopiert nicht.

Wer ein Programm schreibt, sollte möglichst alle Variablen gleich als private deklarieren und Getter und Setter verwenden, da auf diese Weise leicht Beschränkungen hinzugefügt werden können.

Meistens stehen die Konstruktoren ganz oben und Getter und Setter weiter unten.

Kommentieren Kommentare