1、final
在 Java 中,final 关键字可以用于变量、方法和类,分别赋予它们不同的语义和行为。以下是 final 关键字的主要作用
修饰变量
当 final 修饰一个变量时,表示该变量的值一旦初始化后就不能再被改变。这实用于根本范例和引用范例。
- 根本范例:对于根本范例(如 int, double 等),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 }
复制代码
静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。- 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 }
复制代码 静态内部类
非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才华用这个实例去创建非静态内部类。而静态内部类不需要。静态内部类不能访问外部类的非静态的变量和方法。
静态内部类实现单例:- 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 }
复制代码
此中一次的实行结果:以上代码,我们的预期结果应该是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——转到:指定跳转到标签,找到标签后,程序将处理从下一行开始的命令。保留字(现在没用以后可能用到作为关键字)
复制代码
双重校验锁机制在多线程环境中,不愿定线程安全,缘故原由是有指令重排的存在
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |