The String-transformers Examples: Part 1

Strings

We have already seen the type 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".

String Transformers and Interfaces

Let us consider an object which has a method that transforms strings. Suppose that object is called 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.

Connecting method calls with variables

Now, here is a similar program which makes use of two 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.

Further consideration of object variables

An object variable need not refer to the same object throughout its lifetime. We have already seem this in 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.
Matthew Huntbach

These notes were produced as part of the course Introduction to Programming as it was given in the Department of Computer Science at Queen Mary, University of London during the academic years 1998-2001.

Last modified: 13 Sep 2000