Skip to main content

Basics of the Java operators

Posted by hellofadude on November 11, 2013 at 8:51 AM PST

Operators in Java work much like they do in mathematics, producing a value from one or more operands. An operand is any quantity on which an operation can be performed and in Java these include primitives and objects.

Basic arithmetic operators in Java include addition (+), subtraction (-), multiplication (*), division (/) and the assignment operator (=), all of which constitute the binary operators, and, with the exception of the assignment operator, are evaluated from left to right. The assignment operator is evaluated from right to left; it is a way of saying take this value on the right and assign it to the variable on the left.

Unary operators are another kind of operator that are designed specifically to work with only one operand, performing such operations as incrementing and decrementing the value of an operand by one, negating an expression, or inverting a boolean value.

Operator precedence are rules by which we may determine the order of operations when operators of equal precedence appear within the same expression. The easiest way to remember the order of precedence rule, with respect to the basic arithmetic operators is to think of the mnemonic commonly used in mathematics, that is BODMAS, which provides that operations proceed in the following order:-

  • Brackets
  • Order
  • Division
  • Multiplication
  • Addition
  • Subtraction


However, one should be careful to understand that these operators are in actuality grouped by classes of precedence, where division and multiplication have the same precedence, as do addition and subtraction; making it possible to arrange the operators within these two groups in whatever order one may find convenient to ones purposes. In such circumstances, it is often necessary, as well as within the bounds of convention, to use parenthesis to make the order of evaluation explicit. Java provides many more operators for which the BODMAS rule constitutes an inadequate framework within which they may be properly expressed.

Assignment operator

The assignment operator (=) is a way of copying a value on the right-hand side (rvalue), to a variable on the left-hand side (lvalue). The rvalue can be any constant, variable, or any expression that produces a value, while the lvalue must be a distinctly named variable that possesses the capacity to hold to a value. For instance, we may assign a value, 20 to a variable x in the following way:-

x = 20;

or an expression to a variable:

x = 5 + 2;

However we can not assign a variable to a constant; this is to say a constant cannot be an lvalue, for instance, the following expression would incite a compiler error:-

20 = x; //! Error

When you assign primitives, you actually copy a value from one place to another. This is because primitives hold actual values as opposed to objects that hold references. With the assignment of objects, one must become properly acquainted with the phenomenon known as aliasing.
Aliasing can be observed in the behaviour of object references that are copied and that point to the same object. Consider the following example:-
   class Level  {
      int spaces;
   }

   public class CarPark  {
      public static void main(String[] args)  {
           Level l1 = new Level();
           Level l2 = new Level();
           l1.spaces = 10;
           l2.spaces = 3;

           System.out.println("1: l1.spaces: " + l1.spaces +  ", l2.spaces: " + l2.spaces);
           l2 = l1;
           System.out.println("2: l1.spaces: " + l1.spaces +  ", l2.spaces: " + l2.spaces);
           l2.spaces = 0;
           System.out.println("3: l1.spaces: " + l1.spaces +  ", l2.spaces: " + l2.spaces);
       }
    }
    /* Output
   1: l1.spaces: 10, l2.spaces: 3
   2: l1.spaces: 10, l2.spaces: 10
   3: l1.spaces: 0, l2.spaces: 0
   *//

The CarPark class illustrates the logic of a car park spread over many levels. In this instance, the class creates two Level instances (l1 and l2) in the main() and both objects are assigned different field values that denote the number of empty spaces on each level. We can observe the aliasing phenomenon at the point where l1 is assigned to l2 and how, subsequently assigning a different value to l2 appears to change l1 as well, when you would intuitively expect that both objects were completely independent of one and the other. Not quite so, I'm afraid. This is because at the point where l1 is assigned to l2, both now contain the same reference that point to the same object and changes to the one is bound to affect the other. This is fundamentally how Java works with objects and demonstrates the aliasing effect.

To retain two independent objects in this particular instance, we would make the assignment call to the object member explicit, like this: -

l2.spaces = l1.spaces;

This way we can avoid the idiosyncratic behaviour of the aliasing phenomenon, which we must add, is not limited to field assignments alone, but can also occur when you pass an object into a method, where it can appear to change the value of an object or field that exists completely outside of the method scope.

Arithmetic operators

Basic arithmetic operators include addition (+) also used for concatenating Strings, subtraction (-), multiplication (*), division (/) and the modulus operator (%) which produces the remainder from an integer division.

One may also use shorthand notation to perform what we call compound assignments by combining any one of these operators with the assignment operator at a step; for instance, to combine the additive operator with the assignment operator to add 2 to the variable x, we may write: x += 2.

