
面向对象编程
程序设计思路
程序设计思路指的是在编写软件时所采用的思考方式和组织程序结构的方法 。它决定了程序的逻辑结构、模块划分、数据管理方式 以及开发与维护的方式。
面向过程的程序设计(Procedural Programming)
- 基本思想
面向过程的核心思想是:
"把程序看作一系列操作步骤(过程/函数)的集合。"
程序员首先分析问题需要完成哪些"步骤",然后按照一定顺序用代码实现这些步骤。
-
设计流程
-
分析问题 → 明确输入、输出、处理过程
-
分解步骤 → 把整体任务分解为若干子任务(函数)
-
实现函数 → 用过程(或函数)实现每个子任务
-
组合调用 → 按一定逻辑顺序调用这些函数完成任务
-
特点
✅ 优点:
- 思路清晰、结构简单;
- 对小型程序开发效率高;
- 执行速度快,资源占用少。
⚠️ 缺点:
- 代码复用性差;
- 数据与操作分离,修改容易出错;
- 难以维护和扩展,尤其在大型项目中。
面向对象的程序设计(Object-Oriented Programming, OOP)
- 基本思想
面向对象的核心思想是:
"把现实世界中的事物抽象为对象,通过对象之间的交互来完成任务。"
对象包含数据(属性)和操作(方法) ,强调数据与行为的统一。
-
三大基本特性
-
封装(Encapsulation) :
将数据和方法封装在对象内部,对外只暴露必要的接口。
-
继承(Inheritance) :
子类可以继承父类的属性和方法,实现代码复用。
-
多态(Polymorphism) :
同一操作在不同对象中表现出不同的行为。
-
设计流程
-
识别对象 → 分析问题领域中的实体
-
定义类 → 为对象设计类结构(属性 + 方法)
-
建立关系 → 定义类之间的继承或关联关系
-
实现交互 → 通过对象的消息传递完成系统功能
-
特点
✅ 优点:
- 模块化强,代码可复用;
- 易于维护、扩展;
- 结构清晰,贴近人类思维方式。
⚠️ 缺点:
- 程序结构复杂;
- 对小程序来说可能效率较低。
类、对象、成员概述
一、类(Class)
类是一类具有相同属性和行为的对象的抽象描述 。它是对象的模板 或蓝图,定义了对象应该具有的:
- 属性(成员变量)
- 行为(成员方法)
二、对象(Object)
对象是 类的实例(Instance),是根据类创建出来的实际存在的个体。类是"模板",对象是"具体的实例"。
三、成员
类中的组成部分包括:
- 成员变量(属性 / fields)
- 用于描述对象的状态 或属性
- 定义在类中、方法外
- 每个对象都拥有自己的一份成员变量副本
- 成员函数(方法 / 函数)
- 用于定义对象的行为
- 可以操作成员变量,实现逻辑功能
- 构造方法
- 用于在创建对象时对成员变量进行初始化
- 方法名与类名相同,没有返回类型
类的定义与对象创建、使用概述
一、类的定义
使用class关键字
java
[修饰符] class 类名 {
// 成员变量(属性)
类型 变量名;
// 构造方法
类名(参数列表) {
// 初始化对象
}
// 成员方法
返回类型 方法名(参数列表) {
// 方法体
}
}
java
public class Student {
// 1. 成员变量(属性)
String name;
int age;
// 2. 构造方法(用于创建对象)
public Student(String name, int age) {
this.name = name; // this代表当前对象
this.age = age;
}
// 3. 成员方法(行为)
public void study() {
System.out.println(name + " 正在学习 Java。");
}
public void showInfo() {
System.out.println("姓名:" + name + ",年龄:" + age);
}
}
二、对象创建
对象是类的具体实例。通过 new 关键字创建对象。
java
类名 对象名 = new 类名(参数);
java
public class Main {
public static void main(String[] args) {
// 创建 Student 类的对象
Student stu1 = new Student("小明", 20);
Student stu2 = new Student("小红", 19);
// 调用对象的方法
stu1.study();
stu2.showInfo();
}
}
java
Student student = null; // 不引用任何对象
p.name = "小红"; // 编译正确,但是运行时,NullPointerException
三、对象使用
对象是类的一个实例,具备该类事物的属性和行为(即方法)。通过如下方式访问对象成员(包括属性和方法)
java
对象名.属性
对象名.方法
java
public class Animal {
public int legs;
public void eat() {
System.out.println("I'm eating.");
}
public void move() {
System.out.println("I'm moving.");
}
}
java
public class AnimalTest {
public static void main(String args[]) {
Animal a = new Animal();
a.legs = 4; // 访问属性
System.out.println(xb.legs);
a.eat(); // 调用方法
a.move(); // 调用方法
}
}
⭐对象内存分析
堆空间(Heap):所有通过 new 关键字创建的结构(如对象、数组)都存储在堆空间中。
对象属性的存储:对象的属性(成员变量)都保存在堆空间中。
多个对象的独立性:当通过同一个类创建多个对象(如 p1、p2)时,每个对象在堆中都有自己独立的一份属性副本。修改某个对象的属性不会影响其他对象对应属性的值。
java
Person p1 = new Person();
Person p2 = new Person();
System.out.println(p1 == p3); // false
引用赋值的共享性:当使用已有对象为另一个变量赋值(如 p3 = p1)时,系统不会在堆中重新创建新的对象。此时,p3 和 p1 共同指向堆中的同一个对象。若通过其中一个变量修改对象的属性,另一个变量访问该属性时也会看到修改后的结果。(类似于 C 语言的指针)
java
Person p1 = new Person();
Person p3 = p1;
System.out.println(p1 == p3); // true
对象名中存储的是对象地址
java
Student student = new Student();
System.out.println(student); // Student@4e25154f
直接打印对象名和数组名都是显示类型@对象的hashCode值,所以说类、数组都是引用数据类型,引用数据类型的变量(变量本身存在栈中)中存储的是对象的地址,或者说指向堆中对象的首地址。
成员变量(属性 / field)
声明成员变量
java
[修饰符] class 类名{
[修饰符] 数据类型 成员变量名 [= 初始化值];
}
成员变量必须在类中,方法外声明;
权限修饰符包括:private、缺省、protected、public,其他的修饰符包括:static、final 等
对于成员变量可以显式赋初始值,也可以不赋初始值,当对象被创建时编译器会自动为它们根据数据类型赋默认值。
java
public class Example {
int num;
double price;
boolean isActive;
String name;
public void printValues() {
System.out.println("num = " + num); // 0
System.out.println("price = " + price); // 0.0
System.out.println("isActive = " + isActive); // false
System.out.println("name = " + name); // null
}
public static void main(String[] args) {
Example e = new Example();
e.printValues();
}
}
静态变量
成员变量与局部变量
成员变量是定义在类中、方法外的变量,属于类或对象的"属性"。
| 特点 | 说明 |
|---|---|
| 作用域 | 整个类中有效(在类的所有方法中可使用) |
| 存储位置 | 实例变量存储在堆(Heap)中;静态变量存储在方法区(Method Area)中 |
| 生命周期 | 对象存在 → 成员变量存在;对象销毁 → 成员变量消失 |
| 默认值 | 自动初始化(例如 int 默认是 0,boolean 默认是 false,String 默认是 null) |
| 修饰符 | 可以使用访问控制符(public、private 等)和 static |
局部变量是定义在方法中、代码块中或方法参数列表中的变量。
| 特点 | 说明 |
|---|---|
| 作用域 | 仅在定义它的方法或代码块内部有效 |
| 存储位置 | 在栈(Stack)中存储 |
| 生命周期 | 方法调用时创建,方法结束后销毁 |
| 默认值 | 必须手动初始化,否则编译错误;形参靠实参给它初始化 |
| 修饰符 | 不能使用 public、private、static 等修饰符 |
java
public void test() {
int x;
System.out.println(x); // ❌ 编译错误:变量 x 可能未被初始化
}
⭐属性赋值 待续
java
public class Demo {
private int a = 1; // ① 直接定义时赋值
private int b;
{ // ② 实例代码块
b = 2;
System.out.println("实例代码块执行");
}
public Demo() { // ③ 构造方法
a = 3;
System.out.println("构造方法执行");
}
public void setA(int a) { // ④ 通过方法赋值(如 setter)
this.a = a;
}
}
| 方式 | 位置 | 赋值时机 | 示例 |
|---|---|---|---|
| ① 默认值赋值 | 类加载创建对象时 | JVM为对象分配内存时自动初始化 | int a; // 默认值0 |
| ② 显式初始化(定义时赋值) | 定义变量时 | 在构造方法前执行 | int a = 1; |
| ③ 实例代码块赋值 | {} 块中 |
在构造方法之前、在显式初始化之后执行 | { b = 2; } |
| ④ 构造方法赋值 | 构造器中 | 最后执行(创建对象时) | a = 3; |
| ⑤ 方法赋值(如setter) | 调用时 | 对象创建后随时 | obj.setA(5); |
当我们 new Demo() 时,属性赋值的执行顺序如下:
- 父类静态变量 → 父类静态代码块
- 子类静态变量 → 子类静态代码块
(静态部分仅在类第一次加载时执行一次) - 父类实例变量(默认值 → 显式赋值 → 实例代码块)
- 父类构造方法
- 子类实例变量(默认值 → 显式赋值 → 实例代码块)
- 子类构造方法
- 调用普通方法(如 setter)赋值(如果有)
成员函数(函数 / 方法)
- 方法是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中也称为函数或过程。
- 将功能封装为方法的目的是,可以实现代码重用,减少冗余,简化代码。
- ⚠️Java中方法不能独立存在,所有的方法必须定义在类里。
- ⚠️不可以在方法中定义方法。
声明方法
java
[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表] {
方法体
}
- 方法 = 方法头 + 方法体。方法头就是
[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表],也称为方法签名。通常调用方法时只需要关注方法头就可以,从方法头可以看出这个方法的功能和调用格式。方法体是方法被调用后要执行的代码。对于调用者来说,不需要关心方法体。 - 方法头:
- 修饰符:可选。方法的修饰符有很多,例如:public、protected、private、static、abstract、native、final、synchronized 等。其中,权限修饰符有 public、protected、private。并且根据是否有 static,可以将方法分为静态方法和非静态方法。其中静态方法又称为类方法,非静态方法又称为实例方法。
- 返回值类型:表示方法运行的结果的数据类型,方法执行后将结果返回到调用者。无返回值,则声明:void;有返回值,则声明出返回值类型(可以是任意类型),与方法体中"
return 返回值"搭配使用。 - 方法名:属于标识符,命名时遵循标识符命名规则和规范。
- 形参列表:表示完成方法体功能时需要外部提供的数据列表。可以包含零个或若干个参数。无论是否有参数,
()不能省略。如果有参数,每一个参数都要指定数据类型和参数名,多个参数之间使用,分隔。 throws异常列表:可选。后续说明。
- 方法体:方法体必须有
{}括起来,在{}中编写完成方法功能的代码。 - 方法体中
return语句:后续说明。
java
public class MethodDefineDemo {
/**
* 无参无返回值方法
*/
public void sayHello() {
System.out.println("hello");
}
/**
* 有参无返回值方法
* @param length int 第一个参数,表示矩形的长
* @param width int 第二个参数,表示矩形的宽
* @param sign char 第三个参数,表示填充矩形图形的符号
*/
public void printRectangle(int length, int width, char sign) {
for (int i = 1; i <= length ; i++) {
for(int j=1; j <= width; j++) {
System.out.print(sign);
}
System.out.println();
}
}
/**
* 无参有返回值方法
* @return
*/
public int getIntBetweenOneToHundred() {
return (int)(Math.random() * 100 + 1);
}
/**
* 有参有返回值方法
* @param a int 第一个参数,要比较大小的整数之一
* @param b int 第二个参数,要比较大小的整数之二
* @return int 比较大小的两个整数中较大者的值
*/
public int max(int a, int b) {
return a > b ? a : b;
}
}
调用方法
实例方法
- 调用一次就执行一次,不调用不执行。
- 方法中可以调用类中的方法或属性。
- 必须通过实例对象调用。
java
对象.方法名([实参列表])
java
public class Person {
// 实例变量(属于对象)
String name;
int age;
// 构造方法(用于创建对象)
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 实例方法(非 static,必须通过对象调用)
public void introduce() {
System.out.println("大家好,我叫 " + name + ",今年 " + age + " 岁。");
}
// 实例方法(带参数和返回值)
public String greet(String otherName) {
return "你好," + otherName + "!我是 " + name + "。";
}
// 主方法(程序入口)
public static void main(String[] args) {
// 创建对象(实例化)
Person p1 = new Person("小明", 20);
// 调用实例方法(通过对象名.方法名)
p1.introduce();
// 调用带参数和返回值的方法
String msg = p1.greet("小红");
System.out.println(msg);
}
}
静态方法
静态方法属于类本身,用 类名.方法名() 调用;实例方法属于对象,用 对象名.方法名() 调用。
内存分析
- 方法
没有被调用的时候,都在方法区中的字节码文件(.class)中存储。 - 方法
被调用的时候,需要进入到栈内存中运行。方法每调用一次就会执行入栈,即给当前方法开辟一块独立的内存区域,用于存储当前方法的局部变量的值。 - 当方法执行结束后,会释放该内存,称为
出栈,如果方法有返回值,就会把结果返回调用处,如果没有返回值,就直接结束,回到调用处继续执行下一条指令。
方法的参数传递机制
形参与实参
1️⃣ 形参(形式参数)
- 定义: 在定义方法时,括号中声明的变量。
- 位置: 出现在方法定义处。
java
void swap(int a, int b) { ... }
👉 这里的 a、b 是形参。
特点:
- 只在方法内部有效。
- 调用方法时由实参赋值。
2️⃣ 实参(实际参数)
- 定义: 调用方法时传递给方法的真实数据或变量。
- 位置: 出现在方法调用处。
java
swap(x, y);
👉 这里的 x、y 是实参。
特点:
- 实参的值会传递给形参。
- 方法执行完毕后,形参被销毁,实参是否变化取决于参数传递机制。
Java 的参数传递机制
Java 只有一种参数传递方式:
✅ 值传递(Pass by Value)
这句话非常重要------即便是对象,也传递引用的副本(引用的值)。
(1)基本类型
- 传递的是数值本身。
- 方法中对形参的修改不会影响实参。
(2)引用类型(对象)
- 传递的是对象引用的副本(指向同一个对象的地址副本)。
- 方法中可以通过该引用修改对象的内容。
- 但若方法中让引用指向新对象,不会影响外部引用。
举例
java
public class SwapDemo {
public static void main(String[] args) {
int x = 10;
int y = 20;
swap(x, y);
System.out.println("x = " + x + ", y = " + y);
}
static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("方法内: a = " + a + ", b = " + b);
}
}
/*
方法内: a = 20, b = 10
x = 10, y = 20
*/
swap(x, y)时,把x和y的值拷贝一份传入a、b。- 方法中交换的只是形参
a、b的值,不影响外部的x、y。
java
class Num {
int value;
}
public class SwapDemo2 {
public static void main(String[] args) {
Num n1 = new Num();
Num n2 = new Num();
n1.value = 10;
n2.value = 20;
swap(n1, n2);
System.out.println("main中: n1 = " + n1.value + ", n2 = " + n2.value);
}
static void swap(Num a, Num b) {
int temp = a.value;
a.value = b.value;
b.value = temp;
System.out.println("方法内: a = " + a.value + ", b = " + b.value);
}
}
/*
方法内: a = 20, b = 10
main中: n1 = 20, n2 = 10
*/
- 传递的是对象引用的副本。
a、b和n1、n2指向同一块内存中的对象。- 通过
a.value、b.value修改对象内容,外部可见。
java
static void swap(Num a, Num b) {
Num temp = a;
a = b;
b = temp;
}
- 这段代码不会交换外部引用 。因为
a、b是引用变量的副本 ,改变它们的指向不会影响外部的n1、n2。
java
public class TransferTest3 {
public static void main(String args[]) {
TransferTest3 test = new TransferTest3();
test.first();
}
public void first() {
int i = 5;
Value v = new Value(); // 创建一个 Value 对象,假设地址是 @100
v.i = 25; // @100.i = 25
second(v, i); // 此时传递的是 v 的 引用副本(值为 @100),i 的 数值副本(值为 5)
// second() 执行完毕,first() 的局部变量:
// v → @100,v.i = 20
System.out.println(v.i);
}
public void second(Value v, int i) {
i = 0; // 只改变局部 i,不影响 first() 中的 i
v.i = 20; // 修改 @100 对象中的 i 为 20
Value val = new Value(); // 创建新对象,假设地址 @200
v = val; // v 现在指向 @200,不再指向 @100
System.out.println(v.i + " " + i); // @200.i = 15,i = 0
}
}
class Value {
int i = 15;
}
// 15 0
// 20
关键字return
- 作用是结束方法的执行,并将方法的结果返回。
- 如果返回值类型不是
void,方法体中必须保证一定有return 返回值;语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容;如果返回值类型为void,方法体中可以没有return语句,如果要用return语句提前结束方法的执行,那么return后面不能跟返回值,直接写return ;就可以。 - return 语句的直接后面就不能再写其他代码,否则会报错:
Unreachable code。
递归方法(recursion)
递归 是一种让方法 调用自身 的编程技巧,通常用来将复杂问题分解为更小、更简单的相似问题。
java
返回类型 方法名(参数列表) {
if (终止条件) {
return 结果; // 基准情况(base case)
} else {
return 方法名(更接近终止条件的参数); // 递归调用
}
}
计算阶乘(n!)
java
public class RecursionExample {
public static int factorial(int n) {
if (n == 0 || n == 1) { // 基准情况
return 1;
} else { // 递归调用
return n * factorial(n - 1);
}
}
public static void main(String[] args) {
System.out.println(factorial(5)); // 输出 120
}
}
// 执行过程类似:
factorial(5)
→ 5 * factorial(4)
→ 5 * 4 * factorial(3)
→ 5 * 4 * 3 * factorial(2)
→ 5 * 4 * 3 * 2 * factorial(1)
→ 5 * 4 * 3 * 2 * 1 = 120
基准条件(Base Case)
- 必须存在,否则会造成无限递归(StackOverflowError)。
- 是递归停止的"出口"。
递归关系(Recursive Case)
- 每次调用必须使问题规模减小,逐步接近基准条件。
方法栈(Call Stack)
- 每次递归调用都会在栈上分配新的栈帧。
- 返回时栈帧逐步出栈,结果逐层返回。
递归的注意点 ⚠️
- 必须有终止条件,否则会导致无限递归 → 抛出
StackOverflowError。 - 避免重复计算,比如斐波那契数列的递归存在大量重复计算,可以使用 记忆化(memoization) 或改用迭代。
- 栈内存限制,每次递归调用都占用栈空间,层数过多会导致栈溢出。
- 性能开销,相比循环(迭代),递归函数调用有更多的栈操作和函数调用成本。
| 对比项 | 递归 (Recursion) | 迭代 (Iteration) |
|---|---|---|
| 核心思想 | 方法调用自身 | 使用循环结构 (for / while) |
| 终止条件 | 基准情况 | 循环条件 |
| 状态保存 | 隐式在栈上保存 | 显式在变量中保存 |
| 性能 | 调用开销较大 | 通常更快、更节省内存 |
| 可读性 | 简洁、直观(适合分治问题) | 可读性略低(尤其对树、图问题) |
| 风险 | 可能栈溢出 | 通常不会栈溢出 |
| 适用场景 | 树形结构、分治算法、数学定义式 | 数组遍历、计数、累加 |
方法的重载(overload)
在 同一个类 中,如果有 多个方法名字相同但参数列表不同 (参数个数、类型或顺序不同),这些方法就构成 方法重载 。编译器在调用方法时,会根据 传入的参数类型和数量 自动选择匹配的版本。
方法重载条件
-
必须满足以下至少一项不同:
- 参数个数不同;
- 参数类型不同;
- 参数顺序不同(当参数类型不同且顺序交换时)。
注意:
- 仅仅返回值不同不能构成重载。
- 仅仅修饰符不同不能构成重载。
java
int test() {...}
double test() {...} // ❌ 编译错误:方法已定义
方法重载属于 编译时多态(Compile-time Polymorphism)。即在编译阶段,编译器根据参数列表确定调用哪个方法。
java
print(10); // 调用 print(int)
print("Hello"); // 调用 print(String)
会在必要时尝试进行 自动类型提升 来匹配方法,但可能导致歧义
java
void test(int a) { ... }
void test(double a) { ... }
test(10); // 优先匹配 int → 调用 test(int)
test(10.5); // double → 调用 test(double)
test('A'); // char → 自动转为 int → 调用 test(int)
java
void show(int a, double b) {...}
void show(double a, int b) {...}
show(10, 20); // ❌ 编译错误:无法确定调用哪个方法
可变个数形参
可变个数形参(Varargs)是一种允许方法在调用时接受 任意数量参数 的语法特性。它让方法调用更加灵活、简洁。
java
返回类型 方法名(参数类型... 参数名) {
// 方法体
}
注意:形参类型后面有三个点 ...,表示该参数可以接收任意数量的同类型参数。
java
public class VarargsDemo {
// 计算多个整数的和
public static int sum(int... numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
public static void main(String[] args) {
System.out.println(sum()); // 输出 0
System.out.println(sum(5)); // 输出 5
System.out.println(sum(1, 2, 3, 4)); // 输出 10
}
}
int... numbers 实际上在方法内部会被当作一个 int[] 数组 。因此也可以写 sum(new int[]{1,2,3}) 来调用它。编译器会自动把传入的参数"打包"为数组。
⚙️使用规则与注意事项
一个方法最多只能有一个可变参数
java
void test(int... a, String... b); // ❌ 不允许多个 varargs
可变参数必须放在形参列表的最后
java
void print(String name, int... scores); // ✅
void print(int... scores, String name); // ❌
当方法存在重载时,编译器会优先匹配固定参数版本
java
void show(int a) { System.out.println("单个参数"); }
void show(int... a) { System.out.println("可变参数"); }
show(5); // 调用的是 show(int a)
show(1, 2); // 调用的是 show(int... a)
关键字this
this 代表 当前对象(当前实例) 的引用。换句话说,在一个类的实例方法或构造方法中,this 指向 调用该方法的对象本身。
⚠️ 注意:
this只能在 实例方法或构造方法 中使用,静态方法 中不能使用this。⚠️ 注意:
this()调用构造器必须在第一行
this 的常见使用场景
✅ 1. 区分成员变量与局部变量重名
当方法参数名与成员变量名相同时,this 用来区分两者:
java
public class Student {
private String name;
private int age;
public Student(String name, int age) {
// 这里 name 和 age 都是局部变量(构造器参数)
this.name = name; // this.name 是成员变量
this.age = age;
}
public void setName(String name) {
this.name = name;
}
}
📌 如果没有 this,name = name; 只是把参数赋给了参数自己,没有修改成员变量。
✅ 2. 调用类中的其他构造方法
在构造方法中,可以用 this([...]) 调用 本类的其他构造方法,以避免重复代码。
java
public class Person {
private String name;
private int age;
public Person() {
this("未命名", 0); // 调用另一个构造器
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
📌 注意:
this([...])必须放在构造方法的 第一行;- 不能出现循环调用(否则编译错误)。
✅ 3. 在方法中返回当前对象(支持链式调用)
当一个方法返回当前对象本身时,方便链式编程。
java
public class BuilderExample {
private String name;
private int age;
public BuilderExample setName(String name) {
this.name = name;
return this; // 返回当前对象
}
public BuilderExample setAge(int age) {
this.age = age;
return this;
}
public void show() {
System.out.println("Name: " + name + ", Age: " + age);
}
public static void main(String[] args) {
new BuilderExample()
.setName("Alice")
.setAge(22)
.show(); // 链式调用
}
}
✅ 4. 将当前对象作为参数传递
有时需要在方法中将当前对象传递给另一个方法或类。
java
class Printer {
public void print(Person p) {
System.out.println("Printing: " + p);
}
}
class Person {
private String name = "Bob";
public void display() {
Printer printer = new Printer();
printer.print(this); // 将当前对象作为参数传递
}
public String toString() {
return "Person{name='" + name + "'}";
}
public static void main(String[] args) {
new Person().display();
}
}
✅ 5. 在内部类中引用外部类对象
当内部类与外部类的 this 同名或冲突时,可以通过 外部类名.this 来指明外部类实例。
java
public class Outer {
private int x = 10;
class Inner {
private int x = 20;
public void print() {
System.out.println("Inner x: " + x);
System.out.println("Outer x: " + Outer.this.x); // 指向外部类实例
}
}
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.print();
}
}
Inner x: 20
Outer x: 10
底层原理简介
在 JVM 层面,实例方法会自动带上一个隐式参数 ------ this 引用。
java
obj.method();
// 实际上会被编译成:
method(obj);
// 也就是说,每个实例方法的第一个隐含参数就是 this。
对象数组
对象数组(Object Array) 是存放对象引用的数组。每个数组元素其实不是对象本身,而是指向对象的引用(对象的地址)。
java
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
void introduce() {
System.out.println("我叫 " + name + ",今年 " + age + " 岁。");
}
}
java
public class Demo {
public static void main(String[] args) {
// 创建一个长度为3的Person数组
Person[] people = new Person[3];
// 此时数组中元素全是null
for (int i = 0; i < people.length; i++) {
System.out.println(people[i]); // 输出 null
}
// 初始化每个元素
people[0] = new Person("张三", 20);
people[1] = new Person("李四", 22);
people[2] = new Person("王五", 25);
// 使用数组中的对象
for (Person p : people) {
p.introduce();
}
}
}
⚠️Person[] people = new Person[3];代码创建了一个可以存放 3 个 Person 对象引用的数组。但此时数组中的每个元素都是 null ,因为还没有真正创建 Person 对象。使用前必须检查对象是否为 null,否则使用此对象调用成员或函数会抛出 NullPointerException。
java
Person[] people = new Person[3];
people[0].introduce(); // 报错:NullPointerException
可以在创建数组时立即初始化{}
java
Person[] people = {
new Person("张三", 20),
new Person("李四", 22),
new Person("王五", 25)
};
对于多维对象数组,每个元素同样可能是 null。
java
Person[][] team = new Person[2][2];
team[0][0] = new Person("小明", 18);
JavaBean
JavaBean 是一种遵循特定规范的 Java 类(Java Class),主要用于封装数据(data encapsulation)。它常被用作在应用程序的不同层之间传递数据的对象(例如在 MVC 架构中的 Model 部分)。
JavaBean 就是一个可复用的、封装了多个属性和对应方法的类。
🧩 JavaBean 的规范(必须满足以下条件)
-
类必须是 public 的
否则其他包无法访问该类。
-
必须有一个无参构造方法
方便通过反射(Reflection)或框架(如 Spring、JSP)动态创建对象。
-
属性必须是 private 的
确保封装性(Encapsulation)。
-
为每个属性提供 public 的 getter 和 setter 方法
用于外部安全访问和修改属性值。
java
public class UserBean {
// 1. 私有属性
private String name;
private int age;
// 2. 无参构造函数
public UserBean() {}
// 3. 有参构造函数(可选)
public UserBean(String name, int age) {
this.name = name;
this.age = age;
}
// 4. 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;
}
// 5. 可选:重写 toString()
@Override
public String toString() {
return "UserBean{name='" + name + "', age=" + age + "}";
}
}
匿名对象
- 我们也可以不定义对象的句柄,而直接调用这个对象的方法。这样的对象叫做匿名对象。
- 如:new Person().shout();
- 使用情况
- 如果一个对象只需要进行一次方法调用,那么就可以使用匿名对象。
- 我们经常将匿名对象作为实参传递给一个方法调用。
关键字package、import
package 用于声明类所在的包(package) 。包在 Java 中就像文件夹,用于组织和管理类、接口、枚举、注解等,使代码结构更清晰、避免命名冲突。
java
package 包名;
package声明必须写在 Java 源文件的第一行(注释除外)。- 一个
.java文件中只能有一个package声明。 - 包名一般采用公司域名的反写形式,以保证全局唯一性,全部小写。
java
src/
└─ com/
└─ example/
└─ utils/
└─ MathUtil.java
java
package com.example.utils; // 声明该类属于 com.example.utils 包
public class MathUtil {
public static int add(int a, int b) {
return a + b;
}
}
这时,MathUtil 类就属于 com.example.utils 包。
import 用于导入其他包中的类或接口 ,这样在当前类中就可以直接使用类名而不用写全限定名。
import语句必须放在package声明之后、类定义之前。import并不会真的"加载"类,只是让编译器知道类的位置。- 如果需要导入多个类或接口,那么就并列显式多个
import语句 - 如果导入的类或接口是
java.lang包下的,或者是当前包下的,则可以省略此import语句 - 如果已经导入
java.a包下的类,那么如果需要使用a包的子包下的类的话,仍然需要导入。 - 如果在代码中使用不同包下的同名的类,那么就需要使用类的全类名的方式指明调用的是哪个类。
import static的使用:调用指定类或接口下的静态的属性或方法
java
import 包名.类名; // 导入指定类
import 包名.*; // 导入包下所有类(不包括子包)
import static com.test.entity.Person.test; // 静态导入test方法
public class Main {
public static void main(String[] args) {
test(); // 直接使用就可以,就像在这个类定义的方法一样
}
}
封装
什么是封装?为什么要封装?
封装 是面向对象编程(OOP)的基本特性之一
把对象的属性(数据)和方法(操作这些数据的逻辑)封装在一起,并对外隐藏对象的内部实现细节,只暴露必要的接口供外部使用。
封装的主要目的和好处如下:
- 提高安全性(数据保护)
- 防止外部代码直接修改对象内部状态。
- 例如,不能让外部随便把
age改成负数。
- 提高可维护性
- 内部实现改动时,不影响外部调用。
- 例如你修改了
setName()的逻辑,外部调用不需要改。
- 控制访问权限
- 可以通过访问修饰符控制哪些数据对外可见。
- 增强代码的可读性与复用性
- 封装让类职责更清晰,一个类负责自己的状态管理。
- 实现模块化编程
- 每个类都是一个独立模块,便于团队协作和单元测试。
访问(权限)修饰符
在 Java 中,封装通常通过 访问控制修饰符(Access Modifiers) 来实现:
| 访问修饰符 | 当前类 | 同包 | 其他包的子类 | 其他包非子类 |
|---|---|---|---|---|
private |
✅ | ❌ | ❌ | ❌ |
default(无修饰符) |
✅ | ✅ | ❌ | ❌ |
protected |
✅ | ✅ | ✅ | ❌ |
public |
✅ | ✅ | ✅ | ✅ |
- 对于外部类 ,访问修饰符只能是:
public、缺省 - 局部变量不涉及访问控制
- 一般成员变量使用
private修饰,再提供相应的public权限的getter/setter方法访问
java
// 外部类 public
public class Person {
// 私有属性(隐藏数据)
private String name;
private int age;
// 提供公共方法访问属性(暴露接口)
public String getName() {
return name;
}
public void setName(String name) {
if (name != null && !name.isEmpty()) {
this.name = name;
}
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age >= 0 && age <= 120) {
this.age = age;
}
}
}
构造函数(构造器、构造方法)
构造函数 是一种特殊的方法,用于 创建对象时对其进行初始化 。每当我们使用 new 关键字创建对象时,构造函数就会被自动调用。
java
Person p = new Person();
这里的 Person() 就是调用类 Person 的构造函数。
在面向对象编程中,对象通常需要初始状态(例如名字、年龄、颜色等)。构造函数的主要用途就是:
- 初始化对象的成员变量
- 控制对象创建时的行为
- 保证对象在使用前处于有效状态
java
class Person {
String name;
int age;
// 构造函数
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
java
Person p = new Person("Alice", 20);
构造函数会自动把 name 和 age 初始化为对应的值。
基本语法如下:
java
[修饰符] 类名([参数列表]) {
// 初始化代码
}
-
构造函数的 名称必须与类名相同
-
不能有返回类型(包括
void),不能写return返回值 -
可以有参数,也可以没有
-
只能被四种访问控制修饰符修饰。比如
static、final等不能修饰构造函数。 -
使用
new创建对象时自动调用
java
Car c1 = new Car(); // 调用无参构造函数
Car c2 = new Car("BMW", 2023); // 调用有参构造函数
- 对象在构造时分配内存并初始化成员
- 构造函数中可以通过
this(参数)调用同一个类的另一个构造函数,必须写在首行。
java
class Book {
String title;
double price;
Book() {
this("Unknown", 0.0); // 调用另一个构造函数
}
Book(String title, double price) {
this.title = title;
this.price = price;
}
}
- 子类不会继承父类构造函数,但可通过
super()调用。调用父类构造,若不写则默认调用父类无参构造
重载
Java 允许在一个类中定义多个构造函数,只要它们的 参数列表不同(即重载)。
java
class Student {
String name;
int age;
Student() {
System.out.println("无参构造");
}
Student(String name) {
this.name = name;
}
Student(String name, int age) {
this.name = name;
this.age = age;
}
}
java
new Student();
new Student("Tom");
new Student("Lucy", 18);
⭐默认构造函数
如果 类中没有显式定义任何构造函数 ,编译器会自动生成一个 无参的默认构造函数,修饰符默认与类的修饰符相同。因此在类中,至少会存在一个构造器。
java
class Animal {
String name;
}
编译器会自动补上:
java
Animal() { }
🔸 但如果自己定义了任何构造函数 ,编译器就不会自动生成默认构造函数。
java
class Animal {
Animal(String name) { } // 定义了构造函数
}
// 以下语句会报错
Animal a = new Animal(); // ❌ 没有无参构造可用
私有构造方法
如果我们将构造函数声明为 private:
java
public class User {
private String name;
private User(String name) {
this.name = name;
}
}
那么这个类就不能在外部通过 new User("Tom") 创建对象了。这种设计的目的通常是:
- 控制对象的创建方式;
- 防止外部随意实例化对象;
- 在类内部实现某种"受控实例化逻辑"。
☑️常用来实现单例模式:
单例模式是一种常用的设计模式,它确保某个类在程序运行期间只有一个实例 ,并且提供一个全局访问点。
这种设计在以下场景非常有用:
- 日志系统(Logger)
- 数据库连接池(Database Connection Pool)
- 配置管理类(Configuration Manager)
- 缓存或线程池管理类等
为了保证一个类只能有一个实例,我们必须:
- 禁止外部直接创建对象 → 使用私有构造函数;
- 在类内部创建唯一的实例;
- 通过公共静态方法提供访问入口。
java
public class Singleton {
// 1. 定义一个静态私有实例变量
private static Singleton instance;
// 2. 私有构造函数,防止外部直接实例化
private Singleton() {
System.out.println("单例对象被创建");
}
// 3. 提供一个公共静态方法获取实例
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 只有第一次调用时才创建实例
}
return instance;
}
}
java
public class Main {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // 输出 true
}
}
未完待续!!!!