C H A P T E R  4

Operators

Java includes many operators, from ordinary mathematical operators such as a minus sign (−) to operators that only make sense for object-oriented programming, such as instanceof. To start with, let's look at the list of operators in Table 4-1.

images

images

Operator Precedence

The first thing to know about operators is that they have precedence. I can't forget Mr. Smith in junior high algebra class teaching us to memorize Please Excuse My Dear Aunt Sally. That odd phrase is an effective mnemonic for the order of operations (another name for operator precedence) in algebra. It shortens to PEMDAS, which gives us Parentheses, Exponent, Multiplication, Division, Addition, and Subtraction. Thanks to operator precedence (and, in my case, Mr. Smith), when we work with algebra equations, we know to resolve parentheses before we resolve exponents, exponents before multiplication, and so on.

The same kind of thing holds true in Java (and many other programming languages). However, as shown previously, Java has a lot more than six operators. Also, Java has some operators that have the same level of precedence. In those cases, precedence proceeds from left to right for binary operators (except assignment operators) and right to left for assignment operators. That's probably as clear as mud, but we get to some examples shortly that clarify the order of operations and identify some of the problem spots where people often trip.

The Missing Operator: Parentheses

Parentheses aren't in the list of Java operators, but they act as an operator with the highest precedence. Anything in parentheses is resolved first. When a line has several sets of parentheses, they are resolved from innermost to outermost and from left to right. Let's consider some examples in Listing 4-1.

Listing 4-1. Parentheses as an operator

System.out.println(2 + 4 / 2); // division processed first, so prints 4
System.out.println((2 + 4) / 2); // addition processed first, so prints 3
System.out.println((2 + 4) / 3 * 2); // prints 4, not 1 – see below for why
System.out.println((2 + 4) / (3 * 2)); // prints 1
System.out.println((2 + 4) / (2 * 2)); // prints 1 – note the truncation
System.out.println((2.0 + 4.0) / (2.0 * 2.0)); // prints 1.5

The bolded line prints 4 because the division operator gets processed before the multiplication operator. That happens because operators of equal precedence get processed from left to right. In algebra, multiplication comes before division. However, Java is not algebra, and that sometimes trips up new Java developers who remember their algebra. (I've tripped over that difference at least once.)

The fifth line prints 1 because we use integer literals (as we covered in the previous chapter). Consequently, we get back an integer literal. Java truncates (that is, throws away) any remainder when dealing with integers, so 1.5 becomes 1. The last line uses floating-point literals, so it returns 1.5. A value of 1.9 would also be truncated to 1. Truncation is not a kind of rounding; a truncated value has any value to the right of the decimal place removed.

As a rule, remember to use parentheses to clarify your code and to ensure the proper order for your operations. I mention clarity first for a reason: Clarity helps a lot. If you can gain clarity by splitting a line onto multiple lines to make your operations clear for other developers (and for yourself when you return to the code at a later date), then do so. Your fellow developers will thank you if they can understand your code without a struggle.

Postfix Operators

The term postfix has a number of meanings in mathematics, linguistics, and other fields. In computer science, it means an operator that follows an expression. Java's two postfix operators increment (increase by one) and decrement (decrease by one) values. Listing 4-2 shows some examples:

Listing 4-2. Postfix operators

private int getC() {
  int c = 0;
  c++; // c = 1 now
  c--; // c = 0 now
  return c++; //returns 0;
}

So why does that return 0? Because the postfix operators first return the original value and then assign the new value to the variable. That particular language detail bites a lot of new Java programmers. To fix it, use the unary ++ operator (next on our list) before the expression rather than the postfix operator after the expression or move your return statement to its own line. Parentheses around the expression (c++) do not make this method return 1, by the way, because c would have been set to 0 within the parentheses.

Unary Operators

Strictly speaking, a unary operator is an operator that takes just one operand. By that definition, the postfix operators are also unary operators. However, Java distinguishes between the postfix operators and the other unary operators. As we learned previously, the postfix operators return the value before the postfix operation has been applied. The unary operators return their values after the operator has been applied. Table 4-2 briefly describes the unary operators (other than the postfix operators):

images

Let's consider a code example that exercises each of the unary operators (see Listing 4-3):

Listing 4-3. Unary operators

byte a = 0;
++a; // unary prefix increment operator - now a has a value of 1
--a; // unary prefix decrement operator - back to 0
byte b = +1; // unary plus operator (unnecessary)
byte c = -1; // unary minus operator to create a negative number
System.out.println(~b); // bitwise complement operator - prints -2
System.out.println(~c); // bitwise complement operator - prints 0
boolean myCatScratchesTheCouch = false;
System.out.println(!myCatScratchesTheCouch); // logical complement operator - prints true
Understanding the Bitwise Complement Operator

Java (and all programming languages) store values in one or more bytes, and each byte consists of 32 bits. When we talk about bytes in this context, we mean the units computers use for memory. That's not the same as Java's data type called “Byte” (which has a minimum value of -128 and a maximum value of of 127). The JVM stores a value of one as the following binary string: 00000000000000000000000000000001 (a 32-bit binary value that consists of 31 zeroes and a single one). When you use the bitwise complement operator on it, the JVM turns all the zeroes into ones and all the ones into zeroes, resulting in 11111111111111111111111111111110, which evaluates to -2. Similarly, -1 in binary is 11111111111111111111111111111111. Because that's all ones, the bitwise complement operator turns it to all zeroes, so its value is 0.

I won't blame you if you think that's all meaningless trivia, but the bitwise complement operator does have real-world uses. For example, in graphics programming, the color white is generally represented by all the bits being 1. Applying the bitwise complement operator sets all the bits to 0, which generally indicates the color black. The same principle applies to other colors (which have various bits set to 0 or 1). In this fashion, a graphics program can quickly create a negative of an image without any mathematical processing.

The bitwise complement operator promotes the values of byte, short, and char variables to 32 bits before applying the ~ operator. The result of the operator is an int in those cases. That's why the binary strings have 32 characters. This process is called unary numeric promotion. Consider the following code in Listing 4-4:

Listing 4-4. Unary numeric promotion

byte a = 0;
Byte b = new Byte(a); // no problem
Byte c = new Byte(~a); // won't compile because the Byte constructor cannot accept an int
int d = ~a; // no problem

Casting

Software developers often find that they need a variable of one type to be a variable of another type. In Java (and most programming languages), you can't change the type of a variable. Instead, you should create a new variable and convert the existing variable into the new variable's type. That process is called casting, and Java provides an operator of sorts for doing it. I say, “of sorts,” because the casting operator differs according to the data type to which you're casting. In particular, you wrap parentheses around the name of the type to which you're casting and put that operator before the value you want to cast. Casting is often necessary to prevent the compiler from throwing errors when we convert one data type to another. As ever, examples go a long way toward clarifying things (see Listing 4-5).

Listing 4-5. Casting

// Cast a byte to an int
byte b = 123;
int bInt =  b; // no casting necessary

// Cast an int to a short
int i = 123;
short s = (short) i; //(short) is the casting operator – beware of values that are too large

// Cast a float to an int
float f = 12.34f;
int floatInt = (int) f; // floatInt = 12 – the original value is truncated

// Cast a char to a String – oops
char c = 'c'; // can't directly cast a char to a string
Character cChar = new Character(c); // so get a Character wrapper object for our char
String s = cChar.toString(); // and get a String object from the wrapper

In the first example shown previously, casting a byte to an int does not require a cast operator. Because there's no possibility of data loss (an int can hold any value that a byte can hold), the JVM does what's sometimes called “an implicit upcast”—implicit because you don't have to add any syntax to make it happen and upcast because you've gone from smaller to larger in terms of both value range and number of bits. It's also called a widening cast.

In the second example, casting an int to a short, we must have a cast operator, to tell the compiler that we really mean to do that. That's necessary because data loss can occur when casting from a type with more bits (32 in this case) to a type with fewer bits (16 in this case). Suppose i equals 65537. s would then equal 1. That's because the maximum value of a short is 65536. The JVM divides the original value by the maximum value of the variable you're casting into and returns the remainder. As you can imagine, that can produce havoc and be hard to track down. As a rule, don't use narrowing casts (which place the value of a data type with more available bits into a data type with fewer available bits). Widening casts (which place the value of a data type with fewer available bits into a data type with more available bits) aren't a problem (though don't do it unless you have a reason for it), but narrowing casts are dangerous.

