黄子涵

4.3 用户自定义类

简单的类只包含一个简单的main方法。现在来学习如何编写复杂应用程序所需要的那种主力类(workhorse class)。通常,这些类没有main方法,却有自己的实例字段和实例方法。要想构建一个完整的程序,会结合使用多个类,其中只有一个类有main方法。

Employee类

在Java中,最简单的类定义形式为:

class ClassName
{
    field1
    field2 
    . . .
    constructor1
    constructor2
    . . .
    method1
    method2
}

下面看一个非常简单的Employee类。在编写工资管理系统时可能会用到。

class Employee
{
    // instance fields 
    private String name;
    private double salary;
    private LocalDate hireDay;
    
    // constructor 
    public Employee(String n, double s, int year, int month, int day)
    {
        name = n;
        salary = s;
        hireDay = LocalDate.of(year, month, day);
    }
    
    // a method 
    public String getName()
    {
        return name;
    }

    // more methods
    . . .
}

在这个程序中,我们构造了一个Employee数组,并填入了3个Employee对象:

Employee[] staff = new Employee[3];

staff[0] = new Employee("Carl Cracker", . . .);
staff[1] = new Employee("Harry Hacker", . . .);
staff[2] = new Employee("Tony Tester", . . .);

接下来,使用Employee类的raiseSalary方法将每个员工的薪水提高5%:

for(Employee e : staff)
    e.raiseSalary(5);

最后,调用getName方法、getSalary方法和getHireDay方法打印各个员工的信息:

for(Employee e : staff)
    System.out.println("name=" + e.getName()
        +",salary=" + e.getSalary()
        +",hireDay=" + e.getHireDay());

注意,在这个示例程序中包含两个类:Employee类和带有public访问修饰符的EmployeeTest类。EmployeeTest类包含了main方法,其中使用了前面介绍的指令。

源文件名是EmployeeTest.java,这是因为文件名必须与public类的名字相匹配。在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类。

接下来,当编译这段源代码的时候,编译器将在目录下创建两个类文件:EmployeeTest.classEmployee.class

将程序中包含main方法的类名提供给字节码解释器,以启动这个程序:

java EmployeeTest

字节码解释器开始运行EmployeeTest类的main方法中的代码。在这段代码中,先后构造了3个新Employee对象,并显示它们的状态。

程序示例

import java.time.LocalDate;

/*
 * 功能:这个程序测试这个Huangzihan_Employee类
 * @版本:1.13
 * @时间:2018-04-10
 * @作者:黄子涵
 * 
 */

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   Huangzihan_Employee[] huangzihan_staff = new Huangzihan_Employee[4];
	   
	   huangzihan_staff[0] = new Huangzihan_Employee("huangzihan", 75000, 1987, 12, 15);
	   huangzihan_staff[1] = new Huangzihan_Employee("Huangzihan", 50000, 1989, 10, 1);
	   huangzihan_staff[2] = new Huangzihan_Employee("huang_zihan", 40000, 1990, 3, 15);
	   huangzihan_staff[3] = new Huangzihan_Employee("Huang_zihan", 30000, 1996, 7, 10);
	   
	   for(Huangzihan_Employee huangzihan_e : huangzihan_staff) 
	   {
		   huangzihan_e.huangzihan_raiseSalary(5);
	   }
	   
	   for(Huangzihan_Employee huangzihan_e : huangzihan_staff) 
	   {
		   System.out.println("名字="+huangzihan_e.huangzihan_getName()+",工资="+huangzihan_e.huangzihan_getSalary()+",雇佣天数=" + huangzihan_e.getHireDay());
	   }
   }
}

class Huangzihan_Employee
{
	// instance fields
	private String huangzihan_name;
	private double huangzihan_salary;
	private LocalDate huangzihan_hireDay;
	
	// constructor
	public Huangzihan_Employee(String huangzihan_n, double huangzihan_s, int huangzihan_year, int huangzihan_month, int huangzihan_day) 
	{
		huangzihan_name = huangzihan_n;
		huangzihan_salary = huangzihan_s;
		huangzihan_hireDay = LocalDate.of(huangzihan_year, huangzihan_month, huangzihan_day);
	}
	
