【Java 温故而知新系列】基础知识-04 重点关键字(面试经常遇到的)

news/2025/1/5 18:46:25/文章来源:https://www.cnblogs.com/zhulu/p/18635775

1、final 

在 Java 中,final 关键字可以用于变量、方法和类,分别赋予它们不同的语义和行为。以下是 final 关键字的主要作用

修饰变量

final 修饰一个变量时,表示该变量的值一旦初始化后就不能再被改变。这适用于基本类型和引用类型。

  • 基本类型:对于基本类型(如 intdouble 等),final 意味着变量的值不能改变。基本类型详情见这篇【Java 温故而知新系列】基础知识-02 数据基本类型
  • 引用类型:对于引用类型(如对象或数组),final 意味着引用本身不能指向另一个对象,但对象内部的状态是可以改变的(除非对象本身是不可变的)
1 final int x = 5; // 基本类型的 final 变量
2 // x = 10; // 错误:不能修改 final 变量的值
3 
4 final StringBuilder sb = new StringBuilder("Hello");
5 sb.append(" World"); // 正确:可以修改对象的状态
6 // sb = new StringBuilder("Java"); // 错误:不能重新赋值给 final 引用

修饰方法

final 修饰一个方法时,表示该方法不能被子类重写(override)。这有助于确保某些关键行为不会被意外改变,并且可以提高性能优化的可能性,因为编译器知道该方法的行为不会在运行时改变。

代码为证:

 修饰类

当 final 修饰一个类时,表示该类不能被继承。这意味着没有任何其他类可以从这个类派生出来。使用 final 类通常是为了保护类的设计不被更改,或者是因为该类提供了非常具体的实现,不需要也不应该有子类。

其实Java中我们平时用到的很多类都是被final关键字修饰的,比如常用的包装类:Integer、Double 等(包装类详情可见这篇:【Java 温故而知新系列】基础知识-03 基本类型对应之包装类),再比如 String 等等。

修饰方法参数

你也可以将方法参数声明为 final,这意味着在方法体内不能修改这些参数的值。这对于编写清晰、无副作用的方法很有帮助,尤其是在多线程环境中。其实这个跟第一种修饰变量是比较类似的。

代码为证:

2、static

static可以用来修饰变量、方法、代码块和内部类。使用 static 的主要目的是为了创建属于类本身而不是类的实例(对象)的成员。这意味着静态成员可以在没有创建类的任何实例的情况下被访问,并且所有实例共享同一个静态成员。

 静态变量

  • 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
  • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
 1 class  A {
 2     public static Integer count = 10; //静态变量,类变量
 3     public Integer countB= 10;//实例变量
 4 }
 5 public class JavaKeyWorkDemo {
 6     public static void main(String[] args) {
 7         System.out.println(A.count);
 8         A.count = 20;
 9         System.out.println(A.count);
10     }
11 }

 

 

10
20

 

静态方法

静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。

1 public abstract class A {
2     public static void func1(){
3     }
4     // public abstract static void func2();  // Illegal combination of modifiers: 'abstract' and 'static'
5 }

只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因为这两个关键字与具体对象关联。

1 class  A {
2     public static Integer countA = 10; //静态变量,类变量
3     public Integer countB= 10;//实例变量
4     public static void func1(){
5         Integer a = countA;
6         //Integer b = countB;  // Non-static field 'countB' cannot be referenced from a static context 无法从 static 上下文引用非 static 字段 'countB'
7         //Integer c = this.countB;     // 'A.this' cannot be referenced from a static context 无法从 static 上下文引用 'A.this'
8     }
9 }

 

 静态代码块

静态语句块在类初始化时运行一次。静态代码块用于初始化类级别的资源,在类加载到 JVM 时执行一次。如初始化静态变量等。

 1 class  A {
 2     static {
 3         System.out.println("123");
 4     }
 5     public static Integer countA = 10; //静态变量,类变量
 6     public Integer countB= 10;//实例变量
 7     public static void func1(){
 8         Integer a = countA;
 9         //Integer b = countB;  // Non-static field 'countB' cannot be referenced from a static context 无法从 static 上下文引用非 static 字段 'countB'
10         //Integer c = this.countB;     // 'A.this' cannot be referenced from a static context 无法从 static 上下文引用 'A.this'
11     }
12 }
13 public class JavaKeyWorkDemo {
14     public static void main(String[] args) {
15         A a1= new A();
16         A a2= new A();
17     }
18 }
123

静态内部类

非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。静态内部类不能访问外部类的非静态的变量和方法。

