Classes

Contents

 

 

introduction

 

Inheritance

 

Public and Private

 

designers

 

Static variables and methods

 

this

 

Golden rules for classes and inheritance

 

Summary

 

Examples

 

Arrays of objects

 

Exceptions

 

Practice exercises

introduction

Classes are special data types. Classes are groups of

Since classes are data types, variables of this data type can be created. These variables are called instances of the class, or simply objects. The objects are created on the heap . Only references to the objects are stored on the stack . This is the same behavior as arrays, which are also objects in this sense. A background process ( garbage collector) releases the memory space for the objects when they are no longer needed (in C, the programmer is responsible for this).

A simple class for demonstration purposes is, for example, a flag. It should

Here is the declaration of such a class flag

class Flag
{ boolean F; // Variable for the property
    
    void set() // Set method
    {   
        F=true;
    }

    void clear () // Clear method
    {   
        F=false;
    }

    boolean isSet () // Query the status
    {   
        return F;
    }
}

In general, this declaration looks like this.

class name 
{    
     declarations 
}

Here, `name` is the name of the data type. Declarations are either variable declarations or functions. In this example, `set` is used to set the flag, and `clear` is used to clear the flag. `isSet` queries the state of the flag.

The class definition above can be in the same file as our Test class, i.e., in Test.java . Alternatively, it can be in a separate file, which must then be named Flag.java . If the class is declared as public , it must be in its own file.

How do you use this Flag class? Here's an example:

public class Test
{   
    static public void main (String args[])
    {   
        Flag f=new Flag();
        f.set(); // sets fF to true
        System.out.println(f.isSet()); // prints true
        f.clear(); // sets fF to false
        if (!f.isSet()) // is true!
            System.out.println("f is not set!");
    }
}

One declares a reference to an object of type Flag as usual using

Flag f;

However, to create the object on the heap, you must create it using `new` . The command `new` is used for this purpose.

new Flag();

This results in a reference to a flag object. This reference is assigned to f . The purpose of the parentheses () will become clear later.

How do you call a method of f ? The notation used for this is...

f.set();

This is a function call to `set` that refers to `f` . This function call appears to have no parameters. However, it implicitly receives a reference to `f` as a parameter. Within ` set` , the variable `F` is accessed, and `set` needs to know in which instance this variable resides.

You can also address the variables of f directly, for example

fF=true;

instead of f.set() . As we will see, however, this does not correspond to the original intentions of object-oriented programming.

Inheritance

Suppose we want to extend the `Flag` class. Of course, we could simply modify the `Flag` source code . However, let's further assume that this source code is already widely used and we therefore don't want to touch it, or that we only have access to the compiled code. Perhaps we simply want to keep the class as simple as possible and only need the extension for a specific purpose.

This is a task for inheritance. The Flag class is extended while retaining its original properties and methods.

As an example, we extend Flag with the toggle method , which makes Flag true if it is false, and vice versa. It looks like this.

class ToggleFlag extends Flag
{   
    void toggle()
    {   
        if (isSet()) clear();
        else set();
    }
}

ToggleFlag inherits everything from Flag , and adds a toggle method .

The main program that uses this class looks almost exactly the same as before.

public class Test
{   
    static public void main (String args[])
    {   
        ToggleFlag f=new ToggleFlag();
        f.set();
        System.out.println(f.isSet());
        f.clear();
        if (!f.isSet())
            System.out.println("f is not set!");
        f.toggle(); // the new method is used here
        if (!f.isSet())
            System.out.println("f is not set!");
    }
}

ToggleFlag functions largely like Flag . It only has one additional feature: the toggle() method .

Of course, new variables can also be included in children's classes.

Inheritance is one of the most important properties for code reuse.
Old code remains and new code is added.

However, it is also possible to discard old code and overwrite it with new code . As an example, let's consider a class called `Flag` that logs every attempt to call `clear()` , but does not set the flag.

class StrangeFlag extends Flag
{   
    void clear()
    {   
       System.out.println("clear called");
    }
}

The `clear` method of `StrangeFlag` (called `StrangeFlag.clear()` ) prints something, but it doesn't delete `F` . It therefore does something different than the method with the same name in the parent class. This type of redefinition of functions is called overriding.

Variable overriding is also possible, but not common. All new methods of the inherited class then refer to the new variable. In effect, there are two variables with the same name in the inherited class. Obviously, this can lead to confusion.