	// a method
	public String huangzihan_getName() 
	{
		return huangzihan_name;
	}
	
	public double huangzihan_getSalary() 
	{
		 return huangzihan_salary;
	}
	
	public LocalDate getHireDay() 
	{
		return huangzihan_hireDay;
	}
	
	public void huangzihan_raiseSalary(double huangzihan_byPercent) 
	{
		double huangzihan_raise = huangzihan_salary * huangzihan_byPercent / 100;
		huangzihan_salary += huangzihan_raise;
	}
}

运行结果

名字=huangzihan,工资=78750.0,雇佣天数=1987-12-15
名字=Huangzihan,工资=52500.0,雇佣天数=1989-10-01
名字=huang_zihan,工资=42000.0,雇佣天数=1990-03-15
名字=Huang_zihan,工资=31500.0,雇佣天数=1996-07-10

多个源文件的使用

许多程序员习惯于将每一个类存放在一个单独的源文件中。例如,将Employee类存放在文件Employee.java中,将EmployeeTest类存放在文件EmployeeTest.java中。

两种编译源程序的方法

使用通配符调用Java编译器

如果喜欢这样组织文件,可以有两种编译源程序的方法。一种是使用通配符调用Java编译器:

javac Employee*.java

键入命令

这样一来,所有与通配符匹配的源文件都将被编译成类文件。或者键入以下命令:

javac EmployeeTest.java

你可能会感到惊讶,使用第二种方式时并没有显式地编译Employee.java。不过,当Java编译器发现EmployeeTest.java使用了Employee类时,它会查找名为Employee.class的文件。如果没有找到这个文件,就会自动地搜索Employee.java,然后,对它进行编译。更重要的是:如果Employee.java版本较已有的Employee.class文件版本更新,Java编译器就会自动地重新编译这个文件。

注释

如果熟悉UNIX的make工具(或者是Windows中的nmake等工具),可以认为Java编译器内置了make功能。

剖析Employee类

首先从这个类的方法开始。通过查看源代码会发现,这个类包含一个构造器和4个方法:

public Employee(String n, double s, int year, int month, int day)
public String getName()
public double getSalary()
public LocalDate getHireDay()
public void raiseSalary(double byPercent)

public

这个类的所有方法都被标记为public。关键字public意味着任何类的任何方法都可以调用这些方法(共有4种访问级别)。

接下来,需要注意在Employee类的实例中有3个实例字段用来存放将要操作的数据:

private String name;
private double salary;
private LocalDate hireDay;

private

关键字private确保只有Employee类自身的方法能够访问这些实例字段,而其他类的方法不能够读写这些字段。

注释

可以用public标记实例字段,但这是一种很不好的做法。public数据字段允许程序中的任何方法对其进行读取和修改,这就完全破坏了封装。任何类的任何方法都可以修改public字段,从我们的经验来看,有些代码将利用这种存取权限,而这是我们最不希望看到的。因此,这里强烈建议将实例字段标记为private

最后,请注意,有两个实例字段本身就是对象:name字段是String类对象,hireDay字段是LocalDate类对象。这种情况十分常见:类包含的实例字段通常属于某个类类型。

程序示例一

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   String huangzihan_name="黄子涵是帅哥!";
	   System.out.println(huangzihan_name);
	   
	   Huangzihan_Employee huangzihan = new Huangzihan_Employee();
	   System.out.println(huangzihan.huangzihan_getName1());
   }
}

class Huangzihan_Employee
{
	private String huangzihan_name="黄子涵";

	public String huangzihan_getName1() 
	{
		return huangzihan_name;
	}
}

运行结果一

黄子涵是帅哥!
黄子涵

程序示例二

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   System.out.println(huangzihan_name);  
   }
}

class Huangzihan_Employee
{
	private String huangzihan_name="黄子涵";

	public String huangzihan_getName1() 
	{
		return huangzihan_name;
	}
}

运行结果二

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	huangzihan_name cannot be resolved to a variable

	at HuangZiHanTest.main(HuangZiHanTest.java:5)

从构造器开始

下面先看看Employee类的构造器:

public Employee(String n, double s, int year, int month, int day)
{
    name = n;
    salary = s;
    hireDay = LocalDate.of(year, month, day);
}

