From several arrays to classes

edited November 2014 in Common Questions

From several arrays to classes

In the previous article, From several variables to arrays, we shown how we can scale a program by using arrays. We also saw that having to handle several arrays isn't very practical. If we have other kinds of objects, we have more arrays, unrelated. We have to make sure the arrays keep the same size. We have difficulties to add or remove items. We will see how to address these problem, using classes.

The previous sketch

The full sketch of the previous article is as follows:

// Ball parameters
final int BALL_NB = 5;
// Positions
float[] posX = new float[BALL_NB];
float[] posY = new float[BALL_NB];
// Movements (linear)
float[] speedX = new float[BALL_NB];
float[] speedY = new float[BALL_NB];
// Radius of the balls
int[] radius = { 8, 16, 24, 32, 48 };
// And the colors
color[] ballColor = { #DDEE55, #AA44EE, #22BBAA, #0022FF, #00FF22 };

void setup()
{
  size(600, 400);
  smooth();

  // Initialize the balls' data
  for (int i = 0; i < BALL_NB; i++)
  {
    posX[i] = random(radius[i], width - radius[i]);
    posY[i] = random(radius[i], height - radius[i]);
    speedX[i] = random(-7, 7);
    speedY[i] = random(-7, 7);
  }
}

void draw()
{
  // Erase the sketch area
  background(#AAFFEE);
  for (int i = 0; i < BALL_NB; i++)
  {
    // Compute the new ball position
    moveBall(i);
    // And display it
    displayBall(i);
  }
}

void moveBall(int n)
{
  // Move by the amount determined by the speed
  posX[n] += speedX[n];
  // Check the horizontal position against the bounds of the sketch
  if (posX[n] < radius[n] || posX[n] > width - radius[n])
  {
    // We went out of the area, we invert the h. speed (moving in the opposite direction)
    // and put back the ball inside the area
    speedX[n] = -speedX[n];
    posX[n] += speedX[n];
  }
  // Idem for the vertical speed/position
  posY[n] += speedY[n];
  if (posY[n] < radius[n] || posY[n] > height - radius[n])
  {
    speedY[n] = -speedY[n];
    posY[n] += speedY[n];
  }
}

void displayBall(int n)
{
  // Simple filled circle
  noStroke();
  fill(ballColor[n]);
  ellipse(posX[n], posY[n], radius[n] * 2, radius[n] * 2);
}

A simple class definition

A class just represents an object. Here, it will represent a ball, with all its parameters. In its simplest form, it can be seen as a structure, a way to regroup related variables in the same group of data. Somehow, we go back to the original code with one ball, except we wrap the variables in a class declaration:

class Ball // By tradition, class names start with an initial uppercase letter
{
  float posX, posY; // Position
  float speedX, speedY; // Movement (linear)
  int radius; // Radius of the ball
  color ballColor; // And its color
}

And that's all... Note that the name "ballColor" is a bit redundant, since this class variable (also called field) is scoped in a Ball, but we cannot use simply "color" since it is a reserved word in Processing.

This class is just a blueprint, it shows what kind of variables are inside it, but it has no real existence. It is a type, defining a kind of variable. You have to create an instance of this class, to create an object out of it, to have real data space allocated for this instance:

Ball ball = new Ball();

This calls a special function (functions inside classes are called methods), named constructor. Here, we have not created such function, but the compiler provides it, it is called the default constructor.

The ball variable doesn't really hold the object, it holds a reference to this object. The object exists in a special memory, and all variables and parameters of this type just have the reference of this object.

Thus, if you write:

Ball b = ball;

you are actually not creating a copy of the object, unlike what most people think first, but copy of the reference to the same object. If you change the object referenced by ball, you also change the one referenced by ball!

It can seem counter intuitive, because if you write:

int n = 5;
int a = n;

then a and n are independent, if you change one, you don't change the other. But, again, ball and b hold references. If you want a copy of an object, you have to clone it, to make a double in memory, with a new reference.

Initializing the objects

And how we change these objects? So far, the newly created object have all its fields initialized at default values, here zero... We can change this default value at declaration time:

  int radius = 32; // Radius of the ball, 32 by default

inside the class declaration.

But once created, we can also access each field and change it:

Ball ball = new Ball();
ball.posX = 120;
ball.posY = 50;
ball.speedX = -2;
ball.speedY = 3;
ball.radius = 24;
ball.ballColor = #002277;

We have to repeat ball., there is no shortcut for this, unlike some languages.

But we can make our own constructor, with parameters!

class Ball
{
  float posX, posY; // Position
  float speedX, speedY; // Movement (linear)
  int radius; // Radius of the ball
  color ballColor; // And its color

  Ball(float x, float y, float sx, float sy, int r, color c)
  {
    posX = x;
    posY = y;
    speedX = sx;
    speedY = sy;
    radius = r;
    ballColor = c;
  }
}

Thus, we can build the full ball in one call only:

Ball ball = new Ball(120, 50, -2, 3, 24, #002277);

and it comes will all its parameters set.

Inconvenience: with lot of parameters like this, we can easily loose the meaning of each parameter, forget one, invert two, etc. Alas, Java doesn't have named parameters, so we have to deal with it in other ways. For example, we can use comments:

Ball ball = new Ball(
    120, 50, // x and y position
    -2, 3,   // x and y speeds
    24,      // radius
    #002277  // color
    );

A more advanced trick is to use chained setters, but we won't see that here.

Some tricks

Trick 1: sometime, you will see parameters of same name than the class fields. To distinguish them in the method, we use the this. notation, where this just means "this object, the current one being created / accessed". Some people also like to systematically prefix the fields with this everywhere in the class, but I find that it is a bit verbose, redundant and less readable.

  Ball(float posX, float posY, float speedX, float speedY, int radius, color ballColor)
  {
    this.posX = posX;
    this.posY = posY;
    this.speedX = speedX;
    this.speedY = speedY;
    this.radius = radius;
    this.ballColor = ballColor;
  }

Variants add a prefix or a suffix to the parameters names or the field names, eg. m_posX as field name, or posX_ as parameter name.

Trick 2: in Java, we can have method with identical names, they are seen as distinct as long as they have different kinds of parameters. So we can have a simplified constructor, giving for example default values to parameters, and a full constructor to override everything:

  Ball(int r, color c)
  {
    // This actually calls the other constructor, providing the missing values
    this(random(r, width - r), random(r, height - r), random(-7, 7), random(-7, 7), r, c);
  }

  Ball(float x, float y, float sx, float sy, int r, color c)
  {
    posX = x;
    posY = y;
    speedX = sx;
    speedY = sy;
    radius = r;
    ballColor = c;
  }

Note how the generated numbers depend on one parameter.

Using the class

This is nice, but how we use this class in the sketch? Quite simply: instead of declaring n arrays, we only declare one array of Balls:

final int BALL_NB = 5;
Ball[] balls = new Ball[BALL_NB];

Warning! The code above created an array of references to Ball objects. But these references are initialized at the default value for such references: null. This means that if you try and use the array as this, you will get a NullPointerException. You have to fill the array with fresh new instances of this class:

void setup()
{
  size(600, 400);
  smooth();

  // The colors to use
  // Note that plural names are preferred for arrays (and collections) names
  color[] colors = { #DDEE55, #AA44EE, #22BBAA, #0022FF, #00FF22 };
  // Initialize the balls' data
  for (int i = 0; i < BALL_NB; i++)
  {
    // Using the short version of the constructor
    balls[i] = new Ball(8 + i * 8, colors[i]);
  }
}

and, of course, we have to use the balls:

void draw()
{
  // Erase the sketch area
  background(#AAFFEE);
  for (int i = 0; i < BALL_NB; i++)
  {
    // Compute the new ball position
    moveBall(i);
    // And display it
    displayBall(i);
  }
}

void moveBall(int n)
{
  // Move by the amount determined by the speed
  balls[n].posX += balls[n].speedX;
  // Check the horizontal position against the bounds of the sketch
  if (balls[n].posX < balls[n].radius || balls[n].posX > width - balls[n].radius)
  {
    // We went out of the area, we invert the h. speed (moving in the opposite direction)
    // and put back the ball inside the area
    balls[n].speedX = -balls[n].speedX;
    balls[n].posX += balls[n].speedX;
  }
  // Idem for the vertical speed/position
  balls[n].posY += balls[n].speedY;
  if (balls[n].posY < balls[n].radius || balls[n].posY > height - balls[n].radius)
  {
    balls[n].speedY = -balls[n].speedY;
    balls[n].posY += balls[n].speedY;
  }
}

void displayBall(int n)
{
  // Simple filled circle
  noStroke();
  fill(balls[n].ballColor);
  ellipse(balls[n].posX, balls[n].posY, balls[n].radius * 2, balls[n].radius * 2);
  fill(#FF0000);
  text(str(n), balls[n].posX, balls[n].posY);
}

Adding methods

Note that moveBall() and displayBall() act on one ball at a time. And they are quite verbose, having to prefix each variable by the ball they act upon. Somehow, they can be part of the Ball class, since they almost only use class fields, and act on one instance of the class.

By integrating methods (ie. functions) to a class, we go from a simple structure, just holding data (that can change) to a fully autonomous object, offering methods to act on it and to allow it to display itself. And the methods need not to prefix the fields: they are part of the current object they act upon.

The new class becomes:

class Ball
{
  float posX, posY; // Position
  float speedX, speedY; // Movement (linear)
  int radius; // Radius of the ball
  color ballColor; // And its color

  Ball(int r, color c)
  {
    // This actually calls the other constructor, providing the missing values
    this(random(r, width - r), random(r, height - r), random(-7, 7), random(-7, 7), r, c);
  }

  Ball(float x, float y, float sx, float sy, int r, color c)
  {
    posX = x;
    posY = y;
    speedX = sx;
    speedY = sy;
    radius = r;
    ballColor = c;
  }

  void moveBall()
  {
    // Move by the amount determined by the speed
    posX += speedX;
    // Check the horizontal position against the bounds of the sketch
    if (posX < radius || posX > width - radius)
    {
      // We went out of the area, we invert the h. speed (moving in the opposite direction)
      // and put back the ball inside the area
      speedX = -speedX;
      posX += speedX;
    }
    // Idem for the vertical speed/position
    posY += speedY;
    if (posY < radius || posY > height - radius)
    {
      speedY = -speedY;
      posY += speedY;
    }
  }

  void displayBall()
  {
    // Simple filled circle
    noStroke();
    fill(ballColor);
    ellipse(posX, posY, radius * 2, radius * 2);
  }
}

As you can see, the methods are much simpler: no need for an index, as they act on the current object, no need to prefix the fields to access them.

Note the difference between constructors and methods: constructors have no type, not even void, and have the same name than the class. Methods usually start with a lowercase letter (that's just a convention) and have a type (void if they return nothing, or the type of what they return otherwise).

Now draw() becomes:

void draw()
{
  // Erase the sketch area
  background(#AAFFEE);
  for (int i = 0; i < BALL_NB; i++)
  {
    // Compute the new ball position
    balls[i].moveBall();
    // And display it
    balls[i].displayBall();
  }
}

We call the methods with a syntax similar to the field access.

What is nice is that we no longer need to access the fields, now. We could even make them hidden outside of the class (by using a private keyword before each declaration), as only the class has the responsibility to manage them. We say we encapsulated the functionality of the class, it acts as a black box: we don't care of how it is implemented, all we need to know is how to make an instance of it, and how to call its methods.

That's a bit of advanced usage of classes, rarely used in Processing with its small sketches (but it can be used in libraries).

Last note: we can use the new for notation in draw():

void draw()
{
  // Erase the sketch area
  background(#AAFFEE);
  for (Ball ball : balls)
  {
    // Compute the new ball position
    ball.moveBall();
    // And display it
    ball.displayBall();
  }
}

See how ball replaces balls[i] and the for loop iterates on all the items of the array without needing an index.

Tagged:
Sign In or Register to comment.