You can explicitly access the variables and methods of the parent class by prefixing their name with `super.` . In the following example, we first call the `clear` method of `Flag` before logging the call. After that, `clear` functions as before, but also prints something.

class StrangeFlag extends Flag
{   
    void clear()
    {   
        super.clear(); // calls Flag.clear
        System.out.println("clear called");
    }
}

Public and Private

The purpose of the object-oriented approach is to

The second goal, in particular, requires hiding the class's implementation details. Let's assume we defined ToggleFlag as follows.

class ToggleFlag extends Flag
{   
    void toggle()
    {   
        F=!F;
    }
}

This works as long as the base class `Flag` doesn't change and, for example, `F` isn't removed. It's much more logical to use the methods of `Flag`. To prevent child classes or other classes from using details that might change, you can privatize data or methods. In the case of `Flag`, this looks like this:

class Flag
{   
    private boolean F;
    
    public void set()
    {   
        F=true;
    }

    public void clear()
    {   
        F=false;
    }

    public boolean isSet()
    {   
        return F;
    }
}

Now, F can only be accessed via the interfaces set , clear , and isSet .

If you want to ensure that only children can access F, you can declare F as protected , i.e.

... protected boolean F;

designers

It is currently unclear whether our flag is initially set or cleared. While Java ensures that boolean variables are initially cleared, this can also be achieved explicitly using a constructor .

This is what it looks like.

class Flag
{   
    protected boolean F;
    
    public Flag()
    {   
        F=false;
    }

    ...
}

A constructor is therefore a method that has the same name as the class. It has no return type. The constructor is called shortly after an instance of Flag has been created.

It is now also possible to pass a parameter to the constructor. Even having both in the same class is possible using overloading.

class Flag
{   
    protected boolean F;
    
    public Flag()
    {   
        F=false;
    }
    
    public Flag (boolean f)
    {   
        F=f;
    }
    ...
}

The parameter is passed to `new`.

Flag f=new Flag(true);

defines a flag that is initially true. The flag

Flag f=new Flag();

However, it is not initially set because the constructor is called without parameters (the so-called default constructor).

Constructors with parameters are not automatically inherited . They must be declared separately in the child class. However, you can simply call the constructor of the parent class. This is done using the reserved method `super` , which must be placed at the very beginning of the constructor.

class ToggleFlag extends Flag
{   
    public ToggleFlag (boolean f)
    {   
        super(f);
        ...
    }
    ...
}

However, the default constructor is inherited and does not need to be redeclared.

Variables can also be initialized by assigning them a value during declaration. Such initializations are performed before the constructor. The `Flag` class looks like this.

class Flag
{   
    private boolean F=false; // Initialization before the constructor
    
    public void set()
    {   
        F=true;
    }

    public void clear()
    {   
        F=false;
    }

    public boolean isSet()
    {   
        return F;
    }
}

Static variables and methods

We had already used a class declaration before, as in HelloWorld.

public class HelloWorld
{   
    public static void main (String args[])
    {   
        System.out.println("Hello World");
    }
}

This is a class definition. It is public and therefore must be declared in a file named HelloWorld.java , which is only important in the context of packages.

The `main` method of `HelloWorld` is declared as static . This means that it exists only once per class, even if no instance of the class has been created. Therefore, it can be called even without an instance. No implicit reference to an object is passed to it.

Static methods (functions) and properties (variables) of a class exist only once per class .
Non-static methods and properties exist once per instance .

What happens when you enter the command line?

Java HelloWorld

Actually, the Java interpreter looks for a `main` method in the class specified on the command line (in this case, ` HelloWorld` ) and calls it. This method must also be public . In the chapter on subroutines, we called other subroutines from `main` , all of which must be static because no instance of `HelloWorld` was created.

You can declare static variables and methods in any class. These methods are called using the class name instead of the name of an object. We have already encountered such methods. Example:

double x=Math.sqrt(2.0);

sqrt is a static method of the Math class . Another example is

System.out.println("Hello World");

Here you can see a variable ` out` in the class `System` . This variable ` out` is of type `PrintWriter` and has a method called `println` , which is ultimately called.

You can even execute things statically as soon as the class is loaded. This block of statements then acts like a constructor, which is executed once per class. In particular, you can initialize static variables there. It looks something like this:

class Flag
{   
    protected boolean F;
    static
    {   
        System.out.println("Class Flag was used for the first time!");
    }
    ...
}

