Apex 类与 Java 类的区别
虽然 Apex 类以 Java 类为模型,两者工作方式相似,但它们之间存在一些重要的差异。了解这些差异对于从 Java 转向 Apex 开发的开发者至关重要。本章将全面覆盖两者的核心差异、类定义创建、命名约定、名称遮蔽、命名空间、代码版本以及自定义类型在集合中的使用。
Apex 与 Java 的主要差异
内部类与接口
- 内部类和接口只能在外部类内部声明一层深。Apex 不支持多层嵌套的内部类
- 内部类的行为类似于 Java 的静态内部类,但不需要
static关键字。内部类可以像外部类一样拥有实例成员变量,但没有指向外部类实例的隐式指针(不能使用this访问外部类) - 静态方法和静态变量只能在顶级类定义中声明,不能在内部类中声明
访问修饰符
private(默认):如果不指定访问修饰符,方法和变量默认为private——只能在其定义的 Apex 类内部访问。这与 Java 的默认包级访问不同public:意味着方法或变量可以被同一应用程序或命名空间中的任何 Apex 使用global:意味着方法或变量可以被任何有权限访问该类的 Apex 代码使用,不仅限于同一应用程序。如果你声明一个方法或变量为 global,必须同时将包含它的类也声明为global。这个修饰符用于需要在应用程序外部引用的方法——无论是通过 SOAP API 还是其他 Apex 代码
继承与方法重写
- 方法和类默认是 final 的(与 Java 相反——Java 中的方法默认是虚方法)。这意味着如果不加
virtual关键字,类不能被继承,方法不能被重写 - 使用
virtual定义修饰符来允许扩展和重写 - 重写基类方法时,必须显式使用
override关键字 - 接口中定义的方法具有与接口相同的访问修饰符(
public或global)
异常类
- 异常类必须扩展
Exception或其他用户定义的异常 - 它们的名称必须以
Exception结尾 - 异常类有四个内置的隐式构造函数,你也可以添加其他构造函数
其他关键差异
- 运行环境:Java 运行在 JVM 上;Apex 运行在 Salesforce 的多租户云平台上,受 Governor Limits 约束
- 类型系统:Apex 中所有类型都可以为 null;Java 区分原始类型和引用类型。所有 Apex 变量初始化为 null
- String 比较:Apex 中
==比较字符串值(不区分大小写,基于语言环境);Java 中==比较引用 - 集合:Apex 只有 List、Set、Map(全部使用哈希结构,无 ArrayList/HashMap/TreeMap 区分)
- 枚举:Apex 枚举无构造器;Java 枚举可以有构造器和方法
- check exception:Apex 不强制要求 checked exception
- 触发器与匿名块:类和接口可以在触发器和匿名块中定义,但只能作为局部类型
类定义与创建
使用类编辑器在 Salesforce 中创建类:
- 在 Setup 的 Quick Find 框中输入
Apex Classes,选择 Apex Classes - 点击 New
- 点击 Version Settings 指定此类使用的 Apex 和 API 版本。如果你的组织安装了来自 AppExchange 的托管包,你还可以指定此类使用每个托管包的哪个版本。使用所有版本的默认值会将类与 Apex、API 和各托管包的最新版本关联。你可以指定较旧版本的托管包来访问与最新包版本不同的组件或功能
- 在类编辑器中输入 Apex 代码。单个类最多可达100 万个字符(不包括注释、测试方法或使用
@IsTest定义的类) - 点击 Save 保存更改并返回类详情页面,或点击 Quick Save 保存更改并继续编辑。你的 Apex 类必须正确编译后才能保存
类也可以通过点击 Generate from WSDL 从 WSDL 文档自动生成。保存后,类可以通过类方法或变量被其他 Apex 代码(如触发器)调用。
关于向后兼容性:类在存储时会保存指定版本的 Apex 和 API 的版本设置。如果 Apex 类引用了已安装托管包中的组件,类引用的每个托管包的版本设置也会被保存。此外,类存储时会带有一个isValid标志——只要依赖的元数据自上次编译以来没有更改,该标志就设置为 true。如果类中使用的对象名称或字段发生任何更改(包括对对象或字段描述的编辑等表面更改),或者调用该类的另一个类发生更改,isValid标志会被设置为 false。当触发器或 Web 服务调用该类时,代码会被重新编译,如有错误则通知用户;如果没有错误,isValid 标志重置为 true。
Apex 类编辑器功能
Apex 和 Visualforce 编辑器具有以下功能:
- 语法高亮:编辑器自动对关键字、所有函数和运算符应用语法高亮
- 搜索功能:在当前页面、类或触发器中搜索文本。支持区分大小写和正则表达式(遵循 JavaScript 正则表达式规则,可跨行搜索)。替换操作支持正则表达式分组变量(
$1、$2等),例如将<h1>标签替换为<h2>标签并保留原有属性 - 跳转到指定行:高亮显示指定行号,如果该行当前不可见,编辑器会滚动到该行
- 撤销与重做:使用撤销来反转编辑操作,使用重做来恢复被撤销的操作
- 字体大小:从下拉列表中选择字体大小
- 行列位置:光标所在的行号和列号显示在编辑器底部的状态栏中
- 行列计数:总行数和总字符数显示在状态栏中
命名约定
建议遵循 Java 标准命名约定:类名以大写字母开头(PascalCase),方法名以小写动词开头(camelCase),变量名应有意义。
命名限制:
- 不能在同一类中定义同名的类和接口
- 内部类不能与其外部类同名
- 方法、变量和类在类内部有各自的命名空间,因此这三类名称不会互相冲突。特别是,变量、方法和内部类可以同名——这在 Java 中是不允许的
- 变量名不区分大小写,只能包含字母/数字/下划线,必须以字母开头,最长 255 字符,不能使用保留关键字
名称遮蔽(Name Shadowing)
成员变量可以被局部变量遮蔽——特别是函数参数。这允许方法和构造函数采用标准 Java 形式:
public class Shadow {
String s;
Shadow(String s) { this.s = s; } // 同名合法
setS(String s) { this.s = s; } // 同名合法
}
一个类中的成员变量可以遮蔽父类中同名的成员变量。如果两个类位于不同的顶级类中并由不同的团队编写,这可能很有用。例如,如果有一个对类 C 的引用,并且想要访问父类 P 中同名的成员变量 M,应该先将引用分配给 P 的引用。
静态变量的遮蔽:静态变量可以在类层次结构中遮蔽——如果父类 P 定义了静态变量 S,子类 C 也可以声明静态变量 S。在 C 内部引用 S 会引用 C 中的静态变量;要引用 P 中的静态变量,必须使用 P.S 语法。
重要限制:静态类变量不能通过类实例引用。它们必须使用原始变量名(在顶级类文件内部)或使用类名前缀来引用:
public class p1 {
public static final Integer CLASS_INT = 1;
public class c { }
}
p1.c c = new p1.c();
// 这是非法的:
// Integer i = c.CLASS_INT;
// 这才是正确的:
Integer i = p1.CLASS_INT;
命名空间前缀
Salesforce 应用程序支持使用命名空间前缀。命名空间前缀在托管 AppExchange 包中用于区分别人使用的自定义对象和字段名称。
重要:创建命名空间时,使用对用户有用且有意义的信息。不要以人名命名命名空间。一旦分配了命名空间,就无法更改。
在开发者注册全局唯一的命名空间前缀并在 AppExchange 注册后,开发者的托管包中的外部引用采用以下长格式:
namespace_prefix__obj_or_field_name__c
这些完全限定名称在更新工作中的 SOQL/SOSL 语句和 Apex 时可能很繁琐。因此,Apex 为模式名称支持默认命名空间。在查找标识符时,解析器假定当前对象的命名空间也是所有其他对象和字段的命名空间,除非另有指定。因此,存储的类必须直接使用 obj_or_field_name__c 引用定义在同一应用程序命名空间中的自定义对象和字段名称。
提示:仅在引用已从 AppExchange 安装到组织中的托管包中的自定义对象和字段时,才使用命名空间前缀。
调用包方法时使用命名空间
要调用托管包中定义的方法,Apex 允许使用完全限定标识符:
namespace_prefix.class.method(args)
版本化行为变更
在 API 版本 34.0 及更高版本中,自定义 sObjectType 上的 Schema.DescribeSObjectResult 包含带有命名空间前缀的 Map 键,即使命名空间是当前执行代码的命名空间也是如此。如果你使用多个命名空间并生成运行时描述数据,请确保代码使用命名空间前缀正确访问键。
使用 System 命名空间
System 命名空间是 Apex 中的默认命名空间。这意味着在创建系统类的新实例或调用系统方法时可以省略命名空间。例如,因为内置的 URL 类在 System 命名空间中,以下两个语句是等价的:
System.URL url1 = new System.URL('https://MyDomainName.my.salesforce.com/');
// 等价于:
URL url1 = new URL('https://MyDomainName.my.salesforce.com/');
同样,调用 URL 类的静态方法也可以省略 System 前缀:
System.URL.getCurrentRequestUrl();
// 等价于:
URL.getCurrentRequestUrl();
注意:除了 System 命名空间,System 命名空间中还有一个内置的System类,提供assertEquals和debug等方法。注意不要混淆——命名空间和类在这种情况下有相同的名称。System.debug('debug message');和System.System.debug('debug message');是等价的。
使用 System 命名空间消除歧义
不包含 System 命名空间调用系统类的静态方法更简单,但在某些情况下必须包含 System 命名空间来区分内置 Apex 类和同名的自定义 Apex 类。如果你的组织中有与内置类同名的自定义 Apex 类,Apex 运行时会默认使用你的自定义类。
例如,创建一个自定义 Apex 类:
public class Database {
public static String query() {
return 'wherefore art thou namespace?';
}
}
当执行 Database.query('SELECT Name FROM Account LIMIT 1') 时,Apex 会首先在自定义 Database 类中查找 query 方法。然而,这个自定义类中的 query 方法不接受任何参数,找不到匹配项,因此会报错。自定义 Database 类覆盖了 System 命名空间中的内置 Database 类。
解决方案:添加 System 命名空间前缀明确指示 Apex 运行时调用内置类:
sObject[] acct = System.Database.query('SELECT Name FROM Account LIMIT 1');
System.debug(acct[0].get('Name'));
使用 Schema 命名空间
Schema 命名空间提供用于处理模式元数据信息的类和方法。我们隐式导入 Schema.*,但当你代码中的非托管代码与 Schema 命名空间元素发生命名冲突时,必须完全限定你的 Schema 命名空间使用。如果你的组织中有与 sObject 同名的 Apex 类,请在代码中添加 Schema 命名空间前缀到 sObject 名称。
创建 Schema 类的实例或调用 Schema 方法时可以省略命名空间:
Schema.DescribeSObjectResult d = Account.sObjectType.getDescribe();
Map<String, Schema.FieldSet> FSMap = d.fieldSets.getMap();
// 等价于:
DescribeSObjectResult d = Account.sObjectType.getDescribe();
Map<String, Schema.FieldSet> FSMap = d.fieldSets.getMap();
使用 Schema 命名空间消除歧义
使用 Schema.object_name 来引用与自定义类同名的 sObject。这指示 Apex 运行时使用 sObject:
public class Account {
public Integer myInteger;
}
// 创建标准 Account 对象
Schema.Account myAccountSObject = new Schema.Account();
// 创建自定义类实例
Account accountClassInstance = new Account();
myAccountSObject.Name = 'Snazzy Account';
accountClassInstance.myInteger = 1;
命名空间、类和变量名的优先级
因为局部变量、类名和命名空间在理论上都可以使用相同的标识符,Apex 解析器按以下顺序评估形如 name1.name2.[...].nameN 的表达式:
- 解析器首先假定
name1是一个局部变量,name2到nameN为字段引用 - 如果第一个假设不成立,解析器假定
name1是类名,name2是静态变量名,name3到nameN为字段引用 - 如果第二个假设不成立,解析器假定
name1是命名空间名,name2是类名,name3是静态变量名,name4到nameN为字段引用 - 如果第三个假设不成立,解析器报告错误
当表达式以一组括号结尾时(如 name1.name2.[...].nameM.nameN()),解析器评估顺序类似但考虑方法调用。一旦你在表达式中进入 sObject 字段,表达式的其余部分就保持在 sObject 域内——sObject 字段不能引用回 Apex 表达式。
例如,给定以下类:
public class c {
c1 c1 = new c1();
class c1 {
c2 c2;
}
class c2 {
Account a;
}
}
以下表达式都是合法的:
c.c1.c2.a.name
c.c1.c2.a.owner.lastName.toLowerCase()
c.c1.c2.a.tasks
c.c1.c2.a.contacts.size()
类型解析与 System 命名空间中的类型
因为类型系统必须解析定义在本地或其他类中的用户定义类型,Apex 解析器按以下顺序评估类型:
- 对于类型引用
TypeN,解析器首先查找该类型作为标量类型 - 如果未找到,解析器查找本地定义的类型
- 如果仍未找到,解析器查找同名类
- 如果仍未找到,解析器查找系统类型(如 sObject)
对于类型 T1.T2,这可能意味着顶级类 T1 中的内部类型 T2,或者命名空间 T1 中的顶级类 T2(按此优先级顺序)。
Apex 代码版本
为了支持向后兼容性,类和触发器在存储时会保存特定 Salesforce API 版本的版本设置。如果 Apex 类或触发器引用了已安装托管包中的组件,类引用的每个托管包的版本设置也会被保存。这确保了随着 Apex、API 和托管包中的组件在后续发布版本中演进,类或触发器仍然绑定到具有特定已知行为的版本。
为已安装的包设置版本会确定已安装包中任何 Apex 代码的公开接口。这使你可以继续引用在最新版本的已安装包中可能已被弃用的 Apex——前提是你安装了代码被弃用之前的包版本。
通常,你引用最新的 Salesforce API 版本和每个已安装的包版本。如果你保存 Apex 类或触发器时未指定 Salesforce API 版本,则默认关联最新安装的版本。如果你保存或重新部署引用托管包的 Apex 类或触发器时未指定托管包的版本,则默认关联最新安装的托管包版本。
Apex 类与方法的版本化
当类和方法被添加到 Apex 语言中时,这些类和方法可用于你的 Apex 代码保存的所有 API 版本,无论它们是在哪个 API 版本(Salesforce 发布)中引入的。例如,如果一个方法在 API 版本 33.0 中添加,你可以在保存为 API 版本 33.0 的自定义类中使用此方法,也可以在保存为 API 版本 25.0 的类中使用。
此规则有一个例外:ConnectApi 命名空间的类和方法仅在文档中指定的 API 版本中受支持。例如,如果一个类或方法在 API 版本 33.0 中引入,则它在更早的版本中不可用。
API 版本使用指南:
- Salesforce 强烈建议使用最新的可用 API 版本
- 如果暂时无法升级到最新版本,请使用过去三年内发布的 API 版本,以获得更好的性能、安全性和兼容性
- 为降低复杂性,将你的 Apex 代码库整合到使用最少数量的 API 版本,理想情况下只使用一个 API 版本
设置类与触发器的 API 版本和包版本
设置 Salesforce API 版本
- 编辑类或触发器,点击 Version Settings
- 选择 Salesforce API 的 Version。此版本也是与类或触发器关联的 Apex 版本
- 点击 Save
重要提示:如果你将一个对象作为参数从一个 Apex 类 C1 的方法调用传递到另一个类 C2,而 C2 由于 Salesforce API 版本设置的不同而暴露了不同的字段,对象中的字段由 C2 的版本设置控制。
设置托管包版本
作为托管包订阅者,你可以指定你的托管 Apex 类和触发器使用哪个包版本。在 Summer '25 及更高版本中,包订阅者可以使用版本设置为迁移的第二代托管包(2GP)指定 Apex 类或触发器所依赖的版本。
- 在 Setup 的 Quick Find 框中输入
Apex Classes或Apex Triggers,选择相应选项 - 从列表中,点击要配置的 Apex 类或触发器的 Edit
- 点击 Version Settings 选项卡
- 从托管包的 Version 下拉菜单中,选择类或触发器引用的所需版本。即使安装了更高版本的托管包,类或触发器仍继续使用此版本,除非你手动更新版本设置
- 点击 Save
注意事项:
- 默认情况下,引用托管包的 Apex 类或触发器与最后保存或部署时安装的包版本关联
- 如果类或触发器引用了托管包,你不能删除该类或触发器的包版本设置。要查找类或触发器在何处引用了托管包,在详情页面点击 Show Dependencies
- 你还可以通过元数据部署或 API 请求来设置 Apex 类或触发器的包版本
集合中的自定义类型
List 可以保存你的用户定义类型(Apex 类)的对象。用户定义类型的 List 可以被排序。
要对这样的 List 进行排序,你的 Apex 类可以实现 Comparator 接口并将其作为参数传递给 List.sort 方法。或者,你的 Apex 类可以实现 Comparable 接口。排序标准和排序顺序取决于你为 Comparable.compareTo 或 Comparator.compare 方法提供的实现。
要执行基于语言环境的比较和排序,请使用 Collator 类。因为基于语言环境的排序可能因运行代码的用户不同而产生不同的结果,请避免在触发器或期望特定排序顺序的代码中使用它。
自定义类型作为 Map 键与 Set 元素
你可以将自定义 Apex 类的实例添加到 Map 和 Set 中。对于 Map,实例可以作为键或值添加。如果作为键添加,你的类必须实现一些特殊规则才能正确运作——即键能获取到正确的值。同样,如果 Set 元素是自定义类的实例,你的类也必须遵循这些规则。
警告:如果 Map 键或 Set 元素中的对象在添加到集合后发生了更改,由于字段值的改变,它将无法再被找到。
添加 equals 和 hashCode 方法
当使用自定义类型作为 Map 键或 Set 元素时,在你的类中提供 equals 和 hashCode 方法。Apex 使用这两个方法来确定对象键的相等性和唯一性。
这是一个完整的实现示例:
public class PairNumbers {
Integer x, y;
public PairNumbers(Integer a, Integer b) {
x = a;
y = b;
}
public Boolean equals(Object obj) {
if (obj instanceof PairNumbers) {
PairNumbers p = (PairNumbers) obj;
return ((x == p.x) && (y == p.y));
}
return false;
}
public Integer hashCode() {
return (31 * x) ^ y;
}
}
使用示例——验证 Map 的键唯一性和 Set 的去重行为:
Map<PairNumbers, String> m = new Map<PairNumbers, String>();
PairNumbers p1 = new PairNumbers(1, 2);
PairNumbers p2 = new PairNumbers(3, 4);
// 重复键
PairNumbers p3 = new PairNumbers(1, 2);
m.put(p1, 'first');
m.put(p2, 'second');
m.put(p3, 'third');
// Map 大小为 2,因为具有重复键的条目覆盖了第一个条目
System.assertEquals(2, m.size());
// 使用 == 运算符(因为类实现了 equals 方法)
if (p1 == p3) {
System.debug('p1 and p3 are equal.');
}
// 其他 Map 操作
System.assertEquals(true, m.containsKey(p1));
System.assertEquals(true, m.containsKey(p2));
System.assertEquals(false, m.containsKey(new PairNumbers(5, 6)));
// 创建 Set——验证去重
Set<PairNumbers> s1 = new Set<PairNumbers>();
s1.add(p1);
s1.add(p2);
s1.add(p3);
// 只有两个元素,因为 p3 等于 p1
System.assertEquals(2, s1.size());
理解 Apex 与 Java 的这些关键差异,将帮助 Java 开发者快速过渡到 Salesforce 平台的 Apex 开发。遵循本章的命名约定、命名空间规则和版本管理最佳实践,可以确保你的代码在各种环境中稳定可靠。