To represent a "person", in a Java program, we might want to keep track of these three items
The first two could be represented by two Java String variables and the last one by a Java int.
That's three variables to represent one person.
By defining a Java class, Person, we can create one variable to represent a Person.
Syntax:
public class Person { }
Each instance of the Person class will have its own first and last name, and age.
So these members should not be static. (Why not?)
We must also decide whether the three data members should be directly accessible to any code that uses the Person type.
As we will begin to see, it is generally more flexible to not allow direct access to the data members.
Instead, it is a better design to allow controlled access to the data members through methods.
A member (data or method) is directly accessible by a user of the class type if it has the public qualifier.
A member is not directly accessible by a user of the class type if it has the private qualifier.
Note that a method in the Person class, does have direct access to all members of any Person variable it can reference; for example, to any Person variable passed to it as a parameter.
public class Person { private String fname; private String lname; private int age; }
Since all the data members have been made private, public methods are needed to get and/or change the values of these private members.
We can define a public "getXXX" method to return the value of private member XXX for each different private member.
These "get" methods are examples of accessor methods. Their only purpose is to provide possibly controlled access to the value of the private data members.
The "get" methods shouldn't change the private data member, just report its current value.
On the other hand, we can define a public "setXXX" method to change the value of private member XXX if we decide this is to be allowed.
public class Person { private String fname; private String lname; private int age; public String getFname() { return fname; } public String getLname() { return lname; } public int getAge() { return age; } public void setAge(int ageVal) { age = ageVal; } }
A constructor is a special class member whose purpose is to initialize the class data members.
A constructor syntax is almost like a method, but
Example:
public class Person { // Private data members private String fname; private String lname; private int age; // A Constructor public Person(String fnameVal, String lnameVal, int ageVal) { fname = fnameVal; lname = lnameVal; age = ageVal; } // Accessor methods as before ... }
A constructor is typically invoked as part of the new operator.
It is the new operator that actually allocates storage for an instance, and immediately after that, the constructor code is invoked to initialzed the newly allocated instance.
Person p = new Person("Fred", "Flintstone", 31);
The Person class doesn't need a main method, but in that case, the Person class cannot be executed.
This makes sense if you think of Person as a type.
To write a program using the Person type, we should write a different application class that uses the Person type.
Here is a very simple example:
public class PersonApp { public static void printPerson(Person p) { System.out.println("First name: " + p.getFname()); System.out.println("Last name: " + p.getLname()); System.out.println("Age: " + p.getAge()); } public static void main(String[] args) { Person p; String fval, lval; int a; Scanner infile = new Scanner(System.in); System.out.print("First Name: "); fval = infile.next(); System.out.print("Last Name: "); lval = infile.next(); System.out.print("Age: "); a = infile.nextInt(); p = new Person(fval, lval, a); System.out.println("\nPerson created:"); printPerson(p); } }
A constructor that takes no arguments is often called the default constructor.
If you write NO constructors, the Java compiler implicitly creates one for you that initializes each data member with the default value for that data member's type.
Type | Default Value |
---|---|
int | 0 |
double | 0.0 |
boolean | false |
char | '\0' (all bits are 0's) |
any class type | null |
For example, if no constructors were written for the Person class, then we could still create and print an instance
Person p = new Person(); System.out.println("First Name: " + p.getFname()); System.out.println("Last Name: " + p.getLname()); System.out.println("Age: " + p.getAge()); // Output First name: null Last name: null Age: 0
One can sometimes avoid having to make special cases for null values, etc, by always providing your own default constructor rather than relying on the one the compiler may implicitly provide.
For example, this code assumes no constructors were written and relies on the compiler default constructor:
... 33 System.out.println("\nPerson created:"); 34 System.out.println("First Name: " + p.getFname()); 35 System.out.println("Last Name: " + p.getLname()); 36 System.out.println("Age: " + p.getAge()); 37 38 int lastnameLength = p.getLname().length(); ... // Output Person created: First name: null Last name: null Age: 0 Exception in thread "main" java.lang.NullPointerException at PersonApp.main(PersonApp.java:38)
The problem is that p.getLname() has value null. So p.getLname().length() evaluates from left to right as
p.getLname().length() is equal to null.length() That is, the String instance to apply the length() method to is referenced by null.
Using null to invoke any instance method will throw the NullPointerException since null indicates NO actual instance is being referenced.
public Person() { fname = ""; lname = ""; age = 0; }
With this default constructor instead of the compiler supplied default, no exception is thrown for the previous example:
... 33 System.out.println("\nPerson created:"); 34 System.out.println("First Name: " + p.getFname()); 35 System.out.println("Last Name: " + p.getLname()); 36 System.out.println("Age: " + p.getAge()); 37 38 int lastnameLength = p.getLname().length(); 39 System.out.println("Last Name Length: " + lastnameLength); // Output Person created: First name: Last name: Age: 0 Last Name Length: 0
Sometimes it is important to be able to make copies of instances.
For example, you might want to provide a getXXX method to allow users of a class to access some private member, but you don't want them to be able to change that member.
To accomplish this goal you could simply not provide a setXXX method.
However, that may not be enough if the member is of class type.
public class Project { private Person leader; private int projNumber; public Project(Person p, int proj) { leader = p; projNumber = proj; } public Person getLeader() { return leader; } public int getProjNumber() { return projNumber; } }
Since there is no setLeader(Person newLeader) method, it might appear at first that a Project leader member can't be changed after the Project instance is created.
However, the following code creates a Project and then makes the leader be 3 years younger.
1 public static void main(String[] args) 2 { 3 Project prj = new Project(new Person("Tristram", "Shandy", 30), 101); 4 Person p; 5 6 p = prj.getLeader(); 7 p.setAge(27); 8 } Note: At line 7, p is a reference to prj's leader member. and so that leader's age is changed to be 27.
So if omitting setXXX accessors doesn't prevent changes to data members, how do we prevent changes while providing getXXX access?
One way is to return a copy of the member rather than the member itself.
That is, the getXXX method can create a new copy of the data member and return the copy.
Creating a copy of a class type is easy if the class has a copy constructor.
A copy constructor for a class X is just a constructor whose parameter is also of the same type X.
Here is an example of a copy constructor for the Person class:
public Person(Person other) { this.fname = other.fname; this.lname = other.lname; this.age = other.age; }
Once we have the copy constructor for the Person class, the getLeader method can easily make a defensive copy:
public class Project { private Person leader; ... public Person getLeader() { return leader; } private Person leader; ... public Person getLeader() { Person copy = new Person(leader); return copy; }
Now if an attempt is made to change the age of the leader:
Project prj = new Project(new Person("Tristram", "Shandy", 30), 101); Person p = prj.getLeader(); p.setAge(27);
The Person instance returned by getLeader() is only a copy of the leader and so only the copy has the age changed. The age of the leader member of the Project instance is not changed.
Recall that an instance member (method or data member) is accessed using the dot operator with this syntax:
instance.member
An expression can have more than one occurrence of the dot. The Person class example illustrates this:
Person p = new Person("Barney", "Rubble", 30); int n = p.getLname().length();
The dot operators should be evaluated left to right. At each evaluation the left operand of the dot should be a class instance and the right operand should be a member of that class.
p.getLname().length() Left-most dot: p.getLname() is evaluated first p is an instance of Person; getLname() is a method member of Person Result is an instance of String with value "Barney" Second dot: p.getLname().length() = "Barney".length() "Barney".length() is evaluated second "Barney" is an instance of String; length() is a method member of String Result is the int value 6 Note: There can't be another dot: p.getLname().length().____ since the expression to the left of the additional dot is an int rather than an instance of a class.
An instance method of a class can only be called using an actual instance and the dot operator.
So how does a method in a class know which instance was used to call it?
Answer:
Every instance method of any class X automatically has a variable named 'this' of type X which is a reference to the instance used to call the method.
For example, the accessor methods (get and set) of the Person class refer to the instance data members and so should in priniciple use the dot notation.
Here is the getAge() method which returns the age instance member:
public int getAge() { return this.age; } Note: this.age can be abbreviated to simply age. The compiler will check that age is a data member and implicitly interpret age as this.age.
Don't redeclare class instance members locally inside methods; the method can already access class data members.
1 public class Person 2 { 3 private String fname; 4 private String lname; 5 private int age; 6 7 public void setAge(int ageVal) 8 { 9 int age; // Redeclaration here! 10 age = ageVal; 11 } 12 13 } At line 10, the compiler sees 'age' and tries to match it with a declaration. The closest one is at line 9. So the local variable 'age' at line 9 is updated while the class instance member 'age' at line 5 is not changed. Furthermore, as soon as setAge returns, the local variable 'age' at line 9 is deallocated and its value is lost.
The correction is simply to not declare the local variable 'age':
1 public class Person 2 { 3 private String fname; 4 private String lname; 5 private int age; 6 7 public void setAge(int ageVal) 8 { 9 age = ageVal; 10 } 11 12 } Now: the compiler looks for a declaration of 'age' at line 9 and doesn't find a local declaration. So the compiler looks at the class members and finds 'age' at line 5 and interprets the occurrence of 'age' at line 9 to correspond to the class member.
Most classes will also need to write these methods
In addition, if there is some natural ordering of instances of a class X, the class will also likely benefit from implementing this method:
The equals method for a class X allows you to compare instances of X with other instances of X for equality, but also with instances of other classes as well.
Of course, if the other instance is not an instance of X, equals should return false.
The parameter type for equals is Object.
A variable of type Object can reference any class instance.
So how do you tell whether the Object parameter passed to equals is an instance of Person?
Answer: use the instanceof operator.
1 public boolean equals(Object obj) 2 { 3 if ( obj == this ) return true; 4 5 if ( obj instanceof Person ) { 6 Person other = (Person) obj; 7 return other.getLname().equals(this.getLname()) && 8 other.getFname().equals(this.getFname()); 9 } else { 10 return false; 11 } 12 } Notes: Line 3: If obj and this are referencing the same instance, then equals should be true Line 5: The left operand of instanceof should be an instance and the right operand should be a class name. The result is true if the referenced instance is an instance of the class; otherwise false. Line 6: It is necessary to declare a Person variable and assign obj to it with a cast so that the first and last names can be accessed. By line 5, we know that obj will reference a Person, but the compiler will still complain if we write obj.getLname(). The check with the instanceof operator guarantees that the cast on line 6 will succeed; that is, if line 6 is reached, the obj really does reference a Person instance.
The toString() method is used implicitly whenever you use System.out.print, System.out.println, or System.out.printf("%s", ...) to try to print a class instance. E.g.
Person p = new Person("Dino", "Flintstone", 2); System.out.println(p); Note: The print statement is equivalent to System.out.println(p.toString());
The String static method, format, is useful for writing toString() methods.
Here is one possible implementation of toString() for the Person class:
public String toString() { return String.format("First Name: %s, Last Name: %s, Age: %d", fname, lname, age); }
There are several possible ways to order instances of the Person class:
Just as you shouldn't use == to test for equality of class types, you shouldn't use <, or <=, or >, etc., to compare class instances.
Instead, a method should be used: compareTo.
Actually, compareTo, should be the name of a method that corresponds to the most natural ordering on the class instances if there are several possible orderings.
The meaning of compareTo should be
x.compareTo(y) return value | Meaning of the order of x and y |
---|---|
negative value | x is "smaller than" or "comes before" y |
0 | x is "equal" to y |
positive value | x is "greater than" or "comes after" y |
We will write a compareTo for the following ordering on instances of Person.
A Person p will come before Person q if p's last name comes alphabetically before q's. Or if the last names are the same, then p will come before q if p's first name comes before q's first name.
We will take advantage of the fact that the String class already defines its own compareTo method for Strings that follows the previous table description for compareTo methods.
public int compareTo(Person other) { if ( getLname().equals(other.getLname()) ) { return getFname().compareTo(other.getFname()); } else { return getLname().compareTo(other.getLname()); } }
UML includes several graphical notations for specifying classes and their members.
It also has notation for relationships that may exist among different classes.
Here is a UML class diagram for the Person class
Person |
---|
- fname: String - lname: String - age: int |
+ Person() + Person(String first, String last, int ag) + getFname: String + getLname: String + getAge: int + setFname(String first) + setLname(String last) + setAge(int ag) + toString: String + equals(Object obj): boolean + compareTo(Person p): int |
Notes:
Java already defines wrapper classes including:
The purpose of these classes is to provide a class type that can be used in place of the basic non class types: int, double, boolean, etc, when a class type is required..
It isn't hard to write one of these classes, and for practice with the uml notation, here is a class diagram for a class MyInteger. It is so named to distinguish it from the built in Java Integer class, but it is intended to be a similar wrapper class.
MyInteger |
---|
- value: int |
+ MyInteger(int n) $+ parseInt(String s): int + intValue(): int + equals: boolean + compareTo(MyInteger other): int |
public class MyInteger { private int value; public MyInteger(int n) { value = n; } public int intValue() { return value; } public static int parseInt(String s) throws NumberFormatException { return Integer.parseInt(s); } public boolean equals(Object obj) { if ( this == obj) return true; if ( obj instanceof MyInteger ) { MyInteger other = (MyIntger) obj; return other.value == this.value; } else { return false; } } public int compareTo(MyInteger m) { return this.value - m.value; } public String toString() { return String.format("%d", value); } }
The Java Vector class is similar to an array, but it grows automatically.
However, you can only put class types into a Vector, not basic types like int. That means Vector<int> is not allowed, but we can use Vector<MyInteger> as a replacement. This is where wrapper classes are useful.
1 import java.util.Vector; 2 3 public class MyIntegerApp 4 { 5 6 public static void main(String[] args) 7 { 8 Vector<MyInteger> v = new Vector<MyInteger>(); 9 10 for(int i = 0; i < 10; i++) { 11 v.add(new MyInteger(i+1)); 12 } 13 14 MyInteger n; 15 for(int k = 0; k < v.size(); k++) { 16 n = v.get(k); 17 System.out.println(n); 18 } 19 } 20 21 } // Output 1 2 3 4 5 6 7 8 9 10
Arrays are not basic types in Java. In fact, arrays are class types.
Like any class type, an array must be created using the new operator.
Syntax:
elementType[] variable variable = new elementType[integerExpression]
Here are some examples:
int[] a = new int[10]; double[] b = new double[15]; String[] c = new String[20]; a.length is 10 b.length is 15 c.length is 20
Note that the syntax is the same regardless of whether the element type is a basic type or a class type.
An array has a length property (instance member) whose value is the maximum number of elements that can be stored in the array.
Note that the length does not indicate how many elements have actually been stored in the array.
Consider the char data arranged in 5 rows and 6 columns:
p y r e d n i b a c i e n o l w n e k l y u g r w h i t e g
This two dimensional array contains the words:
red, pink, blue, green, and white
somewhere in the rows, columns, and/or diagonally, but possibly with the characters in reverse order.
How could we declare a two dimensional array, letters, to hold these characters?
char[][] letters; letters = new char[5][6]; or char[][] letters; letters = new char[5][]; for(int r = 0; r < 5; c++) { letters[r] = new char[6]; }
If only one subscript is used on letters, it indicates the entire row, which is itself an array.
char[][] letters = new char[5][6]; Then, letters.length = 5 letters[0].length = 6 letters[1].length = 6 letters[2].length = 6 letters[3].length = 6 letters[4].length = 6
The rationale for this is that a two dimensional array with 5 rows and 6 columns is represented in Java as a one dimensional array of length 5, but each element is another 1 dimensional array of length 6.
For a one dimensional array, we can declare it and initialize it at the same time.
Examples:
int[] ia = {10,5,3,9}; // ia.length is 4, ia[0] is 10 String[] sa = {"cat", "pig", "dog"}; // sa.length is 3, sa[2] is "dog"
Two dimensional arrays can be declared with an initialization in much the same way.
int[][] d = {{1,2,3},{4,5,6},{7,8,9},{10,11,12}}; // d.length is 4 (4 rows) // d[0].length = d[1].length = ... = d[3].length = 3 (each row has length 3) // Print the elements of d in 4 rows and 3 columns for(int r = 0; r < d.length; r++) { // d.length is 4 for(int c = 0; c < d[r].length; c++) { // note d[r].length == 3 System.out.printf("%4d", d[r][c]); } System.out.println(); } // Output is 1 2 3 4 5 6 7 8 9 10 11 12