文章目录
一、简介
在 Java 中,修饰符可以分为访问修饰符和非访问修饰符,一般是放在语句的最前端,用于定义类、方法、变量等的访问权限或其他特性。
二、访问修饰符
2.1 public(公有的)
这是最宽松的访问级别。被 public
修饰的类、方法或变量可以被任何其他类访问,无论它们是否在同一个包中,通常用于定义对外提供服务的接口或重要的复用类。
2.2 protected(受保护的)
被 protected
修饰的成员可以被同一个包中的类访问,也可以被不同包下的子类访问,保护成员在一定程度上平衡了封装性和继承性的需求。
2.3 default(默认的)
当没有显式指定任何访问修饰符时,成员具有默认的访问权限。具有默认访问权限的成员只能被同一个包中的其他类访问,适用于在同一个包内协作的类之间共享的实现细节。
⚠️注意:default
这个关键字比较特殊,既属于程序控制语句,也属于类,方法和变量修饰符,还属于访问控制修饰符。
- 在程序控制语句中,当在 switch 中匹配不到任何情况时,可以使用 default 来编写默认匹配的情况。
- 在类,方法和变量修饰符中,从 JDK8 开始引入了默认方法,可以使用 default 关键字来定义一个方法的默认实现。
- 在访问控制修饰符中,如果一个方法前没有任何修饰符,则默认会有一个修饰符 default,但是如果加上了这个修饰符编译器就会报错。
2.4 private(私有的)
这是最严格的访问级别。被 private
修饰的成员只能在其所属的类内部访问,它提供了最高级别的封装性,隐藏了类的内部实现细节,防止外部类直接访问和修改。
2.5 总结(访问权限)
下表列出了被访问修饰符修饰的类、方法、变量等的访问权限:
访问修饰符 | 本类 | 同一包 | 子类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | |
default(默认) | √ | √ | ||
private | √ |
三、非访问修饰符
3.1 static(静态的)
静态变量
-
共享特性:由于静态变量属于类本身,而不是类的对象,所以无论创建多少个类的实例,它们都共享同一个静态变量。这在需要维护一些全局状态或者统计信息时非常有用。
-
访问方式:可以通过类名直接访问静态变量,无需创建对象。这种访问方式使得静态变量在整个程序中都很容易被访问到,提高了代码的可读性和可维护性。
静态方法
-
独立性:静态方法属于类本身,不依赖于类的实例。这意味着静态方法可以在没有创建类的对象的情况下被调用,非常适合执行一些与类相关的通用操作,而不需要涉及类的具体实例状态。
-
访问限制:在静态方法中只能访问静态成员(包括静态变量和静态方法),不能直接访问实例成员。这是因为静态方法在类加载时就存在了,而实例成员只有在创建对象后才存在。
静态代码块
-
初始化时机:静态代码块在类加载时执行,且只执行一次。这使得静态代码块非常适合进行一些一次性的初始化操作,比如初始化静态变量、加载资源文件等。
-
顺序执行:如果一个类中有多个静态代码块,它们会按照在代码中的顺序依次执行。这可以用于确保一些初始化操作的顺序性。
3.2 final(最终的)
修饰变量
-
基本数据类型变量:修饰基本数据类型的变量时,一旦变量被初始化,它的值就不能再被改变,否则编译器报错。这就像给变量设定了一个不可更改的常量值。
-
引用数据类型变量:对于引用数据类型的变量,其引用不能再指向其他对象。这意味着一旦这个变量被赋值为一个对象的引用,它就不能再被重新赋值为另一个对象的引用。但是,这个被引用的对象的内容是可以修改的。
修饰方法
- 禁止重写:方法被修饰时,它不能被子类重写。这在一些情况下非常有用,比如当一个方法的实现是确定的,不希望在子类中被改变时,这个时候就可以使用。
修饰类
- 禁止继承:类被修饰时,这个类不能被继承,即不能有子类。这可以确保这个类的实现是最终的,不能被扩展或修改,有时候,为了防止恶意的继承和修改,可以将一些关键的类标记为final。这样可以确保这些类的行为是可靠的,不会被意外地破坏。
3.3 abstract(抽象的)
抽象类
-
不能创建对象:抽象类不能被实例化,即不能使用 new 关键字来创建抽象类的对象。这是因为抽象类通常只是作为一个框架,定义了一些共同的属性和方法,但可能缺少具体的实现。
-
提供框架:抽象类通常包含抽象方法和非抽象方法。非抽象方法可以提供一些通用的实现,而抽象方法则为子类定义了必须实现的行为,为一组相关的类提供了一个共同的结构和行为规范,有助于提高代码的可维护性和可扩展性。
抽象方法
-
没有方法体:抽象方法只有方法签名(方法的名称、参数类型和参数顺序),没有具体的方法体。这意味着抽象方法只定义了方法的名称、参数列表和返回类型,但没有具体的实现代码。
-
强制子类实现:包含抽象方法的类必须声明为抽象类。当一个子类继承一个抽象类时,它必须实现父类中的所有抽象方法,除非子类本身也是抽象类。这样可以确保子类具有特定的行为,满足抽象类所定义的约定。
3.4 synchronized(同步的)
同步方法
-
作用机制:方法被修饰时,它会在方法上实现同步。这意味着在同一时刻,只能有一个线程可以执行这个方法。当一个线程进入同步方法时,它会获取该方法所属对象的内置锁。其他线程如果也想执行这个方法,就必须等待当前线程释放这个锁。
-
适用场景:适用于多个线程需要同时访问一个对象的某个方法,并且这个方法对对象的状态进行修改的情况。通过同步方法可以确保对对象状态的修改是线程安全的,避免数据不一致的问题。
同步代码块
-
锁对象:在同步代码块中,需要指定一个唯一的锁对象。这个锁对象可以是任何对象,但通常选择一个专门用于同步的对象,以避免与其他可能的锁对象冲突。
-
作用范围控制:同步代码块可以更精确地控制同步的范围,只对需要同步的关键代码部分进行同步,而不是整个方法。这样可以减少同步带来的性能开销,提高程序的执行效率。
性能考虑
- 性能下降原因:过度使用 synchronized 可能会导致性能下降。这是因为当一个线程获取 synchronized 锁后,其他线程必须等待这个锁被释放,这会导致线程阻塞和切换,增加了系统的开销。特别是在高并发的情况下,如果大量线程竞争同一个锁,可能会导致线程频繁地阻塞和唤醒,从而降低程序的执行效率。
- 替代方案选择:在实际应用中,需要根据具体的业务场景和性能需求,合理地使用或者选择其他更高效的同步机制。例如,ReentrantLock 是一种可重入锁,它提供了与 synchronized 类似的功能,但在一些情况下可能具有更好的性能。此外,还可以使用并发容器(如ConcurrentHashMap)、原子类(如AtomicInteger)等非阻塞的同步机制来提高程序的并发性能。
3.5 transient(瞬态的)
防止敏感信息序列化
-
保护隐私:在某些情况下,对象中可能包含敏感信息,如用户密码、加密密钥等。这些信息不应该被序列化并存储在磁盘上或通过网络传输,以防止信息泄露。通过将包含敏感信息的变量声明为 transient,可以确保在对象序列化时,这些变量不会被包含在序列化的数据中。
-
符合安全规范:在处理敏感数据时,遵循安全最佳实践通常要求对敏感信息进行特殊处理。使用 transient 关键字是一种简单而有效的方法,可以确保敏感信息不会被意外地序列化。
优化序列化效率
- 减少数据量:有些变量在反序列化时可以重新计算或获取,或者不是关键的状态信息。将这些变量标记为 transient 可以减少序列化后的数据量,从而提高序列化和反序列化的效率。
3.6 volatile(易变的)
保证可见性
-
多线程同步问题:在多线程环境中,每个线程都有自己的工作内存,线程对变量的操作首先在自己的工作内存中进行,然后再刷新到主内存中。如果没有适当的同步机制,一个线程对变量的修改可能不会立即被其他线程看到,这就可能导致数据不一致的问题。
-
作用机制:变量被修饰时,Java 内存模型会确保所有线程看到这个变量的最新值。每次读取这个变量时,都会直接从主内存中读取,而不是从线程自己的工作内存中读取。每次写入这个变量时,都会立即刷新到主内存中,使得其他线程能够看到这个修改。
禁止指令重排序
-
指令重排序的影响:Java 编译器和处理器为了提高性能,可能会对代码的执行顺序进行重排序。在单线程环境中,这种重排序通常不会影响程序的正确性。但是,在多线程环境中,指令重排序可能会导致一些难以察觉的错误。
-
作用机制:变量被修饰时,Java 内存模型会禁止与其他内存操作进行重排序。这确保了对 volatile 变量的读写操作按照程序代码的顺序执行,从而避免了由于指令重排序而导致的错误。