The third example shows another hazard involved in casting: loss of precision. If you cast a float or a double to an integral type (any of the numeric types with no floating point component), the JVM truncates (that is, removes) the mantissa. So 12.34 becomes 12. Even if it was originally 12.999, it would get truncated to 12. In other words, it doesn't round; it removes. Similarly, casting from a double to a float can lose precision, because a double has more bits than a float and can therefore have greater precision. Again, narrowing casts are risky. Hard-to-find errors can arise from narrowing casts. Don't do it unless you must, and it's a good idea to add some defensive code (which we get to shortly).

images Caution Widening casts are fine; however, narrowing casts are dangerous. Take steps to ensure that your narrowing casts can't receive values that cause trouble.

The fourth example isn't a cast, but it shows how to get from a primitive (a char in this case) to a String. The same pattern applies for all the primitives: First get a wrapper for the primitive and then use the wrapper's toString method.

I mentioned defensive coding for narrowing casts. Defensive coding is a good idea any time you can't be sure the provided value won't cause a problem. In fact, in some kinds of applications (distributed applications are a prime example), it's standard practice to validate (that is, ensure workable values for) all the arguments to a method. Listing 4-6 is an example of defensive coding for a narrowing cast from an int to a byte:

Listing 4-6. Defensive coding for a narrowing cast

private static byte intToByte(int i) {
  if (i > Byte.MAX_VALUE || i < Byte.MIN_VALUE) {
    throw new IllegalArgumentException("integer argument " +
        "is too large or too small to cast to a byte");
  }
  return (byte) i;
}

Then the code that calls intToByte can decide what to do about the problem. Moving the validation to its own method can be a good idea, both to encapsulate each bit of validation and to permit multiple methods to make use of the same bit of validation. Many systems have validation classes (and sometimes packages) that offer a number of such methods, so that the methods in other classes can make use of consistent validation. You get used to that kind of design as you learn to use object-oriented languages, including Java.

Multiplicative Operators

Java has three multiplicative operators: multiplication (*), division (/), and modulus (%). (The modulus operator is often called mod or modulo; in Java shops, you hear expressions such as “a mod b” when someone reads code aloud.)