The following provides an example of the use of arithmetic operators:-

    public class ArithmeticOps {
       public static void main(String[] args) {
           int add = 2 + 4;
           System.out.println( "2 + 4 = " + add);
           int subtract = 10 - 3;
           System.out.println("10 - 3 = " + subtract);
           int divide = 6/2;
           System.out.println("6 / 2 = " + divide);
           int multiply = 5 * 5;
           System.out.println("5 * 5 = " + multiply);
           int modulus = 7 % 2;
           System.out.println("7 % 2 = " + modulus);
        }
     }
    /* Output   
    2 + 4 = 6
    10 - 3 = 7
    6 / 2 = 3
    5 * 5 = 25
    7 % 2 = 1
    *//
 

Unary operators

Unary operators are designed to work on only one operand, and, whereas the unary minus (-) operator negates an expression or inverts the sign on data, the unary plus operator (+) indicates a positive value, even though it is not entirely necessary as unsigned numbers are positive without its use anyway. For example, the unary minus operator may be used to invert the sign on data in the following way:-

int i = -2;

The increment operator (++) increments a value by 1, while the decrement operator (--) provides for the opposite effect i.e. decreases a value by 1. The increment and decrement operators, in addition to modifying a variable, also produce the value of the modified variable as a result.

Here, one must pay heed to the fact that both increment and decrement operators can be applied before (prefix) or after (postfix) the operand. When applied before (i.e., ++2) the operation is performed first and then the value is produced, when applied after (i.e., 2++) the value is first produced before the operation is performed. The same logic applies equally to the decrement operator.
Finally the logical complement operator (!) inverts the value of a boolean or such other expression. You may find this referred to as the logical or conditional NOT (!) operator in certain other text, all of which is of course, not incorrect, however we are happy to classify this as a unary operator first and foremost, by virtue of the fact that it also works with only one operand.
Here is an example that demonstrates the use of unary operators:-

     public class UnaryOps {
         public static void main(String[] args) {
             int i = 2;
             System.out.println("i = " + i);
             System.out.println("i++ = " + i++); //Post-increment
             System.out.println("i = " + i);
             System.out.println("++i = " + ++i); //Pre-increment
             System.out.println("i-- = " + i--); //Post-decrement
             System.out.println("i = " + i);
             System.out.println("--i = " + --i); //Pre-decrement
             System.out.println("i = " + i);
             boolean complement = true;
             System.out.println(complement);
             System.out.println(!complement); // Conditional NOT (!)
        }
     }
    /* Output
    i = 2
    i++ = 2
    i = 3
    ++i = 4
    i-- = 4
    i = 3
    --i = 2
    i = 2
    true
    false
    *//

From the output, one may easily observe how the increment and decrement operation is performed before the value is produced with the prefix form of both operators, while the value is produced before the operation is performed with the postfix form of both operators. The logical complement operator demonstrates how to negate a boolean value from true to false.

Relational operators

Relational operators evaluate the relationship between the values of two operands and produce a boolean result i.e.true or false. They include less than (<), greater than (>), less than or equal to (<=), greater than or equal to (>=), equivalence (==), and nonequivalence (!=). With the exception of the equivalence and nonequivalence operators, all other relational operators work with all primitives except boolean.

The equivalence and nonequivalence operators, in addition to working with all primitives, also work with objects; but here, one must be mindful to note that they actually compare object references and not the actual content of objects themselves. For instance, if you were to try to compare two Double objects:-

  public class CompareDouble {
     public static void main(String[] args) {
        Double d1 = new Double(3.2);
        Double d2 = new Double(3.2);
        System.out.println(d2 != d1);
        System.out.println(d2 == d1);
      }
  }
   /* Output
  true
  false
  *//

The output can be a bit confusing, returning a false result when both objects are tested for equivalence, and a true result when tested for nonequivalence. This is of course not what you might expect given that both objects are the same or to put it another way, both references point to the same object in memory. The reason for what might seem an unusual result is because the == and != operators in fact compare object references as opposed to the actual content of objects themselves.

To make the comparison of the actual content of objects, we may use the special equals() method that exist for all objects like so:-

   public class CompareDouble {
       public static void main(String[] args) {
           Double d1 = new Double(3.2);
           Double d2 = new Double(3.2);
           System.out.println(d2.equals(d1));
       }
   }
   /* Output
   true
   *//

