Java 学习之路 之 final类 (十九)

final 关键字可用于修饰类、变量和方法,final 关键字有点类似 C# 里的 sealed 关键字,永远表示它修饰的类、方法和变量不可改变。

final 修饰变量时,表示该变量一旦获得了初始值就不可改变,final 既可以修饰成员变量(包括类,变量和实例变量),也可是修饰局部变量、形参。有的书上说 final 修饰的变量不能被赋值,这种说法是错误的!严格的说法师,final修饰的变量不可悲改变,一旦获得了初始值,该 final 变量的值就不能被重新赋值。

因为 final 变量获得初始值之后不能被重新赋值,因此final修饰成员变量和修饰局部变量时有一定的不同。

1,final 成员变量

归纳起来,final 修饰的类 Field、实例 Field 能指定初始值的地方如下。

类 Field:必须在静态初始化块中或声明该 Field 时指定初始值。

实例 Field:必须在非静态初始化块、声明该 Field 或 构造器中指定初始值。

下面程序演示了 final修饰成员变量的效果,详细示范了 final 修饰成员变量的各种具体情况。

package com.demo;

public class FinalVariableTest {
	//定义成员变量时指定默认值,合法
	final int a = 6;
	//下面变量将在构造器或初始化块中分配初始值
	final String str;
	final int c;
	final static double d;
	//既没有指定默认值,又没有在初始化块、构造器中指定初始值
	//下面定义 char Field 是不合法的
	//final char ch;
	//初始化块,可对没有指定默认值的实例 Field 指定初始值
	{
		//在初始化块中为实例 Field 指定初始值,合法
		str = "Hello";
		//定义 a Field时已经指定了默认值
		//不能为 a 重复赋值,下面赋值语句非法
		//a = 9;
	}
	//静态初始化块,可对没有指定默认值的类 Field 指定初始值
	static{
		//在静态初始化块中为类 Field 指定初始值,合法
		d = 5.6;
	}
	//构造器,可对既没有指定默认值,又没有在初始化块中
	//指定初始值的实例 Field 指定初始值
	public FinalVariableTest(){
		//如果初始化块中对 str 指定了初始值
		//则构造器中不能对 final 变量重新赋值,下面赋值语句非法
		//str  = "java";
		c = 5;
	}
	public void changeFinal(){
		//普通方法不能为 final修饰的成员变量赋值
		//d = 1.2;
		//不能再普通方法中为 final 成员变量指定初始值
		//ch = 'a';
	}
	public static void main(String[] args) {
		FinalVariableTest ft = new FinalVariableTest();
		System.out.println(ft.a);
		System.out.println(ft.c);
		System.out.println(ft.d);
	}
}
上面程序详细示范了初始化 final 成员变量的各种情形,读者参考程序中的注释应该可以很清楚地看出 final 修饰成员变量的用法。

package com.demo;

public class FinalErrorTest {
	//定义一个 final 修饰的 Field
	//系统不会对 final 成员 Field 进行默认初始化
	final int age;
	{
		//age 没有初始化,所以此处代码将引起错误
		System.out.println(age);
		age = 6;
		System.out.println(age);
	}
	public static void main(String[] args) {
		new FinalErrorTest();
	}
}
2,final 局部变量

系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final 修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。

如果 final 修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该 final 变量赋初始值,但只能一次,不能重新赋值。

package com.demo;

public class FinalLocalVariableTest {
	public void test(final int a){
		//不能对 final 修饰的形参赋值,下面语句非法
		//a = 5;
	}
	public static void main(String[] args) {
		//定义 final 局部变量时指定默认值,则 str 变量无法重新赋值
		final String str = "hello";
		//下面赋值语句非法
		//str = "Java";
		//定义 final 局部变量时没有指定默认值,则 d 变量可被赋值一次
		final double d;
		//第一次赋初始值,成功
		d = 5.6;
		//对 final 变量重复赋值,下面语句非法
		//d = 3.4;
	}
}
3,final 修饰基本类型变量和引用类型变量的区别

当使用 final 修饰基本变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final 只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。

4,可执行“宏替换”的 final 变量

对一个 final 变量来说,不管它是类 Field、实例 Field,还是局部变量,只要改变量满足3个条件,这个final变量就不再是一个变量,而是相当于一个直接量。

