目录
前言
在之前的学习中,我们已经探讨了面向对象编程(OOP)的核心思想,以及类和对象的基本概念。这些概念是现代软件应用程序构建的基石。然而,更进一步的理解需要我们深入研究面向对象编程的三大关键特性,即封装、继承和多态。这些这些特性不仅仅是理论概念,它们是实际编写高质量、可维护和可扩展代码的关键。在本期,我们将重点介绍封装这一重要特性。
1. 封装
1.1 什么是封装
封装(Encapsulation)是面向对象编程(OOP)的一个核心概念,它指的是将数据(成员变量/属性)和操作数据的方法(成员方法)封装在一个单元内部的能力。具体来说,封装有以下关键特点:
-
数据隐藏:封装通过将对象的内部状态(数据成员)隐藏起来,使其不能被直接访问或修改,而只能通过对象的公共接口来进行操作。这样可以有效地保护数据的完整性和安全性,防止未经授权的访问和不当修改。
-
公共接口:封装通过提供公共方法或函数,也称为访问器方法(getter)和修改器方法(setter),允许外部代码与对象进行交互。这些方法定义了对象的行为和如何访问其内部数据,同时隐藏了内部实现细节。
-
实现细节隐蔽:封装将对象的内部实现细节隐藏起来,使对象的使用者无需关心对象是如何存储和处理数据的。这有助于降低代码的复杂性,提高代码的可维护性,因为内部实现可以更改而不会影响外部代码。
-
数据封装:封装还可以通过限制对数据成员的直接访问,强制使用访问器和修改器方法来控制数据的读取和写入。这有助于在数据被访问或修改时执行额外的逻辑,例如验证数据的有效性或触发事件。
封装的主要目标是实现信息隐藏和代码隔离,使得对象能够以一种更加安全、可控和可维护的方式进行使用。它是面向对象编程的重要特性之一,有助于构建模块化、可重用和可扩展的软件系统。
例:
生活中有很多封装的概念。下面以一个简单的例子来说明:
汽车就是一个典型的封装示例。,汽车的引擎是一个对象,通过它我们可以更简易的了解封装的具体特征。
数据隐藏:汽车引擎的内部工作原理对于驾驶汽车的人来说是不可见的。我们不需要知道引擎的每个细节,例如它的每个活塞如何工作,点火系统的细节,或者燃烧室内的精确温度。这些细节都被隐藏在引擎的外部。
公共接口:汽车的引擎提供了一个公共接口,通常是通过启动按钮、油门和刹车等控制装置来访问。这些控制装置是我们与引擎进行交互的方式,而不是直接操作引擎的内部组件。
实现细节隐蔽:我们不需要了解引擎的内部实现细节,例如它是如何将燃料与空气混合并在燃烧室中点燃的。这一切都是引擎的内部工作,我们只需要知道如何使用控制装置来控制汽车。
数据封装:引擎内部可能有各种传感器来监测温度、油压和排放等数据,但这些数据不是公开的,而是通过仪表盘上的显示器来展示给驾驶者。这种封装方式确保了数据的安全性和可访问性。
总之,汽车引擎是一个生活中常见的封装示例。它能够形象的为我们演示如何隐藏复杂的内部细节,提供一个简单的公共接口供用户操作,同时确保数据的安全性和可访问性。
1.2 访问修饰限定符
在Java中,封装是通过类和访问权限这两个关键概念来实现的。类允许我们将数据以及操作数据的方法组合在一起,这种方式更符合人们对事物的自然认知。而访问权限则用于控制哪些方法或字段可以在类的外部直接使用。
Java中有四种主要的访问修饰符(Access Modifiers),用于管理类和类的成员的可见性和访问权限。这些修饰符提供了不同的访问级别,从最广泛到最受限制分别是:public、protected、default(默认,无修饰符),和 private。
-
public:
- 具有最高的访问级别,可以从任何类访问。
- 一个类、字段、方法或内部类被声明为public时,它们对所有其他类都是可见的。
- 例如,如果一个类被声明为public,则可以从任何其他类创建该类的对象。
-
protected:
- protected修饰符指定的成员对于同一包中的其他类和所有子类都是可见的。
- 在不同包中的类只能访问其他类的protected成员,如果它们是该类的子类。
- 通常用于实现继承相关的逻辑,允许子类访问父类的受保护成员。
-
default(默认无修饰符):
- 如果没有明确指定访问修饰符(即没有public、protected、private修饰符),则默认访问级别是包私有(package-private)。
- 在同一包内的其他类可以访问包私有成员,但在不同包中的类无法访问它们。
- 这个访问级别通常用于隐藏实现细节,以避免外部类直接访问。
-
private:
- 具有最低的访问级别,只能在声明它们的类内部访问。
- private修饰符通常用于隐藏类的内部实现细节,确保外部类无法直接访问这些成员。
范围 | private | 默认 | protected | public |
---|---|---|---|---|
同一类中 | ✓ | ✓ | ✓ | ✓ |
同一包中 | ✕ | ✓ | ✓ | ✓ |
不同包中子类 | ✕ | ✕ | ✓ | ✓ |
不同包中非子类 | ✕ | ✕ | ✕ | ✓ |
1.3 封装中的包
根据访问修饰限定符的不同权限我们得知,在Java中,封装不仅限于类内部的数据和方法的组织,还涉及包(Package)的概念。包也是一种封装机制。包用于组织和管理相关类和接口,将它们分组在一起,提供了更高级别的封装和组织代码的方式。
1.3.1 什么是包
在Java中,包可以类比于电脑中的文件夹或目录结构。它们的作用是为了更有效地组织和管理类,就像我们在电脑上将文件按照特定的分类放在文件夹中一样。包可以理解为一种目录,它将多个相关的类和接口收集在一起,使得我们可以更清晰地组织和访问代码。
举例来说,就像为了更好地管理电脑上的音乐,我们可以创建一个文件夹,并在其中按照艺术家、专辑或音乐类型等属性对音乐进行分类一样,包可以让我们在软件开发中更好地组织和分类代码,使代码更易于维护和管理。
另外,在同一个工程中不同的包中允许存在相同名称的类。
1.3.2 包的导入
Java 提供了大量现成的类供开发者使用,比如Date类、ArrayList类等等。这些类里提供了丰富的方法,当我们需要使用到这些方法时就需要用到 import 关键字。
在Java中,我们可以使用 import 关键字来导入其他包中的类,这使得我们可以在当前类中使用这些类。例如,要使用Date类,我们可以使用 import java.util.Date ; 导入 java.util 包中的 Date 类。这样,我们就可以在我们的代码中创建和操作Date对象,而不必从头开始编写日期处理的功能,虽然导入操作本身并不是封装,但它对代码的可重用性有着重要作用。
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date date = new Date();
//得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
如果我们需要用到 java.util 中的其他类, 可以使用 import java.util. ⁎ 来导入;
import java.util.*;
public class Test {
public static void main(String[] args) {
Date date = new Date();
//得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
但是使用 “ import 包 . ⁎ ” 来导入类名时,如果导入的多个包中包含了同名的类,使用通配符 ⁎ 导入可能会导致命名冲突。这会使编译器无法确定要使用哪个类而报错;
import java.util.*;
import java.sql.*;
public class Test {
public static void main(String[] args) {
// util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, 编译出错
Date date = new Date();
System.out.println(date.getTime());
}
}
// 编译出错
Error: java: 对Date的引用不明确
java.sql 中的类 java.sql.Date 和 java.util 中的类 java.util.Date 都匹配
此时需要使用完整的包名来消除歧义。
import java.util.*;
import java.sql.*;
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
System.out.println(date.getTime());
}
}
1.3.3 包的定义
- 包语句: 在Java代码文件的顶部,使用 package 语句来指定该代码所属的包。例如:package com.example.mypackage;
- 唯一性和命名规则: 包名应尽量保持唯一,通常使用公司的域名的颠倒形式来确保唯一性,如 com.example.myproject。包名应使用小写字母,并可以包含数字和下划线,但不应包含特殊字符或空格。
- 代码路径: 包名应与代码存储路径相匹配。例如,如果你的包名是 com.example.mypackage,则代码文件应该存储在相应的目录结构中,例如 com/example/mypackage。
- 默认包: 如果一个类没有指定 package 语句,它将被放置在默认包 src 中。然而,最佳实践是始终为你的类指定一个明确的包,以避免潜在的命名冲突和组织问题。
总之,使用包是一种组织和管理Java代码的重要方式,可以确保代码的唯一性、可维护性和可读性。遵循这些基本规则有助于更好地组织和管理自定义包,并使代码更具结构化和可维护性。
1.3.4 Java 常见包
以下是一些常见的Java标准库包:
1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
2. java.lang.reflect:java 反射编程包;
3. java.net:进行网络编程开发包。
4. java.sql:进行数据库开发的支持包。
5. java.util:是java提供的工具程序包。(集合类等) 非常重要
6. java.io:I/O编程开发包。
7. java.math:包含用于执行大数运算的类,如BigInteger和BigDecimal,用于处理大数字和高精度的数学运算。
2.static 关键字
static 是Java中的一个关键字,它可以用于不同的上下文中,具有不同的含义和用途:
2.1 静态变量
静态变量(Static Variables): 使用 static 关键字声明的变量称为静态变量,也叫类变量。静态变量属于类而不是实例,因此它们对于类的所有实例都是共享的。
静态成员变量的特性:
- 不属于某个具体的对象,是类的属性,所有对象共享的,不存储在某个对象的空间中。
- 既可以通过对象访问,也可以通过类名访问,但一般更推荐使用类名访问,因为它们属于类而不是对象。
- 类变量存储在方法区(Method Area)中,而不是对象的堆内存中。方法区是用于存储类的结构信息、静态变量等的内存区域。
- 生命周期伴随类的一生,即随着类的加载而创建,在整个程序运行期间存在,随着类的卸载而销毁。
public class MyClass {
static int count = 0; // 静态变量,所有实例共享
String name;
int age;
public MyClass() {
}
public MyClass(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
// 静态成员变量可以直接通过类名访问
System.out.println(MyClass.count);
MyClass c1 = new MyClass();
MyClass c2 = new MyClass("brz", 8);
MyClass c3 = new MyClass("wrx", 12);
// 也可以通过对象访问:但是classRoom是三个对象共享的
System.out.println(c1.count);
System.out.println(c2.count);
System.out.println(c3.count);
}
}
最终输出结果均为:0
2.2 静态方法
静态方法(Static Methods): 使用 static 关键字声明的方法称为静态方法。静态方法不依赖于类的实例,可以直接通过类名调用,而不需要创建对象。静态方法通常用于执行与类相关的操作,而不涉及实例特定的数据。例如:
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
调用静态方法:int result = MathUtils.add(3, 5);
注意:
静态方法中不能访问任何非静态成员变量: 静态方法不依赖于类的实例,因此无法访问非静态成员变量,因为非静态成员变量属于类的实例,而静态方法不关联到任何特定的实例。
静态方法中不能调用任何非静态方法: 这是因为非静态方法需要通过对象的引用(
this
)来调用,而静态方法没有与特定对象关联,因此无法传递this
引用。非静态方法是与对象的状态相关的,而静态方法是与类相关的。静态方法无法重写,不能用来实现多态: 静态方法与类相关,而不是与对象相关。因为多态是基于对象的动态绑定,所以只有实例方法(非静态方法)才能被重写和用于实现多态。静态方法在子类中可以被隐藏,但不能被重写。(后续多态介绍)
2.3 静态代码块
静态代码块(Static Initialization Blocks): 静态代码块是一个在类加载时执行的代码块,通常用于初始化静态变量或执行一次性的初始化操作。静态代码块用 static 关键字和大括号包围起来,如下所示:
public class MyClass {
static {
// 静态代码块中的初始化操作
}
}
当涉及到代码块时,我们通常指的是在程序中用大括号 { } 包围的代码区域。代码块根据其定义的位置和关键字可以分为不同类型,下面进行详细介绍。
3. 代码块
代码块就是在程序中用大括号 { } 包围的代码区域。根据代码块定义的位置以及关键字,又可分为以下四种:
- 普通代码块
- 构造块
- 静态块
- 同步代码块(暂时不解释,后期介绍)
3.1 普通代码块
普通代码块是在方法内部定义的,它用于创建局部作用域,其中的变量在块内部有效。普通代码块通常用于控制变量的作用范围和生命周期。
public static void main(String[] args) {
{ //直接使用{}定义,普通方法块
int x = 5 ; // 这个变量只在块内部有效
System.out.println("x1 * 2 = " + x);
}
int x = 10 ;
System.out.println("x2 * 2 = " +x);
}
输出:
x1*2 = 10
x2*2 = 20
3.2 构造代码块
构造代码块,也叫实例代码块,用于在每次创建对象时执行一些初始化操作。与普通代码块不同,构造块定义在类中也没有关键字,它们会在每次对象创建时都执行一次。
public class Subaru{
private String name;
private int age;
public Subaru() {
System.out.println("Driven by What’s Inside");
}
//实例代码块
{
this.name = "impreza";
this.age = 12;
System.out.println("Confidence in Motion");
}
public void show(){
System.out.println("name: " + name + " age: " + age);
}
}
public class Main {
public static void main(String[] args) {
Subaru sti = new Subaru();
stu.show();
}
}
运行结果:
Confidence in Motion
Driven by What’s Inside
name: impreza age: 12
3.3 静态代码块
使用 static 定义的代码块称为静态代码块。只在类加载时执行一次,通常用于初始化静态变量或执行一次性的初始化操作。
public class Subaru{
private String name;
private int age;
public Subaru() {
System.out.println("Driven by What’s Inside");
}
//实例代码块
{
this.name = "impreza";
this.age = 12;
System.out.println("Confidence in Motion");
}
//静态代码块
static{
String producer = Subaru;
System.out.println("FHI——SUBARU");
}
public void show(){
System.out.println("name: " + name + " age: " + age);
}
}
public class Main {
public static void main(String[] args) {
Subaru sti = new Subaru();
stu.show();
}
}
运行结果:
FHI——SUBARU
Confidence in Motion
Driven by What’s Inside
name: impreza age: 12
注意:
- 静态代码块只会执行一次,与对象多少无关;
- 静态成员变量是类的属性,因此是在JVM加载类时开辟空间并初始化的;
- 如果一个类中包含多个静态代码块,在编译代码时,编译器会按照定义的先后次序依次执行(合并);
- 实例代码块只有在创建对象时才会执行;
3.4 代码执行顺序
静态优先,同类中按定义位置先后顺序执行。
成员变量 → 静态代码块 → 静态方法 → 构造代码块 → 构造方法 → 成员方法
-
首先,执行成员变量的初始化,按照定义位置先后顺序执行。
-
接下来,执行静态代码块,这是在类加载时执行的,只会执行一次。
-
然后,执行静态方法。静态方法是可以被直接调用的,所以可以在任何时候执行。
-
构造代码块会在每次创建类的对象时执行,先于构造方法执行。
-
构造方法是在对象创建时执行的,可以有多个构造方法,根据实例化对象时的构造方法不同而执行不同的构造方法。
- 最后,执行成员方法。成员方法是可以被对象调用的。
4. 总结
在本文中,我们深入研究了封装这一关键的面向对象编程概念,以及与之相关的访问修饰符、包的概念,以及静态关键字的用法。封装是编写清晰、安全、可维护代码的基础,它允许我们将数据和操作封装在一起,隐藏内部细节,提供一个简单的接口供其他代码使用。
我们还了解了不同类型的代码块,如普通代码块、构造块和静态块,以及它们在程序中的执行顺序。这些概念有助于我们更好地组织和控制代码的行为。