「Java」面向对象基础(2)
静态字段和静态方法
静态字段
实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。 静态字段并不属于实例。 虽然实例可以访问静态字段,但是它们指向的其实都是Person class
的静态字段。
在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段
来访问静态对象。
1 | class Person { |
静态方法
调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。
1 | class Person { |
因为静态方法属于class
而不属于实例,因此,静态方法内部,无法访问this
变量,也无法访问实例字段,它只能访问静态字段。
通过实例变量也可以调用静态方法,但这只是编译器自动帮我们把实例改写成类名而已。
接口的静态字段
因为interface
是一个纯抽象类,所以它不能定义实例字段。但是,interface
是可以有静态字段的,并且静态字段必须为final
类型:
1 | public interface Person { |
实际上,因为interface
的字段只能是public static final
类型,所以上述代码可以简写为:
1 | public interface Person { |
编译器会自动把该字段变为public static final
类型。
包
Java定义了一种名字空间,称之为包:package
。一个类总是属于某个包,类名只是一个简写,真正的完整类名是包名.类名
。
1 | package ming; // 申明包名ming |
包可以是多层结构,用.
隔开。例如:java.util
。包没有父子关系。没有定义包名的class
使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法。
包作用域
位于同一个包的类,可以访问包作用域的字段和方法。不用public
、protected
、private
修饰的字段和方法就是包作用域。
1 | package hello; |
1 | package hello; |
import
小明的ming.Person
类,如果要引用小军的mr.jun.Arrays
类 :
一、直接写出完整类名
1 | // Person.java |
二、import导入小军的Arrays
,然后写简单类名:
1 | // Person.java |
import mr.jun.*;
表示导入该包下的所有class
。
三、import static
,可以导入可以导入一个类的静态字段和静态方法:
1 | package main; |
编写class的时候,编译器会自动帮我们做两个import动作:
- 默认自动
import
当前package
的其他class
; - 默认自动
import java.lang.*
。
作用域
public
一个.java
文件只能包含一个public
类,但可以包含多个非public
类。如果有public
类,文件名必须和public
类的名字相同。
定义为public
的class
、interface
可以被其他任何类访问:
1 | package abc; |
上面的Hello
是public
,因此,可以被其他包的类访问:
1 | package xyz; |
定义为public
的field
、method
可以被其他类访问,前提是首先有访问class
的权限。
例如:上面的hi()
方法是public
,可以被其他类调用,前提是首先要能访问Hello
类:
1 | package xyz; |
private
定义为private
的field
、method
无法被其他类访问。 private
访问权限被限定在class
的内部,而且与方法声明顺序无关。推荐把private
方法放到后面,因为public
方法定义了类对外提供的功能,阅读代码的时候,应该先关注public
方法
由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private
的权限:
1 | public class Main { |
protected
protected
作用于继承关系。定义为protected
的字段和方法可以被子类访问,以及子类的子类
package
包作用域是指一个类允许访问同一个package
的没有public
、private
修饰的class
,以及没有public
、protected
、private
修饰的字段和方法。
1 | package abc; |
final
用final
修饰class
可以阻止被继承:
1 | package abc; |
用final
修饰method
可以阻止被子类覆写:
1 | package abc; |
用final
修饰field
可以阻止被重新赋值:
1 | package abc; |
用final
修饰局部变量可以阻止被重新赋值:
1 | package abc; |
classpath && jar
classpath
是JVM用到的一个环境变量,它用来指示JVM如何搜索class
。因为Java是编译型语言,源码文件是.java
,而编译后的.class
文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,如果要加载一个abc.xyz.Hello
的类,应该去哪搜索对应的Hello.class
文件。
classpath
就是一组目录的集合,它设置的搜索路径与操作系统相关。
假设classpath
是.;C:\work\project1\bin;C:\shared
,当JVM在加载abc.xyz.Hello
这个类时,会依次查找:
<当前目录>\abc\xyz\Hello.class
C:\work\project1\bin\abc\xyz\Hello.class
C:\shared\abc\xyz\Hello.class
不推荐在系统环境变量中设置classpath
,那样会污染整个系统环境。在启动JVM时设置classpath
才是推荐的做法。实际上就是给java
命令传入-classpath
或-cp
参数:
1 | java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello |
或者使用-cp
简写:
1 | java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello |
没有设置系统环境变量,也没有传入-cp
参数,那么JVM默认的classpath
为.
,即当前目录。
对于核心库,JVM根本不依赖classpath加载核心库
jar包
可以把package
组织的目录层级,以及各个目录下的所有文件(包括.class
文件和其他文件)都打成一个jar文件,这样一来,无论是备份,还是发给客户,就简单多了。
jar包实际上就是一个zip格式的压缩文件,而jar包相当于目录。如果我们要执行一个jar包的class
,就可以把jar包放到classpath
中:
1 | java -cp ./hello.jar abc.xyz.Hello |
这样JVM会自动在hello.jar
文件里去搜索某个类。
创建jar包
在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip
改为.jar
,一个jar包就创建成功。
注意:jar包里的第一层目录,不能是bin
。
jar包还可以包含一个特殊的/META-INF/MANIFEST.MF
文件,MANIFEST.MF
是纯文本,可以指定Main-Class
和其它信息。JVM会自动读取这个MANIFEST.MF
文件,如果存在Main-Class
,我们就不必在命令行指定启动的类名,而是用更方便的命令:
1 | java -jar hello.jar |
jar包还可以包含其它jar包,这个时候,就需要在MANIFEST.MF
文件里配置classpath
了。
在大型项目中,不可能手动编写MANIFEST.MF
文件,再手动创建zip包。Java社区提供了大量的开源构建工具,例如Maven,可以非常方便地创建jar包。
模块
如果a.jar
必须依赖另一个b.jar
才能运行,那我们应该给a.jar
加点说明啥的,让程序在编译和运行的时候能自动定位到b.jar
,这种自带“依赖关系”的class容器就是模块。
把一堆class封装为jar仅仅是一个打包的过程,而把一堆class封装为模块则不但需要打包,还需要写入依赖关系,并且还可以包含二进制代码(通常是JNI扩展)。此外,模块支持多版本,即在同一个模块中可以为不同的JVM提供不同的版本。
编写模块
首先,创建模块和原有的创建Java项目是完全一样的,以oop-module
工程为例,它的目录结构如下:
1 | oop-module |
其中,bin
目录存放编译后的class文件,src
目录存放源码,按包名的目录结构存放,仅仅在src
目录下多了一个module-info.java
这个文件,这就是模块的描述文件。在这个模块中,它长这样:
1 | module hello.world { |
其中,module
是关键字,后面的hello.world
是模块的名称,它的命名规范与包一致。花括号的requires xxx;
表示这个模块需要引用的其他模块名。除了java.base
可以被自动引入外,这里我们引入了一个java.xml
的模块。
当我们使用模块声明了依赖关系后,才能使用引入的模块。例如,Main.java
代码如下:
1 | package com.itranswarp.sample; |
- 切换工作目录到oop-module,在当前目录下编译所有的.java文件,并存放到bin目录下,命令如下:
1 | D:\Program Files\Eclipse\oop-module>javac -d bin src/module-info.java src/com/itranswarp/sample/*.java |
编译成功,原来空白的bin目录下多了class文件:
src目录下的module-info.java被编译到bin目录下的 module-info.class;
src目录下的com/itranswarp/sample/Main.java
和 Greeting.java
被编译到bin/com/itranswarp/sample
目录下的Main.class
和 Greeting.class
- 把bin目录下的所有class文件先打包成jar:
1 | D:\Program Files\Eclipse\oop-module>jar --create --file hello.jar --main-class |
(bin后面是空格再加点(.)表示当前目录)
编译成功后,当前目录增加一个hello.jar文件;
可以直接运行:java -jar hello.jar
- 把jar包转换成模块(.jmod):
1 | D:\Program Files\Eclipse\oop-module>jmod create --class-path hello.jar hello.jmod |
编译成功后,当前目录下得到一个hello.jmod模块文件;
可以直接运行:java --module-path hello.jar --module hello.world
- 打包JRE,jlink裁剪程序用到的模块
D:\Program Files\Eclipse\oop-module>jlink --module-path hello.jmod --add-modules
java.base,java.xml,hello.world --output jre/
- 切换到jre目录,运行JRE
D:\Program Files\Eclipse\oop-module\jre\bin>java --module hello.world