The String-transformers Examples: Part 2

Superclasses and subclasses

As has already been mentioned, it is possible to use the types Pedant, UpperCaser and LowerCaser rather than StringTransformer. For example, we could write version of StringTrans2 which didn't use StringTransformer:
 1 import java.io.*;
 2
 3 class StringTrans2f
 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   Uppercaser st1 = new UpperCaser(), 
12   Pedant st2 = new Pedant();
13   System.out.println("Type a phrase to be transformed:");
14   phrase1=in.readLine();
15   phrase2=st1.transform(phrase1);
16   phrase3=st2.transform(phrase2);
17   System.out.println("\nTransformed phrase is:");
18   System.out.println(phrase3);
19  }
20
21 }
The advantage of the separate superclass StringTransformer is that it allows us to write code which is generalised. For example, you could be told "write some code which takes two StringTransformers and a string, and applies the transform method of the first StringTransformer then the transform method of the second StringTransformer and returns the resulting string. Here is how you could write it:
 1 class Doubler
 2 {
 3 public static String transform(StringTransformer first,StringTransformer second,String phrase)
 4  {
 5   return second.transform(first.transform(phrase));
 6  }
 7 }
It's just a class with a single static method in it. You will see more use of static methods (which can also be called "functions") in the "TwoNums" examples. The important thing is that you can write this method without knowing whether each of st1 and st2 is going to be an UpperCaser, a LowerCaser or a Pedant. Indeed, it can be reused with them set to objects of different types, so long as the types are always subtypes of StringTransformer.

Here is how you would use Doubler in a main method which behaves just like the main method in StringTrans2:

 1 import java.io.*;
 2
 3 class StringTrans12
 4 {
 5 // String transform using UpperCaser then Pedant via Doubler
 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(), st2 = new Pedant();
12   System.out.println("Type a phrase to be transformed:");
13   phrase1=in.readLine();
14   phrase2=Doubler.transform(st1,st2,phrase1);
15   System.out.println("\nTransformed phrase is:");
16   System.out.println(phrase2);
17  }
18
19 }
Of course, you could apply the transform method of Pedant followed by the transform method of UpperCaser if you wrote line 14 of StringTrans12.java as:
 phrase2=Doubler.transform(st2,st1,phrase1);

String-transformers with arguments

One thing you may have noticed about the types UpperCaser, LowerCaser and Pedant is that it only really makes sense to have one object of each type around. There is no way of varying a Pedant object (just like the other two): each time the transform method is called on any Pedant object it does exactly the same thing - returns the argument with "Obviously " added to its front. You could have two Pedant objects about, for example line 11 in StringTrans12 above could be:
 StringTransformer st1 = new Pedant(), st2 = new Pedant();
and then the program would be one that asked for a phrase and printed it out with "Obviously Obviously " in front of it. However, that does not make sense, it would be simpler to have just one Pedant object about and use it twice, so line 11 would be:
 StringTransformer st = new Pedant();
and line 14 would be:
 phrase2=Doubler.transform(st,st,phrase1);
Note this means that when this call is made, on line 5 of Doubler.java, both first and second refer to the same object, a form of aliasing. Sometimes (though it causes no harm in this case) when you write methods you have to be careful to consider that two arguments may both refer to the same object.

However, now let's consider a simple StringTransformer which is parameterised. A NameDropper object has a transform method which adds "Fred says ", "John says " or whatever to the phrase argument, where the particular name ("Fred", "John" etc) is set when the NameDropper object is created. Here's the code for it:

 1 class NameDropper implements StringTransformer
 2 {
 3  private String myName;
 4
 5  public NameDropper(String aName)
 6  {
 7   myName=aName;
 8  }
 9
10  public String transform(String thePhrase)
11  {
12   return myName+" says "+thePhrase;
13  }
14 }
Every NameDropper object has its own field, defined in line 3 of NameDropper.java, where it stores the name it "drops". There was no constructor for UpperCaser, LowerCaser or Pedant because there was nothing a constructor had to do. However, NameDropper needs a constructor, and lines 5-8 are the constructor, because when a new NameDropper is created, its myName field has to be set to the name to be dropped. That name is given to it as an argument to the constructor. So, for example,
StringTransformer st1 = new NameDropper("Fred"), st2 = new NameDropper("John");
will create two NameDropper objects, referred to by st1 and st2, one of which drops the name "Fred and the other the name "John". If you replaced line 11 in StringTrans12.java by the above line, you would get a program which asks for a phrase, and then prints it out with "John says Fred says " added to the front. Now it is clear that we may need any number of NameDropper objects, because each may have a different name to drop.

