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.
String
and StringBuilder
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.
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:
long
literals use the suffix L
or l
: long longNum = 1000L;
int
literals don’t require a suffix, as int
is the default value for integers: int intNum = 1000;
For floating point literals:
float
literals use the suffix F
or f
: float floatNum = 3.14f;
double
literals use the suffix D
or d
, although this suffix is optional as double is the default type for decimal literals: double doubleNum = 3.14D;
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:
F
or L
suffixHere 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.
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:
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");
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);
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();
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.
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:
++
to increment a value or !
to negate a boolean.+
, -
, *
, /
, %
) and comparison operators (>
, <
, >=
, <=
, ==
, !=
).condition ? value_if_true : value_if_false
).=
, +=
, -=
, *=
, /=
, %=
, &=
, ^=
, |=
, <<=
, >>=
, >>>=
).&&
, ||
).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.
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:
x++
happen before prefix ones like ++x
.*
, /
, %
) happen before additive ones (+
, -
).&
, |
, ^
) happen after comparisons (>
, ==
, etc.) but before logical ones (&&
, ||
).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 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.
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:
~n
for a positive number n
results in -(n + 1)
.~(-n)
results in n - 1
.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.
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.
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:
++x
and x++
, the order of operations and side effects come into play. Consider this example:
int x = 5;
int y = ++x + x++; // y = 12, x = 7
Here’s what’s happening:
++x
) is applied first. This increments x
to 6, and the value of the expression ++x
is 6.x++
) is applied. This returns the current value of x
, which is 6, and then increments x
to 7.y
is the sum of the prefix increment expression (6) and the postfix increment expression (6), which is 12.x
is 7 (due to the postfix increment), and y
is 12.int x = 5;
int y = 3 * x++ + 2; // y = 17, x = 6
This works, but it’s clearer to do the increment operation on a separate line before the expression.
The increment and decrement operators cannot be used with boolean values because boolean values can only be true
or false
. They don’t have a “next” or “previous” value like numbers do.
int x = 5;
System.out.println(++x + x++ + x--); // Output: 19
Here’s what’s happening:
++x
increments x
to 6 and returns 6x++
returns 6 (the current value of x
), then increments x
to 7x--
returns 7 (the current value of x
), then decrements x
to 6The complement operator (~
) is useful for low-level bit manipulation tasks, often used in systems programming, embedded systems, networking protocols, cryptography, and more.
The Java compiler recognizes the increment and decrement operators and generates the appropriate bytecode based on whether the operator is used as a prefix or postfix. It’s not something you need to worry about as a programmer, but it’s handled at the bytecode level.
The increment and decrement operators can be used on variables of type float and double. The same prefix/postfix rules apply as with integer types.
++x
versus x++
inside a loop, the difference in the final value of x
after the loop depends on when the increment happens. Consider:
int x = 0;
for(int i = 0; i < 5; i++) {
System.out.println(++x);
}
// Output: 1 2 3 4 5
// x is 5 after the loop
x = 0;
for(int i = 0; i < 5; i++) {
System.out.println(x++);
}
// Output: 0 1 2 3 4
// x is 5 after the loop
In both cases, x
ends up being 5, but when the values are printed is different. With ++x
, x is incremented before its value is printed, while with x++
, the original value of x
is printed before being incremented.
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 (%
).
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
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:
double
, the other is promoted to double
.float
, the other is promoted to float
.long
, the other is promoted to long
.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
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)
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:
float
and double
), it’s important to remember that they have limited precision. This can lead to small inaccuracies in calculations.
double a = 0.1;
double b = 0.2;
double sum = a + b; // 0.30000000000000004 (not exactly 0.3)
This is due to how floating-point numbers are represented in binary. For precise decimal calculations, you should use the BigDecimal
class.
int a = Integer.MAX_VALUE;
int b = 1;
int sum = a + b; // -2147483648 (minimum int value)
For floating-point types, overflow results in Infinity and underflow results in 0.
ArithmeticException
.
int a = 10;
int b = 0;
int result = a / b; // throws ArithmeticException
However, dividing a floating-point number by zero does not throw an exception. It results in Infinity
or NaN
(Not-a-Number).
double a = 10.0;
double b = 0.0;
double result = a / b; // Infinity or NaN (Not-a-Number)
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.
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
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
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:
byte a = 10;
byte b = a << 1; // Compilation error: the result is int
int c = a << 1; // OK
There is no separate unsigned left shift operator in Java. The left shift operator (<<
) inherently shifts bits to the left and fills the rightmost bits with zeros, making it effectively unsigned. The notion of signed or unsigned does not apply to left shift in the same way it does for right shift because shifting left does not involve the sign bit.
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:
+=
(addition assignment)-=
(subtraction assignment)*=
(multiplication assignment)/=
(division assignment)%=
(modulus assignment)&=
(bitwise AND assignment)|=
(bitwise OR assignment)^=
(bitwise XOR assignment)<<=
(left shift assignment)>>=
(signed right shift assignment)>>>=
(unsigned right shift assignment)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.
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.
f
is used for float literalsl
or L
is used for long literalsd
or D
is used for double literalsfloat a = 3.14f;
long b = 100L;
double c = 3.14d; // d is optional here, as double is the default for decimal literals
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).
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:
double a = 3.9999;
int b = (int) a; // b is now 3
int a = 10;
a *= 5 + 2; // a is now 70 ((10 * 5) + 2)
a = 10;
a = a * 5 + 2; // a is now 52 ((10 * 5) + 2)
=
) can be chained to assign the same value to multiple variables.
int a, b, c;
a = b = c = 10; // a, b, and c are all 10
int a = 5;
a += a++; // a is now 10 (5 + 5, then a is incremented)
int a = 10;
a *= 2 + 5; // a is now 70, not 25! (a = a * (2 + 5))
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:
==
(equal to)!=
(not equal to)Here are examples of using these operators:
int a = 10;
int b = 20;
boolean result1 = (a == b); // false
boolean result2 = (a != b); // true
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:
Symmetry: If a.equals(b)
is true
, then b.equals(a)
must also be true
.
Reflexivity: An object must be equal to itself; that is, a.equals(a)
must be true
.
Transitivity: If a.equals(b)
is true
and b.equals(c)
is true
, then a.equals(c)
must be true
.
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
.
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:
Use the @Override
annotation: This ensures you are correctly overriding the method and helps with readability.
Check for null
: The first check should be to see whether the object being compared is null
.
Check for type: Use the instanceof
operator to ensure the objects being compared are of the same type.
Cast the object: Cast the object to the correct type after checking.
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.
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:
==
or !=
, be aware that the results may not be as expected due to the imprecise nature of floating-point representation.
double a = 0.1 + 0.2;
double b = 0.3;
boolean result = (a == b); // false
The ==
and !=
operators have higher precedence than the logical operators (&&
, ||
) but lower precedence than the relational operators (<
, >
, <=
, >=
).
!=
operator is the negation of the ==
operator, so a != b
is equivalent to !(a == b)
.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:
<
(less than)>
(greater than)<=
(less than or equal to)>=
(greater than or equal to)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
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:
float
and double
) using relational operators, be aware that the results may not be as expected due to the imprecise nature of floating-point representation.
double a = 0.1 + 0.2;
double b = 0.3;
boolean result = (a <= b); // false
==
, !=
) and logical operators (&&
, ||
), but lower precedence than arithmetic 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:
&
(logical AND)|
(logical OR)^
(logical XOR)&&
(short-circuit logical AND)||
(short-circuit logical OR)!
(logical NOT)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:
&
(AND) is true
only if both operands are true
.|
(OR) is true
if at least one operand is true
.^
(XOR) is true
if exactly one operand is true
.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
}
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:
The &
, |
, and ^
operators can also be used as bitwise operators when applied to integer types (byte
, short
, int
, long
). In this context, they perform bitwise AND, OR, and XOR operations on the individual bits of the operands.
The !
operator has higher precedence than the &
, |
, ^
, &&
, and ||
operators.
The &
, |
, and ^
operators have lower precedence than the &&
and ||
operators.
The &
, |
, and ^
operators always evaluate both operands, even if the result can be determined from the first operand. This can be less efficient than using the short-circuit operators &&
and ||
when the second operand is expensive to evaluate or has side effects.
The ^
operator returns true
if and only if exactly one of its operands is true
. This is different from the behavior of the !=
operator, which returns true if the operands are not equal.
Logical operators can be combined to form complex boolean expressions. It’s important to use parentheses to clearly specify the intended order of evaluation when multiple operators are used.
boolean result = (a && b) || (c && d);
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.
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.
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).
The String
class provides a rich set of methods for examining and manipulating string content. Here are some of the most commonly used ones:
int length()
: Returns the number of characters in the string.
char charAt(int index)
: Returns the character at the specified index.
int indexOf(String str)
: Returns the index within the string of the first occurrence of the specified substring.
String substring(int beginIndex, int endIndex)
: Returns a new string that is a substring of this string.
String toLowerCase()
: Converts all of the characters in this string to lower case.
String toUpperCase()
: Converts all of the characters in this string to upper case.
boolean equals(Object anObject)
: Compares this string to the specified object.
boolean equalsIgnoreCase(String anotherString)
: Compares this string to another string, ignoring case considerations.
boolean startsWith(String prefix)
: Tests if this string starts with the specified prefix.
boolean endsWith(String suffix)
: Tests if this string ends with the specified suffix.
boolean contains(CharSequence s)
: Returns true
if and only if this string contains the specified sequence of char
values.
String replace(char oldChar, char newChar)
: Returns a new string resulting from replacing all occurrences of oldChar
in this string with newChar
.
String strip()
: Returns a string whose value is this string, with all leading and trailing white space removed.
String trim()
: Returns a string whose value is this string, with all leading and trailing space removed, where space is defined as any character whose codepoint is less than or equal to 'U+0020'
(the space character).
String indent(int n)
: Adjusts the indentation of each line of this string based on the value of n
.
String stripIndent()
: Returns a string whose value is this string, with incidental whitespace removed from the beginning and end of every line.
boolean isEmpty()
: Returns true
if, and only if, length()
is 0.
boolean isBlank()
: Returns true
if the string is empty or contains only white space codepoints, otherwise false
.
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.
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.
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.
StringBuilder
ClassWhile 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.
StringBuilder
MethodsStringBuilder
provides many of the same methods as String
for examining and modifying the character sequence, however, it’s worth mentioning two things:
StringBuilder
instance itself rather than creating a new one.StringBuilder
does not extend from String
, however, both classes extend from the CharSequence interface.Common methods with String
:
int length()
: Returns the number of characters in the StringBuilder
.char charAt(int index)
: Returns the character at the specified position.int indexOf(String str)
: Returns the index within this string of the first occurrence of the specified substring.String substring(int start)
and substring(int start, int end)
: Returns a new string that is a substring of this sequence.Appending values:
StringBuilder append(...)
: Appends the string representation of the argument to the sequence. There are overloads for all primitive types, char
arrays, CharSequence
, and Object
.Inserting data:
StringBuilder insert(int offset, ...)
: Inserts the string representation of the second argument into the sequence at the position specified by the first argument. There are overloads for all primitive types, char
arrays, CharSequence
, and Object
.Deleting contents:
StringBuilder delete(int start, int end)
: Removes the characters in a substring of this sequence.StringBuilder deleteCharAt(int index)
: Removes the char at the specified position.Replacing portions:
StringBuilder replace(int start, int end, String str)
: Replaces the characters in a substring of this sequence with characters in the specified string.setCharAt(int index, char ch)
: Sets the character at the specified index to ch
.Reversing:
void StringBuilder reverse()
: Causes this character sequence to be replaced by the reverse of the sequence.Converting to String
:
String toString()
: Returns a string representing the data in this sequence.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 │
└───────────────────────────────────────────────────┘
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.
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:
\
) in the text block, you need to escape it with another backslash (\\
).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 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.
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.
The Math
class provides several methods for rounding numbers:
int round(float)
and long round(double)
: These methods return the closest int
or long
to the argument. Halfway values (like 0.5) are rounded up, following the round half up convention.double rint(double)
: Returns the double
value that is closest in value to the argument and is equal to a mathematical integer. If two double
values that are mathematical integers are equally close, the even one is chosen.double floor(double)
: Returns the largest (closest to positive infinity) double
value that is less than or equal to the argument and is equal to a mathematical integer.double ceil(double)
: Returns the smallest (closest to negative infinity) double
value that is greater than or equal to the argument and is equal to a mathematical integer.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.
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:
Math.random()
generates a random double between 0.0 and 1.0, let’s call it r
.r * 10
is then a random double between 0.0 and 10.0.(int)(r * 10)
casts this double to an int, effectively rounding it down. So now we have a random integer between 0 and 9.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;
Java has 8 primitive data types: byte
, short
, int
, long
, float
, double
, boolean
, and char
.
Primitive types are the most basic data types and are not objects. They store simple values directly in memory.
Integer literals can be assigned using decimal, hexadecimal (prefix 0x
or 0X
), octal (prefix 0
), or binary (prefix 0b
or 0B
) notation.
Underscores can be used in numeric literals for improved readability but have restrictions on their placement.
Reference types store the memory address where an object resides, rather than the object itself.
Wrapper classes (Boolean
, Byte
, Short
, Integer
, Long
, Float
, Double
, Character
) allow primitives to be used as objects.
Autoboxing automatically converts a primitive to its wrapper class, while unboxing converts a wrapper object to its primitive type.
Wrapper classes provide methods for parsing, converting between types, and more.
Wrapper objects can be null
, while primitives cannot. Unboxing a null
wrapper object throws a NullPointerException
.
Java provides a rich set of operators for mathematical, logical, and bitwise operations.
Operator precedence determines the order of evaluation in expressions. Parentheses can change the default precedence.
Unary operators (++
, --
, +
, -
, ~
, !
) operate on a single operand. Increment and decrement operators (++
and --
) can be used in prefix or postfix form.
Binary operators (+
, -
, *
, /
, %
) operate on two operands. Numeric promotion automatically converts operands to a larger type to prevent precision loss.
Bitwise operators (&
, |
, ^
, ~
) and shift operators (<<
, >>
, >>>
) manipulate individual bits of integer values.
Assignment operators (=
, +=
, -=
, *=
, /=
, %=
, &=
, ^=
, |=
, <<=
, >>=
, >>>=
) assign values to variables. Compound assignment operators combine an operation with assignment.
Equality operators (==
and !=
) compare values for equality. For objects, ==
compares references, while equals() compares contents.
Relational operators (<
, >
, <=
, >=
) compare values and determine their relationship.
Logical operators (&
, |
, ^
, &&
, ||
, !
) perform logical operations on boolean expressions. Short-circuit operators (&&
and ||
) can skip evaluating the second operand based on the first operand’s value.
Strings in Java are immutable, meaning their value cannot be changed once created. Any operation that appears to modify a string actually creates a new string.
String literals are stored in the string pool, a special area of memory. If an equivalent string already exists in the pool, a reference to that string is returned instead of creating a new object.
Strings can be concatenated using the +
operator or the concat()
method. The +
operator is optimized by the compiler into a StringBuilder
operation.
The String
class provides many methods for examining and manipulating string content, such as length()
, charAt()
, substring()
, toLowerCase()
, equals()
, startsWith()
, endsWith()
, replace()
, trim()
, and more.
The toString()
method, inherited from the Object
class, returns a string representation of an object. It can be overridden in custom classes to provide a more informative representation.
The String
class provides format()
and formatted()
methods for creating formatted strings using placeholders.
StringBuilder
is a mutable sequence of characters. It provides similar methods to String
for appending, inserting, and deleting characters, but these methods modify the StringBuilder
itself rather than creating a new object.
StringBuilder
is more efficient than String
when many modifications need to be made, as it avoids creating a new object for each modification.
Important StringBuilder
methods include append()
, insert()
, delete()
, replace()
, reverse()
, and toString()
.
Text blocks provide a more concise and intuitive syntax for representing multi-line strings. They are defined using triple double-quotes ("""
) as delimiters.
Text blocks automatically handle newlines, indentation, and common whitespace prefixes, making them easier to read and write than traditional string literals.
The Math
class provides many static methods for performing mathematical operations, including min()
, max()
, round()
, floor()
, ceil()
, and random()
.
The random()
method can be used in combination with other Math
methods to generate random numbers in a specific range.
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?