J03 Java基础 02:面向对象

Java类


类是一组对象的描述,一个对象是其类的示例。

类和对象和引用


一个简单的类可以如下定义:

1
2
3
4
public class Complex{
public double real;
public double imag;
}

这个类用实部和虚部两个属性描述了复数。

我们可以使用new 来实现一个对象:complex c = new Complex() 。由于我们没有在类中定义构造方法,程序会按照默认的构造方法,以默认值初始黄两个双精度浮点数。

在上一条语句中,c 是对象的一个引用而不是对象本身。可以通过对象的引用访问public 属性。

类的方法


接下来,我们可以给类增加一些方法。基于面向对象的思想,我们不应该直接访问类的属性,类应该提供一些接口,来帮助我们获取和设置属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Complex {
private double real; // 实部
private double imag; // 虚部

// 获取实部
public double getReal() {
return real;
}

// 获取虚部
public double getImag() {
return imag;
}

// 设置实部
public void setReal(double real) {
this.real = real;
}

// 设置虚部
public void setImag(double imag) {
this.imag = imag;
}
}

然后,我们可以增加构造方法和析构方法,以帮助我们更加方便的实现对象。对于析构方法,由于java本身的垃圾回收机制,并不是必要的,但我们还是将其实现以展示。我们使用两种具备不同参数的构造方法,即为重构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Complex(double real, double imag) {
this.real = real;
this.imag = imag;
System.out.println("Complex 对象已创建: " + this);
}

// 默认构造方法
public Complex() {
this(0, 0); // 默认初始化为 0 + 0i
}

// 析构方法(仅示意)
public void finalize() {
System.out.println("Complex 对象已销毁: " + this);
}

除此之外,也可以增加其他方法提供更多的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 加法
public Complex add(Complex other) {
return new Complex(this.real + other.real, this.imag + other.imag);
}

// 减法
public Complex subtract(Complex other) {
return new Complex(this.real - other.real, this.imag - other.imag);
}

// 乘法
public Complex multiply(Complex other) {
double newReal = this.real * other.real - this.imag * other.imag;
double newImag = this.real * other.imag + this.imag * other.real;
return new Complex(newReal, newImag);
}

// 除法
public Complex divide(Complex other) {
double denominator = other.real * other.real + other.imag * other.imag;
double newReal = (this.real * other.real + this.imag * other.imag) / denominator;
double newImag = (this.imag * other.real - this.real * other.imag) / denominator;
return new Complex(newReal, newImag);
}

// 模长
public double modulus() {
return Math.sqrt(real * real + imag * imag);
}

// 共轭
public Complex conjugate() {
return new Complex(real, -imag);
}

并重写toString方法:

1
2
3
4
5
6
7
8
9
// 重写 toString 方法
@Override
public String toString() {
if (imag >= 0) {
return real + " + " + imag + "i";
} else {
return real + " - " + (-imag) + "i";
}
}

静态方法


静态方法(或称函数)是一种不应用于特定对象的方法(因此没有 this 关键字)。我们可以将其视为应用于类本身的方法,调用时需要通过类名进行前缀,例如:ClassName.methodName()

1
2
3
4
5
6
7
8
9
10
11
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
int sum = MathUtils.add(10, 20); // 通过类名调用静态方法
System.out.println("和: " + sum);
}
}

静态方法不能被重写,且只能调用其他静态方法或访问静态变量。

枚举


Java 的枚举类型(enum)是一种特殊的数据类型,专门用于表示一组固定的常量值。它在代码中有助于增强类型安全性、可读性以及简化逻辑处理。

1
2
3
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
  • 枚举常量是 public static final 的。
  • 默认情况下,枚举常量的值是从 0 开始递增的索引。
  • 枚举是一个类,可以包含属性、方法和构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class EnumExample {
public static void main(String[] args) {
Day today = Day.MONDAY;

// 使用枚举与 switch
switch (today) {
case MONDAY:
System.out.println("It's Monday!");
break;
case FRIDAY:
System.out.println("It's Friday!");
break;
default:
System.out.println("It's another day!");
}

// 遍历枚举
for (Day day : Day.values()) {
System.out.println(day);
}
}
}

父类子类


继承


我们来想象一个情况。在java中,我们定义圆形,矩形两种图形。它们可以在一个平面上正确的绘制。