String-transformers with arguments can be used as an alternative way of dealing with the problem that led us to introduce class Doubler above. We could have a type of StringTransformer object which when created takes two further StringTransformer objects as arguments and stores them in fields. Then its own transform method could consist of applying the transform method of these two fields. We call this type of StringTransformer object a Joiner object because it joins the transform methods of two other objects to form a single transform method. Here is the code for it:

 1 class Joiner implements StringTransformer
 2 {
 3  StringTransformer first,second;
 4
 5  public Joiner(StringTransformer st1,StringTransformer st2)
 6  {
 7   first=st1;
 8   second=st2;
 9  }
10 
11  public String transform(String thePhrase)
12  {
13   String phrase1 = first.transform(thePhrase);
14   return second.transform(phrase1);
15  }
16 }
Here is a program which has the same effect as StringTrans2 but uses Joiner
 1 import java.io.*;
 2
 3 class StringTrans12a
 4 {
 5 // String transform using UpperCaser then Pedant via Joiner
 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(), st2 = new Pedant();
12   StringTransformer st3 = new Joiner(st1,st2);
13   System.out.println("Type a phrase to be transformed:");
14   phrase1=in.readLine();
15   phrase2=st3.transform(phrase1);
16   System.out.println("\nTransformed phrase is:");
17   System.out.println(phrase2);
18  }
19
20 }

Mutable objects

Note that once a NameDropper object is created, its behaviour never changes. Once we have created a NameDropper object using new NameDropper("Fred"), whenever we call its transform method it will always return the argument with "Fred says " added to it. This is because a NameDropper object is immutable: there is no way to change its state. It does not have any method of its own which changes the sole thing in its state, its myName field. Neither may anything else change that field since it is declared as private. So once we have assigned:
StringTransformer st = new NameDropper("Fred");
we know that a call st.transform("hello") will always return "Fred says hello", at least until st is assigned to refer to another object.

It can be valuable to have the assurance that an object type is immutable. It can make a program difficult to follow if a call to an object method has different effects in different places even though its arguments are identical. However, some objects need to be mutable. Here is an example of a class which provides StringTransformer objects which are mutable:

 1 class SayingCounter implements StringTransformer
 2 {
 3  private String myName;
 4  private int count;
 5
 6  public SayingCounter(String aName)
 7  {
 8   myName=aName;
 9  }
10 
11  public String transform(String thePhrase)
12  {
13   count++;
14   return myName+"'s saying no."+count+" is "+thePhrase;
15  }
16 }
Here is a program that makes use of objects of this type:
 1 import java.io.*;
 2
 3 class StringTrans13
 4 {
 5 // Demonstration of behaviour of SayingCounter objects
 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 SayingCounter("Fred"), st2 = new SayingCounter("John");
12   System.out.println("Type first phrase:");
13   phrase1=in.readLine();
14   System.out.println("Type second phrase:");
15   phrase2=in.readLine();
16   System.out.println("\nTransformed phrases are:");
17   System.out.println(st1.transform(phrase1));
18   System.out.println(st2.transform(phrase1));
19   System.out.println(st1.transform(phrase2));
20   System.out.println(st1.transform(phrase1));
21  }
22
23 }
A SayingCounter object is like a NameDropper object, in that it has a name which it adds to the phrase it takes in when its transform method is called. But it also has a field called count which it increments by 1 each time the transform method is called. The field is declared on line 4 of SayingCount.java and the increment is on line 13. Note that the count field doesn't have to be initialised. That is because int fields in objects are automatically initialised to 0 when the objects are created. Because the object referred to by st1 in StringTrans13.java is mutable, the method call on that object in line 17 does not return the same value as the identical method call in line 20 even thought the code in between shows no changes to either what st1 refers to or what the argument to the method, phrase1 refers to. But the state of the object referred to by st1 is changed each time its transform method is called, on lines 17, 18 and 20.

Note that each SayingCounter object has its own count field, so the call to transform on line 18 causes the count field of the object referred to by st2 to change, but not that of the object referred to by st1. If line 4 of SayingCounter.java were:

private static int count;
this would not be the case. The word static here indicates that there is a single variable called count shared by all SayingCounter objects, so whenever one of them updated it the update would apply to all of them. A variable declared as static in a class like this so that it is shared by all objects of that class is known as a class variable.
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: 4 Oct 1999