As I mentioned earlier when discussing parentheses, there's no implicit order of operations between these three operators. To the JVM, they all have the same precedence. Because that's the case, the JVM processes them from left to right. Again, Java isn't algebra, though some of the operators and concepts exist in both.

Multiplication and division are obvious enough, but let's look at the modulus operator. As ever, examples help a lot (see Listing 4-7).

Listing 4-7. Modulus operator examples

int a = 9;
int b = 2;
int c = a % b; // c equals 1
float f = 1.9f;
float g = 0.4f;
float h = f % g; // h equals 0.3 – but beware of rounding

As this brief listing shows, the modulus operator divides the first operand by the second operand and returns the remainder.

images Caution Beware of rounding when using the modulus operator on floats and doubles. I rounded this to 0.3, but my JVM actually assigned 0.29999995 to h when I ran this bit of code in Eclipse. That might not matter if you're plotting the location of an avatar in a video game (because the screen has a fairly low number of pixels, so the error isn't large enough to put the avatar in the wrong spot). However, imagine the same error in code that controls a rocket going to Mars. Then it's a large enough error to ensure that your rocket doesn't end up in the right spot to go into orbit, and that's an expensive error indeed. Rounding tends to be problematic in many applications.

Additive Operators

You might not think I'd have much to say about plus (+) and minus (−). However, even they have subtleties worth noting when used in a Java application. As with the multiplicative operators, the order of precedence for the additive operators is the same, so they get processed from left to right, even when the minus precedes the plus operator on a line. That usually doesn't matter. However, when it does matter, it can be a difficult problem to spot.

Also, the plus sign is the string concatenation operator. As we see in various examples, someString + someOtherString = aThirdString. Strictly speaking, the addition operator and the string concatenation operator are different operators. However, they use the same character. The JVM figures whether to use a plus sign as the addition operator or as the string concatenation operator by context. If the JVM determines that the context is numeric, it performs addition operations. If the JVM determines that the context is textual, it performs concatenation operations. Listing 4-8 demonstrates what happens when the JVM encounters different contexts:

Listing 4-8. The ShiftDemo context switching example

int a = 1;
int b = 2;
int c = 3;
System.out.println(a + b + c);
System.out.println("a + b + c = " + a + b + c);
System.out.println("a + b + c = " + (a + b + c));

When run in a program, that code produces the following output (see Listing 4-9):

Listing 4-9. Contect switching example output

6
a + b + c = 123
a + b + c = 6

The context for the first line is numeric, because the first value processed by the println method is numeric. The context for the second line is textual because the first value processed by the println method is a String literal. The third line gets the right value because the parentheses force the addition to happen first, even though the context is textual.

Shift Operators

The shift operators take us back to working with bits. The shift operators require two operands: The integral (no floating-point values allowed) value to shift and the number of places to shift the bits that comprise the value. The signed left shift operator (<<) shifts bits to the left. The signed right shift operator (>>) shifts bits to the right. The signed right shift operator (>>>) shifts bits to the right and fills the left bits with zeroes.

images Note The shift operators work only on integer values.

Listing 4-10 demonstrates what the shift operators do to the value of an int variable.

Listing 4-10. ShiftDemo

package com.apress.javaforabsolutebeginners .examples.shiftDemo;

public class ShiftDemo {

  public static void main(String[] args) throws Exception {
    int b = 127;
    System.out.println("b: " + b);
    System.out.println("b as binary: " + Integer.toBinaryString(b));
    String leftShiftString = Integer.toBinaryString(b<<3);
    System.out.println("binary after signed left shifting 3 places: " +
      leftShiftString);
    System.out.println("value of b after signed shifting left 3 places: " +
      Integer.parseInt(leftShiftString, 2));
    String rightShiftString = Integer.toBinaryString(b>>3);
    System.out.println("binary after signed shifting right 3 places: " +
      rightShiftString);
    System.out.println("value of b after signed shifting right 3 places: " +
      Integer.parseInt(rightShiftString, 2));
    String unsignedRightShiftString = Integer.toBinaryString(b>>>3);
    System.out.println("binary after unsigned shifting right 3 places: " +
      unsignedRightShiftString);
    System.out.println("value of b after unsigned shifting right 3 places: " +
      Integer.parseInt(unsignedRightShiftString, 2));
    b = -128;
    System.out.println("Resetting b to " + b);
    System.out.println("b as binary: " + Integer.toBinaryString(b));
    unsignedRightShiftString = Integer.toBinaryString(b>>>3);
    System.out.println("binary after unsigned shifting right 3 places: " + unsignedRightShiftString);
    System.out.println("value of b after unsigned shifting right 3 places: " +
      Integer.parseInt(unsignedRightShiftString, 2));
  }
}

Running ShiftDemo produces the following output (see Listing 4-11):

Listing 4-11. ShiftDemo output

b: 127
b as binary: 1111111
binary after signed left shifting 3 places: 1111111000
value of b after signed shifting left 3 places: 1016
binary after signed shifting right 3 places: 1111
value of b after signed shifting right 3 places: 15
binary after unsigned shifting right 3 places: 1111
value of b after unsigned shifting right 3 places: 15
Resetting b to -128
b as binary: 11111111111111111111111110000000
binary after unsigned shifting right 3 places: 11111111111111111111111110000
value of b after unsigned shifting right 3 places: 536870896

