Java核心技术卷一 学习笔记(3)
本次的内容主要为Java中的对象和类,对于有经验的C++程序员来说,对本文不会感到陌生,但毕竟语言存在着很多不同之处,仍需对本章的内容进行较为详细的阅读。
面向对象程序设计的概述
类
类是构造对象的模板或者蓝图,可以将类想象成做曲奇饼的模具,将对象想象为曲奇饼。由类构造对象的过程则称之为创建类的实例。
在前文中由看到,用Java编写的所有的代码都是位于某个类中,标准的Java库中提供了几千个类,可用于各种目的,如用户界面设计、日历、日期和网络编程等,当然这些类不可能满足所有的需要,所以还是需要创建一些自己的类,来描述我们应用程序所对应问题的对象。
封装
封装是一个处理对象的重要概念,从形式上而言,封装就是将数据和行为组合在一个包里,并对对象的使用者隐藏具体的实现方式,对象的数据称为实例字段,操作数据的过程称之为方法,作为一个类的实例,特定对象都有一组特定的实例字段值,这些值的集合就是这个对象当前的状态。实现封装的关键在于,不能让类中的方法直接访问其他类的实例字段,程序只能通过对象的方法与对象数据进行交互。
继承
事实上,所有的Java类都源自于一个超类,就是Object,所有的其他类都扩展自这个类。在扩展一个已有的类时,这个扩展后的新类拥有被扩展的类的全部属性以及方法,你只需在新类中提供适用于这个新类的新方法和数据字段即可。通过扩展一个类来建立另外一个类的过程我们称之为继承。
对象
首先,我们要清楚对象的三个主要特性:
1.对象的行为——可以对对象完成哪些操作,应用哪些方法?
2.对象的状态——当调用那些方法时,对象的响应如何?
3.对象的标识——如何区分具有相同行为与状态的不同对象?
对象的这些关键特性会彼此相互影响。对象的行为是用可调用的方法来定义的。每个对象都保存描述当前状况的讯息,这就是对象的状态。而状态不能完全的描述一个对象,每个对象都有唯一的标识。
类之间的关系
在类之间最常见的关系有依赖(uses-a),聚合(has-a)和继承(is-a)关系。
如果一个类的方法使用或操纵另一个类的对象,我们就称之为一个类依赖于另一个类。我们在设计程序时,应尽量可能的将相互依赖的类减至最少,用软件工程的术语就是减少类之间的耦合。
聚合很容易理解,意味着类A的对象包含类B的对象。
继承则表示一个更特殊的类与一个较为一般的类间的关系,一般情况下,特殊的类会继承于较为一般的类。
使用Java中预定义的类
实际上在Java语言中,没有类你将寸步难行,我们曾在前几节提到Math类,我们可以直接使用Math类中的方法,而忽略他是如何实现的,其中Math.random方法,不必了解他的具体实现,只需知道方法名和参数即可,这便是封装的关键所在,所有的类都是这样。
对象与对象变量
使用对象前必须要构造对象并指定他的初始状态,然后对对象应用方法。
在Java语言中我们需使用构造器构造新实例。构造器实际上是一种特殊的方法,用来构造并初始化对象。我们来构造一个Java中存在的用来表示时间的类Date。如下所示。
new Date()
这个表达式构造了一个新对象,初始化为当前的日期和时间。我们可以将对象直接传递给一个方法。
System.out.println(new Date());
在上面这个例子中,我们构造的对象仅仅使用了一次,通常你希望你的对象可以多次使用,因此我们需要将其存放在一个变量中。
Date birthday = new Date();
在对象与对象变量之间有一个重要的区别。
Date deadline;
我们用以上语句定义了一个对象变量deadline,可以用来引用Date类型的对象,但是,变量deadline并不是一个对象,实际上他也没有引用任何对象,此时我们不能在这个变量上使用任何的Date方法。
s = deadline.toString()//error;
以上的语句将产生编译错误
我们要初始化对象deadline才能去使用它。
deadline = new Date();
deadline = birthday;
我们用了初始化和引用其他对象两种方法让他可以使用。
在Java语言中,任何对象的值都是对另一个地方的某个对象的引用。
很多人错误的认为Java中的对象变量相当于C++中的引用,实际上C++中并没有null引用,且引用不能赋值,我们可以把Java中的对象变量看作类似于C++中的对象指针。
Date birthday = new Date();//Java
Date* birthday = new Date();//C++
而且所有的Java对象都储存在堆中,当一个对象包含另一个对象变量时,他只是包含着另一个堆对象的指针。
类库中的LocalDate类
书中使用了LocalDate类对Java预定义类的使用进行了更详细的描述。
LocalDate newYearsEve = LocalDate.of(2020, 12 , 31);
int year = newYearsEve.getYear(); //2020
int month = newYearsEve.getMonthValue(); //12
int day = newYearsEve.getDayOfMonth(); //12
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);
year = aThousandDaysLater.getYear() //2022
我们看到上述代码的方法名较为复杂,而实际上在编译器中你只需要打个.各种方法就会自己出来任由你选择,Java丰富的预定义类和方法调用是它能够经久不衰的一大原因。
用户自定义类
我们用一个简单的Employee类来对自定义类的使用进行详细的描述。以下代码就行了这个类的构造和使用。
import java.time.*;
public class EmployeeTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
// raise everyone's salary by 5%
for (Employee e : staff)
e.raiseSalary(5);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay="
+ e.getHireDay());
}
}
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 byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
首先在这个程序中我们构造了一个Employee数组,填入了三个Employee对象
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
接下来用了Employee类中的raiseSalary方法良心的将每个人的薪水提高5%,用到了foreach循环结构。
// raise everyone's salary by 5%
for (Employee e : staff)
e.raiseSalary(5);
而后调用了getName方法,getSalary方法以及getHireDay方法打印哥哥员工的信息:
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay="
+ e.getHireDay());
我们在这个程序中包含了Employee类和public修饰的EmployeeTest类,其中EmployeeTest包含了main方法。
源文件为EmployeeTest.java,文件名必须与public类的名字相匹配,在一个源文件中只能有一个公共类。
当我们编译这段代码时会产生两个类文件:Employee.class和EmployeeTest.class,可以将程序中baohanmain方法的类名提供给字节码解释器来启动程序。
即java EmployeeTest
分析Employee类
这个类中包含了一个构造器以及四个方法:
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,我们上文中说到一个java文件中只能有一个公共类,要与这里的方法区分开。关键字public意味着任何类的任何方法都可以调用这些方法。
接下来看类中如何存放要操作的数据:
private String name;
private double salary;
private LocalDate hireDay;
关键字private和C++中一样,确保只有类自身的方法能访问这些实例字段。
构造器分析
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
构造器与类同名,构造器括号中包括了要传入的参数,传入相应参数后在构造器中将会给相应的变量赋值。
有几个关键点需要注意:构造器与类同名;每个类可以有一个以上的构造器;构造器可以有任意个参数;构造器无返回值;构造器伴随new一起调用。
Java构造器的工作方式与C++一样,但是所有的Java对象都是在堆中构造的。
注意不要在构造器中定义与实例字段相同名字的局部变量
例如:
public Employee(String n, double s)
{
String name = n;
double salary = s;
}
在这个构造器中声明了局部变量name和salary,这些变量是只可以在构造器内部访问的,会屏蔽掉所有的同名实例字段。
用var声明局部变量
我们可以从变量的初始值推导出它的类型,可以用var关键字声明局部变量无须指定类型。
Employee harry = new Employee("Harry", 50000, 1989, 12, 31);
var harry = new Employee("Harry", 50000, 1989, 12, 31);
上文中的两个代码实现了同样的功能。
包
Java中可以使用包将类组织在一个集合中,借助包可以方便的组织自己的代码。
包名
使用包的主要原因是确保类名的唯一性,若两程序员都建立了Employee类,只要将他们放在不同的包中,就不会冲突。
类的导入
我们可采用两种方式访问一个包中的公共类,第一种就是完全限定名称,包后面跟着类名。
java.time.LocalDate today = java.time.LocalDate.now();
这显然很繁琐,更常用的就是使用import语句,如我们常见的
import java.time.*;
LocalDate today = LocalDate.now();
静态导入
import static java.lang.System.*;
out.println("haha");
这种方法并不常用,但是可以使程序更为简洁。