CSCI A201/A597

Lecture Notes Twenty-Six

Second Summer 2000


Parallel arrays. Arrays of object data. Vectors.
Let's now imagine a different situation than the one with which we introduced Java arrays. Different but similar.

Assume the user has three pieces of information to enter each time:
  • name of product,
  • price in dollars, and a
  • performance score.

The best product is the one that has the highest ratio between performance and price. Yes, simply picking the lowest price may not be the best bargain.

So now we want to analyze our data set from a different, more enhanced perspective. Looks like we are going to build three arrays.

Indeed, that will be our first approach. Easy to write after all these little programs.

We will then propose a better alternative. Let's write the first one first.
import java.util.StringTokenizer; 

class One {
    public static void main(String[] args) {
	System.out.println("Hello, and welcome to the evaluator. "); 
	ConsoleReader console = new ConsoleReader(System.in); 
	int dataSize = 0; 
	while (true) {
	    System.out.print("[" + dataSize + "]% "); 
	    String line = console.readLine(); 
	    if (line == null) break; 
            StringTokenizer tokenizer = new StringTokenizer(line); 
	    String product = tokenizer.nextToken(); 
	    double score = Double.parseDouble(tokenizer.nextToken()); 
	    double price = Double.parseDouble(tokenizer.nextToken()); 
	    dataSize += 1; 
	} 
        System.out.println("\n** Data has been entered now."); 

	System.out.println("** We need to process it."); 

	System.out.println("** We then need to print the results."); 

	System.out.println("\nThank you for using our program!"); 
    } 
} 

Well, where are the arrays? This is just the framework, the protocol.

Indeed, the program doesn't store anything. Well, then let me declare, initialize, and build the arrays:
import java.util.StringTokenizer; 

class One {
    public static void main(String[] args) {
        System.out.println("Hello, and welcome to the evaluator. "); 
        ConsoleReader console = new ConsoleReader(System.in); 
        int dataSize = 0; 
	final int DATA_LENGTH = 1000; 
	String[] names = new String[DATA_LENGTH];
	double[] scores = new double[DATA_LENGTH];
	double[] prices = new double[DATA_LENGTH]; 
        while (true) {
            System.out.print("[" + dataSize + "]% "); 
            String line = console.readLine(); 
            if (line == null) break; 
            StringTokenizer tokenizer = new StringTokenizer(line); 
            String product = tokenizer.nextToken(); 
            double score = Double.parseDouble(tokenizer.nextToken()); 
            double price = Double.parseDouble(tokenizer.nextToken()); 
	    names[dataSize] = product;
	    scores[dataSize] = score;
	    prices[dataSize] = price; 
            dataSize += 1; 
        } 
        System.out.println("\n** Data has been entered now."); 

        System.out.println("** We need to process it."); 

        System.out.println("** We then need to print the results."); 

	for (int i = 0; i < dataSize; i++) 
	    System.out.println(names[i] + " " + 
			       scores[i] + " " + prices[i]);

        System.out.println("\nThank you for using our program!"); 
    } 
} 

I see you have defined three arrays, and are managing them now. Yes, I collect the data and print it at the end.

What kind of processing are you going to do? Find the best bargain, the product with the highest ratio.

Isn't it a pain to have to maintain three arrays at the same time? I'm not sure yet. Let me finish the program.

Sure, this will be good practice. I'll also add a "quit" keyword to what the program understands,

... which would make the conversation a bit cleaner, ... indeed. Here's the updated version:
import java.util.StringTokenizer; 

