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);
}
}
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.
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.
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.
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:
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):
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