Java 面向对象核心语法:从入门到理解
这篇文章是我学习 Java 面向对象时的笔记整理,适合初学者和对 OOP 概念模糊的同学阅读。
一、编程思想:面向对象到底是什么?
刚学编程的时候,总听说"面向对象"这个词,但一直不太理解它和"面向过程"有什么区别。
1.1 面向对象 vs 面向过程
面向过程就像点外卖:重点是"步骤"——先下单,再付款,再等外卖员送过来。整个过程关注的是谁做了什么动作。
面向对象就像雇保镖:你不关心保镖是怎么一招制敌的,你只关心谁能帮你解决问题。重点是找到一个有能力的对象,让它替你干活,有了对象后,就可以和对象进行互动了。
Java 就是一门面向对象的语言,它有三大核心特性:封装、继承、多态。这三个特性贯穿整个 Java 面向对象编程,也是面试中经常被问到的内容。
二、类和对象:Java 世界的基本单位
2.1 什么是类?什么是对象?
- 类:就像图纸或者模具。它定义了"这类东西应该有什么特征(属性)和能做什么事(行为)",但它本身不是真实存在的。
- 对象:就是根据图纸真实造出来的东西,实实在在存在,可以拿来用。
举个例子:
// 定义一个"人类" - 这就是一张图纸
class Person {
// 属性:姓名、年龄、性别
String name;
int age;
String gender;
// 行为:吃饭、睡觉、学习
void eat() {
System.out.println(name + "正在吃饭");
}
void sleep() {
System.out.println(name + "正在睡觉");
}
void study() {
System.out.println(name + "正在学习");
}
}
根据这张图纸创建几个对象:
public class Main {
public static void main(String[] args) {
// 创建对象:类名 对象名 = new 类名();
Person zhangSan = new Person();
Person liSi = new Person();
// 给对象的属性赋值
zhangSan.name = "张三";
zhangSan.age = 20;
zhangSan.gender = "男";
liSi.name = "李四";
liSi.age = 22;
liSi.gender = "女";
// 调用对象的方法
zhangSan.eat(); // 输出:张三正在吃饭
liSi.study(); // 输出:李四正在学习
}
}
类就像手机的设计图,对象就是按照设计图生产出来的真实手机。设计图上有"屏幕大小"、"电池容量",生产出来的手机有具体的"6.7寸屏幕"、"5000mAh电池"。
2.2 创建对象的步骤(内存角度)
理解内存很重要。当你执行 new Person() 时,JVM 做了这些事情:
- 加载字节码文件:JVM 把
Person.class加载到方法区 - 创建引用:在栈内存中创建一个
zhangSan变量 - 开辟堆内存:在堆内存中开辟空间,分配默认值(String 是 null,int 是 0,boolean 是 false)
- 关联地址:把堆内存的地址赋值给栈中的引用变量
Person zhangSan = new Person();
// 执行流程:
// 1. 方法区加载 Person.class
// 2. 栈中创建 zhangSan 引用
// 3. 堆中开辟空间,成员变量默认初始化
// 4. 堆地址赋值给 zhangSan 引用
2.3 成员变量 vs 局部变量
| 对比维度 | 成员变量 | 局部变量 |
|---|---|---|
| 定义位置 | 类中,方法外 | 方法内部 |
| 内存位置 | 堆内存(跟着对象) | 栈内存(跟着方法) |
| 生命周期 | 对象创建存在,对象销毁消失 | 方法进栈创建,方法出栈消失 |
| 默认值 | 有默认值(0、null等) | 没有默认值,必须先赋值 |
class Demo {
String name = "你好"; // 成员变量,有默认值
void test() {
int age; // 局部变量,没有默认值
// System.out.println(age); // 编译报错!必须先赋值
age = 10;
System.out.println(age); // 正常运行
}
}
成员变量像是房子的固定资产(跟着房子走),局部变量像是临时工(方法结束就下班了)。
2.4 匿名对象
匿名对象就是"不留名字的对象",用完就丢。
// 普通写法
Person p = new Person();
p.eat();
// 匿名对象写法
new Person().eat(); // 用完即弃,没有名字
使用场景:
- 当只需要调用一次方法时
- 作为方法参数传递
// 作为参数传递
void doSomething(Person p) {
p.eat();
}
// 调用时直接传匿名对象
doSomething(new Person());
三、封装:给代码加把锁
3.1 什么是封装?
封装就是:把细节藏起来,只暴露该暴露的。
就像手机,你只需要知道屏幕、按键怎么用就行,不需要知道里面电路是怎么连接的。封装的好处:
- 隐藏内部实现细节
- 提高代码复用性
- 提高安全性
3.2 private 关键字:最基础的封装
private 是 Java 提供的"锁",被它修饰的内容只能在本类中访问。
class Person {
private String name; // 姓名被保护起来了
private int age; // 年龄也被保护起来了
// 提供公开的访问方式
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
if (age > 0 && age < 150) { // 安全校验
this.age = age;
}
}
public int getAge() {
return age;
}
}
为什么要这样写?如果直接把 age 暴露出去,别人可能设置成 -100 岁,这显然不合理。但通过 setAge() 方法,可以加上校验逻辑,保证数据的合理性。
3.3 this 关键字
this 就是"当前对象",谁调用方法,谁就是 this。主要用来区分成员变量和局部变量。
class Person {
private String name;
public void setName(String name) {
// 参数 name 和成员变量 name 重名了,用 this.name 区分
this.name = name;
}
}
3.4 构造方法
构造方法比较特殊,有以下几个特点:
class Person {
String name;
// 无参构造
public Person() {
System.out.println("无参构造被调用了!");
}
// 有参构造
public Person(String name) {
this.name = name;
System.out.println("有参构造被调用了!");
}
}
特点:
- 方法名必须和类名相同
- 没有返回值类型(注意不是 void)
- 不能手动调用,只能
new的时候自动调用 - 可以重载
public class Main {
public static void main(String[] args) {
Person p1 = new Person(); // 调用无参构造
Person p2 = new Person("张三"); // 调用有参构造
}
}
赋值顺序:默认初始化 → 显示初始化 → 构造代码块 → 构造方法
3.5 set 方法和构造方法的区别
| 对比 | 构造方法 | set 方法 |
|---|---|---|
| 调用时机 | 创建对象时 | 对象创建之后 |
| 调用次数 | 只能调用一次 | 可以调用无数次 |
| 作用 | 对象初始化 | 修改/设置属性值 |
// 构造方法:创建时赋值
Person p = new Person("张三", 20);
// set 方法:后续修改
p.setAge(21);
p.setName("张三丰");
四、static 关键字:属于类的变量
4.1 static 的特点
被 static 修饰的成员属于类本身,而不是某个对象。所有对象共享同一份数据。
class Chinese {
String name; // 每个对象独立的
static String country = "中国"; // 所有对象共享的
public Chinese(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Chinese c1 = new Chinese("张三");
Chinese c2 = new Chinese("李四");
System.out.println(c1.country); // 中国
System.out.println(c2.country); // 中国
// 推荐用类名访问静态成员
System.out.println(Chinese.country); // 中国
}
}
应用场景:计数器、工具类方法(如 Math、Arrays 类)等。
计数器示例:
class Student {
String name;
static int count = 0; // 记录创建了多少个学生对象
public Student(String name) {
this.name = name;
count++; // 每创建一个对象,计数器+1
}
}
public class Main {
public static void main(String[] args) {
Student s1 = new Student("张三");
Student s2 = new Student("李四");
Student s3 = new Student("王五");
System.out.println("学生总数:" + Student.count); // 输出:3
}
}
4.2 static 注意事项
- 静态不能访问非静态:因为静态成员随类存在,非静态成员随对象存在,不知道哪个对象先存在。
- 静态方法中不能使用 this:理由同上。
class Demo {
int num = 10; // 非静态
static int staticNum = 20; // 静态
// 静态方法
public static void test() {
// System.out.println(num); // 报错!静态不能访问非静态
System.out.println(staticNum); // 可以访问静态
}
}
4.3 Arrays 工具类
Java 官方提供了 Arrays 类,专门用来操作数组:
import java.util.Arrays;
public class ArraysDemo {
public static void main(String[] args) {
int[] arr = {5, 3, 8, 1, 2};
// 1. toString:打印数组
System.out.println(Arrays.toString(arr)); // [5, 3, 8, 1, 2]
// 2. sort:排序
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 5, 8]
// 3. binarySearch:二分查找(使用前必须先排序)
int index = Arrays.binarySearch(arr, 5);
System.out.println("5 的索引是:" + index); // 3
// 4. copyOf:拷贝数组
int[] newArr = Arrays.copyOf(arr, 10);
System.out.println(Arrays.toString(newArr)); // [1, 2, 3, 5, 8, 0, 0, 0, 0, 0]
// 5. fill:填充数组
int[] filled = new int[5];
Arrays.fill(filled, 666);
System.out.println(Arrays.toString(filled)); // [666, 666, 666, 666, 666]
}
}
五、继承:子承父业
5.1 什么是继承?
继承就是"子承父业"——子类继承父类的属性和方法。
// 父类:动物
class Animal {
String name;
public void eat() {
System.out.println("动物在吃东西");
}
}
// 子类:狗
class Dog extends Animal {
// 自动继承了 name 属性和 eat() 方法
public void bark() {
System.out.println("狗在汪汪叫");
}
}
public class Main {
public static void main(String[] args) {
Dog d = new Dog();
d.name = "旺财";
d.eat(); // 继承来的方法
d.bark(); // 自己特有的方法
}
}
好处:
- 提高代码复用性
- 提高代码可维护性
注意:
- 私有成员不能被继承
- 构造方法不能被继承
- 要符合"子类 is a 父类"的逻辑(比如 Dog is an Animal,Cat is an Animal,这是合理的;但 Dog is a Cat 就不对了)
- Java 只支持单继承(一个子类只能有一个直接父类),但可以多层继承
5.2 this 和 super
this:指向当前对象自己super:指向当前对象的父类
class Animal {
String name = "动物";
public void eat() {
System.out.println("动物在吃东西");
}
}
class Dog extends Animal {
String name = "狗";
public void test() {
System.out.println(this.name); // 狗
System.out.println(super.name); // 动物
}
public void eat() {
System.out.println("狗在吃狗粮");
}
public void showEat() {
this.eat(); // 调用自己的 eat
super.eat(); // 调用父类的 eat
}
}
在构造方法中使用 this 和 super:
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
public Dog(String name) {
super(name); // 调用父类的构造方法,必须写在第一行
}
}
注意:this() 和 super() 必须写在构造方法的第一行,且不能同时存在。
5.3 方法重写 vs 方法重载
| 区别 | 重载(Overload) | 重写(Override) |
|---|---|---|
| 发生位置 | 同一个类中 | 父子类中 |
| 方法名 | 必须相同 | 必须相同 |
| 参数列表 | 必须不同 | 必须相同 |
| 返回值 | 无关 | 必须相同或兼容 |
// 重载:同一个类中
class MyClass {
void test() {}
void test(int a) {} // 参数不同
void test(String s) {} // 参数不同
}
// 重写:父子类中
class Animal {
public void eat() {
System.out.println("动物在吃东西");
}
}
class Dog extends Animal {
@Override // 加上这个注解可以让编译器帮我们检查是否真的重写了
public void eat() { // 子类重写父类方法
System.out.println("狗在吃狗粮");
}
}
重写的注意事项:
- 方法名、参数列表必须完全相同
- 访问权限不能变小(public > protected > 默认 > private)
- 静态方法不能被重写
- 私有方法不能被重写
六、代码块:大括号里的秘密
6.1 四种代码块
class Demo {
// 1. 局部代码块 - 在方法里
void method() {
{
int x = 10; // 限制变量生命周期
System.out.println(x);
}
// x 在这里就不能用了
}
// 2. 构造代码块 - 在类中方法外
{
System.out.println("构造代码块 - 创建对象时自动执行");
}
// 3. 静态代码块 - 在类中方法外,加了 static
static {
System.out.println("静态代码块 - 类加载时执行,只执行一次");
}
// 构造方法
public Demo() {
System.out.println("构造方法");
}
}
执行顺序:静态代码块 → 构造代码块 → 构造方法
public class Main {
public static void main(String[] args) {
Demo d1 = new Demo();
Demo d2 = new Demo();
}
}
// 输出:
// 静态代码块 - 类加载时执行,只执行一次
// 构造代码块 - 创建对象时自动执行
// 构造方法
// 构造代码块 - 创建对象时自动执行
// 构造方法
应用场景:静态代码块常用于初始化静态资源,比如加载配置文件、连接数据库等。
七、final 关键字:不可改变
final 的意思是"最终的",被它修饰的内容不能被改变。
// 1. 修饰类 - 不能被继承
final class Constant {
// 2. 修饰变量 - 变成常量(建议大写)
static final double PI = 3.14159;
static final int MAX_VALUE = 100;
// 3. 修饰方法 - 不能被重写
public final void print() {
System.out.println("这是一个最终方法");
}
}
// class MyClass extends Constant {} // 报错!Constant 被 final 修饰,不能被继承
final 修饰变量的细节:
class Demo {
// 情况1:修饰成员变量 - 必须在定义时或构造方法中赋值
final int num1 = 10;
final int num2;
public Demo() {
num2 = 20; // 构造方法中赋值
}
// 情况2:修饰局部变量 - 在使用前赋值即可
public void test() {
final int x;
x = 100; // 使用前赋值
// x = 200; // 报错!不能再次赋值
}
// 情况3:修饰引用类型 - 引用地址不能变,但对象内容可以变
public void test2() {
final Person p = new Person();
p.name = "张三"; // 可以修改对象内容
// p = new Person(); // 报错!不能修改引用地址
}
}
八、内部类:类里面的类
8.1 为什么要内部类?
内部类就是"定义在类里面的类"。有时候一个类只会被另一个类使用,就没必要单独写一个文件了。
8.2 成员内部类
class Outer {
int num = 10;
// 成员内部类
class Inner {
int num = 20;
public void test() {
System.out.println(num); // 20,就近原则
System.out.println(this.num); // 20,Inner 的
System.out.println(Outer.this.num); // 10,Outer 的
}
}
}
public class Main {
public static void main(String[] args) {
// 创建内部类对象
Outer.Inner inner = new Outer().new Inner();
inner.test();
}
}
8.3 私有成员内部类
class Outer {
// 私有成员内部类
private class Inner {
public void show() {
System.out.println("私有内部类的方法");
}
}
// 外部类通过方法间接访问内部类
public void test() {
Inner inner = new Inner();
inner.show();
}
}
8.4 静态成员内部类
class Outer {
static int num = 10;
int num2 = 20;
// 静态成员内部类
static class Inner {
public void test() {
System.out.println(num); // 可以访问外部类的静态成员
// System.out.println(num2); // 报错!不能访问非静态成员
}
}
}
public class Main {
public static void main(String[] args) {
// 创建静态内部类对象(不需要 new 外部类)
Outer.Inner inner = new Outer.Inner();
inner.test();
}
}
8.5 匿名内部类
匿名内部类是没有名字的类,通常用于实现接口或继承抽象类。
// 定义一个接口
interface Swimmable {
void swim();
}
public class Main {
public static void main(String[] args) {
// 匿名内部类写法
Swimmable s = new Swimmable() {
@Override
public void swim() {
System.out.println("海豚在游泳!");
}
};
s.swim();
}
}
匿名内部类编译后会生成一个 .class 文件(比如 Main$1.class),它是一个实现了接口的子类对象。
九、多态:同一个行为,不同的表现
9.1 什么是多态?
多态就是"同一个行为,使用不同对象会有不同的表现"。
比如"动物吃东西"这件事:
- 狗吃狗粮
- 猫吃猫粮
- 鱼吃鱼粮
同样是 eat() 方法,不同对象调用结果不同,这就是多态。
9.2 多态的实现
多态的三个条件:继承、方法重写、父类引用指向子类对象
// 父类
abstract class Animal {
abstract void eat();
}
// 子类
class Dog extends Animal {
@Override
void eat() {
System.out.println("狗在吃狗粮");
}
}
class Cat extends Animal {
@Override
void eat() {
System.out.println("猫在吃猫粮");
}
}
public class Main {
public static void main(String[] args) {
// 父类引用指向子类对象
Animal a1 = new Dog(); // 编译看左边,运行看右边
Animal a2 = new Cat();
a1.eat(); // 输出:狗在吃狗粮
a2.eat(); // 输出:猫在吃猫粮
}
}
编译看左边,运行看右边:
- 编译时:看父类有什么方法,没有就报错
- 运行时:看子类怎么实现的,运行子类的方法
多态访问成员变量:
class Animal {
String name = "动物";
}
class Dog extends Animal {
String name = "狗";
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println(a.name); // 输出:动物(成员变量:编译看左边,运行也看左边)
}
}
9.3 instanceof 判断和向下转型
有时候需要判断一个对象到底是什么具体类型:
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
// instanceof 判断
if (a instanceof Dog) {
System.out.println("这是一条狗");
// 向下转型
Dog d = (Dog) a;
d.bark(); // 可以调用 Dog 特有的方法了
} else if (a instanceof Cat) {
System.out.println("这是一只猫");
Cat c = (Cat) a;
c.catchMouse();
}
}
}
多态的优点:
- 提高代码的可扩展性
- 提高代码的可维护性
// 多态的实际应用
class AnimalFeeder {
public void feed(Animal a) {
a.eat(); // 不管传什么动物,都能喂
}
}
public class Main {
public static void main(String[] args) {
AnimalFeeder feeder = new AnimalFeeder();
feeder.feed(new Dog());
feeder.feed(new Cat());
feeder.feed(new Pig()); // 以后加新动物,不用改 feeder 的代码
}
}
十、抽象类:只定义,不实现
10.1 什么是抽象类?
抽象类就是只定义要做什么,但不规定怎么做的类。
比如"动物"这个概念,你说不清动物具体怎么吃东西,因为狗和猫吃的方式不一样。所以"动物"只能作为一个抽象概念存在。
// 抽象类
abstract class Animal {
String name;
// 抽象方法 - 只有声明,没有实现
abstract void eat();
// 非抽象方法 - 可以有具体实现
public void sleep() {
System.out.println("动物在睡觉");
}
}
// 子类必须实现抽象方法
class Dog extends Animal {
@Override
void eat() {
System.out.println("狗在吃狗粮");
}
}
特点:
- 抽象类不能直接
new创建对象 - 抽象方法必须被子类实现(除非子类也是抽象的)
- 抽象类可以有构造方法(供子类调用)
- 抽象类中可以有抽象方法,也可以有普通方法
十一、接口:能力的契约
11.1 什么是接口?
接口就像一份契约,规定"你能做什么",但不关心"你怎么做"。
// 定义接口
interface Flyable {
// 接口中的成员变量默认是 public static final(常量)
int MAX_SPEED = 1000;
// 接口中的成员方法默认是 public abstract
void fly();
}
// 实现接口
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("鸟儿在飞翔");
}
}
public class Main {
public static void main(String[] args) {
Flyable f = new Bird();
f.fly();
}
}
11.2 接口 vs 抽象类
| 对比 | 接口 | 抽象类 |
|---|---|---|
| 成员变量 | 只能是常量 | 可以是变量 |
| 成员方法 | 只能是抽象方法(Java 8 后可以有默认方法) | 可以有抽象方法,也可以有普通方法 |
| 继承/实现 | 类可以多实现接口 | 类只能单继承抽象类 |
| 构造方法 | 没有 | 可以有 |
什么时候用接口?什么时候用抽象类?
- 接口:侧重"能力"(像 Flyable 表示"能飞"的能力,Swimmable 表示"能游泳"的能力)
- 抽象类:侧重"模板"(像 Animal 定义动物的基本框架)
一个类可以同时继承抽象类并实现多个接口:
abstract class Animal {
abstract void eat();
}
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
// 鸭既属于动物,又能飞,又能游泳
class Duck extends Animal implements Flyable, Swimmable {
@Override
public void eat() {
System.out.println("鸭子在吃虫子");
}
@Override
public void fly() {
System.out.println("鸭子在飞");
}
@Override
public void swim() {
System.out.println("鸭子在游泳");
}
}
十二、权限修饰符:访问控制
| 修饰符 | 本类 | 同包 | 子类 | 其他包 |
|---|---|---|---|---|
| private | ✅ | ❌ | ❌ | ❌ |
| 默认(default) | ✅ | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ✅ | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
记忆方法:
private:只给自己用protected:保护子女public:公开给所有人- 默认:只给同包的人用
总结
Java 面向对象的三大特性:封装、继承、多态。
- 封装:把细节藏起来,暴露必要的接口
- 继承:子类复用父类的代码
- 多态:同一个方法调用,不同对象有不同表现
记住一句话:类是图纸,对象是按照图纸造出来的东西。理解了这句话,后面的概念就都好懂了。
希望这篇笔记对你有帮助!该篇文章是基础介绍,下一篇会详解工作中使用的地方