However, it is also possible to initialize static variables directly.

static double PI=3.14151...;

this

As previously explained, every method, including constructors, implicitly receives a reference to the class instance when it is called. (This does not apply to static methods.) This reference can then be reused under the name ` this` . This might be necessary, for example, if you want to pass the current instance (whose method was called) to another subroutine as a parameter.

The following excerpt serves as an example:

Class A
{   
    void funcA (BClass B)
    {   
        ...
    }
}

class B
{   
    void funcB (AClass A)
    {   
        ...
        A.funcA(this);
    }
}

When calling another method of the same class, it is not necessary to explicitly specify the instance. However, you can do this by using `this` as the instance, for example:

this.func();

Another use of `this` is to call another constructor of the same class. The `Flag` class could also have looked like this.

class Flag
{   
    protected boolean F;
    
    public Flag()
    {   
        this(false); // calls the constructor with parameters.
    }
    public Flag (boolean f)
    {   
        F=f;
    }
    ...
}

Here, `this(false);` calls the constructor with a parameter.

Golden rules

Assuming that the class Child is descended from the class Father , the following rules for inheritance can be established.

Summary

We want to summarize the advantages that the use of classes offers.

Examples

A stack storage

We'll program a last-in, first-out (FIFO) stack for int values. For simplicity, the stack will have a maximum capacity. Note that Java actually already includes a `Stack` class that you can use immediately. This class doesn't even have a limit. However, you can only store int values ​​as objects in this class. More on that later.

The stack naturally needs methods to add numbers to it or remove numbers from it. We also give it a method to view the topmost number without removing it.

Now the code for the Stack class :

class Stack
{ private int Size; // current size 
    private int V[]; // the actual memory
    
    public Stack (int capacity)
    { V=new int[capacity];
        Size=0;
    }

    // place an element on the stack
    public void push (int value)
    { V[Size++]=value; // save and increment size
    }
    
    // pull an element from stack
    public int pull()
    { Size--; // Decrease size
        return V[Size]; // Return the element after the last one
    }
    
    // return the number of elements
    public int size()
    { return Size;
    }
    
    // read top element
    public int peek ()
    { return V[Size-1];
    }
}

There is no error handling. In particular, peek only works on a non-empty stack. Nevertheless, this class is already quite useful.

Now we will revisit the problem of the Towers of Hanoi . Since each tower represents a stack, it makes sense to define the towers as children of the Stack class. However, we still need to check whether placing a disk on top is even allowed.

class Tower extends Stack
{ private int Number; // The number of the tower (1, 2 or 3)

    public Tower (int capacity, int number)
    { super(capacity);
        Number=number;
    }
    
    // push with test if topmost element is smaller
    public void push (int value)
    { if (size()==0 || peek()>value) super.push(value);
        else System.out.println("Illegal move");
    }
    
    public int number ()
    { return Number;
    }
}

Note the call to the Stack.push method using super.push() . Each stack also has a number, which can be retrieved using number() .

The actual main program is similar to the code without objects .

public class Hanoi
{ static Tower A,B,C;

    static public void main (String args[])
    { int n=4;
        // Procure towers:
        A=new Tower(n,1); B=new Tower(n,2); C=new Tower(n,3);
        // Fill Tower A
        for (int i=n; i>=1; i--) A.push(i);
        // Problem solving:
        move(n,A,B,C);
    }

    // Recursive move method
    static void move (int n, tower a, tower b, tower c)
    { if (n==1)
        { b.push(a.pull()); // actual simulation of the movement
            System.out.println(
				"Move disk from "+a.number()+" to "+b.number());
                // Print the movement
        }
        else
        { move(n-1,a,c,b);
            move(1,a,b,c);
            move(n-1,c,b,a);
        }
    }
}

Now the towers need to be initialized. Additionally, the first tower needs to be equipped with disks. Furthermore, the code for adding a disk is already included in the `move` class . Here you can find the complete code for this example. Here is the code that uses the predefined stack class.

Using predefined classes

Java offers a large number of predefined classes. These are organized into packages and must be imported (except for the java.lang package , which contains classes like Math and System ). We'll learn more about packages later.

We will now use the `Random` class as an example , which provides a random number generator. Note how easy this class is to use once you understand its constructor and methods. This information is now fully documented in the Java documentation.

import java.util.Random; // necessary to use Random

