包(package)与 import 机制
本教程共 100 篇 · 第 7 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
7. 包(package)与 import 机制
本节目标:理解包为什么存在、怎么声明、目录该怎么放,并掌握
import的三种写法和编译器的类名查找规则。
撞名字的麻烦
假设小明写了个 Person 类,小红也写了个 Person 类。
现在小白的项目要同时用这两个。编译器懵了:Person p = new Person(); 到底指谁?
更常见的情况是撞上 JDK 自带的类。你写了个工具类叫 Arrays,而 JDK 里本来就有 java.util.Arrays。
Java 的解法是包(package)——给每个类加一个「姓氏」。
完整类名 = 包名 + 类名
一个类的真实身份是完整类名(也叫全限定名):
| 类 | 所在包 | 完整类名 |
|---|---|---|
| 小明的 Person | ming | ming.Person |
| 小红的 Person | hong | hong.Person |
| 你的 Arrays | mr.jun | mr.jun.Arrays |
| JDK 的 Arrays | java.util | java.util.Arrays |
平时写的 Person 只是简写。JVM 只认完整类名,只要包名不同,就是两个毫不相干的类。
声明包
在源文件的第一行(注释除外)写 package:
package com.example.util; // 必须是第一条语句
public class Greeter {
public static void sayHi(String name) {
System.out.println("Hi, " + name);
}
}
```bash
规则很死:
- 一个文件**最多一条** `package` 语句
- 必须放在所有代码之前
- 不写就属于「默认包」
> [!WARNING]
> 默认包(不写 `package`)极易撞名,而且**默认包里的类无法被有包名的类导入**。除了学语法时的临时文件,正式代码一律写包名。
## 包名怎么起
约定俗成的两条:
**全部小写。** `com.example.util`,不要写成 `com.Example.Util`。
**用倒置域名保证唯一。** 公司域名是 `example.com`,包名就以 `com.example` 打头。
常见例子:
```text
org.apache.commons.lang3
com.google.gson
com.fasterxml.jackson.databind
后面的层级按功能自己分:com.example.user.service、com.example.order.dao。
Note包没有父子关系。
java.util和java.util.zip看着像父子,实际是两个毫不相干的包,不存在任何继承或访问上的特殊照顾。这是个高频误解。
另外,别给自己的类起 String、System、List 这类和标准库撞车的名字,找起来会很痛苦。
目录结构必须对上
包名的每一段对应一层目录,这是硬性要求。
com.example.util.Greeter 的源文件必须放在:
src/com/example/util/Greeter.java
```java
完整演示一遍。建两个文件:
`src/com/example/util/Greeter.java`:
```java
package com.example.util;
public class Greeter {
public static void sayHi(String name) {
System.out.println("Hi, " + name);
}
}
src/com/example/app/Main.java:
package com.example.app;
import com.example.util.Greeter; // 导入另一个包里的类
public class Main {
public static void main(String[] args) {
Greeter.sayHi("小明");
}
}
```bash
编译(`-d out` 把产物放进 `out` 目录,目录层级会自动建好):
```bash
javac -d out src/com/example/util/Greeter.java src/com/example/app/Main.java
运行时要用完整类名:
java -cp out com.example.app.Main
```text
```text
Hi, 小明
注意最后那条命令——不是 java Main,而是 java com.example.app.Main。带了包名的类,启动时必须报全名。
编译产物的目录结构和源码是镜像的:
out/
└── com/
└── example/
├── app/
│ └── Main.class
└── util/
└── Greeter.class
```bash
## import 的三种写法
要用别的包里的类,有三条路。
### 写完整类名
```java
package com.example.app;
public class Main {
public static void main(String[] args) {
com.example.util.Greeter.sayHi("小明"); // 全名,又长又啰嗦
}
}
能用,但每次都写一长串太痛苦。
单类型导入(推荐)
import com.example.util.Greeter;
```bash
之后写简单类名 `Greeter` 即可。**这是日常最常用的写法。**
### 按需导入(星号)
```java
import com.example.util.*; // 导入该包下所有类
这里的 * 有两个容易误会的点:
- 不包括子包。
import java.util.*不会导入java.util.zip下的类。 - 不影响性能。 编译器只是多了个查找范围,不会把整包塞进 class 文件。
不推荐用它,原因是可读性:一屏 * 之后,你说不清 Arrays 到底来自哪个包。IDE 也默认帮你展开成单类型导入。
静态导入
import static 可以直接导入类的静态成员,用的时候不写类名:
import static java.lang.Math.PI;
import static java.lang.Math.abs;
public class StaticImportDemo {
public static void main(String[] args) {
System.out.println(PI); // 不用写 Math.PI
System.out.println(abs(-5)); // 不用写 Math.abs
}
}
```text
```text
3.141592653589793
5
用得不多。它省了几个字符,代价是读代码的人不知道 abs 从哪来。
比较合理的使用场景是单元测试里的断言方法(assertEquals 之类),以及数学计算密集的代码。
Tip判断该不该用静态导入,问自己一句:去掉类名之后,别人还看得出这个方法属于谁吗? 看不出就别用。
编译器怎么找类
看到一个简单类名,编译器按这个顺序查:
- 是完整类名吗?是就直接用。
- 当前包里有没有这个类?
import进来的包里有没有?java.lang包里有没有?
四轮都找不到,报错「找不到符号」。
第 4 条解释了一个现象:String、System、Math、Integer 这些类从来不用导入。它们都在 java.lang 包里,编译器自动帮你导了。
public class AutoImport {
public static void main(String[] args) {
String s = "hi"; // java.lang.String,自动可用
System.out.println(s); // java.lang.System,自动可用
System.out.println(Math.max(1, 2)); // java.lang.Math,自动可用
}
}
```bash
> [!NOTE]
> 自动导入的只有 `java.lang` 这一层。`java.lang.reflect`、`java.lang.annotation` 这些子包仍需手动导入——又一次印证「包没有父子关系」。
## 两个同名类怎么办
同时要用 `java.util.Date` 和 `java.sql.Date` 时,**只能 import 一个**,另一个写全名:
```java
import java.util.Date;
public class DateConflict {
public static void main(String[] args) {
Date d1 = new Date(); // java.util.Date
java.sql.Date d2 = new java.sql.Date(0L); // 写全名区分
System.out.println(d1.getClass().getName());
System.out.println(d2.getClass().getName());
}
}
java.util.Date
java.sql.Date
```bash
要是两个都 import,编译器会直接报「引用不明确」。
## 常用基础包一览
刚上手时,你会反复碰到下面这几个包。提前认识一下,看到 `import` 就不慌了:
| 包 | 用途 | 典型类 |
|----|------|--------|
| `java.lang` | 语言核心,自动导入 | `String`、`System`、`Math`、`Integer` |
| `java.util` | 工具与集合 | `ArrayList`、`HashMap`、`Scanner`、`Arrays` |
| `java.io` | 输入输出 | `File`、`InputStream`、`PrintWriter` |
| `java.time` | 日期时间(Java 8 引入) | `LocalDate`、`LocalDateTime`、`Duration` |
| `java.math` | 高精度数值 | `BigDecimal`、`BigInteger` |
| `java.net` | 网络 | `URL`、`Socket`、`HttpURLConnection` |
记住一个规律:**包名越短,越是核心基础。** `java.lang` 最短,因为它是整个语言的地基,所以才被编译器自动导入。
> [!TIP]
> 写代码时不必死记包名。IDE 在你敲出 `ArrayList` 的瞬间就会提示要不要 `import`,按一下回车就补好了。但理解「为什么需要 import」这件事,能帮你在报错时快速定位问题——多半就是缺了某条 `import`。
## 小结
包是 Java 的命名空间,完整类名 = 包名 + 类名,JVM 只认完整类名。
`package` 必须是文件第一条语句,包名全小写、用倒置域名,包之间**没有父子关系**。
包名的每一段对应一层目录,运行带包的类要用完整类名启动。
`import` 优先用单类型导入;`*` 不含子包也不影响性能,但伤可读性。
`java.lang` 由编译器自动导入,它的子包不算在内;同名类冲突时只能导一个,另一个写全名。