静态内部类实现单例:

 1 public class Singleton {
 2  
 3     // 私有构造函数,防止外部直接实例化
 4     private Singleton() {
 5     }
 6  
 7     // 静态内部类,用于持有外部类的实例
 8     private static class SingletonHolder {
 9         private static final Singleton INSTANCE = new Singleton();
10     }
11  
12     // 提供公有的静态方法,用于获取外部类的实例
13     public static Singleton getInstance() {
14         return SingletonHolder.INSTANCE;
15     }
16 }

 

静态导包

在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

import static 包名.类名.静态成员;//导入一个类中指定静态成员
import static com.xxx.ClassName.* //导入一个类中的所有静态成员,可以使用通配符*

 

1 import static java.lang.Math.*;
2 
3 public class StaticImportExample {
4     public static void main(String[] args) {
5         System.out.println(sqrt(16)); // 直接调用 sqrt 方法,无需前缀 Math.
6         System.out.println(PI); // 直接使用 PI 常量,无需前缀 Math.
7     }
8 }

 

初始化顺序

 静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。

存在继承的情况下,初始化顺序为:

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

3、 public,private,protected,以及不写(默认)时的区别

定义:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访 问。Java 支持 4 种不同的 访问权限。
分类:
  • private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部 类)
  • default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用 任何修饰符。使用 对象:类、接口、变量、方法。
  • protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意: 不能修饰类(外部 类)。
  • public : 对所有类可见。使用对象:类、接口、变量、方法

4、this关键字的用法

this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指 针。
this的用法在java中大体可以分为3种:
  • 普通的直接引用,this相当于是指向当前对象本身。
  • 形参与成员名字重名,用this来区分
1 public Person(String name, int age) {
2  this.name = name;
3  this.age = age;
4 }

 

  • 引用本类的构造函数

 

 1  class Person{
 2  private String name;
 3  private int age;
 4 
 5  public Person() {
 6  }
 7 
 8  public Person(String name) {
 9  this.name = name;
10  }
11  public Person(String name, int age) {
12  this(name);
13  this.age = age;
14  }
15  }

 

5、super关键字的用法

super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离 自己最近的一个父类。
super也有三种用法:
  • 普通的直接引用 与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。
  • 子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分 
  • 引用父类构造函数。super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句);this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。 
 1 class Person{
 2     protected String name;
 3 
 4     public Person(String name) {
 5     this.name = name;
 6     }
 7 
 8     }
 9 
10      class Student extends Person{
11      private String name;
12 
13      public Student(String name, String name1) {
14      super(name);
15      this.name = name1;
16      }
17 
18      public void getInfo(){
19      System.out.println(this.name); //Child
20      System.out.println(super.name); //Father
21      }
22 
23      }
24 
25      public class Test {
26      public static void main(String[] args) {
27      Student s1 = new Student("Father","Child");
28      s1.getInfo();
29 
30      }
31  }

 6、volatile

 volatile是一个变量修饰符,只能用来修饰变量,用法也比较简单,只需要在声明一个可能被多线程访问的变量时,使用volatile修饰即可。在并发编程的三大特性——原子性、可见性、有序性中,volatile只能保证可见性和有序性(禁止指令重排),并不能保证原子性

  • 原子性:一个操作不可拆分、不被中断,要么全部执行,要么都不执行。(数据库中ACID的原子性指的是“要么都执行要么都回滚”,这是不同的概念)
  • 可见性:指多个线程之间共享数据的可见性。即当一个线程修改了共享变量时,其它线程能立即看到这个修改
  • 有序性:volatile除了可以保证数据的可见性之外,还有一个强大的功能,那就是它可以禁止指令重排序,所以能在一定程度上保证有序性

不能保证原子性(代码为证):

 1 class VolatileIncr {
 2     volatile int num = 0;
 3     public void add() {
 4         num++;
 5     }
 6     public static void main(String[] args) {
 7         VolatileIncr test = new VolatileIncr();
 8         //启动10个线程
 9         for (int i = 0; i < 10; i++) {
10             new Thread(() -> {
11                 //每个线程执行1000次+1操作
12                 for (int j = 0; j < 1000; j++) {
13                     test.add();
14                 }
15             }, String.valueOf(i)).start();
16         }
17         while (Thread.activeCount() > 2) {
18             Thread.yield();
19         }
20         System.out.println("最后num的值为:" + test.num);
21     }
22 }

 

 

其中一次的执行结果:

最后num的值为:9120进程已结束,退出代码0

以上代码,我们的预期结果应该是10000才对,但执行起来发现,并不是每次都是10000,这就是因为i++这个操作没办法保证原子性。它其实包含三个指令:

  • 执行GETFIELD拿到主内存中的原始值num。
  • 执行IADD进行+1操作。
  • 执行PUTFIELD把工作内存中的值写回主内存中。

当多个线程并发执行PUTFILED指令的时候,会出现写回主存覆盖的问题,所以最终结果可能会比预期的结果要小,所以volatile不能保证原子性。

