public class Example {
int i; // Instance variable
public void exampleMethod() {
int i; // Local variable
i = 0; // uses the local variable
this.i = 10; // uses the instance variable
}
}
Here is another example:
public class Example {
int value;
public Example(int value) {
this.value = value; // assign the value of the local
// parameter to the instance variable
}
}
int i = 0;
int x = 0;
while (x < 10) {
int i; // ERROR: i already exists
.
.
x++;
}
However, Java variables (like C++) only exist in the block in which
they are declared. For example...
int x = 0;
while (x < 10) {
int i = 5;
.
.
x++;
}
x = i; // ERROR: i does not exist here
The same is true of for loops. Variables declared in the for statement
cannot have the same name as already existing variables and they only
exist for the lifetime of the loop.
int i = 5;
for (int i = 0; i < 10; i++) { // ERROR: i already exists
.
.
}
for (int i = 0; i < 10; i++) {
.
.
}
i = 5; // ERROR: i does not exist here
And since the for index is considered to only exist inside the loop,
the following is allowed in Java...
for (int i = 0; i < 10; i++) {
.
.
}
for (int i = 0; i < 10; i++) { // OK: the previous i is gone
.
.
}
Similar for the if statement.
We need to clarify each of these four stages.
The point to notice here is that expressions are never passed as
arguments in mainstream languages. They are always evaluated first,
and the result values are passed.
(this is also called strict evaluation; recall that some
functional languages like Haskell have non-strict, aka "lazy", "on
demand", or "call by need" evaluation)
Remember the discussion of evaluation order for expressions. An operation like + applied to two arguments, e.g. 2*x + 3*y is really a function-call plus(times(2,x),times(3,y))!
The most important rule of expression evaluation is that arguments are evaluated before the function applied to them is called. Most of the time, that is: if-then-else is special many languages! Languages differ in whether or not it is specified in what order multiple arguments are evaluated.
procedure p(f1,f2:t);
var x: t;
begin
...
end;
Given the call "p(e1,e2)" where e1 evaluates to the value a1 and e2
evaluates to the value a2, the simplest semantics of parameter-passing
says that the call is equivalent to executing the block
begin
const f1 := a1;
const f2 := a2;
var x: t;
...
end;
This method of parameter-passing is called call-by-value. A
variant allows f1 and f2 to be local variables as
opposed to local constants.
procedure p(inout f:t);
begin
...
end;
the call "p(x)" where x is the name of a variable is equivalent to
begin
var f:t := a; // "a" denotes the current value of x
...
x := f;
end;
Call by value-result is used in Ada.
This is the semantics of Pascal "var" parameter-passing. We can explain it more formally as follows. Given
procedure p(var f:t);
begin
...
end;
the call "p(x)" where x is the name of a variable is equivalent to
begin
const f: pointer to t := location(x);
...
end;
In some languages, for example C, call-by-reference is not available
but in can be achieved by using call-by-value and passing the location
of a variable instead of its value (since we can easily get the value
of the memory location of a variable using the "&" operator... ).
Consider the following example:
================================================================== program main ... var i, j: integer; procedure foo(inout x,y: integer) (* compare "inout" vs. "var" *) begin i := y end; begin (* main *) i := 1; j := 2; foo(i, j); (* value of i?? *) end. ==================================================================
Here is another example:
================================================================== program main ... var i: integer; procedure bar(inout x,y: integer) (* compare "inout" vs. "var" *) begin x := x + 1; y := y * 2; end; begin (* main *) i := 2; bar(i, i); (* value of i,j?? *) end. ==================================================================
Consider the pseudo-Pascal summation function
function sum (lo,hi:int; macro k:int; macro f:real): real;
var s: real := 0;
begin
for k := lo to hi do s := s + f;
return s;
end;
With call-by-macro the actual argument expressions for k and
f will be evaluated again each time the formal parameter is
used, so we can write for example sum(1,n,j,a[j]*b[j]) to
compute the dot-product of two vectors a and b of length n.
Consider the pseudo-Pascal summation function using call-by-macro from before. This can be implemented using call-by-value with function arguments:
function sum (lo,hi:int; ref k:int; f:void->real);
var s: real := 0;
begin
for k := lo to hi do s := s + f();
return s
end;
function f(): real;
begin
return a[j]*b[j];
end;
The lesson to learn here is that higher-order functions give the
functionality of call-by-macro.
Let us look at object-oriented programming from the point of view of trying to understand it in terms of programming language concepts that we know already. We will compare OOP to abstract data types. However first we will look again at imperative versus functional programming:
Updatable variables and assignment commands are the essence of imperative programming. There are two basic reasons for using variables: (1) to remember intermediate results (2) to keep track of things changing in the outside world.
The second use of updatable variables is much more fundamental. If you think of a software module as simulating something in the real world, e.g. a database system simulates the population of UCSD students, then it is very natural to update variables as events happen in the real world, e.g. a student enrolls in a class.
In object-oriented programming, an object is a generalization of a variable as used to model something in the outside world that can change.
function fact (n: integer): integer;
var temp: integer;
begin
if n = 0 then temp := 1
else
begin
temp := n-1;
temp := fact(temp);
temp := n*temp
end;
return temp
end;
Is using updatable variables to remember intermediate results really
necessary? The answer is no, because using nested expressions,
the programmer can let the compiler take care of computing
intermediate results in the right order:
function fact (n: integer): integer;
begin
return ifthenelse( n=0, 1, n*fact(n-1) )
end;
Temporary variables are inessential.
If you think of a software module as simulating something in the real world, e.g. a database system simulates the population of UCSD students, then it is very natural to update variables as events happen in the real world, e.g. a student adds a class.
In object-oriented programming, an object is a generalization of a variable as used to model an entity in the outside world.
We can think of an object as a (complex) variable that has "implementation-independence". The idea of "representation-independence" is a special case of "implementation-independence", which is an idea that can apply to types, to variables, constants, functions, procedures, and more:
In modeling a real-world entity, what is important is that the model should behave like the entity being modelled, not the details of how the model is implemented.
An object contains data, called its state or private variables, and it has operations that can update its private variables called methods. Methods are sometimes called member functions.
In the broadest sense, a class is a set of similar objects. Often a type is associated with a class (or a class is considered to be a type itself).
We say that an object is an instance of a class. The operations of an object typically have side-effects, so they are not pure functions.
Objects are thought of as "active individuals" rather than passive values. Invoking an operation of an object is called sending a message to the object.
Therefore, instead of writing x := plus(x,y) we write send x "add y" for example. This calls the add method of the object named x. Every method has an unwritten (implicit) first argument, which is the object itself.
class financial-history =
begin
state
cash, receipts, expenses
method init() =
cash := 0; receipts := []; expenses := [];
method receive(amount) =
receipts := append(amount,receipts);
cash := cash + amount;
method spend(amount) =
expenses := append(amount, expenses);
cash := cash - amount;
method cash() = return cash;
end;
Individual objects can be declared in the same way as variables:
var myaccount: financial-history;
A class introduces a collection of operations which are imperative. Methods modify objects which have an internal, updatable state.
In contrast, an ADT introduces a new type. Any operation on a value of the new type must take the value as an input parameter. With a pure ADT, there are no operations that modify values of the type. Instead, some operations can generate "fresh", new values of the type.
Errors are very likely with unlimited direct updating of composite variables. Consider the following example:
const length = 5;
type address = array [1..length] of string;
procedure update (a: address);
var j: integer := 1; s: string;
begin
while (j <= length) and not eof(input) do
begin
readln(s);
a[j] := s;
j := j+1
end
end;
Q: what happens if the user gives an address with less than 5
lines?
Classes are typically not pure, because objects can be updated, i.e. modified. However programming with objects is still safer than programming with regular composite types, because updating can only be done through predefined methods.
class tax-history parent financial-history =
begin
state deductions;
method init() =
deductions := [];
method deductible-spend(amount) =
deductions := append(amount,deductions);
self.spend(amount); // = send self spend(amount)
end;
Every object of type tax-history has three financial-history state
variables, plus one new state variable. It can respond to all
financial-history messages, and some additional messages.
One method of an object can call another of the same object if necessary. The name self (or this)refers to the current object, which is the implicit first argument of each method.
Calls to a self-method should refer to the method with the given name in the child class. This allows child class methods to override parent methods.
For example, sending the message id to an object of class D with the following class definitions should give I'm a type D object:
class C =
begin
method whoami =
return "I'm a type C object";
method id =
print (self.whoami)
end;
class D parent C =
begin
method whoami =
return "I'm a type D object";
end;
Under standard static scoping, the name whoami in the body of the method C.id would refer to the method C.whoami. This is not what we want.
Method names should follow dynamic binding: the method corresponding to a name should be found in the runtime environment, not in the compile-time environment.
Object identity is connected to an "object-centric" view of programming: every operation is about some primary object. Syntactically, every method has an unwritten (implicit) first argument, which is the object itself.
According to this view, the object itself is really a dispatcher: it waits for a message and then selects one of its methods for execution based on which message was received. A class is a function that can generate new objects. For example:
type messages = (next, restart);
function newgenerator (initial:real) =
begin
var seed: real := initial;
return
function (m: messages) =
begin
case m of
next: seed := transform(seed);
restart: seed := initial;
end;
return seed;
end;
end;
function myrandom = newgenerator(1.2345);
function hisrandom = newgenerator(0.1428);
Here newgenerator is a higher-order function that returns another function, which is anonymous. Each returned function is a pseudo-random number generator (PRNG) that can respond to two alternative messages.
The updated PRNG seed is kept in the variable seed which is local to newgenerator so seed is fresh each time that newgenerator is called. However seed is global to the anonymous function so each version of seed keeps its value between calls to the corresponding individual PRNG.