5. Reference Types - ZolotovaNatalia/JavaCourse_2017 GitHub Wiki

Reference Types

Java is a strongly typed language, which means that every variable, every expression should have a type. There are two categories of types in Java: primitive types and reference types. Primitive Data types you can find in the lecture before : primitive data types and also in the Official Oracle Documentation.

Today we talk about Reference types.

What is an object?

The idea behind object oriented programming, is to create our programs in a way that match the problem that we're trying to solve. Imagine that we need to organize coordination and monitoring of the city transport, so that every vehicle(bus, train or tram) can reach the needed station at the right time according to its timetable without outage or delay. This task seems rather complicated, especially if we talk about a big overloaded city with heavy traffic. If we think about what's involved in solving that problem, we can think of lots of real-world objects and entities that are part of this problem. They can be buses, trains, stations, passengers, track of each vehicle. These are the objects that we're all familiar with. They all have their own properties and special behavior.

To resolve the problem like this we're going to structure our code around these real-world objects that can interact with each other in different ways.

For more information see Oracle Official Documentation.

Objects in programming

The software objects are very similar to real-world objects. They also have a state and a behavior. A software object's state is stored in fields and behavior is shown via methods. Using methods we can receive and change properties of objects, operate on the internal state of an object, implement object-to-object communication. All objects have types. In order to define a type we need to create a Class.

What is a Class?

Class is a custom data type that has properties - fields and methods that represent behavior of the future objects.

Let's think about the object of the "Bus" and create a class for this type:

public class Bus{
    int id = 0;
    int maxAmountOfPassengers = 0;
    String brand = "";
    String color = "";
}

The keyword class tells Java that I'm creating a new class. And public, which we'll talk about later, means that my class is public to the world. Anyone can use it. Inside, after that first curly brace, is the definition of my class. Pay attention, that all this code should locate in the file called Bus.java. That's just a rule that Java enforces. If you use Intellij or any other IDE, it thinks about this for you.

So let's look a little closely at the definition for this class. Here you can see the data that is associated with the Bus: id, amount of passengers, brand, color. They are represented in different types: id and maxAmountOfPassengers are integers, color and brand are String. These are what are called member variables. These are variables that exist throughout the class that represent some essential pieces of data that we need to represent our Bus in this case.

Now let's add methods that can give us access to the Bus's fields. They are called getters and setter. Getters return us values of fields, and setters let us to set some value.

public class Bus {

    private int id = 0;
    private int maxAmountOfPassengers = 0;
    private String brand = "";
    private String color = "";

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getMaxAmountOfPassengers() {
        return maxAmountOfPassengers;
    }

    public void setMaxAmountOfPassengers(int maxAmountOfPassengers) {
        this.maxAmountOfPassengers = maxAmountOfPassengers;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}

Pay attention that the fields now have the key word private which means, that they are not seen to the external world and only accessible via getters and setters.

Let's have a look at the method's definition.

Methods with return types

Methods that are supposed to return some value or an object to the external world should be defined with a return type of this value or object. Return type has the second position in the method declaration before the method name. For example, method getColor() returns us a color field that has the type String. That's why the method has String as the return type:

public String getColor() {
    return color;
}

The keyword return tells the compiler that the method returns exactly this value or object.

Methods with no return types

Methods that are supposed not to return any value, but perform some behavior instead have the type void. This keyword shows the compiler that the method returns nothing. For example, method setColor(String color) has the type void, because it was define not to return something but to set the color of our bus, that is passed as the method argument String color.

public void setColor(String color) {
    this.color = color;
}

The keyword this means that we use exactly THIS object and its fields. In our case we use color of THIS Bus and change it to the color that we received in arguments.

While defining any method we have to think in advance which type should have method arguments or return values and use a specific syntax for different cases. Any violation of these rules leads to a compilation error.

Having defined the class Bus we can create an object Bus and set the fields. For that we have to use Constructor.

Constructors in Java

Constructor is a special method that gets called when an objects get created. So when we ask Java to give a new object of type Bus, we call this method. Constructor doesn't have a return type, so we simply say public and then next word in the declaration of this method is just the name of the class. So public, Bus, that's our constructor.

public Bus(){ }

This is a default constructor without arguments and is build by the Java compiler, so you don't have to define it explicitly. But if we want to pass some parameters to the object that we are going to create, for example, id of the bus, brand or color, we need to define a custom constructor that accepts this parameters and set them to properties:

public class Bus{

...

