目录
- 类与对象
- 一、面向对象与面向过程
- 二、类和对象的概念
- 三、类与类之间的关系
类与对象
一、面向对象与面向过程
面向对象(Object-Oriented Programming, OOP)与面向过程(Procedure-Oriented Programming, POP)是两种不同的编程范式,它们在编程思想、特点、优势以及应用场景等方面存在显著差异。
1、编程思想
- 面向过程:是一种以过程为中心的编程思想,主要关注解决问题的步骤,即先分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用。
- 面向对象:是一类以对象作为基本程序结构单位的程序设计语言,其设计是以对象为核心,而对象是程序运行时刻的基本成分。面向对象把构成问题事务分解成各个对象,通过不同对象之间的调用和相互协作来解决问题。
2、特点
-
面向过程
:
- 适合解决简单的问题,不需要过多的协作和抽象。
- 关注问题的解决步骤而不是问题的本质。
- 代码复用性低,扩展性差,不易维护。
- 只有封装,没有继承和多态。
-
面向对象
:
- 适合解决复杂的问题,需要多方的协作和抽象。
- 关注问题的本质。
- 代码复用性高,扩展性好,易于维护。
- 具有封装、继承、多态三大特性。
3、优势
-
面向过程
:
- 性能较好,因为类调用时需要实例化,开销较大,比较消耗资源。
- 在某些特定领域(如单片机、嵌入式开发、Linux/Unix系统编程等)中,性能是最重要的因素,因此面向过程更为适用。
-
面向对象
:
- 易维护:由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
- 易复用:通过继承和多态,程序员可以实现代码的复用和扩展,提高开发效率。
- 易扩展:面向对象的设计使得系统能够更容易地适应变化,增加新的功能或修改现有功能时,对系统其他部分的影响较小。
4、应用场景
- 面向过程:更适合于解决一些简单、直接的问题,或者在一些对性能要求极高的场景中使用。
- 面向对象:在大型软件系统、图形用户界面(GUI)应用程序、游戏开发等复杂场景中表现出色。通过类和对象的层次结构,程序员可以将复杂的系统划分为多个独立的模块,提高代码的可维护性和可扩展性。
综上所述,面向对象与面向过程各有其优势和适用场景。在选择编程范式时,应根据具体问题的复杂程度、性能要求以及开发团队的熟悉程度等因素进行综合考虑。
二、类和对象的概念
1.类的概念
类(Class)是面向对象编程(OOP)中用于定义对象的蓝图或模板。它描述了一组具有相同属性和行为的对象的抽象表示。类定义了对象的状态(通过属性或成员变量)和行为(通过方法)。在编程语言中,类是一种数据类型,但它是一种复合类型,因为它可以包含多个属性和方法。
2.对象的概念
对象(Object)是类的实例。当你使用类来创建实体时,就创建了对象。每个对象都拥有类中定义的属性和方法,但它们的属性值可以是不同的,这取决于创建对象时指定的值。对象代表了现实世界中的实体或概念,如人、车、房子等。
3.成员变量
成员变量(也称为属性)是类中定义的变量,用于存储对象的状态信息。成员变量可以是任何合法的数据类型,包括基本数据类型(如int、float)和引用数据类型(如数组、其他类的对象)。成员变量可以是私有的(private)、受保护的(protected)或公共的(public),这决定了它们的访问范围。
4.成员方法
成员方法(也称为函数或过程)是类中定义的代码块,用于执行操作或返回结果。方法定义了对象可以执行的行为。与成员变量类似,方法也可以有访问修饰符(private、protected、public),控制其访问范围。方法可以接受参数,也可以有返回值。
5.对象的实例化
对象的实例化是指使用类来创建对象的过程。在Java中,这通常使用new
关键字完成,后面跟上类的名称和构造器(如果有必要,还包括传递给构造器的参数)。
在Java中,对象的实例化(也称为创建对象)是面向对象编程中的一个核心概念,它指的是根据类的定义来创建类的实例(即对象)的过程。对象的实例化方式主要有以下几种:
1. 使用new
关键字
这是最常见的实例化对象的方式。通过new
关键字调用类的构造器(构造函数)来创建对象。如果类中没有显式定义构造器,编译器会自动生成一个默认的无参构造器。例如:
java
MyClass obj = new MyClass();
有参构造函数(也称带参数的构造函数)是在实例化对象时使用的。构造函数的主要职责是在创建对象时初始化该对象的状态。当你使用有参构造函数时,你需要在创建对象的同时提供必要的参数,这些参数会被传递给构造函数并用于初始化对象的状态。
以下是一个简单的示例,展示了如何使用有参构造函数在 Java 中实例化对象:
java
public class Person {
private String name;
private int age;
// 有参构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Main {
public static void main(String[] args) {
// 使用有参构造函数实例化 Person 对象
Person person = new Person("Alice", 30);
// 输出 Alice
System.out.println(person.getName());
// 输出 30
System.out.println(person.getAge());
}
}
在这个例子中,Person
类有两个私有属性 name
和 age
,并且有一个接受这两个属性作为参数的构造函数。当我们创建 Person
类的一个实例时,我们传递了 "Alice"
和 30
这两个值给构造函数,这些值被用来初始化 name
和 age
属性。
使用有参构造函数的好处包括:
- 初始化对象状态:可以在创建对象的同时设置其初始状态。
- 代码清晰:明确指出了对象创建时所需的初始信息。
- 强制参数传递:如果没有提供所需的参数,编译器将不允许创建对象,从而减少了错误的可能性。
总结来说,有参构造函数是在创建对象时使用的,用于初始化对象的状态,确保对象在使用之前已经被正确地设置了初始值。
2. 工厂模式
工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们创建了一个工厂类,用于封装实例化对象的逻辑。客户端通过调用工厂类的方法来获取对象实例,而无需直接通过new
关键字来创建对象。工厂模式实现了创建者和调用者的分离,有助于系统的解耦。例如:
java
public class MyClassFactory {
public static MyClass getInstance() {
return new MyClass();
}
}
MyClass obj = MyClassFactory.getInstance();
3. 反射
Java反射机制允许程序在运行时动态地创建对象、访问对象的属性和方法。通过java.lang.Class
或java.lang.reflect.Constructor
类的newInstance()
方法,可以在运行时创建对象的实例。这种方式比较灵活,但效率较低,且容易破坏封装性。例如:
java
Class<?> clazz = Class.forName("com.example.MyClass");
MyClass obj = (MyClass) clazz.newInstance();
// 或者使用Constructor
Constructor<MyClass> constructor = MyClass.class.getConstructor();
MyClass obj2 = constructor.newInstance();
4. clone()
方法
如果类实现了Cloneable
接口并重写了clone()
方法,那么可以通过调用已存在对象的clone()
方法来创建新的对象实例。这种方式是通过复制已存在对象的属性值来创建新对象的,但需要注意的是,这种方式创建的是浅拷贝,如果对象中包含引用类型的属性,则这些引用类型属性仍然指向原对象中的属性值。例如:
java
public class MyCloneableClass implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
MyCloneableClass original = new MyCloneableClass();
MyCloneableClass copy = (MyCloneableClass) original.clone();
注:克隆
(1)什么要使用克隆?
想对一个对象进行复制,又想保留原有的对象进行接下来的操作,这个时候就需要克隆了。
(2)如何实现对象克隆?
实现Cloneable接口,重写clone方法;
实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆。 BeanUtils, apache和Spring都提供了bean工具,只是这都是浅克隆。
(3)深拷贝和浅拷贝区别是什么?
浅拷贝:仅仅克隆基本类型变量,不克隆引用类型变量;
深克隆:既克隆基本类型变量,又克隆引用类型变量;
5. 序列化和反序列化
Java的序列化机制允许将对象的状态保存到字节流中,并可以从字节流中恢复对象的状态。通过序列化将对象保存到文件或通过网络传输,然后在需要的时候通过反序列化从文件或网络中恢复对象。这种方式可以用于对象的深拷贝和对象的持久化存储。例如:
java
// 序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.ser"));
out.writeObject(obj);
out.close();
// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));
MyClass objCopy = (MyClass) in.readObject();
需要注意的是,要序列化的类必须实现Serializable
接口,并且如果类中包含其他需要序列化的对象作为属性,那么这些对象也必须实现Serializable
接口。
综上所述,Java中对象的实例化方式多种多样,每种方式都有其适用的场景和优缺点。在实际开发中,应根据具体需求选择合适的方式。
6.Java内存模型与对象创建
1.Java内存分为栈内存和堆内存,栈内存存放基本数据类型和引用类型变量的引用,堆内存存放对象实例。
2.对象创建时,实例化对象放在Java堆中,包含的成员变量和静态变量根据其类型存放在不同的内存区域。
在Java中,对象的创建和内存分配涉及到几个关键的内存区域。以下是对象创建时,成员变量和静态变量存放的位置:
-
Java堆(Heap):
- Java堆是Java虚拟机(JVM)中用于存储对象实例的区域。当通过
new
关键字创建一个对象时,对象的实例(包括成员变量和方法)会被分配在堆内存中。 - 堆内存是所有线程共享的区域,它由垃圾回收器(Garbage Collector, GC)管理,负责对象的生命周期,包括创建和销毁。
- Java堆是Java虚拟机(JVM)中用于存储对象实例的区域。当通过
-
成员变量:
- 非静态成员变量(实例变量)是与特定对象实例关联的,它们存储在Java堆中,与对象实例一起。
- 每个对象实例都有自己的成员变量副本,这些副本在堆内存中分配。
-
静态变量:
- 静态变量(类变量)是与类关联的,而不是与对象实例关联的。它们存储在方法区(Method Area)或Jdk8及以后版本中的元空间(Metaspace)。
- 方法区是JVM的一块内存区域,用于存储类的结构,如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容等。静态变量在类被加载时创建,并且在整个Java虚拟机的生命周期内只创建一次。
- 从Jdk 8开始,方法区被元空间所取代。元空间不再是虚拟机内存的一部分,而是使用本地内存(即操作系统的内存)来存储类的元数据。
-
方法区/元空间:
- 方法区或元空间是JVM规范中定义的一块内存区域,用于存储类的信息。在不同的JVM实现中,这块内存区域的具体实现可能有所不同。
- 元空间是Jdk 8引入的一个概念,它将类的元数据从JVM内存中移出,存储到本地内存中,这样做可以减少JVM的内存消耗,并提高性能。
-
Java栈:
- 当一个线程执行一个方法时,会创建一个栈帧(Stack Frame),用于存储局部变量和操作数栈。局部变量包括方法中的参数和局部变量,它们是线程私有的,每个线程都有自己的栈。
-
程序计数器:
- 程序计数器是一块小的内存区域,用于存储当前线程执行的字节码的行号指示器。它是线程私有的,每个线程都有一个独立的程序计数器。
总结来说,对象的非静态成员变量存储在Java堆中,而静态变量存储在方法区或元空间中。这些内存区域的设计有助于JVM有效地管理内存和垃圾回收。
3.引用类型变量存储的是对象在堆中的地址,通过这个地址可以访问到对象的内容。
在Java中,引用类型变量(如类的实例、数组等)本身并不直接存储对象的数据;相反,它们存储的是指向实际数据所在位置的引用(或指针)。下面是关于Java中引用类型变量及其存储位置的一些详细信息:
-
堆(Heap):
- 对象的实际数据(即对象的状态,包括所有成员变量)存储在堆中。堆是Java虚拟机(JVM)管理的一块内存区域,主要用于动态分配内存。
- 当你使用
new
关键字创建一个对象时,实际的对象数据就会被分配在堆上。
-
栈(Stack):
- 引用类型变量本身是存储在栈上的。栈是用于存储局部变量的地方,它遵循后进先出(LIFO)原则。
- 当你在方法中声明一个引用类型变量时,这个变量的引用(而不是对象本身)会被存储在栈中。
举个例子来说明这个过程:
java
public class MyClass {
private int x;
private String y;
public MyClass(int x, String y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public String getY() {
return y;
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass(10, "Hello World");
System.out.println(obj.getX());
System.out.println(obj.getY());
}
}
在这个例子中,MyClass obj
是一个存储在栈中的引用变量。当你执行 new MyClass(10, "Hello World")
时,一个 MyClass
类型的对象会在堆上创建,并且 obj
变量将会指向堆上对象的地址。这样,当通过 obj
访问对象的方法或属性时,实际上是在访问堆上的那个对象。
总结来说,在Java中,引用类型变量本身存储在栈中,而它们所引用的对象则存储在堆中。这种分离的设计有助于提高程序的性能和内存管理的效率。
4.基本数据类型直接存放在栈内存中,不需要堆内存。
7.构造器
构造器是一种特殊的方法,用于在创建对象时初始化对象的状态。构造器没有返回类型(包括void
),并且其名称必须与类名完全相同。
默认构造器:如果类中没有明确定义任何构造器,则编译器会自动提供一个无参的默认构造器。但是,如果类中定义了至少一个构造器(无论是有参还是无参),编译器就不会再提供默认构造器。
默认构造器的特点:
- 自动生成:如果类中没有定义任何构造器,编译器会自动生成一个默认构造器。
- 无参数:默认构造器不接受任何参数。
- 访问权限 :在Java 9之前,自动生成的默认构造器的访问权限是包私有的(即默认访问权限,不加任何访问修饰符)。但从Java 9开始,如果类或其父类(如果有的话)在模块中声明,并且该类没有显式声明构造器,那么编译器将生成一个
protected
访问权限的构造器,而不是包私有的。这个改变是为了增强模块封装性。 - 一旦定义其他构造器,默认构造器不再自动生成:如果类中定义了至少一个构造器(无论是否有参数),那么编译器就不会再自动生成默认构造器了。
有参构造器:可以接受参数,用于在创建对象时初始化对象的属性。
构造器重载:允许类中定义多个构造器,只要它们的参数列表不同即可。这样可以根据需要选择适当的构造器来创建对象。
8.对象的使用
主要涉及调用其成员方法和访问其成员变量(如果它们是公开的)。对象通过点操作符(.
)来访问其成员。
在Java中创建和使用对象的基本过程如下:
创建对象
-
声明变量 :
首先,你需要声明一个变量,这个变量将用来存储对象的引用。变量的类型应该是你想要创建的对象所属的类的类型。
javaClassName objectReference; // 假设ClassName是你想要创建的对象所属的类名
-
实例化对象 :
使用
new
关键字来创建类的一个新实例,并通过类的构造函数来初始化这个新对象。构造函数会被自动调用以初始化对象的状态。javaobjectReference = new ClassName(parameters); // 根据构造函数的要求提供参数
使用对象
一旦创建了对象,就可以通过对象引用调用其方法或访问其属性。
-
访问成员变量 :
使用点运算符(
.
)来访问对象的成员变量。javaobjectReference.memberVariable; // 假设memberVariable是类中的一个成员变量
-
调用方法 :
同样地,使用点运算符来调用对象的方法。
javaobjectReference.methodName(parameters); // 假设methodName是类中的一个方法
下面是一个完整的示例:
java
public class Student {
private String name;
private int age;
// 无参构造函数
public Student() {
}
// 有参构造函数
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// 方法
public void study() {
System.out.println(name + " is studying.");
}
// getter 和 setter 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class Main {
public static void main(String[] args) {
// 使用无参构造函数创建对象
Student student1 = new Student();
student1.setName("李四");
student1.setAge(22);
// 使用有参构造函数创建对象
Student student2 = new Student("张三", 20);
// 调用方法
student1.study(); // 输出 李四 is studying.
student2.study(); // 输出 张三是 studying.
// 访问成员变量
System.out.println("学生的名字:" + student1.getName()); // 输出 学生的名字:李四
System.out.println("学生的年龄:" + student2.getAge()); // 输出 学生的年龄:20
}
}
在这个例子中,Student
类有两个构造函数:一个是无参构造函数,另一个是有参构造函数。Main
类中的 main
方法展示了如何使用这两个构造函数来创建 Student
类的对象,并调用对象的方法以及访问其成员变量。
9.对象的销毁
在Java中,对象的销毁主要依赖于Java虚拟机(JVM)内置的垃圾回收机制(Garbage Collection, GC)。垃圾回收器负责自动检测并回收那些不再被程序使用的对象所占用的内存空间。下面详细介绍Java对象的销毁机制及相关实践:
垃圾回收机制
Java中的对象销毁主要是通过垃圾回收机制完成的。当一个对象不再有任何引用指向它时,这个对象就成为了垃圾回收的目标。垃圾回收器会在适当的时机自动回收这些对象,释放其占用的内存空间 。这意味着,通常情况下,开发者无需关心对象何时销毁的问题,因为垃圾回收机制会自动管理对象的生命周期。
手动辅助垃圾回收
尽管垃圾回收机制是自动的,但在某些情况下,开发者也可以采取一些措施来辅助垃圾回收的过程。例如,当一个大的对象不再使用时,可以显式地将其引用设置为null
,这有助于垃圾回收器更快地识别出该对象可以被回收 。需要注意的是,调用System.gc()
方法来请求垃圾回收并不是一个好的做法,因为这会导致不确定的性能影响,并且现代JVM通常能够更好地管理垃圾回收时机 。
finalize() 方法
finalize()
方法是java.lang.Object
类中的一个受保护方法,它允许在对象被垃圾回收之前执行一些清理工作。然而,使用finalize()
方法并不被推荐,因为它的行为是不确定的,并且不能保证一定会被执行。此外,多次调用finalize()
方法也不会有多次机会执行 。现代的最佳实践建议尽量避免使用finalize()
方法,转而使用其他资源管理技术,如try-with-resources
语句来确保资源正确关闭 。
资源管理
对于需要显式关闭的外部资源(如文件、数据库连接等),推荐使用AutoCloseable
接口配合try-with-resources
语句来自动管理这些资源。这种方式可以确保即使在发生异常的情况下,资源也能被正确关闭 。
总结
Java对象的销毁主要依靠垃圾回收机制来自动管理。开发者应当关注的是合理组织代码结构,及时释放不再使用的资源,并尽量采用现代的语言特性(如try-with-resources
)来帮助管理资源。同时,避免使用finalize()
方法来执行清理工作,因为这种方法的效果不可靠且难以预测。通过这些实践,可以有效地管理内存使用,提高程序的健壮性和性能。
GC垃圾回收器
垃圾回收器(Garbage Collector, GC)是Java等语言中的一种内存管理机制,用于自动检测并回收那些不再被使用的对象所占用的内存空间。GC的具体实现和触发时机取决于JVM(Java虚拟机)的实现和配置,但程序员通常不需要也不应该直接干预GC的工作。
GC(Garbage Collection)垃圾回收器是Java等语言自动管理程序内存的机制,负责回收那些不再被程序所使用的内存对象,以释放内存空间供程序继续使用。以下是对GC垃圾回收器的详细解析:
一、GC的基本原理
- 目标:将内存中不再被使用的对象进行回收,释放其占用的内存空间。
- 收集器:GC中用于回收的方法称为收集器。
- 优化目标:由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、老年代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。
二、GC的算法
GC通过不同的算法来检测垃圾对象,并回收其占用的内存空间。常用的算法包括:
-
引用计数算法:
- 原理:为每个对象维护一个引用计数,当有对象引用该对象时,计数加1;当引用失效时,计数减1。当引用计数为0时,表示该对象不再被使用,可以回收。
- 优点:实现简单,执行效率高。
- 缺点:无法解决循环引用的问题。
-
可达性分析算法:
- 原理:从一系列称为GC Roots的根对象出发,通过引用链搜索所有可达的对象,其余未被标记的对象即为垃圾对象。
- GC Roots:包括虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象等。
- 优点:能够准确识别出垃圾对象,是主流虚拟机采用的算法。
-
标记-清除算法:
- 过程:首先标记出所有需要回收的对象,然后统一清除这些对象。
- 缺点:效率不高,且会产生内存碎片。
-
复制算法:
- 原理:将内存分为大小相等的两块,每次只使用其中一块。当这块内存用完时,将存活的对象复制到另一块内存中,然后清除原内存中的所有对象。
- 优点:解决了内存碎片问题,且实现简单。
- 缺点:内存利用率低,只有原来的一半。
-
标记-整理算法:
- 原理:在标记出所有需要回收的对象后,让所有存活的对象都向内存的一端移动,然后直接清除掉端边界以外的内存。
- 优点:解决了内存碎片问题,且内存利用率较高。
- 缺点:成本较高。
-
分代算法:
- 原理:根据对象存活周期的不同将内存划分为几块(如新生代和老年代),新生代中对象存活周期短,使用复制算法;老年代中对象存活周期长,使用标记-整理算法。
三、GC的类型
-
Minor GC(新生代GC):
- 触发条件:新生代(包括Eden区和Survivor区)无法为新生对象分配内存空间时触发。
- 特点:发生频率高,但回收速度快。
-
Major GC(老年代GC):
- 清理对象:主要清理老年代中的对象。
- 特点:发生频率相对较低,但回收对象较多,可能需要较长时间。
-
Full GC(全局GC):
- 清理范围:清理整个堆空间,包括年轻代、老年代和元空间(Java 8及以上版本)。
- 特点:影响范围最广,可能导致应用暂停时间较长。
四、GC垃圾回收器的类型
Java提供了多种垃圾回收器,以适应不同的应用场景和性能需求。常见的垃圾回收器包括:
-
串行垃圾回收器(Serial Garbage Collector):
- 特点:单线程执行垃圾回收,适用于小型应用或单核CPU环境。
-
并行垃圾回收器(Parallel Garbage Collector):
- 特点:多线程执行垃圾回收,提高了垃圾回收的效率,是JVM的默认垃圾回收器。
-
并发标记扫描垃圾回收器(CMS Garbage Collector):
- 特点:以最小化停顿时间为目标,适用于对停顿时间要求较高的应用。
-
G1垃圾回收器(G1 Garbage Collector):
- 特点:面向服务端应用,将堆内存划分为多个区域,并发进行垃圾回收,同时兼顾停顿时间和吞吐量。
五、总结
GC垃圾回收器是Java等语言自动管理内存的重要机制,通过不同的算法和回收器来实现对垃圾对象的回收和内存空间的释放。在实际应用中,需要根据应用的特点和需求选择合适的垃圾回收
10.匿名对象
匿名对象是在创建对象时直接使用的对象,而不是先将其赋值给一个引用变量。匿名对象通常只使用一次,并且主要用于调用该对象的方法。由于匿名对象没有引用变量,因此它们的使用范围非常有限,并且一旦执行完调用的方法,该对象就会被垃圾回收器回收。
三、类与类之间的关系
类与类之间的六种关系
一、继承关系
继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。在Java中继承关系通过关键字extends明确标识,在设计时一般没有争议性。在UML类图设计中,继承用一条带空心三角箭头的实线表示,从子类指向父类,或者子接口指向父接口。
二、实现关系
实现指的是一个class类实现interface接口(可以是多个)的功能,实现是类与接口之间最常见的关系。在Java中此类关系通过关键字implements明确标识,在设计时一般没有争议性。在UML类图设计中,实现用一条带空心三角箭头的虚线表示,从类指向实现的接口。
三、依赖关系
简单的理解,依赖就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是类B的变化会影响到类A。比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖。表现在代码层面,为类B作为参数被类A在某个method方法中使用。在UML类图设计中,依赖关系用由类A指向类B的带箭头虚线表示。
四、关联关系
关联体现的是两个类之间语义级别的一种 强依赖关系,比如我和我的朋友,这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。关联可以是单向、双向的。表现在代码层面,为被关联类B以类的属性形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量。在UML类图设计中,关联关系用由关联类A指向被关联类B的带箭头实线表示。
五、聚合关系
聚合是关联关系的一种特例,它体现的是整体与部分的关系,即 has-a 的关系。此时整体与部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。比如计算机与CPU、公司与员工的关系等。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,聚合关系以空心菱形加实线表示。(菱形指向整体)
六、组合关系
组合也是关联关系的一种特例,这种关系比聚合更强,也称为强聚合。它同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束,比如人和人的大脑。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,组合关系以实心菱形加实线表示。
总结
对于继承、实现这两种关系,它们体现的是一种类和类、或者类与接口间的纵向关系。其他的四种关系体现的是类和类、或者类与接口间的引用、横向关系。关联关系体现的是强依赖,而聚合和组合都是关联关系的一种特例,其二者之间区别在于整体与部分之间的强度。
高内聚、低耦合是软件设计中的核心原则,旨在提升软件系统的质量、可维护性、可扩展性和灵活性。以下是对这两个原则的详细解释:
最高设计原则
一、高内聚
定义:
高内聚指的是模块或组件内部的元素之间具有强烈的相关性,即模块内部的功能和数据紧密相关,形成一个高度独立的整体。高内聚的设计有助于提高代码的可读性、可维护性和复用性。
特点与优势:
- 功能相关性:将相同或相似功能的代码放在同一个模块中,使得代码的组织更加清晰和易于理解。
- 单一职责:每个模块或组件只负责一项功能,避免一个模块承担过多的责任,从而提高模块的可测试性和可维护性。
- 数据局部化:将模块内部的数据隐藏起来,只暴露必要的接口给外部使用,减少模块之间的依赖,提高模块的独立性。
二、低耦合
定义:
低耦合指的是模块或组件之间的联系较少,相互之间的依赖性较弱。低耦合的设计有助于降低系统各组件之间的紧密程度,提高系统的灵活性和可扩展性。
特点与优势:
- 接口独立性:模块之间的接口应该独立于实现,只暴露必要的接口给外部使用,减少模块之间的依赖。
- 松散耦合:模块之间的关系应该是松散的,一个模块的变化不应该对其他模块造成太大的影响。
- 依赖倒置:模块之间的依赖关系应该通过抽象层来实现,高层模块不依赖于低层模块的具体实现,而是通过接口来互相通信。
应用与影响
高内聚、低耦合的设计原则在软件开发中有着广泛的应用,其影响主要体现在以下几个方面:
- 提高代码质量:通过高内聚的设计,代码的组织更加清晰,易于理解和维护;通过低耦合的设计,降低了模块之间的依赖关系,减少了修改一个模块对其他模块的影响。
- 提升系统灵活性:低耦合的设计使得系统各组件之间的连接更加松散,便于在不影响其他组件的情况下对单个组件进行修改或替换。
- 增强系统可扩展性:高内聚、低耦合的设计原则使得系统能够更容易地添加新功能或扩展现有功能,而不必对整体架构进行大规模改动。
- 促进团队协作:高内聚的设计使得每个模块或组件的职责更加明确,有助于团队成员之间的分工与合作;低耦合的设计则减少了模块之间的依赖关系,降低了团队成员之间的沟通成本。
综上所述,高内聚、低耦合是软件设计中的最高设计原则之一,它对于提升软件系统的质量、可维护性、可扩展性和灵活性具有重要意义。在软件开发过程中,开发人员应该始终坚持这一原则,以构建出更加优秀、可靠的软件系统。