While this may seem a bit more intuitive, it is important to note that when using the equals() method to compare objects of your own classes, it will be necessary to override its default behaviour in your own class. This is because the default behaviour of the equals() method is to compare object references. Here is an example that demonstrates the use of relational operators:-
   public class RelationalOps {
      public static void main(String[] args) {
           int x = 10;
           int y = 4;
           boolean equivalence = x == y;
           System.out.println("x == y is " + equivalence);
           boolean nonEquivalence = x != y;
           System.out.println("x != y is " + nonEquivalence);
           boolean lessThan = x < y;
           System.out.println("x < y is " + lessThan);
           boolean greaterThan = x > y;
           System.out.println("x > y is " + greaterThan);
           boolean lessOrEqualTo = x <= y;
           System.out.println("x <= y is " + lessOrEqualTo );
           boolean greaterOrEqualTo = x >= y;
           System.out.println("x >= y is " + greaterOrEqualTo);
      }
   }
   /* Output
   x == y is false
   x != y is true
   x < y is false
   x > y is true
   x <= y is false
   x >= y is true
   *//

Conditional operators

The Conditional or logical operators AND (&&) and OR (||) also produce a boolean result of true or false based on the logical relationship of its arguments. They are known to exhibit a phenomenon known as short-circuiting, which means that the expression is evaluated only to the extent that the truth or falsehood of the it can be clearly determined. This is to say sometimes only a part of the expression need be evaluated in order to determine it's truth or falsehood. Below provides a demonstration on the use of conditional operators:-

   import java.util.Random;
  
   public class ConditionalOps {
      public static void main(String[] args) {
          Random rand = new Random();
          int x = rand.nextInt(50);
          int y = rand.nextInt(50);
          System.out.println("(x > 5) && (y > 5) is " + ((x > 5) && (y > 5)) );
          System.out.println("(x < 5) || (y < 5) is " +  ((x < 5) || (y < 5)) );
       }
     }
     /* Output
    (x > 5) && (y > 5) is true
    (x < 5) || (y < 5) is false
    *//

The example uses the conditional operators (&& and ||) to determine the truth or falsehood of the logical relationship between the values that result from the evaluation of two expressions using relational operators. A Random object is first used to generate two random numbers under 50 in the main() method, both of which are then evaluated within the bounds of relational (< and >) expressions.
It should be noted that conditional AND and OR operators can be applied to boolean values only, added to that is the fact that the boolean value is converted to the appropriate text form if used where a String might be expected for instance in the print statement.

Ternary if-else operator

The ternary if-else operator should not be confused with the if-else statement which is used to control the flow of execution in Java, and even though it can usually be used in place of the if-else statement, it does not quite improve readability in your code.
The ternary if-else operator works with three operands producing a value depending on the truth or falsehood of a boolean assertion. It's form is as follows:-

boolean-exp ? value1 : value2

The logic can be taken to read evaluate value1 if the condition boolean-exp is true, or evaluate value2 if it is false. Run the following example to see the result:-
     public class TernaryOp {
         static boolean result(int x) {
             return x > 50 ? true : false
         }
         public static void main(String[] args) {
             Random() rand = new Random(46);
             for(int i=0; i<5; i++) {
                int y = rand.nextInt(100);
                System.out.println(y + " is greater than 50 is  " + result(y));
             }
         }
     }
    /* Output
   33 is greater than 50 is false
   67 is greater than 50 is true
   75 is greater than 50 is true
   95 is greater than 50 is true
   22 is greater than 50 is false
  *//

The ternary operator should be used in a modest fashion and only necessary when setting a variable to one of two values; otherwise the standard if-else statement should be the preferred choice as it produces more readable code.

Bitwise operators

The bitwise operators, you will find, are operators for which you will rarely find use, however we include it here to keep the programmer better informed. These operators provide you with a way to manipulate individual bits in an integral primitive data type. Integral data types are data types which store a finite subset of integers. Bitwise operators are said to perform Boolean algebra on the corresponding bits in two arguments.

The bitwise AND operator (&) produces a one in the output bit when both input bits are one, otherwise it produces a zero. If either input bit is one, the bitwise OR (|) operator produces a one in the output bit and produces a zero only when both inputs bits are zero. The bitwise EXCLUSIVE OR, otherwise known as XOR (^), produces a one in the output bit if any one of the input bits is a one but not both. The bitwise NOT (~) operator works like a unary operator and produces the opposite of the input bit i.e. zero if input bit is one or vice versa. Here is an example:-

   import java.util.Random;

   public class BitwiseOps {

       static void printBinaryInt(String s, int i) {
           System.out.println(s + ", int: " + i + ", binary:\n " +
                              Integer.toBinaryString(i));
       }

       public static void main(String[] args) {
           Random rand = new Random();
           int x = rand.nextInt();
           int y = rand.nextInt();
           printBinaryInt("x",  x);
           printBinaryInt("y", y);
           printBinaryInt("x & y", x & y);
           printBinaryInt("x | y", x | y);
           printBinaryInt("x ^ y", x ^ y);
           printBinaryInt("~x", ~x);
        }
     }
     /* Output
     x, int: 42, binary:
     101010
     y, int: 3, binary:
     11
     x & y, int: 2, binary:
     10
     x | y, int: 43, binary:
     101011
     x ^ y, int: 41, binary:
     101001
     ~x, int: -43, binary:
     11111111111111111111111111010101
     ~y, int: -4, binary:
     11111111111111111111111111111100
     *//

