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);
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 }
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.
Last modified: 4 Oct 1999