Chapter FOUR
Working with Data


Exam Objectives

Use primitives and wrapper classes. Evaluate arithmetic and boolean expressions, using the Math API and by applying precedence rules, type conversions, and casting.
Manipulate text, including text blocks, using String and StringBuilder classes.

Chapter Content


Understanding Data Types

There are two main data types, primitive and reference:

┌───────────────────────────────────────────────────┐
│                  Java Data Types                  │
│                         │                         │
│          ┌──────────────┴──────────────┐          │
│          │                             │          │
│    Primitive Types               Reference Types  │
│          │                             │          │
│  ┌───────┴───────┐             ┌───────┴───────┐  │
│  │ byte          │             │ Classes       │  │
│  │ short         │             │ Interfaces    │  │
│  │ int           │             │ Arrays        │  │
│  │ long          │             │ Enums         │  │
│  │ float         │             └───────────────┘  │
│  │ double        │                                │
│  │ boolean       │                                │
│  │ char          │                                │
│  └───────────────┘                                │
└───────────────────────────────────────────────────┘

Let’s review each one in more detail.

Primitive Types

Java is a statically typed language, which means that all variables must first be declared before they can be used. A variable’s type determines the values it may contain and what operations can be performed on it.

In Java, primitive types are the most basic data types available. They are not objects and do not belong to any class. Instead, they are defined by the language itself. Primitive types are used to store simple values like integers, floating point numbers, booleans, and characters.

Primitive types are stored directly in memory and are accessed by their values. This is in contrast to reference types (objects), which are accessed by their reference. Because of this direct storage, primitives are faster and require less memory than objects.

There are eight primitive data types in Java:

Type Size (bits) Minimum Value Maximum Value Default
byte 8 -128 127 0
short 16 -32,768 32,767 0
int 32 -2,147,483,648 2,147,483,647 0
long 64 -9,223,372,036,854,775,808 9,223,372,036,854,775,807 0L
float 32 1.4E-45 3.4028235E38 0.0f
double 64 4.9E-324 1.7976931348623157E308 0.0d
boolean 1 n/a n/a false
char 16 ‘\u0000’ (0) ‘\uffff’ (65,535) ‘\u0000’

Let’s break this down.

The integer types (byte, short, int, long) are for whole number values. They differ by the range of values they can hold. A byte is 8 bits and can hold values from -128 to 127. A short is 16 bits and can hold values from -32,768 to 32,767. An int is 32 bits and can hold values from -2,147,483,648 to 2,147,483,647. And a long is 64 bits, holding values from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807.

You might wonder, aren’t all numbers in Java treated the same way? Why do we even need different types like int, long, etc.? The reason is efficiency and memory usage. If you know your values will always be within a certain range, you can use a smaller type to save memory. For example, an int takes up half the memory of a long. In large-scale applications with lots of data, this can make a big difference.

The floating point types (float and double) are for numbers with decimal points. A float is 32 bits and a double is 64 bits. This gives double much more precision than float. Many developers opt to use double for all decimal values to avoid precision issues, but there are scenarios where float can be used to conserve memory if high precision is not needed.

The boolean type has only two possible values: true and false. It’s used for conditional logic.

The char type is for single characters. It uses 16 bits because it uses Unicode encoding, allowing it to represent a wide variety of characters from different languages.

An important thing to remember is that the default values only apply to fields. Local variables, on the other hand, must be explicitly initialized before use; otherwise, your code won’t compile.

You can assign values to variables using literals. In addition to standard decimal notation, Java allows you to assign integer literals using hexadecimal (prefix 0x or 0X), octal (prefix 0) and binary (prefix 0b or 0B) notation.

Here’s an example:

// Decimal notation
int decimalNum = 42;

// Hexadecimal notation
int hexNum = 0x2A; // Equivalent to decimal 42
        
// Octal notation
int octalNum = 052; // Equivalent to decimal 42
        
// Binary notation
int binaryNum = 0b101010; // Equivalent to decimal 42

When assigning literals to variables, it’s important to note that the literal’s type must match the variable’s type. If they don’t match, you may need to use a suffix to specify the literal’s type.

For integer literals:

For floating point literals:

Here are some examples:

long longNum = 1000L; // Suffix L is required
float floatNum = 3.14f; // Suffix f is required
double doubleNum1 = 3.14; // Suffix d is optional
double doubleNum2 = 3.14d; // Suffix d is optional 

If you don’t use the correct suffix, you might encounter a compilation error. For example:

byte longNum = 1000; // Compilation error: integer literal is too large
float floatNum = 3.14; // Compilation error: incompatible types

In these cases, Java assumes the literal is of type int or double respectively, which can’t be directly assigned to a byte or float variable without an explicit cast.

You can also use underscores in numeric literals to improve readability, such as 1_000_000. Here are some examples illustrating the use of underscores in numeric literals:

// Valid use of underscores
int million = 1_000_000;
long creditCardNumber = 1234_5678_9012_3456L;
float pi = 3.14_15F;
double avogadro = 6.022_140_857e23;

However, there are some restrictions. You cannot place an underscore:

Here are some additional examples of invalid underscore placements:

// Invalid use of underscores
int x1 = _1000; // Compilation error: illegal underscore
int x2 = 1000_; // Compilation error: illegal underscore
float y1 = 3_.14F; // Compilation error: illegal underscore
float y2 = 3._14F; // Compilation error: illegal underscore

float y3 = 3.14__F; // Compilation error: consecutive underscores
long z1 = 1000_L; // Compilation error: underscore before L suffix

int x3 = 0_x42; // Compilation error: underscore in position where digits are expected
int x4 = 0b_101010; // Compilation error: underscore in position where digits are expected

These rules are in place to prevent ambiguity and to ensure that the use of underscores does not conflict with other parts of the language syntax.

Reference Types

In the previous section, we explored the concept of primitive types in Java. However, as we know, Java is an object-oriented language, and nearly everything is treated as an object. While primitives provide the basic building blocks, reference types allow us to work with objects and take full advantage of Java’s object-oriented features.

So, what exactly are reference types? Unlike primitives, which hold their values directly, reference types store the memory address where the actual object resides. In other words, a reference variable refers to the location of the object rather than containing the object itself.

String myString = "Hello"; // Reference type

In this example, myString is a reference variable of type String. It doesn’t hold the actual string value but rather a reference to the memory location where the "Hello" object is stored.

This contrasts with how primitive types work:

int myNumber = 42; // Primitive type 

Here, myNumber directly holds the integer value 42 rather than referring to an object.

But what if we want to treat primitives as objects? This is where wrapper classes come into play. Java provides a set of wrapper classes that correspond to each primitive type, allowing them to be used in scenarios that require objects:

Primitive Type Wrapper Class Inherits from Number
boolean Boolean No
byte Byte Yes
short Short Yes
int Integer Yes
long Long Yes
float Float Yes
double Double Yes
char Character No

