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 封装的价值
- 控制数据修改。 外部经过方法修改数据,类可以检查是否合法,减少意外破坏。
- 简化使用。 使用者只需要知道方法怎么调用,不必了解内部如何实现。
- 减少相互依赖。 对外方法的用法和约定保持一致时,内部实现的调整通常不需要调用者跟着修改。
封装不是“所有成员一律 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 实例块:默认姓名 构造方法:小明 第二次创建 实例块:默认姓名 构造方法:小明 按两个阶段理解:
- 类初始化阶段,只进行一次。 静态字段先有默认值,再把静态字段的显式初始化和静态代码块按源码出现顺序执行。这里先把
classRoom设为"1-101",再执行静态块1、静态块2。 - 对象初始化阶段,每个新对象都要进行。 实例字段先有默认值,再把实例字段的显式初始化和实例代码块按源码出现顺序执行,最后执行本例中的构造方法体。
注意:“按源码顺序”是分别在静态初始化阶段、实例初始化阶段内排序,不是把整个类从上到下无差别执行。也不是“所有字段赋值永远先于所有代码块”;同一阶段中,字段的显式初始化与代码块谁写在前,谁先执行。
| 形式 | 位置 | 执行时机 | 常见用途 |
|---|---|---|---|
普通代码块 { ... } | 方法内部 | 流程执行到该处时 | 限制局部变量作用域 |
实例代码块 { ... } | 类内、方法外 | 每次创建对象时 | 初始化实例数据 |
静态代码块 static { ... } | 类内、方法外 | 类初始化时,一次 | 初始化类共享的数据 |
5. 复习自查
1. import 了一个类,就能访问它的所有成员吗?
不能。导入只解决名称使用问题,能否访问还要看访问权限。
2. 静态方法能否使用实例变量?
不能直接使用,但可以通过具体对象访问有权限访问的实例变量;静态方法中不能使用 this。反过来,实例方法可以直接访问本类的静态成员。
3. 创建两个对象,静态代码块、实例代码块、构造方法体分别执行几次?
在本章这种简单类中,若此前尚未初始化该类,静态代码块执行 1 次;实例代码块和构造方法体各执行 2 次。
4. getter/setter 会破坏封装吗?
不会必然破坏封装,它们是对外提供操作的一种方式。关键是按需要开放,并维护数据规则,而不是无条件暴露所有内部状态。
5. 封装后为什么更容易修改代码?
调用者依赖公开方法;在这些方法的用法和约定保持一致的前提下,可以调整类内部的实现,减少对调用者的影响。