Java 类与对象:从入门到“少踩坑”一篇就够

一、最本质:面向过程 vs 面向对象

以Java跟C为例,C++其实就是在C的基础上引入了面向对象的东西,与Java有很多相似之处

  • 面向过程
    想解决问题,就把流程拆成一个个步骤,按顺序执行。代码像流水线:一步一步“做事情”。
  • 面向对象
    不再围着“过程”打转,而是先找出有哪些对象,每个对象负责什么事,然后让对象们互相配合解决问题。

一句话版:

面向过程:亲自干活。
面向对象:先设计工人(对象),然后让工人干活。


二、类和对象:设计图 vs 房子

1. 类:一份“说明书”

在 Java 里,类(class)是对象的设计图,描述了某一类东西有什么属性、能做什么事:

class PetDog {
    // 成员变量 = 属性
    public String name;
    public String color;

    // 成员方法 = 行为
    public void bark() {
        System.out.println(name + " 汪汪叫");
    }
}

关键点:

  • 成员变量:描述对象“是什么”。
  • 成员方法:描述对象“能做什么”。

2. 对象:真正“活起来”的东西

new 创建对象:

PetDog dog = new PetDog();
dog.name = "阿黄";
dog.color = "黑黄";
dog.bark();

要记住:

  • 同一个类可以 new 出很多个对象,每个对象有自己的一份成员变量
  • 类本身不存储具体数据,只是模板;对象才真正占内存。

容易混淆的点

  • PetDog 是类型,dog 才是对象变量。
  • 多个对象之间的成员变量互不影响:改了 dog1.name 不会影响 dog2.name

三、this:对象在方法里的“自我引用”

1. 为什么要有 this

典型写法:

public class Date {
    public int year;
    public int month;
    public int day;

    public void setDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
}

这里形参名和成员变量同名,用 this.year 明确指的是“当前对象的 year”。

如果你写成:

public void setDate(int year, int month, int day) {
    year = year;   // 完全没用
    month = month; // 也是
    day = day;     // 还是
}

你只是在给形参自己赋值,成员变量完全没动,这是典型坑点,按照java的尿性,这几个输出全为0,根本没赋值。

2. this 的规则

  • this 表示:当前正在执行这个方法的那个对象
  • 只能出现在实例方法构造方法中。
  • 在静态方法里不能用 this(因为静态方法可能没有具体对象,没法指向谁)。

易错点

  1. 形参与成员变量同名却不加 this,导致赋值失效。
  2. static 方法里写 this,直接编译报错。

四、构造方法:对象的“初始化入口”

1. 构造方法长什么样

public class Date {
    public int year;
    public int month;
    public int day;

    // 构造方法
    public Date(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
        System.out.println("构造 Date 对象");
    }
}

特点:

  • 方法名必须和类名一模一样。
  • 没有返回值类型,连 void 都不写。
  • new 的时候自动调用,一个对象只会调用一次。

2. 构造方法重载 & this(…)

多种构造方式:

public class Date {
    public int year = 1900;
    public int month = 1;
    public int day = 1;

    public Date() {
        // 使用默认值
    }

    public Date(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }
}

也可以构造器之间互相调用:

public Date() {
    this(1900, 1, 1);   // 必须是第一句
}

3. 默认构造方法的“消失术”

  • 如果你一个构造方法都不写,编译器会偷偷给你加一个无参构造
  • 一旦你手写了任何一个构造方法,这个默认无参构造就不再自动生成。

于是经典错误场景:

public class Person {
    public Person(String name) {}
}

Person p = new Person(); // 编译失败:没有无参构造

4. 成员变量的默认值

  • 成员变量:即使你没手动赋值,也会有默认值(int 是 0,boolean 是 false,引用是 null)。
  • 局部变量:一定要自己赋值,否则编译不过。

构造相关易错点

  1. 写了有参构造却忘了手写无参构造,导致框架或测试里 new Class() 挂掉。
  2. 在构造方法中调用 this(...),但不是第一句,编译错误。
  3. 构造方法互相 this(...) 调用形成闭环,编译报“递归构造器调用”。

五、封装:把细节关在类里面

1. 思想

封装做的就是两件事:

  1. 把数据和操作数据的方法包在一起(写在同一个类里)。
  2. 对外只暴露必要接口,把实现细节藏起来。

典型写法:

public class Student {
    private String name;
    private int age;

    public void setAge(int age) {
        if (age >= 0 && age <= 150) {
            this.age = age;
        }
    }

    public int getAge() {
        return age;
    }
}

外部不能直接乱改 age,必须通过 setAge,中间可以加校验。

2. 四个访问修饰符

按“开放程度”从高到低:

  • public:哪里都能访问。
  • protected:同包 + 子类。
  • default(不写修饰符):同一个包内可以访问。
  • private:只能在本类内部访问。

另外:

  • 顶级类(文件最外层那个类)只能是 public 或 default。
  • 一个 .java 文件里最多有一个 public class,文件名要和它同名。

封装相关易错点

  1. 把类写成 public 之后把文件名忘记改成同名,编译报错。
  2. 成员是 default,不在同一个包中时访问失败 —— 包一换就炸。
  3. 字段暴露为 public,在项目里到处直接改,后期全是“谁改了这个值”的谜团。

六、包(package)与 import

简单记三条:

  1. package com.demo.test; 对应的物理路径就是 com/demo/test/

  2. 同一个工程里,只要包不同,就可以有同名类共存。

  3. import 是为了用短名字访问类:

    import java.util.Date;
    // 之后可以直接写 Date,而不用 java.util.Date
    

