Return-Path: Received: from po10.mit.edu ([unix socket]) by po10.mit.edu (Cyrus v2.1.5) with LMTP; Tue, 04 Feb 2003 13:27:03 -0500 X-Sieve: CMU Sieve 2.2 Return-Path: Received: from pacific-carrier-annex.mit.edu by po10.mit.edu (8.12.4/4.7) id h14IR280023047; Tue, 4 Feb 2003 13:27:02 -0500 (EST) Received: from hermes.sun.com (hermes.sun.com [64.124.140.169]) by pacific-carrier-annex.mit.edu (8.9.2/8.9.2) with SMTP id NAA08763 for ; Tue, 4 Feb 2003 13:27:01 -0500 (EST) Date: 4 Feb 2003 08:47:18 -0800 From: "JDC Tech Tips" To: alexp@mit.edu Message-Id: <296271702004187913@hermes.sun.com> Subject: Core Java Technologies Tech Tips, Feb. 4, 2003 (Variable Argument Lists, Floating-Point Arithmetic) Mime-Version: 1.0 Content-Type: text/html; charset=ISO-8859-1 Content-Transfer-Encoding: 7bit X-Mailer: SunMail 1.0 X-Spam-Flag: NO X-Spam-Score: 1.2, Required 7.5 X-Spam-Level: * (1.2) X-Scanned-By: MIMEDefang 2.28 (www . roaringpenguin . com / mimedefang) Core Java Technologies Technical Tips
image
image
Core Java Technologies Technical Tips
image
   View this issue as simple text February 4, 2003    

In this Issue

Welcome to the Core JavaTM Technologies Tech Tips, February 4, 2003. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SETM).

This issue covers:

Using Variable Argument Lists
Some Things You Should Know About Floating-Point Arithmetic

These tips were developed using Java 2 SDK, Standard Edition, v 1.4.

This issue of the Core Java Technologies Tech Tips is written by Glen McCluskey.

Pixel
Pixel

USING VARIABLE-LENGTH ARGUMENT LISTS

If you've done much C or C++ programming, you might have encountered some code that looks similar to this:

    #include <stdarg.h>
    #include <stdio.h>
    
    void dumpList(int n, ...) {
        va_list ap;
    
        va_start(ap, n);
    
        printf("count = %d: ", n);
        while (n-- > 0) {
            int i = va_arg(ap, int);
            printf("%d ", i);
        }
        printf("\n");
    
        va_end(ap); 
    }
    
    int main() {
        dumpList(1, 1);
    
        dumpList(3, 1, 2, 3);
    
        return 0;
    }

The dumpList function declares a single parameter (n), followed by an ellipsis (...). The ellipsis specifies that a variable number of additional parameters are present. This means that a caller of dumpList can pass an arbitrary number of arguments in addition to the first int argument. The various va_x types and functions are used to extract the parameter values in the dumpList function.

The output of this C program is:

    count = 1: 1 
    count = 3: 1 2 3 

The C/C++ variable argument mechanism is quite useful in certain contexts. But the mechanism is error-prone, because it defeats type checking. For example, if the second dumpList call is changed to:

    dumpList(3, 1, 2, 3.4);

the result is something like this:

    count = 1: 1 
    count = 3: 1 2 858993459

The demo program assumes that an int argument has been passed, when in fact a double (3.4) is passed.

The Java language has no direct equivalent to this feature. However, it's still sometimes useful to be able to pass a variable number of arguments to a method. This tip looks at several ways of doing that.

The first approach you might think of is to overload the dumpList method, like this:

    public class VarDemo1 {
        static void dumpList(Object obj1) {
            System.out.println(obj1);
        }
    
        static void dumpList(Object obj1, Object obj2) {
            System.out.println(obj1 + " " + obj2);
        }
    
        static void dumpList(Object obj1, Object obj2,
        Object obj3) {
            System.out.println(
               obj1 + " " + obj2 + " " + obj3);
        }
    
        public static void main(String[] args) {
            Object obj1 = new Integer(1);
            Object obj2 = new Integer(2);
            Object obj3 = new Integer(3);
    
            dumpList(obj1);
            dumpList(obj1, obj2, obj3);
        }
    }

The idea is that you have a variable number of object references to be passed as arguments, and dumpList methods are defined for each case. This approach will certainly work, but has some serious problems. One problem is that the approach is not general. The VarDemo1 program defines dumpList methods to handle one, two, or three arguments. But what if you have ten arguments that you want to pass? Do you keep adding new dumpList methods?

Another problem is that there can be a lot of code duplication between the various overloaded methods. This duplication implies a long-term maintenance headache.

A second approach to handling variable argument lists is to use an Object[] array, like this:

    public class VarDemo2 {
        static void dumpList(Object[] list) {
            for (int i = 0; i < list.length; i++) {
                System.out.print(list[i] + " ");
            }
            System.out.println();
    
            //list[0] = new Integer(4);
        }
    
        public static void main(String[] args) {
            Object[] veclist1 = {new Integer(1)};
            Object[] veclist2 = {new Integer(1),
                new Integer(2), new Integer(3)};
    
            dumpList(veclist1);
            dumpList(veclist2);
        }
    }

