1. 包:给类分类,也给类名加上范围

项目中的类越来越多时,需要按用途组织;不同地方也可能出现同名的类。包(package)用来组织类,并区分不同包中的同名类。

例如,java.util.Date 和 java.sql.Date 都叫 Date,但它们属于不同的包,是两个不同的类。包名.类名 称为类的全限定名。

1.1 声明包与创建目录

package 表示“当前源文件中的类属于哪个包”,放在源文件开头、import 和类定义之前;前面可以有注释。

package com.bit.demo1; public class Test { public static void main(String[] args) { System.out.println("Hello"); } } 

在 IDEA 的普通项目中,对应目录为:

src/ └── com/ └── bit/ └── demo1/ └── Test.java 
  • 包名通常使用小写,常按“域名倒写 + 项目或模块名”命名,尽量避免重名。
  • 在 IDEA 中:右键 src → New → Package,输入 com.bit.demo1;再右键包 → New → Java Class 创建类。
  • IDEA 会创建目录,并自动添加对应的 package 语句。
  • 不写 package 时,类属于默认包。

1.2 使用其他包中的类

方式一:直接写全限定名。

java.util.Date date = new java.util.Date(); System.out.println(date.getTime()); 

这里 getTime() 返回从 1970 年 1 月 1 日 00:00:00 UTC 到该时间点的毫秒数。此处只用它演示如何使用其他包中的类。

方式二:先用 import 导入类,再写简单类名。

import java.util.Date; public class Test { public static void main(String[] args) { Date date = new Date(); System.out.println(date.getTime()); } } 

二者效果相同。import 只是让当前文件能用简单类名,不会把类复制进当前包,也不会改变访问权限。

源文件中的常见顺序是:

package 声明(如果有) import 导入(如果有) 类定义 

同一个包中的类通常不需要互相导入;java.lang 包中的 String、System 等类也不需要显式导入。

1.3 通配导入与同名冲突

import java.util.*; 

* 允许使用该包中可访问的类型的简单名称,不包含子包中的类型。通常更推荐明确写出需要的类,例如 import java.util.Date;。

下面两条导入同时存在时,单独写 Date 会产生歧义:

import java.util.*; import java.sql.*; 

因为两个包都有 Date,编译器不知道要选哪个。可以明确导入需要的那个类;如果两个同名类都要使用,就用全限定名区分,例如:

java.util.Date date = new java.util.Date(); 

1.4 常见包

包 主要用途
java.lang 常用基础类,如 String、Object,无需显式导入
java.util 常用工具类、集合类等,后续会经常使用
java.io 输入输出,如文件读写
java.net 网络编程
java.sql 数据库访问
java.lang.reflect 反射,即在程序运行时获取类的信息并操作其成员

这里先认识用途,具体用法在后续章节学习。

2. 访问权限与封装:把数据的使用规则放进类里

2.1 先分清哪些地方能访问成员

访问权限控制其他代码能否直接访问字段或调用方法。本章先掌握以下三种:

成员的访问权限 当前类内部 同包的其他普通类 不同包的普通类
private 可以 不可以 不可以
不写访问修饰符(包访问权限) 可以 可以 不可以
public 可以 可以 可以

表格以类本身可被访问为前提,暂不涉及继承和嵌套类。

包访问权限有时称为 default 权限,但不是在字段前写 default,而是什么都不写。

另一种访问修饰符是 protected,它涉及继承中的访问规则,留到继承章节再学。

看一个跨包访问的例子。两个类分别放在各自包对应的目录中。

com/bit/demo1/Computer.java:

