Kihagyás

Practice 04

We will look at the basics of UML modelling; we will discuss the UML class diagram in more detail in the practical course. This note only covers the topic in outline form, there is much more detailed course material available at the lectures (and in the lecture notes). In addition to the lecture, you may also want to check out UML Distilled: A Brief Guide to the Standard Object Modeling Language by Martin Fowler, and other related material can be found online.

UML Basics

  • UML - Unified Modeling Language
  • Official site where the full specification and documentation of the language is available
  • Wikipedia description of UML in English
  • Advantages
    • Open standard
    • Supports all cycles of software development
    • Builds on vast empirical knowledge
    • Many tools and many companies support and develop the standard
  • Can be used to
    • visualize
    • specify
    • create
    • document software system elements
  • Model: description of a complete system
  • Diagram: a visual description of part of a system from a particular perspective
  • Can be divided into two main parts essentially
    • Static models, for example:
      • Class diagram, description
      • Object diagram
      • Package diagram
      • Component diagram
    • Dynamic models, for example:
      • State diagram
      • Sequence diagram
      • Use case diagram

The object

An object is a representation of an entity (it can be real (e.g.: lamp, car, cat) or abstract (e.g.: mathematical function, zombie, unicorn) that has state, behaviour, and identity.

State

The state of an object is one of its possible states of existence (determined by the current value of its properties). This may vary over time, e.g. a light that is off may be on at a later time.

Practical example

In the pet registry program we store cats, including Spotty. Spotty is an object. Spotty weighs just 8 kg, is just 2 years old, is friendly and has no fleas. These properties determine the current status of Spotty, which can change. In a year's time, Spotty will be 3 years old, he may have more or fewer fleas, and of course his weight may change, even between two very close points in time.

In a program, the state will be determined by the attributes (data members) of the object. In a program, this will technically mean that the data members have a specific value of a particular type (e.g.: the on property of a lamp object is an attribute of type boolean, which can take values True or False, and this indicates whether the lamp is on; a property of a car object can be the speed at which it is travelling, which can be represented by a property of type int, and a given car will travel with a specific value within the range of int, which of course can and typically does change).

Practical Example

Spotty's attributes can be for example: weight (floating point), friendly (logic), number of fleas (integer).

Behaviour

The behaviour of an object is a description of how the object reacts to requests from other objects. The object does something when asked, which may or may not change its state (in many cases it does).

Practical example

Spotty's possible behaviour could be for example purring, scratching, eating, wandering.

In a program we describe the behaviour by operations. This will practically mean member functions (operations, methods) in our program, for example: a lamp object's operations on(), off(), which change its state; a car object's methods speedup(), slowdown(), which change the instantaneous speed.

Identity

All objects are unique, even if they are in the same state and can perform the same behaviour. This technically means that even if you have two car objects of the same type, colour, and manufacturer, and they are currently moving at exactly the same speed, they are still different.

The class

A class is effectively a "formal" description of a group of objects. More concretely, it is a description of a group of objects that share attributes, operations, relations with other objects and semantic behaviour. A class refers to the type of an object. Classes may be grouped according to some logic, even hierarchically, into packages.

Note

A class is the type of a given object, i.e. the object is an instance of a class. Take Spotty as an example. Spotty is an object of type Cat, and Spotty is exactly an instance of one class. The properties and behaviour of Spotty are described by the class Cat. Spotty is an instance of the class Cat. You can, of course, create other objects of type Cat. Or, for example, take a concrete car object, made by Toyota, of the Corolla type, black in colour and currently travelling at 46 km/h. It can be an instance of exactly one class, in this case an instance of the Car class. The Car class describes how to represent the manufacturer (text), type (text), color (text), and exactly how fast it is going (integer).

In our example, we'll create the Cat class, which is a description of an object of type Cat in the program. It specifies everything that cats can do in the program.

In the Cat class we will write the properties and behaviour of the cats. We will keep the following properties (attributes) about cats:

  • Name (text)
  • Weight (floating point)
  • Friendliness (logical)
  • Number of fleas on it (integer)
  • Number of times petted (integer)

All cats can meow, roam (a certain distance), cats can be treated for fleas, and we know if they are stray cats. Cats can be petted (with varying degrees of success). So this description is quite simple, not too obvious, and needs to be looked at carefully to understand the requirements. This is where the class diagrams come in.

Class diagram

A UML class diagram is a summary diagram of classes and their relationships. It gives you a very high-level overview of your system, showing what classes you have and how they relate to each other (without it, you would have to dig through every file to get a high-level view of your system, which may not sound so bad, but it can take up an enormous amount of time for as few as 10-20 classes (which can be thousands of lines of code); of course, in real systems, there can be many more classes). Classes can also be bundled into packages here. This diagram type is one of the most basic and widely used diagram types to model our classes and the relationships between them. Class diagrams are discussed in more detail in the lecture, or you can read more on this and this link. The elements of a UML class diagram are classes and the relationships between classes. And there are 3 types of relationships between classes: association, aggregation, and composition.

However, before going into these in detail, let's start from where design usually starts: create classes. But how?

Class

The symbol for class is rectangle. The rectangle has 3 parts: the upper part is the name of the class, the middle part contains the attributes of the class, usually with a type designation, and the lower part contains the operations of the class.

Empty class

Take the cat example from earlier. The Cat class will give us everything that our future cats will know in our program. To do this, we draw a rectangle, and then we specify the class name, the attributes, and then the operations. We usually specify the attributes by type notation, a colon after the attribute name, followed by the type, for example numberOfFleas : int.

Let's take the attributes of the Cat class.

An incomplete Cat class

In addition to the properties, of course, we also need to include the "valid" operations for cats. These will appear in the source code as functions, so in the UML class diagram we need to mark each operation as something like a function, which the future instances of the class will know: we need to specify the return type of the operation (if we know it), its name, and the type (and even name) of the parameters. Different tools allow these in different ways, perhaps the most common form is operationName(parameterType1, parameterType2) : returnType, operationName(name1 : parameterType1, name2 : parameterType2) : returnType.

Add the missing parts to the incomplete diagram.

Cat class

Let's assume that this is all we would have in the first round. That's cool, we've drawn it, but how do we make it into code?

Implementation in Java

As discussed earlier, the formal description of the concrete object(s) is the class. We need to create classes in the code in order to then create objects in memory. Classes are used to formalize objects that have the same properties and operations, such as cats or humans.

There can be many different cats in the program, but the cats share common properties, for example, they all have a name, and a friendly property.

Create a Cat class representing cats. The keyword to create the class is class in Java. Normally, classes are created in separate files with the same name as the class you want to create, plus the .java extension. So let's create the file Cat.java:

public class Cat {

}

The attributes and properties in the UML are created inside the class. The type should basically be the type indicated in the diagram if possible, but if you happen to be working in a language where a particular data type is not available, you can of course choose a similar type (for example, if you see a class diagram with type char*, you can just use String in Java, for example). In the case of the Cat class, this is what our class looks like with the addition of attributes, which we mostly refer to here as data members.

public class Cat {
    String name;
    double weight;
    boolean friendly;
    int numberOfFleas;
    int numberOfPets;
}

We have our class, now we just need to represent the behaviour in the source code. This is done by the methods we write into the class, which define everything that the objects created from the class will be able to "do".

All cats can meow, so we need a meows method (which you can see in the UML class diagram), which has no parameters and returns no values.

public class Cat {
    String name;
    double weight;
    boolean friendly;
    int numberOfFleas;
    int numberOfPets;

    void meows() {
        String meow = "Me";
        for (int i = 0; i < weight; i++) {
            if (i % 2 == 0) {
                meow += "o";
            } else {
                meow += "o";
            }
        }
        meow += "w";
        System.out.println(name + " meows: " + meow);
    }
}

This method thus depends on the weight of the given cat to meow longer or shorter. Of course, you can implement this in a simpler way, by printing a meow of a given length.

Let's prepare the other methods as well.

public class Cat {
    String name;
    double weight;
    boolean friendly;
    int numberOfFleas;
    int numberOfPets;

    void meows() {
        String meow = "Me";
        for (int i = 0; i < weight; i++) {
            if (i % 2 == 0) {
                meow += "o";
            } else {
                meow += "o";
            }
        }
        meow += "w";
        System.out.println(name + " meows: " + meow);
    }

    void roams(double distance) {
        weight--;
        numberOfFleas += (distance / 20) + 1;
    }

    void treatFleas() {
        this.numberOfFleas = 0;
    }

}

Now we have some operations (methods) describing behavior, we have attributes (data members), and we have the Cat class (for now).

From the resulting class we create objects; we call this instantiation. Each object is the instance of exactly one class, but you can create countless object instances of a class. Let's create Spotty using the new operator in the main function.

    public static void main(String[] args) {
        Cat spotty = new Cat("Spotty", true);
        spotty.meows();
        System.out.println("Weight of Spotty: " + spotty.weight);
    }

Compile and then run the program. The output is null meows: Mew, and the weight is not exactly right. It didn't exactly become Spotty, and even its meow isn't right. The reason is that the data members in the class are not initialized properly, they take the default value for the types (previous preactice). To do the initialization properly, we need to create a constructor.

Constructor

The constructor is used to create instances (objects) of classes, this is used to set the data members (variables) declared in the class to the specific values specific to that object (i.e. the current state of a specific instance of Cat). There is a constructor by default, assuming we didn't make one (we didn't write one just now), but we can also make parametric constructors to quickly and easily initialize the object. Importantly, if we create any constructor, the compiler does not create a default, parameterless constructor (as it did before).

  • Its name must be the same as the class name, in all cases.
  • It has no return value/type (it can't have one, not even void).
  • Runs when a new object is created.

Basically, we can create parameterless (default) or parameterized constructors. Constructors are responsible for initializing the object correctly.

    Cat() {
        name = "None";
    }

The default constructor only needs to be actually written in the code if it has a role, writing a completely empty default constructor is redundant. In the example above, for example, we set the name to "None". You can also create parametric constructors.

    Cat(String name) {
        name = name;
    }

However, there is a problem with the extended parameter constructor. Since the variable name in the parameter overlaps the data member created in the class (also name), so the value assignment name = name; makes no sense, since the parameter variable name is equated with itself. Of course, we can solve this too, which is where the this keyword comes in.

Using the this keyword, the object can refer to itself with it, solving the problem above. Of course, this is not only useful in the constructor, it is useful in all cases where the name of a parameter in a method is the same as the name of the data member declared in the class, so we can distinguish them (one is the object's own property, the other is the parameter).

    Cat(String name, boolean friendly) {
        this.name = name;
        this.weight = 6;
        this.friendly = friendly;
        this.numberOfFleas = 0;
        this.numberOfPets = 0
    }

    Cat(String name) {
        this.name = name;
        this.weight = 10;
        this.friendly = true;
        this.numberOfFleas = 0;
        this.numberOfPets = 0
    }

As you can see, of course, you can create multiple parameter constructors (of course, the parameter list must be different for each constructor). The IDE will help you create constructor functions if you ask it to.

  • For IntelliJ IDEA: in the workspace, right-click and Generate, or simply press Alt + Insert and then select Constructor.

Also, for convenience, and to avoid possibly having a lot of code duplication between constructors in the code (which would also copy possible errors), the first statement of a constructor can be a call to another constructor using this. The example above is modified:

    public Cat(String name, double weight, boolean friendly) {
        this.name = name;
        this.weight = weight;
        this.friendly = friendly;
        this.numberOfFleas = 0;
        this.numberOfPets = 0;
    }

    public Cat(String name, boolean friendly) {
        this(name, 6, friendly);
    }

    public Cat(String name) {
        this(name, 10, true);
    }

With the class thus created, we can now create Spotty!

    public static void main(String[] args) {
        Cat spotty = new Cat("Spotty", true);
        spotty.meows();
        System.out.println("Weight of Spotty: " + spotty.weight);

        spotty.weight = -100;
    }

Now the cat will be Spotty, and will meow nicely.

However, there is a new problem that hasn't been mentioned before, but is now visible in the code. Spotty's weight is freely variable, we can change the objects outside to any data we want. This is not very correct, since the object is not aware of the change, and in addition we can rewrite it to any impossible value. And this is where visibilities come in.

Visibilities

With the help of visibilities, we can control the access to data members, methods, as these must be restricted according to the object-oriented paradigm, and can only be accessed and used in a controlled way from the outside. It is part of implementation hiding, i.e. either the program or the class can be used from the outside by users without knowing exactly how it works.

That being said, it is logical that if you have a specific instance of the type Cat, modifying its data members from the outside would be rather strange, since the object would not know that its weight has changed, for example.

Two visibility modifiers (but actually three visibilities) are what we are going to explore now:

  • public - the public tag is visible from everywhere
  • private - the private member is visible only to the class
  • nothing is written - "friendly" visibility, public inside the package, private outside the package

We'll get to the 3rd visibility modifier later.

  • protected - the package, the class and their child classes are visible

In UML diagrams, you can also sometimes see plus (+) and minus (-) signs in front of attributes and operations. A + sign indicates that the operation or property can be used outside the class to modify the state of the object, in Java this means that the data member/metadata method is public. And the - sign means that the property/operation is only intended to be used inside the class. For Java, this will be the private visibility.

Let's add this new knowledge to the Cat class.

Cat class

So now we can see basically what visibility we are assigning to the members of our class. Data members have private visibility and methods have public visibility on the class diagram. This is true in general, by the way, most of the time we want to hide our individual data members and only communicate with the object through the methods, during which the state of the object may of course change. However, there may also be cases where certain data members are public, or particular methods are private. Let's adapt our program accordingly.

public class Cat {
    private String name;
    private double weight;
    private boolean friendly;
    private int numberOfFleas;
    private int numberOfPets = 0;

    public Cat(String name, boolean friendly) {
        this.name = name;
        this.weight = 6;
        this.friendly = friendly;
        this.numberOfFleas = 0;
    }

    public Cat(String name) {
        this.name = name;
        this.weight = 10;
        this.baratsagos = true;
        this.numberOfFleas = 0;
    }

    public Cat() {
        this("Stray kitty", false);
    }

    public void meows() {
        String meow = "Me";
        for (int i = 0; i < weight; i++) {
            if (i % 2 == 0) {
                meow += "o";
            } else {
                meow += "o";
            }
        }
        meow += "w";
        System.out.println(name + " meows: " + meow);
    }

    public void roams(double distance) {
        weight--;
        numberOfFleas += (distance / 20) + 1;
    }

    public void treatFleas() {
        this.numberOfFleas = 0;
    }

}

So now the values of the data members can only be changed at the request of the object. Let's see the previous main method.

    public static void main(String[] args) {
        Cat spotty = new Cat("Spotty", true);
        spotty.meows();
        System.out.println("Weight of Spotty: " + spotty.weight);

        spotty.weight = -100;
    }

We can now neither access nor retrieve the weight of Spotty. We will need a few more methods to do this.

Getter/Setter

The getter and setter functions are used to retrieve (getter) and modify (setter) the values of data members after initialization. They are related to the fact that the data of an object can only be retrieved and modified in a controlled way, through these functions. Usually we just use them for simple retrieval and setting, but we can also put all kinds of checks here, for example. Or, for example, you can make a property externally accessible but not modifiable (by only creating a getter).

Another advantage of using getter/setter functions is that you can even modify the values when getting or setting (for example, you can tell the setter not to let you set the weight of a Cat object to negative).

It is traditionally called get + data member name or set + data member name, for example getWeight or even setWeight. For Boolean values, the getter function is usually named is + data member name, for example isFriendly.

    public String getName() {
        return name;
    }

    public double getWeight() {
        return weight;
    }

    public void setWeight(double weight) {
        this.weight = Math.max(0, weight);
    }

    public boolean isFriendly() {
        return friendly;
    }

The IDE is also kind enough to help you generate them:

  • For IntelliJ IDEA: right-click on the workspace and Generate, or simply press Alt + Insert and then select Getter and Setter.
  • For Eclipse: in the workspace, right-click and select Source > Generate Getters and Setters... to find the corresponding wizard.

When you are done, you can use the getter function to access the data members and the setter function to set the data members within the main method.

    public static void main(String[] args) {
        Cat spotty = new Cat("Spotty", true);
        spotty.meows();
        System.out.println("Weight of Spotty: " + spotty.getWeight());

        spotty.setWeight(-100);
    }

This way, it will no longer be possible to set incorrect values. However, if you want to look at Spotty's data, you would have to print out the data members one by one, but this can be a pain if you want to use this printout in multiple places.

toString

Of course, there is a simpler solution to this, just print our Cat object to the default output using the System.out.println(spotty); statement.

Output

Cat@2ef9b8bc

This is not very informative, but fortunately we can work around this. If we want to provide human-language descriptors of the object with minimal work every time we pass the finished object to the System.out.println method, for example, we need to override the toString() method. This is a public visible method returning String.

    public String toString() {
        return "The Cat is named " + this.name +
                ", he is" + this.weight + " kg, he has " +
                this.numberOfFleas + " fleas, he was pet" +
                this.numberOfPets + " times, and " +
                (friendly ? "he is friendly." : "he is unfriendly.");
    }

Now we have a nicer output. To make the output a bit nicer, we write out a given text depending on the friendly property, using the ternary operator if-else. In this case, we must also use parentheses, otherwise the compiler would want to append the value of the this.friendly property to the existing text and call the ternary operator with the resulting String, which would not make sense, since the ternary operator requires a logical value to the left of the question mark.

Output

The Cat is named Spotty, he is 6.0 kg, he has 0 fleas, he was pet 0 times, and he is friendly.

Both IntelliJ Idea and Eclipse are happy to help with toString so that we don't have to do this robotic work by hand.

  • For IntelliJ Idea: Code > Generate menu item, and then toString() in the popup menu. You can eliminate menu clicks by calling it via the shortcut: Alt + Insert, then toString().
  • For Eclipse: Right-click in the workspace and Source > Generate toString()... to find the associated wizard.

This will generate something like this for you:

    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", weight=" + weight +
                ", friendly=" + friendly +
                ", numberOfFleas=" + numberOfFleas +
                ", numberOfPets=" + numberOfPets +
                '}';
    }

Above the header of the generated toString method, there will also be an @Override string, which is an annotation we will get to that in the next lesson. For now let's just leave it there and it shall not bother us with our further work. The resulting toString is not the prettiest, but we can easily read all the data from it, so it will be useful for any purpose.

The static modifier

Next, we'll learn about the static modifier, which you may recognize from the main function header. A member with a static modifier is not associated with an object (such as a traditional data member associated with the state of an object), but with the class, the type, itself. The static members always belong to the type, and these members are shared by all instances.

Whether it is a data member or a method, if something is static, it is indicated on the UML diagram by an underscore.

Static data member

They are stored in the same place in memory, they will have the same value for all instances, they are shared by practically all instances of the class, they can be referenced without instantiation. These data members belong to the class, not to the objects. They can be of practical importance, for example, if you want to store in a variable a value - for example, the number of objects created - that must be the same for all instances of the object, and even make sense without instantiation, since you can use static data members without instantiation. They can be referenced as ClassName.datamember, as long as the data member is visible from that location.

For example, if you want to store the unfriendly cats you have created in your program, you can use a static data member for this purpose. Accordingly, we complete the UML class diagram.

Cat class

In the code this does not result in many changes, we just need to add a new data member.

public class Cat {
    private String name;
    private double weight;
    private boolean friendly;
    private int numberOfFleas;
    private int numberOfPets;

    private static int UNFRIENDLY_NUMBER = 0;    

Static variables can be given an initial value in several ways, but the easiest is where they are declared. If we don't specify an initial value, it takes the default value for the type (which is exactly 0 for the integer, but we explicitly indicate that we wanted to set it to zero).

Data members belonging to instances (i.e., non-static) can be set in this way if we want. If no other initial value is specified in the constructor, the value specified in the declaration will be taken by the member variable (so if the initial value of a data member is 2, but the constructor sets it to 7, then the value of the data member will be 7 after the object is created).

Static method

These methods are technically not associated with objects, but with the class. They can be called without instantiating an object. We have already seen many examples of these, such as the public static void main(), Integer.parseInt() methods.

In a static method, only static data members and the resulting parameters can be used, since they are not linked to any object, and we can call a static method without their existence, so of course we cannot use this either. These methods cannot be overridden, but we will learn about that later.

Let's create a static function for the Cat class that prints the number of unfriendly cats.

Cat class

Let's implement the function.

    public static void printUnfriendly() {
        System.out.println("In total there are " + UNFRIENDLY_NUMBER + " Cats.");
    }

You can call the printprintUnfriendly function of the Cat class from anywhere, even without an existing Cat instance, by calling Cat.printprintUnfriendly().

UML tools

Task

Person

Create the class Person with the following UML class diagram:

Person class

The function incrementScratches increments the number of scratches. In the static variable GMAIL_ACCOUNT_COUNT, keep count of the number of people using an email account ending in @gmail.com. The constructors initialize the data members as appropriate, of course initially there are no scratches on the resulting People objects.

Rectangle

Write a class representing a rectangle, storing its sides' lengths.

Make a constructor for it that initializes the sides. Write another constructor for the class, which expects only one parameter and can be used to create a square.

Create methods to calculate the perimeter and area.

Write another class that can be executed (it has a main function) and, according to the command line parameters, creates rectangle objects from the Rectangle class and calculates the average of the area and perimeter of the rectangles.

Example of the main function: number triples, the first number indicates whether to initialize the rectangle from 1 or 2 parameters, i.e. to create a square or rectangle, and the following 1 or 2 numbers contain the side lengths of the rectangle: java RectangleMain 1 5 2 10 22 2 9 8 1 100. Meaning: first a square with side length 5, then a rectangle with side lengths 10 and 22, then another rectangle with side lengths 9 and 8, then a square with side length 100.

Objects, More on Classes

Classes

Practical UML

UML 2 Class Diagrams

Coding style

Oracle Code Conventions for the Java Programming Language

Java Programming Style Guide

Java Programming Style Guidelines

Google Java Style Guide

Twitter Java Style Guide