CISC 3115
Modern Programming Techniques
Lecture 4
Classes III: Class Variables and Method, Scoping, Arrays and Classes, Mutability


static — Class Members

Class Variables

We've seen that we have occasion to want a single copy of a variable for the entire class (rather than an instance variable that is created for each object we create). we now explore this in more detail.

The keyword static specifies that the declared variable is a class variable i.e., a single copy exists for the entire class.

Class Methods

Scoping Rules

scope: the places in a program where a variable can be referenced without qualification (i.e., just using the variable's name)
class C {
	void f1() {
		int i;		// local scope
		…
		System.out.println(i);
		…
	}

	void f2(int i) { 	// local scope (parameter)
		…
		System.out.println(i);
		…
	}

	void f3() {
		…
		System.out.println(i);
		…
	}

	int i;   // class scope
}
		

Data Access Within the Class

Summary: Scope of Fields (Class/Instance Methods/Variables/Constants)

Classes as Types

main and Applications

Reading in Objects

We're going to introduce a simple and opinionated technique for reading in objects from a text file. It will also give us an opportunity to use the keyword static for the first time in a context we can explain.

A read Method

The basic idea is to: Here is an example of such a method reading in data for a BankAccount object:
class BankAccount {
	…
	static BankAccount read(Scanner scanner) {
		if (!scanner.hasNextInt()) return null;		
		
		int id = scanner.nextInt();		
		int balance = scanner.nextInt();

		return new BankAccount(id, balance);
	}
	…
	private int id;
	private int balance;
}
Notes

Using the read Method

We can now read in objects using this method:
BankAccount bankAccount = BankAccount.read(scanner);
while (bankAccount != null) {
	System.out.println(ba);
	bankAccount = BankAccount.read(scanner);
Notes

The null value

Default Initialization of Variables

Another Example: A BankAccounts App

Assuming a BankAccount class with id and balance instance variables, and the (by now) usual toString and read methods, code an app that reads in and processes multiple bank accounts:
class BankAccount {
	BankAccount(int id) {this.id = id;}
	public void deposit(int amount) {balance += amount;}
	public void withdraw(int amount) {balance -= amount;}
	
	public int getBalance() {return balance;}
	public int getId() {return id;}

	public String toString() {return "account #"+ id + " balance: $" + balance;}

	private int balance = 0;
	private int id;
}

import java.io.File;
import java.util.Scanner;

class BankAccountsApp {
	public static void main(String [] args) throws Exception {
		Scanner scanner = new Scanner(new File("bank_actions.text"));

		final int CAPACITY = 100;
		BankAccount [] accounts = new BankAccount[CAPACITY];
		int size = 0;

		while (scanner.hasNextInt()) {
			int id = scanner.nextInt();
			String action = scanner.next();
			int amount = scanner.nextInt();

			BankAccount account = search(accounts, size, id);
			if (account == null) {
				account = new BankAccount(id);
				size = add(accounts, size, account, CAPACITY);
				System.out.println("Added " + account);
			}

			switch (action) {
				case "D":
					account.deposit(amount);
					System.out.println("After deposit of $" + amount + ": " + account);
					break;
				case "W":
					account.withdraw(amount);
					System.out.println("After withdrawal of $" + amount + ": " + account);
					break;
				default:
					System.out.println("*** Error *** unknown action: " + action);
			}
		}
		System.out.println();
		System.out.println("=== Accounts ===");
		print(accounts, size);
	}	

	public static BankAccount search(BankAccount [] accounts, int size, int id) {
		for (int i = 0; i < size; i++) 
			if (accounts[i].getId() == id) return accounts[i];
		return null;
	}

	public static int add(BankAccount [] accounts, int size, BankAccount account, int capacity) {
		if (size == capacity) {
			System.err.println("Capacity reached; make arrays larger");
			System.exit(1);
		}
		accounts[size] = account;
		return size+1;
	}
		
	public static void print(BankAccount [] accounts, int size) {
		for (int i = 0; i < size; i++)
			System.out.println(accounts[i].getId() + ": $" + accounts[i].getBalance());
	}

}

1001 D 200
1030 D 100
1030 W 50
1001 W 70
Added account #1001 balance: $0
After deposit of $200: account #1001 balance: $200
Added account #1030 balance: $0
After deposit of $100: account #1030 balance: $100
After withdrawal of $50: account #1030 balance: $50
After withdrawal of $70: account #1001 balance: $130

=== Accounts ===
1001: $130
1030: $50

Arrays of Objects

As mentioned above, a class definition introduces a new type into the system, whose name is that of the class. Just as we would with any other type, we may have occasion to declare an array of instances of a class.

Declaring an Array of Objects

Creating an Array of Objects

Analogous to creating an object of a class, creating an array object involves the new operator:

String [] stringArr = new String[20];	// Creates an array of 20 String references and assign the 
								//	reference to the array object to stringArr
Name [] nameArr = new Name[100];		// Creates an array of 100 Name references and assigns the 
								//	reference to the array object to nameArr

Populating the Array with Instances of the Class

Once the array reference has been assigned a reference to an array object, the elements (which are only reference locations) of the array can be populated by creating instances of the class and assigning their references to the elements of the array:

final int CAPACITY = 100;
Name [] names = new Name[CAPACITY];

names[0] = new Name("Gerald", "Weiss");
names[1] = new Name("David", "Arnow");
names[2] = new Name("Yedidyah", "Langsam");
…

or more typically, the contents of the array would be read in from a file using a read method, which reads the data from the file, creates the object (which is initialized via the constructor being passed the data), and returns its reference:

final int CAPACITY = 100;
Name [] names = new Name[CAPACITY];
int size = 0;

Scanner scanner = new Scanner(new File("names.text"));

Name name = Name.read(scanner);		
while (name != null) {
	if (size >= CAPACITY) {
		System.out.println("Too many names in file -- increase the size of your array");
		System.exit(1);
	}
	names[size++] = name;
	name = Name.read(scanner);		
}

Working With Arrays

Once the array has been populated, you use it as you would any other array:
void print(Name [] names, int size) {
	for (int i = 0; i < size, i++)
		System.out.println(names[i] + (i < size-1 ? ", " : ""));
}

boolean contains(Name [] names, int size, Name name) {
	for (int i = 0; i < size, i++)
		if (names[i].equals(name)) return true;
	return false;
}

Arrays as Instance Variables (Arrays in Objects)

We've seen that a class can be used as the element type of an array. We can also have an array be an instance variable of an object.
class FootballTeam {
	…
	private Player [] offensiveSquad = new Player[11];
	private Player [] defensiveSquad = new Player[11];
	private Player [] specialSquad = new Player[3];
}

class Arr {
	…
	private int [] arr = new int[100];
	private int size = 0;
}

From a BankAccountsApp to a BankAccounts Class

We now restructure our application that worked with an array of bank accounts into a true object class that has an array of bank accounts as an instance variable.

The BankAccounts Class

import java.io.File;
import java.util.Scanner;

class BankAccounts {
	public BankAccount search(int id) {
		for (int i = 0; i < size; i++) 
			if (accounts[i].getId() == id) return accounts[i];
		return null;
	}

	public void add(BankAccount account) {
		if (size == CAPACITY) {
			System.err.println("Capacity reached; make arrays larger");
			System.exit(1);
		}
		accounts[size] = account;
		size++;
	}
		
	public String toString() {
		String result = "";
		for (int i = 0; i < size; i++)
			result += accounts[i] + "\n";
		return result;
	}

	private static final int CAPACITY = 100;
	private BankAccount [] accounts = new BankAccount[CAPACITY];
	private int size = 0;
}

The App

import java.io.File;
import java.util.Scanner;

class BankAccountsApp {
	public static void main(String [] args) throws Exception {
		Scanner scanner = new Scanner(new File("bank_actions.text"));

		BankAccounts accounts = new BankAccounts();

		while (scanner.hasNextInt()) {
			int id = scanner.nextInt();
			String action = scanner.next();
			int amount = scanner.nextInt();

			BankAccount account = accounts.find(id);
			if (account == null) {
				account = new BankAccount();
				account.setId(id);
				accounts.add(account);
				System.out.println("Added " + account);
			}

			switch (action) {
				case "D":
					account.deposit(amount);
					System.out.println("After deposit of $" + amount + ": " + account);
					break;
				case "W":
					account.withdraw(amount);
					System.out.println("After withdrawal of $" + amount + ": " + account);
					break;
				default:
					System.out.println("*** Error *** unknown action: " + action);
			}
		}
	}	
}

Another Example: A Phonebook (Phonebook)

Given the file, "phonebook.text" containing phone entries in the format last-name phone-number, write an application that allows the user to look up a phone number via the last name. You can assume no two entries have the same last name.

An Overview

This example requires an array (container) of objects comprising the multiple entries of a phonebook. What follows is a standard way of structuring and implementing such a situation. The will be three classes:

PhonebookEntry

class PhonebookEntry {
	PhonebookEntry(String name, String number) {
		this.name = name;
		this.number = number;
	}

	String getName() {return name;}
	String getNumber() {return number;}

	public String toString() {return name + ": " + number;}

	public static PhonebookEntry read(Scanner scanner) {
		if (!scanner.hasNext()) return null;
		String name = scanner.next();
		String number = scanner.next();
		return new PhonebookEntry(name, number);
	}

	String name;
	String number;
}

Phonebook

import java.io.*;
import java.util.*;

public class Phonebook {
	public static Phonebook read(Scanner scanner)  {
		Phonebook phonebook = new Phonebook();
		PhonebookEntry entry = PhonebookEntry.read(scanner);
		while (entry != null) {
			phonebook.add(entry);
			entry = PhonebookEntry.read(scanner);
		}
		return phonebook;
	}

	public void add(PhonebookEntry entry) {
		if (size == CAPACITY) {
			System.out.println("Phonebook capacity exceeded - increase size of underlying array");
			System.exit(1);
		}
		entries[size] = entry;
		size++;
	}

	public String lookup(String name) {
		for (int i = 0; i < size; i++)
			if (entries[i].getName().equals(name)) return entries[i].getNumber();
		return null;
	}

	public String toString() {
		String result = "{";
		for (int i = 0; i < size; i++)
			result += entries[i].getName() + ":" + entries[i].getNumber() + (i < size-1 ? ", " : "");
		result += "}";
		return result;
	}

	private static final int CAPACITY = 100;
	private PhonebookEntry [] entries = new PhonebookEntry[CAPACITY];
	private int size = 0;
}

PhonebookApp

import java.io.*;
import java.util.*;

public class PhonebookApp {
	public static void main(String [] args) throws Exception {
		final String FILENAME = "phonebook.text";

		Scanner scanner = new Scanner(new File(FILENAME));

		Phonebook phonebook = Phonebook.read(scanner);

		System.out.println(phonebook);
		System.out.println();

		Scanner keyboard = new Scanner(System.in);

		System.out.print("name? ");
		while (keyboard.hasNext()) {
			String name = keyboard.next();
			String number = phonebook.lookup(name);
			if (number != null) 
				System.out.println(name + ": " + number);
			else
				System.out.println(name + ": *** not found");
			System.out.print("name? ");
		}
	}
}

Sample Test Run

Arnow       123-456-7890
Harrow      234-567-8901
Jones       345-678-9012
Augenstein  456-789-0123
Sokol       567-890-1234
Tenenbaum   678-901-2345
Weiss       789-012-3456
Cox         890-123-4567
Langsam     901-234-5678
Thurm       012-345-6789

Here is a sample execution of the program.
User input is in bold.

{Arnow:123-456-7890, Harrow:234-567-8901, Jones:345-678-9012, Augenstein:456-789-0123, Sokol:567-890-1234, Tenenbaum:678-901-2345, Weiss:789-012-3456, Cox:890-123-4567, Langsam:901-234-5678, Thurm:012-345-6789}

name? Weiss
Weiss: 789-012-3456
name? Arnow
Arnow: 123-456-7890
name? Washington
Washington: *** not found
name? Jones
Jones: 345-678-9012
name? Thrum
Thrum: *** not found
name? Thurm
Thurm: 012-345-6789
name?

An Array Class

We'll now present a more 'complete' class that operates like an array. Create a class that encapsulates (contains as private data and thus protects) the data needed to maintain an array: This is our first example of a container or collection class.

We lose some things by moving from an array to a class:

We gain quite a bit, though

Modelling Things with Several (Logically-Related) Elements: A Container (02-Container)

Write an app that reads values from a file and places them into a container, i.e., a data entity that contains other items of data. The methods to be provided are:

class Container {
	public int add(int value) {
		values[size] = value;
		size++;
		return size;
	}

	public int size() {return size;}

	public boolean isEmpty() {return size() == 0;}

	public int find(int value) {
		for (int i = 0; i < size; i++)
			if (values[i] == value) return i;
		return -1;
	}

	public int get(int index) {return values[index];}

	public String toString() {
		String result = "{";
		for (int i = 0; i < size; i++)
			result += values[i] + (i < size-1 ? ", " : "");
		return result + "}";
	}
	
	private static final int CAPACITY = 100;
	private int [] values = new int[CAPACITY];
	private int size = 0;
}
Notes

Summary

Notes

import java.io.File;
import java.util.Scanner;

class ContainerApp {
	public static void main(String [] args) throws Exception {
		Scanner scanner = new Scanner(new File("container_actions.text"));

		Container container = new Container();

		while (scanner.hasNext()) {
			String action = scanner.next();
			int value;
			int index;

			switch (action) {
				case "A":
					value = scanner.nextInt();
					container.add(value); 
					System.out.print("After adding " + value + ": " + container);
					break;

				case "F":
					value = scanner.nextInt();
					int pos = container.find(value);
					if (pos >= 0) 
						System.out.println(value + " is at position " + pos + " of the container");
					else
						System.out.println(value + " is not in the container");
					break;

				case "G":
					index = scanner.nextInt();
					value = container.get(index);
					System.out.println("The value at location " + index + " is " + value);
					break;

				default:
					System.out.println("*** Error *** unknown action: " + action);
			}
		}
	}	
}

Responsibility-Driven Programming

We want the object to be responsible for its own behavior and integrity.

Basic Class Design

A Taxonomy of Classes

Nested Classes and Static Nested Classes

class Phonebook {
	static class PhonebookEntry {
		…
	}
	…
}

Mutable vs Immutable Classes

Some classes are deliberately designed so that their objects — once initialized — cannot be modified

Immutable Classes

class ImmutableColor {
	public ImmutableColor(int r, int g, int b) {
		this.r = r;
		this.g = g;
		this.b = b;
	}
	…
	public ImmutableColor lighter() {
		if (r < 255 && g < 255 && b < 255) 
			return new ImmutableColor(r+1, g+1, b+1)
		else
			return null;	
	}

	private final int r, g, b;
}
Notes

Mutable Classes

class MutableColor {
	public MutableColor(int r, int g, int b) {
		this.r = r;
		this.g = g;
		this.b = b;
	}
	…
	public MutableColor makeLighter() {
		if (r < 255 && g < 255 && b < 255)  {
			r++;
			g++;
			b++
			return this;
		}
		else
			return null;	
	}

	private int r, g, b;
}
Notes

Criteria for Mutability/Immutability

Files Used in This Lecture