使用 final 修饰符修饰;

在定义该 final 变量时指定了初始值;

该初始值可以在编译时就被确定下来。

final 修饰符的一个重要用途就是定义 “宏变量”。当定义 final 变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个 final 变量本质就是一个 “宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

5,final 方法

Java提供的 Object 类里就有一个 final 方法:getClass(),因为 Java 不希望任何类重写这个方法,所以使用 final 把这个方法密封起来。胆对于该类提供的 toString() 和 equals()方法,都允许子类重写,因此没有使用 final 修饰它们。

下面程序试图重写 final 方法,将会引发编译错误。

public class FinalMethodTest{
    public final void test(){}
}
class Sub extends FinalMethodTest{
    //下面方法定义将出现编译错误,不能重写 final 方法
    public void test(){}
}
上面程序中父类是 FinalMethodTest,该类里定义的 test 方法是一个 final 方法,如果其子类试图重写该方法,将会引发编译错我。

对于一个private 方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法--如果子类中定义一个与父类 private 方法有相同方法名、形同参数列表、相同返回值类型的方法,也不是方法重写,只是重新定义了一个新方法。因此,即使使用 final 修饰一个 private 访问权限的方法,依然可以再其子类中定义一个新方法。因此,即使使用 final 修饰一个 private 访问权限的方法,依然可以再其子类中定义与该方法具有相相同方法名、相同形参列表、相同返回值类型的方法。

public class FinalMethodTest{
    private final void test(){}
}
class Sub extends FinalMethodTest{
    //下面的方法定义不会出现问题
    public void test(){}
}
上面程序没有任何问题,虽然子类和父类同样包含了同名的 void test() 方法,但子类并不是重写父类的方法,因此即使父类的 void test() 方法是用了 final 修饰,子类中依然可以定义 void test() 方法。
final 修饰的方法仅仅是不能被重写,并不是不能被重载,因此下面程序完全没有问题。

public class FinalOverload{
    //final 修饰的方法只是不能被重写,完全可以被重载
    public final void test(){}
    public final void test(String arg){}
}
6,final 类

final 修饰的类不可以有子类,例如 java.lang.Math 类就是一个 final 类,它不可以有子类。

当子类继承父类时,将可以访问到父类内部数据,并可通过重写父类方法来改变父类方法的实现细节,这可能导致一些不安全的因素。为了保证某个类不可悲继承,则可以使用 final修饰这个类。

public final class FinalClass {}
//下面的类定义将出现编译错误
class Sub extends FinalClass {}
因为 FinalClass 类是一个 final 类, 而 Sub 试图 继承 FinalClass 类,这将会引起编译错误。

7,不可变类

不可变类的意思是创建该类的实例后,该实例的 Field 是不可改变的。Java提供的8 个包装类和 java.lang.String 类都是不可变类,当创建它们的实例后,其实例的 Field 不可变。

Double d = new Double(6.5);
String str = new String("Hello");
上面程序创建了一个 Double 对象和一个 String 对象,并为这个两对象传入了 6.5 和 “Hello”字符串作为参数,那么 Double 类和 String 类肯定需要提供实例 Field 来保存这两个参数,但程序无法修改这两个实例 Field 值,因此 Double 和 String 类没有提供修改它们的方法。

如果需要创建自定义的不可变类,可遵守如下规则。

使用 private 和 final 修饰符传入参数来初始化类里的 Field。

提供带参数构造器,用于根据传入参数来初始化类里的 Field。

仅为该类的 Field 提供 getter 方法, 不要为该类的 Field 提供 setter 方法,因为普通方法无法修改 final 修饰的 Field。

如果有必要,重写 Object 类的 hashCode 和 equals 方法。 equals 方法以关键 Field 来作为判断两个对象是否相等的标准,除此之外,还应该保证两个用 equals 方法判断为相等的对象的 hashCode 也相等。

例如,java.lang.String 这个类就做得很好,它就是根据 String 对象里的字符序列来作为相等的标准,其 hashCode 方法也是根据字符串序列计算得到的。下面程序测试了 java.lang.String 类的 equals 和 hashCode 方法。

package com.demo;

