Kihagyás

Practice 06

Repetition

  • Classes (data members, methods, constructors, toString)
  • Visibilities (private, protected, none, public)
  • Inheritance (super, extends, override)

For the class material you will also need the previous Cat example.

Polymorphism - Multiformity

Polymorphism is one of the foundations of object-oriented programming. But what exactly is polymorphism? How can we imagine it? First of all, it is important to clarify what kind of polymorphism we are talking about, because there are two types: static and dynamic polymorphism.

Static polymorphism

Static, or compile-time polymorphism, is the form of polymorphism that is decidable at compile time. In practice, this means that of several methods with the same name but different parameter lists, we can decide at compile time which version of a method will be called at a call site.

This might be known as overload. A very simple example:

public class Calculator {

    public int multiplication(int integer1, int integer2) {
        return integer1 * integer2;
    }

    public int multiplication(int integer1, int integer2, int integer3) {
        return integer1 * integer2 * integer3;
    }

    public String multiplication(String what, int howManyTimes) {
        return what.repeat(howManyTimes);
    }
}
This class is used within the main method:

public class CalcMain {
    public static void main(String[] args) {
        Calculator calc = new Calculator();

        System.out.println(calc.multiplication(2, 5));
        System.out.println(calc.multiplication(2, 5, 3));
        System.out.println(calc.multiplication("Pear", 4));
    }
}

Of course, this is just a simple example, in real life any class can require overloading, as we have seen with constructors for example, where we had several constructors with the same name, but the compiler knew which constructor we wanted to use based on the parameters.

But perhaps the more interesting thing about object-oriented programming will be the dynamic polymorphism.

Dynamic polymorphism

In this case, an instance of a child class can be treated as an instance of its parent, a Tiger object can be stored as a Feline instance (i.e. in a reference of type Feline), and even an array of ancestor types can store a mixture of parent and child types, such as cats, domestic cats, tigers and sabertooth tigers.

However, if we store an object of type child as a parent type, we will not see the (new) methods defined in the child type object's own class. For example:

public class CatMain {

    public static void main(String[] args) {
        Feline something0 = new Feline("Tiny", 10);
        Feline something1 = new HouseCat("Stripe", 4.9, true);
        Feline something2 = new Tiger("Lucy", 24);
        // something2.stronger(...) not visible this way
        Feline something3 = new SabertoothTiger("Buzz", 30);
    }
}

In the above code snippet, the commented out code snippet doesn't work because the average Feline can't tell if it's stronger than a Tiger. Since we store the Tiger object in a Feline reference, we can only use the methods defined in the Feline reference! We will see a method to get around this later.

The reason for this is that since we store the object in the Feline reference, we don't (yet) know what object the reference points to.

        Feline[] cats = new Feline[4];
        cats[0] = new Feline("Tiny", 10);
        cats[1] = new DomesticCat("Stripe", 4.9, true);
        cats[2] = new Tiger("Lucy", 24);
        cats[3] = new SabertoothTiger("Buzz", 30);

        for (int i = 0; i < cats.length; i++) {
            cats[i].meow();
        }

At compile time, we don't know yet what type of objects will be in the Feline array: Feline objects, Tiger objects, or SabertoothTiger objects, or maybe a mixture, since we can put child types in an array. However, if we call the meows() method of each element, we see that for regular felines the method defined in the Feline class is called, while for domestic cats the meows() method defined in the DemoesticCat class is called, and so on. The reason for this is late binding. You can read more about late and early binding here and here.

Source code output:

Output

Stripe meows: MeoOoOow

Lucy roars: RaaAaAaAaAaAaAaAaAaAaAaAaAwR

Buzz roars: RaaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAwR

As mentioned, the child type can be treated as parent, but it doesn't work the other way around! You can't put parent type objects, i.e. plain Feline type objects, into a Tiger array.

Packages

We can organize our classes into packages, as mentioned for UML. For Java classes, this means both a physical and a logical grouping, we usually create packages per different logical units, and using this we can get familiar with the keyword-less, package private visibility and avoid name collisions. Packages can also contain other packages, and can be used to create a complete package hierarchy (for example, a package that deals with the display of the user interface can have logically separate parts that can be located within this package).

Organizing into packages

If we want to put our class into a package, we need to physically put it into the folder or folder structure that symbolizes our package/package hierarchy, and then mark it in the first non-comment line of the source file using the package keyword.

package hu.szte.cats;

The meaning of this is: our class is in the package named cats, this package is in the package named szte, and this package is in the package named hu. Physically, the class is in the project rootdirectory/hu/szte/cats folder. The root directory of the project is the src folder you have seen many times in the project.

Usually the [reverse domain notation] is used to organize package hierarchies. For more information on package naming, see here.

You can create a package using IDEA (right-click on the src folder and then New > Package), in which case the class should start with the line package packagename;. Make sure that the directory structure is the same. If your source file is in the wrong package, most IDEs will help you sort it out (by moving the file or changing the package designation).

Each class has a full name, which consists of the full package hierarchy + class name. So the Feline class above has a fully qualified name if it is in the package hu.szte.cat: hu.szte.cat.Feline. Some other examples are java.lang.String, java.util.Scanner.

However, we never wrote such a long name if we wanted to create a String (or Scanner). And we can avoid this in the future by importing the contents of the necessary packages into our program.

Of course, we can also mark the organization into packages in UML, we can put the related classes (or other packages) into a package in UML, as you can see in the figure below.

Package in UML

Importing packages

Before referring to classes in another package, they must be imported, or their full name must be used (throughout the program) so that the compiler knows what we mean.

import hu.szte.cats.Feline;
import hu.szte.cats.SabertoothTiger;

When importing many classes from a package, instead of importing one by one, you can import all classes in the package using the asterisk character:

import hu.szte.cats.*;

You can sort your imports using IDEA, which you can do by using Ctrl+Alt+O (or the Optimize Imports menu item)

This applies to our own classes, but also to other classes that we didn't write (for example, ready-made classes in the JDK). Exceptions to this are all java.lang classes, which include for example all wrapper classes (Integer, Float, etc.) or the String class. We have never imported this package before, and have been able to create String instances.

We can use static import, but in many cases this will only make the code less readable, understandable and maintainable. You can read more about static import here.

In our Java files, package markup (if any) always precedes imports, and indeed everything else, except for a comment before the package package name; line.

Visibilities

UML markup Modifier Class Package Descendants All
+ public Visible Visible Visible Visible
# protected Visible Visible Visible Not visible
~ no keyword Visible Visible Not visible Not visible
- private Visible Not visible Not visible Not visible

Garbage collection

Object lifetime in Java: (lifetime = from object creation to memory release) When an object is created with the new keyword, the object is created on the heap (unlike primitive types). The memory release is done automatically in Java, by the garbage collector (garbage collector). For more information about the Java garbage collector, see this, this and this page. If an object is no longer used, it can be set to null, a keyword indicating that the reference does not refer to any object (e.g. tiger = null;) Call garbage collector manually (may not run):

System.gc();

You can have a method called finalize() for your classes. This method will be run when the garbage collector frees memory that is deemed redundant, and a particular object that is no longer in use will be deleted. It is of course not known when it will run, or if it will run at all. The purpose of the method is to free up some resource used by the object (an example of this will be given later).

Tasks

Pub simulator creation

Packages