一文带你搞懂Java-final关键字

引言

阅读《Java并发编程实战》的基础知识篇发现java中的final作用实在是太大了,故结合实例深入剖析final关键字。

基础

修饰类

final修饰类时意味着该类不能被继承,所有方法都将为final,所有在final类中给任何方法添加final是没有任何意义的。

修饰方法

private方法是隐式的final,final方法是可以重载的。

修饰参数

将参数列表中的参数申明为final,意味着无法在方法中更改参数引用所指向的对象。常常用在匿名内部类中。

interface GreetingService {
    void greet(String message);
}

public void executeGreeting(final String name) {
    GreetingService service = new GreetingService() {
        @Override
        public void greet(String message) {
            name = "hello"; // 报错
            System.out.println(message + " " + name);
        }
    };

    service.greet("Hello");
}

注意从Java8开始,即使局部变量和参数没有显式地被声明为final,只要它们实际上没有被修改,就可以在匿名内部类或lambda表达式中使用。

修饰变量

编译期常量和非编译期常量

import java.util.Random;

public class Test {
    //编译期常量
    final int i = 1;
    //非编译期常量
    Random random = new Random();
    final int k = random.nextInt();
}

所以final修饰的字段不都是编译期间常量,如上的k只是在初始化之后无法被更改了。

static final

static final 只占据一段不能改变的存储空间,必须在定义的时候进行赋值。

blank final

允许生成空白final,但是该字段被使用之前需要被赋值,通常在构造器进行赋值。

final修饰引用变量

被final修饰的变量无法被改变引用,但引用变量内部仍然可以修改值,如下。

import java.util.ArrayList;

public class Test {
    private final List<Integer> list = new ArrayList<>();
    
    
    public void add(int num) {
        list.add(num); //是允许的
    }
    
    public void change(List<Integer> otherList) {
        list = otherList; //不被允许
    }
}

指令重排序

final为基本类型

写final域重排序规则
/**
 * @author hyy (hjlbupt at 163 dot com)
 */
public class FinalInitialDemo {

    private int a;
    private boolean flag;
    private FinalInitialDemo demo;

    public FinalInitialDemo() {
        a = 1;
        flag = true;
    }

    public void writer() {
        demo = new FinalInitialDemo();
    }

    public void reader() {
        if (flag) {
            int i = a * a;
            if (i == 0) {
                // On my dev machine, the variable initial always success.
                // To solve this problem, add final to the `a` field and `flag` field.
                System.out.println("Fuck! instruction reordering occurred.");
            }
        }
    }

    @SuppressWarnings("InfiniteLoopStatement")
    public static void main(String[] args) throws Exception {
        while (true) {
            FinalInitialDemo demo = new FinalInitialDemo();
            Thread threadA = new Thread(demo::writer);
            Thread threadB = new Thread(demo::reader);

            threadA.start();
            threadB.start();

            threadA.join();
            threadB.join();
        }
    }
}

上述代码存在并发安全问题,writer和reader同时进行,writer线程进行类的初始化,此时JVM可能会进行指令的重排序,将a,flag等变量的初始化赋值重排序到构造函数之外,导致reader读取的a,flag变量是基础变量的初始值即0和false(指令顺序不一定发生,并且需要特定的硬件和JVM环境)。

原理

  • JMM禁止编译器把final域的写重排序到构造函数之外
  • 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障,可以禁止处理器把final域的写重排序到构造函数之外。
读final域重排序规则
/**
 * @author hyy (hjlbupt at 163 dot com)
 */
public class FinalInitialDemo {

    private int a;
    private boolean flag;
    private FinalInitialDemo demo;

    public FinalInitialDemo() {
        a = 1;
        flag = true;
    }

    public void writer() {
        demo = new FinalInitialDemo();
    }

    public void reader() {
       FinalInitialDemo referenceDemo = demo;
       int a = referenceDemo.a;
       boolean flag = referenceDemo.flag;
    }

    @SuppressWarnings("InfiniteLoopStatement")
    public static void main(String[] args) throws Exception {
        while (true) {
            FinalInitialDemo demo = new FinalInitialDemo();
            Thread threadA = new Thread(demo::writer);
            Thread threadB = new Thread(demo::reader);

            threadA.start();
            threadB.start();

            threadA.join();
            threadB.join();
        }
    }
}

观察到reader线程读取FinalInitialDemo的引用,成员变量a和flag。如果reader在未读取到对象的引用时,就在读取对象的普通域变量,这显然是错误的操作。

