CATEGORII DOCUMENTE |
Asp | Autocad | C | Dot net | Excel | Fox pro | Html | Java |
Linux | Mathcad | Photoshop | Php | Sql | Visual studio | Windows | Xml |
While the local copy produced by clone( ) gives the desired results in the appropriate cases, it is an example of forcing the programmer (the author of the method) to be responsible for preventing the ill effects of aliasing. What if you're making a library that's so general purpose and commonly used that you cannot make the assumption that it will always be cloned in the proper places? Or more likely, what if you want to allow aliasing for efficiency - to prevent the needless duplication of objects - but you don't want the negative side effects of aliasing?
One solution is to create immutable objects which belong to read-only classes. You can define a class such that no methods in the class cause changes to the internal state of the object. In such a class, aliasing has no impact since you can read only the internal state, so if many pieces of code are reading the same object there's no problem.
As a simple example of immutable objects, Java's standard library contains "wrapper" classes for all the primitive types. You might have already discovered that, if you want to store an int inside a collection such as a Vector (which takes only Object handles), you can wrap your int inside the standard library Integer class:
//: ImmutableInteger.java
// The Integer class cannot be changed
import java.util.*;
public class ImmutableInteger
} ///:~
The Integer class (as well as all the primitive "wrapper" classes) implements immutability in a simple fashion: they have no methods that allow you to change the object.
If you do need an object that holds a primitive type that can be modified, you must create it yourself. Fortunately, this is trivial:
//: MutableInteger.java
// A changeable wrapper class
import java.util.*;
class IntValue
public String toString()
}
public class MutableInteger
} ///:~
Note that n is friendly to simplify coding.
IntValue can be even simpler if the default initialization to zero is adequate (then you don't need the constructor) and you don't care about printing it out (then you don't need the toString( )):
class IntValue
Fetching the element out and casting it is a bit awkward, but that's a feature of Vector, not of IntValue.
It's possible to create your own read-only class. Here's an example:
//: Immutable1.java
// Objects that cannot be modified
// are immune to aliasing.
public class Immutable1
public int read()
public boolean nonzero()
public Immutable1 quadruple()
static void f(Immutable1 i1)
public static void main(String[] args)
} ///:~
All data is private, and you'll see that none of the public methods modify that data. Indeed, the method that does appear to modify an object is quadruple( ), but this creates a new Immutable1 object and leaves the original one untouched.
The method f( ) takes an Immutable1 object and performs various operations on it, and the output of main( ) demonstrates that there is no change to x. Thus, x's object could be aliased many times without harm because the Immutable1 class is designed to guarantee that objects cannot be changed.
Creating an immutable class seems at first to provide an elegant solution. However, whenever you do need a modified object of that new type you must suffer the overhead of a new object creation, as well as potentially causing more frequent garbage collections. For some classes this is not a problem, but for others (such as the String class) it is prohibitively expensive.
The solution is to create a companion class that can be modified. Then when you're doing a lot of modifications, you can switch to using the modifiable companion class and switch back to the immutable class when you're done.
The example above can be modified to show this:
//: Immutable2.java
// A companion class for making changes
// to immutable objects.
class Mutable
public Mutable add(int x)
public Mutable multiply(int x)
public Immutable2 makeImmutable2()
}
public class Immutable2
public int read()
public boolean nonzero()
public Immutable2 add(int x)
public Immutable2 multiply(int x)
public Mutable makeMutable()
public static Immutable2 modify1(Immutable2 y)
// This produces the same result:
public static Immutable2 modify2(Immutable2 y)
public static void main(String[] args)
} ///:~
Immutable2 contains methods that, as before, preserve the immutability of the objects by producing new objects whenever a modification is desired. These are the add( ) and multiply( ) methods. The companion class is called Mutable, and it also has add( ) and multiply( ) methods, but these modify the Mutable object rather than making a new one. In addition, Mutable has a method to use its data to produce an Immutable2 object and vice versa.
The two static methods modify1( ) and modify2( ) show two different approaches to producing the same result. In modify1( ), everything is done within the Immutable2 class and you can see that four new Immutable2 objects are created in the process. (And each time val is reassigned, the previous object becomes garbage.)
In the method modify2( ), you can see that the first action is to take the Immutable2 y and produce a Mutable from it. (This is just like calling clone( ) as you saw earlier, but this time a different type of object is created.) Then the Mutable object is used to perform a lot of change operations without requiring the creation of many new objects. Finally, it's turned back into an Immutable2. Here, two new objects are created (the Mutable and the result Immutable2) instead of four.
This approach makes sense, then, when:
You need immutable objects and
You often need to make a lot of modifications or
It's expensive to create new immutable objects
Consider the following code:
//: Stringer.java
public class Stringer
public static void main(String[] args)
} ///:~
When q is passed in to upcase( ) it's actually a copy of the handle to q. The object this handle is connected to stays put in a single physical location. The handles are copied as they are passed around.
Looking at the definition for upcase( ), you can see that the handle that's passed in has the name s, and it exists for only as long as the body of upcase( ) is being executed. When upcase( ) completes, the local handle s vanishes. upcase( ) returns the result, which is the original string with all the characters set to uppercase. Of course, it actually returns a handle to the result. But it turns out that the handle that it returns is for a new object, and the original q is left alone. How does this happen?
If you say:
String s = 'asdf';
String x = Stringer.upcase(s);
do you really want the upcase( ) method to change the argument? In general, you don't, because an argument usually looks to the reader of the code as a piece of information provided to the method, not something to be modified. This is an important guarantee, since it makes code easier to write and understand.
In C++, the availability of this guarantee was important enough to put in a special keyword, const, to allow the programmer to ensure that a handle (pointer or reference in C++) could not be used to modify the original object. But then the C++ programmer was required to be diligent and remember to use const everywhere. It can be confusing and easy to forget.
Objects of the String class are designed to be immutable, using the technique shown previously. If you examine the online documentation for the String class (which is summarized a little later in this chapter), you'll see that every method in the class that appears to modify a String really creates and returns a brand new String object containing the modification. The original String is left untouched. Thus, there's no feature in Java like C++'s const to make the compiler support the immutability of your objects. If you want it, you have to wire it in yourself, like String does.
Since String objects are immutable, you can alias to a particular String as many times as you want. Because it's read-only there's no possibility that one handle will change something that will affect the other handles. So a read-only object solves the aliasing problem nicely.
It also seems possible to handle all the cases in which you need a modified object by creating a brand new version of the object with the modifications, as String does. However, for some operations this isn't efficient. A case in point is the operator '+' that has been overloaded for String objects. Overloading means that it has been given an extra meaning when used with a particular class. (The '+' and '+=' for String are the only operators that are overloaded in Java and Java does not allow the programmer to overload any others ).
When used with String objects, the '+' allows you to concatenate Strings together:
String s = 'abc' + foo + 'def' + Integer.toString(47);
You could imagine how this might work: the String "abc" could have a method append( ) that creates a new String object containing "abc" concatenated with the contents of foo. The new String object would then create another new String that added "def" and so on.
This would certainly work, but it requires the creation of a lot of String objects just to put together this new String, and then you have a bunch of the intermediate String objects that need to be garbage-collected. I suspect that the Java designers tried this approach first (which is a lesson in software design - you don't really know anything about a system until you try it out in code and get something working). I also suspect they discovered that it delivered unacceptable performance.
The solution is a mutable companion class similar to the one shown previously. For String, this companion class is called StringBuffer, and the compiler automatically creates a StringBuffer to evaluate certain expressions, in particular when the overloaded operators + and += are used with String objects. This example shows what happens:
//: ImmutableStrings.java
// Demonstrating StringBuffer
public class ImmutableStrings
} ///:~
In the creation of String s, the compiler is doing the rough equivalent of the subsequent code that uses sb: a StringBuffer is created and append( ) is used to add new characters directly into the StringBuffer object (rather than making new copies each time). While this is more efficient, it's worth noting that each time you create a quoted character string such as "abc" and "def", the compiler turns those into String objects. So there can be more objects created than you expect, despite the efficiency afforded through StringBuffer.
Here is an overview of the methods available for both String and StringBuffer so you can get a feel for the way they interact. These tables don't contain every single method, but rather the ones that are important to this discussion. Methods that are overloaded are summarized in a single row.
First, the String class:
Method |
Arguments, Overloading |
Use |
Constructor |
Overloaded: Default, String, StringBuffer, char arrays, byte arrays. |
Creating String objects. |
length( ) |
Number of characters in String. |
|
charAt() |
int Index |
The char at a location in the String. |
getChars( ), getBytes( ) |
The beginning and end from which to copy, the array to copy into, an index into the destination array. |
Copy chars or bytes into an external array. |
toCharArray( ) |
Produces a char[] containing the characters in the String. |
|
equals( ), equals-IgnoreCase( ) |
A String to compare with. |
An equality check on the contents of the two Strings. |
compareTo( ) |
A String to compare with. |
Result is negative, zero, or positive depending on the lexicographical ordering of the String and the argument. Uppercase and lowercase are not equal! |
regionMatches( ) |
Offset into this String, the other String and its offset and length to compare. Overload adds "ignore case." |
Boolean result indicates whether the region matches. |
startsWith( ) |
String that it might start with. Overload adds offset into argument. |
Boolean result indicates whether the String starts with the argument. |
endsWith( ) |
String that might be a suffix of this String. |
Boolean result indicates whether the argument is a suffix. |
indexOf( ), lastIndexOf( ) |
Overloaded: char, char and starting index, String, String, and starting index |
Returns -1 if the argument is not found within this String, otherwise returns the index where the argument starts. lastIndexOf( ) searches backward from end. |
substring( ) |
Overloaded: Starting index, starting index, and ending index. |
Returns a new String object containing the specified character set. |
concat( ) |
The String to concatenate |
Returns a new String object containing the original String's characters followed by the characters in the argument. |
replace( ) |
The old character to search for, the new character to replace it with. |
Returns a new String object with the replacements made. Uses the old String if no match is found. |
toLowerCase( ) toUpperCase( ) |
Returns a new String object with the case of all letters changed. Uses the old String if no changes need to be made. |
|
trim( ) |
Returns a new String object with the white space removed from each end. Uses the old String if no changes need to be made. |
|
valueOf( ) |
Overloaded: Object, char[], char[] and offset and count, boolean, char, int, long, float, double. |
Returns a String containing a character representation of the argument. |
intern( ) |
Produces one and only one String handle for each unique character sequence. |
You can see that every String method carefully returns a new String object when it's necessary to change the contents. Also notice that if the contents don't need changing the method will just return a handle to the original String. This saves storage and overhead.
Here's the StringBuffer class:
Method |
Arguments, overloading |
Use |
Constructor |
Overloaded: default, length of buffer to create, String to create from. |
Create a new StringBuffer object. |
toString( ) |
Creates a String from this StringBuffer. |
|
length( ) |
Number of characters in the StringBuffer. |
|
capacity( ) |
Returns current number of spaces allocated. |
|
ensure- |
Integer indicating desired capacity. |
Makes the StringBuffer hold at least the desired number of spaces. |
setLength( ) |
Integer indicating new length of character string in buffer. |
Truncates or expands the previous character string. If expanding, pads with nulls. |
charAt( ) |
Integer indicating the location of the desired element. |
Returns the char at that location in the buffer. |
setCharAt( ) |
Integer indicating the location of the desired element and the new char value for the element. |
Modifies the value at that location. |
getChars( ) |
The beginning and end from which to copy, the array to copy into, an index into the destination array. |
Copy chars into an external array. There's no getBytes( ) as in String. |
append( ) |
Overloaded: Object, String, char[], char[] with offset and length, boolean, char, int, long, float, double. |
The argument is converted to a string and appended to the end of the current buffer, increasing the buffer if necessary. |
insert( ) |
Overloaded, each with a first argument of the offset at which to start inserting: Object, String, char[], boolean, char, int, long, float, double. |
The second argument is converted to a string and inserted into the current buffer beginning at the offset. The buffer is increased if necessary. |
reverse( ) |
The order of the characters in the buffer is reversed. |
The most commonly-used method is append( ), which is used by the compiler when evaluating String expressions that contain the '+' and '+=' operators. The insert( ) method has a similar form, and both methods perform significant manipulations to the buffer instead of creating new objects.
By now you've seen that the String class is not just another class in Java. There are a lot of special cases in String, not the least of which is that it's a built-in class and fundamental to Java. Then there's the fact that a quoted character string is converted to a String by the compiler and the special overloaded operators + and +=. In this chapter you've seen the remaining special case: the carefully-built immutability using the companion StringBuffer and some extra magic in the compiler.
C++ allows the programmer to overload
operators at will. Because this can often be a complicated process see Chapter 10 of my book Thinking in C++ Prentice-Hall,
1995) the
Java designers deemed it a "bad" feature that shouldn't be included in Java. It
wasn't so bad that they didn't end up doing it themselves, and ironically
enough, operator overloading would be much easier to use in Java than in C++.
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 720
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved