|
Welcome to the Core Java Technologies Tech Tips for July 16, 2004. 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 Collections to Sort and Shuffle a List
Lighting a 3D Scene
These tips were developed using Java 2 SDK, Standard Edition, v 1.4.2.
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.
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 latest Java platform releases, tutorials, and
newsletters.
java.net - A web forum for collaborating and building solutions
together.
java.com - The marketplace for Java technology, applications and
services.
USING COLLECTIONS TO SORT AND SHUFFLE A LIST
The class java.util.Collections contains many methods designed to operate on objects that implement the List interface. This utility class includes methods to perform a binary search on a list for a specified object, to copy the elements of one list into another, and to swap elements at specified positions in a list. In this tip, you will investigate two simple methods in the Collections class: shuffle() and rotate(). Then you'll use another Collections method, sort(), to sort items in a list. To sort there must be some way to compare objects. In this tip, you will sort using Comparable and later a Comparator.
Let's start with the following example. You have a deck of six cards numbered zero through five. You display the initialized deck and then rotate it by two positions, that is, you call rotate(2). This results in taking the bottom two cards, putting them on top of the deck, and moving everything else down. If you have a deck of n cards, then rotate for r<n, amounts to "cutting the cards" so that you take n-r cards off the top of the deck and move those cards below the stack of cards that remains on the table.
If instead you call rotate(-r) then you are moving r cards from the top to the bottom. You can think of this as rotating r cards in the opposite direction or you can think of rotate(n-r) as being equivalent to rotate(-r).
You can expect to get different results when you shuffle the cards. The Collections class javadocs describe the shuffling algorithm as producing results in all possible permutations with equal likelihood.
Here is a simple program that initializes and shuffles a deck of
cards.
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
public class CutAndShuffle {
private static List miniDeck = new ArrayList(6);
private static void initializeDeck() {
for (int i = 0; i < 6; i++) {
miniDeck.add(new Integer(i));
}
}
private static void printDeck(String message) {
System.out.println(message + "\n");
for (int i = 0; i < 6; i++) {
System.out.println("card " + i +
" = " + miniDeck.get(i));
}
System.out.println("============");
}
public static void main(String[] args) {
initializeDeck();
printDeck("Initialized Deck:");
Collections.rotate(miniDeck, 2);
printDeck("Deck rotated by 2:");
Collections.rotate(miniDeck, -2);
printDeck("Deck rotated back by 2:");
Collections.shuffle(miniDeck);
printDeck("Deck Shuffled:");
}
}
When you run the CutAndShuffle program, you should get output similar to the following. As previously noted, it is likely that your results from the call to shuffle() will be different.
Initialized Deck:
card 0 = 0
card 1 = 1
card 2 = 2
card 3 = 3
card 4 = 4
card 5 = 5
============
Deck rotated by 2:
card 0 = 4
card 1 = 5
card 2 = 0
card 3 = 1
card 4 = 2
card 5 = 3
============
Deck rotated back by 2:
card 0 = 0
card 1 = 1
card 2 = 2
card 3 = 3
card 4 = 4
card 5 = 5
============
Deck Shuffled:
card 0 = 1
card 1 = 2
card 2 = 4
card 3 = 5
card 4 = 0
card 5 = 3
============
The shuffle() and rotate() methods in Collections do not depend on you being able to compare elements in any way. However sorting does require a way to compare elements. To apply a sort() you to need to determine which of two non-equivalent objects precedes the other. There are two ways to achieve this:
In the first approach, if o1 and o2 are instances of a class that implements Comparable, then o1.compareTo(o2) should return 0 if the two objects are equal, a negative integer if o1 is less than o2, and a positive integer if o1 is greater than o2. The value of the returned integer is not important, the sign (positive or negative) is. So o1.compareTo(o2) must have the opposite sign of o2.compareTo(o1). For three objects, compareTo() should reflect transitivity of the underlying comparison. In other words if o1.compareTo(o2)>0 and o2.compareTo(o3)>0, then o1.compareTo(o3)>0.
The second approach takes advantage of the fact that the wrapper classes, including Integer, implement Comparable. As a result, you can easily sort a List comprised of classes of type Integer using the Collections.sort() method. Here's an example:
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
public class ShuffleAndSort {
List miniDeck = new ArrayList(6);
void initializeDeck() {
for (int i = 0; i < 6; i++) {
createCard(i);
}
}
void printDeck(String message) {
System.out.println(message);
for (int i = 0; i < 6; i++) {
System.out.println("card " + i +
" = " + miniDeck.get(i));
}
System.out.println("============");
}
void createCard(int i) {
miniDeck.add(new Integer(i));
}
void sort(){
Collections.sort(miniDeck);
}
void exerciseDeck() {
initializeDeck();
Collections.shuffle(miniDeck);
printDeck("Deck Shuffled:");
sort();
printDeck("Deck Sorted:");
}
public static void main(String[] args) {
new ShuffleAndSort().exerciseDeck();
}
}
The ShuffleAndCode program might appear to have excess methods. Certainly createCard() and sort() could be inlined. Creating separate methods makes it easy to extend the ShuffleAndSort class. You can do this by overriding these methods to vary what objects are added to the deck and how the deck is sorted. In this case you get a report that the deck is shuffled. You can then sort the deck using the method call:
Collections.sort(miniDeck);
The output from ShuffleAndSort should look something like the following.
Deck Shuffled:
card 0 = 4
card 1 = 2
card 2 = 1
card 3 = 3
card 4 = 5
card 5 = 0
============
Deck Sorted:
card 0 = 0
card 1 = 1
card 2 = 2
card 3 = 3
card 4 = 4
card 5 = 5
============
The miniDeck is able to sort itself because Integer implements Comparable. You can also choose to sort it differently by using a different signature of the sort() method. This other signature takes the List to be sorted as the first argument and the Comparator object as the second object. You can replace the sort() method in ShuffleAndSort with the following:
void sort(){
Collections.sort(miniDeck,
Collections.reverseOrder());
}
This time the deck will be sorted from high card to low card.
Suppose instead that you sort objects of a type that does not implement Comparable. Create, for example, the following Rank class:
public class Rank {
private int rank;
public Rank(int rank) {
this.rank = rank;
}
public int getRankInt(){
return rank;
}
public String toString() {
return "" + rank;
}
}
Now create the following class that inherits from ShuffleAndSort and creates a deck of objects of type Rank:
public class UseRank extends ShuffleAndSort {
void createCard(int i) {
miniDeck.add(new Rank(i));
}
public static void main(String[] args) {
new UseRank().exerciseDeck();
}
}
When you compile and run UseRank you get a ClassCastException. There are two ways to solve this problem:
- You create a
Comparable version of Rank.
- You store the rank as an
int and implement compareTo() "by hand."
The first solution takes advantage of the fact that Integer implements Comparable and stores the actual rank as an Integer and not an int. Then the compareTo() method calls the compareTo() method in the Integer class. Here's what this approach looks like:
public class ComparableRank implements Comparable {
private Integer rank;
public ComparableRank(int rank) {
this.rank = new Integer(rank);
}
public String toString() {
return "" + rank;
}
public int compareTo(Object o) {
ComparableRank cr = (ComparableRank) o;
return (rank.compareTo(cr.rank));
}
}
In the second solution, you store the rank as an int, and you need to implement compareTo() like this:
public int compareTo(Object o) {
ComparableRank2 cr = (ComparableRank2) o;
return (rank - cr.rank);
}
Note that for the purposes of this tip, returning the difference of rank and cr.rank works fine. The rank attribute is non-negative and the deck size is smaller than 231-1. Here is a more careful implementation of compareTo():
public int compareTo(Object o) {
ComparableRank2 cr = (ComparableRank2) o;
if (rank < cr.rank) return -1;
else return 1;
}
This assumes that the value of the rank attribute is unique in a deck. You could also extend this comparison to check for equality, in which case you would return 0.
In both approaches you should override toString() so that the printout matches the format from the previous examples. Here is an example of the second approach:
public class ComparableRank2 implements Comparable {
private int rank;
public ComparableRank2(int rank) {
this.rank = rank;
}
public String toString() {
return "" + rank;
}
public int compareTo(Object o) {
ComparableRank2 cr = (ComparableRank2) o;
return (rank - cr.rank);
}
}
You can run these versions by changing the body of the createCard() method to load objects of type ComparableRank or of type ComparableRank2.
Recall the second way of determining which of two non-equivalent objects precedes the other in a sort: Create a companion class that implements Comparator and encapsulates the rules for comparing two objects of a certain type. Taking this approach with objects of type Rank, you fill the deck with objects of that type. Then you create a Comparator that is used to sort those objects. This solution is ideal to use with classes that you cannot alter or easily subclass. The compare() method in the following RankComparator class is essentially the same as the compareTo() method in ComparableRank2. You cast to the type of objects that you are supporting in this Comparator and then implement the comparison:
import java.util.Comparator;
public class RankComparator implements Comparator {
public int compare(Object o1, Object o2) {
Rank r1 = (Rank) o1;
Rank r2 = (Rank) o2;
return r1.getRankInt() - r2.getRankInt();
}
}
To use this new RankComparator, you simply override the sort() method from UseRank and pass in the RankComparator as the second argument:
import java.util.Collections;
public class UseRankComparator extends UseRank {
void sort() {
Collections.sort(miniDeck, new RankComparator());
}
public static void main(String[] args) {
new UseRankComparator().exerciseDeck();
}
}
The output from UseRankComparator should look something like the following.
Deck Shuffled:
card 0 = 0
card 1 = 4
card 2 = 5
card 3 = 3
card 4 = 1
card 5 = 2
============
Deck Sorted:
card 0 = 0
card 1 = 1
card 2 = 2
card 3 = 3
card 4 = 4
card 5 = 5
============
You have seen that when you want to sort a List it must be possible to compare the items in that list. You can use items of a type that implements Comparable. Fortunately, this includes the classes that wrap the primitive types. A second approach is to create a companion class that implements Comparator and encapsulates the rules for comparing two objects of a certain type.
For more information about Collections, see the Collections trail in the Java Tutorial.
LIGHTING A 3D SCENE
The October 14, 2003 Tech Tip Introducing the Java3D API introduced the Java 3D API through a number of examples. These examples included the display of spinning objects such as boxes and three dimensional text. To view the text it was necessary to provide a light. In this tip, you will explore the different types of lights that you can use in a Java3D application. You will experiment with four types of lights: ambient, directional, point, and spot.
Let's begin with an example that is similar to the Text3DWorld example in the October Tech Tip. One major difference between Text3DWorld and the following program, Framework3D, is that the setLighting() method in Framework3D is declared abstract. You will override this method in each of the examples that follow.
import com.sun.j3d.utils.universe.SimpleUniverse;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.Alpha;
import javax.media.j3d.RotationInterpolator;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.Appearance;
import javax.media.j3d.Material;
import javax.media.j3d.Font3D;
import javax.media.j3d.FontExtrusion;
import javax.media.j3d.Text3D;
import javax.media.j3d.Shape3D;
import javax.swing.JFrame;
import javax.vecmath.Vector3f;
import java.awt.BorderLayout;
import java.awt.GraphicsConfiguration;
import java.awt.Font;
public abstract class Framework3D extends JFrame {
private Transform3D rotate1 = new Transform3D();
private Transform3D rotate2 = new Transform3D();
public Framework3D() {
super("3 - D");
Canvas3D canvas3D = createCanvas3D();
BranchGroup scene = createSceneGraph();
connect(canvas3D, scene);
}
private Canvas3D createCanvas3D() {
setSize(300, 300);
getContentPane().setLayout(new BorderLayout());
GraphicsConfiguration config =
SimpleUniverse.getPreferredConfiguration();
Canvas3D canvas3D = new Canvas3D(config);
setSize(300, 300);
getContentPane().add(canvas3D);
return canvas3D;
}
public BranchGroup createSceneGraph() {
BranchGroup objRoot = new BranchGroup();
TransformGroup mover = moveTextBack();
TransformGroup spinner = createSpinner();
objRoot.addChild(mover);
mover.addChild(spinner);
spinner.addChild(createTextShape());
spinner.addChild(makeSpin(spinner));
setLighting(mover);
return objRoot;
}
private TransformGroup createSpinner() {
TransformGroup spinner = new TransformGroup();
spinner.setCapability(TransformGroup.
ALLOW_TRANSFORM_WRITE);
return spinner;
}
private TransformGroup moveTextBack() {
Transform3D transform3D = new Transform3D();
transform3D.setTranslation(
new Vector3f(0.0f, 0.0f, -1.0f));
return new TransformGroup(transform3D);
}
private Shape3D createTextShape() {
Appearance textAppear = new Appearance();
textAppear.setMaterial(new Material());
Font3D font3D = new Font3D(
new Font("Helvetica", Font.PLAIN, 1),
new FontExtrusion());
Text3D textGeom = new Text3D(font3D,
new String("3 \t D"));
textGeom.setAlignment(Text3D.ALIGN_CENTER);
Shape3D textShape = new Shape3D();
textShape.setGeometry(textGeom);
textShape.setAppearance(textAppear);
return textShape;
}
private RotationInterpolator
makeSpin(TransformGroup spinner) {
RotationInterpolator rotator =
new RotationInterpolator(new Alpha(-1, 3000),
spinner);
rotator.setTransformAxis(rotateCube());
BoundingSphere bounds =
new BoundingSphere();
rotator.setSchedulingBounds(bounds);
return rotator;
}
private Transform3D rotateCube() {
rotate1.rotX(Math.PI / 4.0d);
rotate2.rotY(Math.PI / 3.0d);
rotate1.mul(rotate2);
return rotate1;
}
private void connect(Canvas3D canvas3D,
BranchGroup scene) {
SimpleUniverse simpleU =
new SimpleUniverse(canvas3D);
simpleU.getViewingPlatform().
setNominalViewingTransform();
simpleU.addBranchGraph(scene);
}
abstract void setLighting(TransformGroup objMove);
}
Now light the scene using a javax.media.j3d.AmbientLight. As mentioned earlier, this is a source that you use to simulate outdoor lighting. There is little you can set in an AmbientLight object other than its color, the bounds of its influence, and whether or not it is on. The color of this light will be the same in later examples so that you can compare the various effects. Compile and run the following program, AmbientEx, and you will see a muted reddish purple uniformly applied to the spinning text.
import javax.media.j3d.TransformGroup;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.AmbientLight;
import javax.vecmath.Color3f;
public class AmbientEx extends Framework3D {
void setLighting(TransformGroup objMove) {
AmbientLight light = new AmbientLight();
light.setInfluencingBounds(new BoundingSphere());
light.setColor(new Color3f(1.0f, 0.0f, 0.5f));
objMove.addChild(light);
}
public static void main(String[] args) {
new AmbientEx().setVisible(true);
}
}
Contrast that effect with the light produced by javax.media.j3d.DirectionalLight. A directional light bring out the dimensionality of the text. The light acts as if it's emitted from a far away source, where all of the rays are parallel and traveling in the same direction. Unlike later examples, you do not specify the location of the source, but you do use the setDirection() method to indicate the path of the light rays. Compile and run the following program, DirectionalEx, and you will see a more vibrant color coupled with shadows. This coupling of color and shadows helps define the shape.
import javax.media.j3d.TransformGroup;
import javax.media.j3d.DirectionalLight;
import javax.media.j3d.BoundingSphere;
import javax.vecmath.Vector3f;
import javax.vecmath.Color3f;
public class DirectionalEx extends Framework3D {
void setLighting(TransformGroup objMove) {
DirectionalLight light =
new DirectionalLight();
light.setInfluencingBounds(new BoundingSphere());
light.setDirection(new Vector3f(0.6f, 1.0f, -1.0f));
light.setColor(new Color3f(1.0f, 0.0f, 0.5f));
objMove.addChild(light);
}
public static void main(String[] args) {
new DirectionalEx().setVisible(true);
}
}
You can use more than one DirectionLight at a time. In the following program, DirectionalEx2, a green light is used to fill the shadows cast by the purple light.
import javax.media.j3d.TransformGroup;
import javax.media.j3d.DirectionalLight;
import javax.media.j3d.BoundingSphere;
import javax.vecmath.Vector3f;
import javax.vecmath.Color3f;
public class DirectionalEx2 extends Framework3D {
void setLighting(TransformGroup objMove) {
DirectionalLight light =
new DirectionalLight();
light.setInfluencingBounds(new BoundingSphere());
light.setDirection(new Vector3f(0.6f, 1.0f, -1.0f));
light.setColor(new Color3f(1.0f, 0.0f, 0.5f));
objMove.addChild(light);
DirectionalLight light2 =
new DirectionalLight();
light2.setInfluencingBounds(new BoundingSphere());
light2.setDirection(new Vector3f(-0.6f, -1.0f, -1.0f));
light2.setColor(new Color3f(0.0f, 1.0f, 0.0f));
objMove.addChild(light2
);
}
public static void main(String[] args) {
new DirectionalEx2().setVisible(true);
}
}
You can also combine two different types of lights. In this case the AmbientLight is used to soften the shadows of the DirectionalLight.
import javax.media.j3d.TransformGroup;
import javax.media.j3d.AmbientLight;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.DirectionalLight;
import javax.vecmath.Color3f;
import javax.vecmath.Vector3f;
public class AmbientEx2 extends Framework3D {
void setLighting(TransformGroup objMove) {
AmbientLight light = new AmbientLight();
light.setInfluencingBounds(new BoundingSphere());
light.setColor(new Color3f(1.0f, 0.0f, 0.5f));
objMove.addChild(light);
DirectionalLight light2 = new DirectionalLight();
light2.setInfluencingBounds(new BoundingSphere());
light2.setDirection(new Vector3f(-0.6f, -1.0f, -1.0f));
light2.setColor(new Color3f(1.0f, 0.0f, 0.5f));
objMove.addChild(light2);
}
public static void main(String[] args) {
new AmbientEx2().setVisible(true);
}
}
The next type of light to consider is a javax.media.j3d.PointLight. This light behaves much like a bare incandescent bulb. Picture a point source that radiates out in all directions. You need to use setPosition() to place the source. You can also set the color as before. The other constraint you can set is the attenuation. This is how you influence how quickly the light fades as you move away from the source. There are three components to the attenuation. There is a constant component a, a linear component b, and a quadratic component c. A PointLight is attenuated by 1/(a + b x + c x2) where x is the distance between the light and the point being illuminated. By default, the attenuation equals one. When you run the following program, PointEx, you will see that the portions of the text that are nearer the light source are more brightly lit than those farther away.
import javax.media.j3d.TransformGroup;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.PointLight;
import javax.vecmath.Color3f;
public class PointEx extends Framework3D {
void setLighting(TransformGroup objMove) {
PointLight light = new PointLight();
light.setInfluencingBounds(new BoundingSphere());
light.setPosition(-0.5f, 0.5f, 0.8f);
light.setColor(new Color3f(1.0f, 0.0f, 0.5f));
light.setAttenuation(0f, 0f, 2f);
objMove.addChild(light);
}
public static void main(String[] args) {
new PointEx().setVisible(true);
}
}
A subclass of PointLight is javax.media.j3d.SpotLight. With a spot light you set the direction of the cone of light in addition to the placement and constraints that you can set with PointLight. You can set the spread angle, which is half of the cone angle. Finally, a spotlight has another means of attenuation. The light intensity can decrease the further the light ray is away from the axis of the light. In the program below, SpotEx, you use setPosition(), setDirection(), and setSpreadAngle() to configure the SpotLight object. You will only see a portion of the spinning text illuminated.
import javax.media.j3d.TransformGroup;
import javax.media.j3d.SpotLight;
import javax.media.j3d.BoundingSphere;
import javax.vecmath.Color3f;
public class SpotEx extends Framework3D {
void setLighting(TransformGroup objMove) {
SpotLight light = new SpotLight();
light.setInfluencingBounds(new BoundingSphere());
light.setPosition(0.0f, 0.0f, 2.0f);
light.setDirection(0.0f, 0.0f, -1.0f);
light.setSpreadAngle(.3f);
light.setColor(new Color3f(1.0f, 0.0f, 0.5f));
light.setAttenuation(0f, 0f, 1f);
objMove.addChild(light);
}
public static void main(String[] args) {
new SpotEx().setVisible(true);
}
}
In this tip you experimented with different types of light sources used in a Java3D application. You will most likely find that you get best results when you combine the different types, as appropriate. Just as a photographer will use a fill flash even on a bright and sunny day, mixing lights will improve the realism of the scenes you are creating.
In addition to considering the appearance of a scene you are lighting, you might also need to consider the performance. Although performance penalties can depend on the hardware on which you're running, the general rule for peak performance is to use the simplest possible lighting model. In general, this means that you should use fewer lights when possible. Also, local lights are more expensive than infinite lights. Finally, if you need to use a local light, you will find that a positional light is less expensive than a spot light.
For more information about lighting a 3D scene, see Chapter 6. Lights in the Java 3D API Tutorial.
|
|
 |
 |
|
|
 |
 |
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
"Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/developer/JDCTechTips/index.html
Copyright 2004 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.
|