Java中的非访问修改器关键字是一组关键字,你可以在Java类及其成员的声明中直接应用到这些关键字。
与访问修改器关键字一起,非访问修改器关键字将帮助你控制你的Java应用结构(类、方法和变量)的行为。
到今天为止,Java语言中目前有七个非访问修改器关键字。
最常用的三个修改器关键字是:
static- 适用于方法和变量final- 适用于类、方法和变量abstract- 适用于类和方法
而剩下的四个则使用频率较低:
synchronized- 适用于方法volatile- 适用于变量transient- 适用于变量native- 适用于方法
本教程将帮助你学习这些非访问修改器关键字在应用于Java类、构造函数、方法和变量时如何工作。
让我们从static 关键字开始。
静态修饰符的解释
static 修改器允许你创建独立于类的对象实例而存在的方法和变量。
通过将一个方法或变量声明为static ,你可以在不创建类的新实例的情况下调用它们。
例如,假设你有一个Person 类,其中有以下的变量和方法:
class Person {
static String name = "Nathan";
static void greetings(){
System.out.println("Hello from" + name);
}
}
那么无论何时你需要利用name 变量或Person 类的greetings() 方法,你都不需要创建一个新的Person 对象。
你可以直接调用Person 类的成员,如下图所示:
public class Main {
public static void main(String[] args) {
Person.name = "Jack";
Person.greetings(); // Hello from Jack
}
}
如果你有一些Java编程经验,你可能会注意到,static 修饰符被应用到许多Math 类成员中。
这就是为什么你能够调用Math 类的成员,如Math.PI 变量和Math.max() 方法,而无需创建一个新的Math 类的实例。
现在你知道了static 修改器,让我们来了解一下final 修改器。
最后的修饰符解释
final 修饰符允许你创建不能被修改的类、方法和变量。
当你对一个变量应用final 修改器时,那么该变量的值将保持不变:
class Person {
final String name = "Nathan";
}
当你创建一个上面的Person 类的实例时,你将不能改变name 变量的值,如下图所示:
public class Main {
public static void main(String[] args) {
Person developer = new Person();
developer.name = "Jack";
// ^ Error: cannot assign a value to final variable
}
}
当你使用final 修改器声明一个方法时,那么这个方法就不能被继承这个方法的类所覆盖。
假设你有如下的Person 类,有一个final 方法:
class Person {
final void greetings(){
System.out.println("Hello World!");
}
}
那么继承了Person 类的类就不能覆盖上面的greetings() 方法:
public class Main extends Person{
@Override
void greetings() {
// ^ Error: overridden method is final
super.greetings();
}
}
最后,当你在类的声明中应用final 修饰符时,那么你将不能创建一个继承该类方法和变量的子类。
下面是如何声明一个final 的类:
final class Person {
String name = "Nathan";
}
任何试图将extends Person 类的类都会抛出一个错误:cannot inherit from final class 。
final 修饰符经常与static 修饰符一起使用,以创建一个你可以用于你的应用程序的实用程序或辅助类。
Math 类就是这样一个辅助类的例子:
public static final double PI = 3.141592653589793;
通过应用final 修饰符,你不能意外地改变Math.PI 变量的值。
抽象修饰符的解释
abstract 修饰符主要用于Java类和方法。任何标记为abstract 的类都需要被扩展,不能被实例化。
假设你有下面这个abstract 类:
abstract class Person {
String name = "Nathan";
}
那么你将不能实例化一个如下的Person 类的对象:
public static void main(String[] args) {
Person person = new Person();
// ^ Error: Person is abstract; cannot be instantiated
}
一个abstract 类可以同时拥有abstract 和普通方法。一个abstract 方法必须被类的继承者所重写。
当你创建一个abstract 方法时,你只需要声明该方法和它的参数。你不需要创建该方法的主体块,如下图所示:
abstract class Person {
abstract void greetings(String name);
}
继承上述Person 类的类将需要覆盖greetings 方法以防止任何错误:
class Developer extends Person {
@Override
void greetings(String name) {
System.out.println("Hello from Developer "+ name);
}
}
现在你已经了解了三个最常用的Java非访问修饰符。
让我们继续学习剩下的四个非访问修饰符,首先是synchronized 修饰符。
同步修饰符的解释
synchronized 修饰符允许你控制一个共享方法的访问,防止该方法被多个线程同时访问。
当你想在一个共享资源被使用时阻止对它的访问时,这个修饰符很有用。
例如,假设你有一个方法可以打印一个从1到5的数字,如下所示:
class Number {
void printNumbers(String threadName) {
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + ": "+ i);
}
}
}
现在假设你有两个不同的线程,从Number 对象中调用printNumbers() 方法,如下所示:
class FirstThread extends Thread {
Number num;
FirstThread(Number num) {
this.num = num;
}
public void run() {
num.printNumbers("First thread");
}
}
class SecondThread extends Thread {
Number num;
SecondThread(Number num) {
this.num = num;
}
public void run() {
num.printNumbers("Second thread");
}
}
当你从你的main() 方法启动这两个线程时,那么这两个线程将执行printNumbers() 方法,产生不一致的输出:
public static void main(String[] args) {
Number num = new Number();
FirstThread ft = new FirstThread(num);
SecondThread st = new SecondThread(num);
ft.start();
st.start();
}
每次你运行上面的程序时,输出都会有所不同:
# First run
First thread: 1
Second thread: 1
First thread: 2
Second thread: 2
First thread: 3
Second thread: 3
First thread: 4
Second thread: 4
First thread: 5
Second thread: 5
# Second run
Second thread: 1
Second thread: 2
Second thread: 3
Second thread: 4
Second thread: 5
First thread: 1
First thread: 2
First thread: 3
First thread: 4
First thread: 5
# Third run
First thread: 1
Second thread: 1
Second thread: 2
Second thread: 3
Second thread: 4
Second thread: 5
First thread: 2
First thread: 3
First thread: 4
First thread: 5
为了解决这个不一致的问题,你可以声明两个不同的Number 对象传递给线程构造函数,如下图所示
public static void main(String[] args) {
Number numOne = new Number();
Number numTwo = new Number();
FirstThread ft = new FirstThread(numOne);
SecondThread st = new SecondThread(numTwo);
ft.start();
st.start();
}
或者你可以将printNumbers() 方法声明为synchronized ,如下所示:
class Number {
synchronized void printNumbers(String threadName) {
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + ": "+ i);
}
}
}
通过使用synchronized 修改器,只要printNumbers() 方法被第一个获得该方法访问权的线程使用,它就会被阻断,不被其他线程访问。
volatile修饰符的解释
volatile 关键字修改Java变量的方式与synchronized 关键字修改Java方法的方式相似。
volatile 修饰符使一个被多个线程访问的变量的读写过程保持一致。
一个线程可以在CPU缓存中存储一个变量的副本。每次线程更新该变量的值时,只有CPU缓存中的变量副本被更新,而CPU内存中的主变量仍然保留旧值。
通过使用volatile 修改器,Java将把变量的更新从CPU缓存同步到CPU内存中的主副本。
下面是如何声明一个volatile 的变量:
class Person {
volatile String name = "Nathan";
}
如果你是Java的新手,synchronized 和volatile 可能会让你感到困惑,但不用担心,因为它们比前三个修饰符使用的频率低。
瞬态修饰符解释
transient 修饰符允许你在对象序列化过程中跳过一个变量值,在反序列化过程中隐藏该值不被显示。
一个transient 变量需要一个实现了Serializable类的类。
下面是一个有transient 和普通变量的Person 类的例子:
import java.io.Serializable;
class Person implements Serializable {
transient int id = 779072;
String name = "Nathan";
}
当你序列化上面这个类的对象实例时,那么transient 变量id 将不会被写入输出文件。
那么当你把它反序列化为一个对象时,只有name 变量的值可以为你所用。
id 变量的值将被还原为其数据类型的默认值。在这种情况下,Integer类型的默认值是0 。
本机修改器解释
native 修改器允许你创建使用本地代码实现的抽象方法。
方法的签名看起来如下:
class Person {
native void greetings();
}
上面的native 关键字意味着greetings() 方法是用独立于平台的本地代码实现的。
为了使该方法发挥作用,你需要导入实现greetings() 方法的本地库。这个本地代码通常是用C或C++编写的。
本机代码可以使用Java Native Interface(简称JNI)来创建、编译和链接到你的java程序中。
如何使用JNI已经超出了本教程的范围。这里只需要说,native 修改器允许你调用使用本地代码实现并通过JNI链接到你的Java程序的方法。
总结
你已经了解了Java中的非访问修改器,以及它们如何影响你的Java应用程序结构的行为。
如果你仍然对它们感到困惑,请不要担心。随着你编写更多的Java应用程序,这些修饰符的使用对你来说将是本能的。