Bootstrap

Java 入门指南:非访问修饰符

在Java中,非访问修饰符(也称为非权限修饰符)主要用于控制类、方法或变量的特定行为,而不是访问级别。这些修饰符不改变成员(变量、方法或类)的可见性,而是影响它们的使用方式、存储方式或执行方式。

static

在Java中,static 修饰符是一种关键字,用于指定成员(变量、方法、嵌套类等)和类的静态特性

特点

  1. 静态成员:使用 static 修饰的成员(变量或方法)属于类本身,而不是类的实例。它们与类的实例无关,可以在没有创建实例的情况下直接使用。

  2. 共享性:静态成员在类的多个实例之间共享,只有一份拷贝。对静态成员的修改会影响到所有使用它的实例。

  3. 静态上下文:静态成员不依赖于类的实例,可以在静态上下文(即静态方法或静态代码块)中直接访问。

使用场景

  1. 静态变量:使用 static 修饰的成员变量被称为静态变量(或类变量),在类的所有实例之间共享,可以通过类名直接访问。静态变量常用于保存与类相关的信息,如常量、计数器等。
    无论一个类实例化多少对象,它的静态变量只有一份拷贝。 静态变量也被称为类变量。局部变量不能被声明为 static 变量

  2. 静态方法:使用 static 修饰的成员方法被称为静态方法(或类方法),可以直接通过类名调用,无需创建类的实例。静态方法不能访问非静态成员(变量、方法),因为它们不依赖于类的实例。

  3. 静态代码块:使用 static 修饰的代码块被称为静态代码块,用于在类的加载过程中执行一些初始化操作。静态代码块在类加载时运行一次,用于初始化静态变量,会优先于 main() 方法执行。

Java 7 之前,无 main() 方法,静态代码块也会执行成功,Java 7 及之后就无法执行了,会抛出 NoClassDefFoundError 的错误

在实际的项目开发中,通常使用静态代码块来加载配置文件到内存当中

  1. 静态内部类:Java 允许我们在一个类中声明一个内部类,它允许我们只在一个地方使用一些变量,使代码更具有条理性和可读性

    常见的内部类有四种,成员内部类局部内部类匿名内部类静态内部

public class Test {  
    private static int count = 0;  
      
    public static void increment() {  
        count++;  
    }  
      
    static {  
        System.out.println("静态代码块执行");  
    }  
}

使用规范

  1. 命名约定:静态变量使用全大写字母、下划线分隔的命名方式,而静态方法使用驼峰命名法。

  2. 静态方法的限制:静态方法只能访问静态成员,无法访问非静态成员,因为非静态成员依赖于类的实例。

  3. 静态上下文的限制:静态上下文中无法使用this关键字,因为静态上下文不依赖于类的实例。

  4. 作为全局状态的容易被修改:静态成员的共享性使得它们容易被修改,可能导致全局状态变得不可预测,因此需要谨慎使用,不滥用全局状态。

  5. 线程安全问题:静态成员的共享性可能导致线程安全问题,需要根据具体情况进行同步或采取其他线程安全措施。

  6. 适度使用:只有在确实需要被多个实例共享或无需实例化的成员才应该使用 static 修饰符,避免滥用。

final

在Java中,final 是一种修饰符,可以用于修饰类、方法和变量。使用 final 修饰符可以表示不可改变的特性

使用场景

  1. 修饰类:使用final修饰的类是不可继承的,即不能有子类。声明为final的类通常是为了防止类的修改或继承破坏原有的设计和实现

  2. 修饰方法:使用final修饰的方法不能被子类重写(覆盖),即具有最终实现。在类的设计中,有时可能希望某个方法的实现不被修改或不允许子类进行修改,可以使用 final 修饰该方法。

  3. 修饰变量:使用 final 修饰的变量称为常量,一旦赋予初值后就不可再次修改。final 变量必须在声明时或构造函数中赋值,且不能再被赋予新值。

public final class Test {  
    public final void show() {  
        // 方法体  
    }  
      
    public static final int MAX_VALUE = 100;  
}

