Java 基础-面向对象思想知识点详解

0 阅读16分钟

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 做了这些事情:

  1. 加载字节码文件:JVM 把 Person.class 加载到方法区
  2. 创建引用:在栈内存中创建一个 zhangSan 变量
  3. 开辟堆内存:在堆内存中开辟空间,分配默认值(String 是 null,int 是 0,boolean 是 false)
  4. 关联地址:把堆内存的地址赋值给栈中的引用变量
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 注意事项

  1. 静态不能访问非静态:因为静态成员随类存在,非静态成员随对象存在,不知道哪个对象先存在。
  2. 静态方法中不能使用 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 面向对象的三大特性:封装、继承、多态

  • 封装:把细节藏起来,暴露必要的接口
  • 继承:子类复用父类的代码
  • 多态:同一个方法调用,不同对象有不同表现

记住一句话:类是图纸,对象是按照图纸造出来的东西。理解了这句话,后面的概念就都好懂了。

希望这篇笔记对你有帮助!该篇文章是基础介绍,下一篇会详解工作中使用的地方