package com.bit.demo1; public class Computer { private String cpu = "i7"; String brand = "HW"; public String screen = "14英寸"; public void showCpu() { System.out.println(cpu); // 当前类内部,可以访问 private 成员 } } 

com/bit/demo2/TestComputer.java:

package com.bit.demo2; import com.bit.demo1.Computer; public class TestComputer { public static void main(String[] args) { Computer computer = new Computer(); System.out.println(computer.screen); // 可以:public computer.showCpu(); // 可以:通过公开方法使用私有数据 // System.out.println(computer.brand); // 错误:不同包,不能访问包权限成员 // System.out.println(computer.cpu); // 错误:其他类不能直接访问 private 成员 } } 

如果把 TestComputer 移到 com.bit.demo1 包中,它就能访问 brand,但仍不能直接访问 cpu。“同一个包”和“同一个类”是两种不同的范围。

2.2 封装解决什么问题

封装就是把数据和操作数据的方法组织在类中,隐藏内部细节,通过对外提供的方法与对象交互。

例如,年龄不应该随意变成负数。如果把 age 设为 public,外部就能直接执行 stu.age = -1;。将它改为 private 后,外部必须经过类提供的方法修改,类就有机会检查数据。

这里的“对外接口”指公开的使用方式,例如方法,不是专指 Java 的 interface 语法。

class Student { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { if (age < 0) { System.out.println("年龄不能为负数"); return; } this.age = age; } public void show() { System.out.println(name + "," + age + "岁"); } } public class Test { public static void main(String[] args) { Student stu = new Student(); stu.setName("小明"); stu.setAge(18); stu.setAge(-1); // 被拒绝,年龄仍然是 18 System.out.println(stu.getAge()); stu.show(); } } 

输出:

年龄不能为负数 18 小明,18岁 
  • getXxx() 通常用于读取数据,叫 getter;setXxx(...) 通常用于修改数据,叫 setter。它们都是普通方法,名称是一种约定。
  • this.age = age; 中,左边是当前对象的字段,右边是方法参数。
  • private 阻止的是类外直接访问,不影响本类的方法操作这些数据。
  • 不必给每个字段都提供 getter 和 setter。 只开放确实需要的操作;只读数据可以只提供 getter。

2.3 封装的价值

  1. 控制数据修改。 外部经过方法修改数据,类可以检查是否合法,减少意外破坏。
  2. 简化使用。 使用者只需要知道方法怎么调用,不必了解内部如何实现。
  3. 减少相互依赖。 对外方法的用法和约定保持一致时,内部实现的调整通常不需要调用者跟着修改。

封装不是“所有成员一律 private”,也不是“写了 getter/setter 就自动安全”。关键在于:哪些细节隐藏,哪些操作开放,以及开放的操作是否维护了数据规则。

3. static:属于类,还是属于某个对象

3.1 实例变量各有一份,静态变量共同使用一份

学生的姓名因人而异,应当是实例变量。如果当前程序约定所有学生共用一个教室,教室就可以是静态变量。

class Student { public String name; public static String classRoom = "1-101"; } public class Test { public static void main(String[] args) { // 不创建对象,也能访问静态变量 System.out.println(Student.classRoom); Student s1 = new Student(); Student s2 = new Student(); s1.name = "小明"; s2.name = "小红"; Student.classRoom = "1-102"; System.out.println(s1.name + ":" + Student.classRoom); System.out.println(s2.name + ":" + Student.classRoom); } } 

输出:

1-101 小明:1-102 小红:1-102 

可以把关系理解为:

s1 对象:name = "小明" s2 对象:name = "小红" Student 类共享的数据:classRoom = "1-102" 

static 修饰的成员变量又叫静态成员变量、类变量,不在每个对象中各保存一份。它的生命周期与类相关,不会因为某一个对象不再使用就消失。

虽然 Java 允许写 s1.classRoom,但访问的仍然是共享变量,并不是 s1 独有的数据。推荐写 Student.classRoom,明确表示它属于类。

是否使用 static 取决于数据是否真的需要全类共享。如果学生可能属于不同教室,教室就不适合简单地定义成所有学生共享的变量。

3.2 静态方法与静态成员的封装

被 static 修饰的方法叫静态方法、类方法,推荐通过 类名.方法名(...) 调用,不需要先创建对象。

静态成员也受访问权限限制。可以将静态字段设为 private,再按需要提供公开方法:

class Student { private static String classRoom = "1-101"; public static String getClassRoom() { return classRoom; } public static void setClassRoom(String classRoom) { Student.classRoom = classRoom; } } public class Test { public static void main(String[] args) { Student.setClassRoom("1-102"); System.out.println(Student.getClassRoom()); // 1-102 } } 

Student.classRoom 表示类变量;右边的 classRoom 是参数。static 不会让 private 成员变成公开成员。

3.3 为什么静态方法不能直接访问实例成员

实例成员需要先确定“是哪一个对象的”,静态方法本身没有当前对象,所以不能使用 this,也不能直接访问实例成员。

class Student { private int age = 18; public void show() { System.out.println(age); } public static void test() { // System.out.println(age); // 错误:没有指定对象 // System.out.println(this.age); // 错误:静态方法中没有 this // show(); // 错误:没有指定调用对象 Student stu = new Student(); System.out.println(stu.age); // 正确:访问 stu 的年龄,且代码位于本类内部 stu.show(); // 正确:调用 stu 的实例方法 } } 

因此,准确说法是:静态方法不能直接访问实例成员,但可以通过具体对象访问有权限访问的实例成员。 main 是静态方法,却能先 new 对象再调用实例方法,原因就在这里。

方法所在环境 直接访问本类静态成员 直接访问当前对象的实例成员 使用 this
实例方法 可以 可以 可以
静态方法 可以 不可以,需要具体对象 不可以

3.4 静态变量如何初始化

本章掌握两种常见写法:

// 就地初始化:定义时直接给值 private static String classRoom = "1-101"; 

另一种是使用静态代码块,适合需要多条语句完成的初始化,下一节说明。

通常不要把静态变量的初始化放进构造方法:构造方法每创建一个对象就会执行,把共享数据放进去赋初值,可能导致每次创建对象都重置共享状态。

3.5 静态导入:省略静态成员前的类名

理解 static 后,再看 import static 就容易了:普通导入让类名可以简写,静态导入让指定类中可访问的静态方法、静态字段可以直接使用名称。

import static java.lang.Math.pow; import static java.lang.Math.sqrt; public class Test { public static void main(String[] args) { double result = sqrt(pow(3, 2) + pow(4, 2)); System.out.println(result); // 5.0 } } 

pow(a, b) 求 a 的 b 次方,sqrt(x) 求平方根。上面的计算等价于:

double result = Math.sqrt(Math.pow(3, 2) + Math.pow(4, 2)); 

也可以写 import static java.lang.Math.*;。静态导入只是简化写法,不改变方法的性质;如果省略类名反而难以判断来源,保留类名会更清楚。

4. 代码块:位置不同,执行时机不同

代码块是用 {} 包围的一段代码。本章重点区分普通代码块、实例代码块和静态代码块。同步代码块涉及多线程,留到后续章节学习。

4.1 普通代码块:限制局部变量的作用范围

普通代码块写在方法内部,执行流程走到这里时才执行。

public class Test { public static void main(String[] args) { { int x = 10; System.out.println(x); } // 上一个 x 的作用域已经结束 int x = 100; System.out.println(x); } } 

输出依次为 10、100。内层块中的 x 出了块就不能访问,后面的 x 是重新声明的另一个变量。

4.2 实例代码块:每个对象都会执行的初始化

实例代码块也叫构造代码块,写在类内部、方法外部,不加 static。

class Student { private String name; { name = "小明"; System.out.println("实例代码块"); } public Student() { System.out.println("构造方法:" + name); } } 

每次执行 new Student(),都会先运行实例代码块,再运行这里的构造方法体:

实例代码块 构造方法:小明 
  • 实例代码块用于实例初始化,可以使用 this 和实例成员。
  • 它没有方法名和参数,不能像普通方法那样单独调用。
  • 即使把它写在构造方法定义的后面,执行时也仍然先于该构造方法体。

4.3 静态代码块:类初始化时执行一次

静态代码块写在类内部、方法外部,形式是 static { ... },通常用于初始化静态数据。

class Student { private static String classRoom; static { classRoom = "1-101"; System.out.println("静态代码块"); } } 

静态代码块在类初始化时执行,同一个类正常初始化一次后,不会因为又创建了对象而重复执行。 它不属于某个对象,所以也不能使用 this 或直接访问实例成员。

类初始化可以由首次创建对象、调用该类的静态方法等操作触发。本章示例中的静态变量不是常量,首次访问它也会触发类初始化;因此,不创建对象,也可能执行静态代码块。

“类加载”和“类初始化”不完全相同;这里只需记住:静态代码块中的语句是在运行程序、初始化类时执行的,不是在编译时执行的。

4.4 用一次完整运行看清执行顺序

下面的类不涉及继承,重点观察静态初始化、实例初始化和构造方法体的先后关系。

class Student { private static String classRoom = "1-101"; private String name = "默认姓名"; static { System.out.println("静态块1:" + classRoom); classRoom = "1-102"; } static { System.out.println("静态块2:" + classRoom); } { System.out.println("实例块:" + name); name = "小明"; } public Student() { System.out.println("构造方法:" + name); } } public class Test { public static void main(String[] args) { System.out.println("第一次创建"); Student s1 = new Student(); System.out.println("第二次创建"); Student s2 = new Student(); } } 

输出:

第一次创建 静态块1:1-101 静态块2:1-102 实例块:默认姓名 构造方法:小明 第二次创建 实例块:默认姓名 构造方法:小明 

按两个阶段理解:

  1. 类初始化阶段,只进行一次。 静态字段先有默认值,再把静态字段的显式初始化和静态代码块按源码出现顺序执行。这里先把 classRoom 设为 "1-101",再执行静态块1、静态块2。
  2. 对象初始化阶段,每个新对象都要进行。 实例字段先有默认值,再把实例字段的显式初始化和实例代码块按源码出现顺序执行,最后执行本例中的构造方法体。

注意:“按源码顺序”是分别在静态初始化阶段、实例初始化阶段内排序,不是把整个类从上到下无差别执行。也不是“所有字段赋值永远先于所有代码块”;同一阶段中,字段的显式初始化与代码块谁写在前,谁先执行。

形式 位置 执行时机 常见用途
普通代码块 { ... } 方法内部 流程执行到该处时 限制局部变量作用域
实例代码块 { ... } 类内、方法外 每次创建对象时 初始化实例数据
静态代码块 static { ... } 类内、方法外 类初始化时,一次 初始化类共享的数据

5. 复习自查

1. import 了一个类,就能访问它的所有成员吗?

不能。导入只解决名称使用问题,能否访问还要看访问权限。

2. 静态方法能否使用实例变量?

不能直接使用,但可以通过具体对象访问有权限访问的实例变量;静态方法中不能使用 this。反过来,实例方法可以直接访问本类的静态成员。

3. 创建两个对象,静态代码块、实例代码块、构造方法体分别执行几次?

在本章这种简单类中,若此前尚未初始化该类,静态代码块执行 1 次;实例代码块和构造方法体各执行 2 次。

4. getter/setter 会破坏封装吗?

不会必然破坏封装,它们是对外提供操作的一种方式。关键是按需要开放,并维护数据规则,而不是无条件暴露所有内部状态。

5. 封装后为什么更容易修改代码?

调用者依赖公开方法;在这些方法的用法和约定保持一致的前提下,可以调整类内部的实现,减少对调用者的影响。