Each primitive type has a corresponding wrapper class, most of which inherit from the Number class. The Boolean and Character classes are exceptions, as they don’t represent numeric values.

Wrapper classes provide methods for creating instances from many representations and for converting between different data types. Here are some examples:

  1. Parsing methods:
    • Integer.parseInt(String s): Parses a string argument as a signed decimal integer.
    • Double.parseDouble(String s): Parses a string argument as a double-precision floating-point number.
    • Boolean.parseBoolean(String s): Parses a string argument as a boolean value.

    For example:

    int num = Integer.parseInt("42");
    double value = Double.parseDouble("3.14");
    boolean flag = Boolean.parseBoolean("true");
    
  2. Conversion methods:
    • Integer.valueOf(String s): Returns an Integer object holding the value of the specified string representation.
    • Long.valueOf(long l): Returns a Long object holding the specified primitive long value.
    • Double.valueOf(double d): Returns a Double object holding the specified primitive double value.

    For example:

    Integer myInt = Integer.valueOf("100");
    Long myLong = Long.valueOf(1234567890L);
    Double myDouble = Double.valueOf(2.71828);
    
  3. Conversion between numeric types:
    • Integer.byteValue(): Returns the value of an Integer as a byte.
    • Long.intValue(): Returns the value of a Long as an int.
    • Float.doubleValue(): Returns the value of a Float as a double.

    For example:

    byte myByte = myInt.byteValue();
    long myLong = myInt.longValue();
    
  4. Character methods:
    • Character.isDigit(char ch): Determines if the specified character is a digit.
    • Character.isLetter(char ch): Determines if the specified character is a letter.
    • Character.toUpperCase(char ch): Converts the character argument to uppercase.

    For example:

    char myChar = '7';
    boolean isDigit = Character.isDigit(myChar);
    boolean isLetter = Character.isLetter(myChar);
    char upperCase = Character.toUpperCase(myChar);
    

Notice that these methods are static, allowing you to use them without creating an instance of the wrapper class.

These are just a few examples of the methods provided by wrapper classes for creating instances and converting between different representations. Each wrapper class offers a range of methods specific to its corresponding primitive type, providing flexibility and convenience when working with different data formats and conversions.

So, do wrapper classes make primitives objects? Not quite. Wrapper classes are separate from primitives but provide a way to wrap primitives in an object form. This allows primitives to be used in contexts that expect objects, such as collections or when using generics.

To bridge the gap between primitives and their wrapper classes, Java introduced autoboxing and unboxing. As mentioned before, autoboxing is the automatic conversion of a primitive type to its corresponding wrapper class, while unboxing is the reverse process:

int num = 42;
Integer objNum = num; // Autoboxing
int num2 = objNum;    // Unboxing

In this example, num is automatically boxed into an Integer object when assigned to objNum. Similarly, objNum is unboxed back to an int when assigned to num2. This happens implicitly, making it convenient to switch between primitives and their wrapper classes.

Autoboxing and unboxing work with all primitive types and their corresponding wrapper classes, not just with int and Integer. Java handles these conversions automatically based on the context in which they are used.

It’s important to note that while autoboxing and unboxing simplify code readability, they could have some performance implications. Each conversion between a primitive and its wrapper class involves creating or discarding an object, which adds a small overhead. In most cases, this overhead is negligible, but it can add up in performance-sensitive scenarios with frequent autoboxing/unboxing operations.

One advantage of wrapper classes is their ability to represent the absence of a value using null. While primitives cannot be null, wrapper objects can.

Integer num = null;
int value = num; // NullPointerException

Here, assigning null to num is valid because it’s an Integer object. However, attempting to unbox num to an int throws a NullPointerException. This behavior allows for more explicit handling of null values and can be useful in scenarios where a variable might not have a value assigned.

Also, it’s worth noting that wrapper classes are immutable, meaning their values cannot be changed once assigned. When you perform operations on a wrapper object, a new object is created with the updated value rather than modifying the existing object.

Integer num = 42;
num++;

In this example, the ++ operation on num creates a new Integer object with the value 43 rather than modifying the original object. This behavior ensures thread safety and avoids unexpected side effects when sharing wrapper objects across multiple parts of the program.

Operators

Introducing Operators

An operator is a symbol that tells the compiler to perform specific mathematical or logical operations.

Java provides a rich set of operators to perform operations on variables and values:

Many developers new to Java mistakenly believe operators are just for math operations. But operators also play an important role in controlling program flow, performing logical operations, manipulating bits, and more. For example:

int a = 10;
int b = 5;
        
// Arithmetic operator
System.out.println(a + b);  // 15 

// Comparison operator  
System.out.println(a > b);  // true

// Logical operator
System.out.println((a > b) && (a != b)); // true

As you can see, operators in Java go well beyond basic arithmetic.

Operator Precedence

An important concept to grasp with operators is precedence. Just like in mathematics, some operators in Java have higher precedence than others, meaning they get evaluated first in an expression.

For instance, consider this code:

int result = 10 + 5 * 2;
System.out.println(result);

You might expect result to be 30 (10 + 5 is 15, then 15 * 2). But actually, it prints out 20. That’s because the * operator has higher precedence than +. So 5 * 2 is evaluated first, giving 10, and then 10 is added to the original 10.

Here’s a table showing operator precedence in Java, from highest to lowest:

Category Operator Associativity
Postfix [] [] . (args) ++ -- Left to right
Unary ++ -- + - ~ ! Right to left
Multiplicative * / % Left to right
Additive + - Left to right
Shift << >> >>> Left to right
Relational < > <= >= instanceof Left to right
Equality == != Left to right
Bitwise AND & Left to right
Bitwise XOR ^ Left to right
Bitwise OR | Left to right
Logical AND && Left to right
Logical OR || Left to right
Ternary ? : Right to left
Assignment = += -= *= /= %= &= ^= |= <<= >>= >>>= Right to left

As the table shows, most operators evaluate left-to-right. So in an expression like a + b - c, a + b happens first, then - c.

But the assignment operators and the unary operators actually evaluate right-to-left. Consider this code:

int a = 10;
int b = 20;
int c = (a = 3) + (b = 5);
System.out.println(a + ", " + b + ", " + c); // 3, 5, 8

Here, a is assigned 3, b is assigned 5, and because assignment evaluates right-to-left, the assignments happen before the addition. So c ends up as 8 (3 + 5), while a is 3 and b is 5.

This right-to-left evaluation allows chained assignments, like a = b = c = 5. The 5 is assigned to c, then that result is assigned to b, and finally to a, right-to-left.

It’s a lot to remember, and memorizing the entire precedence table isn’t necessary. The key points are:

  1. Postfix operations like x++ happen before prefix ones like ++x.
  2. Multiplicative operations (*, /, %) happen before additive ones (+, -).
  3. Bitwise operations (&, |, ^) happen after comparisons (>, ==, etc.) but before logical ones (&&, ||).
  4. Assignments evaluate last, and right-to-left.