构造器与类同名

可以看到,构造器与类同名。在构造Employee类的对象时,构造器会运行,从而将实例字段初始化为所希望的初始状态。

例如,当使用下面这条代码创建Employee类的实例时:

new Employee("James Bond", 100000, 1950, 1, 1)

将会把实例字段设置为:

name = "James Bond";
salary = 100000;
hireDay = LocalDate.of(1950, 1, 1);    //January 1, 1950

构造器结合new运算符来调用

构造器与其他方法有一个重要的不同。构造器总是结合new运算符来调用。不能对一个已经存在的对象调用构造器来达到重新设置实例字段的目的。例如,

james.Employee("James Bond", 250000, 1950, 1, 1)  //ERROR

将产生编译错误。

有关构造器的内容

现在只需要记住:

  • 构造器与类同名。
  • 每个类可以有一个以上的构造器。
  • 构造器可以有0个、1个或多个参数。
  • 构造器没有返回值。
  • 构造器总是伴随着new操作符一起调用。

警告

请注意,不要在构造器中定义与实例字段同名的局部变量。例如,下面的构造器将不会设置salary

public Employee(String n, double s, . . .)
{
    String name = n; // ERROR 
    double salary = s; // ERROR
    . . .
}

这个构造器声明了局部变量namesalary。这些变量只能在构造器内部访问。这些变量会遮蔽(shadow)同名的实例字段。有些程序员偶尔会不假思索地写出这类代码,因为他们的手指会不自觉地增加数据类型。这种错误很难检查出来,因此,必须注意在所有的方法中都不要使用与实例字段同名的变量。

用var声明局部变量

在Java 10中,如果可以从变量的初始值推导出它们的类型,那么可以用var关键字声明局部变量,而无须指定类型。例如,可以不这样声明:

Employee harry = new Employee("Harry Hacker", 50000, 1989, 18, 1);

只需要写以下代码:

var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);

这一点很好,因为这样可以避免重复写类型名Employee

从现在开始,倘若无须了解任何Java API就能从等号右边明显看出类型,在这种情况下我们都将使用var表示法。不过我们不会对数值类型使用var,如intlongdouble,使你不用当心00L0.0之间的区别。对Java API有了更多使用经验后,你可能会希望更多地使用var关键字。

注意var关键字只能用于方法中的局部变量。参数和字段的类型必须声明。

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   var huangzihan = new Huangzihan_Employee[1];
	   huangzihan[0] = new Huangzihan_Employee("黄子涵",123);
	   for(Huangzihan_Employee a1 : huangzihan)
	   {   
	       System.out.println("使用var变量:"+a1.get_name()+a1.get_number());
	   }
	   
	   System.out.println();
	   
	   Huangzihan_Employee[] Huangzihan = new Huangzihan_Employee[1];
	   Huangzihan[0] = new Huangzihan_Employee("黄子涵",123);
	   for(Huangzihan_Employee a2 : huangzihan)
	   {   
	       System.out.println("不使用var变量:"+a2.get_name()+a2.get_number());
	   }
   }
}

class Huangzihan_Employee
{
	private String huangzihan_name;
	private int huangzihan_number;
	
	public Huangzihan_Employee(String huangzihan_s, int huangzihan_n)
	{
		huangzihan_name = huangzihan_s;
		huangzihan_number = huangzihan_n;
	}
	
	public String get_name()
	{
		return huangzihan_name;
	}
	
	public int get_number()
	{
		return huangzihan_number;
	}
}

运行结果

使用var变量:黄子涵123

不使用var变量:黄子涵123

使用null引用

一个对象变量包含一个对象的引用,或者包含一个特殊值null,后者表示没有引用任何对象。

听上去这是一种处理特殊情况的便捷机制,如未知的名字或雇用日期。不过使用null值时要非常小心。

NullPointerException异常

如果对null值应用一个方法,会产生一个NullPointerException异常。

LocalDate birthday = null;
String s = birthday.toString(); // NullPointerException

程序示例

import java.time.LocalDate;

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   LocalDate birthday = null;
	   String s = birthday.toString(); // NullPointerException
	   
	   System.out.println(s);
   }
}