public class Test
{   
    static public void main (String args[])
    {   
        Random r=new Random(1017);
            // initialize the generator with 1017
        for (int i=0; i<10; i++)
            System.out.println(r.nextGaussian()); 
                // generate 10 normally distributed random numbers
    }
}

Static classes

The following class provides a temperature converter. It is not intended to create instances, as it only contains static elements.

class temperature
{   
    private static double factor;
    
    static
    { Factor=9.0/5.0;
    }

    public static double Fahrenheit (double c)
    { return c*Factor+32.0;
    }

    public static double celsius (double f)
    { return (f-32.0)/Factor;
    }
}

A typical call looks something like this:

public class Test
{   
    static public void main (String args[])
    {   
        System.out.println(Temperature.fahrenheit(20));
    }
}

Static imports

This is an abbreviation. If the variable PI is needed multiple times in a file , you have to write Math.PI each time . Alternatively, it is possible to import the variable PI statically.

import static java.lang.Math.PI;

Now you can simply write PI .

Arrays of objects

When creating an array of instances of a class, the individual objects must be initialized in a loop. Although the array is created using ` new` , it initially contains only empty references (with the value `null` ).

To create approximately 10 flags, you will need the following code:

Flag f[] = new Flag[10];
for (int i=0; i<10; i++) Flag[i] = new Flag();

Exceptions

In Java, error conditions are handled by exceptions . These exceptions can be caught using a try statement. As an example, we'll catch a division by zero:

public class Test
{   
    public static void main (String args[])
    { try
        {   
            int n=0;
            n=1/n;
        }
        catch (Exception e)
        {   
            System.out.println("Caught: "+e);
        }
    }
}

The output of this program is

Caught: java.lang.ArithmeticException: / by zero

The program can continue normally after the exception is caught. In particular, the error that occurred can be handled and possibly corrected.

In general, the try statement has the following form:

try
 statement block 
catch ( ExceptionType  Name)  statement block
...
catch ( ExceptionType  Name ) statement block 
finally statement block

ExceptionType is any child class of Exception . As above, you can also catch all exceptions in general. The name is only used to identify the exception within the catch block. The block at `finally` is always executed, regardless of which exception occurred. `finally` can be omitted.

An exception doesn't necessarily need to be caught. It can also be propagated upwards from a method. This is done using the `throws` statement. This statement indicates that an exception is possible within a method. The calling program must then catch the exception. As an example, we define a new child class of `Exception` that indicates that an array index is odd.

class IndexOddException extends Exception
{   
}

public class Test
{   
    public static void main (String args[])
    {   
        try
        {   
            test(new double[20],11);
        }
        catch (IndexOddException e)
        {   
            System.out.println("Odd");
        }
        catch (ArrayIndexOutOfBoundsException e)
        {   
            System.out.println("Out of bounds");
        }
    }

    static double test (double a[], int i) throws IndexOddException
    {   
        if (i%2==1) throw new IndexOddException();
        return a[i];
    }
}

The new exception serves only to differentiate between the two catch statements.

An ideal candidate for an exception would be error handling in a stack. For example, our Towers of Hanoi example could throw an exception if an attempt is made to place a disk on top of a larger one.

Important details

Exercisetasks

  1. Write a class `Koord` that can hold an x-coordinate and a y-coordinate. The class should have a constructor `Koord(x,y)` .
  2. Add methods to the class for reading x and y , and for setting x and y (simultaneously), namely x() , y() , and set(x,y) . Test with a main program.
  3. Write Point and Circle as children of Coord . Circle should have an additional variable r , including methods for setting the radius.
  4. Write a subprogram of the test class that tests whether a point lies in a circle, namely boolean contains(circle k, point p) .
  5. Generate 1,000,000 random points and test how many lie within a circle with a radius of 1/2 centered at (1/2, 1/2) . Do not save the points!

Solution .

Problems without solutions

  1. Write a class for complex numbers. The constructor should look like this:
    Complex z = new Complex(x,y);
  2. Write the operators + , - , * and / and the function abs for complex numbers as static functions of the class Complex , e.g.
    static public Complex plus (Complex a, Complex b);
  3. Write a conversion toString() as a method of Complex . Then try printing complex numbers directly using System.out.println() .
  4. Generate an array containing complex numbers.
  5. Write a subroutine that calculates the scalar product of two complex vectors.
  6. Rewrite the Stack class so that it throws an exception in case of an error.

Back to the Java course