When in doubt, parentheses can always be used to make the order explicit. The expressions (a + b) * c and a + (b * c) are unambiguously different.

In general, parentheses should be used whenever the precedence is unclear or to improve readability. But they shouldn’t be overused to the point of clutter. With a solid grasp of operator precedence, many parentheses become unnecessary, leading to cleaner, more readable code.

Let’s look at a few more examples so you can better understand operators and precedence in Java:

int x = 10;
int y = 20;
int z = 30;

System.out.println(x + y - z);  // 10 + 20 - 30 = 0
System.out.println(x - y + z);  // 10 - 20 + 30 = 20
System.out.println(x * y / z);  // 10 * 20 / 30 = 6
System.out.println(x / y * z);  // 10 / 20 * 30 = 0

In the first two statements, the operators have the same precedence (+ and -), so they’re evaluated left-to-right. In the third and fourth statements, * and / have higher precedence and are evaluated first, left-to-right, before the results are added. In the fourth example, 10 / 20 is zero (because we are using integers).

Now let’s mix in some assignments and unary operators:

int a = 5;
int b = 10;
int c = ++a * b--;
System.out.println(a + ", " + b + ", " + c);  // 6, 9, 60

Here, ++a increments a to 6 before the multiplication. Then 6 * 10 gives 60, which is assigned to c. Finally, b-- decrements b to 9, but after the multiplication. So we end up with a as 6, b as 9, and c as 60.

The right-to-left nature of assignments is critical to understand:

int x = 2;
int y = 3;
int z = 1;
x += y -= z;
System.out.println(x + ", " + y + ", " + z);  // 4, 2, 1

First, z (1) is subtracted from y (3), giving 2, which is then assigned back to y. Then this value (2) is added to x (2), giving 4, which is assigned back to x. So x ends up as 4, y as 2, and z remains 1.

Logical and bitwise operators can add further complexity:

int a = 10;  // 1010 in binary
int b = 6;   // 0110 in binary

System.out.println(a & b);  // 1010 & 0110 = 0010 (2 in decimal)
System.out.println(a | b);  // 1010 | 0110 = 1110 (14 in decimal)
System.out.println(a ^ b);  // 1010 ^ 0110 = 1100 (12 in decimal)

System.out.println(a > 5 && b < 10); // true && true = true
System.out.println(a > 5 || b < 5);  // true || false = true

The bitwise operators &, |, and ^ perform AND, OR, and XOR operations on each bit of the numbers. The logical operators && and || perform AND and OR on boolean conditions, with && having higher precedence.

Lastly, let’s not forget the ternary operator, which is like a compact if-else statement:

int x = 10;
int y = 20;
int max = (x > y) ? x : y;
System.out.println(max);  // 20

Here, (x > y) is false, so the value after the colon (y, which is 20) is assigned to max.

In the next sections, we’ll review each type of operators in more detail.

Unary Operators

Unary operators are operators that work on only one operand. You have already seen some unary operators in action, such as the logical complement operator (!) used with boolean values. However, in this section, we’ll cover the unary operators that are primarily used with numeric types.

Complement and Negation Operators

The unary complement operator (~), also known as the bitwise complement operator, inverts all the bits in a number, effectively changing each 0 to 1 and each 1 to 0. In Java, integers are represented using 32 bits in two’s complement format.

For positive numbers, the bitwise complement will flip all the bits, and the resulting number is the negation of the original number minus one. This is because inverting all bits and then interpreting the result in two’s complement yields -(n + 1).

Here’s how it works:

For a positive number like 5, the binary representation is:

0000 0000 0000 0000 0000 0000 0000 0101

When you apply the bitwise complement operator, it flips all the bits:

1111 1111 1111 1111 1111 1111 1111 1010

In two’s complement, this is the representation of -6. Therefore, ~5 in Java equals -6.

For negative numbers, the bitwise complement operator also flips all the bits. The result is the positive version of the original number minus one, because flipping all bits of a negative number and interpreting it in two’s complement yields the positive counterpart decreased by 1.

For example, let’s take -5. In two’s complement, it’s represented as:

1111 1111 1111 1111 1111 1111 1111 1011

Applying the bitwise complement operator:

0000 0000 0000 0000 0000 0000 0000 0100

This binary number represents 4 in decimal. So, ~(-5) equals 4.

In summary:

On the hand, the unary negation operator (-) is simpler to understand. This operator is used to negate a numeric value, effectively changing its sign. For example, if x is 5, then -x would be -5.

A common misconception is that the negation operator is the same as subtracting the number from zero. While the end result may be the same, the negation operator works differently under the hood. It directly changes the sign bit of the number, rather than performing a subtraction operation.

Increment and Decrement Operators

Java also provides increment (++) and decrement (--) operators, which are used to increment or decrement a variable’s value by 1. These operators can be used in either prefix or postfix form.

Here’s a table summarizing the different increment and decrement operators:

Operator Name Description Example
++x Prefix increment operator Increments x by 1, then returns the new value of x ++x
x++ Postfix increment operator Returns the current value of x, then increments x by 1 x++
--x Prefix decrement operator Decrements x by 1, then returns the new value of x --x
x-- Postfix decrement operator Returns the current value of x, then decrements x by 1 x--

One question is whether you can use increment and decrement operators with boolean values. The answer is no. These operators are only applicable to numeric types like int, long, float, double, etc.

Another point of confusion is whether x++ increments x before or after the expression it’s used in. The postfix increment operator (x++) returns the original value of x, and then increments x after that value is returned. So if you have an expression like y = x++;, y will be assigned the original value of x, and then x will be incremented.

On the flip side, if you use --x, it decreases the value of x before the expression is evaluated. So y = --x; would first decrement x, and then assign the new value of x to y.

Also, you might wonder if there’s a difference between ++x and x++ if they are the only operations in a statement. In this case, there is no difference. Both will increment x by 1. The difference only comes into play when the increment operation is part of a larger expression.

Summary of Unary Operators

Here’s a comprehensive table of the unary operators in Java:

Operator Name Description Example
+ Unary plus Indicates a positive value (rarely used) +x
- Unary minus Negates a value -x
++ Increment Increments a value by 1 ++x (prefix) x++ (postfix)
-- Decrement Decrements a value by 1 --x (prefix) x-- (postfix)
~ Bitwise complement Inverts all bits ~x

Note that the complement operator (~) only works on integer types, not on float or double.

A few more nuanced points to consider:

Binary Operators

Binary operators are operators that work on two operands. Java provides a set of binary arithmetic operators for performing basic mathematical operations on numeric operands. These operators include addition (+), subtraction (-), multiplication (*), division (/), and modulus (%).

Arithmetic Operators