The static printBinaryInteger() method displays the resulting integer using the toBinaryString() method of an Integer object, to convert that value to binary. All bitwise operators with the exception of the unary NOT (~) operator can be used with the = sign to combine the operation and assignment.

Shift operators

Shift operators include the left-shift operator (<<) and the signed right-shift operator (>>), which are also used to manipulate bit patterns. The signed right-shift operator shifts the operand to the left, toward the right, by the number of bits specified to the right of the operator; using signed extension, it inserts zeroes at the higher order bits if the value is positive, and inserts ones if the value is negative.
The signed left-shift operator shifts the operand to the left, toward the left, also by the number of bits specified to the right, but inserting zeros at the lower order bits. Java also has the unsigned right shift (>>>), which makes use of a zero extension; inserting zeros into higher order bits regardless of the sign. The following example demonstrates the use of these operators:-

    import java.util.Random;

    public class ShiftOps {
        public static void main(String[] args) {
            Random rand = new Random();
            int x = rand.nextInt(50);
            int y = rand.nextInt(50);
            System.out.println("x is " + Integer.toBinaryString(x));
            System.out.println("y is " + Integer.toBinaryString(y));
            x >>= 2;
            y >>= 2;
            System.out.println("x >>= 2 is " + Integer.toBinaryString(x));
            System.out.println("y >>= 2 is " + Integer.toBinaryString(y));
            x <<= 10;
            y <<= 10;
             System.out.println("x <<= 10 is " + Integer.toBinaryString(x));
            System.out.println("y <<= 10 is " + Integer.toBinaryString(y));
             x >>>= 10;
            y >>>= 10;
            System.out.println("x >>>= 10 is " + Integer.toBinaryString(x));
            System.out.println("y >>>= 10 is " + Integer.toBinaryString(y));
          }
      }
     /* Output
     x is 1101
     y is 101101
     x >>= 2 is 11
     y >>= 2 is 1011
     x <<= 10 is 110000000000
     y <<= 10 is 10110000000000
     x >>>= 10 is 11
     y >>>= 10 is 1011
     *//
 

Casting

The programmer should be mindful of casting operations and their implications in Java, particularly as it concerns primitives. Java allows you to make explicit casts from one primitive type to another where necessary, with the exception of the boolean primitive type which of course, does not do casts.

Casting is a way to convert from one type to another, for instance from a float to a double. With a widening-conversion (i.e. going from a type that holds less information to one that holds much more), you will find this to be a safe operation and as there is no risk of losing information, Java is able to perform these sorts of casts automatically; however with a narrowing-conversion, the compiler will always force you to use an explicit cast. To perform a cast, you simply put the desired type between parenthesis and to the left of any value you wish to cast. Check out the following example:-

   public class Cast {
       public static void main(String[] args) {
            double d = 5.2;
           int i = (int)d;// narrowing conversion cast required
           long l = i;// widening conversion automatic cast
           byte b = (byte)l;// narrowing conversion cast required
           System.out.println(d);
           System.out.println(i);
           System.out.println(l);
           System.out.println(b);
       }
    }
    /* Output
    5.2
    5
    5
    5
    *// 

It is also good practice to be aware that when performing mathematical operations in Java, the larger or largest data type determines the size of the result of that expression; for instance if you were to multiply an int by say a double primitive data type, the result will almost certainly be double, likewise if you were to perform an operation with of any of the smaller data types i.e. byte, char or short with an int, the resulting value will be of type int. An explicit cast will be necessary to assign any resulting value to the smaller type.

Summary

The reader should, by this time, be well acquainted with how to use basic arithmetic operators in Java, the use of the assignment operator in particular, and the peculiar nature of the phenomenon that is aliasing in the assignment of objects, also the use of unary operators that provide signage to data, as well as those that negate an expression or boolean value, and both forms of the increment and decrement operators. We have also covered relational operators, including equivalence and nonequivalence operators and how these operators are known to compare references as distinct from the content of objects themselves.
Further, we have discussed conditional or logical operators and how they exhibit short-circuiting behaviour, the ternary if-else operator as distinct from the if-else statement, bitwise operators and of course shift operators. Finally we discussed casting and how they affect primitive data types in Java.

contact me @ kaseosime@btinternet.com