Java面向对象基础:类、对象、属性与内存分析
引言
学完变量、控制流、数组和方法之后,我们已经能用Java写出不少"能跑"的程序。但代码一多,就会发现问题:所有逻辑都堆在main里,函数和函数之间靠参数传来传去,数据和行为被强行割裂。
面向对象(Object-Oriented,OO)就是用来解决这个痛点的。它把"数据"和"操作数据的方法"打包成一个整体,让代码组织更贴近现实世界的结构。本篇博客面向Java初学者,带你从面向过程平滑过渡到面向对象。
一、面向过程 vs 面向对象
1.1 两种思想的对比
面向过程关注的是"怎么做":把一个大问题拆成若干步骤,每一步用一个函数实现,按顺序调用即可。
面向对象关注的是"谁来做":先分析问题里有哪些"角色"(对象),每个角色有哪些属性和行为,再让它们协作完成任务。
经典例子:把大象装进冰箱。
- 面向过程的写法:打开门 → 装大象 → 关上门。
- 面向对象的写法:先设计
Person、Fridge、Elephant三个类,让人去操作冰箱和大象。
java
public class Main {
public static void main(String[] args) {
Person p = new Person("张三");
Fridge f = new Fridge();
Elephant e = new Elephant();
p.pull(f); // 人拉开冰箱
p.pull(e); // 人拉大象过来
p.push(e); // 人把大象推进去
p.push(f); // 人关上冰箱
}
}
1.2 何时用哪种思想?
面向过程执行效率高、思路直接,适合小型脚本、单片机程序等场景。面向对象更适合大型软件:模块清晰、易于维护和扩展。Java是纯面向对象语言,一切代码都必须写在类里。
小贴士:面向对象并非"高级"于面向过程,二者是不同维度的思想。Java中依然大量使用方法(过程),只是这些方法被组织到了类和对象里。
二、面向对象的三大特征
面向对象有三大核心特征,本篇只做简介,后续博客会逐一深入。
1. 封装(Encapsulation):把属性和行为封装进对象,隐藏内部实现细节,只暴露必要的接口。就像汽车,你只需要知道踩油门能加速,不需要懂发动机内部结构。
2. 继承(Inheritance) :子类可以复用父类的属性和方法,避免重复造轮子。比如Dog继承Animal,自动拥有"吃""睡"等行为。
3. 多态(Polymorphism) :同一个方法调用,不同对象表现出不同行为。比如同样调用animal.cry(),狗叫"汪汪",猫叫"喵喵"。
理解了三大特征的整体轮廓后,我们就能从最基础的"类与对象"开始动手了。
三、类与对象的概念
3.1 生活化比喻
类(Class)是设计图,对象(Object)是按图生产的产品。
| 类 | 对象 |
|---|---|
| 楼房的设计图纸 | 一栋真正盖起来的楼房 |
| 汽车的车型设计 | 一辆停在楼下的真车 |
| 学生类的描述 | 张三这个具体的学生 |
一张图纸可以盖出很多栋楼,但每栋楼都有自己的门牌号、住户、装修。同样,一个类可以创建多个对象,每个对象的属性值各自独立。
3.2 概念定义
- 类 :是一类具有共同特征 的事物的抽象描述。
- 对象:是这类事物的一个具体的个体、实体、实例。
以"学生类"为例:
- 属性:姓名、学号、性别、专业、成绩......
- 行为:注册、学习、考试......
类只描述"有哪些属性和行为",具体值要由对象来确定。
四、声明类的格式
4.1 基本语法
java
【修饰符】 class 类名 {
// 成员变量(属性)
// 成员方法(行为)
}
- 修饰符 :可以是
public、final、abstract等,初学阶段先用public。 - 类名 :使用大驼峰命名法(每个单词首字母大写),如
HelloWorld、Student。 - 一份
.java文件中只能有一个public类,且类名要和文件名一致。
4.2 第一个自定义类
java
package com.atguigu.oop;
public class Student {
public String name; // 姓名
public int age; // 年龄
public String tel; // 电话
public int score; // 成绩
}
注意 :
package语句声明了类所属的包(即文件夹),用于避免类名冲突并组织代码。包名习惯用公司域名倒置,例如com.atguigu.xxx。
五、创建对象:new关键字
5.1 语法格式
java
类名 对象名 = new 类名(); // 无参
类名 对象名 = new 类名(实参列表); // 有参(需配合构造方法)
new关键字做了三件事:在堆中开辟内存、初始化属性默认值、返回对象的首地址。对象名本质是一个引用变量,存的是对象在堆中的地址。
5.2 命名对象与匿名对象
java
public class CreateObjectDemo {
public static void main(String[] args) {
// 命名对象:用变量接收引用,可重复使用
Student s = new Student();
System.out.println(s); // com.atguigu.oop.Student@34c45dca
// 匿名对象:不赋给任何变量,用一次即丢
System.out.println(new Student());
}
}
5.3 匿名对象的使用场景
匿名对象没有名字,创建后无法再次访问,常见于以下场景:
- 只调用一次方法 :
new Scanner(System.in).nextInt(); - 作为实参传递 :
printInfo(new Student()); - 作为方法的返回值 :
return new MyDate();
小贴士 :如果同一个对象要被使用多次,必须用命名对象。匿名对象每
new一次就开辟一块新内存,滥用会浪费资源。
六、类的成员之一:属性(成员变量)
6.1 声明格式
java
【修饰符】 class 类名 {
【修饰符】 数据类型 成员变量名;
}
成员变量也叫属性、字段。它写在类里、方法外。
6.2 访问方式
java
对象名.属性
6.3 完整示例
java
public class StudentTest {
public static void main(String[] args) {
Student s1 = new Student();
// 赋值前:输出默认值
System.out.println(s1.name); // null
System.out.println(s1.age); // 0
// 赋值
s1.name = "张三";
s1.age = 23;
// 赋值后
System.out.println(s1.name); // 张三
System.out.println(s1.age); // 23
// 新建一个对象,属性值独立
Student s2 = new Student();
System.out.println(s2.name); // null
}
}
小贴士:成员变量有默认值(整数0、浮点0.0、布尔false、字符'\u0000'、引用null),无需手动初始化即可使用。这是和局部变量最大的不同。
七、成员变量分类:静态 vs 非静态
7.1 何时加static?
判断原则非常简单:
- 如果属性的值是每个对象独立的 → 不加static → 实例变量。
- 如果属性的值是所有对象共享的 → 加static → 静态变量(类变量)。
7.2 代码对比
java
public class Account {
public static double rate; // 利率:所有人共享 → 静态变量
public double balance; // 余额:每人独立 → 实例变量
public String id; // 账号:每人独立 → 实例变量
}
class AccountTest {
public static void main(String[] args) {
Account a1 = new Account();
a1.id = "111";
a1.balance = 5000;
Account a2 = new Account();
a2.id = "222";
a2.balance = 10000;
// 静态变量推荐用"类名."访问
Account.rate = 0.035;
System.out.println(a1.id + "," + a1.balance + "," + a1.rate);
System.out.println(a2.id + "," + a2.balance + "," + a2.rate);
}
}
注意 :
a1.rate = 0.035语法上也合法,但不推荐。因为静态变量属于类而非某个对象,用类名.静态变量才能体现共享语义。
7.3 详细对比表
| 对比维度 | 静态成员变量(类变量) | 非静态成员变量(实例变量) |
|---|---|---|
是否加static |
是 | 否 |
| 共享性 | 所有对象共享同一份 | 每个对象各自独立一份 |
| 内存存储位置 | JDK6及以前:方法区;JDK7+:堆(Class对象的反射字段);JDK8+:堆 | 堆(随对象一起) |
| 初始化时机 | 类加载时初始化 | new对象时初始化 |
| 访问方式 | 推荐类名.变量,也可对象名.变量 |
只能对象名.变量 |
| 生命周期 | 类存在即存在,类卸载才销毁 | 对象创建时存在,对象被GC回收时销毁 |
| 默认值 | 有默认值 | 有默认值 |
JDK版本变化小贴士:
- JDK6及之前:静态变量存储在方法区(永久代)。
- JDK7:静态变量的引用从方法区移到了堆中。
- JDK8+:永久代被废除,方法区改名为元空间(Metaspace),使用本地内存;静态变量随Class对象存放在堆中。
八、类的成员之二:方法
8.1 声明格式
java
【修饰符】 class 类名 {
【修饰符】 static 返回值类型 方法名(【形参列表】) { // 静态方法
方法体语句;
}
【修饰符】 返回值类型 方法名(【形参列表】) { // 实例方法
方法体语句;
}
}
8.2 静态方法 vs 实例方法
判断原则:
- 方法体中需要访问本类的实例变量 → 必须是实例方法。
- 方法体中不访问本类的实例变量 → 可以定义为静态方法,调用更方便(无需new对象)。
工具类的方法大多是静态的,比如Math.random()、Arrays.sort(),调用时直接类名.方法名即可。
8.3 跨类调用
java
类名.静态方法(); // 静态方法
对象名.实例方法(); // 实例方法
8.4 为什么要定义方法?
为了让代码可复用。下面的矩形类把"求面积""求周长""获取信息"分别封装成方法,每次需要时直接调用即可。
java
public class Rectangle {
public double length;
public double width;
// 求面积:实例方法,直接使用实例变量
public double area() {
return length * width;
}
// 求周长
public double perimeter() {
return 2 * (length + width);
}
// 获取信息:本类内部方法之间可以互相调用
public String getInfo() {
return "长:" + length + ",宽:" + width
+ ",面积:" + area() + ",周长:" + perimeter();
}
}
class RectangleTest {
public static void main(String[] args) {
Rectangle r1 = new Rectangle();
r1.length = 5;
r1.width = 3;
System.out.println("r1的" + r1.getInfo());
}
}
8.5 同类中成员的互相访问
在同一类中:
- 静态方法 :只能访问本类的静态变量和静态方法,不能直接访问实例变量和实例方法。
- 实例方法:既能访问静态成员,也能访问实例成员。
小贴士:因为静态方法不依赖对象存在,而实例变量必须依附于对象。静态方法被调用时可能还没有任何对象,所以它无法直接使用实例变量。
九、static关键字详解
static可以修饰成员变量、成员方法、代码块、内部类。它修饰的成员属于"类级别",不属于某个对象。
核心特点:
- 随着类的加载而加载,生命周期最长。
- 优先于对象存在,可以不new对象就直接通过类名访问。
- 所有对象共享同一份静态成员。
- 可以直接用类名调用 :
类名.静态变量、类名.静态方法()。
java
public class Chinese {
public static String countryName; // 国名:所有中国人共享
public String name; // 姓名:每人独立
public int age; // 年龄:每人独立
}
class ChineseTest {
public static void main(String[] args) {
Chinese.countryName = "中国"; // 类名访问静态变量
Chinese c1 = new Chinese();
c1.name = "李涛";
c1.age = 25;
Chinese c2 = new Chinese();
c2.name = "王芳";
c2.age = 22;
System.out.println(c1.name + "," + c1.countryName); // 李涛,中国
System.out.println(c2.name + "," + c2.countryName); // 王芳,中国
}
}
十、this与super简介
本篇源笔记未深入讲解this和super,但它们在面向对象中至关重要,这里做简要介绍,后续博客会展开。
this:代表"当前对象"的引用。当实例方法的局部变量与成员变量同名时,用this.变量名区分;构造方法间互相调用也用this(...)。super:代表"父类对象"的引用,用于在子类中访问父类的成员,常用于方法重写后调用父类原版方法。
java
public class Student {
private String name;
public void setName(String name) {
this.name = name; // this.name指成员变量,name指参数
}
}
注意 :
this和super都是对象级别的引用,不能在静态方法中使用。
十一、对象的内存分析
理解对象在内存中如何存放,是真正掌握面向对象的关键。Java虚拟机内存主要涉及三块区域:
11.1 三大内存区域
- 栈(Stack):存放方法的栈帧,方法内的局部变量、对象的引用(地址)都存在这里。方法执行完即出栈。
- 堆(Heap) :存放
new出来的对象本体,对象的成员变量(实例变量)也在这里。由GC自动回收。 - 方法区/元空间(Method Area / Metaspace):存放类的信息(Class对象)、静态变量、常量、方法的字节码等。JDK8后改名为元空间,使用本地内存。
11.2 new对象的过程
执行Student s = new Student();时,发生了什么?
- 类加载:JVM检测到
Student类,把它的Class信息加载到方法区。 - 堆中开辟空间:在堆中为对象分配内存。
- 默认初始化:实例变量被赋默认值(int→0,引用→null等)。
- 显式初始化:执行成员变量的显式赋值语句。
- 构造方法执行:执行构造方法体(如果有的话)。
- 返回地址:把对象在堆中的首地址赋给栈中的引用变量
s。
11.3 内存图示意
以如下代码为例:
java
Student s1 = new Student();
s1.name = "张三";
s1.age = 23;
Student s2 = new Student();
s2.name = "李四";
栈 堆 方法区/元空间
+-----------------+ +---------------------+ +------------------+
| main栈帧 | | Student对象 @a1 | | Student类信息 |
| s1 = @a1 | -----> | name = "张三" | | 字节码 |
| s2 = @a2 | | age = 23 | | 静态变量 |
+-----------------+ | tel = null | +------------------+
| score= 0 |
+---------------------+
| Student对象 @a2 |
| name = "李四" |
| age = 0 |
| ... |
+---------------------+
可以看到:每个对象在堆中独立占用一块内存,但它们都指向方法区里同一份类信息。多个对象通过各自的引用变量区分。
11.4 引用类型成员变量的内存分析
当成员变量是引用类型时,对象内部存的是另一个对象的地址,形成"对象套对象"的结构。
java
public class Employee {
public String name;
public MyDate birthday; // 引用类型成员变量
}
class EmployeeTest {
public static void main(String[] args) {
Employee e = new Employee();
e.name = "张三";
e.birthday = new MyDate(); // 堆中再开一块MyDate对象
e.birthday.year = 2000; // 通过引用逐级访问
e.birthday.month = 5;
e.birthday.day = 1;
}
}
小贴士 :
e.birthday.year的访问路径是栈中e → 堆中Employee对象 → 堆中MyDate对象 → year。任何一环为null都会抛NullPointerException。
十二、成员变量与局部变量的区别
这是初学者最容易混淆的点,下面用表格梳理。
| 对比维度 | 成员变量 | 局部变量 |
|---|---|---|
| 声明位置 | 类中、方法外 | 方法中、方法的形参、代码块中 |
| 作用域 | 整个类 | 从声明处到所在{}结束 |
| 默认值 | 有默认值,可直接使用 | 无默认值,必须先赋值再使用 |
| 存储位置 | 实例变量在堆中;静态变量在堆中(JDK8+) | 栈中(方法栈帧内) |
| 生命周期 | 实例变量随对象生灭;静态变量随类生灭 | 随方法调用而生,方法结束即亡 |
| 修饰符 | 可用public/private/static/final等 |
只能用final,不能用访问修饰符和static |
java
public class Demo {
public int memberVar = 10; // 成员变量,有默认值
public void test() {
int localVar; // 局部变量,无默认值
// System.out.println(localVar); // 编译报错:可能尚未初始化
localVar = 20;
System.out.println(localVar);
}
}
小贴士 :如果局部变量和成员变量同名,方法内优先使用局部变量(就近原则)。若要访问被遮蔽的成员变量,需用
this.变量名。
十三、基本类型 vs 引用类型作为成员变量
Java的数据类型分两大类:
- 8种基本类型 :
byte、short、int、long、float、double、char、boolean。 - 引用类型:数组、类、接口等。
成员变量无论是基本类型还是引用类型,都存在堆中。但存储内容不同:
- 基本类型成员变量:直接存值。
- 引用类型成员变量:存的是另一个对象的地址(引用)。
13.1 引用类型成员变量示例
java
public class MyDate {
public int year;
public int month;
public int day;
public String getDateInfo() {
return year + "年" + month + "月" + day + "日";
}
}
public class Employee {
public String name; // String是引用类型
public MyDate birthday; // 自定义类也是引用类型
public String getEmployeeInfo() {
return "姓名:" + name + ",生日:" + birthday.getDateInfo();
}
}
class EmployeeTest {
public static void main(String[] args) {
Employee e = new Employee();
e.name = "张三";
e.birthday = new MyDate();
e.birthday.year = 2000;
e.birthday.month = 5;
e.birthday.day = 1;
System.out.println(e.getEmployeeInfo());
// 姓名:张三,生日:2000年5月1日
}
}
13.2 链式访问
引用类型成员变量可以一直"点"下去:
java
e.birthday.year // 逐级解引用
每一级.都是一次从引用到对象的跳转。这就是面向对象程序中"对象协作"的体现:一个对象内部包含另一个对象,多个对象组合成复杂结构。
注意 :自己写的类也是数据类型,可以像
int、String一样作为成员变量、方法参数、返回值。这是面向对象编程的核心能力。
十四、匿名对象再深入
14.1 什么是匿名对象
java
new Student(); // 这就是一个匿名对象,没有变量名接收
它有对象实体的内存,但没有引用变量指向它,只能使用一次。
14.2 典型使用场景
场景1:只调用一次方法
java
new Scanner(System.in).nextInt();
调用完毕,Scanner对象立即成为垃圾,等待GC回收。
场景2:作为方法实参
java
printInfo(new Student());
public static void printInfo(Student s) {
System.out.println(s.name);
}
场景3:作为方法返回值
java
public static MyDate createToday() {
return new MyDate(); // 返回匿名对象
}
14.3 注意事项
java
new Student().name = "张三"; // 赋值成功,但对象立刻丢弃,无意义
new Student().study(); // 调用方法OK
new Student().study(); // 又new了一个新对象,与上一行无关
小贴士:匿名对象一旦不使用就成垃圾。需要多次访问同一对象时,必须用命名对象接收引用。
十五、小结
本篇博客带大家从思想到代码、从语法到内存,建立了面向对象的基础认知。回顾要点:
- 思想转变:从"怎么做"(面向过程)转向"谁来做"(面向对象)。
- 三大特征:封装、继承、多态是面向对象的根基。
- 类与对象 :类是模板,对象是实例;用
new创建对象。 - 属性:成员变量有默认值;区分静态变量(共享)和实例变量(独立)。
- 方法:静态方法属于类,实例方法属于对象;同类中静态不能直接访问实例成员。
- static:类级别成员,生命周期长,所有对象共享。
- 内存分析 :栈存引用、堆存对象、方法区存类信息;理解
new的全过程。 - 变量对比:成员变量vs局部变量、基本类型vs引用类型成员变量,差别巨大。
- 匿名对象:用于一次性场景,避免冗余引用。
掌握了这些基础,下一步就可以进入封装(private + getter/setter) 、构造方法 、this关键字的深入学习了。面向对象的大门已经打开,欢迎继续探索。
写完这篇博客,建议动手敲一遍所有示例代码,并自己画一画内存图。代码会忘,但内存图一旦画明白,面向对象就再也不会忘。