    public Bus(int id, int maxAmountOfPassengers, String brand, String color){
        this.id = id;
        this.maxAmountOfPassengers = maxAmountOfPassengers;
        this.brand = brand;
        this.color = color;
    }

...

}

But doing in this way you deprecate the default constructor and if you want to use it in the future as well, you have to define it also. In this example we leave only custom constructor.

Constructor with arguments lets us to create object with predefined values for its fields, that cannot be changed without special methods. In our case, having such constructor, we can remove setters, because the values will be set via constructor and cannot be changed in the future. Now we have such version of this class:

public class Bus {
    private int id = 0;
    private int maxAmountOfPassengers = 0;
    private String brand = "";
    private String color = "";

    public Bus(int id, int maxAmountOfPassengers, String brand, String color){
        this.id = id;
        this.maxAmountOfPassengers = maxAmountOfPassengers;
        this.brand = brand;
        this.color = color;
    }

    public int getId() {
        return id;
    }

    public int getMaxAmountOfPassengers() {
        return maxAmountOfPassengers;
    }


    public String getBrand() {
        return brand;
    }

    public String getColor() {
        return color;
    }
}

Let's see how to create a Bus object.

Create objects

Create another class called Street with the main function: Tip: to generate main method very quickly you can use an autocompletion in Intellij. Just type psvm and Intellij suggests you to add public static void main() method.

Inside this main method declare and initialize variables for id, maxAmountOfPassengers, brand and color. To produce a bus call the keyword new, then say Bus and then have the parentheses in passing those arguments:

public class Street {
    public static void main(String[] args) {
        int id = 1;
        int maxAmountOfPassengers = 30;
        String brand = "BMW";
        String color = "red";
        
        new Bus(id, maxAmountOfPassengers, brand, color);
    }
}

We created our first bus, but we can't use it. In Java we need references to use objects, that's why such non-primitive types are called Reference Types. For reference we simply need to define variable with the type Bus: Bus bmwBus and refer it a newly created object:

public class Street {
    public static void main(String[] args) {
        int id = 1;
        int maxAmountOfPassengers = 30;
        String brand = "BMW";
        String color = "red";

       Bus bmwBus = new Bus(id, maxAmountOfPassengers, brand, color);
    }
}

Now we manufactured our first BMW Bus which we can now manipulate. For instance, we can receive bus's properties using its methods. For now Bus has only getters. Later we will create more other funny methods. To call any method that belongs to an object type reference name, dot and the method: bmwBus.getBrand(). Print bus properties to the console:

public 
public class Street {
    public static void main(String[] args) {
        int id = 1;
        int maxAmountOfPassengers = 30;
        String brand = "BMW";
        String color = "red";

       Bus bmwBus = new Bus(id, maxAmountOfPassengers, brand, color);

       System.out.println("New bus: ");
       System.out.println("Id = " + bmwBus.getId());
       System.out.println("Max amount of passengers = " + bmwBus.getMaxAmountOfPassengers());
       System.out.println("Producer = " + bmwBus.getBrand());
       System.out.println("Color = " + bmwBus.getColor());
    }
}

New methods for the Bus.

Add new other public method to the class Bus that can simulate some behavior: go(), stop(), openDoors(), closeDoors(). These methods don't have to return smth, so they just print the command that is given by its driver:

public class Bus {

...

    public void go(int dist){
        System.out.println("Bus go " + dist + " km");
    }

    public void stop(){
        System.out.println("Bus stop");
    }

    public void openDoors(){
        System.out.println("Doors are opened");
    }

