02. Classes in OOP (Part 2)

Multi-valued attributes, class attributes, derived attributes, and class methods in object systems.

Educational outcomes

Student:

  • explains how to implement constructs such as class extents, extent persistence, complex attribute, optional attribute, mult-valued attribute, class attribute, derived attribute in natural language (understanding);
  • identifies and corrects errors in the source code, which is an implementation of the above structure;
  • lists constructs implemented in the source code (understanding);
  • implements the above structures using the selected programming language (application);
  • indicates functional differences between two selected programs based on code snippets source (analysis);
  • creates a program that implements specific business requirements using the constructs learned from the lectures (synthesis);

Justification

Modern programming languages ​​are mostly based on the object paradigm. Therefore, its practical understanding is a necessary basis for working in this profession. Moreover, programmers often make systems based on analytical materials. And these largely use object-oriented graphical notations (such as UML). During this and the next lessons, you will deepen your theoretical bond, but first of all you will learn how to implement different concepts from object-orientness. This will allow you to design and implement modern business applications.

Object methods

Methods are used to describe the behavior of an object. Each of them can return a value (but does not have to) and retrieve parameters (also does not have to). Contrary to the attributes where the variation was large, here we have a fairly simple division:

Definition

Object method has access and only works on a specific object (for which it was called).

More precisely, they have access to its attributes and other methods of this object (that is defined in the class to which this object belongs). We can not directly refer to another object (including its contents) - even from the same class. We can say that a single object is not aware of the existence of other objects of a given class (unless it is accessed through the extent). Simplified examples:

  • string getName()we will return the name of the specific employee (the one for whom we have called this method).
  • void setSalary(double newSalary)we change the salary of a particular employee.
  • bool isFemale()we ask if a particular employee is a woman.

And how about programming languages? Object methods in Java, C# or C++ have the same semantics as in the UML. The concrete method operates on the object to which it was invoked. It has access to all its elements: attributes and other methods. In the body of the method, we can use the special keyword thiswhich is a reference to the object to which the method was invoked (more on this in the books on Java/C++/C#). The sample code for a very simple method is shown in the following listing:

Java
public float getPrice() {
	return price;
}
Knowledge Check
Select all that apply

Which statements are true?

Class methods

Definition

Class method has access to the whole extent, and therefore to all objects belonging to that class

The mothods may operate on the extent, but they do not have to: for example, a class method that returns a value without looking at extent (therefore, class methods operate on the extent is not completely accurate). As in the case of attributes, class methods are invoked in favor of a class, not a particular object. This allows them to be used even when there are no objects of the same class. Examples:

  • static Employee findBestWorker()
  • static void setSalary(double newSalary)An analogous example was given for object methods, but this one has a different semantics: we increased the salary for one employee, here for all of them (whole extent).

Class methods in the sense of UML unfortunately do not appear in popular programming languages. Someone could protest: how about the static keyword? After all, it solved the problem of class attributes - does not this help us here? Partially yes, but not quite - I suggest to remind ourselves what we just wrote about class methods (and extents).

Class method is characterized by the following properties:

  • It can be called for class. This allows us to use it even when there are no objects of the class. And this can be achieved using the static keyword, occurring in Java, and in C++, as well as in C#.
  • The second, very important feature is access to class extent. And here comes the problem. We said that in the languages ​​we are interested in, there is no class extent, which obviously means that the method with the word static can not automatically access it.

As a result, we need to implement the class method ourselves. Our approach depends on how we manage the extent:

  • An extent within the same class. Thus class methods in the same class with the keyword static. Because in the same class there is also a container also declared with the keyword staticour class method has trouble-free access to it, and thus extent.
  •                 
  • Extent as an additional class. Class methods are placed in the add-on class - this time without the keyword static.

Below is an example of two class methods implemented within the same business class:

Java
package mt.mas;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class Employee {
	private static List<Employee> extent = new ArrayList<>();

	private double salary;
	private String name;

	// ...

	public Employee(String name double salary) {
		this.salary = salary;
		this.name = name;

		// Add to the extent
		extent.add(this);
	}

	public double getSalary() {
		return salary;
	}

	public void setSalary(double newSalary) {
		this.salary = newSalary;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return String.format("Emp '%s' sal: %s" getName(), getSalary());
	}

	/**
		* Class method returning all employees having a salary higher than the given minimum.
		* @param minSalary
		* @return
		*/
	public static List<Employee> findEmployeesWithMinSalary(float minSalary) {
		// Using Java 8 Functional Streams to find/process data
		return extent.stream().filter(emp -> emp.getSalary() > minSalary).collect(Collectors.toCollection(ArrayList::new));
	}

	/**
		* Class method setting a new salary for all the employees.
		* @param newSalary
		*/
	public static void changeSalary(double newSalary) {
		// For each employee in the extent
		for (Employee p : extent) {
			p.setSalary(newSalary);
		}
	}
}
Knowledge Check
Select all that apply

