
Reduce, Reuse, Refactor: What Is An Object?
Table of Contents:
Where you are in the Reduce, Reuse, Refactor series:
- On The Relationship Between Automation & Code
- What Is An Object (this post)
- Object Variables
- Interfaces & Object Inheritance
In On The Relationship Between Automation & Code, you got a glimpse at the magic of Object-oriented programming: now it’s time to step back and really examine the phrase itself, and what an object really is. For the purposes of this series, let’s consider the nature of objects as an intense duality: objects are both the “shapes” that code can take, and instances of those shapes. This is consistent with the way that other Object-oriented programming languages refer to objects, and it partially ignores the phrase “custom object”, which in Salesforce tends to muddy the waters between code and how code within a Salesforce system can be related to records within the database.
Acquiring this fundamental understanding of objects, and really digging into exactly what an object is, will help you to accelerate your ability to use objects to achieve your goals. An engineer requires many tools to do their jobs; understanding how those tools work helps to identify which tool is right for which job.
Defining Classes As Objects
It’s tempting to answer this question by simplifying massively and saying that everything can be an object in Apex. But more to the point: an object can be both the declaration for a class, as well as an instance of that class. Classes are defined in Apex by following a few key points:
- visibility keyword: “global”, “public”, or “private” (protected visibility is not applicable to classes, at least in Apex)
- sharing keywords: we’ll largely be glossing over this one for the purposes of this series, but “sharing” as a concept within Salesforce applies two key rules at a class level: which records a class has access to within the database, as well as what kinds of records and fields a class has access to
- “virtual” and “abstract” modifiers (more on this later; “static” and “sealed” keyword modifiers are not available in Apex for classes)
- “class” keyword
- the name of the class
- inheritance keywords: first, the “extends” keyword to stipulate a parent class. Multiple inheritance is not supported (only a single class name can follow the “extends” keyword), followed by interface-based inheritance using the “implements” keyword. A class can implement as many interfaces as it would like to, in a comma-separated list
Putting that all together, we might come up with something like this (ignoring #2 for now):
public class Example implements System.Queueable, System.Finalizer {
}
If we were to try to deploy this class as-is, the compiler would complain since Example
does not satisfy the contract for System.Queueable
and also does not satisfy the contract for System.Finalizer
:
Class Example must implement the method: void System.Queueable.execute(System.QueueableContext)
Class Example must implement the method: void System.Finalizer.execute(System.FinalizerContext)
We’ll get to those errors in a second. For now, let’s remove the inheritance piece of the class declaration to show how easy it is to define simple objects:
public class Example {
}
// and then, later, within another class
public class SomethingElse {
// here, the capital Example is the type declaration
// and the lowercase "example" is the variable name
// using the same word to define something takes advantage
// of something called "class shadowing", which we'll talk about later
private final Example example = new Example();
public class SomethingElse() {
// this is a constructor - more on this in just a bit
// and the below is another example of how multiple versions of a class
// can be used at any given time
Example secondExample = new Example();
}
}
But a class, by itself, makes for a pretty boring object. To make things interesting, we need to discuss methods (referred to as functions in some other languages, but generally used interchangeably).
Defining Methods For Classes
Just as classes use keywords to declare themselves, so too do methods, and they follow a similar rubric to the class-based keywords:
- visibility keyword: “global”, “public”, “protected” or “private” - “global” methods can be referred to from anywhere in a codebase, even from within code in other namespaces (more on this later), “public” methods can be referenced anywhere within the same namespace, “protected” methods can only be referred to within instances of a class or its subclasses, and “private” methods can only be referred to within the contents of the class in question
- static keyword: more on this later
- “virtual” or “abstract” and/or “override” modifiers (we’ll get more into the “override” modifier in a bit, but it does what it sounds like — it overrides a parent class’s implementation with its own implementation)
- return type: a method either returns something, or it does not. A method that does not return anything is said to do so by using the “void” keyword
- the name of the method
- arguments for the method, provided parathetically as a comma-separated list of types and the names those arguments will use within the method
To make our Example
class above deploy correctly, for example, we need to define the two methods it’s asking us for:
public class Example implements System.Queueable, System.Finalizer {
public void execute(System.QueueableContext qc) {
}
public void execute(System.FinalizerContext fc) {
}
}
In addition to the above, methods can also be annotated. Annotations are PascalCase keywords above a method declaration that begin with the character @
. I’ll be mostly glossing over these for now, in favor of referring to them when it’s actually relevant to do so, but one common annotation seen on the platform that we’ll see again in the Object Variables post is the @AuraEnabled
annotation:
@AuraEnabled
annotated methods can be referenced in your Salesforce JavaScript code via the Aura and Lightning Web Components frameworks. Static methods can be called directly; instance methods can also be decorated with@AuraEnabled
, but only when they begin with the wordget
and all this serves to do is return a computed property as part of that object to JavaScript:
public class Example {
@AuraEnabled
public static Example generateExample() {
return new Example();
}
@AuraEnabled
public String getText() {
return 'Hello, world!';
}
}
// if the generateExample method is called via JavaScript
// it returns an object that looks like this:
// { text: "Hello world!" }
Methods are the means by which we dictate how our objects should “behave”. When we call a method, we’re expecting that instance of an object to do something — something that might have a capturable result, which will also be an object, or a void result:
- an example of a capturable result is the call to
Math.round(15.4)
, which will return an instance of the Integer class (in this case, as the value 15) - an example of a void result is the call to
System.debug('some message')
, which will print an instance of the String class with the text “some message” to a log file
In the Object-oriented programming language Smalltalk, all objects are descendants of the Object
parent class by default, and Apex follows this paradigm as well. The influence that Smalltalk had on the direction that many other Object-oriented languages have taken isn’t directly applicable to working day-to-day within the Apex programming language, but it can help shed additional light on many paradigms seen throughout Java-ish languages to this day. In Smalltalk, there’s no new
keyword to initialize new instances of an object — instead, the top-level Object
class (which can vary in name) declares a method called new
, and subclasses can override that method to provide their own implementation. In languages like Java and Apex, new
the keyword declares to the system our desire to create an instance of a specific class, or object. But in order to do so we have to pass arguments to a class such that the constraints of at least one of that object’s constructor methods are satisfied. But what are constructors?
Constructors: A Special Kind Of Object Method
All Apex classes silently extend from the Object
top-level class, and that parent class gives all Apex classes access to five special methods, and the zero argument constructor method is the first. While the specifics of this phrase will make more sense as we move forward in the series, the practicalities of it can be explained immediately: new Example();
shows how a class’s constructor method can be invoked. In this case, because there is nothing between the parentheses, we are invoking the default constructor for a class. Constructors always are defined by reusing the name of the class within a method signature.
An object can redefine its zero argument based constructor:
public class Example {
// here, Example re-using the name of the class
// makes this a constructor
public Example() {
}
}
This is most commonly seen when an object only has one constructor but we want to prevent the wrong sort of access to creating instances of that object, since (remember!) methods can define (and redefine) visibility for methods that they inherit:
public class Example {
private Example() {
// ta-da!
}
public static Example getExample() {
// this is allowed, because a class can refer
// to its own private methods
return new Example();
}
}
// but within some other class
public class SomeOtherClass {
// this assignment will fail with the following message:
// Error: Method is not visible: void Example.<init>()
private final Example notAllowed = new Example();
private final Example allowed = Example.getExample();
}
Lastly, by some special rule of fate, objects lose access to their special, implicit, zero argument constructor once they’ve defined any other constructor (but they can explicitly redefine it if a zero argument constructor is also desired). If we were to return to the RetryMechanism
class from the Introduction, for example:
public abstract class RetryMechanism {
private static final Integer DEFAULT_RETRY_CAP = 5;
private final Integer retryCap;
// now that RetryMechanism has defined
// another constructor that takes in an argument
// the zero-argument constructor cannot be called
public RetryMechanism(Integer retryCap) {
this.retryCap = retryCap;
}
// etc...
}
Abstract classes don’t respond to the new
keyword:
// in some other class, this fails to deploy with:
// Abstract classes cannot be constructed: RetryMechanism
new RetryMechanism();
But if we were to briefly change the abstract
keyword to virtual
:
public virtual class RetryMechanism {
private static final Integer DEFAULT_RETRY_CAP = 5;
public RetryMechanism(Integer retryCap) {
this.retryCap = retryCap;
}
// etc...
}
Then using the new
keyword fails for a different reason: “No default constructor available in super type: RetryMechanism.” So when we look back at the actual RetryMechanism
example, the beginning of the class declares two constructors, one public and one protected:
public abstract class RetryMechanism {
private final Integer retryCap;
private static final Integer DEFAULT_RETRY_CAP = 5;
protected Object potentialNext;
public RetryMechanism(Integer retryCap) {
this.retryCap = retryCap;
}
protected RetryMechanism() {
this(DEFAULT_RETRY_CAP);
}
// etc...
}
By re-defining the zero argument constructor, we allow subclasses to optionally define a retry cap; otherwise the default value of 5 will be used when retrying. Protected visibility means that this method is only visible by subclasses of RetryMechanism
, which are declared using the extends
keyord (like the Api
class from our Intro):
// Api is referred to as a subclass of RetryMechanism
// and as such can use public and protected methods defined in
// RetryMechanism using the "this." keyword, which we'll cover
// later in this post
public class Api extends RetryMechanism {
private static HttpRequest req;
public override Boolean hasNext() {
return this.potentialNext == null;
}
public override Object next() {
return new Http().send(req);
}
public static HttpResponse makeRequest(HttpRequest req) {
req = req;
// we're allowed to call new Api() here because this
// subclass has implicit access to its parent class's
// constructor, otherwise known as its super constructor
return (HttpResponse) new Api().getOrRetry();
}
}
Compare this with other example from the Intro, which has to perform additional logic while being constructed. Objects that perform additional logic in a constructor have to first call their super constructor using the super
keyword; they also have to pass all arguments required by that constructor when doing so:
public class SlackMessenger extends RetryMechanism {
private final Slack.BotClient client;
private String slackChannelId;
private Slack.ViewReference formattedMessage;
public SlackMessenger(String appName, String slackWorkspaceId) {
// by calling the zero argument constructor
// we are explicitly opting in to using the DEFAULT value of 5
// for retries from this class
// and the call to super() MUST come first
super();
this.client = Slack.App.getAppByName(appName).getBotClientForTeam(slackWorkspaceId);
}
// etc...
}
It’s considered a best practice when defining objects with classes to minimize the use of different constructors. That’s because what begins as a convenience ends up leading to option paralysis the more constructors (and, more specifically, the more overloads, which is the word used to describe methods that use the same name) that end up being available. Additionally, unless you’re very careful, having more than one or two constructors can easily lead to situations where an object is not initialized properly because different constructors end up intended for different use-cases. Best to be safe and limit the number of constructors being used.
This same best practice largely applies to method overloads, as well. We’ll get there, don’t worry!
Invoking Object Constructors
You’ve already seen one way in which object constructors are invoked, using the new
keyword. There are two other ways that objects can be constructed within Apex, for three total:
- Using the
new
keyword. - Using the
newInstance()
method on theSystem.Type
class that’s part of the standard Apex library (caveats apply; namely, that an object has to provide a zero argument constructor, and if it has multiple constructors the zero argument constructor is the only one that will be called). - Deserialization, which is the process by which a String instance in Apex is converted to another class (we’ll cover what this actually means later on in the series).
Initializers: Like Constructors, But Different
If you’ve spent some time browsing examples of Apex code, chances are you’ve seen examples of static initializers, as they’re used here and there to do one-time setup, particularly within test classes. They look like this:
@IsTest
private class ExampleClass {
static User specificUser;
// this is an itialization block
static {
specificUser = new User();
}
}
In general, initialization blocks are useful as an organization tool, given that they provide a level of indentation that makes them stand out. A static initialization block runs once (per transaction) the very first time a class type is referenced. Static initializers aren’t the only initializers though, and this fact is routinely glossed over within Apex despite the general applicability of initializers in general. That is to say: instance initialization blocks are also allowed. They look like this:
public class AnotherExample {
// this is an instance initialization block
{
System.debug('In initialization block');
}
static {
System.debug('In static initialization block');
}
public AnotherExample() {
System.debug('In constructor');
}
}
Invoking this class leads to the following output:
// In static initialization block
// In initialization block
// In constructor
I want to be clear: it’s extremely rare to see instance initialization blocks out in the wild, but they have a purpose beyond indentation and organization that’s worth mentioning: they can be used to consolidate code that needs to be run in a class with multiple constructors, particularly classes that you don’t want to define a zero-argument constructor for. That being said, they should also be used with caution since most people are not familiar with them. Remember: code is an expression of intent, and the last thing we should aspire to is confusing others when writing code.
The Static Keyword For Methods
Objects in Apex can define both static methods and static variables. Static methods have three important distinctions when compared to regular methods (which are commonly referred to as “instance methods” because they are invoked by instances of an object, or class):
- They can only “see” the public methods for other classes, and the static methods for the class they’re defined within.
- They can only “see” the public variables for other classes, and the static variables for the class they’re defined within.
- They are stored differently than instance methods. You could say that they’re stored more efficiently, but that’s a radical simplification of an incredibly complex topic, and one we mostly won’t be covering.
A static method is not invoked on the instance of a class — an object — but rather is invoked from the type declaration of that object. Using the Api
class as an example:
public class SomeOtherClass {
public void someMethod() {
HttpRequest someRequest = new HttpRequest();
someRequest.setMethod('GET');
someRequest.setEndpoint('https://example.com');
// we invoke the makeRequest method on Api
// by referring to the type declaration for that class
HttpResponse response = Api.makeRequest(someRequest);
}
}
Contrast that with an instance method, shown in the SlackMessenger
example:
public class SomeOtherClass {
public void someMethod() {
// here we need an instance of the class
// to access its public method(s)
Slack.ChatPostMessageResponse response = new SlackMessenger(
'someSlackApp',
'some slack workspace, probably begins with a W'
).sendMessage(
'slack channel id',
// ViewReference classes map to a collection of Slack "blocks"
// and form an easy way of composing the various pieces of a rich-text
// message. They're really cool, unlike the syntax for retrieving them!
Slack.View.ExampleView.get()
);
}
}
If you’re familiar with JavaScript, I’ve seen some people liken static methods to the concept of prototypical inheritance within JavaScript, and the concept of the type for an object existing as a “prototype” can be a useful heuristic when learning the basics. In Apex, it’s also important to delineate clearly between object types, which are represented by the left-hand side of methods and variables, and the Type
class, which is a class whose object instances map to representations of other classes.
If you have an instance of a class — an object — you can only refer to instance methods on that object. For example:
// in some Apex class, this would fail with
// "Static method cannot be referenced from a non static context"
new Api().makeRequest(new HttpRequest());
Now you know how to define static methods and instance methods for an object. We only have one additional subject to cover — object variables — and then (believe it or not!) — we’ll have cleared the basics of object creation in Apex. But before we can proceed, it’s time to talk about the other special methods I referenced earlier when talking about the Object
parent class from which all Apex classes are derived from.
Methods All Objects Can Access
If you review the Object documentation for Apex, you’ll see three methods listed. But, because there’s a special case for everything there are actually four methods that we’re going to cover here!
Equals, And The “This” Keyword
Equals is a really interesting method, and that’s because it gets to do double duty — it can be called explicitly, like such:
Integer firstNumber = 1;
Integer secondNumber = 2;
// outputs: true
System.debug(firstNumber.equals(secondNumber));
That’s totally standard, and nothing unusual. But the second way that equals can be called is unusual, and it also happens to be unusual in two ways:
Integer firstNumber = 1;
Integer secondNumber = 2;
// outputs: false
System.debug(firstNumber == secondNumber);
This is functionally the equivalent of firstNumber.equals(secondNumber)
.
But hold on, you might say, what’s up with that double equals sign? And you’d be right to call that out! Equality checks using two equals is really the first time we’re using syntax that’s truly new; while I won’t be touching too much on syntax, I think it’s fair to call this one out because there’s an easy to understand rationale for using two equals signs:
- assignment uses a single equals sign to assign one or more variables to a value. For instance,
Integer firstNumber = 1
assigns the type Integer with the name “firstNumber” to the value of 1
While it’s idiomatic, or common, to see ==
being used, I know some engineers prefer to explicitly use the method-based syntax for comparison because the ==
comparison operator will prevent you from comparing two objects of differing types without extra work, whereas .equals()
the method allows you to compare disparate types immediately. For example:
public class One {
}
public class Two {
}
// fails to compile with the following message:
// Comparison arguments must be compatible types: One, Two
System.debug(new One() == new Two());
// BUT this is absolutely allowed:
System.debug((Object) new One() == (Object) new Two());
// Outputs: false
The next important thing to know about equals()
is that it’s a virtual method. Let’s start to build up a mental model for what the base Object class looks like, keeping that in mind:
global abstract class Object {
// Object's default equals implementation is
// basically checking that the memory reference
// for this and the memory reference for that
// are the same
global virtual Boolean equals(Object that) {
return this == that;
}
}
Which brings me to my next point — the all-powerful this
keyword. We’ve glossed over it until now, but this
refers to the instance of the object you’re currently working with. I tend to advise people to always preface instance methods — so, non-static ones — and instance variables (which we’ll be covering right after reviewing the Object class methods) with this.
, like such:
public class ThisExample {
// we'll be covering the final keyword soon
private final Integer firstVariable;
public ThisExample() {
// referring to a variable using this. on the left side
// and a method on the right side
this.firstVariable = this.getFirstVariable();
}
private Integer getFirstVariable() {
// instance methods can refer to static methods
// but not vice versa
return staticMethodExample(Math.random() * 1000);
}
private static Integer staticMethodExample(Double randomNumber) {
return randomNumber.round().intValue();
}
}
Something that’s not always obvious to people when they’re not familiar with the fundamentals of Object-oriented programming is that this
can be passed as an argument to any method that accepts that object’s type as a parameter:
public class ThisExample {
private final Integer firstVariable;
public ThisExample() {
this.firstVariable = this.getFirstVariable();
}
// new!
public void process() {
new ThatExample().process(this);
}
private Integer getFirstVariable() {
return staticMethodExample(Math.random() * 1000);
}
private static Integer staticMethodExample(Double randomNumber) {
return randomNumber.round().intValue();
}
}
public class ThatExample {
public void process(ThisExample thisExample) {
// do stuff here
}
}
Get comfortable with the concept of this
— it’s one of the most crucial underpinnings to “thinking in objects.”
We’ll discuss equals()
as a method more later on in this post. For now, just know that every object has an equals
method, and because every object ultimately descends from the Object parent class, equality (and inequality, through the !=
comparison operator) are the only comparison operators that can be called between disparate types. For example:
// same basic example as before
public class One {
}
public class Two {
}
// Outputs: false
System.debug((Object) new One() == (Object) new Two());
// Outputs: true
System.debug((Object) new One() != (Object) new Two());
// Fails to compile with the message:
// "Inequality operator not allowed for this type: Object"
System.debug((Object) new One() > (Object) new Two());
Now’s the time to show off a basic override example. Recall our mental model of the Object parent class, earlier:
global abstract class Object {
global virtual Boolean equals(Object that) {
return this == that;
}
}
Normally, virtual and abstract methods can be overridden by subclasses using the override
keyword when re-declaring the same method. However, because all rules must have exceptions: though all classes descend from Object, when you override equals()
, you don’t use the override
keyword:
public class One {
// no "override" keyword after public
// and before the type signature
public Boolean equals(Object that) {
if (that instanceof Two) {
return true;
}
return false;
}
}
public class Two {
// no "override" keyword after public
// and before the type signature
public Boolean equals(Object that) {
if (that instanceof One) {
return true;
}
return false;
}
}
// and if we were to re-run our debug statements now,
// using .equals where appropriate to prevent casting
// outputs: false
System.debug(new One() == new One());
// outputs: true
System.debug(new One().equals(new Two()));
// outputs: false
System.debug(new Two() == new Two());
// outputs: true
System.debug(new Two().equals(new One()));
Providing a custom implementation for equals()
will make more sense after we talk about declaring variables for objects. For now, let’s move on to hashCode()
.
The Hashcode Method
After spending what feels like quite a bit of time on equals()
, it might surprise you to learn that the behavior of the first method is closely related to this one. But what is a hash code? Put simply, it’s the unique identifier for an object — within reason, and, particularly, within the confines of a single transaction (we’ll be getting more into the question of what a transaction is later on). It has the type signature of an Integer
, which (glancing at the docs) means:
Integers have a minimum value of -2,147,483,648 and a maximum value of 2,147,483,647
So, to start with, the hash code for any given object needs to be reduced to a number, and it’s got a pretty big range of possible values.
Let’s expand our mental model for the Object parent class to include it:
global abstract class Object {
global virtual Boolean equals(Object that) {
return this == that;
}
// unlike equals, trying to "fuzz" out
// a simple implementation for hashCode()
// isn't really possible
global virtual Integer hashCode() {
return 0;
}
}
So how do we compute and define an object’s hash code? Here’s what the Apex developer documentation has to say about the rules a hash code must follow:
If the hashCode method is invoked on the same object more than once during execution of an Apex request, it must return the same value. The hash code value is the same provided no information used in equals comparisons on the object is modified. The hash code value need not remain consistent from one Apex execution request (the transaction) to another execution of the same application. If two objects are equal, based on the equals method, hashCode must return the same value. If two objects are unequal, based on the result of the equals method, it is not required that hashCode return distinct values.
Indeed, if we return to one of our example classes and debug out the hash code in two separate transactions like such:
// you can create classes that have nothing in them
public class One {
}
// outputs 483487383
System.debug(new One().hashCode());
// in a separate transaction
// outputs 1473654514
System.debug(new One().hashCode());
The reason hashCode()
occasionally becomes relevant is because it’s the combination of the equals()
method and the hashCode()
method that determines the uniqueness of objects when they’re used within Sets and as the keys in Maps. In order to demonstrate this, I’ll use an example that jumps ahead a bit to show off how variables and methods tend to coexist:
public class Tuple {
public final Object one;
public final Object two;
public Tuple(Object one, Object two) {
this.one = one;
this.two = two;
}
public Boolean equals(Object other) {
// when you don't know the type of another object
// you can use the instanceof keyword to check it
if (other instanceof Tuple) {
// surrounding a type name after the assignment operator, "="
// indicates a type hint to the compiler - it says:
// "trust me, this is an instance of the Tuple class"
Tuple that = (Tuple) other;
// we don't really care which order one and two are in
// so long as they match on both sides
return this.one == that.one && this.two == that.two ||
this.two == that.one && this.one == that.two;
}
return false;
}
}
A “tuple” is a common paradigm in programming that pairs two values together, though it’s much more effective when generics are allowed because it reduces the need to cast variables. To show off what our custom equals method is doing, we can debug out a few values:
// outputs: true
System.debug(new Tuple(1, 1) == new Tuple(1, 1));
// outputs: true
System.debug(new Tuple(1, 2) == new Tuple(2, 1));
// outputs: false
System.debug(new Tuple(1, 1) == new Tuple(2, 1));
It follows, then, that if one object equals another, we should be able to easily detect it in a set, because that’s how we see Sets working with other kinds of objects:
// when we use the curly brace initializer for collections,
// we are initializing them with values in them; in this case,
// the values of 1, 2, and 3 are put into the Set as it's initialized
// outputs: true
System.debug(new Set<Object>{ 1, 2, 3 }.contains(1));
Set<Object> records = new Set<SObject>{
new Account(Name = 'One'),
new Account(Name = 'Two')
};
// outputs: true
System.debug(records.contains(new Account(Name = 'One')));
// outputs: false
System.debug(records.contains(new Account(Name = 'Three')));
But the answer is … that’s not quite true! And you can see that with a trivial example using our new Tuple
class:
Set<Tuple> tuples = new Set<Tuple>{ new Tuple(1, 1) };
// false
System.debug(tuples.contains(new Tuple(1, 1)));
// and even worse:
tuples.add(new Tuple(1, 1));
// we'd expect this to still be 1, but it's 2!
System.debug(tuples.size());
And that’s where hashCode()
actually comes into play:
public class Tuple {
public final Object one;
public final Object two;
public Tuple(Object one, Object two) {
this.one = one;
this.two = two;
}
public Boolean equals(Object other) {
// when you don't know the type of another object
// you can use the instanceof keyword to check it
if (other instanceof Tuple) {
Tuple that = (Tuple) other;
// we don't really care which order one and two are in
// so long as they match on both sides
return this.one == that.one && this.two == that.two ||
this.two == that.one && this.one == that.two;
}
return false;
}
// here we use "override", thankfully; only "equals"
// fails to conform to this paradigm
public override Integer hashCode() {
// "^" is the bitwise or operator - hashCodes also tend
// to use prime numbers to "seed" the hash with different values
return one.hashCode() ^ two.hashCode() * 27;
}
}
And with all of that ceremony out of the way, using the set with our custom Tuple
class now works as expected:
Set<Tuple> tuples = new Set<Tuple>{ new Tuple(1, 1) };
Tuple first = new Tuple(1, 1);
// returns: 26
System.debug(first.hashCode());
// now returns: true
System.debug(tuples.contains(first));
tuples.add(new Tuple(1, 1));
tuples.add(new Tuple(1, 2));
tuples.add(new Tuple(1, 3));
// now returns: 3
System.debug(tuples.size());
// returns: 25
System.debug(new Tuple(1, 2).hashCode());
// returns 24
System.debug(new Tuple(1, 3).hashCode());
// returns 31
System.debug(new Tuple(1, 4).hashCode());
// returns 50
System.debug(new Tuple(2, 4).hashCode());
// returns 382
System.debug(new Tuple(14, 4).hashCode());
While the hashCode()
implementation I’ve just shown is a naive one, it also should suffice for 99% of use-cases. The only times implementing a custom hash code becomes particularly onerous is when the object you’re trying to hash has a ton of variables, since the hash (by necessity) should involve each and every field and property (more on the distinction between the two in the Object Variables post).
Lastly, let’s review one of the hashCode()
implementation rules that rears its head in relevance every now and then when it comes to SObjects within sets:
The hash code value is the same provided no information used in equals comparisons on the object is modified
An SObject is a generated Apex class that represents a Standard or Custom Object (which I am capitalizing to separate it as a concept from what I mean when I say “custom object”, which in the parlance of programming is commonly understood to mean a class) as an Apex class. Just as Object is the base parent class for all Apex classes, so too do all “tables” for your particular Salesforce instance inherit from the base SObject
class:
global abstract class SObject {
global void put(String fieldName, Object value) {
// ...etc
}
}
// and then, for instance:
global class Account extends SObject {
public String Name { get; set; }
public Decimal AnnualRevenue { get; set; }
// etc ...
}
// how that relates to hashCode() and equals():
Account one = new Account(Name = 'One');
Set<Account> accounts = new Set<Account>{ one };
// outputs: true
System.debug(accounts.contains(one));
// now we modify the in-memory reference we have
one.AnnualRevenue = 15;
// outputs: false
System.debug(accounts.contains(one));
// outputs: {Account:{Name=One, AnnualRevenue=15}}
System.debug(accounts);
This is one of those shocking moments for people, because it’s the first time the significance of hashCode()
becomes clear. When values are changed on any object, the hash code for that object changes. Sets and Maps require the hash code value for an object up-front in order to properly use them, because the values in Sets and the keys in Maps are promised to be unique so long as the hash code for an object remains the same. Furthermore, modifications to an object after it’s been put into a Set or Map don’t recalculate the hashing value that was used, though the collection does reflect the change to that object within it. It’s incredibly important to keep that in mind; as soon as you start using custom classes or SObjects within Sets or Maps, you have to be extremely careful about how modification of the values within those collections occurs — otherwise you run the risk of populating such a collection with duplicates. When you find yourself in situations like this, it’s best to come armed with this knowledge.
If all of this seems like too much info at the moment, I promise that it’ll make more sense once we’ve covered collections in greater detail. For now, let’s move on to the toString()
method.
The toString() Method
Let’s expand upon our mental model for the Object class once more:
global abstract class Object {
global virtual Boolean equals(Object that) {
return this == that;
}
// we know now that hashCode()'s implementation is considerably more complex
global virtual Integer hashCode() {
return 0;
}
global virtual String toString() {
// this is an example usage of the System.Type class
// in the sense that using the {class name}.class
// returns an object instance of System.Type, which
// includes the getName() method
return Object.class.getName() + ':[ all of the static and instance variables for your class ]';
}
}
If we return to our Tuple
class from the hashCode()
section, we can immediately see how the default implementation for toString()
works:
public class Tuple {
public final Object one;
public final Object two;
public Tuple(Object one, Object two) {
this.one = one;
this.two = two;
}
public Boolean equals(Object other) {
if (other instanceof Tuple) {
Tuple that = (Tuple) other;
return this.one == that.one && this.two == that.two ||
this.two == that.one && this.one == that.two;
}
return false;
}
public override Integer hashCode() {
return one.hashCode() ^ two.hashCode() * 27;
}
}
// fun fact: debug statements use toString() by default
// outputs: Tuple:[one=1, two=1]
System.debug(new Tuple(1, 1));
// second fun fact: addition with strings ALSO calls toString()
// and ANY addition where a string is used -
// denoted by text within single quotes - coerces ALL values
// that are part of that addition to an instance of type String
// with the concatenated (added) values of all of those objects'
// toString() methods
// outputs: Tuple:[one=1, two=1] concat Tuple:[one=2, two=2]
System.debug(new Tuple(1, 1) + ' concat ' + new Tuple(2, 2));
// and of course, you can always call the method explicitly
// outputs Tuple:[one:2, two: 3]
System.debug(new Tuple(2, 3).toString());
So the default implementation of toString()
is already set up to provide quite a bit of info in the event that you’re logging objects out. Custom implementations for toString()
are typically done for a few reasons:
- to omit sensitive information from being printed / persisted to logs
- to control the amount of information that’s printed out: objects with many variables tend to become unwieldy when it comes time to read them in the console
Here’s an example:
public class Person {
public String name { get; set; }
public String identifier { get; set; }
public override String toString() {
return Person.class.getName() + ':[name=' + this.name +']';
}
}
In this example, if an instance of the Person class is being passed around the system, the custom toString()
implementation prevents the identifier
property (which may include too much Personally Identifiable Information, or PII), from being displayed.
The Secret, Final Method For Objects: clone()
If you look at the Object class documentation, only equals()
and hashCode()
and toString()
are explicitly mentioned — but there’s actually a fourth method that’s present on the Object class, and it’s one that’s worth knowing a bit about.
Let’s examine how clone works by continuing to use our Tuple
class as an example:
Tuple one = new Tuple(3, 3);
Tuple two = new Tuple (42, 0);
Tuple three = one.clone();
// outputs: Tuple:[one=3, two=3]
System.debug(three);
So clone()
as a method exactly copies all of the variables from one object instance to another one. If you’re familiar with the abstract SObject
class within the standard Apex library, you might be aware of clone()
— and the overloads for it — because it’s commonly used to clone records in memory. In order to benchmark the performance for clone()
, let’s run two snippets in separate transactions:
Long startingTime = System.now().getTime();
List<Tuple> tuples = new List<Tuple>();
for (Object unused : new Object[100000]) {
tuples.add(new Tuple(1, 1));
}
// outputs: Filling 100k tuples took: 5.333 seconds
System.debug('Filling 100k tuples took: ' + (System.now().getTime() - startingTime) / 1000.00 + ' seconds');
// and then again, using clone
Long startingTime = System.now().getTime();
List<Tuple> tuples = new List<Tuple>();
Tuple template = new Tuple(1, 1);
for (Object unused : new Object[100000]) {
tuples.add(template.clone());
}
// outputs: Cloning 100k tuples took: 2.924 seconds
System.debug('Cloning 100k tuples took: ' + (System.now().getTime() - startingTime) / 1000.00 + ' seconds');
As we delve into benchmarking in more detail, you’ll find that run times can and will vary wildly, but the underpinnings of this experiment hold true across many different transactions: clone()
is fast as hell. It turns out that copying objects is much more performant than instantiating new ones. There are two lessons to be learned here:
- Knowing that
clone()
outperforms the construction of new objects might come in handy for situations where you need to rapidly create many different object instances - Knowing how to benchmark is an important skill — and we can use objects to make benchmarking easy. We’ll return to this subject soon!
To follow up on point #2, and the importance of being able to test hypotheses, there’s a great Kent Beck saying that I love to quote:
We could carefully reason about this given our knowledge of the system, but we have clean code and we have tests that give us confidence that the clean code works. Rather than apply minutes of suspect reasoning, we can just ask the computer by making the change and running the tests. In teaching Test Driven Development (TDD), I see this situation all the time—excellent software engineers spending 5 to 10 minutes reasoning about a question that the computer could answer in 15 seconds. Without the tests you have no choice, you have to reason. With the tests you can decide whether an experiment would answer the question faster. Sometimes you should just ask the computer.
While we haven’t actually started to cover the subject material of testing in any great detail, running code to challenge our assumptions is a form of testing. I know many talented engineers that possess stellar powers of reasoning, divine critical thinking, and legendary powers of recall, and many of them still grind to a halt when trying to answer questions that a computer can answer easily. One of the basic skills that can immediately elevate your ability to outperform your peers is this basic tenet: code can answer questions faster than you can think about them.
Defining Classes For Classes
As though methods weren’t enough, I’ll mention (in passing for now) that classes can themselves define inner classes, though in Apex inner classes cannot themselves define nested inner classes. We’ll be discussing the practicalities of how useful inner classes can be later on in the series; for now, I’ll simply show the syntax off for declaring them:
public class OuterClassExample {
public class InnerExample {
// can refer to static variables
// (which we will cover in the next chapter/post)
// of OuterClassExample, but cannot declare
// its own static variables
}
public InnerExample createInnerExample() {
return new InnerExample();
}
}
// and then outside of OuterClassExample, inner classes
// end up referred to using dot notation:
OuterClassExample.InnerExample innerExample =
new OuterClassExample().createInnerExample();
Inner classes are insanely useful — truly one of the greatest features that Object-oriented programming brings to the table (!) — but we’ll need to cover a bit more subject material before beginning to peel back exactly why that is. Read the Object Variables post for a sneak peek at using inner classes effectively!
Conclusion
In this post, we learned a lot about objects: how they’re both blueprints for and instances of classes. How they’re defined, and how the methods within them are defined. While we’re not quite out of the woods on the subject, having skirted rather neatly around the notion of variables and how those contribute to the makeup and performance of a class, we’ve still managed to cover quite a bit of subject material:
- how to define an object using classes
- how an object can only extend one parent-level class
- how an object can implement as many interfaces as you’d like, so long as it satisfies the “contract” of methods defined by that interface
- how to define methods on an object, and how visibility keywords affect where that method can be called
- that all objects in Apex descend from the Object parent class, which has four methods on it:
equals()
, which doesn’t require the override keyword when creating a custom equals implementationhashCode()
, which allows an object to define its own uniqueness following a specific formulatoString()
, which is used implicitly in many places and explicitly “shows” the representation of an object according to a pre-defined set of rules or, optionally, according to a custom implementation that you can provideclone()
, the hidden method, which quickly copies all of the variables from one object instance to another one
Along the way, you’ve learned about the static keyword, about prototypes, and a little bit about inheritance and testing. Maybe it’s all been a bit of a blur. I’ll try to not undersell this information, because it’s not talked about enough and understanding (truly understanding!) the basics is absolutely crucial. Don’t believe me? Not knowing the fundamentals of Apex and trying to be an engineer is like trying to live in a home with a poorly built foundation. Sooner or later, it’s going to cost you.
Now that we’ve covered the basics about the Object parent class, it’s time to deepen our understanding of another fundamental pillar: that of variables. Let’s go!
- ← Reduce, Reuse, Refactor: On The Relationship Between Automation & Code
- Reduce, Reuse, Refactor: Object Variables →