使用规范

  1. 命名约定:final 常量的命名通常使用全大写字母,并使用下划线进行分隔。

  2. 及早初始化:final 变量应在声明时或构造函数中赋予初值,不应延迟初始化。

  3. 保持一致性:在整个代码库中保持使用 final 修饰符的一致性,以提高代码的可读性和可维护性。

  4. 谨慎使用:final 修饰符应根据实际需求进行选择,避免滥用。只有在确实需要不可修改性或禁止继承的情况下使用 final 修饰符。

  5. 不仅仅是编码约定:final 修饰符不仅仅是一种编码约定,还可以帮助编译器和开发人员理解代码的意图和约束。

abstract

在Java中,包括在许多编程语言中、C#和C++等,abstract 是一个修饰符(modifier),用于声明一个抽象类或抽象方法。声明抽象类的唯一目的是为了将来对该类进行扩充

抽象类和抽象方法的主要目的是为了提供一个继承的框架,让派生类必须实现某些方法,同时又允许为这些方法提供一些通用的实现

通过使用抽象类和抽象方法,可以在代码中定义一些通用的行为和规范,增强代码的可扩展性和复用性,提高代码的可维护性和可读性。

对于只需要定义一些抽象方法的情况,可以考虑使用接口来替代抽象类,以提供更大的灵活性和扩展性。

使用场景

  1. 抽象类(Abstract Class)
    通过使用abstract关键字修饰类,可以将一个类声明为抽象类。抽象类不能被直接实例化,只能被继承,作为其他类的基类使用。

    抽象类可以包含抽象方法和非抽象方法,可以有普通方法的实现和成员变量。抽象类通常用于定义一些通用的行为和规范,要求派生类必须实现其中的抽象方法。若派生类未实现其中的抽象方法,那么该派生类也要声明为抽象类。

    例如:

 abstract class Animal {
     public abstract void makeSound();
 }
  1. 抽象方法(Abstract Method)
    通过方法声明中使用 abstract 关键字,可以将一个方法声明为抽象方法。
    当方法被声明为抽象方法时,它没有方法体,只有方法签名(没有具体的实现)。抽象方法必须声明在抽象类中,并且派生类必须实现这些抽象方法。抽象方法只能存在于抽象类中,不能在普通类中定义。

    例如:

 abstract class Animal {
     public abstract void makeSound();
 }

 class Dog extends Animal {
     public void makeSound() {
         System.out.println("汪汪!");
     }
 }
  1. 接口(Interface):通过使用 interface 关键字声明一个接口,其中的方法默认都是抽象方法。

使用规范

  1. 抽象类命名规范:通常抽象类的命名应该以抽象概念为前缀,例如 AbstractBase等,并且类名要具有描述性。

  2. 抽象方法命名规范:抽象方法的命名应该以动词开头,描述方法的行为。

  3. 子类必须实现抽象方法:如果一个类继承了抽象类或实现了接口,那么它必须实现所有的抽象方法,除非它自己也声明为抽象类或接口。

  4. 抽象类不能被实例化:抽象类不能创建对象,只能作为父类被继承。

  5. 抽象方法没有方法体:抽象方法只有方法签名,没有具体的实现代码。

  6. 接口的方法默认是抽象方法:在接口中声明的方法默认都是抽象方法,不需要使用 abstract 关键字。

synchronized

synchronized 是Java中的关键字,用于实现线程的同步和互斥synchronized 关键字可以用于修饰方法或代码块,用于控制对共享资源的访问。它的主要作用是 保证多个线程在并发执行时的安全性和一致性

使用场景

  1. 修饰方法:可以使用 synchronized 修饰方法,将整个方法设置为同步方法。

    当一个线程进入同步方法时,会自动获取该方法所属对象(或类)的锁,在同步方法中:

    • 对于非静态方法,使用的锁是当前实例对象(this)
public synchronized void method(){}
  • 对于静态方法,锁为当前类对象(Class对象)
public static synchronized void method(){}

其他线程在获取到锁之前将被阻塞,必须等待锁的释放才能执行该方法

