|
Welcome to the Core Java Technologies Tech Tips for January 4, 2005. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE).
This issue covers:
Using the Varargs Language Feature
Covariant Parameter Types
These tips were developed using the Java 2 Platform Standard Edition Development Kit 5.0 (JDK 5.0). You can download JDK 5.0 at http://java.sun.com/j2se/1.5.0/download.jsp.
This issue of the Core Java Technologies Tech Tips is written by Daniel H. Steinberg, Director of Java Offerings for Dim Sum Thinking, Inc, and editor-in-chief for java.net (http://java.net).
See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms.
For more Java technology content, visit these sites:
java.sun.com - The Java technology source for developers. Get the latest Java platform releases, tutorials, newsletters and more.
java.net - A web forum where enthusiasts of Java technology can collaborate and build solutions together.
java.com - The ultimate marketplace promoting Java technology,
applications and services.
USING THE VARARGS LANGUAGE FEATURE
Have you ever found yourself in the following situation? You need to pass in many instances of the same object type to a method, but you don't know at compile time how many instances there will be. In the past, the only way to handle this situation was to bundle these objects in an array or other collection. However with J2SE 5.0, you now have the added convenience of using variable arity parameters, known less formally as varargs. In this tip, you will learn why it's a good idea to take advantage of varargs as a client of an API. The tip also cautions against introducing varargs to an API that you create, unless it is warranted.
Let's start with an example that illustrates the use of varargs. In the following program, VarGreeter, the method printGreeting() takes an unspecified number of String objects as input. This is indicated by the use of ellipses in the parameter declaration String ... names. In general, a method can have at most one parameter that is a vararg, it must be the last parameter taken by the method, and it is denoted by the object type, a set of ellipses ( ... ), and the name of the variable.
class VarGreeter {
public static void printGreeting(String... names) {
for (String n : names) {
System.out.println("Hello " + n + ". ");
}
}
public static void main(String[] args) {
printGreeting("Paul", "Sue");
}
}
In VarGreeter, the method printGreeting() is invoked with the call printGreeting("Paul","Sue"). If you run VarGreeter, you get the message:
Hello Paul.
Hello Sue.
Change the line in VarGreeter that calls printGreeting() to have as many String object arguments as you like. Each name that you pass in will be greeted. The printGreeting() method works its way through whatever collection of strings it is passed, and greets each in turn. What is perhaps most striking is that this works if no string is passed. You can see this by changing main() to the following and rerunning VarGreeter.
public static void main(String[] args) {
printGreeting();
}
This time you will have no output because there is no one to greet. This example illustrates an appropriate use of varargs. You have an unspecified number of String objects to process in the same way.
At compile time a vararg is converted to an array. You can see that the printGreeting() method uses an enhanced for loop to iterate through the array. Using a vararg is different than using an array. The next few examples highlight the similarities and differences. First consider VarGreeter2:
public class VarGreeter2 {
public static void printGreeting(String... names) {
for (String n : names) {
System.out.println("Hello " + n + ". ");
}
}
public static void main(String[] args) {
printGreeting(args);
}
}
The key is that you can pass in an array as the parameter. The args variable is an array of type String. Run this version with the command:
java VarGreeter2 Paul Sue
and you will get the same output as before:
Hello Paul.
Hello Sue
You can experiment with different numbers of command-line arguments. You can even pass in no arguments. Ordinarily, you need a guard clause to make sure that args does not have length zero. When required, you should check for proper input. In this case, there are no such requirements. The printGreeting() method can handle zero or more strings passed as a list of strings or passed as a single array of strings.
Now consider VarGreeter3. Here you will see that you can even change the signature of main(). Although there is no reason for doing this, the canonical public static void main(String[] args) can be changed to public static void main(String ... args):
public class VarGreeter3 {
public static void printGreeting(String... names) {
for (String n : names) {
System.out.println("Hello " + n + ". ");
}
}
public static void main(String... args) {
printGreeting(args);
}
}
Run VarGreeter3 with the command:
java VarGreeter3 Paul Sue
and you will get the same output as before:
Hello Paul.
Hello Sue.
Using "main(String... args)" makes it easier to invoke main directly from Java. This can be useful for writing unit tests of your main() method.
So far, you have not seen any difference between explicitly declaring printGreeting() using varargs and using an array. Now it's time to look at a difference. Change the signature from printGreeting(String ... names) to printGreeting(String[] names). Notice that in VarGreeter4, you can still pass in an array of strings with the call printGreeting(args):
public class VarGreeter4 {
public static void printGreeting(String[] names) {
for (String n : names) {
System.out.println("Hello " + n + ". ");
}
}
public static void main(String... args) {
printGreeting(args);
}
}
You'll still get the same results as before if you run the command:
java VarGreeter4 Paul Sue
The difference is if you try to call printGreeting using something like printGreeting("Paul", "Sue") as shown in VarGreeter5:
public class VarGreeter5 {
public static void printGreeting(String[] names) {
for (String n : names) {
System.out.println("Hello " + n + ". ");
}
}
public static void main(String... args) {
printGreeting("Paul", "Sue");
}
}
Try compiling VarGreeter5. It won't compile:
javac VarGreeter5.java
VarGreeter5.java:10: printGreeting(java.lang.String[]) in
VarGreeter5 cannot be applied to
(java.lang.String,java.lang.String)
printGreeting("Paul", "Sue");
^
1 error
The lesson is that varargs give you a great deal of flexibility as the client of an API.
Warning: The following examples illustrate some of the dangers in introducing varargs to an API. It is at least as important to know when it is inappropriate to use a technique as it is to know when to use it.
The GridLayout class has three constructors, one takes four arguments, one takes two arguments, and one takes no arguments. The constructor with two arguments allows a user to specify the number of rows and columns in the resulting grid. The constructor with four arguments also allows a user to specify the size of the horizontal gap between rows and the size of the vertical gap between columns. The two-argument constructor assumes those values to be zero. The no-argument constructor provides defaults for the number of rows and columns. The following program, PreVarGridLayout, is a simplified class with three constructors that are defined in much the same way as for the GridLayout class:
class PreVarGridLayout {
PreVarGridLayout() {
this(1, 1);
}
PreVarGridLayout(int rows, int cols) {
this(rows, cols, 0, 0);
}
PreVarGridLayout(int rows, int cols, int hgap, int vgap) {
System.out.println("Create a grid with rows = "
+ rows + " and cols = " + cols + ", the hgap = "
+ hgap + " and the vgap = " + vgap + ".");
}
public static void main(String[] args) {
System.out.println("Call no arg constructor:");
new PreVarGridLayout();
System.out.println("Call two arg constructor: 2,3");
new PreVarGridLayout(2, 3);
System.out.println
("Call four arg constructor: 4,5,6,7");
new PreVarGridLayout(4, 5, 6, 7);
}
}
In main(), you can see that each constructor is called. If you run PreVarGridLayout, you will get the following output:
Call no arg constructor:
Create a grid with rows = 1 and cols = 1, the hgap = 0 and
the vgap = 0.
Call two arg constructor: 2,3
Create a grid with rows = 2 and cols = 3, the hgap = 0 and
the vgap = 0.
Call four arg constructor: 4,5,6,7
Create a grid with rows = 4 and cols = 5, the hgap = 6 and
the vgap = 7.
You can transform the program to take advantage of varargs, as shown in VarGridLayout:
public class VarGridLayout {
VarGridLayout(Integer... size) {
Object temp[] = {1,1,0,0};
System.arraycopy(size, 0, temp, 0, size.length);
System.out.printf("Create a grid with rows = %d " +
"and cols = %d, the hgap = %d and the vgap = " +
"%d. \n", temp);
}
public static void main(String[] args) {
System.out.println("Call no arg constructor:");
new VarGridLayout();
System.out.println("Call two arg constructor: 2,3");
new VarGridLayout(2, 3);
System.out.println(
"Call four arg constructor: 4,5,6,7");
new VarGridLayout(4, 5, 6, 7);
}
}
There are no changes to main(), so the constructors are called in the same way in VarGridLayout as they were in PreVarGridLayout, that is, before varargs were introduced. The difference is that now there is a single constructor. The constructor takes zero, two, or four arguments as before and prints the same output. Notice that this time printf() is used to write to the output stream. The printf() facility uses varargs. This allows you to pass in temp as the last parameter to printf(). The method treats temp as an array of type Integer.
But there's a serious problem in using varargs this way. You can see the problem by changing the last line of the main() method in PreVarGridLayout to:
new PreVarGridLayout(4,5,6,7,8);
and in VarGridLayout to:
new VarGridLayout(4,5,6,7,8);
Recompile PreVarGridLayout:
javac PreVarGridLayout.java
PreVarGridLayout.java:23: cannot find symbol
symbol : constructor PreVarGridLayout(int,int,int,int,int)
location: class PreVarGridLayout
new PreVarGridLayout(4,5,6,7,8);
^
1 error
You get a compiler error because that signature does not match any of the available constructors for PreVarGridLayout. In fact, if you use an IDE with code sense, it should be able to signal before compile time that this will not compile.
Now compile VarGridLayout. You will get no warnings at compile time because your constructor can take zero or more Integer objects as parameters. If you run VarGridLayout, you will get an ArrayIndexOutOfBoundsException.
You could provide a check that size.length is either zero, two, or four, and throw your own exception otherwise. The point is that this check is left to you, it is not enforced by the compiler. This means that developers using your code will not have support at compile or writing time for making the correct calls. So the lesson is do not use varargs for this purpose.
Another issue in using varargs is knowing which method will be called if there appears to be competing signatures. In the following class, WhichOne, there are three constructors. The first constructor takes a vararg argument, the second takes two int arguments, and the third takes two Integer arguments:
public class WhichOne {
WhichOne(Integer... size) {
System.out.println("Var Args version.");
}
WhichOne(int i, int j) {
System.out.println("Version with int args.");
}
WhichOne(Integer i, Integer j) {
System.out.println("Version with Integer args.");
}
public static void main(String[] args) {
System.out.println("Call w/ two arg:2,3");
new WhichOne(2, 3);
System.out.println("Call w/ Integer two arg: 2,3");
new WhichOne(new Integer(2), new Integer(3));
}
}
Run WhichOne and you will see the following output:
Call w/ two arg:2,3
Version with int args.
Call w/ Integer two arg: 2,3
Version with Integer args.
Even with the varargs version of the constructor, the most specifically-matching constructor is called. Eliminate the second and third constructors so that WhichOne looks like the following:
public class WhichOne {
WhichOne(Integer... size) {
System.out.println("Var Args version.");
}
public static void main(String[] args) {
System.out.println("Call w/ two arg:2,3");
new WhichOne(2, 3);
System.out.println("Call w/ Integer two arg: 2,3");
new WhichOne(new Integer(2), new Integer(3));
}
}
Rerun WhichOne. You should see the following output:
Call w/ two arg:2,3
Var Args version.
Call w/ Integer two arg: 2,3
Var Args version.
In the absence of the more specific constructors, the varargs version is called by both new WhichOne(2,3) and by new WhichOne(new Integer(2), new Integer(3)). It is important in constructing an API to make clear to the client how to call the desired method. So the lesson here is avoid using varargs on overloaded methods.
To reiterate, the advantage of using varargs is that as a client of an API, you are free to call a method using a sequence of instances of an object of the specified type or an array. You have the freedom to make the call using any number of instances of that type. There are also dangers in using varargs. You might want to avoid providing methods that use varargs when it is important to specify the number of entries or to prevent autoboxing.
For more information about varargs, see the document Varargs.
COVARIANT PARAMETER TYPES
The December 1, 2004 Tech Tip Covariant Return Types presented an example of using this new J2SE 5.0 facility. This facility allows you to create methods in a subclass that return an object whose type is a subtype of that returned by the method it overrides. In the following tip you will see the
difficulties involved in extending this capability to method parameters.
The example discussed here was provided by Sun Engineer Peter von der Ahé, and is based on a suggestion he received from computational theologist Gilad Bracha. The intent is to demonstrate that although there are good reasons for implementing covariant return types, implementing covariant method parameters is unsound.
The basic mechanism outlined in the December tip began with two pairs of classes. In this tip, let's simplify the classes:
class A {}
class B extends A {}
Now, define a class, Super, that contains a method that returns an object of type A:
class Super {
public A someMethod(){
return new A();
}
}
Next, create a class, Sub, that extends Super. In J2SE 1.4 and previous releases, you could not override someMethod() by changing its return type. In particular, the following code would not compile:
class Sub extends Super {
public B someMethod() {
return new B();
}
}
Previous to J2SE 5.0, the compiler would issue a message like this:
someMethod() in Sub cannot override someMethod() in Super;
attempting to use incompatible return type.
In J2SE 5.0, this code compiles and the narrower type B is a legal return type for the someMethod() method in the subclass, Sub.
Is it possible to perform a similar narrowing of type for a method parameter? Let's try by creating a "circle of life" example with the Food and Animal interfaces:
interface Food {
int glycemicIndex();
}
interface Animal {
void eat(Food food);
}
There are no problems in implementing the Food interface, as the following class, Grass, shows:
class Grass implements Food {
public int glycemicIndex() {
return 20; // this is a guess, don't base your diet
// on this
}
}
An antelope is an animal that eats food and it is itself food for an animal such as a lion. Here are versions of compilable classes that model the antelope and lion. The classes implement the eat() method declared in the Animal interface:
class Antelope implements Food, Animal {
public void eat(Food food) {
}
public int glycemicIndex() {
return 5; // this is a guess, don't base your diet
// on this
}
}
class Lion implements Animal {
public void eat(Food food) { }
}
The problem with this is that the code allows an Antelope and a Lion to eat anything of type Food. You might be tempted to try the following to restrict the Antelope to only eat objects of type Grass, and the Lion to only eat objects of type Antelope:
class Antelope implements Food, Animal {
public void eat(Grass food) {
}
public int glycemicIndex() {
return 5; // this is a guess, don't base your diet
// on this
}
}
class Lion implements Animal {
public void eat(Antelope food) { }
}
Unfortunately, however, this will no longer compile. You are allowed to add the method with the signature eat(Grass food) to the Antelope class, but you cannot declare that Antelope implements Food. That's because you are not implementing the signature declared in the interface. Your compiler issues a warning that includes something like this.
Antelope is not abstract and does not override abstract
method eat(Food) in Animal
Lion is not abstract and does not override abstract method
eat(Food) in Animal
A solution involves the use of Generics. Start with the declaration of Generics, and declare eat() so that its argument is at least of type Food:
public interface Food {
int glycemicIndex();
}
public interface Animal <F extends Food> {
public void eat(F food);
}
This now more accurately represents the idea that an object of type Animal eats a particular type of Food. The implementation of Grass does not need to change:
public class Grass implements Food {
public int glycemicIndex() {
return 20; //this is a guess, don't base your diet
// on this
}
}
The implementation of Antelope and of Lion can now specify the type of Food that each Animal can eat:
public class Antelope implements Food, Animal<Grass> {
public int glycemicIndex() {
return 5; //this is a made up number
}
public void eat(Grass food) {
}
}
public class Lion implements Animal<Antelope> {
public void eat(Antelope food) {
}
}
You can now exercise these classes, again by employing generics:
class Example {
static <F extends Food> void feed
(Animal<F> animal, F food) {
animal.eat(food);
}
public static void main(String... args) {
feed(new Lion(), new Antelope());
}
}
There is nothing built into J2SE 5.0 to automatically handle covariant parameter types in the same way that covariant return types are now accommodated. You can, however, follow the pattern in this tip to employ generics to allow for extending the type being passed as a parameter to a method.
|
|
 |
 |
|
|
 |
 |
IMPORTANT: Please read our Licensing, Terms of Use, and Privacy policies:
http://developer.java.sun.com/berkeley_license.html
http://www.sun.com/share/text/termsofuse.html
Privacy Statement: Sun respects your online time and privacy (http://sun.com/privacy). You have received this based on your email preferences. If you would prefer not to receive this information, please follow the steps at the bottom of this message to unsubscribe.
Comments? Send your feedback on the Core Java Technologies Tech Tips to: http://developers.sun.com/contact/feedback.jsp?category=newslet
Subscribe to other Java developer Tech Tips:
- Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME).
To subscribe to these and other Sun Developer Network publications:
- Go to the Sun Developer Network Subscriptions page, choose the newsletters you want to subscribe to and click
"Submit".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Submit".
ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/developer/JDCTechTips/index.html
Copyright 2005 Sun Microsystems, Inc. All rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/developer/copyright.html
Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks (http://www.sun.com/suntrademarks/) of Sun Microsystems, Inc. in the United States and other countries.
|