原理

  • 在读一个对象的final域之前,一定会先读这个包含final域对象的引用
  • 编译器会在读final域操作的前面插入一个loadload屏障,可以禁止处理器读取对象的普通域在读取对象引用之前。

final为引用类型

写final域重排序规则

这里参考了pdai.tech博客中的内容

public class FinalReferenceDemo {
    final int[] arrays;
    private FinalReferenceDemo finalReferenceDemo;

    public FinalReferenceDemo() {
        arrays = new int[1];  //1
        arrays[0] = 1;        //2
    }

    public void writerOne() {
        finalReferenceDemo = new FinalReferenceDemo(); //3
    }

    public void writerTwo() {
        arrays[0] = 2;  //4
    }

    public void reader() {
        if (finalReferenceDemo != null) {  //5
            int temp = finalReferenceDemo.arrays[0];  //6
        }
    }

    @SuppressWarnings("InfiniteLoopStatement")
    public static void main(String[] args) throws Exception {
        while (true) {
            FinalReferenceDemo demo = new FinalReferenceDemo();
            Thread threadA = new Thread(demo::writerOne);
            Thread threadB = new Thread(demo::writerTwo);
            Thread threadC = new Thread(demo::reader);

            threadA.start();
            threadB.start();
            threadC.start();

            threadA.join();
            threadB.join();
            threadC.join();
        }
    }
}

线程A先执行writerOne方法,线程B执行writerTwo方法,线程C执行reader方法。在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。

原理

由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序(简单来说即是要等构造函数对final域操作完成后才能进行其他操作)。

读final域重排序规则

上述代码只能保证线程C能看到线程A对final引用的对象的成员域的写入,即能看到arrays[0]=1。而线程B对数组元素的写入是否能看到就不确定了(线程B和线程C存在数据竞争)。

防止重排序的前提条件

上述谈到final域初始化和构造函数初始化之间不能发生指令重排序有一个前提条件:该对象的引用不能在构造函数中“逸出”。

This引用逃逸

参考《Java并发编程实战》的内容,作者提到了两个常见的对象逸出情况:

  • 在构造函数中注册事件监听
  • 在构造函数中启动新线程
public class ThisEscape {
    private final int var;
    
    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e); //一旦注册成功则可能触发回调, 导致访问var变量可能是未赋值的变量,
                    // 隐含发布了ThisEscape实例本身.
                }
            });
        
        // other initial......
        
        var = 1;
    }
    
    public void doSomething(Event e) {
        System.out.println(var);
    }
}
public class ThisEscape {
    private final int var;

    public ThisEscape() {
        new Thread(new EscapeRunnable()).start();
        // ...
        
        var = 1;
    }

    private class EscapeRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println(ThisEscape.this.var);
            // ThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 
            // 即发生了外围类的this引用的逃逸
        }
    }
} 

简单来说,this逃逸就是说在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引发错误。

解决方案

public class SafeListener {
    private final EventListener listener;
    
    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        }
    }
    
    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }
}

参考资料

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
张正友标定法是一种常用的相机标定方法,广泛应用于计算机视觉领域。该方法通过采集一系列已知的三维物体在相机坐标系下的二维投影点,来计算相机内外参数矩阵,从而实现相机的几何校正和测量。 具体步骤如下: 1. 初始化标定板:选择一个特定的标定板,例如棋盘格,然后在每个方格的交叉点上贴上黑白相间的标志。 2. 放置标定板:将标定板放置在计算机视觉系统所见范围内,保证标定板能够在不同角度、位置下被相机观察到。 3. 拍摄标定图像:使用相机对标定板进行拍摄,至少需要12-20幅图像,图像应该包含不同的姿态和视角。 4. 检测标志物:从每个标定图像中提取特征点,通常使用角点检测算法来检测标志物的位置。 5. 计算相机参数:根据提取的特征点,通过最小二乘法来计算相机的内部参数(焦距、主点坐标)和外部参数(旋转矩阵、平移向量)。 6. 优化结果:根据计算得到的相机参数,利用优化算法来进一步提高标定的精度。 7. 验证标定结果:使用标定结果对图像进行校正,并测量标定板上的特征点,通过计算误差指标来验证标定结果的准确性。 总之,张正友标定法通过采集已知物体在相机坐标系下的二维投影点,实现了相机参数的计算和校正,对于计算机视觉中的三维重建、目标检测等任务具有重要意义。掌握这种标定方法可以帮助我们更好地理解相机成像过程,提高图像处理和计算机视觉算法的精度和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WeiXiao_Hyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值