ShiftDemo and its output reveal a number of things worth knowing about the shift operators:

  • The left-hand operator represents the value to be shifted, and the right-hand operator indicates the number of bits by which to shift (known as the shift distance).
  • Using a shift operator on byte, char, or short values promotes those values to int values. Unary numeric promotion strikes again. It's unary because both operands are separately promoted before the operation is performed. So, a byte value shifted by another byte value leads to two separate unary promotions before the shift operation happens. Sometimes, that doesn't matter. Other times, you might need to account for the promotion when assigning the result of the shift to another variable.
  • Signed shifting causes trouble with negative values. In the ShiftDemo example, I didn't use the signed shift operators (<< and >>) because doing so on that value produces an exception. That happens because of the way Java stores negative numbers; shifting the bits that comprise a negative number results in a binary string that Java can't recognize as any number.
  • Integer.toBinaryString shows the minimum possible number of digits. In many of the values shown by the ShiftDemo results, the internal representation is 32 bits long, with all the bits to the left of the value shown being 0 (that is, those values are 0-padded). That's why the binary strings for the unsigned right shift operator are 32 bits long.
  • When used on a relatively small negative number, the unsigned shift operator can create a large positive number. That happens because the result of the operation is a string of binary digits that the JVM can recognize as a number. However, that large number might not be what you have in mind. And that takes us to our last point:
  • Shifting is tricky. Test very carefully when you use the shift operators.

So why use the shift operators? Because they are the fastest possible operators. The simplest and fastest operation a computer can do is to shift a binary value. If you can be sure that shifting produces the values you want or if you happen to be working with values that aren't really values but rather collections of switches (such as graphics settings), the shift operators can provide efficient ways to manipulate those values or switches. We cover the idea of using a value as a series of switches when we get to the bitwise operators, later in this chapter.

Relational Operators

The relational operators compare things to one another. In particular, they determine whether one value is greater than, less than, equal to, or not equal to another value. For the relational operators to work, the items being compared have to be comparable. That sounds obvious, but it has a particular meaning in Java. The language specification defines what it means to be comparable for the primitives. Thus, you can compare an int to a float and get a meaningful result (the JVM promotes the int to a float and then does the comparison). So long as one or the other can be cast to the other value, Java can meaningfully compare primitives, and Eclipse tells you when they can't be.

> (greater than), < (less than), >= (greater than or equal to), and <= (less than or equal to) all work on primitives, but they don't work on objects. Conversely, instanceof works on objects but not on primitives. Java does provide ways to compare objects to one another, but not through any operators. We get to comparing objects later in this chapter.

Listing 4-12 illustrates the comparison of primitives

Listing 4-12. Comparing primitives

int a = 0;
float b = 1.0f;
System.out.println(a > b);

That bit of code prints “false” in the console.

The instanceof operator can be a bit tricky, because a class that extends another class is also an instance of the parent class. Consider the following small program, implemented in three classes (see Listing 4-13).

Listing 4-13. instanceof test

package com.apress.javaforabsolutebeginners .examples.instanceofTest;

public class Person {
  String firstName;
  String lastName;