public class ImmutableStringTest {
	public static void main(String[] args) {
		String str1 = new String("Hello");
		String str2 = new String("Hello");
		//输出 false
		System.out.println(str1 == str2);
		//输出 true
		System.out.println(str1.equals(str2));
		//下面两次输出的 hashCode 相同
		System.out.println(str1.hashCode());
		System.out.println(str2.hashCode());
	}
}
下面定义一个不可变的 Address 类,程序把 Address 类的 detail 和 postCode 成员变量都使用 private 隐藏起来,并使用 final 修饰这两个成员变量,不允许其他方法修改这两个 Field 值。

package com.demo;

public class Address {
	private final String detail;
	private final String postCode;
	//在构造器里初始化两个实例 Field
	public Address(){
		this.detail = "";
		this.postCode = "";
	}
	public Address(String detail, String postCode){
		this.detail = detail;
		this.postCode = postCode;
	}
	//仅为两个实例 Field 提供 getter 方法
	public String getDetail(){
		return this.detail;
	}
	public String getPostCode(){
		return this.postCode;
	}
	//重写 equals 方法,判断两个对象是否相等
	public boolean equals(Object obj){
		if(this == obj){
			return true;
		}
		if(obj != null && obj.getClass() == Address.class){
			Address ad = (Address)obj;
			//当 detial 和 postCode 相等时,可认为两个Address 对象相等
			if(this.getDetail().equals(ad.getDetail()) && this.getPostCode().equals(ad.getPostCode()) ){
				return true;
			}
		}
		return false;
	}
	public int hashCode(){
		return detail.hashCode() + postCode.hashCode() * 31;
	}
}
对于上面的 Address 类,当我们在程序中创建了 Address 对象后,同样无法修改该 Address 对象的 detail 和 postCode Field。

与不可变类对应的是可变类,可变类的含义是该类的实例 Field是可变的。大部分时候所创建的类都是可变类,特别是 JavaBean,因为总是为其Field提供了 setter 和 getter 方法。

与可变类相比,不可变类的实例在整个生命周期中永远处于初始化状态,它的 Field不可改变。因此对不可变类的实例的控制将更加简单。(更多详细讲解请参考 疯狂java 第2版)

8,缓存实例的不可变类

不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变实例,则应该考虑缓存这种不可变类的实例。毕竟重复创建相同的对象没有太大的意义,而且加大系统开销。如果可能,应该将已经创建的不可变类的实例进行缓存。

缓存是软件设计中一个非常有用的模式,缓存的实现方式有很多种,不同的实现方式可能存在较大的性能差别,关于缓存的性能问题此处不做深入讨论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目标检测(Object Detection)是计算机视觉领域的一个核心问题,其主要任务是找出图像中所有感兴趣的目标(物体),并确定它们的别和位置。以下是对目标检测的详细阐述: 一、基本概念 目标检测的任务是解决“在哪里?是什么?”的问题,即定位出图像中目标的位置并识别出目标的别。由于各物体具有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具挑战性的任务之一。 二、核心问题 目标检测涉及以下几个核心问题: 分问题:判断图像中的目标属于哪个别。 定位问题:确定目标在图像中的具体位置。 大小问题:目标可能具有不同的大小。 形状问题:目标可能具有不同的形状。 三、算法分 基于深度学习的目标检测算法主要分为两大: Two-stage算法:先进行区域生成(Region Proposal),生成有可能包含待检物体的预选框(Region Proposal),再通过卷积神经网络进行样本分。常见的Two-stage算法包括R-CNN、Fast R-CNN、Faster R-CNN等。 One-stage算法:不用生成区域提议,直接在网络中提取特征来预测物体分和位置。常见的One-stage算法包括YOLO系列(YOLOv1、YOLOv2、YOLOv3、YOLOv4、YOLOv5等)、SSD和RetinaNet等。 四、算法原理 以YOLO系列为例,YOLO将目标检测视为回归问题,将输入图像一次性划分为多个区域,直接在输出层预测边界框和别概率。YOLO采用卷积网络来提取特征,使用全连接层来得到预测值。其网络结构通常包含多个卷积层和全连接层,通过卷积层提取图像特征,通过全连接层输出预测结果。 五、应用领域 目标检测技术已经广泛应用于各个领域,为人们的生活带来了极大的便利。以下是一些主要的应用领域: 安全监控:在商场、银行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值