可见性(代码为证):

 1 class Visibility {
 2     private boolean flag = false;
 3     public void start() {
 4         new Thread(() -> {
 5             System.out.println("Thread1 start");
 6             while (!flag) {
 7                 // 不断循环,等待flag变为true
 8             }
 9             System.out.println("Thread1 complet");
10         }).start();
11         // 确保线程1先启动
12         try {
13             Thread.sleep(100);
14         } catch (InterruptedException e) {
15             e.printStackTrace();
16         }
17         new Thread(() -> {
18             System.out.println("Thread2 start");
19             // 修改flag的值为true
20             flag = true;
21             System.out.println("Thread2 complet");
22         }).start();
23     }
24     public static void main(String[] args) {
25         Visibility example = new Visibility();
26         example.start();
27     }
28 }

 

执行结果:

 

从结果看,程序“卡”在了Thread1的while循环中,说明Thread1读到的flag还是flase,但是Thread2已经把它改为true了,这是为什么?这是因为Thread1在执行的时候,就把flag的副本保存在这就的工作内存中,之后就会一直读取自己线程工作内存中flag变量的值,而不会去主内存中重新获取新的值。

加了volatile修饰后的运行结果:

 

有序性(代码为证):

下面是一个经典的例子:双重检测实现单例的例子(面试经常问这样实现单例会不会有问题?有什么问题?如何解决?)

 1 public class Singleton {
 2     //私有化构造函数
 3     private Singleton(){}
 4     //单例对象(无volatile修饰)
 5     private static Singleton instance=null;
 6     public static Singleton getInstance(){
 7         //第一次检测
 8         if (instance==null){
 9             //加锁
10             synchronized (Singleton.class){
11                 //第二次检测
12                 if (instance==null){
13                     //初始化
14                     instance=new Singleton();
15                 }
16             }
17         }
18         return instance;
19     }
20 }

 

双重校验锁机制在多线程环境中,不一定线程安全,原因是有指令重排的存在,在极端情况下,上述的单例对象可能发生空指针异常

我们假设线程1和线程2同时请求getSingleton()方法的时候:

  • 线程1执行到instance=new Singleton();,开始初始化。
  • 线程2执行到“第一次检测”的位置,判断singleton == null。
  • 线程2经过判断发现singleton !=null,于是就直接执行return instance。
  • 线程2拿到singleton对象后,开始执行后续的操作。

以上过程看似没有什么问题,但在第4步执行后续操作的时候,是有可能抛空指针异常的,这是因为在第3步的时候,线程2拿到的singleton对象并不是一个完整的对象。

很明显instance=new Singleton();,这段代码出现了问题,那我们来分析一下,这个代码的执行过程可以简化成3步:

  • JVM为对象分配一块内存M。
  • 在内存上为对象进行初始化。
  • 将内存M的地址赋值给singleton变量。

因为将内存的地址赋值给singleton变量是最后一步,所以线程1在这一步骤执行之前,线程2在对singleton == null判断一直都是true,那么它会一直阻塞,直到线程1执行完。

但是这个过程并不是一个原子操作,并且编译器可能会进行重排序,如果以上步骤被重排序为:

  • JVM为对象分配一块内存M。
  • 将内存M的地址赋值给singleton变量。
  • 在内存上为对象进行初始化。

这样的话线程1会先内存分配,再执行变量赋值,最后执行初始化。也就是说在线程1执行初始化之前,线程2对singleton == null的判断会提前得到一个false,于是便返回了一个不完整的对象,所以在执行后续操作时,就发生了空指针异常。

要解决的话,直接禁止它指令重排就行了,所以volatile就派上用场了,只需要用volatile修饰一下instance即可。

7、其他

class (类):public class A(){}花括号里是已实现的方法体,类名需要与文件名相同interface (接口):public interface B(){}花括号里有方法体,但没有实现,方法体句子后面是英文分号“;”结尾abstract (声明抽象):public abstract class C(){}介于类与接口中间,可以有,也可以没有已经实现的方法体implements (实现):用于类或接口,实现接口public class A interface B(){}extends (继承):用于类继承类public class A extends D(){}new (创建新对象):A a=new A();A表示一个类import (引入包的关键字):当使用某个包的一些类时,仅需要类名,即可自动插入类所在的包package (定义包的关键字):将所有相关的类放在一个包类以便查找修改等assert(断言):assert <bool expression>,以 Integer类的IntegerCache 方法 中代码举例  assert IntegerCache.high >= 127。断言是为了方便调试程序,并不是发布程序的组成部分。理解这一点是很关键的。默认情况下,JVM是关闭断言的。因此如果想使用断言调试程序,需要手动打开断言功能。在命令行模式下运行Java程序时可增加参数-enableassertions或者-ea打开断言。可通过-disableassertions或者-da关闭断言(默认情况,可有可无)。const——常量,常数:用于修改字段或局部变量的声明。保留字(现在没用以后可能用到作为关键字)goto——转到:指定跳转到标签,找到标签后,程序将处理从下一行开始的命令。保留字(现在没用以后可能用到作为关键字)

 

 