  public Person (String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

package com.apress.javaforabsolutebeginners .examples.instanceofTest;

public class Student extends Person {
  String schoolName;

  public Student (String firstName, String lastName, String schoolName) {
    super(firstName, lastName);
    this.schoolName = schoolName;
  }
}


package com.apress.javaforabsolutebeginners .examples.instanceofTest;

public class InstanceofTest {

  public static void main(String[] args) {
    Student student = new Student("Sam", "Spade", "Noir U");
    System.out.println(student instanceof Student);
    System.out.println(student instanceof Person);
  }

}

Both print statements print “true” in the console. After all, a student is a person. The nature of object-oriented programming, where one class always extends another, demands that the instanceof operator works this way.

The instanceof operator can cause subtle problems. In particular, the instanceof operator won't throw an error if its right-hand operand is null. Instead, it always returns false in those cases. If you use something like myClass instanceof myBaseClass and have inadvertently set myBaseClass to null, the result is false. You might then think that myClass isn't an instance of myBaseClass, but you can't actually know that if you're comparing to null. This is another great place to practice defensive programming and make sure you're not getting null there.

If you're careful to make sure you know the relationships between the classes you use (which is part of being a good object-oriented programmer) and to ensure that you're not comparing to null, you shouldn't get into too much trouble with the instanceof operator.

Equality Operators

Java has two equality operators: == (equals) and != (not equals). (The exclamation point, both in this context and in others, is sometimes called a “bang” by software developers.) The equality operator (==) and the inequality operator (!=)work the same way (each is the negative of the other), so I'll confine this discussion to the equality operator.

Java uses a single equal sign as the base assignment operator (we get to the assignment operators later in this chapter), so it can't use a single equal sign for comparisons. Consequently, Java (and the other languages that share Java's basic syntax) uses two equal signs.

A common mistake among folks new to Java (and not-so-new folks who aren't careful with their typing) is to use a single equal sign (the assignment operator) when they should use two equal signs (the equality operator) and accidentally assign a value to a variable rather than compare the variable's value to something else. Consider the following example in Listing 4-14:

Listing 4-14. Accidental assignment

for (int counter = 0; counter < 10; counter++) {
  if (counter = 0) { // should be == rather than =
    // do something
  }
}

That bit of code, if not corrected, causes an infinite loop. In every iteration, counter gets set to 0, so its value is always less than 10. Fortunately, Eclipse and other modern development tools warn you when you do this. You can still do it, but at least you do it intentionally (though you really should re-structure your code if you need to reset a value). I have to admit that I have gotten into exactly that situation more than once (because of poor typing), though never since I started using Eclipse, thanks to its warnings. That's one of the reasons I had you install Eclipse at the start of this journey.

For primitives, the equality operators work exactly as you probably think they would. However, for objects, the equality operators indicate whether two references refer to the same instance. As ever, examples help (see Listing 4-15).

Listing 4-15. Equality operator examples

int a = 0;
int b = 1;
String s = "s";
String sToo = "s";
System.out.println(a == b);
System.out.println(s == sToo);

That bit of code prints “true” for a == b and “false” for s == sToo. That's because s and sToo are references to different instances of the String object. So, even though they have the same value, they are not equal in the eyes of the equality operators. Also, s == "s" prints false, because the string literal produces yet another instance of the String class.

To compare objects (including instances of the String class), Java programmers use the equals method. Any object whose instances can be compared to each other should implement the equals method (which is defined in the Object class). Because it makes sense for String objects to be compared to one another, the String class implements the equals method. That way, we can use code similar to Listing 4-16 to compare instances of String.

Listing 4-16. Testing equality for objects

String s = "s";
String sToo = "s";
System.out.println(s.equals(sToo));
System.out.println(s.equals("s"));

Both of these print statements produce “true” in the console. We cover comparing objects in more depth later in this chapter.

Bitwise AND Operator (&)

The bitwise AND operator (&) compares two binary values and sets each bit to 1 where each bit in the two values being compared is 1. As usual, an example helps (see Listing 4-17).

Listing 4-17. Bitwise AND operator (&)

Byte byte1 = Byte.parseByte("01010101", 2); // byte1 = 85
Byte byte2 = Byte.parseByte("00111111", 2); // byte2 = 63
int result = byte1 & byte2; // result = 21

The value of result is 21 because taking the bits where both bits in the compared value is 1 gives a binary result of 00010101, which happens to be the binary representation of 21.

Bitwise Exclusive OR Operator (^)

The bitwise Exclusive OR (often shortened to XOR) operator (^) compares two binary values and sets each bit to 1 if the bits differ. Again, examples make the best explanations for this kind of thing (see Listing 4-18).

Listing 4-18. Bitwise Exclusive OR operator (^)

Byte byte1 = Byte.parseByte("01010101", 2); // byte1 = 85
Byte byte2 = Byte.parseByte("00111111", 2); // byte2 = 63
int result = byte1 ^ byte2; // result = 106

The value of result is 106 because taking the bits where either bit in the compared values is 1 gives a binary result of 01101010, which happens to be the binary representation of 106.

Bitwise Inclusive OR Operator (|)

The bitwise Inclusive OR operator (|) compares two binary values and sets each bit to 1 if either bit is 1. Again, examples make the best explanations for this kind of thing (see Listing 4-19).

Listing 4-19. Bitwise Inclusive OR operator (|)

Byte byte1 = Byte.parseByte("01010101", 2); // byte1 = 85
Byte byte2 = Byte.parseByte("00111111", 2); // byte2 = 63
int result = byte1 | byte2; // result = 127

The value of result is 127 because taking the bits where either bit is 1 in the compared values is 1 gives a binary result of 01101010, which happens to be the binary representation of 127.

You might ask when anyone would ever use these bitwise operators. They have a number of useful applications, actually. Game developers often use bitwise operations for high-speed graphics processing. Again, imagine a game in which two objects merge somehow (maybe shooting a blue object with a red bullet makes the object turn purple). The bitwise and operator (&) provides a high-speed way to determine the new color. The bitwise operators are also often used to see which combination of mouse buttons have been pressed or to see whether a mouse button has been held down while the mouse was moved (creating a drag operation). So they might seem like oddball operators, but they definitely have their uses.

Logical AND Operator (&&)

The logical AND operator (&&) returns true if both arguments are true and false if either one is false. It's most often used within if statements but is handy anywhere you need to be sure that two boolean values are true. Also, the logical AND operator is one of the most-often used operators. If we had a nickel for every time we typed &&....

Listing 4-20 is an example.

Listing 4-20. Logical AND operator (&&)

if (2 > 1 && 1 > 0) {
  System.out.println("Numbers work as expected");
}

In that code snippet, we have three operators. Thanks to operator precedence, the two comparison operators (>) get evaluated before the logical AND operator (&&), so we don't need parentheses to make things work correctly (though you might want parentheses for clarity, depending on your coding style).

Also, you can chain multiple logical AND operators (and most other operators), as shown in Listing 4-21.

Listing 4-21. Chaining logical AND operators

if (3 > 2 && 2 > 1 && 1 > 0) {
  System.out.println("Numbers work as expected");
}

The result of the middle comparison (2 > 1) serves as an operator to both of the logical AND operators. That kind of chaining is common in all programming problems, regardless of language. We're fortunate that Java makes it easy to do.

Logical OR Operator (||)

The logical AND operator (||) returns true if either of its arguments are true and false only if both arguments are false. Otherwise, it works just like the logical AND operator.

Listing 4-22 is an example.

Listing 4-22. Logical AND operator (&&)

if (2 > 1 || 1 < 0) {
  System.out.println("Numbers work as expected");
}

As with logical AND operators, you can chain multiple logical AND operators, as shown in Listing 4-23.

Listing 4-23. Chaining logical AND operators

if (3 > 2 || 2 < 1 || 1 < 0) {
  System.out.println("Numbers work as expected");
}

images Note Eclipse warns us that we have dead code within the comparisons when we run this code. Because 2 is always greater than 1, the JVM won't try to compare 1 to 0, so Eclipse tells us that we can remove that comparison. Of course, real-world examples check to make sure that a data stream isn't null and has at least some content. Listing 4-24 contains that code.

Listing 4-24. A more realistic use for the logical OR operator

if (dataStream == null || dataStream.length == 0) {
  log.error("No data available!");
}

Again, the equality operators have higher precedence, so we don't need additional parentheses.

Assignment Operators

The assignment operators set values and assign object references. The basic assignment operator (=) is by far the most often used operator. Every time we put something like int = 0 (assigning a value to a primitive) or Date now = new Date() (assigning an object reference to a variable) into an example, we use the basic assignment operator.

Java provides a number of compound assignment operators (often called shortcut operators). Listing 4-25 is an example of one of the compound assignment operators.

Listing 4-25. Compound assignment operator

int myInt = 2;
myInt *= 2; // equivalent to myInt = myInt * 2;
System.out.println(myInt); // prints 4;

Each of the compound operators applies the first operator within the compound operator and the value after the compound operator and then applies the assignment operator. In the previous example, the multiplication operator is applied to myInt with the value after the compound operator (in this case, doubling the value of myInt) and then sets myInt to the result.

The compound assignment operators can lead to unexpected results. Consider the following code snippet in Listing 4-26. Before reading past the snippet, ask yourself “What is the value of myInt at the end?”

Listing 4-26. Compound assignment problem

int myInt = 2;
myInt *= (myInt = 4);
System.out.println(myInt);

The last assignment before the print statement is myInt = 4, so you might think the answer must be 4. But we set myInt to 4 and then multiply it by itself, so you might think the answer must be 16. In fact, neither is correct. The code snippet prints 8. It might seem odd at first, but it makes sense once you understand how the JVM deals with a compound operator. When the JVM encounters the compound operator, it knows myInt equals 2. Until the entire operator is resolved, the value of myInt can't be changed. (This is the only case we know of where parentheses aren't processed first, by the way.) Consequently, the assignment within the line is ignored, but the value within the assignment is used. After all that, the result is 2 times 4, or 8.

images Note This issue is pretty much an academic curiosity. It's an issue the developers of the Java language had to account for, but smart programmers don't write code like this. Smart programmers break up their operations so that the effect of each line is clear with minimal interpretation. Remember, if you program for a living, other programmers have to read your code and understand it, and very few development shops will tolerate this kind of confused and confusing code.

Table 4.3 shows all the assignment operators, including the compound operators.

images

Comparing and Sorting Objects

As I indicated previously, comparing objects differs from comparing primitives. The comparison operators work for primitives, but they do not work for objects. Instead, Java requires the use of a number of different interfaces and methods to compare objects.

Implementing the equals Method

The first comparison is to see whether one object is equal to another. As we saw previously, the equality and inequality operators determine only whether two objects use the same object reference (that is, the same place in memory). Consequently, two different objects, each with its own memory address, are never equal. The following class might help illustrate the issue (see Listing 4-27).

Listing 4-27. Using the equality operator with objects

package com.apress.javaforabsolutebeginners .examples.comparing;

public class CompareTest {

