Topic 3
Aggregation and Polymorphism
First - a revision exercise
We will begin with a short revision exercise on last week's material.
Exercise 1
Answer to exercise 1
The code presented above is not ideal for reusability. The eat() method is displaying the success, or otherwise, by means of println() statements. This means that it could not be reused within a non-console application (e.g. a GUI or Android app) and furthermore, the application would be forced to display the exact text of the success or error messages; the application developer could not customise how the app displays the success or failure of the eat() method.
As we saw last week, it would be better to return a boolean to indicate the success or otherwise. Then, each application making use of the Cat class could decide how to present the success or failure of the eat() operation to its users.
Collections and Lists
Previously we looked at how to create simple arrays. However, we can also use other data structures in Kotlin, for example the List (similar to the Python list), and the HashMap (similar to the Python dictionary).
In Kotlin, collections divide into mutable collections (those that can be altered) and immutable collections (those which cannot, which are more efficient in cases where the data in the collection never changes).
The List
This example shows basic use of a List:
We create a list of former lecturers at the university and use a for/in loop to iterate through each.
- Note that listOf() returns an immutable list (a list of type List); this cannot be altered (e.g. it does not have an add() method)
Creating a Mutable List
This example shows basic use of a mutable List:
Note how we use mutableListOf.
- This returns an object of class
MutableList, which has anadd()method - Also note that
peopleListisval, notvar, even though it's aMutableList- Even though we add new data to the list, the actual object reference
peopleList(which represents the memory address of the list) doesnotchange - Hence we can use a
value for this example
- Even though we add new data to the list, the actual object reference
We can create an empty list by omitting the arguments, e.g:
Note how we have to specify the data type of the list in angle brackets here (
<String>), because the Kotlin compiler is unable to work it out from the contents of the list.
The Map
- Kotlin has a Map, which is similar to the equivalent HashMap data structure in Java, the dictionary in Python, or the associative array in PHP
- A Map is a collection which can be indexed using non-numeric indices, called keys
- It is the same concept as a dictionary in Python
- Like Python dictionaries, maps can be indexed using simple array indexing syntax (i.e. the square brackets, [])
- Like lists, Maps can be mutable or immutable
Creating a mutable Map
This example creates a MutableMap:
- Note how we use the syntax "key to value" when initialising the map, to set up the keys (indices) and values
- Note that all keys and all values have to be a particular type (both String here)
- Also note how we can embed more complex expressions in strings through the use of the dollar symbol
$followed by the expression in curly brackets. So for example, here we wish to embedolympus["name"]inside the string we are displaying. Because the quotes surrounding the indexname(i.e."name") would conflict with the quotes surrounding the string as a whole, we surround the whole expression with a dollar and curly brackets:${olympus["name"]}. In general, complex expressions of any kind (anything more complex than a simple variable) should be surrounded with brackets preceded by a dollar sign.
Aggregation
So far we have considered single objects, or objects within arrays or lists. However, real object-oriented systems typically have many objects interacting with one another. A common scenario is to have objects within other objects and this is called aggregation.
Aggregation is one of two similar concepts. The other is composition. What is the difference between the two?
- With aggregation, the inner object can exist without the outer object. So for example, the driver of a car might be a
Personobject aggregated within theCarobject. - With composition, the inner object cannot meaningfully exist outside the outer object. So the relation between a car and its engine might be a composition relation, because the engine cannot do anything meaningful outside the car.
Why is aggregation useful?
Imagine we wanted to write a program to manage stock for a shop. We could write a main() which creates an list of Product objects (as in the example above) and implement functionality within the main() to search for products, sells products, adds new products and so on. However a more object-oriented approach would be to create a class which represents the shop as a whole. This class could be called Shop. It could contain methods to add a new product, search for a product, or sell a product, and could contain, within it, a mutable list of Product objects (this would be the aggregation). The advantage of creating a Shop class is that it could be reusable: we could create a Shop class which represents a shop, and then reuse it in many different programs. Also it allows another level of encapsulation: the outside world can use the Shop via its method, without knowing its inner workings.
Example
This example shows the use of a Shop class, as well as a Product class and a test main().
How is this working?
- We have a Shop class which contains a mutable list of Products within it. This is aggregation.
- The list of Products is private. This is encapsulation again. The outside world (e.g. the main() ) cannot access the list directly. Access to the list is controlled by the Shop. The only way the outside world can manipulate the list is via the methods of Shop. You can think of each method of Shop as a "portal" or "gateway" through which the inner workings of Shop exchange information from the outside world. Each "portal" (method) would typically include some validation, to check that the outside world isn't doing anything unrealistic. The diagram below shows this.