线程A 调用一个对象的非静态同步方法,线程B 调用同一对象的静态同步方法时,不会发生互斥,因为方法占用的锁不同。

public class Counter {  
    private int count = 0;  
      
    public synchronized void increment() {  
        count++;  
    }  
}
  1. 修饰代码块:可以使用 synchronized 修饰代码块,指定需要同步的代码范围。synchronized 代码块 需要指定传入一个对象作为锁,只有持有该对象锁的线程才能执行该代码块,其他线程将被阻塞,需要等待锁的释放才能执行。
sychronized(obj){
	// ...
}

一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他 synchronized 方法,但是其他线程还是可以访问该对象的其他 synchronized 方法。

线程A 在释放锁之前所有可见的共享变量,在 线程B 获取同一个锁之后,将立刻变得对 线程B 可见。

使用规范

  1. 锁的粒度:应根据实际需求合理选择锁的粒度,过大的锁粒度可能导致性能问题,过小的锁粒度可能降低并发性能。

  2. 避免死锁:在使用多个锁的情况下,需要避免产生死锁的情况,即循环等待锁的发生,避免循环依赖和相互等待锁的情况。

  3. 锁的重入:同一个线程在拥有锁的情况下,可以重入同步代码,即可以再次获取相同锁。

  4. 精确控制同步范围:应尽量缩小同步代码块的范围,只对必要的操作进行同步,提高程序的并发性能。

  5. 锁的选择:根据实际需求,选择适当的锁对象,避免多个线程无意中使用了相同的锁对象,导致不必要的同步。

  6. 需要创建多个实例对象时,将 synchronized 作用于静态方法,防止不同的对象进入各自的对象锁,破坏线程安全。

通过合理使用 synchronized 关键字,可以确保多线程环境下数据的安全性,避免竞态条件和线程冲突,保证程序的正确性和稳定性。遵循synchronized 的规范和最佳实践,可以优化代码的性能和可维护性。

transient

transient 是Java语言中的一个关键字(只能修饰字段),用于标记类中的成员变量。当一个成员变量被 transient 修饰时,在 对象的序列化过程中,此成员变量的值将被忽略,不会被序列化到输出流中

在实际开发过程中,我们常常会遇到这样的问题,一个类的有些字段需要序列化,有些字段不需要,比如说用户的一些敏感信息(如密码、银行卡号等),为了安全起见,不希望在网络操作中传输或者持久化到磁盘文件中,那这些字段就可以加上 transient 关键字。

使用场景

在需要进行序列化的类中,将不希望被序列化的字段用 transient 关键字修饰。例如:

import java.io.Serializable;

public class Person implements Serializable {
	private static final long serialVersionUID = 1L;
    
    private String name;
    private transient int age;
    // 其他字段、构造方法和方法省略
}

在上述示例中,age 字段被 transient 关键字修饰,表示在对象的序列化过程中,该字段不会被持久化存储

transient 关键字修饰的成员变量在反序列化时会被自动初始化为默认值,例如基本数据类型为 0,引用类型为 null,所以要在程序中做适当的处理。

使用规范

  • 字段必须是可序列化的类的成员,即类必须实现 Serializable 接口。

  • 一般来说,被 transient 修饰的字段都是不可序列化的字段,如密码、临时变量等。

  • transient 关键字只能修饰字段,不能修饰方法和类。

  • transient 关键字修饰的字段,在对象进行序列化和反序列化过程中将被忽略。字段的默认值在反序列化后将被恢复。

  • 如果一个类继承了其他类,并添加一个字段并用 transient 修饰,那么父类中该字段的序列化与反序列化行为不受影响。

  • 在某些情况下,transient 字段可能需要手动进行初始化,在对象反序列化后,需要有相应的逻辑来恢复其值,以避免反序列化后出现空值,否则字段会被赋予其类型的默认值。

