类与类之间的关系
类与类之间的六种关系
继承关系
定义:
继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。在Java中继承关系通过关键字extends明确标识,在设计时一般没有争议性。在UML类图设计中,继承用一条带空心三角箭头的实线表示,从子类指向父类,或者子接口指向父接口。
类图:
代码:
// 定义一个基类,名为Animal
class Animal {
// 基类的属性
protected String name;
// 基类的构造方法
public Animal(String name) {
this.name = name;
}
// 基类的方法
public void eat() {
System.out.println(name + " is eating.");
}
}
// 定义一个派生类,继承自Animal类
class Dog extends Animal { // 使用extends关键字来表示继承
private String breed; // 派生类的私有属性
// 派生类的构造方法
public Dog(String name, String breed) {
super(name); // 调用基类的构造方法
this.breed = breed;
}
// 重写基类的方法
@Override // 使用@Override注解来明确表示重写
public void eat() {
System.out.println(name + ", a " + breed + ", is eating.");
}
// 派生类特有的方法
public void bark() {
System.out.println(name + " barks!");
}
}
// 主类,包含main方法,用于执行程序
public class Main {
public static void main(String[] args) {
// 创建基类的对象
Animal animal = new Animal("Generic Animal");
animal.eat(); // 调用基类的方法
// 创建派生类的对象
Dog dog = new Dog("Buddy", "Labrador");
dog.eat(); // 调用派生类重写的方法
dog.bark(); // 调用派生类特有的方法
}
}
实现关系
定义:
实现指的是一个class类实现interface接口(可以是多个)的功能,实现是类与接口之间最常见的关系。在Java中此类关系通过关键字implements明确标识,在设计时一般没有争议性。在UML类图设计中,实现用一条带空心三角箭头的虚线表示,从类指向实现的接口。
类图:
代码:
// 定义一个接口,名为Actionable
interface Actionable {
void performAction();
}
// 定义一个类,名为Student,它实现了Actionable接口
class Student implements Actionable {
private String name;
public Student(String name) {
this.name = name;
}
// 实现Actionable接口中的performAction方法
@Override
public void performAction() {
System.out.println(name + " is performing an action.");
}
}
// 主类,包含main方法,用于执行程序
public class Main {
public static void main(String[] args) {
// 创建Student类的对象
Student student = new Student("Alice");
// 调用Student对象的performAction方法
student.performAction();
}
}
依赖关系
定义:
简单的理解,依赖就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是类B的变化会影响到类A。比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖。表现在代码层面,为类B作为参数被类A在某个method方法中使用。在UML类图设计中,依赖关系用由类A指向类B的带箭头虚线表示。
类图:
代码:
// 被依赖类:Printer
class Printer {
public void print(String content) {
System.out.println("Printing: " + content);
}
}
// 依赖类:Document
class Document {
private Printer printer; // 依赖于Printer类
// 构造方法,注入Printer对象
public Document(Printer printer) {
this.printer = printer;
}
// 使用Printer对象的方法
public void displayContent(String content) {
printer.print(content); // 依赖关系在这里体现
}
}
// 主类,包含main方法,用于执行程序
public class Main {
public static void main(String[] args) {
// 创建Printer对象
Printer printer = new Printer();
// 创建Document对象,并注入Printer对象
Document document = new Document(printer);
// 使用Document对象,间接使用Printer对象
document.displayContent("Hello, World!");
}
}
关联关系
定义:
关联体现的是两个类之间语义级别的一种强依赖关系,比如我和我的朋友,这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。关联可以是单向、双向的。表现在代码层面,为被关联类B以类的属性形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量。在UML类图设计中,关联关系用由关联类A指向被关联类B的带箭头实线表示,在关联的两端可以标注关联双方的角色和多重性标记。
类图:
代码:
// 定义一个类,名为Person,表示一个人
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// 定义另一个类,名为Car,表示一辆车
class Car {
private String model;
private Person owner; // Car类与Person类存在关联关系
public Car(String model) {
this.model = model;
}
public void setOwner(Person owner) {
this.owner = owner;
}
public Person getOwner() {
return owner;
}
public void drive() {
if (owner != null) {
System.out.println(owner.getName() + " is driving a " + model);
} else {
System.out.println("No owner is driving the " + model);
}
}
}
// 主类,包含main方法,用于执行程序
public class Main {
public static void main(String[] args) {
// 创建Person对象
Person person = new Person("Alice");
// 创建Car对象,并设置其owner属性
Car car = new Car("Toyota Camry");
car.setOwner(person);
// 调用Car对象的drive方法
car.drive();
}
}
聚合关系
定义:
聚合是关联关系的一种特例,它体现的是整体与部分的关系,即has-a的关系。此时整体与部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。比如计算机与CPU、公司与员工的关系等,比如一个航母编队包括海空母舰、驱护舰艇、舰载飞机及核动力攻击潜艇等。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,聚合关系以空心菱形加实线箭头表示。
类图:
代码:
// 定义一个类,名为University,表示一所大学
class University {
private String name;
private Department[] departments; // 聚合关系:大学包含多个系
public University(String name) {
this.name = name;
}
public void addDepartment(Department department) {
if (departments == null) {
departments = new Department[1];
departments[0] = department;
} else {
Department[] temp = new Department[departments.length + 1];
System.arraycopy(departments, 0, temp, 0, departments.length);
temp[departments.length] = department;
departments = temp;
}
}
public void showDetails() {
System.out.println("University: " + name);
if (departments != null) {
for (Department department : departments) {
System.out.println("Department: " + department.getName());
}
}
}
}
// 定义另一个类,名为Department,表示一个系
class Department {
private String name;
private Professor[] professors; // 聚合关系:系包含多个教授
public Department(String name) {
this.name = name;
}
public void addProfessor(Professor professor) {
if (professors == null) {
professors = new Professor[1];
professors[0] = professor;
} else {
Professor[] temp = new Professor[professors.length + 1];
System.arraycopy(professors, 0, temp, 0, professors.length);
temp[professors.length] = professor;
professors = temp;
}
}
public String getName() {
return name;
}
}
// 定义一个类,名为Professor,表示一位教授
class Professor {
private String name;
public Professor(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// 主类,包含main方法,用于执行程序
public class Main {
public static void main(String[] args) {
// 创建教授对象
Professor professor1 = new Professor("John Doe");
Professor professor2 = new Professor("Jane Smith");
// 创建系对象,并添加教授
Department computerScience = new Department("Computer Science");
computerScience.addProfessor(professor1);
computerScience.addProfessor(professor2);
// 创建大学对象,并添加系
University university = new University("Tech University");
university.addDepartment(computerScience);
// 显示大学及其系和教授的详细信息
university.showDetails();
}
}
组合关系
定义:
组合也是关联关系的一种特例,它体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合。它同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束,比如人和人的大脑。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,组合关系以实心菱形加实线箭头表示。
类图:
代码:
// 定义一个类,名为Brain,表示一个人的大脑
class Brain {
public void think() {
System.out.println("The brain is thinking.");
}
// 当销毁大脑时执行的方法
protected void finalize() throws Throwable {
System.out.println("The brain has been destroyed.");
// 调用父类的finalize()方法
super.finalize();
}
}
// 定义另一个类,名为Person,表示一个人
class Person {
private Brain brain; // 组合关系:一个人包含一个大脑
public Person() {
this.brain = new Brain(); // 人创建时,创建其大脑
}
// 当销毁人时执行的方法
@Override
protected void finalize() throws Throwable {
System.out.println("The person is being destroyed.");
// 销毁大脑
brain = null; // 清除大脑引用,允许垃圾回收
// 调用父类的finalize()方法
super.finalize();
}
public void work() {
brain.think(); // 人工作时,大脑在思考
}
}
// 主类,包含main方法,用于执行程序
public class Main {
public static void main(String[] args) {
Person person = new Person(); // 创建人实例
person.work(); // 人工作
// 为了演示目的,手动调用finalize方法
// 注意:在实际应用中,finalize方法的调用时机不确定,不应该依赖它来释放资源
person.finalize();
}
}
耦合度
面向对象编程中的六种关系,按照耦合度从高到低排列,通常如下:
-
继承(Inheritance):
- 耦合度高。子类与父类之间存在强关联,父类的变化可能直接影响到子类。
-
实现(Implementation):
- 耦合度中等。类实现接口时,需要实现接口的所有方法,但接口本身不包含实现。
-
聚合(Aggregation):
- 耦合度较低。一个类是另一个类的成员,但可以独立存在。整体对部分的了解较少。
-
组合(Composition):
- 耦合度较高。一个类包含另一个类的实例,部分的生命周期依赖于整体,但部分可以被替换。
-
关联(Association):
- 耦合度根据具体情况而定。一个类引用另一个类,但不一定拥有它。可以是松耦合,如通过方法参数传递。
-
依赖(Dependency):
- 耦合度低。一个类在方法级别或类级别上使用另一个类,但不持有其引用,通常是通过局部变量或方法参数。
耦合度的解释:
- 继承: 子类继承父类的所有属性和方法,耦合度最高。父类的修改可能直接影响所有子类。
- 实现: 类实现接口时,耦合度中等。接口的修改可能需要类进行相应的修改,但实现类可以独立于接口的其他实现类存在。
- 聚合: 类之间存在“拥有”关系,但整体对部分的了解较少,部分可以独立于整体存在,耦合度较低。
- 组合: 类之间存在“部分-整体”的强关联,部分的生命周期依赖于整体,耦合度较高。但部分可以被替换,提供了一定程度的灵活性。
- 关联: 两个类之间存在使用关系,但这种使用可能是临时的或通过方法参数传递,耦合度可以很低。
- 依赖: 最低耦合度。一个类可能在某个方法中使用另一个类的对象,但不持有对该对象的持久引用。
最高设计原则
"高内聚,低耦合"是软件设计中两个非常重要的原则,它们帮助创建易于维护、扩展和理解的系统。
-
高内聚(High Cohesion):
- 内聚性是指单个模块或类内部元素彼此关联的程度。高内聚意味着类或模块中的元素紧密相关,并且共同协作以完成一个单一、明确定义的任务。
- 高内聚的类或模块通常具有以下特点:
- 功能单一:每个类或模块只负责一项任务或功能。
- 易于理解:由于功能集中,理解和维护变得更加容易。
- 易于测试:可以独立地测试单个模块的功能。
-
低耦合(Low Coupling):
- 耦合度是指不同模块之间相互依赖的程度。低耦合意味着各个模块之间的依赖关系尽可能少,每个模块可以独立地更改和更新,而不影响其他模块。
- 低耦合的系统通常具有以下特点:
- 易于修改:模块间的依赖性小,修改一个模块不太可能影响到其他模块。
- 易于扩展:可以添加新功能而不影响现有代码。
- 重用性高:独立模块可以被重用在不同的项目中。
实现高内聚和低耦合的策略:
- 单一职责原则(SRP): 每个类应该只有一个引起它变化的原因,即每个类只负责一项任务。
- 开放/封闭原则(OCP): 类应该对扩展开放,对修改关闭。这意味着设计应该允许新增功能,而不是修改现有代码。
- 里氏替换原则(LSP): 子类应该能够替换其基类而不影响程序的正确性。
- 接口隔离原则(ISP): 不应该强迫客户依赖于它们不使用的方法。
- 依赖倒置原则(DIP): 高层模块不应依赖于低层模块,两者都应该依赖于抽象;抽象不应依赖于细节,细节应依赖于抽象。
- 使用设计模式: 如工厂模式、策略模式、观察者模式等,可以帮助降低模块间的耦合度。
- 依赖注入(DI): 通过外部注入依赖,而不是让模块自己创建或查找依赖,可以降低模块间的耦合度。
- 使用接口和抽象类: 定义清晰的接口,让模块间的交互基于抽象而不是具体实现。