当有两个包里都有 Date(比如 java.util.Datejava.sql.Date)时:

  • 同时 import java.util.*;import java.sql.*;,再写 Date 就会“引用不明确”。
  • 这时必须写全名:java.util.Datejava.sql.Date

七、static关键字:属于“类”,而不是属于某个对象

1. static 变量(类变量)

public class Student {
    public String name;
    public int age;

    public static String classRoom = "Bit306";
}

特点:

  • 只保存一份,所有对象共享。
  • 更推荐用 Student.classRoom 访问,而不是 stu.classRoom
  • 随类加载创建,随类卸载销毁。

2. static 方法(类方法)

public class Student {
    public String name;
    public static String classRoom;

    public static String getClassRoom() {
        return classRoom; // OK
        // return name;   // 错误:不能访问非静态成员
    }
}

规则:

  • 调用不需要对象(直接类名+静态方法):Student.getClassRoom()
  • 不能访问非静态成员变量 / 非静态方法。
  • 不能使用 this

原因很简单:静态方法可能在任何对象创建之前就被调用,根本不知道你想操作哪一个具体对象。

3. 静态代码块

public class Student {
    public static String classRoom;

    static {
        classRoom = "Bit306";
        System.out.println("加载 Student 类");
    }
}
  • 类第一次被使用时执行一次。并且只执行一次
  • 用于复杂的 static 初始化比较方便。

static 易错点

  1. 在 static 方法里访问实例变量 / 实例方法,编译错误。
  2. 把明显属于“对象自己”的属性写成 static,导致所有对象共享数据,逻辑错得很隐蔽。
  3. 在构造函数里初始化 static 字段,造成语义混乱(虽然能跑,但不推荐)。

八、代码块:普通、实例、静态

1. 普通代码块

写在方法里的 { },就是个局部作用域:

public static void main(String[] args) {
    {
        int x = 10;
        System.out.println(x);
    }
    int x = 100;
}

作用:变量作用范围控制。

2. 实例代码块(构造代码块)

写在类里、没有修饰符:

public class Student {
    private String name;
    private int age;

    {   // 实例代码块
        this.name = "默认名";
        this.age = 18;
        System.out.println("实例代码块执行");
    }

    public Student() {
        System.out.println("构造方法执行");
    }
}

特点:

  • 每次 new 对象都会执行一次,而且早于构造方法。
  • 可以把多个构造方法的公共初始化逻辑放这里(不过实际项目中更多是通过构造器之间 this() 调用)。

3. 静态代码块(前面讲过)

  • 类加载时执行一次。
  • 用于初始化 static 成员。

执行顺序总览(记个大概就行)

  • 类加载:
    静态成员默认值 → 静态成员就地初始化 → 静态代码块(从上到下)。
  • new 对象:
    实例成员默认值 → 实例成员就地初始化 → 实例代码块 → 构造方法。

九、内部类:类里再套一个类

常见几种:

  1. 实例内部类

    public class Outer {
        private int a;
        static int b;
        int c;
    
        class Inner {
            int c;
            public void test() {
                a = 10;       // 访问外部类实例字段
                b = 20;       // 访问外部类静态字段
                c = 30;       // 内部类自己的 c
                Outer.this.c = 40; // 外部类的 c
            }
        }
    }
    

    创建方式:

    Outer outer = new Outer();
    Outer.Inner inner = outer.new Inner();
    

    要点:必须先有外部类对象,才能 new 实例内部类

  2. 静态内部类

    public class Outer {
        static int b;
    
        static class Inner {
            public void test() {
                b = 20;  // 只能访问外部类静态成员
            }
        }
    }
    
    Outer.Inner inner = new Outer.Inner();
    

    不依赖外部类对象。

  3. 局部内部类 / 匿名内部类

    • 局部内部类:写在方法里的类,只在方法内部使用。
    • 匿名内部类:以后写接口实现、回调时到处能见,算是语法糖。

内部类是一个偏“语法增强”的工具,先记住有这个东西,知道基本使用方式就够了。


十、toString:打印对象时的“名片”

默认情况:

Person p = new Person();
System.out.println(p);

输出类似:com.demo.Person@1b6d3586,这只是对象地址的一个编码形式,看着没啥用。

常规做法是重写 toString()

public class Person {
    String name;
    String gender;
    int age;

    @Override
    public String toString() {
        return "[" + name + ", " + gender + ", " + age + "]";
    }
}

以后直接 System.out.println(p); 就能打印友好的信息,非常利于调试、打日志。


十一、超浓缩易错点清单(复习时对着看一遍)

  • 类与对象

    • 类是模板,对象才占内存;多个对象各有自己的成员变量。
  • this

    • 形参与成员变量同名时,一定要用 this.xxx = xxx;
    • static 方法里不能使用 this
  • 构造方法

    • 名字必须和类名一样,不能写返回值类型。
    • 一旦自己写了构造方法,默认无参构造就没了;需要就手写。
    • this(...) 调用其他构造器必须位于构造方法第一行。
  • 变量初始化

    • 成员变量有默认值,局部变量必须手动初始化。
  • 封装与访问控制

    • 字段通常用 private,暴露 getter/setter。
    • 不同包访问 default 成员会失败;public 类文件名必须相同。
  • static

    • static 变量共享一份数据;static 方法不能访问非 static 成员,也不能用 this
    • 静态代码块只在类加载时执行一次。
  • 代码块 & 顺序

    • new 对象时,实例字段默认值 → 实例字段就地初始化 → 实例代码块 → 构造方法。
  • 内部类

    • 实例内部类创建需要外部类对象:outer.new Inner()
    • 静态内部类可以直接 new Outer.Inner()
  • toString

    • 想赋予打印对象除了内存地址外实际的意义,可以重写 toString()

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