Skip to content

Day 2 · 面向对象基础:类、封装与 static ​

面向对象思想 ​

为什么要面向对象 ​

核心是解耦:降低耦合,让代码分开,一个对象只专注做一件事。 面向过程写法里前面出问题会直接影响后面,耦合度高,改一处容易牵一片。

  • 类:对象的「模板」/「图纸」,定义一类事物共有的属性和行为
  • 对象:具体事物,真实存在,是根据类创建出来的
java
public class 类名 {
    数据类型 变量名1;
    数据类型 变量名2;
    // ...

    // 修饰符:目前用 public
    修饰符 返回值类型 方法名(数据类型 变量名, 数据类型 变量名2, ...) {
        方法体;
        return 结果;
    }
}

相同目录下不能有相同名称的类。

补充整理:这条更准确的说法是——一个 .java 文件里最多只能有一个 public 类, 且文件名必须与它一致;同包下本来就不允许出现同名的类(全类名 = 包名 + 类名,会冲突)。 非 public 的类可以有多个、也能和 public 类写在同一个文件里,只是不推荐。

另外模板里的「修饰符」和「返回值类型」是两回事:public 是访问修饰符(可省), void / int / String 是返回值类型(不能省,构造器除外)。方法没有返回值时写 void, 写了 void 就不能 return 结果;,只能 return; 或直接不写。

成员变量与局部变量 ​

成员变量 ​

定义在类中、方法外,也叫对象变量。

  • 引用数据类型(非基本类型的四类八种)的默认值是 null
  • 成员变量有默认值
  • 成员变量属于对象,随着对象的销毁而销毁

局部变量 ​

定义在方法中或方法声明上,也叫方法变量。

  • 局部变量没有默认值(如变量 num 没有初始化)
  • 使用之前必须初始化
  • 局部变量属于方法,随着方法执行而创建、方法弹栈而销毁
对比成员变量局部变量
位置类中、方法外方法中 / 方法声明上
默认值有(基本类型 0 / 0.0 / false,引用类型 null)无,必须先赋值
生命周期随对象随方法调用(栈帧)
存储位置堆(对象内)栈

补充整理:上表的「存储位置」是理解后面值传递的关键—— 局部变量和引用本身在栈里,对象的实际内容在堆里。 编译器会检查局部变量「可能未初始化」的路径,哪怕 if 分支逻辑上一定覆盖, 只要它静态分析不出来就会报错,这是新手很常见的编译错误。

值传递 vs 引用传递 ​

  • 如果是基本数据类型,值不会变
  • 如果是引用数据类型,值会变

补充整理:这是 Java 最经典的误解,Java 只有值传递(pass by value),没有引用传递。 上面两句话描述的是现象,不是机制:

  • 传基本类型时,拷贝的是值本身,方法内改副本不影响外部。
  • 传引用类型时,拷贝的是引用的地址值。所以「通过引用改对象内部字段」会生效 (两个引用指向同一个堆对象),但「把参数重新指向一个新对象」不会生效 (只改了副本这个地址)。
java
void change(int n)          { n = 100; }              // 外部不变
void change(User u)         { u.name = "李四"; }      // 外部对象的 name 变了
void reassign(User u)       { u = new User("王五"); } // 外部仍然指向原对象

判断口诀:看方法里是「改对象」还是「换对象」。换对象一定不影响调用方。 Python 里对应的概念更接近「传对象引用」,两者不要混着记。

构造方法(Constructor) ​

也叫构造器,相当于 Python 中的魔法方法 __init__:

  1. 用来创建对象
  2. 并对对象赋初值
java
public class Student {
    // 成员变量(对象属性)
    String name;
    int age;

    // 构造方法:名字和类名一样,没有返回值
    public Student(String sName, int sAge) {
        name = sName;
        age = sAge;
    }
}

无参构造器 ​

Java 会自动给一个类创建构造器,默认就有一个无参构造器,赋值用的是系统默认值。 所以习惯上要把无参构造器显式写上;一旦自己写了构造器,系统就不会再自动提供。

补充整理:这个坑几乎人人踩过——「一旦定义了任意构造器(有参也算),编译器就 不再赠送无参构造器」。此时若有人写 new Student() 会直接编译报错。 框架(Jackson、MyBatis、Spring 反射创建对象)通常依赖无参构造器, 所以定义有参构造器时顺手补一个无参的:

java
public Student() { }
public Student(String name, int age) { this.name = name; this.age = age; }

另外构造器没有返回值类型,写成 public void Student() 就变成普通方法了; 构造器里可以用 this(...) 调用本类其他构造器,且必须放在第一行(Day 3 讲 super() 时会对比)。

封装 ​

目的是程序的安全。

private 关键字 ​

private 修饰的成员只能在本类访问。四种访问修饰符的权限范围:

修饰符同类同包子类其他
private是否否否
(默认 / 包私有)是是否否
protected是是是否
public是是是是

记忆口诀:private < 默认 < protected < public,权限越来越大。

本类访问用 private、同包访问用默认、子父类访问用 protected、到处访问用 public。

远近关系:本类、同包下的子父类、同包下的非子父类、不同包下的子父类、不同包下的非子父类。

补充整理:protected 的「子类」有个容易忽略的细节—— 不同包的子类只能通过自己(或子类的引用)访问 protected 成员, 不能通过父类实例去访问;而「同包」这一档是无条件放开的。 实际项目里的常规搭配:字段一律 private,getter/setter 按需 public, 只给子类用的钩子方法用 protected,不写修饰符(包私有)的情况要谨慎, 很容易被同包的其他类意外依赖。