class One {
    public static void main(String[] args) {
        System.out.println(
          "Hello, and welcome to the evaluator. Type quit to quit."); 
        ConsoleReader console = new ConsoleReader(System.in); 
        int dataSize = 0; 
	final int DATA_LENGTH = 1000; 
	String[] names = new String[DATA_LENGTH];
	double[] scores = new double[DATA_LENGTH];
	double[] prices = new double[DATA_LENGTH]; 
        while (true) {
            System.out.print("[" + dataSize + "]% "); 
            String line = console.readLine(); 
            if (line == null || line.equals("quit")) break; 
            StringTokenizer tokenizer = new StringTokenizer(line); 
            String product = tokenizer.nextToken(); 
            double score = Double.parseDouble(tokenizer.nextToken()); 
            double price = Double.parseDouble(tokenizer.nextToken()); 
	    names[dataSize] = product;
	    scores[dataSize] = score;
	    prices[dataSize] = price; 
            dataSize += 1; 
        } 
        System.out.println("\n** Data has been entered now."); 
        System.out.println("** We need to process it."); 
	double highest = scores[0] / prices[0]; 
	for (int i = 0; i < dataSize; i++) {
	    if (highest < scores[i] / prices[i]) 
                highest = scores[i] / prices[i]; 
	} 
        System.out.println("** We then need to print the results."); 
	for (int i = 0; i < dataSize; i++) {
	    System.out.print  (names[i] + " " +  // print not println
			       scores[i] + " " + prices[i]);
	    if (highest == scores[i] / prices[i]) 
		System.out.println(" ** "); 
	    else System.out.println(); 
	}
        System.out.println("\nThank you for using our program!"); 
    } 
} 

Can you show me the program live? I sure can:
frilled.cs.indiana.edu%java One
Hello, and welcome to the evaluator. Type quit to quit.
[0]% one 1 2
[1]% two 4 9
[2]% six 2 1
[3]% ten 3 3
[4]% quit

** Data has been entered now.
** We need to process it.
** We then need to print the results.
one 1.0 2.0
two 4.0 9.0
six 2.0 1.0 ** 
ten 3.0 3.0

Thank you for using our program!
frilled.cs.indiana.edu%

Looks like a fractions program to me... Ratios, they're ratios.

How do you feel about your program? I feel proud of being able to manipulating three arrays at the same time.

Tired, perhaps. How do you call your arrays? They are called parallel arrays.

The ith slice
  • names[i]
  • prices[i]
  • scores[i]

... contains data that needs to be processed together. Parallel arrays become a headache in larger programs.

The programmer must ensure that the arrays always have the same length and that each slide is filled with values that actually belong together. Most importantly, any method that operates on a slice must get all arrays as parameters, which is tedious to program.

The remedy is simple. Yes: look at the slice, and find the concept that it represents.

Then make the concept into a class.
class Product {
  String name; 
  double score; 
  double price; 
}

Then use an array of objects.
Product[] data = new Product[DATA_LENGTH]

The program has now one array (of objects). Vectors are arrays of objects, which can grown and shrink as necessary.

We'll get to that in a second. Can you now eliminate the parallel arrays in our application? I sure can:
import java.util.StringTokenizer; 

class Product {
    String name;
    double score; 
    double price;
    Product(String n, double s, double p) {
	name = n; score = s; price = p; 
    } 
} 

class Two {
    public static void main(String[] args) {
        System.out.println(
          "Hello, and welcome to the evaluator. Type quit to quit."); 
        ConsoleReader console = new ConsoleReader(System.in); 
        int dataSize = 0; 
	final int DATA_LENGTH = 1000; 
	Product[] data = new Product[1000]; 
        while (true) {
	    Product p = Two.readProduct(console, dataSize); 
            if (p == null) break; 
            data[dataSize] = p; 
            dataSize += 1; 
        } 
        System.out.println("\n** Data has been entered now."); 
        System.out.println("** We need to process it."); 
	double highest = data[0].score / data[0].price; 
	for (int i = 0; i < dataSize; i++) {
	    if (highest < data[i].score / data[i].price) 
                highest = data[i].score / data[i].price; 
	} 
        System.out.println("** We then need to print the results."); 
	for (int i = 0; i < dataSize; i++) {
	    Two.printProduct(data[i], highest); 
	}
        System.out.println("\nThank you for using our program!"); 
    } 

