面向对象的三大基本特征
java 封装、继承、多态介绍
1.Java 的封装 (Encapsulation)
1.1.定义
封装(Encapsulation) 是面向对象编程的四大基本概念之一,它指的是将数据(属性)和操作数据的方法捆绑在一起,并隐藏对象内部的工作细节。通过封装,可以保护对象的状态不被外部直接访问,同时提供公共接口来进行交互。
1.2.目标
- 信息隐藏: 限制对类成员的访问,以防止外部代码随意修改对象状态。
- 提高安全性: 减少了外部代码对类内部状态的直接访问,增强了数据的安全性。
- 增强模块化: 封装使得每个类都成为独立的模块,易于维护和扩展。
1.3.实现方式
在 Java 中,封装主要通过以下几种方式实现:
- 访问修饰符
- private: 最严格的访问级别,只能在定义它的类中访问。
- protected: 可以在同一个包中的所有类以及不同包中的子类中访问。
- default(无修饰符): 只能在同一个包内的类中访问。
- public: 最宽松的访问级别,可以在任何地方访问。
- getter 和 setter 方法
- 提供受控访问:即使属性是私有的,也可以通过公共方法来获取或设置它们的值。
- Getter 方法: 用于读取私有属性的值。
- Setter 方法: 用于设置私有属性的值,通常会包含验证逻辑以确保数据的有效性。
- 构造函数
- 初始化对象时设置初始状态,确保对象创建时具有有效的值。
1.4.示例代码
public class Person {
// 私有属性,外界无法直接访问
private String name;
private int age;
// 默认构造函数
public Person() {}
// 带参数的构造函数
public Person(String name, int age) {
this.name = name;
setAge(age); // 使用setter进行验证
}
// Getter 方法
public String getName() {
return name;
}
// Setter 方法,包含验证逻辑
public void setName(String name) {
if (name != null && !name.isEmpty()) {
this.name = name;
} else {
System.out.println("Name cannot be empty.");
}
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age > 0 && age < 120) {
this.age = age;
} else {
System.out.println("Invalid age value.");
}
}
}
1.5.总结
通过使用访问修饰符、构造函数和 getter/setter 方法,Java 的封装特性允许开发者控制对类成员的访问,从而保护对象的状态并提供安全的数据访问途径。这不仅提高了代码的安全性和可维护性,还促进了更好的模块化设计。
2.继承 (Inheritance)
继承是面向对象编程(OOP)中的一种机制,它允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。这促进了代码的重用,并有助于建立类之间的层次结构。
- 关键字:
extends
- 特点:
- 子类继承了父类的所有非私有成员(字段、方法)。
- 构造器不被继承,但可以通过
super()
调用父类构造器。 - 子类可以覆盖父类的方法(通过方法重写实现)。
- 子类可以添加新的字段和方法。
2.1.示例
class Animal {
void eat() {
System.out.println("This animal eats.");
}
}
class Dog extends Animal {
// 方法重写
@Override
void eat() {
System.out.println("The dog eats dog food.");
}
// 新增方法
void bark() {
System.out.println("Woof!");
}
}
2.2.方法重写 (Method Overriding)详细说明
方法重写是指子类可以重新定义父类中的方法。被重写的方法签名(包括名称、参数列表)必须完全一致,而返回类型、抛出异常列表等可以在一定条件下有所不同。重写方法的实际执行版本是在运行时根据对象的实际类型决定的,这是动态多态性的体现。
- 特点:
- 必须出现在继承关系中(即发生在父类与子类之间)。
- 方法签名必须相同(包括方法名和参数列表)。
- 子类的方法不能比父类的方法更严格的访问权限(如父类是
public
,子类不能是private
或protected
)。 - 子类的方法可以有不同的返回类型,但只能是父类返回类型的子类型(协变返回类型)。
- 使用
@Override
注解来表明这是一个重写的方法,有助于编译器检查是否正确实现了重写。
2.2.1.示例
class Animal {
void sound() {
System.out.println("Some generic animal sound.");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Meow!");
}
}
public class OverridingExample {
public static void main(String[] args) {
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.sound(); // 输出: Woof!
myCat.sound(); // 输出: Meow!
}
}
3.多态 (Polymorphism)
多态是指相同的操作作用于不同的对象时,可以有不同的解释,产生不同的执行结果。在Java中,多态主要体现在两个方面:
编译时多态(静态多态/方法重载):
- 同一个类中有多个同名但参数列表不同的方法。
- 方法的选择是在编译期决定的。
运行时多态(动态多态/方法重写):
- 父类引用指向子类对象,并调用子类重写的方法。
- 方法的实际执行版本是在运行时根据对象的实际类型决定的。
3.1.示例
// 假设我们有上述定义的Animal和Dog类
public class PolymorphismExample {
public static void main(String[] args) {
Animal myDog = new Dog(); // 父类引用指向子类对象
myDog.eat(); // 输出: The dog eats dog food.
// 下面这行代码会报错,因为myDog是Animal类型的引用,
// 它不知道Dog特有的bark()方法
// myDog.bark();
}
}
3.2.方法重载 (Method Overloading)细节说明
方法重载是指在一个类中可以有多个同名的方法,但这些方法必须具有不同的参数列表(即参数的数量、类型或顺序不同)。编译器根据调用时提供的参数自动选择合适的方法版本。这被称为静态多态性,因为方法的选择是在编译期确定的。
- 特点:
- 只能在同一个类中进行。
- 参数列表必须不同(包括参数的数量、类型或顺序)。
- 返回类型可以相同也可以不同,但不能仅靠返回类型区分重载方法。
- 访问修饰符可以不同。
3.2.1示例
class Calculator {
// 加法:两个整数相加
int add(int a, int b) {
return a + b;
}
// 加法:三个整数相加
int add(int a, int b, int c) {
return a + b + c;
}
// 加法:两个浮点数相加
double add(double a, double b) {
return a + b;
}
}
public class OverloadingExample {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(1, 2)); // 输出: 3
System.out.println(calc.add(1, 2, 3)); // 输出: 6
System.out.println(calc.add(1.5, 2.5)); // 输出: 4.0
}
}
4.构造函数 (Constructor)
构造函数是一种特殊的方法,用于初始化新创建的对象。每个类都可以有一个或多个构造函数,它们的名字必须与类名相同,并且没有返回类型(包括void
)。如果一个类没有显式定义任何构造函数,Java 编译器会自动提供一个无参的默认构造函数。
- 特点:
- 名称必须与类名相同。
- 没有返回类型。
- 可以重载(即可以有多个不同参数列表的构造函数)。
- 构造函数主要用于对象初始化,比如设置初始值等。
4.1.示例
class Person {
String name;
int age;
// 默认构造函数
public Person() {
this.name = "Unknown";
this.age = 0;
}
// 带参数的构造函数
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
4.2.默认构造函数 (Default Constructor)
如果一个类中没有任何构造函数,则编译器会自动生成一个无参数的默认构造函数。这个默认构造函数不会执行任何操作,只是简单地调用父类的无参数构造函数(如果有的话)。一旦你在类中定义了至少一个构造函数,编译器将不再生成默认构造函数。
5.类变量 (Class Variables) 和 成员变量 (Instance Variables)
- 类变量: 被
static
关键字修饰的变量,属于整个类而不是单个对象。所有该类的实例共享同一个类变量。 - 成员变量: 未被
static
修饰的变量,属于每个对象的私有数据。每个实例都有自己的一份副本。
5.1示例
class Counter {
static int count = 0; // 类变量
int instanceCount = 0; // 成员变量
Counter() {
count++;
instanceCount++;
}
}
6.局部变量 (Local Variables)
局部变量是在方法、构造函数或块内部声明的变量。它们的作用域仅限于声明它们的代码块内,在离开该代码块后就会被销毁。局部变量不能使用访问修饰符(如 public
, private
, protected
),但可以使用其他修饰符如 final
。
6.1.示例
public class Example {
void methodWithLocalVariable() {
int localVariable = 10; // 局部变量
System.out.println(localVariable);
}
}
7.成员变量和方法的作用域 (Scope of Instance Variables and Methods)
- 成员变量 的作用域是整个类,可以在类中的任何地方访问,除非它们被更细粒度的作用域限制(例如在静态上下文中尝试访问非静态成员变量会导致编译错误)。
- 方法 的作用域也是整个类,可以从类内的任何地方调用,除非它们是私有的(
private
),那么只能在声明它们的类内部访问。
7.1示例
class ScopeExample {
private String memberVariable = "I'm a member variable"; // 私有成员变量
public void printMemberVariable() {
System.out.println(memberVariable); // 在类的方法中访问成员变量
}
public void anotherMethod() {
printMemberVariable(); // 在另一个方法中调用公共方法
}
}
8.平台无关性 (Platform Independence)
Java 语言的一个显著特点是它的 平台无关性,这使得 Java 程序可以在不同的操作系统上运行而无需重新编译。这种特性主要得益于 Java 虚拟机(JVM)和字节码(Bytecode)的概念。
- Java 源代码:开发者编写的是
.java
文件。 - 编译过程:Java 编译器将源代码编译成与平台无关的中间代码,即字节码(
.class
文件)。这个过程生成的字节码不是针对任何特定硬件架构的机器指令,而是专为 JVM 设计的一种抽象指令集。 - 解释执行:当程序运行时,JVM 将字节码转换为具体操作系统的本地机器指令,并在该平台上执行。每个操作系统都有自己的 JVM 实现,负责处理这一转换。
由于 JVM 可以在多种平台上实现,因此只要安装了相应的 JVM,相同的 Java 字节码就可以在 Windows、Linux、macOS 等不同系统上无缝运行,这就是所谓的“一次编写,到处运行”(Write Once, Run Anywhere)。
9.值传递 (Pass by Value)
Java 中的所有参数传递都是 按值传递(pass by value),这意味着当你传递一个变量给方法时,实际上是在传递该变量值的一份副本。对于基本数据类型(如 int
, float
, char
等),传递的就是实际值;而对于引用类型(如对象、数组等),传递的是对象引用的副本。这意味着即使你改变了传入的方法中的参数值,也不会影响到原始变量,因为它们是独立的副本。
然而,对于引用类型的参数,虽然传递的是引用的副本,但副本仍然指向堆内存中同一个对象,所以如果修改了对象的状态(例如改变对象的属性),这些更改会反映在原始对象上,因为两者指向的是同一个实例。
9.1.示例
public class PassByValueExample {
// 对于基本数据类型,传递的是值的副本
public static void changePrimitive(int x) {
x = 100;
}
// 对于引用类型,传递的是引用的副本,但是引用指向的对象是相同的
public static void changeObject(Builder builder) {
builder.setName("Changed Name");
}
public static void main(String[] args) {
int num = 5;
changePrimitive(num);
System.out.println(num); // 输出: 5,因为传递的是值的副本
Builder myBuilder = new Builder("Original Name");
changeObject(myBuilder);
System.out.println(myBuilder.getName()); // 输出: Changed Name,因为修改了对象的状态
}
}
class Builder {
private String name;
public Builder(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}