封装的好处 ​

  1. 隐藏细节:外部只能通过方法访问,不能直接改字段
  2. 添加校验:setter 里可以加合法性检查
  3. 保护数据:避免外部乱改导致对象状态不一致

static 关键字 ​

static 修饰的成员属于类,而不是某个对象。加载这个变量时,会随着类的加载而加载,存放在静态方法区中。

  • 什么时候定义静态变量、什么时候定义成员变量?——共享变量用静态变量
  • static 可以修饰成员变量,也可以修饰成员方法
  • 不用创建对象就能用,减少内存;调用方式:类名.成员,也可以 对象名.成员名(不推荐后者)

补充整理:「对象名.静态成员」虽然编译能过,但会让人误以为它是实例成员, 而且空引用调用静态成员也不会抛 NullPointerException(编译期就绑定成类名调用了), 很容易掩盖 bug。一律用 类名.静态成员。

还有一条生命周期差异:静态成员随类加载而创建、随类卸载而销毁,整个 JVM 里只有一份; 成员变量每个对象各一份。所以工具类方法、常量、全局计数器适合 static, 而「用户姓名」「订单金额」这种对象状态绝不能设成 static。

静态方法不能访问非静态成员 ​

java
public class Demo {
    public String name = "普通成员变量";
    public static String staticName = "静态成员变量";

    public void show() {
        System.out.println("普通成员方法");
    }

    public static void staticShow() {
        System.out.println("静态成员方法");
    }

    public static void main(String[] args) {
        // 访问静态成员:直接使用
        System.out.println(staticName);
        staticShow();

        // 访问非静态成员:必须先创建对象,通过对象调用
        Demo d = new Demo();
        System.out.println(d.name);
        d.show();
    }
}

补充整理:原稿这段示例的类名是 Deme、里面却 new Demo14(),直接粘贴编译不过, 已统一成 Demo。

原因值得记:静态方法执行时可能还没有任何对象存在,没有隐式的 this, 自然没法访问「属于对象」的成员;反过来,普通成员方法里访问静态成员完全没问题。 同类的还有两条:static 方法里不能用 this / super,也不能被重写(Day 3 会讲)。

静态代码块 ​

java
public class Config {
    static {
        System.out.println("Config 类加载了");
    }
}

会随着类的加载而加载,往往只加载一次;一般把常量、初始化配置(jdbc、spring 等)放进去。

补充整理:类初始化时按代码书写顺序依次执行静态变量赋值和静态代码块, 且 JVM 保证多线程环境下只执行一次(这也是写线程安全单例的一种思路)。 与之相对的是实例代码块 { ... },每次 new 对象都会执行,且先于构造器。 执行顺序:父类静态 → 子类静态 → 父类实例块 / 构造器 → 子类实例块 / 构造器。

自定义工具类 Math ​

补充整理:原稿这里只有标题没有内容,补一个工具类的标准写法。 工具类的共同特征是「只有静态方法、不需要状态」,所以要做三件事:

java
public final class MathUtil {
    private MathUtil() { }                 // 1. 私有构造器,禁止 new
    public static int add(int a, int b) {  // 2. 只提供静态方法
        return a + b;
    }
}
  1. 用 import static com.xxx.MathUtil.add; 静态导入后可以省略类名直接写 add(1, 2)。

JDK 自带的 java.util.Math(Math.abs / Math.max / Math.random)就是这个套路, 注意别和 Apache 的 MathUtils、以及 BigDecimal 混用。

关于抽象类(原稿记录,待核对) ​

原稿在 static 这一节里记了一段:

java
abstract class Person {
}

「这样内部不能创建对象,外部也不能创建方法;之后类里的静态变量和静态方法只能通过类来调用」。

补充整理:这段应该是记串行了——抽象类不能实例化是对的,但「静态成员只能通过类调用」 属于 static 的规则,跟抽不抽象没有关系;普通抽象类里照样可以有实例成员、实例方法、构造器。 抽象类的完整规则放在 Day 3 讲,这里先不展开,免得两套概念搅在一起。

包 ​

命名规范 ​

  • 全小写
  • 一般是公司域名倒过来写:www.baidu.com → com.baidu

package 关键字 ​

java
package com.jlx;   // 声明 Demo 在 com.jlx 目录中

public class Demo17 {
    public static void main(String[] args) {
    }
}

作用:在不同包下可以声明同名类。在不同包中使用某个类就需要 import 导包:

java
import com.jlx.Person;   // 导入单个类
import com.jlx.*;        // * 通配符,导入包下所有类

全类名 = 包名.类名

使用 java.lang 包里的类时不需要导包,可以直接用。

补充整理:几条实用规则——

  • package 语句必须是文件第一行有效代码(注释不算),且一个文件只能有一条; import 写在 package 之后、类定义之前。
  • import 只是告诉编译器去哪找类,并不会把类的代码「加载」进来; java.lang(String、System、Math、Object)默认自动导入。
  • 通配符 * 不递归子包,import java.util.* 导不到 java.util.concurrent 里的类; 它也不影响性能(编译期就解析成具体类了),争议主要在可读性上,团队规范常禁止使用。
  • 两个包里有同名类时(如 java.util.Date 与 java.sql.Date),不能都 import, 得有一个写全类名:new java.sql.Date(...)。
  • 包名和磁盘目录必须一一对应,package com.jlx; 的文件就得放在 com/jlx/ 下, 否则编译能过、运行时报 NoClassDefFoundError。