Tutorial 14: Working with Objects

Object-Oriented Programming

In the kinds of programs that we've written so far, it has often been the case that we have had some data and some functions that work together closely. For example, if we want to have a number of circles drawn to the screen in different colors we might do something like this:

  • Show Sketch
/** @peep sketchcode */
float[] circleX; // x-coordinates of circles
float[] circleY; // y-coordinates of circles
float[] circleRadius; // radius values of circles
color[] circleColour; // colour values for circles
 
void setup() {
  size(300, 300);
  colorMode(HSB, 360, 100, 100);
  circleX = new float[100];
  circleY = new float[100];
  circleRadius = new float[100];
  circleColour = new float[100];
  for (int i = 0; i < circleX.length; i++) {
    circleX[i] = random(width);
    circleY[i] = random(height);
    circleRadius[i] = random(10, 40);
    circleColour[i] = color(random(360), 100, 100);
  }  
}
 
void draw() {
  noStroke();
  for (int i = 0; i < circleX.length; i++) {
    fill(circleColour[i]);
    ellipse(circleX[i], circleY[i], 2*circleRadius[i], 2*circleRadius[i]);
  }
}

This code relies on the fact that circleX[i], circleY[i], circleRadius[i] and circleColor[i] all relate to the same "object", a circle. In object-oriented programming a "class" is a way to group together the definitions for variables and functions that work together. Using object-orented programming we can re-write the above as the following:

  • Show Sketch
/** @peep sketchcode */
Circle[] circles;
 
void setup() {
  size(300, 300);
  colorMode(HSB, 360, 100, 100);
  circles = new Circle[100];
  for (int i = 0; i < circles.length; i++) {
    circles[i] = new Circle(random(width), random(height), random(10, 40), color(random(360), 100, 100));
  }
}
 
void draw() {
  for (int i = 0; i < circles.length; i++) {
    circles[i].draw();
  }
}
 
class Circle {
  float x;
  float y;
  float radius;
  color colour;
 
  Circle(float _x, float _y, float _radius, float _colour) {
    x = _x;
    y = _y;
    radius = _radius;
    colour = _colour;
  }
 
  void draw() {
    noStroke();
    fill(colour);
    ellipse(x, y, 2*radius, 2*radius);
  }
}

There are a couple of things to notice about this second implementation:

  1. The data relating to a single circle is kept together in the class, making it clear that they are related
  2. The functions that uses the data for a single circle is kept together with the data, making it clear that this is what it works with
  3. The main sketch can deal with the more meaningful idea of "circles" rather than numbers that happen to related to a circle
  4. The main sketch code doesn't have to know very much about the inner workings of the Circle class, e.g., exactly how it is drawn, to be able to use it

These are some of the important benefits of using object-oriented programming, which you may like to take advantage of in your own code.

Classes

In general, a class has a name that begins with a capital letter, like a proper noun. It is defined using the keyword "class" and the things that belong inside the class are enclosed in curly brackets.

    class MyClass {
      // Things that are part of the class go here...
    }

    A class can define some variables (called fields in object-oriented programming) and functions (called methods in object-oriented programming) that belong together. It is perfectly valid to create a class that only contains fields, for example:

      class MyClass {
        int field1;    // Variables inside a class are called fields
        float field2;  // You can have fields of any known data type
        String field3; // You can also have fields that are objects
      }

      Fields can be of any type. Every instance of a class (see the next section on "Objects") has it's own set of fields that are unique to it. In other words, these variable declarations are repeated for every object that we create and these define the unique qualities of the object.

      Typically, we want to define some functions that can work with the variables that belong to an object. Just like the functions that we've written so far, methods can return values (or not if declared as "void") and optionally take parameters.

        class MyClass {
          // Definition of fields goes here...
         
          void method1() {
            // Implementation of method1 goes here...
          }
         
          float method2() {
            // Implementation of method2 goes here...
          }
         
          int method3(int parameter1) {
            // Implementation of method3 goes here...
          }
        }

        Classes can contain special functions called constructors, which have the same name as the class they are contained within. Constructors describe how an instance of a class (an object) is constructed by assigning values to the fields that belong to an instance.

          class MyClass {
            // Definition of fields goes here...
           
            MyClass() {
              // Implementation of default constructor goes here...
            }
           
            MyClass(String aString) {
              // Implementation of alternative constructor goes here...
            }
           
            // Definition of methods goes here...
          }

          As you can see a class may have multiple constructors, these provide different ways to construct an object. Each constructor must take a different set of parameters, otherwise Processing will not know which constructor to call. The constructor that doesn't take any parameters is called the "default constructor".

          The following piece of code gives the general structure of a class that brings together fields, methods and constructors:

            class MyClass {
              int field1;    // Variables inside a class are called fields
              float field2;  // You can have fields of any known data type
              String field3; // You can also have fields that are objects
             
              // Constructors are special types of functions with the same
              // name as the class they belong to.
              MyClass() {
                // Implementation of default constructor goes here...
              }
             
              MyClass(String aString) {
                // Implementation of alternative constructor goes here...
              }
             
              void method1() {
                // Implementation of method1 goes here...
              }
             
              float method2() {
                // Implementation of method2 goes here...
              }
             
              int method3(int parameter1) {
                // Implementation of method3 goes here...
              }
            }

            The following sections will explore the concepts introduced here in more detail.

            Objects

            To use a class we need to create "instances" of it, called objects, we do this by creating variables of the type of the class and then "constructing" an instance of the class to be stored in the variable using the new operator. Here's an example using the example class from above:

              // Declare a variable called "anObject" that uses the class "MyClass" as its data type
              MyClass anObject;
              // Construct a new instance of the "MyClass" class and store it in the variable "anObject"
              anObject = new MyClass();

              As with other data types, we can combine the declaration and construction into a single statement:

                MyClass anObject = new MyClass();

                It is also possible to construct arrays of objects, in a way that is similar to using other data types that we have encountered before.

                  // Declare an array of objects called "someObjects" that can store objects of type "MyClass"
                  MyClass[] someObjects;
                  // Assign an array big enough to hold 100 instances of the "MyClass" class to "someObjects"
                  // Note that this does not construct 100 objects, it just reserves the space for them in the array
                  someObjects = new MyClass[100];

                  Again, we can combine the declaration and assigment in one statement:

                    // Declare and assign an array of objects called "someObjects" that can store objects of type "MyClass"
                    // Note that this does not construct 100 objects, it just reserves the space for them in the array
                    MyClass[] someObjects = new MyClass[100];

                    The biggest difference is that when we create an array of objects, we only reserve the memory to store the objects in, we don't construct all of the objects to fill the array automatically. We say that we "allocate" the memory for the array but we don't "instatiate" the objects that go inside the array. To fill the array we need to to iterate through the allocated memory and instatiate a single instance of the class that should go inside it:

                      // Declare and assign an array of objects called "someObjects" that can store objects of type "MyClass"
                      MyClass[] someObjects = new MyClass[100];
                      // Iterate through the array and construct an object for every index location
                      for (int i = 0; i < someObjects.length; i++) {
                        // Construct an instance of "MyClass" and store it at index i in the array "someObjects"
                        someObjects[i] = new MyClass();
                      }

                      Fields

                      Classes define fields that are like variables but are contained within objects. Just like variables, fields are declared with the type of the data that can be stored in the field. For example, a class used to represent a coloured circle will likely have fields that can store values for the position (e.g. x and y coordinates) and radius of the circle together with its colour. For example:

                        class Circle {
                          float x;
                          float y;
                          float radius;
                          color colour;
                        }

                        How would you change the class to support stroke colour and stroke weight?

                        Design a class called Square, which contains fields to represent a square defined by a centre point and a side length.

                        The Dot Syntax

                        To access the fields defined for an object, we use the "dot syntax", which takes the form of "object.field". This can work for single objects like an instance of the Circle class above:

                          Circle aCircle = new Circle();
                          aCircle.x = width/2;
                          aCircle.y = height/2;
                          aCircle.radius = 100;
                          aCircle.colour = color(255);

                          The last four lines of this example code show how a program can access the fields defined for the object "aCircle". The dot syntax is also used to access the fields associated with an object stored in an array. The following implementation of the original example code shows how this works:

                          • Show Sketch
                          /** @peep sketchcode */
                          Circle[] circles;
                           
                          void setup() {
                            size(300, 300);
                            colorMode(HSB, 360, 100, 100);
                            circles = new Circle[100];
                            for (int i = 0; i < circles.length; i++) {
                              circles[i] = new Circle();
                              circles[i].x = random(width);
                              circles[i].y = random(height);
                              circles[i].radius = random(10, 40);
                              circles[i].colour = color(random(360), 100, 100);
                            }  
                          }
                           
                          void draw() {
                            noStroke();
                            for (int i = 0; i < circles.length; i++) {
                              fill(circles[i].colour);
                              ellipse(circles[i].x, circles[i].y, 2*circles[i].radius, 2*circles[i].radius);
                            }
                          }
                           
                          class Circle {
                            float x;
                            float y;
                            float radius;
                            color colour;
                          }

                          Create a version of the example program that uses your Square class instead of the Circle class.

                          this

                          Every object has a special field that references itself called this. We'll see how it can be used a little later when we talk about constructors but we can use this with the dot syntax to reference the fields of an object, consequently, this.x is exactly the same as simply x in an object with a field called x.

                          Objects in Objects

                          It is important to understand that objects can contain other objects. For example, Processing provides a class called PVector, which is a useful way to combine together x, y and z values in a single variable. So we could rewrite the above as:

                            class Circle {
                              PVector position;
                              float radius;
                              color colour;
                            }

                            Using the dot syntax it is possible to navigate into fields that are objects. For example, to access the x field in the position field of an instance of the Circle class we can use the following syntax:

                              aCircle.position.x = 100;

                              Objects can even contain fields of the same type as the class they are an instance of, so it's possible for have classes like this:

                                class Person {
                                  String firstName;
                                  String familyName;
                                  Person mother;
                                  Person father;
                                }

                                In this class a Person is represented such that a record of their mother and father are stored as Person objects. To use this class, we might write code like the following:

                                  Person jane = new Person();
                                  jane.firstName = "Jane";
                                  jane.familyName = "Bond";
                                   
                                  Person james = new Person();
                                  james.firstName = "John";
                                  james.familyName = "Dond";
                                   
                                  Person jimmy = new Person();
                                  jimmy.firstName = "Jimmy";
                                  jimmy.familyName = "Doe";
                                   
                                  jimmy.mother = jane;
                                  jimmy.father = james;

                                  It is also possible to store arrays of objects in an object, so we can extend the above class as shown in the following:

                                    class Person {
                                      String firstName;
                                      String familyName;
                                      Person mother;
                                      Person father;
                                      Person[] friends;
                                    }

                                    In this definition of the Person class, there is an array of instances of the Person class that represents the friends of a person. Extending the previous example, we can add and access the array of friends for Jimmy Bond in the following way:

                                      Person moneypenny = new Person();
                                      // Insert classified information about Ms Moneypenny here...
                                      Person m = new Person();
                                      // Insert classified information about M here...
                                      Person q = new Person();
                                      // Insert classified information about Q here...
                                       
                                      jimmy.friends = new Person[3];
                                      jimmy.friends[0] = moneypenny;
                                      jimmy.friends[1] = m;
                                      jimmy.friends[2] = q;

                                      As you can see initialising and using an array of objects inside an object is much like using them anywhere else, we need to allocate the memory to hold the objects and then fill the indices of the array with the objects.

                                      Write a class that contains an array of circles and use this class to store a line of circles by writing a sketch that initialises the object and its array.

                                      Methods

                                      Using classes to define objects as collections of related data is very helpful but it doesn't reduce the amount of knowledge that we must know about the internal structure of the objects that we want to use. By combining fields with methods that have been designed to work with the data stored in an object, we can dramatically reduce the need for a program to access the data held in fields directly. For example, we can extend the previous Circle class to include a draw() function that uses the data stored in an object's fields to draw it:

                                        class Circle {
                                          float x;
                                          float y;
                                          float radius;
                                          color colour;
                                         
                                          void draw() {
                                            noStroke();
                                            fill(colour);
                                            ellipse(x, y, 2*radius, 2*radius);
                                          }
                                        }

                                        The draw() ensures that no stroke is drawn, sets the fill color according to the value stored in the field colour in the Circle object and finally draws an ellipse at the coordinates (x, y) specified using the radius for the Circle object. Notice how the draw() method uses the fields without needing the dot syntax? This is because the fields and the method belong together in an object and any reference to a field is automatically considered to refer to the field held by the object to which the method belongs.

                                        Extend the Square class that you wrote previously to allow objects to draw themselves by adding a draw() method to the class.

                                        We can use the draw() method of Circle class to greatly simplify the draw() function in the sketch:

                                          void draw() {
                                            for (int i = 0; i < circles.length; i++) {
                                              circles[i].draw();
                                            }
                                          }

                                          Notice how in the code below the main draw() function simply iterates through the array of circle objects. We can use this approach to alter our example code:

                                          • Show Sketch
                                          /** @peep sketchcode */
                                          Circle[] circles;
                                           
                                          void setup() {
                                            size(300, 300);
                                            colorMode(HSB, 360, 100, 100);
                                            circles = new Circle[100];
                                            for (int i = 0; i < circles.length; i++) {
                                              circles[i] = new Circle();
                                              circles[i].x = random(width);
                                              circles[i].y = random(height);
                                              circles[i].radius = random(10, 40);
                                              circles[i].colour = color(random(360), 100, 100);
                                            }
                                          }
                                           
                                          void draw() {
                                            for (int i = 0; i < circles.length; i++) {
                                              circles[i].draw();
                                            }
                                          }
                                           
                                          class Circle {
                                            float x;
                                            float y;
                                            float radius;
                                            color colour;
                                           
                                            void draw() {
                                              noStroke();
                                              fill(colour);
                                              ellipse(x, y, 2*radius, 2*radius);
                                            }
                                          }

                                          Replace the circles in the above code with squares by using your extended Square class.

                                          Constructors

                                          A constructor is a special type of method that shares the same name as the class it belongs to. Unusually, constructors do not declare a return type. Constructors always return an instance of the class that they are defined for. We can add a constructor to the Circle class as follows:

                                            Circle(float _x, float _y, float _radius, float _colour) {
                                              x = _x;
                                              y = _y;
                                              radius = _radius;
                                              colour = _colour;
                                            }

                                            In this case, the constructor for the Circle class requires that we pass four parameters to it, which it stores in the instance of the class as the values for x, y, radius and colour inside the class. Typically, we will arrange the code inside a class so that the constructors come before other methods:

                                              class Circle {
                                                float x;
                                                float y;
                                                float radius;
                                                color colour;
                                               
                                                Circle(float _x, float _y, float _radius, float _colour) {
                                                  x = _x;
                                                  y = _y;
                                                  radius = _radius;
                                                  colour = _colour;
                                                }
                                               
                                                void draw() {
                                                  noStroke();
                                                  fill(colour);
                                                  ellipse(x, y, 2*radius, 2*radius);
                                                }
                                              }

                                              Parameter/Field Names

                                              Notice the naming convention that's being used in the above constructor, the parameters all start with an underscore character. There is no requirement for constructors to use this convention and different programmers have different ways of working with fields and parameters, this is just my personal preference. For example, some programmers prefer to use an underscore at the start of all field names, so that they can easily tell which variables in a method belongs to an object:

                                                class Circle {
                                                  float _x;
                                                  float _y;
                                                  float _radius;
                                                  float _colour;
                                                 
                                                  Circle(float x, float y, float radius, float colour) {
                                                    _x = x;
                                                    _y = y;
                                                    _radius = radius;
                                                    _colour = colour;
                                                  }
                                                 
                                                  void draw() {
                                                    noStroke();
                                                    fill(_colour);
                                                    ellipse(_x, _y, 2*_radius, 2*_radius);
                                                  }
                                                }

                                                Both of these approaches solve a common problem that the parameters that we pass into a constructor are simply values that we want to store in fields. Consequently, it is tempting to name them the same, like this:

                                                  // Example of a common problem when writing constructors
                                                  Circle(float x, float y, float radius, float colour) {
                                                    x = x;
                                                    y = y;
                                                    radius = radius;
                                                    colour = colour;
                                                  }

                                                  Unfortunately, Processing has no way to know that the references to x, y, radius and colour on the left of each of the assignments refers to the fields of an object and the references to x, y, radius and colour on the right of each assignment refers to the parameters with the same name. In fact, Processing will assume that all of the references are to the parameters because these are defined on the inner-most block (set of curly brackets) and it will effectively reassign the values of all of the parameters to themselves - this is not an error and Processing will not complain about doing this, so you should be careful when writing constructors like this. The two naming conventions described above solve this problem using the underscore character. An alternative approach is to use the special this field to diambiguate the references for fields in the object, like this:

                                                    // Example of using this to diambiguate references to fields
                                                    Circle(float x, float y, float radius, float colour) {
                                                      this.x = x;
                                                      this.y = y;
                                                      this.radius = radius;
                                                      this.colour = colour;
                                                    }

                                                    This is one of the most common uses of the special this field. It allows us to use the same names for the parameters to the constructor and the fields in the class. All of these approaches to solving the problem of naming parameters is valid and you should choose whichever one you are most comfortable with.

                                                    Try rewriting the constructor and/or fields for your Square class using different naming conventions to see which one you prefer.

                                                    Using a constructor can simplify the code needed to build objects, for example, the setup() function in example code from before can now be rewritten to use the constructor instead of assigning values to the individual fields of each object specifically:

                                                      void setup() {
                                                        size(300, 300);
                                                        colorMode(HSB, 360, 100, 100);
                                                        circles = new Circle[100];
                                                        for (int i = 0; i < circles.length; i++) {
                                                          circles[i] = new Circle(random(width), random(height), random(10, 40), color(random(360), 100, 100));
                                                        }
                                                      }

                                                      This makes it even more clear that our code is dealing with circles and not just bundles of variables grouped together. Here is our example that we've been converting to using objects, no with the use of the constructor:

                                                      • Show Sketch
                                                      /** @peep sketchcode */
                                                      Circle[] circles;
                                                       
                                                      void setup() {
                                                        size(300, 300);
                                                        colorMode(HSB, 360, 100, 100);
                                                        circles = new Circle[100];
                                                        for (int i = 0; i < circles.length; i++) {
                                                          circles[i] = new Circle(random(width), random(height), random(10, 40), color(random(360), 100, 100));
                                                        }
                                                      }
                                                       
                                                      void draw() {
                                                        for (int i = 0; i < circles.length; i++) {
                                                          circles[i].draw();
                                                        }
                                                      }
                                                       
                                                      class Circle {
                                                        float x;
                                                        float y;
                                                        float radius;
                                                        color colour;
                                                       
                                                        Circle(float _x, float _y, float _radius, float _circle) {
                                                          x = _x;
                                                          y = _y;
                                                          radius = _radius;
                                                          colour = _circle;
                                                        }
                                                       
                                                        void draw() {
                                                          noStroke();
                                                          fill(colour);
                                                          ellipse(x, y, 2*radius, 2*radius);
                                                        }
                                                      }

                                                      Default Constructors

                                                      A class can define multiple different types of constructors that take different numbers and types of parameters. For example, to save us from having to always specify a colour, we might create a constructor that uses white as a default colour, like so:

                                                        Circle(float _x, float _y, float _r) {
                                                          x = _x;
                                                          y = _y;
                                                          radius = _radius;
                                                          colour = color(255);
                                                        }

                                                        A constructor for a class that doesn't take any parameters is called the "default constructor". A default should specify default values for all of an object's fields:

                                                          Circle() {
                                                            x = 100;
                                                            y = 100;
                                                            radius = 40;
                                                            colour = color(255);
                                                          }

                                                          Write a default constructor for your Square class that provides default values for all of an object's fields.

                                                          Constructors Calling Constructors

                                                          It is common to provide multiple constructors like this, each one providing one or more default values for fields. A common idiom (way of doing things) is for constructors that supply default values to call more general constructors that require all of the values to be passed to them. This way, when we need to change something in the most general constructor it is automatically applied to all other constructors. The way that one constructor calls another constructor is to use the special this() method that exists for all classes. Here is an example of using this technique for the Circle class:

                                                            class Circle {
                                                              float x;
                                                              float y;
                                                              float radius;
                                                              color colour;
                                                             
                                                              Circle() {
                                                                this(100, 100, 40);
                                                              }
                                                             
                                                              Circle(float _x, float _y, float _radius) {
                                                                this(_x, _y, _radius, color(255));
                                                              }
                                                             
                                                              Circle(float _x, float _y, float _radius, color _colour) {
                                                                x = _x;
                                                                y = _y;
                                                                radius = _radius;
                                                                colour = _colour;
                                                              }
                                                             
                                                              // Other methods go here...
                                                            }

                                                            Rewrite the default constructor for your Square class to call the most general constructor you have written with default values as parameters.

                                                            Excercises

                                                            To practice using classes complete the following exercises and upload the resulting sketches to your portfolio:

                                                            1. Create a new sketch that uses your Square class.
                                                            2. Create a new class that represents triangles and use it to produce a design.
                                                            3. Add methods to the Circle class and your Square and Triangle classes to support different stroke weights and colours.
                                                            4. Create a new sketch that mixes circles, triangles and squares using your classes.

                                                            Comments

                                                            Nobody has said anything yet.