String
in the
"Hello" examples. It is an object type which
is built into Java. We have seen that strings have the special property
(which no other object type has) of being capable of being used with the
+
operator. If a
and b
are strings, then
a+b
is a string consisting of the characters of a
joined onto the characters of b
. If just one of a
and b
are strings, then in a+b
whichever is not a
string is first converted to a string equivalent then the characters of the
two are joined together. Note that the *
operator cannot be used
with strings. You might think that perhaps "abcd"*3
would be
"abcdabcdabcd"
, but it isn't, it isn't a meaningful value and
you would get a compiler error if you included it in a program.
Apart from this, the class String
has a number of methods
which are provided for you by Java. As they are built into Java, you can make
use of them but you can't change them. The full list can be found in the
official Java documentation for String
,
here, though at this stage because this is the full
documentation you would probably find a little too much detailed information
available there.
One useful method is charAt
. If S
is a string,
then S.charAt(i)
where i
is an int
variable (though any expression that evaluates to an int
could be
used in its place), gives the character in S
at the position of
the number stored in i
. However, counting starts at 0
rather than 1, so if S
stores "Fred"
, and i
stores 1, then S.charAt(i)
represents 'r'
, not
'F'
. S.charAt(0)
represents 'F'
,
S.charAt(2)
represents 'e'
, S.charAt(3)
represents 'd'
, and it is an error to refer to
S.charAt(4)
(strictly, it causes an exception to be
thrown, we cover exceptions here).
As charAt
takes an int
as its argument and
returns a char
, it has signature:
char charAt(int index)The type
char
is the primitive type for single characters. Note
that if an actual character values is written, it is surrounded by single
quotes rather than the double quotes of strings.
Another useful method in class String
is length
.
If S
is a string, then S.length()
is its
length, an int
, so as it has no arguments, the signtaure of
length
is:
int length()There are also several "transformer" methods. While these can be thought of as changing a string, they actually return a new string with the appropriate changes made while leaving the old one unchanged, that is they are constructive methods as discussed in the example. An important property of strings is that they do not have any destructive methods, therefore the type
String
is an immutable type. For example, one transformer
method is toUpperCase
. So S.toUpperCase()
is
a String
value equivalent to that referred to by S
,
except that all the characters are in upper case. So if S
refers
to "Hello Fred"
then S.toUpperCase()
refers to
"HELLO FRED"
. If we have another String
variable,
T
then,
T = S.toUpperCase()will cause
T
to refer to HELLO FRED
while
S
still refers to "Hello Fred"
.
obj
, then we want
obj.transform(S)
to give a string which is the string
referred to by S
transformed in some ways. Note this
is different from S
using its own method to transform its
string, as with toUpperCase
above. Here, obj
is a separate object. As above, though, it does not change what
S
refers to, it returns a new string, which could be assigned
to another string variable, as in:
T = obj.transform(S);Java gives a way of describing objects in terms of what they do rather than how they do it, called an interface. A Java
interface
describes a type of object by listing the signatures of the methods of that
object. For example:
interface StringTransformer { public String transform(String thePhrase); }defines a type
StringTransformer
. Objects of type
StringTranformer
have a method called transform
which takes a string and returns a string. One way of thinking of this is
as a "contract": objects of type StringTransformer
are
contracted to provide this transform
method, but everything
else about them is up to them. These four lines above should occur in a
separate file called StringTransformer.java
.
Of course, we do actually need somewhere a way to say how any particular
StringTranformer
object will work. That is done by an
implementation. Here is an implementation of StringTransformer
which implements the transform
method by calling the
toUpperCase
method of the string passed to it as an argument:
class UpperCaser implements StringTransformer { public String transform(String thePhrase) { return thePhrase.toUpperCase(); } }What this says is that an
UpperCaser
object is a kind of
StringTransformer
object. But there may be other kinds of
StringTransformer
objects. Here is the definition of another:
class Pedant implements StringTransformer { public String transform(String thePhrase) { return "Obviously "+thePhrase; } }This says that a
Pedant
object is another kind of
StringTransformer
object. When the transform
method of a Pedant
object is called with a string as an
argument, the result is a string the same as that argument but with the
characters "Obviously "
joined to its front.
Another way of saying this is that UpperCaser
and
Pedant
are both subtypes of StringTransformer
.
This idea that one type may be a subtype of another is crucial to
object-oriented programming, and we shall return to the theme in more
detail later. The code for the classes UpperCaser
and
Pedant
should be put in separate files UpperCaser.java
and Pedant.java
. Java is able to link them together when they
are compiled.
If we have two types, A
and B
and B
is a subtype of A
(so the file B.java
starts
with class B implements A
), then it is possible to assign
a B
value to an A
variable. In the above
example, a variable of type StringTransformer
may be
made to refer to an object of type UpperCaser
or to an
object of type Pedant
. Here is a program which shows the
use of StringTransformer
:
1 import java.io.*; 2 3 class StringTrans1 4 { 5 // String transform using UpperCaser 6 7 public static void main(String[] args) throws IOException 8 { 9 String phrase1,phrase2; 10 BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 11 StringTransformer st1 = new UpperCaser(); 12 System.out.println("Type a phrase to be transformed:"); 13 phrase1=in.readLine(); 14 phrase2=st1.transform(phrase1); 15 System.out.println("\nTransformed phrase is:"); 16 System.out.println(phrase2); 17 } 18 19 }Here, on line 11, the
StringTransformer
variable st1
is set to refer to a new UpperCaser
object that is created. If
the word UpperCaser
on this line were changed to
Pedant
it would refer to a new Pedant
object.
It would be possible to write line 11 as:
UpperCase st1 = new UpperCaser();This would mean, however, that
st1
could refer only to an
UpperCaser
object, and it would be an error to try to make
it refer to a Pedant
object.
StringTransformer
objects:
1 import java.io.*; 2 3 class StringTrans2 4 { 5 // String transform using UpperCaser then Pedant 6 7 public static void main(String[] args) throws IOException 8 { 9 String phrase1,phrase2,phrase3; 10 BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 11 StringTransformer st1 = new UpperCaser(), st2 = new Pedant(); 12 System.out.println("Type a phrase to be transformed:"); 13 phrase1=in.readLine(); 14 phrase2=st1.transform(phrase1); 15 phrase3=st2.transform(phrase2); 16 System.out.println("\nTransformed phrase is:"); 17 System.out.println(phrase3); 18 } 19 20 }One of the
StringTransformer
variables is set to refer to
an UpperCaser
object, the other to a Pedant
object. Note how the variables of type String
are used to
make a sort of dataflow. The variable phrase1
is used to pass the
initial phrase from in
, which produces it via its
readLine
method, to st1
as the argument to its
transform
method. Variable st2
is used to pass the
phrase returned from this method call to a transform
call on
object st2
. Variable st3
is used to pass the
phrase returned from that to the println
method of
System.out
, causing this final phrase to be printed.
This could be represented diagramatically as:
where the boxes are labelled with the name of the object and the method used for reading/transformation/writing and the arcs are labelled with the variable name used to cause the data to flow from one to the other. Note, the dataflow may "fork" if a variable which is set to a value is used as an argument more than once without resetting. This is the case in the following program:
1 import java.io.*; 2 3 class StringTrans3 4 { 5 // String transform using then Pedant UpperCaser and LowerCaser 6 // i.e. fork in data flow. 7 8 public static void main(String[] args) throws IOException 9 { 10 String phrase1,phrase2,phrase3,phrase4; 11 BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 12 StringTransformer st1 = new Pedant(), 13 st2 = new UpperCaser(), 14 st3 = new LowerCaser(); 15 16 System.out.println("Type a phrase to be transformed:"); 17 phrase1=in.readLine(); 18 phrase2=st1.transform(phrase1); 19 phrase3=st2.transform(phrase2); 20 phrase4=st3.transform(phrase2); 21 System.out.println("\nTransformed phrases are:"); 22 System.out.println(phrase3); 23 System.out.println(phrase4); 24 } 25 26 }which can be represented by the data-flow diagram:
Here phrase2
can be considered as a channel which carries
the result of the transform
method on st1
to
be the arguments of the transofrm
method on st2
and the transform
method on st3
.
However, data flow diagrams do not quite capture the notion of objects
interacting. Suppose line 14 of the file StringTrans2.java
were:
phrase2=st2.transform(phrase1);the program would then involve the single object referred to by
st2
interacting by returning results from two separate method calls. The fact that
this is a single object is perhaps not quite captured by the data flow
diagram:
Note that a variable may be reused in a dataflow role if the value it
transports is not needed later in the program. So a version of
StringTrans2
could be written in which a single variable
is used to move data from method call results to method call arguments:
1 import java.io.*; 2 3 class StringTrans2a 4 { 5 // String transform using UpperCaser then Pedant 6 7 public static void main(String[] args) throws IOException 8 { 9 String phrase; 10 BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 11 StringTransformer st1 = new UpperCaser(), st2 = new Pedant(); 12 System.out.println("Type a phrase to be transformed:"); 13 phrase=in.readLine(); 14 phrase=st1.transform(phrase); 15 phrase=st2.transform(phrase); 16 System.out.println("\nTransformed phrase is:"); 17 System.out.println(phrase); 18 } 19 20 }In fact, it is not necessary to use a variable at all to convey the idea of data flow. You can write the method call in the place of the argument rather than assign it to a separate variable, and then use that variable as an argument. So lines 14 and 15 of
StringTrans2
could be written
as the single line:
phrase3=st2.transform(st1.transform(phrase1));doing away with a separate variable which conveys the result of the method call
st1.transform
to become the argument to the
method call st2.transform
. StringTrans2
could
be written as:
1 import java.io.*; 2 3 class StringTrans2b 4 { 5 // String transform using UpperCaser then Pedant 6 7 public static void main(String[] args) throws IOException 8 { 9 String phrase; 10 BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 11 StringTransformer st1 = new UpperCaser(), st2 = new Pedant(); 12 System.out.println("Type a phrase to be transformed:"); 13 phrase=st2.transform(st1.transform(in.readLine())); 14 System.out.println("\nTransformed phrase is:"); 15 System.out.println(phrase); 16 } 17 18 }or as:
1 import java.io.*; 2 3 class StringTrans2c 4 { 5 // String transform using UpperCaser then Pedant 6 7 public static void main(String[] args) throws IOException 8 { 9 String phrase; 10 BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 11 StringTransformer st1 = new UpperCaser(), st2 = new Pedant(); 12 System.out.println("Type a phrase to be transformed:"); 13 phrase=in.readLine(); 14 System.out.println("\nTransformed phrase is:"); 15 System.out.println(st2.transform(st1.transform(phrase))); 16 } 17 18 }It is not possible to do away completely with a holding variable as it is required here to print the line
"Transformed phrase is:"
after the call to in.readLine
which reads the original
phrase, but before the call to System.out.println
which
prints it. When method calls are embedded in this way, the innermost one
is executed first (those taking the course
Functional Programming
will find that Miranda, with its lazy evaluation mechanism doesn't
work this way), so on line 13 of StringTrans2b
, the call to
in.readLine
takes place before the call to transform
on st1
which takes place before the call to transform
on st2
.
Whether you use holding variables or whether you use method calls embedded within other method calls is a matter of taste. It makes no difference to how your program executes, it's just a matter of what makes your program easier to understand when you as a human reader look at it. Experienced programmers tend to make more use of embedded calls, because once you are used to it, it is more concise and easier to see how the calls fit together. However, excessive use of embedded calls can lead to very complicated constructs, and there's no benefit in doing this.
On the subject of readability, it is helpful to use meaningful variable names.
Again, it's a matter of taste over whether the extra characters used drown
the structure of the program or help make it clearer by explaining what each
variable is meant to refer to. Whatever names you choose to use make no
difference to how your Java program executes. Here is a version of
StringTrans3
which uses more meaningful variable names:
1 import java.io.*; 2 3 class StringTrans3a 4 { 5 // String transform using then Pedant UpperCaser and LowerCaser 6 // i.e. fork in data flow 7 8 public static void main(String[] args) throws IOException 9 { 10 String initPhrase,pedantPhrase,upperPedantPhrase,lowerPedantPhrase; 11 BufferedReader textReader = new BufferedReader(new InputStreamReader(System.in)); 12 StringTransformer pedant1 = new Pedant(), 13 upperCaser1 = new UpperCaser(), 14 lowerCaser1 = new LowerCaser(); 15 16 System.out.println("Type a phrase to be transformed:"); 17 initPhrase = textReader.readLine(); 18 pedantPhrase = pedant1.transform(initPhrase); 19 upperPedantPhrase = upperCaser1.transform(pedantPhrase); 20 lowerPedantPhrase = lowerCaser1.transform(pedantPhrase); 21 System.out.println("\nTransformed phrases are:"); 22 System.out.println(upperPedantPhrase); 23 System.out.println(lowerPedantPhrase); 24 } 25 26 }Note that
StringTrans3
demonstrates where holding variables
have to be used: when they are needed to hold a value which is used more than
once. Since the result of the call on line 18 is used twice,
it needs to be stored in a variable, so the call can't be embedded in
another call.
StringTrans2a
, since
strings are objects, and the variable phrase
was used to
refer to different strings there. Similarly, a single variable could be
used to refer to different StringTransformer
objects. For
example, here is a version of StringTrans2
where the same
variable is used to refer first to an UpperCaser
object,
then to a Pedant
object:
1 import java.io.*; 2 3 class StringTrans2d 4 { 5 // A version of StringTrans2 demonstrating re-use of a StringTransformer variable 6 7 public static void main(String[] args) throws IOException 8 { 9 String phrase1,phrase2,phrase3; 10 BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 11 StringTransformer st = new UpperCaser(); 12 System.out.println("Type a phrase to be transformed:"); 13 phrase1=in.readLine(); 14 phrase2=st.transform(phrase1); 15 st = new Pedant(); 16 phrase3=st.transform(phrase2); 17 System.out.println("\nTransformed phrase is:"); 18 System.out.println(phrase3); 19 } 20 21 }On line 11, the variable
st
is set to refer to the
UpperCaser
object, but on line 15 it is switched to refer
to the Pedant
object. At this point, the UpperCaser
object automatically disappears because nothing refers to it after line 15.
It is possible to create objects without having any variable refer to them,
in a way somewhat similar to embedded method calls. Here is a version of
StringTrans2
which uses such "anonymous objects":
1 import java.io.*; 2 3 class StringTrans2e 4 { 5 // A version of StringTrans2 demonstrating anonymous objects 6 7 public static void main(String[] args) throws IOException 8 { 9 String phrase1,phrase2,phrase3; 10 BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 11 System.out.println("Type a phrase to be transformed:"); 12 phrase1=in.readLine(); 13 phrase2=(new UpperCaser()).transform(phrase1); 14 phrase3=(new Pedant()).transform(phrase2); 15 System.out.println("\nTransformed phrase is:"); 16 System.out.println(phrase3); 17 } 18 19 }On line 13, a new
UpperCaser
object is created and its
transform
method with argument phrase1
is
immediately applied to it, returning the result in phrase2
.
This makes it clear that the UpperCaser
object is created
only for this single use of its transform
method. A new
Pedant
object is similarly created, used for its
transform
method, and then immediately deleted, on line 15.
Last modified: 13 Sep 2000