  public static void main(String[] args) {
    Object a = new Object();
    Object b = new Object();
    Object c = b;
    System.out.println(a == b);
    System.out.println(b == c);
  }

}

That code prints “false” for the first comparison and “true” for the second. In essence, we create a single object with two names (b and c), so the second comparison yields a value of “true.” The new keyword offers a big hint here. For c, we don't create a new object, just another reference to an existing one. To see whether two objects are equal, we have to compare objects that implement the equals method. Let's return to our Person class and expand it to have an equals method (see Listing 4-28).

Listing 4-28. Person class with equals method

package com.apress.javaforabsolutebeginners .examples.comparing;

public class Person {
  String firstName;
  String lastName;

  public Person (String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  public boolean equals(Person p) {
    if (p == null) {
      return false;
    }
    if (p == this) {
      return true;
    }
    if (!(p instanceof Person)) {
      return false;
    }
    if (p.lastName.equals(this.lastName)
        && p.firstName.equals(this.firstName)) {
      return true;
    } else {
      return false;
    }
  }

  public int hashCode() {
    int result = 17;
    result *= firstName.hashCode() * 37;
    result *= lastName.hashCode() * 37;
    return result;
  }
}

Let's examine that equals method. I followed a standard recipe for implementing it. (The Java community has best practices for many things, and we show them to you whenever they come up, as here.) First, I check for null, just to make sure we have an object. Then I check to see whether it's the same object, in which case they are certainly equal. Then we check to make sure that, if we do have an object, it's a Person object. If not, the comparison is always false. But remember that other classes might extend our Person class, so a Student object might be equal to our Person object. Because Student extends Person, this equals method works for both. However, Student might implement its own equals method to account for the school each student attends. Finally, I check all the relevant fields within the class. If they are all equal, it must be the same person. Naturally, a real-world Person class probably includes middle name, address fields, social security number, and possibly even ancestors and descendants. Two fields will do for the sake of a demonstration, though. Now, let's rewrite CompareTest to use our new Person object (see Listing 4-29).

Listing 4-29. Checking people objects for equality

package com.apress.javaforabsolutebeginners .examples.comparing;

public class CompareTest {

