了解Java中的非访问修改器关键字

131 阅读8分钟

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的新手,synchronizedvolatile 可能会让你感到困惑,但不用担心,因为它们比前三个修饰符使用的频率低。

瞬态修饰符解释

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应用程序,这些修饰符的使用对你来说将是本能的。