1.隐式和显式参数
方法对对象进行操作并访问其实例字段。 例如,方法
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
为调用此方法的对象中的薪金实例字段设置新值。
number007.raiseSalary(5);
效果是将007.salary字段的值增加5%。 更具体地说,该调用执行以下指令:
double raise = number007.salary * 5 / 100;
number007.salary += raise;
raiseSalary方法具有两个参数。 第一个参数称为隐式参数,是出现在方法名称之前的Employee类型的对象。 第二个参数是方法名称后括号内的数字,是一个显式参数。 (有人将隐式参数称为方法调用的目标或接收者。)
如您所见,显式参数在方法声明中显式列出,例如,double byPercent。 隐式参数不会出现在方法声明中。在每个方法中,关键字this都引用隐式参数。 如果愿意,可以编写以下方式的raiseSalary方法:
public void raiseSalary(double byPercent)
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
一些程序员更喜欢这种样式,因为它可以清楚地区分实例字段和局部变量。
C ++注:在C ++中,通常在类之外定义方法:
void Employee::raiseSalary(double byPercent) // C++, not Java
{
. . .
}
如果您在类内定义方法,则该方法将自动成为内联方法:
class Employee{
. . .
int getName() { return name; } // inline in C++
}
在Java中,所有方法都在类本身内部定义。 这不会使它们内联,寻找内联替换的机会是Java虚拟机的工作。 即时编译器监视对短的,通常调用的且未被覆盖的方法的调用,并对其进行优化。
2.封装的好处
最后,让我们更仔细地研究一下相当简单的getName,getSalary和getHireDay方法
public String getName(){
return name;
}
public double getSalary(){
return salary;
}
public LocalDate getHireDay(){
return hireDay;
}
由于它们仅返回实例字段的值,因此有时也称为字段访问器。
将name, salary和hiredDay字段公开,而不是使用单独的访问器方法,这会更方便吗?但是,name字段是只读的。 一旦在构造函数中设置了它,就没有方法可以对其进行更改。 因此,我们可以保证name字段·永远不会被破坏。salary字段不是只读的,但是只能通过raiseSalary方法进行更改。
特别是,如果该值被证明是错误的,则仅需要调试该方法。 如果salary字段是公开的,那么搞砸值的罪魁祸首可能在任何地方。有时候,您碰巧想要获取并设置一个实例的某一字段的值。 然后,您需要提供三项:
•私有数据字段;
•公共字段访问器方法;
•公共字段更改器方法。
这比提供单个公共数据字段要繁琐得多,但是有很多好处。首先,您可以更改内部实现,而不会影响该类的方法以外的任何代码。 例如,如果名称的存储更改为
String firstName;
String lastName;
然后可以将getName方法更改为返回
firstName + " " + lastName
此更改对于程序的其余部分是完全不可见的。
当然,访问器和更改器(mutator)方法可能需要做大量工作才能在新旧数据表示之间进行转换。 这给我们带来了第二个好处:更改器(Mutator)方法可以执行错误检查,而仅分配给字段的代码可能不会造成麻烦。 例如,一个setSalary方法可能检查salary从不小于0。
注意:注意不要编写返回对可变对象的引用的访问器方法。例如,在Employee类中,getHireDay方法返回了一个classDate对象:
class Employee{
private Date hireDay;
. . .
public Date getHireDay()
{
return hireDay; // BAD
}
. . .
}
与不具有更改器(mutator)方法的LocalDate类不同,Date类具有更改器(mutator)方法setTime,您可以在其中设置毫秒数。Date对象是可变的事实破坏了封装! 考虑以下恶意代码:
Employee harry = . . .;
Date d = harry.getHireDay();
double tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000;
d.setTime(d.getTime() - (long) tenYearsInMilliSeconds);// let's give Harry ten years of added seniority
原因很微妙。 d和harry.hireDay指的是同一对象(请参见图4.5)。 如果需要返回对可变对象的引用,则应首先克隆它。克隆是存储在新位置的对象的精确副本。 这是更正后的代码
class Employee
{
. . .
public Date getHireDay()
{
return (Date) hireDay.clone(); // OK
}
. . .
}
根据经验,在需要返回可变字段的副本时,请始终使用clone.
3.类的访问原则
一种方法可以访问其类中所有对象的私有数据。 例如,考虑一个对比两个雇员的equals方法:
class Employee
{
. . .
public boolean equals(Employee other)
{
return name.equals(other.name);
}
}
一个典型的调用是:
if (harry.equals(boss)) . .
此方法访问Harry的私有字段,这并不奇怪。 它还可以访问boss对象的private 字段。 这是合法的,因为boss是类型Employee的对象,并且Employee类的方法被允许访问Employee类型的任何对象的私有字段。
C++注意:C ++具有相同的规则。 方法可以访问其类的任何对象的私有数据,而不仅仅是隐式参数。
4.final实例字段
您可以将实例字段定义为final。 构造对象时必须初始化此字段。 也就是说,您必须确保在每个构造函数结束后都已设置了字段值。 之后,该字段可能不会再次被修改。
例如,Employee类的name字段可能被声明为final,因为在构造对象之后它永不改变-没有setName方法。
class Employee{
private final String name;
. . .
}
final修饰符对于类型为基本类型或不可变类的字段特别有用。 (如果一个类的任何方法都不更改其对象,则该类是不可变的。例如,String类是不可变的。)对于可变类,final修饰符可能会造成混淆。 例如,考虑字段
private final StringBuilder evaluations;
在Employee构造函数中初始化为:
evaluations = new StringBuilder();
final关键字仅意味着存储在evaluations变量中的对象引用将永远不会再引用其他StringBuilder对象。 但是对象可以被修改:
public void giveGoldStar()
{
evaluations.append(LocalDate.now() + ": Gold star!\n");
}