- For example,
searchForProduct()will returnnullif the product cannot be found. Because of this, the method needs to return a nullableProduct. The
sellProduct()method callssearchForProduct()to try and find the product with that name. If it can be found, the product is sold by calling itssell()method. Note the expression we use to return the appropriate boolean value:. This is returning
falseifsell()returnsnull; we will discuss this in more detail below.Therefore, we have a Shop class which models how a shop works in Kotlin code - with inbuilt error checking. This Shop class, with its error checks, could thus be reused in any Kotlin program, and the error checks wouldn't have to be written again.
Introducing Elvis
You will have seen the code:
in the previous example. What does it mean?
- This code is trying to return the value of
p.sell(), which will be a boolean indicating whether the product could be sold successfully. - However,
pmight be null (if that product does not exist), so we have to use the safe-access operator?.to callsell() - What do we return if
pis null however? - We use the Elvis operator,
?:. (named because it looks a bit like Elvis Presley, side-on). - In cases where we're trying to call a method (or access an property) of a nullable with the safe-access operator, the Elvis operator allows us to set a default value in cases where the nullable is null
- So here, if
pis null and we cannot callsell(), we will return the valuefalse. - Thus, this is a concise way of returning a value using the Elvis operator to account for
ppossibly being null. Ifpis not null, we will return whateversell()returns (true or false depending on whether the product is in stock). Otherwise, we will returnfalseusing the Elvis operator, to indicate that the product could not be found. - More on nullability here.
Returning boolean status codes to indicate success or failure
You'll also notice in the above code that sell() and sellProduct() do not use a println to inform the user of success or otherwise. Instead, they return a boolean. This boolean status code is then used in the main() to print an appropriate message.
Why is this done? It's the same reason we used toString() in earlier topics. It increases the reusability of our code, in that it can now be used in non-console applications such as desktop GUI applications web applications or Android apps, where println() cannot be used. The equivalent of main() in these apps could test the boolean return code and display an appropriate message in an appropriate format. Also it means that different console-mode applications which use our code can display their own message rather than a hard-coded message within Product or Shop.
Also note this code in the main():
This once again shows the use of an if statement as an expression, as seen last week. It evaluates to either "$p sold" if the product could be sold (true is returned) or "$p could not be sold" if the product could not be sold (false is returned).
Polymorphism
Polymorphism ("many forms") is another key feature of object-oriented development. It means:
- the ability to produce different behaviour with a superclass method, depending on the subclass we are actually referencing
What does this mean? Imagine we have an object of a superclass type which actually references an instance of the subclass, and when we call a method, the subclass version of the method will be called. This is probably best shown by example.
Above is an inheritance hierarchy consisting of Animal as an abstract superclass and various specific types of animal as subclasses. The subclasses override the makeNoise() method by returning a string representing the noise that the animal makes. Note also the concise syntax for methods which simply return a value:
We do not need to declare the return type or define a code block for the method, but simply set the function "equal to" the value we wish to return.
Moving onto a main() which illustrates polymorphism by using this inheritance hierarchy:
Note how we create a list of Animal objects but fill it with subclasses of Animal. We are able to do this: A variable of a superclass type can point to any subclass of that superclass.
The polymorphism is then illustrated by the loop. We loop through all the animals in the list and call makeNoise() on each. Even though the variable animal is of type Animal, the appropriate makeNoise() method for the animal at that position in the list will be called. So our output will be:
Runtime Binding
Polymorphism is able to occur due to runtime binding. This means that the method to call is decided at run time (when the program is run) rather than when we compile the program. Think about why that has to be true for polymorphism to work. In the above example, we hardcoded the animals in our list to be a dog, a cat and a cow. But in some examples, the contents of the list might depend on user input. For example, in a student records system application, you might have a list containing students, including undergraduates and postgraduates. But the contents of the list would depend on the university administrator adding students to the list while the program is running, in other words, there is no way in which the compiler can determine the exact contents of the list.
Polymorphism in Method Parameters
Another scenario where polymorphism is freqently observed is in method parameters. method parameter of any given superclass will allow us to pass in any subclass of that superclass as an argument to the method. If we then perform an operation on the object passed to the method, the object will behave as the appropriate subclass, even though the type of the parameter is the superclass
Here is an example:
- The Oven class includes a
cook()method which takes an item ofFoodas a parameter and displays its cooking time. This allowscook()to cook any subclass ofFood. ThegetCookingTime()method of the particular subclass of Food that has been added to the Oven will be called, even thoughfis of typeFood - So, the parameter
fwill exhibit different behaviour depending on what subclass it refers to.
Food inheritance hierarchy
Imagine the following:
- The
getCookingTime()method ofFishreturns 50 * the weight of the fish - The
getCookingTime()method ofVegetablesreturns 10 * the weight of the vegetables - The
getCookingTime()method ofCakereturns 100 * the weight of the cake
Now imagine a main():
This will give us the following results:
Even though the data type of the parameter passed to Oven.cook() is Food, it will be referring to a subclass, so the subclass version of getCookingTime() will be called.
Coding Exercises
Ensure you have done up to question 8 from Week 2 first.
- Lists: Return to your
Studentexample from previous weeks by cloning it from GitHub. In yourmain(), create a mutable list of students, and in the loop you have written already, add eachStudentobject (these could beUndergraduateorMastersobjects if you did the inheritance exercise) to the list as soon as you create it. Then, when all students have been entered, write a second loop to loop through the list and display each student in turn. Aggregation: Make a new branch on your
Studentproject calledaggregationand switch to it in Git Bash:In this branch, create a
Universityclass. TheUniversityshould contain, as an attribute, a list ofStudents. TheUniversityshould include these methods:enrolStudent()- this should add a new student to the university by adding it to the list.findStudentById()- this should find a single student by student ID. If no student with that ID can be found, it should returnnull.findStudentsByName()- this should find all students that have a particular name. It should return a list of all matching students. To do this, you will need to declare a mutable list in the method, loop through all students, and add any students with that name to the list before returning it at the end. The return type should beMutableList<Student>.
In your
main(), create aUniversityobject. Then, develop a simple menu-driven application to add and search for students. The menu should look like this:You will need a
Stringvariable to read in the user's menu choice. Add awhileloop which keeps looping until this variable has the value "4". Inside the loop, test which option the user entered, and either:- add a student (by reading in the student details, creating a
Studentobject and adding it to theUniversity), - search for a student using their ID (printing out the found student), or
- search for all students with a given name, printing out the results.
Make sure you push to the aggregation branch on GitHub, i.e.
- Polymorphism: You need to have completed the inheritance question from Topic 2 in order to complete this question. Enhance the application so that a user can enter either undergraduate or masters students when they add a student. Depending on what the user enters, create either an undergraduate or a masters student and add it to the list of students. Test it by searching for the students you've just added. This should illustrate polymorphism by displaying either undergraduate details or masters' details, depending on what type of student was found.