运行结果

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.time.LocalDate.toString()" because "birthday" is null
	at HuangZiHanTest.main(HuangZiHanTest.java:8)

这是一个很严重的错误,类似于“索引越界”异常。如果你的程序没有“捕获”异常,程序就会终止。正常情况下,程序并不捕获这些异常,而是依赖于程序员从一开始就不要带来异常。

定义一个类时,最好清楚地知道哪些字段可能为null。在我们的例子中,我们不希望namehireDay字段为null。(不用担心salary字段。这个字段是基本类型,所以不可能是null。)

hireDay字段肯定是非null的,因为它初始化为一个新的LocalDate对象。但是name可能为null,如果调用构造器时为n提供的实参是nullname就会是null

处理null值的两种解决方法

处理null值的宽容型方法

对此有两种解决方法。“宽容型”方法是把null参数转换为一个适当的非null值:

if(n == null) name = "unknown"; else name = n;

在Java 9中,Objects类对此提供了一个便利方法:

public Employee(String n, double s, int year, int month, int day)
{
    name = Objects.requireNonNullElse(n, "unknown");
    . . .
}
程序示例
import java.util.Objects;

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   { 
	   Huangzihan_Employee[] huangzihan = new Huangzihan_Employee[1];
	   huangzihan[0] = new Huangzihan_Employee("huangzihan",123);
	   for(Huangzihan_Employee Huangzihan : huangzihan) 
	   {
		   System.out.println(Huangzihan.get_name());
		   System.out.println(Huangzihan.get_number());
	   }
   }
}

class Huangzihan_Employee
{
	private Object huangzihan_name;
	private int huangzihan_number;
	
	public Huangzihan_Employee(String huangzihan_s, int huangzihan_n)
	{
		huangzihan_number = huangzihan_n;
		huangzihan_name = Objects.requireNonNullElse(huangzihan_n, "unknown");
	}
	
	public Object get_name()
	{
		return huangzihan_name;
	}
	
	public int get_number()
	{
		return huangzihan_number;
	}
}
运行结果
123
123

处理null值的严格型方法

“严格型”方法则是干脆拒绝null参数:

public Employee(String n, double s, int year, int month, int day)
{
    Objects.requireNonNull(n, "The name cannot be null");
    name = n;
    . . .
}

程序示例

import java.util.Objects;

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   { 
	   Huangzihan_Employee[] huangzihan = new Huangzihan_Employee[1];
	   huangzihan[0] = new Huangzihan_Employee("huangzihan",123);
	   for(Huangzihan_Employee Huangzihan : huangzihan) 
	   {
		   System.out.println(Huangzihan.get_name());
		   System.out.println(Huangzihan.get_number());
	   }
   }
}

class Huangzihan_Employee
{
	private Object huangzihan_name;
	private int huangzihan_number;
	
	public Huangzihan_Employee(String huangzihan_s, int huangzihan_n)
	{
		Objects.requireNonNull(huangzihan_n, "黄子涵是帅哥!");
		huangzihan_name = huangzihan_n;
	}
	
	public Object get_name()
	{
		return huangzihan_name;
	}
	
	public int get_number()
	{
		return huangzihan_number;
	}
}

运行结果

123
0

null名字构造对象

如果有人用一个null名字构造了一个Employee对象,就会产生NullPointerException异常。乍看上去这种做法好像不太有用。不过这种方法有两个好处:

  • 1.异常报告会提供这个问题的描述。
  • 2.异常报告会准确地指出问题所在的位置,否则NullPointerException异常可能在其他地方出现,而很难追踪到真正导致问题的这个构造器参数。

注释

如果要接受一个对象引用作为构造参数,就要问问自己:是不是真的希望接受可有可无的值。如果不是,那么“严格型”方法更合适。

隐式参数与显式参数(这里不太懂!!!)

方法用于操作对象以及存取它们的实例字段。例如,以下方法:

public void raiseSalary(double byPercent)
{
    double raise = salary * byPercent / 100;
    salary += raise;
}    

将调用这个方法的对象的salary实例字段设置为一个新值。考虑下面这个调用:

number007.raiseSalary(5);

它的结果是将nunber007.salary字段的值增加5%。具体地说,这个调用将执行下列指令:

double raise = number007.salary * 5 / 100;
numbere007.salary += raise;

raiseSalary方法有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法名前的Employee类型的对象。第二个参数是位于方法名后面括号中的数值,这是一个显式(explicit)参数。(有人把隐式参数称为方法调用的目标接收者。)可以看到,显式参数显式地列在方法声明中,例如double byPercent。隐式参数没有出现在方法声明中。

关键字this

在每一个方法中,关键字this指示隐式参数。如果喜欢的话,可以如下改写raiseSalary方法:

public void raiseSalary(double byPercent) 
{
    double raise = this.salary * byPercent / 100; 
    this.salary += raise;
}    

有些程序员更偏爱这样的风格,因为这样可以将实例字段与局部变量明显地区分开来。

封装的优点

最后再仔细看一下非常简单的getName方法、getSalary方法和getHireDay方法。

public String getName()
{
    return name;
}
public double getSalary()
{
    return salary;
}    
public LocalDate getHireDay()
{
    return hireDay;
}

字段访问器

这些都是典型的访问器方法。由于它们只返回实例字段值,因此又称为字段访问器

如果将namesalaryhireDay字段标记为公共,而不是编写单独的访问器方法,难道不是更容易一些吗?

不过,name是一个只读字段。一旦在构造器中设置,就没有任何办法可以对它进行修改,这样我们可以确保name字段不会受到外界的破坏。

虽然salary不是只读字段,但是它只能用raiseSalary方法修改。特别是一旦这个值出现了错误,只需要调试这个方法就可以了。如果salary字段是公共的,破坏这个字段值的捣乱者有可能会出没在任何地方(那就很难调试了)。

获得或设置实例字段的值

有些时候,可能想要获得或设置实例字段的值。那么你需要提供下面三项内容:

  • 一个私有的数据字段;
  • 一个公共的字段访问器方法;
  • 一个公共的字段更改器方法。

这样做要比提供一个简单的公共数据字段复杂些,但却有着下列明显的好处:

首先,可以改变内部实现,而除了该类的方法之外,这不会影响其他代码。例如,如果将存储名字的字段改为:

String firstName;
String lastName;

那么getName方法可以改为返回

firstName + " " + lastName

这个改变对于程序的其他部分是完全不可见的。

当然,为了进行新旧数据表示之间的转换,访问器方法和更改器方法可能需要做许多工作。但是,这将为我们带来第二点好处:更改器方法可以完成错误检查,而只对字段赋值的代码可能没有这个麻烦。例如,setSalary方法可以检查工资是否小于0。

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   { 
	   Huangzihan_Employee[] huangzihan = new Huangzihan_Employee[1];
	   huangzihan[0] = new Huangzihan_Employee("黄", "子涵");
	   for(Huangzihan_Employee Huangzihan : huangzihan) 
	   {
		   System.out.println(Huangzihan.get_name());
	   }
   }
}

class Huangzihan_Employee
{
	private String huangzihan_firstname;
	private String huangzihan_lastname;
	
	public Huangzihan_Employee(String huangzihan_fn, String huangzihan_ln)
	{
		huangzihan_firstname = huangzihan_fn;
		huangzihan_lastname = huangzihan_ln;
	}
	
	public String get_name()
	{
		return huangzihan_firstname+huangzihan_lastname;
	}
}

运行结果

黄子涵

警告

注意不要编写返回可变对象引用的访问器方法。下面的Employee类就违反了这个设计原则,其中的getHireDay方法返回了一个Date类对象:

class Employee 
{
    private Date hireDay;
    . . .
    public Date getHireDay()
    {
        return hireDay;   //  BAD
    }
    . . .
}

更改器方法setTime

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);
// let's give Harry ten years of added seniority

出错的原因很微妙。dharry.hireDay引用同一个对象(请参见图(返回可变数据字段的引用))。对d调用更改器方法就可以自动地改变这个Employee对象的私有状态!

image