双重校验锁机制在多线程环境中,不一定线程安全,原因是有指令重排的存在

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/863455.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

春节不打烊,超市管理工具如何助力商家应对销售高峰

春节超市管理工具种类繁多,每种工具都有其独特的功能和优势。在选择工具时,超市应根据自身的需求和实际情况进行选择,以确保管理效率和服务质量的提升。春节期间,超市管理工具种类繁多,这些工具旨在帮助超市高效管理库存、促进销售、调度员工以及提升顾客服务质量。以下是…

vue3 在渲染md中的数学公式

常规的md转数学公式插件无法解决此问题 问题: 在渲染过程中 \t 被转义 导致渲染出错**方案为:将\t 转义为\t ** 依赖的插件及版本"katex": "^0.16.15","markdown-it": "^14.1.0","markdown-it-katex": "^2.0.3",…

重新定义电商团队协作:在线文档工具的战略作用

在当今快速发展的电商行业,团队协作效率对业务成功至关重要。尤其是跨部门沟通和信息共享,已经成为提升电商团队组织架构优化的关键因素。而一个功能强大的在线协同编辑文档工具正是提升这些关键环节的解决方案之一。 电商团队组织架构的挑战 电商企业通常面临复杂的业务流程…

基于海豚调度功能开发——推送下游系统数据库连接信息批量修改方案与实现

功能需求背景 由于信创(信息技术应用创新产业)改造要求,上个月已将从数据仓库推送下游官网系统的ORACLE数据库相关数据推送任务迁移到信创人大金仓临时数据库,整体任务完成切换上线并试运行稳定。 由于年底需要进行信创数据库的正式上线,目前需要将人大金仓临时数据库切换…

Pycharm 2024 安装激活详细使用教程(激活至2026,实测是永久,亲测!)

开发工具推荐:Pycharm 安装激活详细使用教程(激活至2026,实际上永久,亲测!)申明:本教程 Pycharm补丁、激活码均收集于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除。若条件允许,希望大家购买正版 ! Pycharm是JetBrains公司推出的一款功能强大的Python集…

服务器迁移中心——“工作组迁移”使用指南

简介 服务器迁移中心(Server Migration Center,简称SMC)是阿里云提供给您的迁移平台。专注于提供能力普惠、体验一致、效率至上的迁移服务,满足您在阿里云的迁移需求。 工作组迁移是SMC的一项功能,提供标准化迁移流程,可以同时管理多个迁移源的生命周期,适用于需要批量迁…

arcgis server 10.4许可过期

注册机生成注册码 10.4许可基本在2025年1月1日过期,用生成码生成一版新的许可替换即可,生成许可虽然只支持10,改成104即可。 注册机链接地址: [https://drive.google.com/file/d/1H9BXzr1tklbAPO3QH-FYl80sn1yJlB02/view?usp=drive_link](https://drive.google.com/file/d…

delphi djson 类与JSON 互转,与 Java、Golang 一致写法

前因 为什么要开发这个 JSON库?原因是 delphi 官方的 json 既没有处理 null(也叫零值)的问题;举例说明吧: 开发者 往往 需要 类与JSON 之间 进行序列化 和 反序列化;接下来我们举个例子: Person {id: Int64; // IDname: string; //姓名desc: string; //描述}这样一个类 在…

Windows更改远程桌面端口.241202

为了远程安全,默认在3389改为别的端口。 本示例为3389改为533891、步骤:打开“开始→运行”,输入“regedit”,打开注册表,进入以下路径: [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server\Wds\rdpwd\Tds\tcp]修改PortNamber修改成所希望的端口即可…

数据分层 ODS DW DM层级.241203

在数据仓库的设计过程中,数据分层是一种重要的组织方式,能够提高数据处理效率和数据质量。数据分层通常包括原始数据(Operational Data Store,ODS)、明细数据(Data Warehouse,DW)和汇总数据(Data Mart,DM)三个层级。下面将详细介绍这三个层级的作用以及如何优雅地设…

OpenEuler文件被锁定的解决方法网卡修改不生效的解决办法.241202

欧拉系统(含centos等linux系统)修改文件,一直提示readonly,不让改。原因有可能是这个文件给锁定了。解决方法: 使用以下两个命令: • chattr 改变文件属性 • lsattr 文件 查看文件属性 例如: • chattr +i 将文件锁住,任何用户都不能进行修改 • chattr +a 只能向文件…