    public void closeDoors(){
        System.out.println("Doors are closed");
    }

...

}

Call these methods in any order in the main method of the class Street and simulate a bus driving:

public class Street {
    public static void main(String[] args) {
        int id = 1;
        int maxAmountOfPassengers = 30;
        String brand = "BMW";
        String color = "red";

       Bus bmwBus = new Bus(id, maxAmountOfPassengers, brand, color);

       System.out.println("New bus: ");
       System.out.println("Id = " + bmwBus.getId());
       System.out.println("Max amount of passengers = " + bmwBus.getMaxAmountOfPassengers());
       System.out.println("Producer = " + bmwBus.getBrand());
       System.out.println("Color = " + bmwBus.getColor());

       bmwBus.go(5);
       bmwBus.stop();
       bmwBus.openDoors();
       bmwBus.closeDoors();
       bmwBus.go(3);
    }
}

Composition in Java

Composition in Java is the design technique to implement has-a relationship in classes. This means that an object A has another object B.

Let's implement a composition for our example with Bus and add a Passenger to the bus so that we have has-a relationship between Bus and Passenger.

Create a class Passenger with field name, id, age, constructor with arguments and getters for these fields:

public class Passenger {
    private int id;
    private int age;
    private String name = "";

    public Passenger(int id, int age, String name) {
        this.id = id;
        this.age = age;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

Edit class Bus and add the field with the type Passenger and a set method that sets the passenger:

public class Bus{

...

    private Passenger passenger;

    public void setPassenger(Passenger passenger){
        this.passenger = passenger;
    }

...

}

If we don't execute this method, the passenger will not be set and it stays null. Null means that there is no object and a reference points to nowhere. Once we pass in the set method any Passenger object, the reference passenger points to it.

Bus with many passengers

Since we have a bus that can transfer a fixed amount of passengers at the same time, we can adjust our Bus class to have an array of passengers. That means that the Bus class will have an array of type Passenger with the length equals to maxAmountOfPassengers. This array is created after the object Bus is created, when we execute constructor and pass there arguments. Replace the single passenger field with the array of type Passenger and put the array creation into the constructor. Remove also method setPassenger():

public class Bus{

...

    private Passenger[] passengers;

    public Bus(int id, int maxAmountOfPassengers, String brand, String color){
        this.id = id;
        this.maxAmountOfPassengers = maxAmountOfPassengers;
        passengers = new Passenger[maxAmountOfPassengers];
        this.brand = brand;
        this.color = color;
    }

...

}

Add passengers to the bus

Now let's implement adding passengers in the bus. As we have an array of passengers and it has a fixed length, we need to remember the last index of the passenger in the array. For that create one more field called amountOfPassengers. It will be increased every time we add a new passenger. Now add a method that fills the bus with passengers and a getter for the field amountOfPassengers:

public class Bus {
...
    private int amountOfPassengers = 0;
...

    public void addPassenger(Passenger passenger){
        passengers[amountOfPassengers] = passenger;
        amountOfPassengers++;
    }

    public int getAmountOfPassengers() {
        return amountOfPassengers;
    }

...

}

Method that returns all passengers from the bus

public class Bus {

...

    public Passenger[] getPassengers() {
        return passengers;
    }

...
}

Add passengers to the bus in the main function

public class Street {
    public static void main(String[] args) {
        int id = 1;
        int maxAmountOfPassengers = 30;
        String brand = "BMW";
        String color = "red";

       Bus bmwBus = new Bus(id, maxAmountOfPassengers, brand, color);
       Passenger passenger1 = new Passenger(1, 20, "Peter");
       Passenger passenger2 = new Passenger(2, 40, "Fritz");
       Passenger passenger3 = new Passenger(3, 60, "Klara");

       bmwBus.addPassenger(passenger1);
       bmwBus.addPassenger(passenger2);
       bmwBus.addPassenger(passenger3);

       System.out.println("New bus: ");
       System.out.println("Id = " + bmwBus.getId());
       System.out.println("Max amount of passengers = " + bmwBus.getMaxAmountOfPassengers());
       System.out.println("Producer = " + bmwBus.getBrand());
       System.out.println("Color = " + bmwBus.getColor());

       bmwBus.go(5);
       bmwBus.stop();
       bmwBus.openDoors();
       bmwBus.closeDoors();
       bmwBus.go(3);
    }
}

Print out amount of passengers

System.out.println("Amount of passengers in the bus : " + bmwBus.getAmountOfPassengers());

Bus overloading

If we add to the bus amount of passengers more then the value of parameter maxAmountOfPassengers, we will receive an error from Java during the run time. To avoid such situation we should add a condition in the addPassenger() method: if the amount of passengers in the bus is less than maxAmountOfPassengers then we allow other passengers get into the bus:

public class Bus{

...

    public void addPassenger(Passenger passenger){
        if(amountOfPassengers < maxAmountOfPassengers){
            passengers[amountOfPassengers] = passenger;
            System.out.println("Passenger " + passenger.getName() + " entered the bus");
            amountOfPassengers++;
        }else {
            System.out.println("Passenger " + passenger.getName() + " should take another bus");
        }
    }

...

}

Here we print out a message when we add a passenger. passenger.getName() is an example how we can use object properties. In this case we get passenger's name and print it to the console.

Exercise 1

In the function addPassengers() we can pass the same passenger several times and at the end one passenger takes several sits or occupy the whole bus. Fix this issue by adding additional checking logic in the method.

Exercise 2

Create another class Pet that belongs to the class Passenger. Add properties to Pet : name, type(dog, cat, rabbit, piggy), age. Add getters for these fields. Edit the method addPassenger() so that the passenger is allowed to enter a bus if his pet is something not huge : dog, cat, mouse. Print out a message "The Passenger passengerName is not allowed to enter the bus" if a passenger that has a big pet.

Pair Programming Exercises

Exercise 1

Implement a prototype of an application that controls population of different animals in the Zoo. Define state and behavior for the objects.

  1. Add several properties to the Zoo type : amount of animal types, amount of kangaroos or giraffes, arrays of some animals (kangaroos, giraffes or whatever you like), city, square occupied, amount of aviaries and so on.
  2. Add behavior to the Zoo : getters and setters for some properties, adding animals of the specific type, calculating amount of animals, reporting(printing to the console) how many animals were added or removed.
  3. Add also properties and methods to animal objects. They can be of different types: Kangaroo, Giraffe, Fox and so on.

Exercise 2

Implement a prototype of an application that monitors Bank activity and its Client. Define state and behavior for the objects.

  1. Add several properties to the Bank type: name, amount of clients, array with clients, amount of deposits.
  2. Implement a composition : Bank has-a Client. You can also add a composition Bank has-a Manager.
  3. Implement several methods for the Bank type. They can be for example: getters and setters for the properties, adding clients, setting main manager, calculate sum of all deposits if the clients have them.
  4. Add also properties and methods to the types Manager and Client.

Exercise 3

Implement a prototype of an application that controls School and Groups with Children studying there. Define state and behavior for the objects.

  1. Add several properties to the School type: name, amount of groups, array with groups.
  2. Add several properties to the Group type: max amount of children, name of the teacher, array of children, age needed to be in this group.
  3. Implement methods for Group:
    1. Overloaded constructor that accepts parameters: maxAmountOfChildren, teacherName, averageAge;
    2. Method that adds a child to the group, if the age corresponds to the average age of the group, and there is a place in the group. If a child is/is not added to the group, print a message about that;
    3. Method that returns all children.
  4. Implement a composition : Group has-many children (array of children).
  5. Add several properties to the Child type: name, age.
  6. Implement methods for the Pupil type: getters for the fields.
  7. Implement a composition : School has-many groups (array of groups).
  8. Implement several methods for the School type.
    1. Getters and setters for the fields;
    2. Adding groups;
    3. Getting amount of groups.
    4. Add a child to school. If the child can not be added to any of the groups, print a message.