The addition (+), subtraction (-), and multiplication (*) operators work as you’d expect:

int a = 10;
int b = 20;
int sum = a + b; // 30
int difference = b - a; // 10
int product = a * b; // 200

The division operator (/) performs division between two numeric operands. It’s important to note that when used with integer operands, the division operator performs integer division, which means it returns the quotient and discards any remainder.

int a = 10;
int b = 3;
int quotient = a / b; // 3

If you want to perform floating-point division and get a fractional result, at least one of the operands needs to be a floating-point type (float or double).

int a = 10;
double b = 3.0;
double quotient = a / b; // 3.3333333333333335

The modulus operator (%) returns the remainder after performing integer division.

int a = 10;
int b = 3;
int remainder = a % b; // 1

Numeric Promotion

When performing arithmetic operations on operands of different types, Java automatically promotes the operands according to a set of rules known as numeric promotion.

Numeric promotion is the automatic conversion of a smaller numeric type to a larger numeric type to prevent loss of precision during arithmetic operations. This allows you to perform arithmetic operations on mixed types without having to explicitly cast the operands.

Java follows these rules for numeric promotion:

  1. If either operand is of type double, the other is promoted to double.
  2. Otherwise, if either operand is of type float, the other is promoted to float.
  3. Otherwise, if either operand is of type long, the other is promoted to long.
  4. Otherwise, both operands are promoted to int.

Here are some examples:

int a = 10;
double b = 20.0;
double result1 = a + b; // a is promoted to double

float c = 10.0f;
long d = 20L;
float result2 = c + d; // d is promoted to float

short e = 10;
short f = 20;
int result3 = e + f; // e and f are promoted to int

Adding Parentheses to Change the Order of Operation

You can use parentheses to change the default order of operations in an arithmetic expression. Expressions inside parentheses are evaluated first.

int a = 10;
int b = 20;
int c = 30;
int result = a + b * c; // 610 (multiplication happens first)
int result2 = (a + b) * c; // 900 (addition happens first)

However, when using parentheses, it’s important to ensure that they are properly balanced. Each opening parenthesis must have a corresponding closing parenthesis. Mismatched parentheses will result in a compilation error.

