Lec 02 - Class Instance/Methods, Inheritance

Slides:

Information Hiding

The information hiding principle is about hiding the internal from outsiders.

A Problem

In the last lecture, we have introduced the idea of abstraction barrier. Above the abstraction barrier is the client, and as usual, the client shouldn't modify any content below the abstraction barrier since it belongs to the implementer unless the implementer allows to do so.

For example, we have a Circle class, an object c initialized from this class, and the client wants to set the r (one of the fields) to a certain number, let's say 10 (c.r = 10). But now, the implementer says the r field in the class will be changed to d (diameter). Then, bad things will happen since calling c.r = 10 will generate a compilation error! To fix it, the client have change every c.r to c.d ! This is tedious!

This problem is caused by the client's breaking the abstract barrier to access the field, which should belong to the implementer.

public and private

To solve this problem, many Object-oriented languages allow programmers to explicitly specify if a field or a method can be accessed from outside the abstraction barrier. Java, for instance, supports private and public access modifiers. Their difference are summarized in the table below

Accessed from
private
public

Inside the class

Yes

Yes

Outside the class

No

Yes

Such a mechanism to protect the abstraction barrier from being broken is called data hiding or information hiding. This protection is enforced by the compiler at compile time.

Usually, we make fields as private becasue of the information hidden principle, unless we have proper reason to make it public to the client, such as it is a constant, like Math.PI, which will we see later in class fields.

Constructor

But now, a new problem emerges. If we define all our fields in the class to be private, then how we can initialize them? 😂 To solve this problem, it is common for a class to provide methods to initialize these internal fields.

A method that initializes an object is called a constructor.

So, basically, we can see constructor as a way to "set up" our object.

A constructor method is a special method within the class. It cannot be called directly but is invoked automatically when an object is instantiated. In Java, a constructor method has the same name as the class and has no return type. A constructor can take in arguments just like other functions. Let's add a constructor to our Circle class:

The this keyword

As you have noticed, the code above introduces the this keyword. this is a reference variable that refers back to the calling object itself. It can be used to distinguish between two variables of the same name. In the example above, this.x = x means we want to set the field x of this object to the parameter x passed into the constructor.

Default Constructor

If we have a class that has no explicit constructor, then a default constructor will be added automatically at compile time. The default constructor has no parameter and has no code written for the body.

Note

Tell, Don't Ask

Accessors and Mutators

Similar to providing constructors, a class can also provide methods to retrieve or modify the properties of the object. These methods are called the accessor (or getter) or mutator (or setter).

For example, for our Circle class, we have the following accessors and mutators.

Fields
Accessors
Mutators

x

getX

setX

y

getY

setY

r

getR

setR

Advantage

The use of accessors and mutators has the following advantages over using the public keyword to make the field public directly. By having an accessor and a mutator:

1

We are adding a layer of abstraction. For instance, we can still rename a field without affecting the client.

Why? This is because we are calling the methods (a.k.a accessors and mutators) to retrieve or modify the fields in the object, so as long as we don't change the method name, changing the field name merely in class won't affect the client's code.

2

We may be able to peform some checks on the mutator.

This is because sometimes user may set invalid or not-compatible values to the fields. However, by using a mutator, we can do some checks to prevent this kind of bug-prone behavior. For example,

Disadvantage

However, the use of accessors and mutators is not an error-free solution. For example, when the client is calling an accessor, the client should know the type that will be returned, a.k.a, the type of the certain field in the class. However, the client should not know this kind of information since it belongs to the implementer! So, here comes the first problem:

  1. Information Leak

The problem does not stop here. Let's say if the implementer wants to change the type of the field, which will also change the return type of the accessors, and the client doesn't know that! This is very dangerous because likely the original code will cause a compilation error!

  1. May cause compilation error

For example, let's use the following code regarding our Circle class as an example:

As we have seen earlier in the subtype, int is the subtype of double. Assign a double to a int is considered as narrowing type conversion and it is not allowed without explicit casting! So, here we will get a compilation error!