    public static Product readProduct(ConsoleReader console, int rank) {
	System.out.print("[" + rank + "]% ");
	String line = console.readLine(); 
        if (line == null || line.equals("quit")) return null; 
        StringTokenizer tokenizer = new StringTokenizer(line); 
	String name = tokenizer.nextToken();
	double score = Double.parseDouble(tokenizer.nextToken());
	double price = Double.parseDouble(tokenizer.nextToken());
	return new Product(name, score, price); 
    } 

    public static void printProduct(Product p, double highest) {
	System.out.print(p.name + " " + p.score + " " + p.price);
	if (highest == p.score / p.price)
	    System.out.println(" ** ");
	else System.out.println();
    } 
} 



The program now has a single array of Product objects. This shows that the process of eliminating arrays was successful.

The set of parallel arrays is replaced by a single array. Each element in the resulting array corresponds to a slice in the set of parallel arrays.

Once you have this single concept available, it suddenly becomes much easier to give the program a better structure. The program easily factors out methods for reading and printing objects.

Indeed. To really see the advantage of using objects instead of parallel arrays, consider the readProduct method in the program above.

How would you implement that method if you didn't have a product object? You would have to return three values: the name, price, and score of the next product.

Georg Theiner was the first in this semester's A201 to point it out, earlier when he was working on his Fractions program. Well done, Georg!

In Java you can't return more than one value in a method. But you can put three values in an object and return the object.

Objects are containers too. Of course, and that's what they do best!

Now that we've seen arrays of objects, let's look into ... arrays as object data.

What's a polygon? Something with a lot of knees if you know Greek.

A polygon is a closed sequence of lines. To describe a polygon, you need to store the sequence of its corner points.

An array of Points! Yes.

What is a Point? Point2d.Double would work well for us.

What then is a Polygon?
class Polygon {
  Point2D.Double[] corners; 
  int cornersSize; 
  Polygon(int n) {
    corners = new Point2D.Double[n];     
    cornersSize = 0; 
  } 
  void add (Point2D.Double p) {
    corners[cornersSize] = p; 
    cornersSize += 1; 
  } 
} 

We model a polygon as a class containing an array of points (the instance variable corners) The class contains one constructor, which receives the size of the polygon,

... and a method, to add corners (i.e., points). Can you add a draw method?

It would have to be something like this: Good name, too.
class Polygon {
    Point2D.Double[] corners;
    int cornersSize; 
    Polygon(int n) {
	corners = new Point2D.Double[n];
	cornersSize = 0;
    }
    void add (Point2D.Double p) {
	corners[cornersSize] = p;
	cornersSize += 1;
    }
    void drawOnto(Graphics2D g2) {
	for (int i = 0; i < cornersSize; i++) {
	    Point2D.Double from = corners[i]; 
	    Point2D.Double to = corners[(i + 1) % cornersSize]; 
	    Line2D.Double side = new Line2D.Double(from, to); 
	    g2.draw(side); 
	} 
    }
} 

Now, can you show this to me in a running program? How about an applet?
import java.applet.*;
import java.awt.*; 
import java.awt.geom.*; 
import java.util.*; 

public class Three extends Applet {
    Polygon p; 
    public void start() {
	final int CORNERS = 6; 
	p = new Polygon(CORNERS); 
	Random gen = new Random(); 
	for (int i = 0; i < CORNERS; i++) {
	    int x = gen.nextInt(this.getWidth()); 
	    int y = gen.nextInt(this.getHeight()); 
	    Point2D.Double p = new Point2D.Double(x, y); 
	    this.p.add(p); // with your permission... 
	} 
    }
    public void paint(Graphics g) {
	Graphics2D g2 = (Graphics2D)g;
	g2.setColor(Color.red); 
	this.p.drawOnto(g2); 
    } 
} 


class Polygon {
    Point2D.Double[] corners;
    int cornersSize; 
    Polygon(int n) {
	corners = new Point2D.Double[n];
	cornersSize = 0;
    }
    void add (Point2D.Double p) {
	corners[cornersSize] = p;
	cornersSize += 1;
    }
    void drawOnto(Graphics2D g2) {
	for (int i = 0; i < cornersSize; i++) {
	    Point2D.Double from = corners[i]; 
	    Point2D.Double to = corners[(i + 1) % cornersSize]; 
	    Line2D.Double side = new Line2D.Double(from, to); 
	    g2.draw(side); 
	} 
    }
} 