Which statements are true?

Overloading methods

Method overloading is not purely object-oriented concept because it does not use any specific concepts of objectivity. After all, it is used in all modern object-oriented languages, and is therefore worth discussing.

Definition

Method overloading is the creation of another method with the same name as the overloaded method, but different number and/or type of parameters.

Why the second "same" method? Will that help us? Let's look at the following example:

Java
public float getPrice() {
	return price;
}
public float getPrice(float vatTax) {
	return price * (1.0f + vatTax / 100.0f);
}

In a class, eg. Product there is a method that returns its net price. What if we want to find out about the gross price? We can create a method, eg getGrossPrice(). Another way is to overload the basic method with a parameter defining eg VAT rate (in percentage). Thanks to that, by referring to it in the code of the program, we do not have to recall its special name - we just call the method with the parameter.

Persistence of an extent

Definition

Persist class extent occurs when its objects "survive" the system shutdown - once it is restarted we will have the same objects.

The feature is extremely important. In one form or another it occurs in almost all computer systems: starting with games (we remember the state of the game, which is just defined by the state of objects in the game), through office applications (eg. editor in which I write this lecture), and ending in databases.

In programming languages ​​such as Java, MS C# or C++, this feature does not occur directly. We mean that there is no special keyword, the use of which with the definition of the class, for example, will ensure the persistence of its extent. That is why you need to implement it somehow. One way is to manually store the extent on the disk and then load it. Other, more unusual may be the use of such memory in a computer that does not lose its state after power off (eg. flash).

The most common way to achieve the persistency is to use a disk file. This requires storing data in the file and reloading it in memory, after a reloading a system. This approach has a certain problem. The shortest can be characterized as follows: after loading we do not have the same objects, but similar ones. Is this a problem? Yes it can be. First of all, it is not a perfect persistence, because we do not have the same objects in it. Secondly, more importantly, from a practical point of view, we can have problems related to the links between objects. If you use native programming language references, they represent specific places in memory where the connected objects reside. If we load them from the file and re-create them, 99% will be placed somewhere else, but the read references show the "old" places. You have to deal with it somehow - we'll show you how to do it later.

Fortunately, modern programming languages ​​like Java and C# provide a mechanism called serialization. This technique significantly automates and facilitates data sustainability. Let's take a look at two approaches: manual and (semi) automatic.

Knowledge Check
Select all that apply

Which statements are true?

Persistence of extent - manual solution

Compared to the use of serialization, manual implementation has almost the same advantages - aside from one. The only drawback is quite laborious - especially if you want to solve the problem of linking objects optimally. Returning to the advantages of this approach:

  • Speed. We directly select the items to be saved and the mode (such as binary, text, structured text) of the file. This allows us to use the maximum speed offered by a particular programming language.
  • Total control over the final effect. It is us - the programmers are responsible for the whole implementation, which allows us to completely control what we write (because we do not have to want to store everything) and how we write.
  • Greater resistance to changes in classsource code. We'll get back to the topic later.
  • Small file. Because we save only the selected items, the file can be as small as possible. In one of the projects I was working on, the files I saved were quite large. We had to remember the next position coordinates on the screen of certain elements (as the number of int type - they occupy 4 bytes each). Because their values ​​were in the range 0 - 1000, we decided to use the short type (2 bytes). After analyzing the data we have applied even more clever solution. We only remember the differences between the coordinates. Because they are relatively small, they are in the range 0-255. This way, we can save them using one byte. As you can see, sometimes the way data is written has little in common with their physical storage in the class.

Of course, there is a lot of manual approaches to data persistence implementation. One possible solution is presented below:

Java
public class Movie {

	//	 [...]

	private String title;
	private float price;
	private LocalDate additionDate;  // requires Java 8+

	private void write(DataOutputStream stream) throws IOException {
		stream.writeUTF(title);
		stream.writeFloat(price);
		stream.writeLong(additionDate.toEpochDay());    // count of days where day 0 is 1970-01-01 (ISO)
	}

	private void read(DataInputStream stream) throws IOException {
		title = stream.readUTF();
		price = stream.readFloat();
		long epochDay = stream.readLong();
		additionDate = LocalDate.ofEpochDay(epochDay);
	}
}

Remarks to the above listing:

  • (5). Standard attributes for storing business data.
  • (9). A method that saves the business data to a file. More specifically, to the stream. Thanks to this separated functionality, you can also save other extras to the same stream, so we get one file containing all the infromation from our system. In the method's body, we save one attribute after another.
  • (15). Data reading method. Notice the order - it is identical to the order of writing.
  • (12), (19). Standard simple types in Java can not handle the date. Therefore, we write the number of days (12), and then construct a date object (19).

