Java学习笔记:面向对象
1. 面向对象的基本思想
算法 + 数据结构 = 程序
数据结构,是对数据的描述。在程序中要指定用到哪些数据,以及这些数据的类型和数据的组织形式。
算法,是对操作的描述。即要求计算机进行操作的步骤。
- 传统的结构化程序设计将算法置于数据结构之前,首先确定如何操作数据,然后再决定如何组织数据的结构,以便操作数据。
- 而“面向对象(objected-oriented programming, OOP)”思想却调换了这个顺序,将数据放在第一位,然后再考虑操作数据的算法。
对比图:
- 对于一些规模较小的问题,将其分解为过程的开发方式比较理想。而面向对象更加适用于解决规模较大的问题。
2. 对象与类的概念
- 类是构造对象的模板或蓝图。
- 对象是类的一个实例。
- 对象变量引用对象,类似于C++中的对象指针。
- 由类构造对象的过程称为创建类的实例。
3. 类的结构
一个类的基本结构如下所示:
<modifier> class <className> {
<field>
<constructor>
<method>
}
例如,书上的一个简单的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
...
}
3.1 修饰符(modifier)
修饰符分为访问修饰符和非访问修饰符,修饰符使用的对象可以是类、字段或者方法,访问修饰符用来标识访问权限,非访问修饰符用来标识功能。
访问修饰符:
default
(即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。private
: 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)public
: 对所有类可见。使用对象:类、接口、变量、方法protected
: 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
非访问修饰符:
static
: 用来修饰类方法和类变量。final
: 用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。abstract
: 用来创建抽象类和抽象方法。synchronized
和volatile
: 主要用于线程的编程。
3.2 字段(field)
3.2.1 字段的分类
按使用修饰符的不同,字段可分为:
- 常量字段 : 声明为
static final
的字段,可视为常量(Constant)。由于常量字段与对象是否创建无关,所以在定义类时就要给定其初始值。 - 静态字段 : 声明为
static
的字段。常量字段和类变量都属于静态字段。 - 非静态字段 : 没有声明为
static
的字段。又可称为实例变量。 - 成员变量 : 除常量字段外的其他所有字段。
- 实例变量 : 没有声明为
static
的成员变量,它就是非静态字段。从技术上讲,对象将各自的状态存储在“实例变量(非静态字段)”中,它们的值对类的每个实例(每个对象)来说都是独有的。因此,本着封装的精神,将实例变量(非静态字段)声明为private
是值得提倡的。 - 类变量 : 声明为
static
的成员变量,它属于静态字段。
- 实例变量 : 没有声明为
提示:声明为 final
的成员变量,类似于常量,可作为对成员变量值的一种限定和保护。
3.2.2 字段的初始化
- 方法中的局部变量必须明确地初始化。
- 在类中,如果没有初始化字段,将会自动初始化为默认值(0、false或null)。
3.3 方法(method)
3.3.1 方法参数
3.3.1.1 隐式参数与显式参数
方法用于操作对象以及存取它们的实例字段。例如,以下方法:
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
将调用这个方法的对象的salary
实例字段设置为一个新值。
由此我们可以看出:raiseSalary
方法有两个参数:
- 第一个参数称为隐式参数,是该方法所在类的对象。
- 第二个参数称为显式参数,是位于方法名后括号中的数值。
3.3.1.2 按值调用与引用调用
- 按值调用 : 方法接收调用者提供的值。
- 引用调用 : 方法接收调用者提供的变量地址
在C++中,可以采用按值调用,也可采用引用调用。
下面是引用调用的一个例子,这是一个有效的交换函数:
void swap(int &x, int &y) {
int tmp = x;
x = y, y = tmp;
}
下面是按值调用的一个例子,这是一个无效的交换函数:
void swap(int x, int y) {
int tmp = x;
x = y, y = tmp;
}
然而,在Java中,所有的方法总是采用按值调用。也就是说,方法得到的是所有参数值的一个“副本”,方法不能修改传递给它的任何参数变量的内容。
虽然如此,但是Java与C++的另一个不同之处在于Java中的对象变量储存的是指向内存中该对象的一个指针,类似于C++中的对象指针。
下面的例子便是一个有效的交换方法:
package MyJava;
public class MyNum {
private int x;
public MyNum(int x) {
this.x = x;
}
public void setX(int x) {
this.x = x;
}
public int getX() {
return x;
}
}
package MyJava;
public class MyMain {
public static void swap(MyNum x, MyNum y) {
int tmp = x.getX();
x.setX(y.getX());
y.setX(tmp);
}
public static void main(String[] args) {
MyNum x = new MyNum(1);
MyNum y = new MyNum(2);
swap(x, y);
System.out.println(x.getX());
System.out.println(y.getX());
}
}
3.3.2 this
关键字
在每一个方法中,this
指示隐式参数,如可以重写raiseSalary
方法:
public void raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
另外,this
关键字也可以调用其他的构造器,下面将会说到。
3.3.3 访问器方法、更改器方法
- 访问器方法 : 只访问对象而不修改对象的方法有时称为访问器方法,如上面代码中的
getX()
方法。 - 更改器方法 : 可以访问对象并修改对象的方法有时称为更改器方法,如上面代码中的
setX(int)
方法。
为了实现类的封装性,我们会将字段设为私有,然后提供公共的访问器方法与更改器方法。
这样操作的好处是,对象中的字段不会轻易受到外界的破坏,我们能够很清楚的知道什么地方获取了什么字段、什么地方更改了什么字段。并且,这种操作让程序设计更加模块化,
3.3.4 静态方法
静态方法是不在对象上执行的方法,它没有隐式参数,例如,Math
类的pow
就是一个静态方法,表达式Math.pow(x, a)
。简而言之,静态方法属于类而不属于对象。
- 由于静态方法不能在对象上执行,所以它不能访问类中的任何非静态实例字段和非静态方法。
- 但是,类中的字段和方法却可以使用静态方法。
- 可以使用对象调用静态方法。
3.3.5 工厂方法
静态方法还有另外一种常见的用途。类似LocalDate
和NumberFormat
的类使用静态工厂方法来构造对象。例如 :
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // prints $0.10
System.out.println(percentFormatter.format(x)); // prints 10%
使用静态工厂方法主要有2个原因:
- 无法命名构造器。构造器的名字必须与类名相同。但是,这里希望有两个不同的名字,分别得到货币实例和百分比实例。
- 使用构造器时,无法改变所构造对象的类型。而工厂方法实际上返回
DecimalFormat
类的对象,这是NumberFormat
的一个子类。
3.3.6 main
方法
main
方法是程序启动的入口。
public class Application {
public static void main(String[] args) {
// construct objects here
...
}
}
3.3.7 重载
如果多个方法有相同的名字、不同的参数,便出现了重载。编译器必须挑选出具体调用哪个方法。它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法。如果编译器找不到匹配的参数,就会产生编译错误,这个查询匹配的过程叫做重载解析。
要完整的描述一个方法,需要指定方法名以及参数类型。这叫做方法的签名,返回值类型不是方法签名的一部分。
3.4 构造器(constructor)
构造器是一类特殊的方法,它有以下特点:
- 构造器与类同名。
- 每个类可以有一个以上构造器。
- 构造器可以有0个、1个或多个参数。
- 构造器没有返回值。
- 构造器总是伴随着
new
操作符一起调用。
使用this
关键字可以调用所在类的其他构造器。
public class Employee {
String name;
int age;
...
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public Employee(String name) {
this(name, 0);
}
}
使用super
关键字可以调用父类构造器,下面会说到。
3.5 初始化块
调用构造器的具体步骤:
- 如果构造器的第一行调用了另一个构造器,则基于所提供的参数执行第二个构造器。
- 否则,
- 首先,所有数据字段初始化为默认值(0、false或null),
- 然后,按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块。
- 执行构造器主体代码。
3.5.1 对象初始化块
只要构造这个类的对象,初始化块就会执行。
class Employee {
private static int nextId;
private int id;
private String name;
private double salary;
//object initialization block
{
id = nextId;
nextId++;
}
public Employee(String n, double s) {
name = n;
salary = s;
}
...
}
3.5.2 静态初始化块
在类第一次加载的时候,将会进行静态字段的初始化。所以,静态初始化块不能初始化非静态字段。与实例字段一样,它们的默认值是0、false或null。
所有静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行。
//static initialization block
static {
var generator = new Random();
nextId = generator.nextInt(10000);
}
4. 类的特性
4.1 类的封装性
封装是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。
- 对象中的数据称为实例字段(instance field)
- 操作数据的过程称为方法(method)
- 作为一个类的实例,特定对象都有一组特定的实例字值段。这些值的集合就是这个对象当前的状态
实现封装的关键在于方法绝对不能让类中的方法直接访问其他类的实例字段。程序只能通过方法的对象与对象数据进行交互。封装给对象赋予了“黑盒”的特征,这是提高重用性和可靠性的关键。这意味着一个类可以完全改变储存数据的方式,只要仍旧使用相同的方法操作数据,其他对象就不会知道也不用关心这个类所发生的变化。
4.2 类的继承性
继承是面向对象的另一个基本概念。它的基本思想是,可以基于已有的类创建新的类。