int result = (a + b) * c; // correct
int result2 = (a + b * c; // compilation error (mismatched parentheses)

Summary of Binary Operators

Here’s a table summarizing the binary operators in Java:

Operator Name Description Example
+ Addition Adds two operands a + b
- Subtraction Subtracts the second operand from the first a - b
* Multiplication Multiplies two operands a * b
/ Division Divides the first operand by the second a / b
% Modulus Returns the remainder of division a % b

Some nuanced points to consider are:

Bitwise and Shift Operators

In addition to the arithmetic operators, Java provides a set of bitwise and shift operators that allow you to manipulate the individual bits of integer values. These operators are particularly useful when working with flags, masks, and low-level system operations.

Bitwise Operators

Java has four bitwise operators: AND (&), OR (|), XOR (^), and complement (~).

The bitwise AND operator (&) returns a 1 in each bit position for which the corresponding bits of both operands are 1s.

int a = 0b1010; // 10
int b = 0b1100; // 12
int result = a & b; // 0b1000 = 8

The bitwise OR operator (|) returns a 1 in each bit position for which the corresponding bits of either or both operands are 1s.

int a = 0b1010; // 10
int b = 0b1100; // 12
int result = a | b; // 0b1110 = 14

The bitwise XOR (exclusive OR) operator (^) returns a 1 in each bit position for which the corresponding bits of either but not both operands are 1s.

int a = 0b1010; // 10
int b = 0b1100; // 12
int result = a ^ b; // 0b0110 = 6

The bitwise complement operator (~) is a unary operator that inverts all the bits of its operand.

int a = 0b1010; // 10
int result = ~a; // 0b11111111111111111111111111110101 = -11

Shift Operators

Java provides three shift operators: left shift (<<), signed right shift (>>), and unsigned right shift (>>>).

The left shift operator (<<) shifts the bits of the first operand left by the number of positions specified by the second operand. The new rightmost bits are filled with 0s.

int a = 0b1010; // 10
int result = a << 1; // 0b10100 = 20

Each left shift effectively doubles the number.

The signed right shift operator (>>) shifts the bits of the first operand right by the number of positions specified by the second operand. The new leftmost bits are filled with the sign bit (0 for positive numbers, 1 for negative numbers), preserving the sign of the number.

int a = 0b1010; // 10
int result = a >> 1; // 0b0101 = 5

Each signed right shift effectively halves the number, rounding down.

The unsigned right shift operator (>>>) is similar to the signed right shift, but the new leftmost bits are always filled with 0s, regardless of the sign.

int a = 0b11111111111111111111111111110110; // -10
int result = a >>> 1; // 0b01111111111111111111111111111011 = 2147483643

Summary of Bitwise and Shift Operators

Here’s a summary of the bitwise and shift operators in Java:

Operator Name Description Example
& Bitwise AND Returns 1 if both bits are 1 a & b
| Bitwise OR Returns 1 if at least one bit is 1 a | b
^ Bitwise XOR Returns 1 if exactly one bit is 1 a ^ b
~ Bitwise Complement Inverts all bits ~a
<< Left Shift Shifts bits left, filling with 0s a << b
>> Signed Right Shift Shifts bits right, filling with sign bit a >> b
>>> Unsigned Right Shift Shifts bits right, filling with 0s a >>> b

A few more nuanced points to consider:

Assignment Operators

Assignment operators are used to assign values to variables. In addition to the simple assignment operator (=), Java provides compound assignment operators that combine an arithmetic or bitwise operation with assignment.

Compound assignment operators combine an arithmetic or bitwise operation with the assignment operation. They provide a concise way to modify the value of a variable based on its current value.

The general syntax for compound assignment operators is:

variable op= expression;

Where op is one of the arithmetic or bitwise operators (+, -, *, /, %, &, |, ^, <<, >>, >>>).

This is equivalent to:

variable = variable op expression;

The compound assignment operators are:

Here are examples of each compound assignment operator:

int a = 10;

a += 5;  // equivalent to a = a + 5; a is now 15
a -= 3;  // equivalent to a = a - 3; a is now 12
a *= 2;  // equivalent to a = a * 2; a is now 24
a /= 4;  // equivalent to a = a / 4; a is now 6
a %= 5;  // equivalent to a = a % 5; a is now 1

int b = 0b1010; // binary representation of 10

b &= 0b1100;  // equivalent to b = b & 0b1100; b is now 0b1000 (8 in decimal)
b |= 0b0101;  // equivalent to b = b | 0b0101; b is now 0b1101 (13 in decimal)
b ^= 0b1001;  // equivalent to b = b ^ 0b1001; b is now 0b0100 (4 in decimal)
b <<= 2;      // equivalent to b = b << 2; b is now 0b10000 (16 in decimal)
b >>= 1;      // equivalent to b = b >> 1; b is now 0b01000 (8 in decimal)
b >>>= 2;     // equivalent to b = b >>> 2; b is now 0b00010 (2 in decimal)

Compound assignment operators are not only more concise but can also be more efficient than their expanded equivalents. This is because the variable is only evaluated once in the compound form, while it’s evaluated twice in the expanded form.

For example, consider the following code:

int[] array = {1, 2, 3, 4, 5};
int index = 2;

array[index++] += 10; // More efficient
array[index++] = array[index++] + 10; // Less efficient

In the first line, index is only incremented once after its value has been used to access the array element. In the second line, index is incremented twice, leading to unexpected behavior and less efficient code.

Changing the Primitive Type with Suffixes

When assigning a value to a variable of a different primitive type, you can use the suffixes f, l, and d to specify the type of the literal value.

float a = 3.14f;
long b = 100L;
double c = 3.14d; // d is optional here, as double is the default for decimal literals

Casting Values

When assigning a value of one type to a variable of another type, you may need to use casting to explicitly convert the value to the target type.

int a = 10;
byte b = (byte) a;

In this example, the int value is cast to a byte before assignment.

When assigning a value that is too large or too small for the target type, overflow or underflow can occur.

byte a = 127;
a++; // a is now -128 (underflow)

byte b = -128;
b--; // b is now 127 (overflow)

In these examples, incrementing the maximum value of a byte causes underflow, and decrementing the minimum value causes overflow.

To avoid unexpected behavior due to overflow or underflow, it’s important to review your assignments and ensure that the values are appropriate for the target types.

int a = 1000;
byte b = (byte) a; // b is now -24 (overflow)

Here, casting the int value 1000 to a byte results in overflow, as 1000 is outside the range of a byte (-128 to 127).

Summary of Assignment Operators

Here’s a summary table of the assignment operators in Java:

Operator Name Example
= Simple assignment a = 10
+= Addition assignment a += 5
-= Subtraction assignment a -= 5
*= Multiplication assignment a *= 5
/= Division assignment a /= 5
%= Modulus assignment a %= 5
&= Bitwise AND assignment a &= 5
|= Bitwise OR assignment a |= 5
^= Bitwise XOR assignment a ^= 5
<<= Left shift assignment a <<= 2
>>= Signed right shift assignment a >>= 2
>>>= Unsigned right shift assignment a >>>= 2

There are a few nuanced points to consider:

Equality Operators

Equality operators in Java are used to compare two values for equality or inequality. They return a boolean result (true or false) based on the comparison.

Java provides two equality operators:

Here are examples of using these operators:

int a = 10;
int b = 20;

boolean result1 = (a == b); // false
boolean result2 = (a != b); // true

Understanding Equality

When using equality operators, it’s important to understand how Java compares values for equality.

For primitive types, the == operator compares the actual values:

int a = 10;
int b = 10;
boolean result = (a == b); // true

However, for objects, the == operator compares the object references, not the contents of the objects:

String s1 = new String("Hello");
String s2 = new String("Hello");
boolean result = (s1 == s2); // false

In this case, s1 and s2 are two different objects in memory, even though they contain the same string value.

To compare the contents of objects, you should use the equals() method:

String s1 = new String("Hello");
String s2 = new String("Hello");
boolean result = s1.equals(s2); // true

However, when comparing objects using the equals() method, it’s important to ensure that the class of the objects has overridden the equals(Object) method to provide a meaningful comparison.

The default implementation of equals(Object) in the Object class simply compares object references, just as the == operator does. To compare the contents of objects, you need to override equals(Object) in your class:

class Person {
    private String name;
    private int age;

    // Constructor, getters, setters...

    @Override
    public boolean equals(Object obj) {
        // Implementation...
    }
}

To override the equals method in Java, you need to follow certain rules to ensure the method works correctly and adheres to the contract defined by the Object class:

  1. Symmetry: If a.equals(b) is true, then b.equals(a) must also be true.

  2. Reflexivity: An object must be equal to itself; that is, a.equals(a) must be true.

  3. Transitivity: If a.equals(b) is true and b.equals(c) is true, then a.equals(c) must be true.

  4. Consistency: If a.equals(b) returns true once, it must continue to return true as long as neither object is modified. Similarly, if it returns false, it must consistently return false.

  5. Non-nullity: a.equals(null) must always return false.

Here is an example of overriding the equals method in the Person class:

public class Person {
    private String name;
    private int age;

    // Constructor, getters, and setters

    @Override // 1.
    public boolean equals(Object obj) {
        // 2. Check if obj is the same as this object
        if (this == obj) {
            return true;
        }
        // 3. Check if obj is null or not an instance of Person
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        // 4. Cast obj to Person and compare significant fields
        Person person = (Person) obj;
                                       
        // 5. Compare significant fields
        return age == person.age && (name != null ? name.equals(person.name) : person.name == null);
    }

    @Override
    public int hashCode() {
        // Ensure consistency with the equals method
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}

The above example shows how to correctly override the equals method:

  1. Use the @Override annotation: This ensures you are correctly overriding the method and helps with readability.

  2. Check for null: The first check should be to see whether the object being compared is null.

  3. Check for type: Use the instanceof operator to ensure the objects being compared are of the same type.

  4. Cast the object: Cast the object to the correct type after checking.

  5. Compare significant fields: Compare the fields that determine equality using the == operator for primitive fields and the equals method for object fields.

Also, always override hashCode when overriding equals to maintain the general contract that equal objects must have equal hash codes.

Summary of Equality and Inequality Operators

Here’s a summary table of the equality and inequality operators in Java:

Operator Name Example
== Equal to a == b
!= Not equal to a != b

There are some nuanced points to consider:

Relational Operators

Relational operators in Java are used to compare two values and determine their relationship. They return a boolean result (true or false) based on the comparison.

Java provides four relational operators:

These operators can be used with numeric primitive types and char.

Here are examples of using relational operators with integer values:

int a = 10;
int b = 20;

boolean result1 = (a < b);  // true
boolean result2 = (a > b);  // false
boolean result3 = (a <= b); // true
boolean result4 = (a >= b); // false

And here’s another with char values, which compares the Unicode values of the characters:

char c1 = 'a';
char c2 = 'b';
boolean result = (c1 < c2); // true

Summary of Relational Operators

Here’s a summary table of the relational operators in Java:

Operator Name Example
< Less than a < b
> Greater than a > b
<= Less than or equal to a <= b
>= Greater than or equal to a >= b

Also, there are two nuanced points to consider:

Logical Operators

Logical operators in Java are used to perform logical operations on boolean expressions. They return a boolean result (true or false) based on the operands and the specific operator used.

Java provides six logical operators:

These operators can be used with boolean values or expressions that evaluate to boolean values.

Here are examples of using logical operators:

boolean a = true;
boolean b = false;

boolean result1 = a & b;  // false
boolean result2 = a | b;  // true
boolean result3 = a ^ b;  // true
boolean result4 = a && b; // false
boolean result5 = a || b; // true
boolean result6 = !a;     // false

And these are the truth tables for the logical AND (&), logical OR (|), and logical XOR (^) operators:

Logical AND (&):

a b a & b
false false false
false true false
true false false
true true true

Logical OR (|):

a b a | b
false false false
false true true
true false true
true true true

Logical XOR (^):

a b a ^ b
false false false
false true true
true false true
true true false

In summary:

Short-circuit

The && and || operators are short-circuit operators. They evaluate the second operand only if it is necessary, based on the result of the first operand.

For &&, if the first operand is false, the entire expression will be false, regardless of the second operand. Therefore, the second operand is not evaluated.

For ||, if the first operand is true, the entire expression will be true, regardless of the second operand. Therefore, the second operand is not evaluated.

Short-circuiting can be useful for avoiding a NullPointerException when checking for null before accessing an object’s methods or fields:

String str = null;
if (str != null && str.length() > 0) {
    // This code will not throw a NullPointerException
}

However, be careful when using short-circuit operators with expressions that have side effects (for example, method calls that modify data or have other consequences):

int a = 10;
if (a > 5 || ++a > 10) {
    // a will be 11 if a > 5, but will remain 10 if a <= 5
}

Summary of Logical Operators

Here’s a summary table of the logical operators in Java:

Operator Name Example
& Logical AND a & b
| Logical OR a \| b
^ Logical XOR (exclusive OR) a ^ b
&& Short-circuit logical AND a && b
|| Short-circuit logical OR a || b
! Logical NOT !a

Here are some nuanced points to consider:

String and StringBuilder

A string is simply a sequence of characters. However, under the hood, strings have some unique properties and optimizations that are important to understand.

String greeting = "Hello World!";

The String class in Java is immutable, meaning once a string object is created, its value cannot be changed. This may seem counterintuitive at first, after all, we often modify strings in our programs. But what’s really happening is that a new string object is being created each time, while the original remains unchanged.

This immutability brings some advantages. Strings can be shared safely between multiple parts of a program without worrying about one part accidentally modifying the string for everyone else. The JVM can also optimize memory by reusing common strings.

However, immutability also means that operations which modify a string (like concatenation) are less efficient, because a new string must be created each time.

Creating Strings

There are a few ways to create a string in Java:

String literalString = "I am a literal string";
String objectString = new String("I am a String object");

Both achieve the same end result, a string with the specified value. However, there’s a slight difference in how the JVM handles these.

When you create a string literal, the JVM first checks the string pool, a special area of memory reserved just for strings. If an equivalent string already exists in the pool, the JVM simply returns a reference to that existing string, rather than allocating new memory.

String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2);  // Prints 'true'

Here, s1 and s2 actually refer to the same string object in memory, because "Hello" was already in the string pool.

In contrast, using the new keyword always creates a new object, even if an equivalent string already exists in the pool.

String s3 = new String("Hello");
System.out.println(s1 == s3);  // Prints 'false'

Here, despite s1 and s3 having the same content, they refer to different objects in memory.

If you have a string object and you want to ensure it’s using the memory-optimized string from the pool, you can use the intern() method.

String s4 = s3.intern();
System.out.println(s1 == s4);  // Prints 'true'

After interning s3, s4 now refers to the same pooled string as s1.

However, it’s important to use intern() judiciously. Overuse can actually lead to performance issues, as the string pool is a finite resource. It’s best used for strings that you expect to be frequently reused throughout your program.

String Concatenation

Concatenating strings is a common operation, and Java provides two main ways to do it.

String s1 = "Hello";
String s2 = "World";
String s3 = s1 + " " + s2;  // Using the + operator
String s4 = s1.concat(" ").concat(s2);  // Using the concat() method

Both approaches yield the same result. However, there are some differences to consider.

The + operator is often more readable and is optimized by the Java compiler into a StringBuilder operation (which we’ll cover shortly). When you use +, the compiler actually transforms it into something like this:

String s3 = new StringBuilder(s1).append(" ").append(s2).toString();

So, even though it looks like you’re creating a new string with each +, the compiler is smart enough to use a StringBuilder under the hood to optimize it.

On the other hand, the concat method is a direct method of the String class. It concatenates the specified string to the end of the current string and returns a new string. This is the signature of the method:

String concat(String str)

And here’s another example:

String s4 = s1.concat(" ");  // s4 is "Hello "
s4 = s4.concat(s2);  // s4 is now "Hello World"

One advantage of using concat is that it’s more explicit about what’s happening, you’re calling a method to concatenate strings, rather than using an operator. This can make the code more readable, especially for developers who are new to Java and might not be familiar with how the + operator is optimized.

However, concat can only concatenate one string at a time, so for concatenating multiple strings, you’d need multiple calls to concat, which can get cumbersome. The + operator allows for concatenating multiple strings in a single expression, which is often more convenient.

Ultimately, the choice between + and concat often comes down to personal preference and coding style. Many developers prefer + for its conciseness and readability, while others prefer the explicitness of concat.

However, it’s important to be cautious when using either approach in loops, as it can lead to performance issues due to the creation of many intermediate string objects. In such cases, StringBuilder should be used directly.

String result = "";
for (int i = 0; i < 100; i++) {
    result = result.concat(Integer.toString(i));  // Inefficient!
}

This code will create a new string on each iteration of the loop. For large numbers of iterations, this can be very inefficient in terms of both time and memory. A StringBuilder should be used in such cases (which we’ll discuss later).

Important String Methods

The String class provides a rich set of methods for examining and manipulating string content. Here are some of the most commonly used ones:

Each of these methods provides a specific utility, and together they form a powerful toolkit for working with strings. However, remember that due to the immutability of strings, the methods that have String as return type return a new string rather than modifying the original. For example:

String s1 = "  Hello World   ";
String s2 = s1.strip();
System.out.println(s1);  // Still prints "  Hello World   "
System.out.println(s2);  // Prints "Hello World"

This also allows for a technique known as method chaining, where multiple methods are invoked in a single expression.

String result = "  Hello World  ".trim().toUpperCase().replace('O', '0');
System.out.println(result);  // Prints "HELL0 W0RLD"

Here, the original string is trimmed of whitespace, then converted to uppercase, and finally all 'O' characters are replaced with '0'. Each method returns a new string that becomes the base for the next method in the chain.

Chaining can make your code more concise and readable, but it’s important not to overdo it. Excessively long chains can be hard to understand and debug.

Also, some of these methods work with indexes. An index ranges from 0 to length() - 1. The first char value of the sequence is at index 0, the next at index 1, and so on, just like it works with arrays.

Overriding toString()

One special method to be aware of is toString(). This method is defined in the Object class, which all classes in Java inherit from. It returns a string representation of the object.

By default, this string is not very informative (it includes the object’s class name and hash code). However, we can override toString() in our own classes to provide a more useful representation.

public class Person {
    private String name;
    private int age;

    // Constructor and other methods...

    @Override
    public String toString() {
        return "Person[name=" + name + ",age=" + age + "]";
    }
}

Now, when we print a Person object, we’ll get a nicely formatted string:

Person alice = new Person("Alice", 25);
System.out.println(alice);  // Prints "Person[name=Alice,age=25]"

This is especially useful for logging and debugging purposes.

Formatting Strings

In addition to manipulating strings, Java provides powerful tools for formatting them. The String class includes format() and formatted() methods which allow you to create a formatted string using a format string and arguments.

String name = "Alice";
int age = 25;
String city = "Florida";
String formatted = String.format("My name is %s, I'm %d years old, and I live in %s.", name, age, city);
System.out.println(formatted);
// Prints "My name is Alice, I'm 25 years old, and I live in Florida."

The format string includes placeholders (%s for strings, %d for integers, etc.) which are replaced by the corresponding arguments.

Here are some of the most common formatting placeholders:

Format Specifier Description
%s String
%c Character
%d Decimal integer
%f Floating-point number
%t Date/time
%n Newline

These are just a few examples, the full list of formatting options is quite extensive, allowing for precise control over the output format.

Using the StringBuilder Class

While the String class is powerful, its immutability can lead to performance issues when you need to make many modifications to a string. This is where StringBuilder comes in.

StringBuilder is a mutable sequence of characters. It provides similar methods to String for appending, inserting, and deleting characters. However, these methods modify the StringBuilder itself rather than creating a new object.

This mutability allows for more efficient code when you need to make multiple modifications to a string. With String, each modification creates a new string object, which can be costly in terms of time and memory if done frequently, such as in a loop. StringBuilder avoids this by modifying its internal character sequence directly.

Furthermore, StringBuilder methods can be chained together, similar to String methods. However, because StringBuilder is mutable, each method in the chain modifies the same StringBuilder instance and returns a reference to it, allowing for further chaining:

StringBuilder sb = new StringBuilder("Hello");
sb.append(" World").insert(0, "Hey, ").delete(4, 9);
System.out.println(sb);  // Prints "Hey, World"

In this example, we start with a StringBuilder containing "Hello". We then append " World" to it, insert "Hey, " at the beginning, and delete the characters from index 4 to 8 (inclusive). Each of these operations modifies the same StringBuilder instance.

You can create a StringBuilder in a few ways:

StringBuilder sb1 = new StringBuilder();  // Creates an empty StringBuilder
StringBuilder sb2 = new StringBuilder(10);  // Creates a StringBuilder with initial capacity of 10
StringBuilder sb3 = new StringBuilder("Hello");  // Creates a StringBuilder initialized with the string "Hello"

When you create a StringBuilder without specifying an initial string, it starts with a default capacity of 16 characters. If you know that you’ll be building a larger string, you can specify a higher initial capacity to avoid automatic resizing later, which can be costly.

Important StringBuilder Methods

StringBuilder provides many of the same methods as String for examining and modifying the character sequence, however, it’s worth mentioning two things:

Common methods with String:

Appending values:

Inserting data:

Deleting contents:

Replacing portions:

Reversing:

Converting to String:

Here’s an example demonstrating some of these methods:

StringBuilder sb = new StringBuilder("Hello");
sb.append(" there");  // Now contains "Hello there"
sb.insert(5, ",");  // Now contains "Hello, there"
sb.replace(7, 12, "world");  // Now contains "Hello, world"
sb.delete(5, 7);  // Now contains "Helloworld"
sb.reverse();  // Now contains "dlrowolleH"

String finalString = sb.toString();
System.out.println(finalString);  // Prints "dlrowolleH"

In this example, we start with a StringBuilder containing "Hello", then append " there" to it, insert a comma after "Hello", replace "there" with "world", delete the comma and space, reverse the whole string, and finally convert it to a String.

It’s important to note that while StringBuilder is mutable, it’s not synchronized. If multiple threads are accessing the same StringBuilder instance concurrently and at least one of the threads is modifying it, you should ensure proper synchronization in your code to avoid data corruption. If you need a thread-safe version, you can use StringBuffer, which is like StringBuilder but is synchronized (at the cost of some performance overhead).

Finally, here’s a diagram that summarizes the differences between String and StringBuilder:

┌───────────────────────────────────────────────────┐
│           String vs StringBuilder                 │
│                                                   │
│  ┌─────────────────────┐ ┌─────────────────────┐  │
│  │       String        │ │    StringBuilder    │  │
│  ├─────────────────────┤ ├─────────────────────┤  │
│  │ - Immutable         │ │ - Mutable           │  │
│  │ - Thread-safe       │ │ - Not thread-safe   │  │
│  │ - Slower for        │ │ - Faster for        │  │
│  │   concatenation     │ │   concatenation     │  │
│  │ - Less memory       │ │ - More memory       │  │
│  │   efficient for     │ │   efficient for     │  │
│  │   many modifications│ │   many modifications│  │
│  └─────────────────────┘ └─────────────────────┘  │
│                                                   │
│  Use for:               Use for:                  │
│  - Constant strings     - Building strings        │
│  - Simple concatenation - Many modifications      │
│  - Thread safety needed - Performance critical    │
│                           string operations       │
└───────────────────────────────────────────────────┘

Text Blocks

A text block provides a more concise and intuitive syntax for representing strings that preserves newlines and indentation without the need for explicit escape sequences or concatenation:

String traditional = "{\n" +
                     "  \"name\": \"John Doe\",\n" +
                     "  \"age\": 30\n" +
                     "}";

String textBlock = """
                   {
                     "name": "John Doe",  
                     "age": 30
                   }
                   """;

As you can see, the text block version is much cleaner and easier to read. It gets rid of all the noise of newline characters (\n) and escaped quotes (\") that clutter the traditional string literal. With text blocks, what you see is what you get, the string appears in your code exactly as it will be outputted.

To define a text block, you use three double-quotes (""") as the opening and closing delimiters. The content of the text block appears between these delimiters and can span multiple lines:

String textBlock = """
                   This is a Text Block.
                   It can contain multiple lines,
                     indentation,
                   and "special" characters.
                   """;

Note that the closing delimiter (""") must appear on a line by itself and be followed by a semicolon. Any whitespace after the closing delimiter on that line will be ignored.

A common misconception is that you can use a single quote to close a text block that was opened with triple quotes. However, this is not the case. The opening and closing delimiters for a text block must always be three double-quotes (""").

The compiler treats all content between the delimiters as part of the string literal, including newlines, indentation, and any other whitespace. However, there are a few rules around indentation and escaping that we’ll discuss shortly.

Features of Text Blocks

One of the key features of text blocks is their ability to represent multi-line strings naturally, without resorting to explicit newline characters or string concatenation.

String multiLine = """
                   First line
                   Second line
                   Third line
                   """;

This text block will preserve the newlines and indentation exactly as written, resulting in a string with three lines of text. You don’t need to manually add \n characters or worry about lining up concatenated strings.

Text blocks also provide automatic indentation handling based on the position of the closing delimiter. The compiler will determine a common whitespace prefix from the lines between the delimiters and automatically strip that prefix from each line.

String indented = """
                    Line 1
                      Line 2
                    Line 3
                    """;

// Equivalent to:
// "Line 1\n  Line 2\nLine 3\n"

In this example, the closing delimiter is aligned with the least indented line (Line 1). Therefore, the common whitespace prefix is four spaces, which gets stripped from each line. The resulting string will have Line 2 indented by two spaces relative to the other lines.

It’s important to note that text blocks do not automatically trim all leading and trailing whitespace. The compiler only removes the common whitespace prefix based on the closing delimiter’s position. Any additional leading or trailing whitespace will be preserved in the final string.

Consider this example:

String traditional = "  \n  ";  // This evaluates to two spaces, a newline, and two more spaces.
String textBlock = """
                     \n  
                   """;        // Evaluates to two spaces, a newline, two spaces, 
                               // and an additional final newline added by the text block syntax.

Here, the traditional string will include spaces before and after the newline as they are explicitly part of the string. In the textBlock, all spaces and the newline are preserved as they appear, and a newline is added at the end because of how text blocks handle the closing delimiter.

Another important aspect of text blocks is escaping special characters. The rules for escaping in text blocks are mostly the same as in traditional string literals, with a few caveats:

Here’s an example:

String escaped = """
                 This is a "quoted" text with \\ and \u0040.
                 """;

A common misconception is that backslashes are ignored in text blocks since they are multi-line literals. However, this is not the case. Backslashes still have their special meaning in text blocks and need to be escaped if you want to include a literal backslash in the string.

And, as you can see in the above example, text blocks also support the use of Unicode escapes (\uXXXX) for representing characters by their Unicode code points.

Lastly, text blocks can be combined with traditional string literals and even other text blocks using the + operator, just like regular strings:

String name = "John";
String greeting = """
                  Hello, """ + name + """
                  . How are you?
                  """;

Here, we concatenate a text block with a traditional string literal (name) to create a personalized greeting. The + operator appears on the same line as the opening and closing delimiters of the text blocks. Placing it on a separate line might lead to unintended whitespace in the resulting string.

The Math API

The Math API provides a rich set of static methods for performing mathematical operations. It includes methods for finding the minimum and maximum of two values, rounding numbers, determining the ceiling and floor of a value, and generating random numbers. Let’s review each of these.

Finding the Minimum and Maximum

The Math class provides min and max methods to find the minimum and maximum of two values respectively. These methods are overloaded to accept int, long, float, and double arguments:

static double max(double a, double b)
static float max(float a, float b)
static int max(int a, int b)
static long max(long a, long b)

static double min(double a, double b)
static float min(float a, float b)
static int min(int a, int b)
static long min(long a, long b)

Here are some examples:

int min = Math.min(5, 10);  // min is 5
int max = Math.max(5, 10);  // max is 10

double min2 = Math.min(5.7, 10.2);  // min2 is 5.7
double max2 = Math.max(5.7, 10.2);  // max2 is 10.2

These methods are useful when you need to enforce a range on a value.

Rounding Numbers

The Math class provides several methods for rounding numbers:

Here are some examples:

long roundedLong = Math.round(5.7);  // roundedLong is 6
int roundedInt = Math.round(5.4f);  // roundedInt is 5

double rintValue = Math.rint(5.5);  // rintValue is 6.0 (ties round to even)
double rintValue2 = Math.rint(6.5);  // rintValue2 is 6.0

double floorValue = Math.floor(5.7);  // floorValue is 5.0
double ceilingValue = Math.ceil(5.2);  // ceilingValue is 6.0

As you can see, the floor is the largest integer less than or equal to the value, while the ceiling is the smallest integer greater than or equal to the value.

Generating Random Numbers

The Math class includes a random() method that returns a double value with a positive sign, greater than or equal to 0.0 and less than 1.0:

static double random()

This method is useful for generating random numbers.

double randomValue = Math.random();  // randomValue is a random double between 0.0 and 1.0

You can use Math.random() in combination with other Math methods to generate random numbers in a specific range. For example, to generate a random integer between 1 and 10 (inclusive), you can do this:

int randomInt = (int)(Math.random() * 10) + 1;

Here’s how this works:

  1. Math.random() generates a random double between 0.0 and 1.0, let’s call it r.
  2. r * 10 is then a random double between 0.0 and 10.0.
  3. (int)(r * 10) casts this double to an int, effectively rounding it down. So now we have a random integer between 0 and 9.
  4. Finally, we add 1 to shift the range to between 1 and 10.

You can adjust this formula to generate random numbers in any integer range. For example, to generate a random number between min and max (inclusive), you can use:

int randomNum = (int)(Math.random() * (max - min + 1)) + min;

Key Points

Practice Questions

1. Which of the following statements about Java primitive and reference data types is true?

A) A double can be directly assigned to a float without casting.
B) A boolean can be cast to an int.
C) A String can be assigned to an Object reference variable.
D) A char is a reference data type.
E) An int can store a long value without any explicit casting.

2. What is the output of the following code snippet?

public class OperatorTest {
    public static void main(String[] args) {
        int a = 5;
        int b = 10;
        int c = 15;
        int result = a + b * c / a - b;
        System.out.println(result);
    }
}

A) 25
B) 35
C) 20
D) 15

3. Which of the following statements about String and StringBuilder is true?

A) StringBuilder objects are immutable.
B) String objects can be modified after they are created.
C) StringBuilder is synchronized and thread-safe.
D) StringBuilder provides methods for mutable sequence of characters.
E) String and StringBuilder have the same performance characteristics for string manipulation.

4. Which of the following statements about text blocks are true? (Choose all that apply.)

A) Text blocks can span multiple lines without needing escape sequences for new lines.
B) Text blocks preserve the exact format, including whitespace, of the code as written.
C) Text blocks can only be used within methods.
D) Text blocks automatically trim leading and trailing whitespace from each line.
E) Text blocks require a minimum indentation level of one space.

5. Which of the following statements about the Math class is true?

A) The Math.round() method returns a double.
B) The Math.random() method returns a random integer.
C) The Math.max() method can only be used with integers.
D) The Math.pow() method returns the result of raising the first argument to the power of the second argument.
E) The Math.abs() method can only be used with positive numbers.

Do you like what you read? Would you consider?


Do you have a problem or something to say?

Report an issue with the book

Contact me