In addition to methods of writing and reading the state of a single object, we need methods to write and read the state of the whole extent. Naturally, we place them in a class that manages extents.

Java
public class Movie {

	//	 [...]

	private static List<Movie> extent = new ArrayList<>();

	public static void writeExtent(DataOutputStream stream) throws IOException {
		// Number of objects
		stream.writeInt(extent.size());

		for (Movie movie : extent) {
			movie.write(stream);
		}
	}

	public static void readExtent(DataInputStream stream) throws IOException {
		Movie movie = null;

		// Get the number of written objects
		int objectCount = stream.readInt();

		// remove the current extent
		extent.clear();

		for (int i = 0; i < objectCount; i++) {
			movie = new Movie();
			movie.read(stream);
		}
	}
}

Comment to the code above:

  • (3). Because of clarity, the constructor that adds the newly created object to the container is not shown.
  • (7). A method that writes extent to a given stream.
  • (9). At the beginning, we save the number of objects in the extent - it will be needed for reading.
  • (11). Then we iterate through the entire container containing references to individual objects and call the method that writes the single object (shown previously).
  • (16). The method that reads the contents of the extent from the specified stream.
  • (20). First we read the number of saved objects.
  • (23). We clean up the existing extent (we remove all existing objects of the class).
  • (25). Knowing the number of objects stored, we create new ones (26) and read their contents by calling the method for each of them (27).

Let's look how we can use it:

Java
// a test method

final String extentFile = "d:\\mas-extent.bin"; // TODO update the file location

var movie1 = new Movie("Terminator 1" LocalDate.now(), 29.90f);
var movie2 = new Movie("Terminator 2" LocalDate.now(), 34.90f);

// A test: persistency of the extent (manual impl.)
try {
	// Write the extent to the given stream
	var out2 = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(extentFile)));
	Movie.writeExtent(out2);
	out2.close();

	// Read the extent from the given stream
	var in2 = new DataInputStream(new BufferedInputStream(new FileInputStream(extentFile)));
	Movie.readExtent(in2);
	in2.close();
} catch (IOException e) {
	e.printStackTrace();
}

Movie.showExtent();

Remarks:

  • (3). Specify the location of the file on disk that will hold the extent.
  • (5). Create two instance instances (objects) of the Movie class. Thanks to the special constructor design, objects are automatically added to the extent.
  • (11).Create output stream connected to the file.
  • (12). Calling a class method that writes extent to a given stream.
  • (13). Closing the stream. This is a very important step which ommited can result in various errors, such as file lock or loss of "saved" data.
  • (16). Creating a stream for reading from a file.
  • (17). Calling a class method that reads the contents of a file.
  • (18). Closing the stream.
  • (20). Essential exception handling.

Below we see the contents of the console after launching the above program. It is worth noting that the numbers after the "@" (as we mentioned they are addresses in memory) are different before writing and after reading. This is an illustration that the use of writing to a file results in creating similar objects (because the titles of the movie match), but not the same objects (because their computer identity (references - addresses) is different).

Plain Text
Extent of the class: mt.mas.Movie
Movie: Terminator 1, id: mt.mas.Movie@61a485d2
Movie: Terminator 2, id: mt.mas.Movie@1.8Me
Extent of the class: mt.mas.Movie
Movie: Terminator 1, id: mt.mas.Movie@4439f31e
Movie: Terminator 2, id: mt.mas.Movie@5dfcfece
Knowledge Check
Select all that apply

Which statements are true?

The manual implementation of persistence described above is very simple. It works pretty well when you save individual instances. Unfortunately, it does not allow proper treatment of linked objects. Let's look at the figure:

Links among objects

It presents a fragment of two extents of classes: Movie and Actor. The objects of these classes are linked in two directions, ie. the Movie has a connection to the Actors who played it, and the Actor is connected to all the Movies in which he/she played. What's the problem? Well, let's think about how we would write these extents to the file and then read them?

As in the presented approach, we are trying to save objects from the movie class - one by one. But how do we remember information about actors of a particular movie? We said earlier that writing a reference as a number would not work, since the object would be in a different place of memory when read (so the reference should be different). We can try to save this way:

  • As before, we save individual attributes,
  • When we come across an object, we call the write() method. The same way as we called this method from the saving extent. Will it work? There are two potential problems: looping (from the Movie we call the Actor, Actor from the Movie, etc.), and the same items are written multiple times. This is because the same object can be linked from many other objects. By using such a simple method, information about the actor "Arnold Schwarzenegger" will be recorded many times (because this actor played in many Movies).

As you can see, this is not the best way. You can try another approach:

  • Each object has its own unique identifier.
  • When we need to write information about an object, we will remember its id, not a reference.
  • The read of this file is a two-step process: first load the data with the identifiers, and then replace the identifiers with the native references.

