本小节学习如何编写复杂应用程序所需要的那种主力类。
这些类通产没有main方法,却有自己的实例字段和实例方法
会结合使用多个类,其中只有一个类有main方法
Java类实现——示例
类定义格式
最简单的类定义格式为:
class ClassName{
field1;
field2;
.....;
方法1;
方法2;
方法3;
方法4;
....;
}
简单的类的实现
class Emoloyee{
private String name;//姓名
private double salary;//工资
private LocalDate hireDay;//入职日期
//构造器
public Emoloyee(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 byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
}
类代码测试
import java.time.LocalDate;
public class EnployeeTest {
public static void main(String[] args) {
Emoloyee[] staff = new Emoloyee[3];
staff[0] = new Emoloyee("张三",3600,2021,10,30);
staff[1] = new Emoloyee("李四",80000,2020,10,30);
staff[2] = new Emoloyee("王五",4000,1998,10,30);
//每个人涨工资5%
for (Emoloyee e: staff){
e.raiseSalary(5);
}
//打印每个人的工资
for (Emoloyee e:staff){
System.out.println(e.getName()+"涨工资后工资为:"+e.getSalary());
}
}
}
class Emoloyee{
private String name;//姓名
private double salary;//工资
private LocalDate hireDay;//入职日期
//构造器
public Emoloyee(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 byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
}
运行结果
张三涨工资后工资为:3780.0
李四涨工资后工资为:84000.0
王五涨工资后工资为:4200.0
代码解析
在这个程序中包含两个类:Emplyee类和带有public修饰符的EmplyeeTest类,其中使用了前面介绍的指令
文件名必须与public类的名字相匹配,在一个源文件中,只能有一个public类。但是可以有任意数目的非公共类
当编译这个源代码的时候,编译器在目录下创建两个类文件:EmployeeTest.class
和Employee.class
将程序中包含main方法的类名提供给字节码解释器,以启动这个程序
java EmployeeTest
字节码解释器开始运行EmployeeTest类的main方法中的代码,在这段代码中,先后构造了三个新的Employee对象,并显示他们的状态。
多个源文件的使用
很多程序员喜欢将每一个类存放在单独的源文件中。
如果喜欢这样组织文件,可以有两种编译方式。
一是使用通配符调用Java编译器:
javac Employee*.java
这样依赖,所有与通配符匹配的源文件都将被编译成类文件
二是键入下列命令
javac EmployeeTest.java
使用第二种方式时并没有显式地编译Employee.java。
当Java编译器发现EmoplyeeTest.java使用了Employee类时候,他会查找名为Emplyee.java地文件。如果没有找到这个文件,就会自动地搜索Employee.java,并将其编译
注意:当Employee类更新之后,Java编译器会重新编译这个文件
剖析Employee类
方法
public Emoloyee(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意味着任何类的任何方法都可以调用这些方法。(Java中一共有四种访问级别)
实例字段
private String name;//姓名
private double salary;//工资
private LocalDate hireDay;//入职日期
该类有三个实例字段用来存放数据。
关键字private确保只有Employee类自身能够访问这些实例字段,二其他类的方法不能够读写这些字段。
注意:有两个实例字段本身就是对象,String
和LocalDate
从构造器开始
该类的构造器如下
public Emoloyee(String n,double s,int year,int month,int day){
name = n;
salary = s;
hireDay = LocalDate.of(year,month,day);
}
构造器特点
构造器与类同名
在构造Employee类的对象时,构造器会运行,从而将实例字段初始化为,想要的初始状态。
构造器与其他方法有一个重要的不同。构造器总是结合new运算符来调用。不能对一个已经存在的对象带哦用构造器来达到重新设置实例字段的目的。
现在只需要记住:
- 构造器与类同名
- 每个类可以有一个以上的构造器。
- 构造器可以有0个、1个或多个参数。
- 构造器没有返回值
- 构造器总是伴随着new操作符一起调用。
C++注释
Java构造器的工作方式和C++一样,但是,所有Java对象都是在堆中构造的,构造器总是结合new操作符一起使用。C++程序员总容易犯的错误就是忘记new操作符
警告
不要在构造器中定义域实例字段同名的局部变量
这些变量会遮蔽同名的实例字段
必须注意在所有的方法中都不要使用与实例字段同名的变量。
用var声明实例变量(Java 10)
在Java10中,如果可以从变量的初始值推导出它们的类型,那么可以用var关键字声明局部变量,而无需指定类型。
倘若无需了解任何Java API就能从等号右边明显看出类型,在这种情况下,我们都将使用var关键字。
注意:var关键字只能用于方法中的局部变量。参数和字段类型必须声明。
使用null引用
一个对象变量包含一个对象的引用,或者包含一个特殊的值null,后者表示没有引用任何对象。
这是一种处理特殊情况的便捷机制,不过使用null值要非常小心
如果对null值引用一个方法,会产生一个NullPointerException
异常
这是一个很严重的错误,类似于”越界索引异常“。
正常情况下,程序并不捕获这些异常,而是依赖于程序员一开始就不要带来异常。
定义一个类时,最好清楚的直到哪些字段可能为null
关于对象变量引用为null时的解决办法
“宽容型”解决办法
把null换成一个适当的非null值,代码类似如下:
if (n == null) name = "unknow";else name = n;
在Java9中,object类对此提供了一个便利的方法
public Emoloyee(String n,double s,int year,int month,int day){
name = Objects.requireNonNullElse(n,"unknown");
......
}
“严格型”解决办法
该办法干脆地拒绝了null参数
public Emoloyee(String n,double s,int year,int month,int day){
Objects.requireNonNull(n,"The name cannot be null");
name = n;
}
如果有人给构造器传递一个null对象,俺么会产生一个NullPointerException
异常。
这种做法有两个好处:
- 异常报告会提供这个问题的描述。
- 异常报告会准确地指出问题所在的为止,否则
NullPointException
异常可能在其他地方出现,而很难追踪到真正导致问题的这个构造函数。
注释:
如果要接受一个对象引用作为构造参数,要问问自己:是不是真的希望接受可有可无的值。如果不是,那么“严格型”的方法更适合。
隐式参数与显示参数
方法用于操作对象以及存储它们的实例字段
//按百分比涨工资
public void raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
该方法有两个参数。第一个参数称为隐式参数,是出现在方法前的Employee类型的对象。第二个参数是位于方法名后面括号中的数值,这是一个显示参数。
上述方法中,隐式参数没有写出来,实际上应该是Emoloyee
,而显式参数便是double byPercent
在每一个方法中,关键字this指示隐式参数。如果喜欢的话,可以改写上述方法:
//按百分比涨工资
public void raiseSalary(double byPercent){
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
有些程序员更偏爱这种风格,因为可以将实例字段与局部变量明显的区分开来。
封装的优点
//对实例字段进行封装,三个实例字段只允许初始化,不允许修改
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
这三个方法都是典型的访问器方法。由于它们只返回实例字段,因此又称为字段访问器
将字段设置成私有的,同时提供访问其方法,是封装的典型操作之一。
有些时候,你想要获取或设置实例字段的值,可能需要以下三项内容
- 一个私有的数据字段
- 一个公共的字段访问器方法
- 一个公共的字段更改器方法
这样做比直接操作实例变量更麻烦一点,但是却有着明显的好处
- 可以改变内部实现,而除了该类的方法以外,这不会影响其他代码。
- 更改器方法可以完成错误检查,而直接对字段进行复制可能就没有这个好处。
注意
不要返回可变对象引用的访问器方法。
本例中的LocalDate和String都是不可变的对象,因此可以直接返回,而没有潜在风险。
这样子会破坏封装性。
基于类的访问权限
方法可以访问调用这个方法的对象的私有数据。
一个方法可以访问所有类的所有对象的私有数据。
C++注释
C++具有同样的原则,方法可以访问所属类任何对象的私有特性,而不仅限于隐式参数
私有方法
尽管大多数时候方法都被设计成公共的,但是某些特殊情况下,将方法设计成私有可能很有用
将方法设计成私有的
如果你希望将一个计算代码分解成若干个独立的辅助方法。通常这些辅助方法不应该成为公共接口的一部分,这是由于他们往往与当前实现关系非常紧密。或者需要一个特殊协议或者调用次序。
最好将这样的方法设置成私有的。
如何将方法设置成私有的?
只需要将关键字public
换成private
就可以了
注意
将方法设计成私有的,将没有以为保证这个方法依然可用。如果数据的表示发生了变化,这个方法可能会变得难以实现,或者不再需要;这并不重要。
重点在于,只要方法是私有的,类的设计者就可以确信它不会在别处使用,苏哦亦可以将其删去。
如果一个方法是公共的,就不能简单地将其山区,因为可能会有其他代码依赖这个方法。
final实例字段
将实例字段定义为final。这样的字段必须在构造对象时初始化。
必须确保每一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。
示例
private final String name;//姓名
这样对象在构造之后,就不会改变这个字段。
final修饰符对于类型为基本类型或者不可变类的字段尤其有用。(如果类中的所有方法都不会改变其对象,这样的类就是不可变的类)
对于可变的类,使用final修饰符可能会造成混乱。
例如;
private final StringBuilder evaluations;
evaluations = new StringBuilder();
final关键字的修饰只是表示存储在evaluation变量中的对象引用不会再指示另一个不同的StringBuilder对象。不过这个对象可以更改。