文章目录
4.3 用户自定义类
现在来学习如何编写复杂应用程序所需要的那种主力类。通常这些类都没有main方法。他们有自己的实例字段和实例方法。要想构建一个完整的程序,会结合使用多个类,其中只有一个有main方法。
4.3.1 Employee类
Java中,最简单的类的定义形式为:
class ClassName{
field1
field2
...
constructor1
constructor2
...
method1
method2
...
}
下面是一个非常简单的Employee类:
class Employee{
//实例变量
private String name;
private double salary;
private LocalDate hireDay;
//构造函数
public Employee(String n,double s,int year,int month,int day){
name=n;
salary=s;
hireDay=LocalDate.of(year,month,day);
}
//方法
public String getName(){
return name;
}
//其他方法
}
下面的代码是Employee类的一个实际应用的完整代码:
import java.time.LocalDate;
public class Employee {
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String n,double s,int year,int month,int day){
name=n;
salary=s;
hireDay=LocalDate.of(year, month, day);
}
public String getName(){
return name;
}
public double getSalary(){
return salary;
}
public LocalDate getHireDay(){
return hireDay;
}
public void raiseSalary(double byPersent){
double raise =salary*byPersent/100;
salary=salary+raise;
}
}
import java.time.*;
public class EmployeeTest {
public static void main(String[] args) {
Employee[] staff =new Employee[3];
staff[0]=new Employee("carl", 75000, 1987, 12, 15);
staff[1]=new Employee("harry", 50000, 1989, 10, 1);
staff[2]=new Employee("tony", 40000, 1990, 3, 15);
for (Employee e : staff){
e.raiseSalary(5);
}
for (Employee e:staff){
System.out.println("name="+e.getName()+",salary="+e.getSalary()+",hireDay="+e.getHireDay());
}
}
}
- 上面的代码中,我们构造了一个Employee数组,并且填入了三个Employee对象。
- 然后使用Employee类的raiseSalary方法将每个员工的薪水提高5%
- 最后,调用get类方法打印输出各个员工的信息
注意一下细节,文件名必须与public修饰的类的名字相匹配。在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类。
而这段代码在编译的时候,会生成两个类文件:Employee.class和EmployeeTest.class
4.3.2 多个源文件的使用
上一节的两个类可以分别放进两个java文件中,Employee.java和EmployeeTest.java。
如果没有使用集成开发环境,可以键入: javac Employee*.java
这种方式使用了通配符的方式。稍微方便一点
4.3.3 剖析Employee类
下面各个小节会将会对Employee类进行剖析。
首先,从这个类的方法开始,它包括了一个构造器和4个方法。这个类的所有方法都被标记为public,这意味着任何类的任何方法都可以调用这些方法。然后,这个类的实例会有三个实例字段用来存放将要操作的数据。关键字private意味着只有Employee类自身的方法才能访问这些实例字段。
注意,Employee有两个实例字段本身就是对象。name是String类对象,hireDay是LocalDate类对象。
这种情况是很常见的,类包含的实例字段通常属于某个类类型。
4.3.4 从构造器开始
仔细观察Employee类的构造器,
可以看到,构造器与类同名。在构造Employee类的对象时,构造器会执行,从而将实例字段初始化为所希望的初始状态。
构造方法与其他方法一个重要的不同就是,构造器总是结合new运算符来调用。不能对一个已经存在的对象调用构造函数来达到重置实例字段的目的。
记住一些构造器相关的东西:
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是和new在一起
注意,不要在构造器中定义与实例字段同名的局部变量。比如:
public Employee(String n,double s,int year,int month,int day){
String name=n; //错误
double salary=s; //错误
.......
}
上面的构造器声明了局部的name和salary。这样的变量只能在构造器内部访问。而且它们会遮蔽同名的实例字段。
4.3.5 用var声明局部变量
在Java10中,如果可以从变量的初始值推导出它们的类型,那么可以用var关键字声明局部变量,无须指定类型。
例如,
var harry=new Employee("harry",50000,1989,10,1);
这带来的好处就是不必重复写类型名。
注意,var关键字只能用于方法中的局部变量。参数和字段的类型必须声明。
4.3.6 使用null引用
一个对象变量包含一个对象的引用,或者包含一个特殊的值null。后者表示没有引用任何对象。
使用null值时要非常的小心。如果对null值应用一个方法,会产生一个NullPointerException异常。比如,
LocalDate birthday =null;
String s =birthday.toString(); // NullPointerException
在定义一个类时,你最好清楚地知道哪些字段可能为null。
在我们现在的这个例子中,name可能为null。如果调用构造器时为n提供的实参是null,name就会是null。
对此有两种解决方法:
“宽容型”方法就是把null参数转换成一个适当的非null值:
if(n==null){
name="unknown";
}else{
name=n;
}
java9中,Objects类对此提供了一个便利方法:
public Employee(String n,double s,int year,int month,int day){
name=Objects.requireNonNullElse(n,"unknown");
......
}
"严格型"方法是干脆拒绝null参数:
public Employee(String n,double s,int year,int month,int day){
Objects.requireNonNull(n,"The name can not be null");
name=n;
......
}
如果使用一个null名字构造一个Employee对象,就会产生NullPointerException异常。这有两个好处:
- 异常报告会提供这个问题的描述
- 异常报告会指出问题所在的位置
4.3.7 隐式参数与显示参数
方法用于操作对象以及存取它们的实例字段。
比如,
public void raiseSalary(double byPersent){
double raise =salary*byPersent/100;
salary=salary+raise;
}
这个方法会将调用它的对象的salary设置为一个新的值。
具体一点的,
number007.raiseSalary(5);
它的结果就是将number007.salary字段的值增加5%。具体而言,这个调用将执行下列命令:
double raise=number007.salary*5/100;
number007.salary=number007.salary+raise;
raiseSalary方法有两个参数,第一个是隐式参数,是出现在方法名前的Employee类型的对象。第二个参数是位于方法名后面括号中的数值。这是一个显式参数。
在每一个方法中,关键字this指示隐式参数。raiseSalary方法可以这么写:
public void raiseSalary(double byPercent){
double raise =this.salary*5/100;
this.salary=this.salary+raise;
}
这样做可以将实例字段与局部变量明显的分开。
4.3.8 封装的优点
getName() getSalary getHireDay() 这些都是典型的访问器方法。它们只返回实例字段值,因此也叫字段访问器。
name是只读字段,一旦在构造器中设置,就没有任何办法可以对它进行修改。就能保护name字段不受外界破坏。
salary虽然不是只读字段,但它只能用raiseSalary方法修改。好处是:一旦这个值出现错误,只需要调试这个方法就可以了。而如果salary是公共的,破坏这个字段的捣乱者可以出现在任何地方。这就非常难调试程序。
如果你想要获得或设置实例字段的值,你需要提供三项内容:
- 一个私有的数据字段
- 一个公共的字段访问器方法
- 一个公共的字段更改器方法
这样当然比直接提供一个公共的字段要复杂一些,但也是有着一些明显的好处:
首先,可以改变内部实现,这样只会影响本类的方法,但不会影响其他代码。比如:
将存储名字的字段改为:
String firstName;
String lastName;
那么getName方法可以返回: firstname+" "+lastName 。
这个改变对于程序的其他部分是完全不可见的。
当然,为了进行新旧数据表示之间的转换,访问器方法和更改器方法可能会做一些工作。但,这同时就带来了第二个好处,更改器方法可以完成错误检查。比如,setSalary方法可以检查工资是否小于0。
警告:不要编写返回可变对象引用的访问器方法!!!
比如,
class Employee{
private Date hireDay;
...
public Date getHireDay(){
return hireDay; //这样做非常不好
}
...
}
LocalDate类没有更改器方法,与之不同。Date类有一个更改器方法setTime,可以在这里设置毫秒数。所以,Date对象是可变的,这就破坏了封装性!!!
请看如下代码:
Employee harry=...;
Date d=harry.getHireDay();
double tenYearsInMilliSeconds=10*365.25*24*60*60*1000;
d.setTime(d.getTime()-(long)tenYearsInMilliSeconds);
出错的原因很微妙。d和harry.hireDay()引用同一个对象。对d调用更改器方法就可以改变Employee对象的私有状态!!!(这就很不符合封装的要求。)
如果需要返回一个可变对象的引用,首先要对他进行克隆(clone)。
对象克隆是指存放在另一个新位置上的对象副本。
修改后的代码:
class Employee{
...
public Date getHireDay(){
return (Date) hireDay.clone(); // good
}
...
}
经验之谈:如果需要返回一个可变数据字段的副本,就因该使用clone()。
4.3.9 基于类的访问权限
注意,方法可以访问调用这个方法的对象的私有数据。(这作者咋想的,能再给我绕口一点?)
一个方法可以访问所属类的所有对象的私有数据,这令很多人感到奇怪!(不奇怪吗?奇怪死了)
举个栗子,下面看一下用来比较两个员工的equals方法。
class Employee{
...
public boolean equals(Employee other){
return name.equals(other.name);
}
}
典型的调用方式是:
if(harry.equals(boss)){
...
}
这个方法访问harry的私有字段,这点并不会让人奇怪。不过,它还访问了boss的私有字段。这是合法的。原因是:boss是Employee类型的对象,而Employee类的方法可以访问任何Employee类型对象的私有数据。(看了这么久Java,头一次清晰的知道。)
4.3.10 私有方法
在设计一个类时,如果数据字段是共有的,这是非常危险的事情。所以,应该将所有的数据字段都设置为私有的。
绝大多数方法都被设计成共有的。但在某些情况下,将方法设计为私有是很有用的。
有时,你可能会希望将一个计算代码分为若干部分,每个部分都是一个辅助方法。这些辅助的方法不应该是公共的接口。这些辅助方法与当前的实现关系十分密切,或者它们之间有一定的特殊协议或是调用次序。最好的设计就是将它们设计为私有方法。
私有方法使用private关键字。
如果将方法设计成了私有,这时你改变方法的实现方式,就没有义务保证这个方法依然可用。如果数据表示发生了变化,这个方法可能会变得难以实现,或者不再需要。这都不重要。(作者到底想说啥???)
重点在于,只要方法是私有的,类的设计者就可以确信这个方法不会在别处使用,可以放心的将其删去。如果一个方法是公共的,就不能简单地删除,因为可能有其他代码依赖这个方法。
4.3.11 final实例字段
将实例字段定义为final,这样的字段必须在构造对象的时候就初始化!!!就是说,必须确保在每一个构造器执行后,这个字段的值已经设置。以后也不能修改这个字段。
class Employee{
private final String name;
...
}
上面的代码中,对象构造之后,name的值不会再改变。没有setName()方法。
final修饰符对于基本类型的字段或者不可变类的字段尤其有用。
(如果类中的所有方法都不会改变其对象,这样的类就是不可变的类,例如String类就是不可变类)
对于可变的类,使用final修饰符可能会造成混乱。比如,
private final StringBuilder evaluations;
它在Employee构造器中的初始化为:
evaluations =new StringBuilder();
final关键字表示:存储在evaluations中的对象引用不会再指示零一个不同的StringBuilder对象。
不过,这个对象可以更改:
public void giveGoldStar(){
evaluations.append(LocalDate.now()+":Gold star!\n");
}
我的理解:
- 其实就是说,final修饰是对象变量evaluations,这个对象变量中的引用不能变!!!
- 对象变量引用的对象并没有被final修饰,所以对象爱咋变咋变。final管不着。