但是这是,我们的用户告诉我们,它需要移动这些图形。如此,在当前解决方案中,有必要知道用户选择移动它的对象的确切类型(即在对象上调用正确的方法)。或者更为普遍的,这三种图形包含多种“重复”的方法,我们希望它们能做到平移,旋转等等多种操作。为每个类单独重复添加这些方法显然并不合理。在面向过程的编程语言中,我们可以定义函数,并在两个类中分别调用这些函数,但在java中我们通常不会这个干。


一个 “坏 ”的解决方案是所谓的 “联合 ”解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Figure {
private Point center; // 图形中心点
private int dim1; // 半径或宽度
private int height; // 高度
private int type; // 图形类型:1 表示圆形,2 表示矩形

// 构造方法:设置中心点和半径
public Figure(Point center, int radius) {
this.center = center;
this.dim1 = radius;
this.type = 2; // 默认类型为矩形
}

// 移动方法:根据向量移动图形
public void move(Point vector) {
center.move(vector);
}

// 计算图形周长的方法
public double perimeter() {
if (type == 1) {
// 圆形的情况
} else if (type == 2) {
// 矩形的情况
}
}
}

在某种程度上,我们通过使用相同的属性来表示多个对象的不同和排他性特征(这里,属性是圆的半径或矩形的宽度)来“欺骗”自己。

这个方法的缺点在于可拓展性。任何新类型的 figure 的添加都需要您修改此类,因此需要重新测试。

LSP和OCP

原则 目的 重点
LSP(里氏替换原则) 保证子类能够完全替代父类,且程序行为不变 子类不能破坏父类的行为契约,继承关系必须合理。
OCP(开放封闭原则) 提高代码的扩展性和稳定性,通过扩展实现功能,而非修改原有代码 面向接口编程,抽象变化点,避免直接修改已有代码。

继承是一种将两个或多个类的公共描述提取到一个唯一的类中的方式(从而避免了重复描述),这个类被称为超类(super-classe);那些继承了该描述并对其进行扩展的类称为子类(sous-classe)

另一种术语称超类为父类(classe-mère),子类为子类(classe-fille)

还有一种源自 C++ 的术语,使用基类(classe de base,super-classe)派生类(classe dérivée,sous-classe)来表示。


在这个问题中,圆形和矩形的相同特征是中心这一属性和平移这一方法。事实上,大部分图像都具备这些特征。我们将这些特征提取为一个父类。

1
2
3
4
5
6
7
8
9
10
11
public class Figure {
private Point center;

public Figure(Point center) {
this.center = center;
}

public void move(Point vector) {
center.move(vector);
}
}

假定Point已经良好的定义了。

然后让圆形和矩形继承自这一类,我们将会使用extends 关键字指示其父类。以圆形为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Circle extends Figure {
private int radius;

public Circle(Point center, int radius) {
super(center);
this.radius = radius;
}

public double perimeter() {
return 2 * Math.PI * radius;
}
}

super关键字相当于父类的this 关键字,在调用super(center);时,实际调用的是父类的构造函数。

然而,此时出现了一个问题。对于圆形来说,圆心应该是一个比较重要的内容。但由于父类中使用了private访问修饰符,子类是无法访问的。一种解决方法是

修饰符 类本身 包内访问 子类访问 全局访问
private
(默认,包级别)
protected
public

最简单的修改方式是使用protected 修饰符。


但是,还有一个潜藏的问题没有解决。

如果我们的代码是return super.center,那将会返回一个指向center的引用,这是可修改的。


所有的Java类都直接或者间接的继承自java.lang.Object 类。这意味着它们会包含该类的方法:

  • toString():返回对象的字符串表示。
  • equals(Object obj):判断两个对象是否“相等”。
  • hashCode():返回对象的哈希值。
  • clone():创建对象的副本(需要实现 Cloneable 接口)。
  • getClass():返回对象的运行时类。

关于equals(Object obj) 方法,通常== 比较的是两个变量的引用是否指向同一个对象(即内存地址是否相同),equals() 用于比较两个对象是否在意义上“相等”,通常比较对象的内容。

但是,Object 类中默认的 equals() 方法基于 ==,即比较的是对象的内存地址。所以通常需要重写。


为什么要提到这些方法呢,因为clone方法可以解决之前描述的问题。将代码修改为return super.center.clone 就可以实现返回副本。但是,这与引发了另一个问题,类需要实现了 Cloneable 接口,才能使用clone方法。由于接口部分在下文中,这里直接给出解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Point implements Cloneable {
private int x;
private int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

public Point clone() throws CloneNotSupportedException {
return (Point) super.clone();
}
}

多态


回忆我们提到的另一个问题:“有必要知道用户选择移动它的对象的确切类型”。考虑以下测试程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {

public static void test(User user) {
Circle c = new Circle(new Point(100, 100), 10);
c.move(new Point(10, 10));
double d = c.perimeter();

Figure f = c;
f.move(new Point(-10, -10));
d = f.perimeter();

f = user.chooseFigure();
f.move(new Point(30, -20));
d = f.perimeter();
}
}

chooseFigure() 方法假设会根据用户的选择返回一个 Circle 或一个 Rectangle 的实例。由于 Java 是静态类型语言(在编译时进行类型检查,包括方法的调用检查),编译器会拒绝调用在接收对象的类中未声明的方法(例如这里的 perimeter() 方法)。其他动态类型语言(如 Python)则会在运行时检查该方法是否存在,只有在运行时确认不存在时才会报错。

换句话说,对于Java来说,必须保证无论创建的对象是哪种类型,perimeter() 方法都是可用的。我们需要在所有 Figure 类型的对象中声明一个 perimeter() 方法,即使在 Figure 类中无法直接实现该方法的具体功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Figure {
protected Point center;

public Figure(Point center) {
this.center = center;
}

public void move(Point vector) {
center.move(vector);
}

// 所有图形都有一个周长
public double perimeter() {
// 但我们在这里无法计算
return -1;
}
}

另外,在Java运行时执行的方法是对象所属实际类(CircleRectangle)中的方法,而不是编译时变量(Figure)中定义的方法。这种机制称为动态绑定 liaison dynamique(基于运行时类型的选择),与静态绑定 liaison statique(基于编译时类型的选择)相对。


这种性质被称为多态性(Polymorphism),该术语来源于古希腊语中“πολλοί (polloí)”(意为“多个”)和“μορφος (morphos)”(意为“形式”),因为同一个形式(即对一个对象调用同一个方法)可以对应多种行为。


抽象


抽象类


更加严谨的,我们应该阻止用户创建FigureFigure 是一个概念性的对象,仅具有一些基本特性,但并未完整地描述一个具体的真实对象。

这可以通过 Java 中的 abstract 关键字来实现。如果尝试创建一个 Figure 的实例(例如通过 new Figure()),编译器将检测到这一错误并报错。因此,我们将 Figure 称为抽象类 classe abstraite。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {

public static void test(User user) {
Circle c = new Circle(new Point(100, 100), 10);
c.move(new Point(10, 10));
double d = c.perimeter();

Figure f = c;
f.move(new Point(-10, -10));
d = f.perimeter();

f = user.chooseFigure();
f.move(new Point(30, -20));
d = f.perimeter();
}
}

在抽象类中,我们删除了之前为perimeter() 方法提供的代码,并也添加了abstract 关键字,这被称为抽象方法,味着不需要在该类中为此方法提供具体实现;然而,任何非抽象的子类必须定义自己的版本。

Downcast向下转型


我们之前已经看到,由于 CircleFigure 的子类,可以通过 Figure 类型的引用变量来引用一个 Circle 的实例(例如 Figure f = c;)。这是可能的,因为继承的语义表明,子类的每个实例间接也是其超类的实例。

然而,反向的转换(即从 Figure 转换回 Circle)并不直接可行,因为一个 Figure 的实例并不一定是一个 Circle 的实例;但它可能是。

如果要使用这种转换,通常使用instanceof 和显示转换的组合实现。

1
2
3
if (f instanceof Circle) {
c = (Circle) f;
}

接口


接口用于定义一组行为规范(即方法的声明),由实现类进行具体实现。接口类似于一种特殊的抽象类,但只包含方法签名(无属性),并定义了类必须实现的方法。早期版本接口中只能包含抽象方法。自 Java 8 起,接口可以包含默认方法(default methods)和静态方法,但接口中的方法默认是 public

接口可以解决多继承问题,一个类可以实现多个接口,从而弥补了多继承的不足。接口之间可以继承,即一个接口可以继承另一个接口的定义。

1
2
3
4
5
6
7
8
9
package java.awt;

public interface Shape {
boolean contains(double x, double y);
boolean contains(Point2D p);
Rectangle getBounds();
boolean intersects(Rectangle2D r);
}

  • 该接口 Shape 定义了与几何形状相关的方法。
  • 实现该接口的类(如 RectangleCircle)必须提供这些方法的具体实现。

Java 中使用 implements 关键字表示一个类实现一个或多个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package java.awt;

public class Rectangle extends Rectangle2D implements Shape, Serializable {

public boolean contains(double x, double y) {
// ...
}

public Rectangle getBounds() {
// ...
}
// ...
}