注意事项

  1. 不可序列化的成员变量:被 transient 修饰的成员变量将不参与对象的序列化过程。这通常适用于一些不需要被序列化的敏感数据、临时数据或可通过其他方式恢复的数据。

  2. 默认值:被 transient 修饰的成员变量在反序列化后会被赋予默认值,例如 数值类型为0,布尔类型为false,引用类型为null,而不是被序列化时的值

  3. 额外处理:如果需要在序列化和反序列化过程中对被 transient 修饰的成员变量进行特殊的处理,需要自定义实现writeObject()和readObject() 方法来手动保存和恢复这些成员变量的状态。

  4. 注意安全性:被transient修饰的成员变量在内存中仍然存在,所以在处理敏感数据时要格外小心,确保适当地处理和清除对应的数据。

volatile

volatile,用于声明变量,用来修饰被不同线程访问和修改的共享变量,确保变量的可见性和有序性。在 JVM 底层,volatile 是用内存屏障实现的。

观察汇编代码,对变量加入 volatile 关键字时,会多出一个 lock 前缀指令,lock 前缀指令实际上相当于一个内存屏障(也称内存栅栏)

使用场景

  1. 修饰变量:可以使用 volatile 修饰变量,即 volatile 变量。被 volatile 修饰的变量具有可见性和禁止重排序的特性。

    volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值

    成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。一个 volatile 对象引用可能是 null。

public class Flag {  
    private volatile boolean stop = false;  
      
    public void run() {  
        while (!stop) {  
            // 执行任务  
        }  
    }  
      
    public void stopTask() {  
        stop = true;  
    }  
}
  1. 控制循环条件:当多个线程共同操作一个控制循环的标志变量时,可以使用 volatile 确保标志的及时可见性。

  2. 状态标记:当某个变量表示对象的状态,并且该状态可能被多个线程共享和修改时,使用 volatile 修饰该变量可以保证状态的一致性,确保共享变量的状态在不同线程间同步。

  3. 单例模式的实现:在双重检查锁定的单例模式中,可以使用 volatile 关键字修饰实例变量,确保实例的可见性和正确初始化。

volatile变量特性

  1. 可见性:被 volatile 修饰的变量对于所有线程都是可见的。当一个线程修改了 volatile 变量的值,其他线程可以立即看到最新的值,而不会使用过期的缓存副本。

  2. 禁止重排序:被 volatile 修饰的变量的读写操作具有禁止重排序的效果。它可以保证 volatile 变量的赋值操作不会被编译器重排序到其他内存操作的前面或后面。

  3. 轻量级同步机制:volatile 提供了一种轻量级的线程同步机制,避免了使用锁造成的线程切换和上下文切换的开销。

使用规范

  1. 可见性要求:只有当变量的值可能会被多个线程同时访问并且其中一个线程对变量的写操作,而其他线程需要读取该变量的最新值时,才使用volatile 关键字。

    如果变量只会被单个线程访问,或者多个线程访问但只有某个线程对其进行写操作,那么 volatile 关键字是不必要的。

  2. 不具备原子性:volatile 关键字只能确保对变量的读取和写入操作的可见性,但并不能保证复合操作的原子性。对于多线程并发更新的场景,需要使用其他具有原子性保证的机制

  3. 替代锁的使用:在某些场景下,volatile 可以作为一种轻量级的替代锁的机制,用于实现简单的线程同步。但对于复杂的同步需求,volatile不能保证访问变量的线程之间的互斥性

    例如,多个线程对一个 volatile 变量进行自增操作时,可能会发生竞态条件。这种情况下,应该使用其他同步机制(例如 Locksynchronized)来保证原子性。

    如果需要实现互斥访问,应该使用其他同步机制,如 synchronizedReentrantLock 等。

  4. 避免依赖于先前状态:当使用 volatile 关键字修饰变量时,应当避免依赖于变量的先前状态。因为多个线程之间无法保证操作的顺序,某个线程读取到的值可能并不是最新的。

    如果需要依赖于先前状态,那么 volatile 关键字可能并不适用,应该使用其他同步机制。

  5. 深入理解用法:使用volatile关键字需要深入理解其特性和适用场景,避免滥用。正确地使用 volatile 可以解决一些特定的并发问题

;