包:Java 组织类的方式
# 包:Java 组织类的方式
包是 Java 重要的概念
# 引出 package 包的概念
Java 提供了很多类给我们使用,我们自己也可以定义类,但我们可能会遇到以下情况:
- 同事艾米莉雅定义了一个 Arrays 类,而 JDK 也自带了 Arrays 类,这会导致冲突;
- 可能有人说,换个名字不就行了吗?理论上是可以的。但如果在一个大型的软件开发中,很多人共同参与编写代码,同事拉姆定义了一个 Person 的类,同事雷姆也定义了 Person 类,怎么办?难道每次定义一个类都和别人说一声:“我已经定义了这个类,其他人请不要重名。”?
- 还有的时候,我们会引用其他人或公司提供的工具类(不论是开源还是商用),如果每次引用之前,都检查下自己的项目中是否有重名的情况,太难了……
- 简化 classpath 的设置(后面会讲)
因此,Java 定义了一个叫做“包(package)”的概念,也就是说我们可以建立很多个包,每个包放不同的类。一个类总是属于某个包,如果一个类没有定义 package,会归到一个默认的无名 package。一个包下面可以再新建几个包,称为子包。
一个包下面不能有同名的类;但不同包下可以有同名的类,解决了冲突的问题。
那如何定位到具体的包呢?只需写出完整的路径即可,因此完整的类名格式为:包名.类名。 平时我们所说的 Person、Arrays 类都是简称。
在 JVM 运行的时候,只看完整类名,因此,只要包名不同,类就不同。
举几个例子。
例子 1:包其实就相当于电脑上的文件夹;在日常使用电脑的过程中,我们可能会这样分类文件:新建一个文件夹,里面放番剧;新建一个文件夹,里面放电影等。如果我们要找某个文件,就先进去文件夹,再找到具体的文件。这样,即使电影和番剧有重名的文件,也不会有冲突,因为他们在不同的文件夹下。
例子 2:假如你名叫张三,又假如与你同一单位的人中有好几个都叫张三。某天单位领导在会上宣布,张三被任命为办公室主任,你简直不知道是该哭还是该笑。但如果你的单位中只有你叫张三,你才不会在乎全国叫张三的人有多少个,因为其他张三都分布在全国各地、其他城市,你看不见他们,摸不着他们,自然不会担心。
package 是 Java 一个很基础很重要的概念,请务必掌握。
# 使用包来组织类
现在,我们开始学习使用包了。假设艾米莉雅同事写的 Person 类在包 emilia 里。我们新建一个 emilia 文件夹,里面新建一个 Person.java 文件。我们得先使用 package 关键字,声明这个类属于哪个包的:
package emilia;
public class Person {
private String name;
public Person(String name){
this.name = name;
}
public String getName(){
return name;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
其他注意点:
每个.java 文件只能有一个 package。
包可以是多级结构,例如包下面再新建一个包,用小数点隔开,例如
java.util
。要特别注意:包没有父子关系。
java.util
和java.util.zip
是不同的包,两者没有任何继承关系。没有定义包名的
class
,它使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法。Java 中的 package 的实现是与计算机文件系统相结合的,即你有什么样的 package,在硬盘上就有什么样的存放路径。例如,某个类的 package 名为 org.apache.commons.log,那么,这个类就应该必须存放在 org/apache/commons/log 的路径下面。
关于包的取名:Sub 公司推荐的做法是,根据自己公司的倒置的域名来确保唯一性。因为域名是不会和其他公司重复的。在工作和正式的场景中,不要随意取名;如果是自己练习一些小的 demo 的话倒没什么关系。
例如有个公司叫 apache,其官网是 apache.org,那么可以这样组织类:
- org.apache
- org.apache.commons.log
子包,就根据具体的功能自行命名。
使用 import 导入类
在讲 classpath 的时候,我们说过,当我们需要用到的类分在不同的路径时,编译时需要指定多个 classpath,classpath 字符串将会变的非常长。而 package 的引入,很好的解决了这个问题。我们可以将 classpath 完成的路径搜索功能,转移到 import 语句上,从而使 classpath 的设置简洁明了。
我们在 Hello.java 里导入 emilia.Person 类
import emilia.Person;
public class Hello{
public static void main(String[] args){
Person person = new Person("Peter");
System.out.println(person.getName());
}
}
2
3
4
5
6
7
8
我们先删除之前的 class 文件。然后尝试下运行:
D:\> javac -cp "D:/JavaTest" D:/JavaTest/Hello.java
D:\> java -cp "D:/JavaTest" Hello
Peter
2
3
4
5
尽管这次我们只设置了 D:\Java Test 的 classpath,但编译及运行居然都通过了!事实上,Java 在搜索.class 文件时,共有三种方法:
全局性的设置,就是我们之前设置的全局环境变量,其优点是一次设置,每次使用;
在每次的 javac 及 java 命令行中自行设置 classpath,这也是本文使用最多的一种方式,其优点是不加重系统环境变量的负担;
根据 import 指令,将其内容在后台转换为 classpath。
JDK 将读取全局的环境变量 classpath 及命令行中的 classpath 选项信息,然后将每条 classpath 与经过转换为路径形式的 import 的内容相合并,从而形成最终的 classpath.
在我们的例子中,JDK 读取全局的环境变量 classpath 及命令行中的 classpath 选项信息,得到 D:\JavaTest。接着,将 import emilia.Person 中的内容,即 emilia.Person 转换为 emilia\Person, 然后将 D:\JavaTest 与其合并,成为 D:\JavaTest\emilia\Person,这就是我们所需要的 Person.class 的路径。
在 Hello.java 中有多少条 import 语句,就自动进行多少次这样的转换。而我们在命令行中只需告诉 JDK 最顶层的 classpath 就行了,剩下的则由各个类中的 import 指令代为操劳了。这种移花接木的作法为我们在命令行中手工地设置 classpath 提供了极大的便利。
# 包作用域
package 除了有避免命名冲突的问题外,还引申出一个保护当前 package 下所有类文件的功能。
Java 语言提供了访问修饰符,每个修饰符的作用:
- public 修饰符:表示它们可以被任何其他的类访问
- protected 修饰符: 对同一包内的类和所有子类可见。
- default:只能被在同一个包内的其他类被访问。
- private 修饰符:限定方法和数据域只能在它自己的类中被访问
我们举个例子,新建一个 scopeTest 目录,在里面新建两个 Java 类
package scopeTest;
public class Person {
//package Scope method
void hello(){
System.out.println("This is Person\'s hello method!");
}
}
2
3
4
5
6
7
8
package scopeTest;
import scopeTest.Person;
public class Main {
public static void main(String[] args) {
Person p = new Person();
p.hello();
}
}
2
3
4
5
6
7
8
编译和运行:
D:\JavaTest> javac ./scopeTest/Main.java
D:\JavaTest> java scopeTest.Main
This is Person's hello method!
2
3
注意:如果主类恰好也在一个 package 中(在大型的开发中,其实这才是一种最常见的现象),那么 java 命令行的类名前面就必须加上包名。
接下来我们尝试新增访问修饰符为 private 的方法,
package scopeTest;
public class Person {
//package Scope method
void hello(){
System.out.println("This is Person\'s hello method!");
}
private void hello2(){
System.out.println("This is Person\'s hello2 method!");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
可以看到编译都报错了
D:\JavaTest> javac ./scopeTest/Main.java
.\scopeTest\Main.java:7: 错误: hello2() 在 Person 中是 private 访问控制
p.hello2();
^
1 个错误
2
3
4
5
# import 的其他知识点
在写 import
的时候,可以使用 *
,表示把这个包下面的所有 class
都导入进来(但不包括子包的 class
):
import emilia.*;
我们一般不推荐这种写法,因为在导入了多个包后,很难看出类属于哪个包。
还有一种 import static
的语法,它可以导入可以导入一个类的静态字段和静态方法:
package importTest;
import importTest.*;
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
Person p = new Person();
p.hello();
out.println("Hello import!");
}
}
2
3
4
5
6
7
8
9
10
11
12
import static
很少使用。
# 不使用 import
如果我们用到了两个不同包下的同名类,或者我们仅仅是使用一次某个类,那么有时候不用导入也可以,只需写出全类名即可。
例如,我们使用 java.util.Date
类:
public class Hello {
public static void main(String[] args) {
java.util.Date d = new java.util.Date();
System.out.println(d);
}
}
2
3
4
5
6
编译和运行:
javac Hello.java
java Hello
Sat Nov 26 17:07:11 CST 2022
2
3
如果我们不说明的话,会报错。我们尝试去掉全类名:
public class Hello {
public static void main(String[] args) {
Date d = new Date();
System.out.println(d);
}
}
2
3
4
5
6
报错如下:
javac Hello.java
Hello.java:3: 错误: 找不到符号
Date d = new Date();
^
符号: 类 Date
位置: 类 Hello
.\example4\Hello.java:3: 错误: 找不到符号
Date d = new Date();
^
符号: 类 Date
位置: 类 Hello
2 个错误
2
3
4
5
6
7
8
9
10
11
12
如果有两个 class
名称相同,那么只能 import
其中一个,另一个必须写完整类名。
# 默认的 import
JVM 自带的 Java 标准库,实际上也是以 jar 文件形式存放的,这个文件叫 rt.jar
,一共有 60 多 M。
在我们运行代码的时候,Java 会默认帮我们导入这个 jar 包,相当于是 import java.lang.*
。这样一些基础的类就不用我们导入了,例如 String,Object。我们可以直接使用:
public class Hello {
public static void main(String[] args) {
String str = "Hello String";
Object obj = new Object();
System.out.println(d);
}
}
2
3
4
5
6
7
注意:自动导入的是 java.lang 包,但类似 java.lang.reflect 这些包仍需要手动导入。
# class 文件结构与包
一般情况下,编译后的 class 文件也按照包的结构来存放,这样比较规范,我们可以使用 javac -d 自动生成符合规范的 class。我们新建 3 个 package 和 Person 类
├── emilia
│ └── Person.java
├── ram
│ └── Person.java
└── rem
└── Person.java
2
3
4
5
6
文件夹里的内容如下:emilia/Person.java
package emilia;
public class Person {}
2
ram/Person.java:
package ram;
public class Person {}
2
rem/Person.java:
package rem;
public class Person {}
2
我们编译:
javac -d ./bin ./emilia/Person.java ./ram/Person.java ./rem/Person.java
编译后,class 文件也是按包的层次结构来存放的(会自动根据 package 的层次创建文件夹)
├── bin
│ ├── emilia
│ │ └── Person.class
│ ├── ram
│ │ └── Person.class
│ └── rem
│ └── Person.class
├── emilia
│ └── Person.java
├── ram
│ └── Person.java
└── rem
└── Person.java
2
3
4
5
6
7
8
9
10
11
12
13
全部编译:可以用通配符
javac -d ./bin ./emilia/*.java
在 Windows 下,不能用 */*.java
的方法(指全部子目录下的全部 Java 文件),因为 Windows 不支持。
# 参考
本文主要是参考廖雪峰老师的博客,自己动手实践而得,感谢:
包 - 廖雪峰的官方网站 (opens new window)
classpath 和 jar - 廖雪峰的官方网站 (opens new window)
Java 入门实例 classpath 及 package 详解 - 橘子山寨 - BlogJava (opens new window)
# 小结
Java 编译器最终编译出的 .class
文件只使用完整类名,因此,在代码中,当编译器遇到一个 class
名称时:
如果是完整类名,就直接根据完整类名查找这个
class
;如果是简单类名,按下面的顺序依次查找:
- 查找当前
package
是否存在这个class
; - 查找
import
的包是否包含这个class
; - 查找
java.lang
包是否包含这个class
。
- 查找当前
如果按照上面的规则还无法确定类名,则编译报错。
如果有两个 class
名称相同,那么只能 import
其中一个,另一个必须写完整类名。