So, the question comes, when should we use accessors and mutators?

The "Tell, Don't Ask" Principle

One guiding principle on whether the implementer should provide and whether the client should call the accessor and mutator is the "Tell, Don't Ask" principle. Here is the "Tell, Don't Ask" principle,

we should tell an object what to do, instead of asking an object for its state and then performing the task on its behalf.

For example, in the example above, what we are trying to do is as follows:

By applying the "Tell Don't Ask" principle, a better approach would be to add a new boolean method in the Circle class,

and let the client tell the Circle object to check if the point is within the circle.

Now, the Circle class can change its internal structure (e.g., the type of the fields) without affecting the client.

Class Field

In our Circle class, we have a constant π\pi and this constant π\pi is universal and does not really belong to any object (The value of π\pi is same for every circle!). In C, we have the solution to define a global constant using the define keyword.

In Java, we can associate these global values and functions with a class instead of with an object. For instance. Java predefines a java.lang.Math class that is populated with constants PI and E (for Euler's number ee), along with a long list of mathematical functions.

To associate a method or a field with a class in Java, we declare them with the static keyword. We can additionally add the keyword final to indicate that the value of the field will not change and public to indicate that the field is accessible from outside the class. In short, the combination of public static final modifiers is used for constant values in Java.

We call these static fields that are associated with a class as class fields and fields that are associated with an object as instance fields. Class fields are useful for storing pre-computed values or configuration parameters associated with a class rather than individual objects. static fields have exactly one instance of it throughout the lifetime of the program.

Access Class Fields

1

Import the certain class

For example, if we want to use the PI constant defined in class java.lang.Math, we should first use the import keyword to import the java.lang.Math class

2

Use class.FIELD_NAME to access the class field

After importing the class, we can use class.FIELD_NAME to acccess the specific class field.

Class Method

Similar to a static field, a static method is associated with a class, not with any instance of the class. Such a method is called a class method.

A class method is always invoked without being attached to an instance, so it cannot access its instance fields or call other of its instance methods. The reference this has no meaning within a class method. Furthermore, just like a class field, a class method should be accessed through the class. For example, in our Circle class, we wish to assign a unique integer identifier to every Circle object ever created:

Other examples of class methods include the methods provided in java.lang.Math: sqrt, min, etc. These methods can be invoked through the Math class: e.g., Math.sqrt(x).

The static keyword

Recap that for static fields (i.e., class fields), we only have exactly one instance of it throughout the lifetime of the program. More generally, a field or method with modifier static belongs to the class rather than the specific instance. In other words, they can be accessed/updated (for fields, assuming proper access modifier) or invoked (for methods, assuming proper access modifier) without even instantiating the class.

More on this

As we have seen this first time in the constructor, let's talk more about this with static methods.

As a follow up, if we have not instantiated a class, no instance of that class has been created. The keyword this is meant to refer to the current instance, and if there is no instance, the keyword this has no meaning! Therefore, within the context of a static method, Java actually prevents the use of this from any method with the static modifier.

The opposite is not true. We can access class fields from non-static methods.

The main() method

The most common class method you will use is probably the main method.

Every Java program has a class method called main, which serves as the entry point to the program. To run a Java program, we need to tell the JVM the class whose main method should be invoked first. In the example that we have seen,

will invoke the main method defined within the class Hello to kick start the execution of the program.

The main method must be defined in the following way:

Composition

Till now, in our class, we only use the primitive type as the fields. However, it is advised and a good practice to use other classes (a.k.a reference type) in the fields. And this technique is called composition.

Basically, the main advantage of using composition is that it adds more abstraction. Recall that we wish to hide the implementation details as much as possible, protecting them with an abstraction barrier, so that the client does not have to bother about the details and it is easy for the implementer to change the details.

As you will see later also, what composition does is to implement a "has-a" relationship.

Heap and Stack

Stack

The stack contains variables. Bascially, stack is the region where all variables (including primitive types and object references) are allocated and stored.

Stack Frame

Stack frame is usually called call frame. Recall that the same variable names can exist in the program as long as they are in different methods. This means that the variables are contained within the call frames. Call frames are created when we invoke a method and are removed when the method completes.

And usually, when an instance method is called, a stack frame is created and it contains:

  1. the this reference

  2. the method arguments

  3. local variables within the method

When a class method is called, the stack frame does not contain the this reference.

  1. Instance and class fields are not variable. As such, fields are not in the stack.

  2. If we make multiple nested method calls, as we usually do, the stack frames get stacked on top of each other.

Heap

The heap stores dynamically allocated objects. To put it simply, whenever you use the keyword new, a new object is created in the heap, but the reference varaible will be stored on the stack.

An object in the heap contains the following information:

  • Class name.

  • Instance fields and the respective values.

  • Captured variables (more on this in later units).

Stack and Heap Diagram

Example One - Object Reference

For the following code:

Its stack and heap diagram should look like as follows:

Note that after Line 6, the field c (not the variable c) inside the Circle class is referenced to the Point object, not the center variable!

Example Two - Call Method

For the following code,

Its stack and frame diagram should look like as follows:

Summary

To summarize, Java uses call by value for primitive types, and call by reference for objects.

Inheritance

Recall the concept of subtyping. We say that S<:TS<:T if any piece of code written for type TT also works for type SS.

Now, let's use the example of Circle class. Suppose we want to add a color to the Circle class to make it a ColoredCircle. Now, we may find that all the methods and properties within the Cricle class should apply to ColoredCircle as well! So, we can think ColoredCricle as a subtype of Circle.

Inheritate from a class

We now show you how we can introduce this subtype relationship in Java, using the extends keyword. We can implement our ColoredCircle class this way:

We have just created a new type called ColoredCircle as a class that extends from Circle. We call Circle the parent class or superclass of ColoredCircle; and ColoredCircle a subclass of Circle.

Line 5 of the code above introduces another keyword in Java: super. Here, we use super to call the constructor of the superclass, to initialize its center and radius.

Run-Time Type

For the following code:

Recall that Circle is called the compile-time type of c. Here, we see that c is now referencing an object of the subtype ColoredCircle. Since this assignment happens during run-time, we say that the run-time type of c is ColoredCircle. The distinction between these two types will be important later in Polymorphism.

Here, Java allows us to assign a variable to store its subtype because this is considered as a widening type conversion and as we have seen earlier, this happens automatically and is allowed.

Overriding

Before we talk about overriding, let's introduce another very important point in Java, that is:

In Java, every class that does not extend another class inherits from class Object implicitly. Object is, therefore, the "ancestor" of all classes in Java and is at the root of the class hierarchy.

The Object class provides useful common methods to all objects, below are two of them:

  1. equals(Object obj), which checks if two objects are equal to each other, and

  2. toString(), which returns a string representation of the object as a String object.

Among them, the second method is quite interesting and explains why Java allows "addition" on string and integer, which was covered in Lec 01. For example,

Customize toString method

We may note that Circle@1ce92674 is not user-friendly so that we want customize it. To do so, let's define our own toString method in Circle to override the Object::toString(). This technique is called Overriding.

From this example, we can see that inheritance is not only good for extending the behavior of an existing class but through method overriding, we can alter the behavior of an existing class as well.

Using super to access overriden methods

Method Signature and Descriptor

The method signature of a method contains:

  1. the method name

  2. the number of parameters,

  3. the type of each parameter,

  4. and the order of its parameters.

The method descriptor is defined as the method signature plus the return type.

For example, for the following code

Its method signature is C::foo(B1, B2) and its method descriptor is A C::foo(B1, B2).

Note that for the parameters, we are using their type instead of their names. And class names are not a requirement in method signature and method descriptor, they are used just to make this notation clearer.


So, using the idea of method siganature and method descriptor, overriding basically happens when a subclass defines an instance method with the same method descriptor as an instance method in the parent class.

Last updated