We will not give an example implementation because it would be rather complicated - instead we use an existing mechanism called serialization.

Knowledge Check
Select all that apply

Which statements are true?

Persistence of extent, serialization

Serialization is a mechanism implemented in Java/C# libraries. Allows you to automatically:

  • saving graph of objects to a stream,
  • reading the graph of objects from the stream.

What does it mean to write/read an entire object graph? TLet's get back to the example from the above drawing. During writing information about a movie class object named "Terminator 1", we also store information about all the objects it points to like "Arnold Schwarzenegger" and "Michael Biehn" (both from the Actor class). Also, information about all the objects pointed to by these objects is recorded, in this example all the movies and all actors. In other words, the serialization mechanism makes sure that all reachable objects (regardless of how many intermediaries have to "pass") were in a valid state. And it does so smartly that it solves the problems we previously mentioned: looping and rewriting the same objects.

Commentary

If you want the saving to take place with optimal utilization of resources, then we need all the serialized elements to be sent to a single stream - even those from different classes. Otherwise "computer" will not be able to recognize these repetitions. For this reason, serialization to multiple files (for example, one file per extents) is a fatal error - and so every file in the file stores the entire graph of related objects, including multiple extents (when multiple objects are interconnected).

The only requirement that must be met to use serialization is the "special" implementation of the Serializable interface by the class (and all its attributes). This implementation is "special" because in the simplest case we declare its implementation by the class, but we do not need to implement its methods manually. This will be taken care by the Java "compiler". This interface must be implemented not only by the object from which the storing starts (object graph), but by all objects that are being written. Otherwise, we'll see an exception. The good news is that most classes provided by Java or C# implement this interface.

From the programmer's point of view we can list the following features of this approach to the persistency of an extent:

  • Ease of use. The simplest case requires only adding two words to the class definition.
  • Slower writng/reading.
  • Larger file than with manual write. These last two features are due to the fact that together with the business data, various technical information are stored. Not only are they occupying the disk space, they also slows down the whole process. Another reason for the slowdown is the need to get information about building an object while running a program.
  • Pretty high sensitivity to changes in class code. That is to say, most of the changes we make to the source code using serialization will result in incompatibility of the saved data. In other words: we serialize class objects, make changes to the source code, and when we try to read data we get an exception. This situation can occur even if we do not change the attributes of the class (because that would be quite obvious), but even if we add some method.

Fortunately, we have some control over how serialization works:

  • Adding and overriding (own read/write implementation) of the following methods:
    • private void writeObject(ObjectOutputStream stream) throws IOException
    • private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
  • Label the selected attributes with the transient keyword. This will not automatically save them.

Below there are two listings: a business class code with a declared implementation of the Serializable interface and how to use it. As we mentioned, it is worth noting that we do not provide bodies for the methods declared in this interface. So, as we promised, the whole thing is to add two words: implements Serializable.

Java
public class Movie implements Serializable {

	//	 [...]

	private String title;
	private float price;
	private LocalDate additionDate;  // requires Java 8+

	private static List<Movie> extent = new ArrayList<>();

	public static void writeExtent(ObjectOutputStream stream) throws IOException {
		stream.writeObject(extent);
	}

	public static void readExtent(ObjectInputStream stream) throws IOException ClassNotFoundException {
		extent = (ArrayList<Movie>) stream.readObject();
	}
}

And the demonstration of serialization's utilization:

Java
final String extentFile = "d:\\mas-extent.ser"; // TODO update the file location

var movie1 = new Movie("Terminator 1" LocalDate.now(), 29.90f);
var movie2 = new Movie("Terminator 2" LocalDate.now(), 34.90f);

// A test: persistency of the extent (impl. using serialization)
try {
	// Write the extent to the given stream
	var out = new ObjectOutputStream(new FileOutputStream(extentFile));
	Movie.writeExtent(out);
	out.close();

	// Read the extent from the given stream
	var in = new ObjectInputStream(new FileInputStream(extentFile));
	Movie.readExtent(in);
	in.close();
} catch (IOException | ClassNotFoundException e) {
	e.printStackTrace();
}

Movie.showExtent();
Knowledge Check
Select all that apply

Which statements are true?

Other ways of achieving extent persistency

The above-described ways of implementing the persistency do not use any additional components - whether in the form of libraries or external applications. These are not the only options a programmer has. You can use at least two other approaches.

The first one is to use a database. Its greatest disadvantages include the need to map the Java constructs (classes) to structures from the database. Since currently most popular database solutions are based on relational technologies, the developer is forced to abandon most object-oriented constructs.

The second solution assumes utilization of libraries that make it easier to work with data (not just persistence). Often, under the intermediary layer (which is just a library), there is a database management system.

We will return to these ideas during next lessons.