LearnFrenzy
Sign in Take a free test

Data Types, Strings, Dates and Operators · Lesson 7 of 41

Apex Operators: Arithmetic, Comparison, ?. and ??

5 min read Updated Nov 8, 2026 Saurabh Samir

Operators are the small symbols that do the work in every expression. Most behave exactly as in Java – but Apex has a few of its own: string comparison ignores case, === compares references, and two newer operators, ?. and ??, remove most of the null checks that used to clutter Apex code.

In this lesson

  • Arithmetic and assignment operators
  • Comparison: == vs ===
  • Logical operators and short-circuiting
  • Ternary, instanceof
  • Safe navigation (?.) and null coalescing (??)

Arithmetic and assignment

Integer stock = 100;
stock -= 15;                 // stock = stock - 15
stock *= 2;
Integer boxes = stock / 12;
Integer loose = Math.mod(stock, 12);       // Apex has no % operator – use Math.mod
System.debug('stock ' + stock + ' → ' + boxes + ' boxes + ' + loose + ' loose');

Integer i = 5;
Integer post = i++;          // use, then add
Integer pre = ++i;           // add, then use
System.debug('post ' + post + ', pre ' + pre + ', i ' + i);

String code = 'ORD-' + 2026 + '-' + 7;   // + with a String concatenates
System.debug(code);
System.debug(1 + 2 + ' items');          // left to right: 3 + ' items'
System.debug('items: ' + 1 + 2);         // 'items: 1' + 2

Debug log

stock 170 → 14 boxes + 2 loose
post 5, pre 7, i 7
ORD-2026-7
3 items
items: 12

Apex has no % operator – use Math.mod(a, b). And watch expressions that mix numbers and strings: evaluation runs left to right, so once a String appears, + means "join".

Comparison: == vs ===

System.debug('10 > 9: ' + (10 > 9) + ', 10 != 9: ' + (10 != 9));
System.debug('strings, ==: ' + ('Closed Won' == 'closed won'));      // case-insensitive
System.debug('strings, <: ' + ('apple' < 'Banana'));                 // case-insensitive too

Account a1 = new Account(Name = 'Acme');
Account a2 = new Account(Name = 'Acme');
Account a3 = a1;
System.debug('a1 == a2: ' + (a1 == a2));     // sObjects: compares field values
System.debug('a1 === a2: ' + (a1 === a2));   // exact equality: same object in memory?
System.debug('a1 === a3: ' + (a1 === a3));

Integer missing = null;
System.debug('null == null: ' + (missing == null));

Debug log

10 > 9: true, 10 != 9: true
strings, ==: true
strings, <: true
a1 == a2: true
a1 === a2: false
a1 === a3: true
null == null: true
  • == compares values: for sObjects and collections it compares the contents; for Strings it ignores case.
  • === and !== check whether two references point to the same object.
  • For custom classes, == compares references unless the class defines equals() and hashCode().

Logical operators short-circuit

Opportunity opp = null;

// && stops at the first false, so opp.Amount is never read when opp is null
if (opp != null && opp.Amount > 100000) {
    System.debug('big deal');
} else {
    System.debug('no opportunity, no error');
}

Boolean isManager = false, isOwner = true;
System.debug('can edit: ' + (isManager || isOwner));
System.debug('read only: ' + !(isManager || isOwner));

Debug log

no opportunity, no error
can edit: true
read only: false

Ternary and instanceof

Decimal amount = 250000;
String tier = amount >= 1000000 ? 'Enterprise' : amount >= 100000 ? 'Mid-market' : 'SMB';
System.debug(tier);

Object value = Date.newInstance(2026, 11, 8);
System.debug('Date? ' + (value instanceof Date) + ', String? ' + (value instanceof String));

SObject rec = new Contact(LastName = 'Rao');
if (rec instanceof Contact) {
    Contact c = (Contact) rec;
    System.debug('Contact: ' + c.LastName);
}

Debug log

Mid-market
Date? true, String? false
Contact: Rao

Safe navigation ?. and null coalescing ??

Records from queries often have empty lookups and blank fields. The safe navigation operator ?. stops and returns null when the left side is null, instead of throwing. The null coalescing operator ?? supplies a fallback when a value is null:

insert new Account(Name = 'Orphan Contact Co');
Contact c = new Contact(LastName = 'Iyer');      // no Account
insert c;
c = [SELECT LastName, Account.Name, Account.Owner.Name, Email FROM Contact WHERE Id = :c.Id];

// old way: nested null checks
String oldWay = (c.Account != null && c.Account.Name != null) ? c.Account.Name : 'No account';
System.debug('old way: ' + oldWay);

// new way
System.debug('?. : ' + c.Account?.Name);
System.debug('?? : ' + (c.Account?.Name ?? 'No account'));
System.debug('email: ' + (c.Email ?? 'no email on file'));

// ?? with a single-row query: no "List has no rows" exception
Account fallback = new Account(Name = 'Default account');
Account found = [SELECT Name FROM Account WHERE Name = 'Does Not Exist' LIMIT 1] ?? fallback;
System.debug('found: ' + found.Name);

// ?. does NOT protect list indexing – an empty list still throws
List<Account> none = [SELECT Name FROM Account WHERE Name = 'Does Not Exist'];
try {
    System.debug(none[0]?.Name);
} catch (ListException e) {
    System.debug('ListException: ' + e.getMessage());
}

Debug log

old way: No account
?. : null
?? : No account
email: no email on file
found: Default account
ListException: List index out of bounds: 0

Single-row queries: use ??, not ?.

Assigning a query to one record – Account a = [SELECT … LIMIT 1]; – throws "List has no rows for assignment to SObject" when nothing matches. Adding ?? fallback to the query returns the fallback instead (null is allowed too). ?. can't help here, and it doesn't make list[0] safe either. The ?? operator needs API version 60.0 or later.

Operator precedence, briefly

From highest to lowest: grouping ( ), unary ! ++ --, * /, + -, comparisons < > <= >=, equality == !=, &&, ||, ??, ternary ?:, assignment. When in doubt, add parentheses – they cost nothing and help the next reader.

Practice

Try it yourself

  1. Rewrite with ?. and ??: String region = (opp.Account != null && opp.Account.BillingState != null) ? opp.Account.BillingState : 'Unknown';

    Show answer

    String region = opp.Account?.BillingState ?? 'Unknown';

  2. What does System.debug('Total: ' + 10 + 5); print, and how do you print 15?

    Show answer

    "Total: 105" – the String is joined with 10, then with 5. Use parentheses: 'Total: ' + (10 + 5).

Quick check

How do you get a remainder in Apex?

Math.mod(a, b) – there is no % operator.

What's the difference between == and ===?

== compares values (case-insensitive for Strings); === checks for the same object reference.

What does a?.b return if a is null?

null, without throwing.

Summary

  • Use Math.mod for remainders; mind left-to-right String concatenation.
  • == compares values (ignoring case for Strings); === compares references.
  • && and || short-circuit, so put null checks first.
  • ?. and ?? replace most manual null checks.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular