|
CSCI A201/A597 and I210
Lecture Notes Sixteen
Second semester 2000-2001
|
The reading assignment for this is 270-291, and 298-301.
|
Diagrams like the ones presented in the previous set of lecture notes are important.
|
They can help you understand what's happening inside a program when it's compiled and run.
|
|
You should not expect to use diagrams all the time.
|
Their purpose is mostly to help you understand the concepts, the basics, by providing a
very detailed picture, as if under a microscope.
|
|
Once you understand those, you're all set.
|
And you can think Java without drawing diagrams. You'd be manipulating them in your mind, almost without knowing it.
|
|
Today in class we'll finish lecture notes of Tuesday and then start chapter 7.
|
We'll finish chapter 7 quickly, although there will be one
important new thing we will touch on.
|
|
Recursion.
|
The saying goes that "to understand recursion you first need to
understand recursion".
|
|
That's just a humorous saying. Recursion, in fact, is easy, and
thoroughly useful.
|
Once you have a fixed point. (But we'll get to that shortly.)
|
|
We'll also use this time to tie some loose ends.
|
For example the condition that expresses the definition of a bad move in Nim.
|
|
Yes, it was expressed as follows:
| |
if (number <= 0 || ((number > height / 2) && (number != 1))) {
System.out.println("***Bad move: you lose.");
System.exit(0);
} else {
| |
Or, just the condition, again:
|
(number <= 0 || ((number > height / 2) && (number != 1)))
|
Especially the last part was tricky.
|
Using de Morgan's law we can reformulate this to represent a good move.
|
|
Yes, we write down its negation.
|
Here it is (a good move):
|
(number > 0 && ((number <= height / 2) || (number == 1)))
|
Even if this is no easier to read than the first version you now have a choice.
|
To me this looks better, easier to understand, clearer.
|
|
To me too.
| Now the last loose end before we
jump into methods (chapter 7). |
|
Here's a program:
| What does it do? |
class Show {
public static void main(String[] args) {
String a = "class Show {\n public static void ";
String b = "main(String[] args) {\n\n\n }\n}";
System.out.println(a + b);
}
}
|
It prints part of itself.
|
Can you write a program that outputs itself, that is,
that prints its source code exactly when run?
|
|
That will be the challenge for the remaining of the semester.
|
Turn that in before the last lecture for big bonus.
|
|
Can I write the program with what I have?
|
Yes. The solution that will be shown in class will be
using for loops, characters, and Strings.
|
|
All right, I'll work on that.
|
Now let's do chapter 7.
|
|
Methods.
|
You have already implemented several simple methods
and are familiar with the basic concepts.
|
|
Let's go over parameters, return values, and variable scope
in a more systematic fashion.
|
We will also review some of the more technical issues, such as
static methods and variables.
|
|
When we implement a method we define the parameters of the method.
|
|
public class BankAccount {
...
public void deposit(double amount) {
...
}
..
}
The deposit method has two parameters: one is explicit,
called amount, and has type double.
|
We expect to receive a value in it to be deposited.
|
The other one is implicit, and can be referred to by the
the keywqord this.
|
What we mean is that inside an instance method (like deposit)
the keyword this will always refer to the host object.
|
So if it's always available and always in the same way, the this
reference is not even mentioned in the list of parameters.
|
But from any instance method we can use the keyword this to refer to
the object that contains the method.
|
Therefore amount is called a formal
parameter for the method deposit.
|
When we want to deposit some money we need to know two things:
|
|
a) the account's name, and
|
b) the amount of money
|
|
... and we need to actually invoke the method,
|
... in order to deposit the money.
|
// somewhere in main (or another method)
myChecking.deposit(allowance - 200);
In this example myChecking is an object of type BankAccount
|
... and allowance is probably a double.
|
|
Both should be declared and initialized before we use them.
|
When deposit starts running the value of the expression
allowance - 200
|
|
... becomes the actual parameter or argument to the method,
| ... and will be known by the name of amount while the
method is running. |
|
When the method returns the formal
parameter variables are abandoned, and their values are lost.
|
The entire process is like a phone call: you call
myChecking's deposit method...
|
... and you give it some input: thye amount that you
want to deposit.
|
It then starts working for you and you stay on the line.
|
|
When it's done it says so, before you hang up.
|
Sometimes it returns a value
before the two of you can end the conversation.
|
deposit only says when it's done, without
returning anything. It is declared as void.
|
void is its return type.
|
|
Yes: it does not return anything to its caller.
|
Hence: void.
|
|
It only does what it is supposed to do, and then it says:
"I'm done."
|
Which is also called returning except not as in "returning a value",...
|
|
... rather, like in "returning from a trip".
|
A trip to the bank.
|
|
Explicit parameter variables are no different from other variables.
|
You can modify them during the execution of a method: but unless you have a good reason for
that it is considered bad style.
|
|
Let's now consider a more complicated example:
| |
public class BankAccount {
...
public void transfer(BankAccount other, double amount) {
withdraw(amount);
other.deposit(amount);
}
...
}
|
|
This method can be used to transfer money from
one account to another.
|
|
Here's how we can use it:
| |
momsSavings.transfer(myChecking, allowance);
| |
I have one question before we go further though...
|
|
Yes, what is it?
|
Weren't we supposed to write
this.withdraw(amount)
in the definition of the transfer method?
|
|
Yes, and please make the correction in your notes.
|
OK, I've updated mine.
|
|
But why does it work though?
|
Because it defaults to it, anyway.
|
I think that using this makes it more uniform and explicit.
|
And I think so too.
|
|
How many formal parameters does this new function (method) have?
|
Two of them are explicit: other and amount.
|
The first one is a BankAccount.
|
The second one is a double.
|
And in addition to that the method
will be able to access the object to which it belongs using this. |
Yes, this is always available in an instance
method and means: "the object that contains this method", or "this
method's host". |
|
What happens when the method is invoked?
|
When the method is invoked the reference to myChecking is
copied into the method's other formal parameter...
|
... and allowance will be copied into amount.
|
Note that both the object references and the numbers are copied into the method.
|
|
After the method exits the two bank account balances have changed.
|
The method was able to change the accounts because it received
copies of the object references.
|
Of course, the contents of the allowance variable was not changed.
|
In Java no method can modify the contents of a number
variable that is passed as a parameter.
|
|
What names should we give to the parameters?
|
You can give any names you want to method parameters.
|
|
Choose explicit names for parameters that have
specific roles; choose simple names for those that
are completely generic.
|
The goal is to make the reader understand the purpose of the
parameter without having to read the method's description.
|
|
The compiler takes the types of the
method parameters and return values
very seriously.
|
It is an error to call a method with
a value of incompatible type, ...
|
|
... or use it in a context that is
not compatible with its return type (if any).
|
Java is a strongly typed language.
|
The compiler automatically converts from int to double
and from ordinary classes to superclasses (see chapter 9).
|
It does not convert, however, when
there is a possibility of information
loss...
|
|
... and does not convert between numbers and strings and objects.
|
This is a useful feature, because it lets the compiler
find programming errors before they create havoc when the
program runs.
|
|
A method that accesses an object and returns some
information about it, without changing the object,
is called an accessor method.
|
Such as getBalance.
|
|
In contrast, a method that modifies the state of an
object is called a mutator method.
|
deposit and withdraw are mutator methods.
|
|
You can call an accessor method
|
... as many times as you like.
|
|
If that's all you do, you will always get the same answer, and it does not change the state of the object.
|
Some classes have been designed such that objects of that kind have only accessor methods and no mutators at all.
|
|
Such classes are called immutable.
|
An example is the String class.
|
Once a String has been constructed, its contents
never change.
|
For example, the substring method
does not remove characters from the original string.
|
|
Instead it constructs a new string that contains the
substring characters.
|
Here's another example of an accessor method that
simultaneously looks at two objects:
|
public class BankAccount {
public boolean equals(BankAccount other) {
return (this.getBalance() == other.getBalance());
}
}
|
It makes use of two other accessors (or, rather,
the same accessor invoked on two different objects)
and compares the values that they return.
|
It returns the truth value that comes out
of the comparison.
|
|
So we could use it as follows:
|
Very good.
|
if (account1.equals(account2)) {
// they have the same balance...
} else {
// they do not have the same balance...
}
|
In general the expectation is that accessor methods
do not modify any parameters, ...
|
... and that mutator methods
do not modify any parameters beyond this.
|
|
This ideal situation is not always entirely the case.
|
For example the transfer method discussed before.
|
It changes its this, ...
|
... while also updating the other account.
|
|
Such a method is said to have a side effect.
|
A side effect of a method is any kind of
observable behaviour outside the object.
|
|
In an ideal world, all methods would be accessors that simply return an answer
without changing any value at all.
|
In fact, programs that are written in so-called functional
programming languages,
|
|
... such as Scheme or ML,
|
... come close to this ideal.
|
|
Of course, in an object oriented programming language,
we use objects to remember state changes.
|
Therefore, a method that just changes the state of its implicit parameter
is certainly acceptable.
|
|
A method that does anything else is said to have a side effect.
|
While side-effects cannot be completely eliminated, they can
be the cause of surprises and problems and should be minimized.
|
|
Sometimes you write methods that don't belong
to any particular object.
|
Such a method is called a static method or a
class method and needs to be declared as static.
|
In contrast, methods such as getBalance, withdraw, and
deposit in the preceding sections are often called instance
methods,
|
... because they operate on particular instances of an object.
|
|
There's one of each in each object (of that type) that gets created.
|
Math.sqrt is a static method.
|
And every application must have a static method called
|
... main.
|
|
Correct. Here's another example, that involves only numbers:
| |
class NumericMethods {
public static boolean approxEqual (double x, double y) {
final double EPSILON = 1E-14;
double xymax = Math.max(Math.abs(x), Math.abs(y));
return Math.abs(x - y) <= EPSILON * xymax;
}
// more numeric methods could come here...
}
| |
This method encapsulates computation that involves no objects at
all, only numbers (and booleans), hence only primitive
types.
|
|
To call (or use) a static method you need to supply
the name of the class, for example:
| |
double r = Math.sqrt(2);
if (NumericMethods.approxEqual(r * r, 2))
System.out.println("Math.sqrt(2) is approx. 2");
| |
... same as we do with Math.sqrt.
|
Now we can tell you why the main method is static:
|
... when the program starts there may not be any object at all.
|
|
Therefore the first method in a program must be a static method.
|
Good enough.
|
|
To summarize our knowledge about static methods we can say that...
|
... a static method is a method that does not belong to any object, and that has only explicit parameters.
|
|
Let's look at some examples now.
| What does the following example illustrate? |
public class Example {
public static void addOneToIt (int number) {
System.out.println(number);
number = number + 1;
System.out.println(number);
}
public static void main(String[] args) {
int value = 3;
System.out.println(value);
Example.addOneToIt(value);
System.out.println(value);
}
}
|
Let's walk through the method call.
|
When the call is made the parameter value is set to the
same value as the argument.
|
|
The value is copied.
|
Changes to it are not seen outside.
|
|
That's all there is to it.
|
Easy.
|
|
Is there a moral to it?
|
In Java method parameters are copied into the parameter variables when a method starts.
|
|
Computer scientists call this call mechanism "call by value".
|
As you have seen there are some limitations to the "call by value" mechanism.
|
|
It is not possible to implement methods that modify the contents
of number variables.
|
Other programming languages support an alternate mechanism, called "by reference".
|
|
This involves passing only the address to where the number variable is stored.
|
This is what happens when you pass an object as an actual parameter.
|
|
Let's see an example.
|
Oh, boy. I like examples best.
|
class NumberHolder {
int value = 1;
}
class Example {
public static void main(String[] args) {
NumberHolder n = new NumberHolder();
System.out.println(n.value);
Example.addOneToIt(n);
System.out.println(n.value);
}
public static void addOneToIt (NumberHolder n) {
n.value = n.value + 1;
}
}
|
|
But we've seen this before, haven't we?
|
|
Yes, when we discussed copying of variables.
|
Primitive types are copied by value, while
reference types are copied by reference.
|
|
Good enough.
|
References though are still passed by value.
|
|
Understood. Can we see an example?
|
Oh boy -- that's what I like best.
|
class Pair {
double x;
double y;
Pair(double x, double y) {
this.x = x;
this.y = y;
}
void report() {
System.out.println("Hello! I'm at: (" + x + ", " + y + ")");
}
}
class Testing {
public static void main(String[] args) {
Pair a = new Pair(100, 0);
Pair b = new Pair(0, 100);
a.report();
b.report();
Testing.swap(a, b);
a.report();
b.report();
}
static void swap(Pair a, Pair b) {
Pair temp = a;
a = b;
b = temp;
}
}
|
I like it.
|
Easy and understandable. But it still gives you a level of indirection.
|
Yes. You can, at least in principle, get inside those Pairs.
|
Let's summarize: a Java method can update an object's state
|
|
... using the reference to it,
|
... but it cannot replace the contents
of an object reference.
|
|
This shows that object references are passed by value in Java.
|
Although we can safely say that objects themselves are passed
|
|
... by reference.
|
Exactly.
|
|
Except that the reference itself is copied.
|
Copied, yes -- but pointing to the same thing the original one was.
|
|
Fair enough.
|
The distinction is clear now.
|
A method that has a return type other than void
must return a value, by executing a statement of the form:
|
|
return <expression> ;
|
Yes, but let's see if we can come up with something new.
|
Well, for one thing, you can return the value of any expression.
|
|
You don't need to store the result in a variable and then return the variable.
|
When a return is processed, the method exits immediately.
|
|
This is convenient for handling exceptional cases in the beginning.
|
|
|
|
Oh, yes, here's an example:
|
public static int fibo (int n) {
if (n == 1)
return 1;
else if (n == 2)
return 1;
else {
int fOlder = 1;
int fOld = 1;
int result = fOld + fOlder;
for (int i = 3; i <= n; i++) {
result = fOld + fOlder;
fOlder = fOld;
fOld = result;
}
return result;
}
}
|
It is important that every branch of a method return a value.
|
Also, a method whose return type is not void always needs
to return a value.
|
If the method contains several if/else
branches make sure that each one of them returns a
value.
|
At the end of every possible path through a non-void method
there should be a return statement,
|
|
... returning the value of an expression of compatible type.
|
|
|
|
For example is this right?
|
public static int fibo (int n) {
if (n <= 0)
System.out.println("Incorrect argument!");
else if (n == 1)
return 1;
else if (n == 2)
return 1;
else {
int fOlder = 1;
int fOld = 1;
int result = fOld + fOlder;
for (int i = 3; i <= n; i++) {
result = fOld + fOlder;
fOlder = fOld;
fOld = result;
}
return result;
}
}
|
It is not, because if the argument is negative we don't return anything.
|
What should we return, then?
|
|
I don't know, what do you think of this one?
|
|
return Math.round(Math.pow((1 - Math.sqrt(5))/ 2, n))
Or we should throw an Exception.
|
Yes, but about those perhaps some other time...
|
|
We have now encountered the four kinds of variables that
Java supports.
|
- Instance variables
- Static variables
- Local variables
- Parameter variables
|
|
The lifetime of a variable defines when the variable is created and how long
it stays around.
|
When an object is constructed, all its instance variable are created.
|
|
As long as the object is around its instance variables are around, with the object, inside
the object.
|
A static variable is created when its class is first loaded,
and it lives as long as the class.
|
|
A local variable is created when the program enters
the statement that defines it.
|
It stays alive until the block that
encloses the variable definition is exited.
|
public void withdraw (double amount) {
if (amount <= balance) {
double newBalance = balance - amount;
// local variable newBalance created and initialized
balance = newBalance;
} // end of lifetime of local variable newBalance
}
| |
If you tried to print newBalance right before the end of the method
you'd get an error.
|
Yes, and the reason is: it's known only in the then branch of the
if statement.
|
Inside the inner pair of curly braces.
|
|
Finally, when a method is called, its parameter variables are
created.
|
They stay alive until the method returns to the caller.
|
|
Next, let us summarize what we know about the
initialization of these four types of
variables.
|
Instance variables and static variables are automatically initialized
with a default value...
|
... which is 0 for numbers, false
for boolean and null for objects,
|
Yes. So instance variables and static variables are automatically initialized
with a default value...
|
|
... unless you specify another initial value.
|
Very good.
|
|
Parameter variables are initialized with copies of the actual
parameters.
|
That's when the method gets called.
|
|
Local variables are not initialized by default.
|
For local variables you must supply an initial value, and the compiler
complains if you try to use a local variable that you never initialized.
|
|
The scope of a variable is that part of a program
that can access it.
|
The part of the program in which you can access it, the variable, is
the scope of the variable, yes.
|
OK. As you know, instance and static variables are usually
declared as private, and you can access them only in
the methods of their class.
|
I see... Scope answers the question: can I see it?
|
|
The scope of a local variable extends from the point of its definition
to the end of the enclosing block.
|
The scope of a parameter variable is the entire body of its
method.
|
|
Now let's look a bit closer to a few situations.
|
We're going to go through a few examples.
|
|
It sometimes happen that the same variable name is used in two methods:
| |
public static double area(Rectangle rect) {
double r = rect.getWidth() * rect.getHeight();
return r;
}
public static void main(String[] args) {
Rectangle r = new Rectangle(5, 10, 20, 30);
double a = area(r);
...
}
| |
These variables (the two r's) are independent of each other.
|
You can have variables with the same name r in different methods,
|
... just as you can have different motels with the same name
(let's say, "Super 8") in different cities.
|
|
In this situation the scopes of the two variables are disjoint.
|
Problems arise, however, if you have two or more variable names
with overlapping scope.
|
|
Like when you have two Kroger's in the same city.
|
Almost, but not exactly. In Java this situation is called
shadowing.
|
|
There are rules in the language that tell you which one of the
variables you will be referring to if you use the ambiguous name.
|
Can we see some examples?
|
class Employee {
String name;
Employee (String name) {
this.name = name;
// this is mandatory not just good style here!!
}
}
| |
The parameter, which is like a local variable, shadows the instance variable.
|
|
The Java language specifies that when there is a conflict
between a local variable name and an instance variable name the
local variable wins out.
| This sounds pretty arbitrary
but there is actually a good reason. |
You can still refer to the instance variable using this
|
Which you should do anyway.
|
|
Do you have any questions?
|
No, but I have something close to that.
|
class Puzzle {
public static void main(String[] args) {
Puzzle p = new Puzzle();
System.out.println("Final result: " + p.fun(6));
}
int fun(int n) {
int result;
if (n == 0) return 0;
else {
// [1]
result = n + fun(n - 1);
// [2]
return result;
}
}
}
|
Neat. What do we do with it?
|
Well, what's the program computing?
|
Once you figure that out replace the first comment with
a println statement that prints the variable
n. |
Explain the change in output.
|
Then go back to the original code, and replace the second comment with the
println statement that prints the variable
n.
|
Explain the change in output.
|
|
Then notice the name of the function.
|
I know, I know, this was a lot of fun...
|
Last updated: Mar 1, 2001 by Adrian German for A201