Day 2 · 面向对象基础:类、封装与 static
面向对象思想
为什么要面向对象
核心是解耦:降低耦合,让代码分开,一个对象只专注做一件事。 面向过程写法里前面出问题会直接影响后面,耦合度高,改一处容易牵一片。
- 类:对象的「模板」/「图纸」,定义一类事物共有的属性和行为
- 对象:具体事物,真实存在,是根据类创建出来的
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),没有引用传递。 上面两句话描述的是现象,不是机制:
- 传基本类型时,拷贝的是值本身,方法内改副本不影响外部。
- 传引用类型时,拷贝的是引用的地址值。所以「通过引用改对象内部字段」会生效 (两个引用指向同一个堆对象),但「把参数重新指向一个新对象」不会生效 (只改了副本这个地址)。
javavoid change(int n) { n = 100; } // 外部不变 void change(User u) { u.name = "李四"; } // 外部对象的 name 变了 void reassign(User u) { u = new User("王五"); } // 外部仍然指向原对象判断口诀:看方法里是「改对象」还是「换对象」。换对象一定不影响调用方。 Python 里对应的概念更接近「传对象引用」,两者不要混着记。
构造方法(Constructor)
也叫构造器,相当于 Python 中的魔法方法 __init__:
- 用来创建对象
- 并对对象赋初值
public class Student {
// 成员变量(对象属性)
String name;
int age;
// 构造方法:名字和类名一样,没有返回值
public Student(String sName, int sAge) {
name = sName;
age = sAge;
}
}无参构造器
Java 会自动给一个类创建构造器,默认就有一个无参构造器,赋值用的是系统默认值。 所以习惯上要把无参构造器显式写上;一旦自己写了构造器,系统就不会再自动提供。
补充整理:这个坑几乎人人踩过——「一旦定义了任意构造器(有参也算),编译器就 不再赠送无参构造器」。此时若有人写
new Student()会直接编译报错。 框架(Jackson、MyBatis、Spring 反射创建对象)通常依赖无参构造器, 所以定义有参构造器时顺手补一个无参的:javapublic 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,不写修饰符(包私有)的情况要谨慎, 很容易被同包的其他类意外依赖。
封装的好处
- 隐藏细节:外部只能通过方法访问,不能直接改字段
- 添加校验:
setter里可以加合法性检查 - 保护数据:避免外部乱改导致对象状态不一致
static 关键字
static 修饰的成员属于类,而不是某个对象。加载这个变量时,会随着类的加载而加载,存放在静态方法区中。
- 什么时候定义静态变量、什么时候定义成员变量?——共享变量用静态变量
static可以修饰成员变量,也可以修饰成员方法- 不用创建对象就能用,减少内存;调用方式:
类名.成员,也可以对象名.成员名(不推荐后者)
补充整理:「
对象名.静态成员」虽然编译能过,但会让人误以为它是实例成员, 而且空引用调用静态成员也不会抛NullPointerException(编译期就绑定成类名调用了), 很容易掩盖 bug。一律用类名.静态成员。还有一条生命周期差异:静态成员随类加载而创建、随类卸载而销毁,整个 JVM 里只有一份; 成员变量每个对象各一份。所以工具类方法、常量、全局计数器适合
static, 而「用户姓名」「订单金额」这种对象状态绝不能设成static。
静态方法不能访问非静态成员
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 会讲)。
静态代码块
public class Config {
static {
System.out.println("Config 类加载了");
}
}会随着类的加载而加载,往往只加载一次;一般把常量、初始化配置(jdbc、spring 等)放进去。
补充整理:类初始化时按代码书写顺序依次执行静态变量赋值和静态代码块, 且 JVM 保证多线程环境下只执行一次(这也是写线程安全单例的一种思路)。 与之相对的是实例代码块
{ ... },每次new对象都会执行,且先于构造器。 执行顺序:父类静态 → 子类静态 → 父类实例块 / 构造器 → 子类实例块 / 构造器。
自定义工具类 Math
补充整理:原稿这里只有标题没有内容,补一个工具类的标准写法。 工具类的共同特征是「只有静态方法、不需要状态」,所以要做三件事:
javapublic final class MathUtil { private MathUtil() { } // 1. 私有构造器,禁止 new public static int add(int a, int b) { // 2. 只提供静态方法 return a + b; } }
- 用
import static com.xxx.MathUtil.add;静态导入后可以省略类名直接写add(1, 2)。JDK 自带的
java.util.Math(Math.abs/Math.max/Math.random)就是这个套路, 注意别和 Apache 的MathUtils、以及BigDecimal混用。
关于抽象类(原稿记录,待核对)
原稿在 static 这一节里记了一段:
abstract class Person {
}「这样内部不能创建对象,外部也不能创建方法;之后类里的静态变量和静态方法只能通过类来调用」。
补充整理:这段应该是记串行了——抽象类不能实例化是对的,但「静态成员只能通过类调用」 属于
static的规则,跟抽不抽象没有关系;普通抽象类里照样可以有实例成员、实例方法、构造器。 抽象类的完整规则放在 Day 3 讲,这里先不展开,免得两套概念搅在一起。
包
命名规范
- 全小写
- 一般是公司域名倒过来写:
www.baidu.com→com.baidu
package 关键字
package com.jlx; // 声明 Demo 在 com.jlx 目录中
public class Demo17 {
public static void main(String[] args) {
}
}作用:在不同包下可以声明同名类。在不同包中使用某个类就需要 import 导包:
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。