  public static void main(String[] args) {
    Person samSpade = new Person("Sam", "Spade");
    Person greatNoirDetective = new Person("Sam", "Spade");
    System.out.println(samSpade == greatNoirDetective);
    System.out.println(samSpade.equals(greatNoirDetective));
  }

}

CompareTest now prints “false” and then “true,” even though samSpade and greatNoirDetective are different references to different Person objects. That's because our equals method isn't comparing references but rather the relevant fields of the two objects.

The hashCode and equals methods are closely related because they rely on the same premise. They add together a set of values to create a value that can be used for comparison (an equals-to comparison in the case of the equals method and equals-to, greater-than, and less-than comparisons in the case of the hashCode method).

images Note When you implement the equals method, you must also implement the hashCode method. The hashCode method determines where an object reference goes when put into a collection that uses a hashing algorithm (such as the HashMap and HashTable classes). We address those two collections (and others) when we get to data structures, later in the book. The goal of the hashCode method is to return a unique (or at least nearly unique) identifier; that's why it starts with a prime number and multiplies by another prime number. Again, we get to that in much greater detail.

images Caution When you implement both equals and hashCode (and you should implement both if you implement one), you must make sure that they use the same fields. For example, if you use firstName and lastName in the equals method, you must use firstName and LastName in the hashCode method. Otherwise, your comparisons fail. Worse yet, you don't get exceptions; you get the wrong behavior and a hard-to-find bug.

images Caution The hashCode method must generate equal values for equal objects. Otherwise, comparisons that should succeed fail, and you have another hard-to-find bug.

Comparisons for Sorting

I include sorting in this topic because it's impossible to sort things without comparing them. Suppose you want to sort a bunch of colorful rocks into the spectrum. To do so, you pick up each rock, compare it to the other rocks, and use that information to decide where in the row of rocks each rock belongs. Sorting comes to most people pretty readily, with little training. However, a computer has to be told exactly how to do it. To that end, Java offers two mechanisms for creating comparisons that can be used for sorting: implementing the compareTo method from java.util.Comparable and creating a class that extends java.util.Comparator. We do both for our Person class. Classes don't have to implement both, but many do. The String class offers a fine example of a class that implements both comparison interfaces (and equals and hashCode), by the way.

To be able to compare objects with the goal of sorting them, we have to know more than whether one object equals another. We also have to know whether one object is greater than or less than another object. For our color-sorting example, we can assign an integer value to each color and then sort our rocks by putting each one to the left of all the rocks its color value is greater than and to the right of all the rocks its color value is less than. Rocks with the same color value make piles of rocks whenever that happens (and that's comparable to what happens in Java when hash code values are identical).

Implementing java.lang.Comparable

Another best practice acknowledged by the Java community is to use java.lang.Comparable for comparisons that are “natural” to the object. People naturally sort themselves by name (just look at a phone book), so it makes sense to implement java.lang.Comparable and have its compareTo method tell us whether a name is less than, equal to, or greater than another person's name. Listing 4-30 shows our Person class again, with the addition of implementing java.lang.Comparable. For our Person example, we get the numeric value of each String field, add them together, and use that as the numeric value of our Person object.

Listing 4-30. Person class implementing java.lang.Comparable

package com.apress.javaforabsolutebeginners .examples.comparing;

public class Person implements Comparable<Person> {
  String firstName;
  String lastName;
    public Person (String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  public boolean equals(Person p) {
    if (p == null) {
      return false;
    }
    if (p == this) {
      return true;
    }
    if (!(p instanceof Person)) {
      return false;
    }
    if (p.lastName.equals(this.lastName)
        && p.firstName.equals(this.firstName)) {
      return true;
    } else {
      return false;
    }
  }

  public int hashCode() {
    int result = 17;
    result *= firstName.hashCode() * 37;
    result *= lastName.hashCode() * 37;
    return result;
  }

  public int compareTo(Person p) {
    int thisTotal = firstName.hashCode() + lastName.hashCode();
    int pTotal = p.firstName.hashCode() + p.lastName.hashCode();
    if (thisTotal > pTotal) {
      return 1;
    }
    if (thisTotal < pTotal) {
      return -1;
    }
    // must be equal
    return 0;
  }
}

The contract that the compareTo method guarantees (as described in its documentation) is that it will return a positive number if the local object is greater than the object in the argument, a negative number if the local object is less than the object in the argument, and 0 if the two are equal. It's also important that, given the same object as an argument, the equals method returns true and the compareTo method returns 0. To do that, just use the same fields in both methods. If you need to break the rule, be sure to document it in the Javadoc for the compareTo method.

images Tip A common problem is to try to cast a class that implements java.lang.Comparable to a class of your own. Because they are in different packages (your class is in your own package and the other class is in the java.lang.Comparable package), you can't cast your class to the other class. Integer is probably the class that most often causes this problem, but any class that extends java.lang.Comparable has the same issue. So, if you see a ClassCastException, remember this particular problem, because it is a likely cause of the exception.

Remember that to compare Person objects, you need to create another class with a main method, set up a few Person objects, and then compare them. I include a class to do just that at the end of the chapter, but why not give it a try now? As with so many other things, you can't learn to program unless you program.

Implementing java.util.Comparator Although the Java community uses java.lang.Comparable for natural comparisons, we implement a Comparator when we want to compare objects in some arbitrary way. Also, because Comparator objects are themselves classes, it's possible to implement many different Comparator objects for the same class. Let's expand our Person object to have a field by which we probably wouldn't usually want to sort and then create a Comparator class to sort by it. That kind of thing happens pretty often, really, because it allows for grouping by not-so-obvious characteristics. Listing 4-31 shows the modified Person class.

Listing 4-31. Person class with a favorite book field

package com.apress.javaforabsolutebeginners .examples.comparing;

public class Person implements Comparable<Person> {
  String firstName;
  String lastName;
  String favoriteBook;
    public Person (String firstName, String lastName, String favoriteBook) {
    this.firstName = firstName;

    this.lastName = lastName;
    this.favoriteBook = favoriteBook;
  }

  public boolean equals(Person p) {
    if (p == null) {
      return false;
    }
    if (p == this) {
      return true;
    }
    if (!(p instanceof Person)) {
      return false;
    }
    if (p.lastName.equals(this.lastName)
        && p.firstName.equals(this.firstName)) {
      return true;
    } else {
      return false;
    }
  }

  public int hashCode() {
    int result = 17;
    result *= firstName.hashCode() * 37;
    result *= lastName.hashCode() * 37;
    return result;
  }

  public int compareTo(Person p) {
    // sort by last name first
    if (lastName.compareTo(p.lastName) > 0) {
      return 1;
    }
    if (lastName.compareTo(p.lastName) < 0) {
      return -1;
    }
    // last names must be equal
    // so compare first names
    if (firstName.compareTo(p.firstName) > 0) {
      return 1;
    }
    if (firstName.compareTo(p.firstName) < 0) {
      return -1;
    }
    // both names must be equal
    return 0;
  }
}

And Listing 4-32 shows the book comparator class.

Listing 4-32. Book comparator class

package com.apress.javaforabsolutebeginners .examples.comparing;

import java.util.Comparator;

public class BookComparator implements Comparator<Person> {

  public int compare(Person p1, Person p2) {
    return p1.favoriteBook.compareTo(p2.favoriteBook);
  }
}

In this case, all we have to do is use the String class's compareTo method. Note that we must tell the comparator what kind of thing to compare (with Comparator<Person>). Otherwise, we have to accept arguments of type Object and cast to objects of type Person.

images Note You don't need comparators for arrays of primitives or for collections of any objects that implement java.lang.Comparable (such as String and Integer). Those objects are already comparable.

Finally, Listing 4-33 shows CompareTest, expanded to use both kinds of comparison.

Listing 4-33. CompareTest using both comparisons

package com.apress.javaforabsolutebeginners .examples.comparing;

import java.util.ArrayList;
import java.util.Collections;

public class CompareTest {

  public static void main(String[] args) {
    Person samSpade = new Person("Sam", "Spade", "The Maltese Falcon");
    Person sherlockHolmes =
      new Person("Sherlock", "Holmes", "The Sign of the Four");
    Person johnWatson = new Person("John", "Watson", "A Study in Scarlet");
    Person drWatson = new Person("John", "Watson", "A Study in Scarlet");
    // compare the two that are really equal
    System.out.println(johnWatson == drWatson);
    System.out.println(johnWatson.equals(drWatson));
    System.out.println();
    System.out.println("Sorting by name");
    // Make a collection from our characters and sort them
    ArrayList<Person> characters = new ArrayList<Person>();
    characters.add(samSpade);
    characters.add(sherlockHolmes);
    characters.add(johnWatson);
    characters.add(drWatson);
    // sort by the natural values (uses compareTo())
    Collections.sort(characters);
    for (int i = 0; i < characters.size(); i++) {
      Person person = characters.get(i);
      System.out.println(person.firstName + " "
        + person.lastName + " likes " + person.favoriteBook);
    }
    System.out.println();
    System.out.println("Sorting by favorite book");
    // sort by book (uses the Comparator)
    Collections.sort(characters, new BookComparator());
    for (int i = 0; i < characters.size(); i++) {
      Person person = characters.get(i);
      System.out.println(person.firstName + " "
        + person.lastName + " likes " + person.favoriteBook);
    }
  }
}

And Listing 4-34 shows the output from our test class.

Listing 4-34. CompareTest output

false
true
Sorting by name
Sherlock Holmes likes The Sign of the Four
Sam Spade likes The Maltese Falcon
John Watson likes A Study in Scarlet
John Watson likes A Study in Scarlet
Sorting by favorite book
John Watson likes A Study in Scarlet
John Watson likes A Study in Scarlet
Sam Spade likes The Maltese Falcon
Sherlock Holmes likes The Sign of the Four

Summary

In this chapter, we learned:

  • Java has a large number of operators.
  • Java's operators have precedence (that is, some operators are processed before other operators).
  • Java has some seemingly odd operators (such as the bitwise operators) but that they all have their uses.
  • The details of each of the operators.
  • When and how to use the equals and hashCode methods.
  • How to compare objects with their natural values and how to compare objects with arbitrary values.

This chapter presents a lot of complex information in a fairly short space. I recommend reading it again. Also, as I mentioned before, there's no substitute for doing some programming. You should create a few classes that can be compared in various ways and do some fiddling with the less-obvious operators (such as the bitwise operators).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.212.212