Looks like you're generating a new hexagon everytime you restart the applet, and paint paints it. Yes, but the hexagon takes care of its own painting.

The paint method simply gives it the keys, ... and the polygon draws itself on the context indicated by paint.

Well, I think arrays are nice. Isn't it a nuisance that their size is fixed, though?

Yes, unfortunately you need to specify the size of an array when an array is allocated, even though the actual size may not be known at that time. If you know that the array can never hold more than a certain number of elements,

... you can allocate a large array and partially fill it using a companion variable ... to remember how many elements are actually in the array.

You can also dynamically grow an array ... by allocating a larger array,

... shoveling the contents from the smaller in the larger array, ... and then attaching the larger array to the array variable.

This is tedious and repetitive code. The Vector class automates this process.

A vector is an "array" of objects that grows automatically. You add new elements at the end of the vector with the add method.

The Vector class needs to be imported from the same package as the string tokenizer. The constructor of choice is the no-arg constructor, which gives you a vector of size 0 (zero).

As with arrays, vector positions start at 0. The number of elements currently stored in a vector is obtained by the size method.

Reading an element from a Vector is considerably more complicated. Yes, a Vector can hold objects of any type at all.

A Vector collects values of type Object, and all Java classes are subclasses of the generic class Object. When you insert an element into a vector with the add or set method, the object reference is automatically converted to a plain Object reference.

That means, though, that you get only Object references when you retrieve objects from a vector, no matter what you put in. To recover the original type that you have put in the vector you need to cast back to it,

... in the exact same way that we recover the Graphics2D that the paint methods in an applet actually receives from the system. A good knowledge of inheritance would come in handy now.

I know... So, let's look at an example instead!
import java.util.*; 

class Product {
    String name;
    double score; 
    double price;
    Product(String n, double s, double p) {
	name = n; score = s; price = p; 
    } 
} 

class Two {
    public static void main(String[] args) {
        System.out.println(
          "Hello, and welcome to the evaluator. Type quit to quit."); 
        ConsoleReader console = new ConsoleReader(System.in); 
	Vector data = new Vector(); 
	int dataSize = 0; 
        while (true) {
	    Product p = Two.readProduct(console, dataSize); 
            if (p == null) break; 
            data.add(p); 
	    dataSize += 1; 
        } 
        System.out.println("\n** Data has been entered now."); 
        System.out.println("** We need to process it."); 
	Product p = (Product)data.get(0); 
	double highest = p.score / p.price; 
	for (int i = 0; i < dataSize; i++) {
	    p = (Product)data.get(i); 
	    if (highest < p.score / p.price) 
                highest = p.score / p.price; 
	} 
        System.out.println("** We then need to print the results."); 
	for (int i = 0; i < dataSize; i++) {
	    Two.printProduct((Product)data.get(i), highest); 
	}
        System.out.println("\nThank you for using our program!"); 
    } 

    public static Product readProduct(ConsoleReader console, int rank) {
	System.out.print("[" + rank + "]% ");
	String line = console.readLine(); 
        if (line == null || line.equals("quit")) return null; 
        StringTokenizer tokenizer = new StringTokenizer(line); 
	String name = tokenizer.nextToken();
	double score = Double.parseDouble(tokenizer.nextToken());
	double price = Double.parseDouble(tokenizer.nextToken());
	return new Product(name, score, price); 
    } 

    public static void printProduct(Product p, double highest) {
	System.out.print(p.name + " " + p.score + " " + p.price);
	if (highest == p.score / p.price)
	    System.out.println(" ** ");
	else System.out.println();
    } 
} 

I see the vector related statements are marked. Yes, and notice that the helper methods are unchanged.

dataSize is used just as an ornament. Yes, and tomorrow we will implement our own Vector class!

I can hardly wait. I can see that.

Last updated: August 1, 2000 by Adrian German for A201