This approach works quite well. One example of its use is the java.text.MessageFormat class:

    import java.text.*;
    
    public class VarDemo3 {
        public static void main(String[] args) {
            String fmt = 
               "Error at line {0} of file {1}: {2}";
    
            Object[] arglist = {new Integer(37),
                "abc.java", "missing ("};
    
            String msg = 
               MessageFormat.format(fmt, arglist);
    
            System.out.println("msg = " + msg);
        }
    }

This class is a rough equivalent to the printf function illustrated in the first demo above. The embedded "{0}" and so on in the format string are references to the arguments passed to the format method in the Object[] array.

When you run this program, the result is:

    msg = Error at line 37 of file abc.java: missing (

The array approach works fairly well, but one drawback is that the called method can modify the array contents. The commented line of code in the VarDemo2 program illustrates this point. In general, such modification is undesirable.

A final approach uses the collections framework, like this:

    import java.util.*;
    
    public class VarDemo4 {
        static void dumpList(List list) {
            System.out.println(list);
    
            //list.set(0, new Integer(4));
        }
    
        public static void main(String[] args) {
            List list = new ArrayList();
    
            list.add(new Integer(1));
            list.add(new Integer(2));
            list.add(new Integer(3));
    
            list = Collections.unmodifiableList(list);
    
            dumpList(list);
        }
    }

This approach solves the problem of modifying the passed-in argument list. If you uncomment the "list.set" line, and run the program, it will give an exception. The exception is triggered because the list has been wrapped in an unmodifiable view, using Collections.unmodifiableList.

Using the collections framework has other advantages. For example, notice that the top of the dumpList method prints the whole list using a single statement, rather than using a loop. In general, by using collection features it's easier to operate on lists as lists, rather than an element at a time. Note that the collections framework has features for treating Object[] arrays as lists (Arrays.asList) and converting lists to arrays (ArrayList.toArray).

For more information about using variable argument lists, see section 16.6, List, 16.8.2, The Unmodifiable Wrappers, and 16.9, The Arrays Utility Class, in "The JavaTM Programming Language Third Edition" by Arnold, Gosling, and Holmes.

Pixel
Pixel

SOME THINGS YOU SHOULD KNOW ABOUT FLOATING-POINT ARITHMETIC

Suppose that you are doing some Java programming involving floating-point calculations. Suppose too that your code takes the value 0.1 and adds it to itself a couple of times, like this:

    public class Fp1 {
        public static void main(String[] args) {
            double val1 = 0.1;
            double val2 = 0.3;
    
            double d = val1 + val1 + val1;
    
            if (d != val2) {
                System.out.println("d != val2");
            }
    
            System.out.println(
               "d - val2 = " + (d - val2));
        }
    }

Everyone knows that:

    0.1 + 0.1 + 0.1 == 0.3

at least in mathematical terms. But when you run the Fp1 program, you discover that this equality no longer holds. The program output looks like this:

    d != val2
    d - val2 = 5.551115123125783E-17

This result seems to indicate that 0.1 + 0.1 + 0.1 differs from 0.3 by about 5.55e-17.

What is wrong here? The problem is that IEEE 754 floating-point arithmetic, as found in the Java language, doesn't use decimals to represent numbers. Instead it uses binary fractions and exponents. To understand what this means in practice, consider trying to write various decimal fractions as the sum of a series of powers of two:

    0.5 = 1/2

    0.75 = 1/2 + 1/4

    0.875 = 1/2 + 1/4 + 1/8

    0.1 = 1/16 + 1/32 + 1/256 + 1/512 + 1/4096 + 1/8192 + ...

The last series is infinite, which means that 0.1 is not exactly representable in floating-point format. The infinite series must be cut off at some point, rounded, and so on, in order to be represented in a 64-bit double value. This process results in some amount of error.

There are several ways to fix the equality testing problem illustrated above. You could check that d and val2 are close in value rather than exactly equal. Or you could use the BigDecimal class, and opt out of floating-point arithmetic entirely.

Here's another example where floating-point works differently from what you'd expect when applying math rules:

    public class Fp2 {
        public static void main(String[] args) {
            double val1 = 12345.0;
            double val2 = 1e-16;
    
            if (val1 + val2 == val1) {
                System.out.println(
                   "val1 + val2 == val1");
            }
        }
    }

The result of the Fp2 program is:

    val1 + val2 == val1

In mathematical terms, if:

    a > 0 && b > 0

then:

    a + b != a

But this program demonstrates behavior that violates this rule. The problem is with the precision of floating-point values. The precision is the actual number of bits used to represent the value (the binary fraction), and is distinct from the range of values that can be expressed using floating-point. For Java double values, there are 53 bits of precision, or about 16 decimal digits. The example above implies a precision of approximately 20 digits, since the addition can be rewritten as:

    1.2345e+4 + 1e-16

Because the precision of double values is not sufficient to represent the result of this addition, the 1e-16 is effectively ignored.

Here's another example that illustrates how the laws of math are violated:

    public class Fp3 {
        public static void main(String[] args) {
            double d1 = 1.6e308;
            double d2 = d1 * 2.0 / 2.0;
    
            System.out.println(d1);
            System.out.println(d2);
        }
    }

If d1 is multiplied and then divided by 2.0, the result should be d1, right? Unfortunately not. The result is actually this:

    1.6E308
    Infinity

The initial multiplication d1 * 2.0 overflows, and the division by 2.0 comes too late to do any good. The maximum double value is approximately 1.8e308. Multiplying 1.6e308 by 2.0 results in 3.2e308, which becomes positive infinity.

Let's look at another facet of floating-point arithmetic. If you've used integer arithmetic much, you know that division by zero triggers an ArithmeticException. But what about similar usage with floating-point? Here's an example:

    public class Fp4 {
        public static void main(String[] args) {
            System.out.println(1.0 / -0.0);
            System.out.println(1.0 / 0.0);
    
            System.out.println(0.0 / 0.0);
        }
    }

When you run this program, the output is:

    -Infinity
    Infinity
    NaN

The program throws no exceptions for floating-point division by zero. When a positive or negative value is divided by zero, the result is an infinity.

This example shows that there is, in fact, more than one zero value. There are negative and positive zeros. The sign of the zero is taken into account in deciding whether the result of the division is negative or positive infinity.

What about the third division, 0.0 / 0.0? The result of this operation is the value NaN (not a number).

Negative and positive zeros and NaN have some odd properties. Let's look at another example to help clarify this:

    public class Fp5 {
        public static void main(String[] args) {
            double d1 = -0.0;
            double d2 = +0.0;
            if (d1 < d2) {
                System.out.println("-0.0 < +0.0");
            }
    
            double d3 = 0.0 / 0.0;
            if (d3 == d3) {
                System.out.println("d3 == d3");
            }
        }
    } 

If you run the Fp5 program, you'll see that it prints nothing. As you saw in the previous example, the sign of a zero is propagated, for example 1.0 / -0.0 == -Infinity and 1.0 / +0.0 == +Infinity. But that's not the same as saying that -0.0 < +0.0, as the example above demonstrates (d1 is not less than d2, and so nothing is printed). Whether -0.0 comes "before" +0.0 depends on your perspective.

The Fp5 program also illustrates the point that NaN values are unordered, so that d3 is not equal to itself. This is the way that you write a method to tell whether a value is NaN. If the value is not equal to itself, then it is NaN. Such methods actually already exist -- see the Float.isNan and Double.isNaN methods.

Let's look at a final example that illustrates the ordering of some of the odd floating-point values discussed so far:

    public class Fp6 {
        static double[] list = {
            Double.NEGATIVE_INFINITY,
            -0.0,
            +0.0,
            Double.MIN_VALUE,
            Double.MAX_VALUE,
            Double.POSITIVE_INFINITY
        };
    
        public static void main(String[] args) {
            for (int i = 0; i < list.length; i++) {
                System.out.println(list[i]);
            }
        }
    }

The result of running the program is:

    -Infinity
    -0.0
    0.0
    4.9E-324
    1.7976931348623157E308
    Infinity

The list presents -0.0 before +0.0. As already mentioned, whether -0.0 really comes before +0.0 depends on your perspective.

NaN is not on the list because it is not ordered with respect to other values.

Note that Double.MAX_VALUE and Double.POSITIVE_INFINITY are distinct values.

This tip looked at a few of the properties of floating-point arithmetic. It pays to be careful when using floating-point because it behaves differently than you might expect if you simply apply the laws of mathematics.

For more information about floating-point arithmetic, see section 4.2.3, Floating-Point Types, Formats, and Values, in "The JavaTM Language Specification Second Edition" by Gosling, Joy, Steele, and Bracha. Also see the following IEEE-754 documents: Storage Layout and Ranges of Floating-Point Numbers and IEEE-754 Floating-Point Conversion

Pixel
Pixel
Pixel

Reader Feedback

  Very worth reading    Worth reading    Not worth reading 

If you have other comments or ideas for future technical tips, please type them here:

 

Have a question about JavaTM programming? Use Java Online Support.

Pixel
Pixel

IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies:
http://www.sun.com/share/text/termsofuse.html http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html


Comments? Send your feedback on the Core JavaTM Technologies Tech Tips to: jdc-webmaster@sun.com

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 (J2EETM).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2METM).

To subscribe to these and other JDC publications:
- Go to the JDC Newsletters and Publications 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/jdc/TechTips/index.html


Copyright 2003 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.


This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html


Sun, Sun Microsystems, Java, Java Developer Connection, J2SE, J2EE, and J2ME are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.

Sun Microsystems, Inc.
Please unsubscribe me from this newsletter.