静态字段和静态方法

静态字段

实例字段在每个实例中都有自己的一个独立“空间”,但是静态字段只有一个共享“空间”,所有实例都会共享该字段。 静态字段并不属于实例。 虽然实例可以访问静态字段,但是它们指向的其实都是Person class的静态字段。

在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段来访问静态对象。

1
2
3
4
5
6
class Person {
public String name;
public int age;

public static int number; // 静态字段
}

静态方法

调用实例方法必须通过一个实例变量,而调用静态方法则不需要实例变量,通过类名就可以调用。

1
2
3
4
5
6
7
8
9
10
11
class Person {
public static int number;

public static void setNumber(int value) { // 静态方法
number = value;
}
}

...
Person.setNumber(99);
...

因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。

通过实例变量也可以调用静态方法,但这只是编译器自动帮我们把实例改写成类名而已。

接口的静态字段

因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型:

1
2
3
4
public interface Person {
public static final int MALE = 1;
public static final int FEMALE = 2;
}

实际上,因为interface的字段只能是public static final类型,所以上述代码可以简写为:

1
2
3
4
5
public interface Person {
// 编译器会自动加上public statc final:
int MALE = 1;
int FEMALE = 2;
}

编译器会自动把该字段变为public static final类型。

Java定义了一种名字空间,称之为包:package。一个类总是属于某个包,类名只是一个简写,真正的完整类名是包名.类名

1
2
3
4
package ming; // 申明包名ming

public class Person {
}

包可以是多层结构,用.隔开。例如:java.util。包没有父子关系。没有定义包名的class使用的是默认包,非常容易引起名字冲突,因此,不推荐不写包名的做法。

包作用域

位于同一个包的类,可以访问包作用域的字段和方法。不用publicprotectedprivate修饰的字段和方法就是包作用域。

1
2
3
4
5
6
7
8
package hello;

public class Person {
// 包作用域:
void hello() {
System.out.println("Hello!");
}
}
1
2
3
4
5
6
7
8
package hello;

public class Main {
public static void main(String[] args) {
Person p = new Person();
p.hello(); // 可以调用,因为Main和Person在同一个包
}
}

import

小明的ming.Person类,如果要引用小军的mr.jun.Arrays类 :

一、直接写出完整类名

1
2
3
4
5
6
7
8
// Person.java
package ming;

public class Person {
public void run() {
mr.jun.Arrays arrays = new mr.jun.Arrays();
}
}

二、import导入小军的Arrays,然后写简单类名:

1
2
3
4
5
6
7
8
9
10
11
// Person.java
package ming;

// 导入完整类名:
import mr.jun.Arrays;

public class Person {
public void run() {
Arrays arrays = new Arrays();
}
}

import mr.jun.*;表示导入该包下的所有class

三、import static,可以导入可以导入一个类的静态字段和静态方法:

1
2
3
4
5
6
7
8
9
10
11
package main;

// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;

public class Main {
public static void main(String[] args) {
// 相当于调用System.out.println(…)
out.println("Hello, world!");
}
}

编写class的时候,编译器会自动帮我们做两个import动作:

  • 默认自动import当前package的其他class
  • 默认自动import java.lang.*

作用域

public

一个.java文件只能包含一个public类,但可以包含多个非public类。如果有public类,文件名必须和public类的名字相同。

定义为publicclassinterface可以被其他任何类访问:

1
2
3
4
5
6
package abc;

public class Hello {
public void hi() {
}
}

上面的Hellopublic,因此,可以被其他包的类访问:

1
2
3
4
5
6
7
8
package xyz;

class Main {
void foo() {
// Main可以访问Hello
Hello h = new Hello();
}
}

定义为publicfieldmethod可以被其他类访问,前提是首先有访问class的权限。

例如:上面的hi()方法是public,可以被其他类调用,前提是首先要能访问Hello类:

1
2
3
4
5
6
7
8
package xyz;

class Main {
void foo() {
Hello h = new Hello();
h.hi();
}
}

private

定义为privatefieldmethod无法被其他类访问。 private访问权限被限定在class的内部,而且与方法声明顺序无关。推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法

由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private的权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) {
Inner i = new Inner();
i.hi();
}

// private方法:
private static void hello() {
System.out.println("private hello!");
}

// 静态内部类:
static class Inner {
public void hi() {
Main.hello();
}
}
}

protected

protected作用于继承关系。定义为protected的字段和方法可以被子类访问,以及子类的子类

package

包作用域是指一个类允许访问同一个package的没有publicprivate修饰的class,以及没有publicprotectedprivate修饰的字段和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package abc;
// package权限的类:
class Hello {
// package权限的方法:
void hi() {
}
}

class Main {
void foo() {
// 可以访问package权限的类:
Hello h = new Hello();
// 可以调用package权限的方法:
h.hi();
}
}

final

final修饰class可以阻止被继承:

1
2
3
4
5
6
7
8
9
package abc;

// 无法被继承:
public final class Hello {
private int n = 0;
protected void hi(int t) {
long i = t;
}
}

final修饰method可以阻止被子类覆写:

1
2
3
4
5
6
7
package abc;

public class Hello {
// 无法被覆写:
protected final void hi() {
}
}

final修饰field可以阻止被重新赋值:

1
2
3
4
5
6
7
8
package abc;

public class Hello {
private final int n = 0;
protected void hi() {
this.n = 1; // error!
}
}

final修饰局部变量可以阻止被重新赋值:

1
2
3
4
5
6
7
package abc;

public class Hello {
protected void hi(final int t) {
t = 1; // error!
}
}

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
2
3
4
5
6
7
8
9
10
oop-module
├── bin
├── build.sh
└── src
├── com
│ └── itranswarp
│ └── sample
│ ├── Greeting.java
│ └── Main.java
└── module-info.java

其中,bin目录存放编译后的class文件,src目录存放源码,按包名的目录结构存放,仅仅在src目录下多了一个module-info.java这个文件,这就是模块的描述文件。在这个模块中,它长这样:

1
2
3
4
module hello.world {
requires java.base; // 可不写,任何模块都会自动引入java.base
requires java.xml;
}

其中,module是关键字,后面的hello.world是模块的名称,它的命名规范与包一致。花括号的requires xxx;表示这个模块需要引用的其他模块名。除了java.base可以被自动引入外,这里我们引入了一个java.xml的模块。

当我们使用模块声明了依赖关系后,才能使用引入的模块。例如,Main.java代码如下:

1
2
3
4
5
6
7
8
9
10
11
package com.itranswarp.sample;

// 必须引入java.xml模块后才能使用其中的类:
import javax.xml.XMLConstants;

public class Main {
public static void main(String[] args) {
Greeting g = new Greeting();
System.out.println(g.hello(XMLConstants.XML_NS_PREFIX));
}
}
  1. 切换工作目录到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

  1. 把bin目录下的所有class文件先打包成jar:
1
2
D:\Program Files\Eclipse\oop-module>jar --create --file hello.jar --main-class
com.itranswarp.sample.Main -C bin .

(bin后面是空格再加点(.)表示当前目录)

编译成功后,当前目录增加一个hello.jar文件;

可以直接运行:java -jar hello.jar

  1. 把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

  1. 打包JRE,jlink裁剪程序用到的模块

D:\Program Files\Eclipse\oop-module>jlink --module-path hello.jmod --add-modules

java.base,java.xml,hello.world --output jre/

  1. 切换到jre目录,运行JRE

D:\Program Files\Eclipse\oop-module\jre\bin>java --module hello.world