程序示例
import java.util.Date;

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   { 
	   Huangzihan_Employee huangzihan = new Huangzihan_Employee(null, 0, null);
	   Date huangzihan_d = huangzihan.get_day();
	   double tenYearsInMilliSecons = 10*365.25*24*60*60*1000;
	   huangzihan_d.setTime(huangzihan_d.getTime()-(long)tenYearsInMilliSecons);
	   System.out.println(huangzihan);
	   System.out.println(huangzihan_d);
	   System.out.println(tenYearsInMilliSecons);
   }
}

class Huangzihan_Employee
{
	private Date huangzihan_day;
	private String huangzihan_name;
	private int huangzihan_salary;
	
	public Huangzihan_Employee(String huangzihan_n, int huangzihan_s, Date huangzihan_d)
	{
		huangzihan_name = huangzihan_n;
		huangzihan_salary = huangzihan_s;
		huangzihan_day = huangzihan_d;
	}

	public Date get_day()
	{
		return huangzihan_day;
	}
	
	public String get_name() 
	{
		return huangzihan_name;
	}
	
	public int get_salary() 
	{
		return huangzihan_salary;
	}
}
运行结果
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.Date.getTime()" because "huangzihan_d" is null
	at HuangZiHanTest.main(HuangZiHanTest.java:10)

克隆(clone)

如果需要返回一个可变对象的引用,首先应该对它进行克隆(clone)。对象克隆是指存放在另一个新位置上的对象副本。下面是修改后的代码:

class Employee 
{
    . . .
    public Date getHireDay()
    {
        return(Date)  hireDay.clone();   //OK
    }
}

这里有一个经验,如果需要返回一个可变数据字段的副本,就应该使用clone

基于类的访问权限

从前面已经知道,方法可以访问调用这个方法的对象的私有数据。一个方法可以访问所属类的所有对象的私有数据,这令很多人感到奇怪!例如,下面看一下用来比较两个员工的equals方法。

class Employee 
{
    . . .
    public boolean equals(Employee other)
    {
        return name.equals(other.name);
    }    
}   

典型的调用方式是

if(harry.equals(boss)) . . .

这个方法访问harry的私有字段,这点并不会让人奇怪,不过,它还访问了boss的私有字段。这是合法的,其原因是bossEmployee类型的对象,而Employee类的方法可以访问任何Employee类型对象的私有字段。

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   { 
	   String huangzihan="黄子涵";
	   String Boss_huangzihan ="老板黄子涵";
	   Huangzihan_Employee Huangzihan = new Huangzihan_Employee(huangzihan);
	   Huangzihan_Employee Boss_Huangzihan = new Huangzihan_Employee(Boss_huangzihan);
	   
	   if(Huangzihan.equals(Boss_Huangzihan)) 
	   {
		   System.out.println("黄子涵是帅哥!!!");
	   }
	   else 
	   {
		   System.out.println("黄子涵是帅哥!");
	   }
   }
}

class Huangzihan_Employee
{
	private String huangzihan_name;
	
	public Huangzihan_Employee(String huangzihan_n)
	{
		huangzihan_name = huangzihan_n;
	}

	public boolean huangzihan_equals(Huangzihan_Employee other) 
	{
		return huangzihan_name.equals(huangzihan_name);
	}
}

运行结果

黄子涵是帅哥!

私有方法

在实现一个类时,由于公共数据非常危险,所以应该将所有的数据字段都设置为私有的。然而,方法又应该如何设计呢?尽管绝大多数方法都被设计为公共的,但在某些特殊情况下,将方法设计为私有可能很有用。有时,你可能希望将一个计算代码分解成若干个独立的辅助方法。通常,这些辅助方法不应该成为公共接口的一部分,这是由于它们往往与当前实现关系非常紧密,或者需要一个特殊协议或者调用次序。最好将这样的方法设计为私有方法。

在Java中,要实现私有方法,只需将关键字public改为private即可。

通过将方法设计为私有,如果你改变了方法的实现方式,将没有义务保证这个方法依然可用。如果数据的表示发生了变化,这个方法可能会变得难以实现,或者不再需要;这并不重要。重点在于,只要方法是私有的,类的设计者就可以确信它不会在别处使用,所以可以将其删去。如果一个方法是公共的,就不能简单地将其删除,因为可能会有其他代码依赖这个方法。

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");
}    
posted @ 2021-08-25 02:08  黄子涵  阅读(80)  评论(0编辑  收藏  举报