final修饰
final修饰变量时,表示该变量一旦获得了初始值就不可被改变,final既可以修饰成员变量(包括类变量和实例Z变量),也可以修饰局部变量、形参。有的树上介绍说final修饰的变量不能被赋值,这种说法是错误的!严格的说法是,final修饰的变量不可能改变,一旦获得了初始值,该final变量的值就不能被重新赋值。
因为final变量获得初始值之后不能被重新赋值,因此final修饰成员变量和修饰局部变量时有一定的不同。
final成员变量
成员变量时随类初始化或对象初始化二初始化的。当类初始化时,系统会为该类的类Field分配内存,并分配默认值;当创建对象时,系统会为该对象得实例Field分配内存,并分配默认值。也就是说,当执行静态初始化块时可以对类Field赋初始值;当执行普通初始化块、构造器时可对实例Field赋初始值。因此,成员变量的初始值可以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。
对于final修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、’\u0000’、false或null,这些成员变量也就完全失去了存在的意义。因此Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值。
归纳起来,final修饰的类Field、实例Field能指定初始值的地方如下:
- 类Field:必须在静态初始化块中或声明该Field时指定初始值。
- 实例Field:必须在非静态初始化块、声明该Field或构造器中指定初始值
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修饰成员变量的用法。
与普通成员变量不同,final成员变量(包括实例Field和类Field)必须有程序员显示初始化,系统不会对final成员进行隐式初始化。
如果打算在构造器、初始化块中对final成员变量进行初始化,则不要在初始化之前就访问成员变量的值。例如下面程序将会引起错误:
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();
}
}
上面程序中定义了一个final成员变量:age,系统不会对age成员进行隐式初始化,所以初始化中粗体字标识的代码行将引起错误,因为它试图访问一个未初始化的变量。
final局部变量
系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。
如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该final变量赋初始值,但只能一次,不能重复赋值;如果final修饰的局部变量在定义时已经指定默认值,则后面代码占用不能再对该变量赋值:
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;
System.out.print(d);
}
}
在上面程序中还示范了final修饰形参的情况。因为形参在调用该方法时,由系统根据传入的参数来完成初始化,因此适用final修饰的形参不能被赋值。
final修饰基本类型变量和引用类型变量的区别
当使用fianl修饰接班类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一对象,但这个对象完全可以发生改变,下面程序示范了final修饰数组和Person对象的情形:
import java.util.Arrays;
class Person
{
private int age;
public Person(){}
//有参数构造器
public Person(int age)
{
this.age=age;
}
public void setAge(Integer age){
this.age=age;
}
public Integer getAge(){
return this.age;
}
}
public class FinalReferenceTest
{
public static void main(String[] args)
{
//final修饰数组变量,iArr是一个引用变量
final int[] iArr={5, 6, 12, 9};
System.out.println(Arrays.toString(iArr));
//对数组元素进行排序,合法
Arrays.sort(iArr);
System.out.println(Arrays.toString(iArr));
//对数组元素赋值,合法
iArr[2]=-8;
System.out.println(Arrays.toString(iArr));
//下面语句对iArr重新赋值,非法
//iArr=null;
//final修饰Person变量,p是一个引用变量
final Person p=new Person(45);
//改变Person对象的age Field,合法
p.setAge(23);
System.out.println(p.getAge());
//下面语句对p重新赋值,非法
//p=null;
}
}
可见,final修饰的引用类型变量不能被重新赋值,但可以改变引用类型变量所引用对象的内容。p变量也使用了final修饰,表明p变量不能被重新赋值,但p变量所引用Person对象的Field可以被改变。
可执行“宏替换”的final变量
对一个final变量来说,不管它是类Field、实例Field,还是局部变量,只要该变量满足3个条件,这个final变量就不再是一个变量,而是相当于一个直接量。
- 使用final修饰符修饰;
- 在定义该final变量时指定了初始值;
- 该初始值可以在编译时就被确定下来。
看如下程序:
public class FinalLocalTest {
public static void main(String[] args){
//定义一个普通局部变量
final int a=5;
System.out.print(a);
}
}
对于这个程序来说,变量a其实根本不存在,当程序执行System.out.println(a);代码时,实际转换为执行System.out.println(5)
final修饰符的一个重要用途就是定义“宏变量”。当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
除了上面那种为final变量赋值时赋直接量的情况外,如果被赋的表达式只是基本的算术表达式或字符串连接运算,没有访问普通变量,调用方法,Java编译器同样会将这种final变量当成“宏变量”处理。示例如下:
public class FinalReplaceTest
{
public static void main(String[] args)
{
//下面定义了4个final“宏变量”
final int a=5 + 2;
final double b=1.2 / 3;
final String str="疯狂" + "Java";
final String book="疯狂Java讲义:" + 99.0;
// 下面的book2变量的值因为调用了方法,所以无法在编译时被确定下来
final String book2="疯狂Java讲义:" + String.valueOf(99.0); //①
System.out.println(book=="疯狂Java讲义:99.0");
System.out.println(book2=="疯狂Java讲义:99.0");
}
}
上面程序中粗体字代码定义了4个final变量,程序为这4个变量赋初始值指定的初始值要么是算术表达式,要么是字符串连接运算。即使字符串连接运算中包含隐式类型(将数值转换为字符串)转换,编译器依然可以再编译时就确定a、b、str、book这4个变量的值,因此他们都是“宏变量”。
①行代码定义的book2与book没有太大的区别,只是定义book2变量时显式将数值99.0转换为字符串,但由于该变量的值需要调用String类的方法,因此编译器无法在编译时确定book2的值,book2不会被当成“宏变量”处理。
下面一个程序:
public class StringJoinTest
{
public static void main(String[] args)
{
String s1="疯狂Java";
//s2变量引用的字符串可以在编译时就确定下来
//因此引用常量池中已有的"疯狂Java"字符串
String s2="疯狂" + "Java";
System.out.println(s1==s2);
//定义2个字符串直接量
String str1="疯狂"; //①
String str2="Java"; //②
//将str1和str2进行连接运算
String s3=str1 + str2;
System.out.println(s1==s3);
}
}
让s1==s3输出true也很简单,只要让编译器可以对str1、str2两个变量执行“宏替换”,这样编译器即可在编译阶段就确定s3的值,就会让s3指向字符串池中缓存的“疯狂Java”。也就是说,只要将①、②两行代码所定义的str1、str2使用final修饰即可。
final方法
final修饰的方法不可被重写,如果处于某些原因,不希望子类重写父类的某个方法,所以使用final把这个方法密封起来。但对于该类提供toString()和equals()方法,都允许子类重写,因此没有使用final修饰他们。
下面程序试图重写final方法:
public class FinalMethodTest
{
public final void test(){}
}
class Sub extends FinalMethodTest
{
//下面方法定义将出现编译错误,不能重写final方法
// public void test(){}
}
该类里定义的test方法是一个final方法,如果其子类试图重写该方法,将会引发编译错误
“重写”父类的private final方法:
public class PrivateFinalMethodTest
{
private final void test(){}
}
class Sub extends PrivateFinalMethodTest
{
//下面的方法定义不会出现问题
public void test(){}
}
上面程序没有任何问题,虽然子类和父类同样包含了同名的void test()方法,但子类并不是重写父类的方法,因此即使父类的void test()方法使用了final修饰,子类中依然可以定义void test()方法。
final修饰的方法仅仅是不能被重写,并不是不能被重载,final修饰的方法仅仅是不能被重写,并不是不能被重载。
final类
final修饰的类不可以有子类,例如java.lang.Math类就是一个final类,它不可以有子类。
当子类继承父类时,将可以访问到父类内部数据,并可通过重写父类方法来改变父类方法的实现细节,这可能导致一些不安全的因素。为了保证某个类不可被继承,则可以使用final修饰这个类
不可变类
不可变(immutable)类的意思是创建该类的实例后,该实例的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方法:
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值。
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;
// 当detail和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;
}
}
与可变类相比,不可变类的实例在整个生命周期中永远处于初始化状态,它的Field不可改变。因此对不可变类的实例的控制将更加简单。
下面程序试图定义一个不可变的Person类,但因为Person类包含一个引用类型Field,且这个引用类是可变类,所以导致Person类也变成了可变类:
class Name
{
private String firstName;
private String lastName;
public Name(){}
public Name(String firstName , String lastName)
{
this.firstName=firstName;
this.lastName=lastName;
}
public void setFirstName(String firstName)
{
this.firstName=firstName;
}
public String getFirstName()
{
return this.firstName;
}
public void setLastName(String lastName)
{
this.lastName=lastName;
}
public String getLastName()
{
return this.lastName;
}
}
public class Person
{
private final Name name;
public Person(Name name)
{
this.name=name;
}
public Name getName()
{
return name;
}
public static void main(String[] args)
{
Name n=new Name("悟空", "孙");
Person p=new Person(n);
// Person对象的name的firstName值为"悟空"
System.out.println(p.getName().getFirstName());
// 改变Person对象的name的firstName值
n.setFirstName("八戒");
// Person对象的name的firstName值被改为"八戒"
System.out.println(p.getName().getFirstName());
}
}
运行上面程序,不难发现Person对象的name的firstName已经被改变了,这就破坏了设计Person类的初衷。
为了保持Person对象的不可变性,必须保护好Person对象的引用类型Field:name,让程序无法访问到Person对象的name Field,也就无法利用name Field的可变性来改变Person对象了。为此,我们将Person类改为如下:
public class Person
{
private final Name name;
public Person(Name name)
{
//设置name Field为临时创建的Name对象,该对象的firstName和lastName
//与传入的name对象的firstName和lastName相同
this.name=new Name(name.getFirstName(), name.getLastName());
}
public Name getName()
{
//返回一个匿名对象,该对象的firstName和lastName
//与该对象里的name的firstName和lastName相同
return new Name(name.getFirstName(), name.getLastName());
}
public static void main(String[] args)
{
Name n=new Name("悟空", "孙");
Person p=new Person(n);
// Person对象的name的firstName值为"悟空"
System.out.println(p.getName().getFirstName());
// 改变Person对象的name的firstName值
n.setFirstName("八戒");
// Person对象的name的firstName值没有被更改
System.out.println(p.getName().getFirstName());
}
}
Person类改写了设置name Field的方法,也改写了name的getter方法。当程序向Person构造器里传入一个Name对象时,该构造器创建Person对象时并不是直接利用已有的Name对象(利用已有的Name对象有风险,因为这个已有的Name对象是可变的,如果程序改变了这个Name对象,将会导致Person对象也发生变化),而是重新创建了一个Name对象来赋给Person对象的name Field。当Person对象返回name Field时,它并没有直接把name Field返回,直接返回name Field的值也可能导致它所引用的Name对象被修改。
因此,如果需要设计一个不可变类,尤其要注意其引用类型Field,如果引用类型Field的类是可变的,就必须采取必要的措施来保护该Field所引用的对象不会被修改,这样才能创建真正的不可变类。
缓存实例的不可变类
不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用西安通的不可变类实例,则应该考虑缓存这种不可变类的实例。毕竟重复创建相同的对象没有太大的意义,而且加大系统开销。如果可能,应该将已经创建的不可变类的实例进行缓存。
缓存是软件设计中一个非常有用的模式,缓存的实现方式有很多种,不同的实现方式可能存在较大的性能差别,关于缓存的性能问题此处不作深入讨论。
本节将使用一个数组来作为缓存池,从而实现一个缓存实例的不可变类:
class CacheImmutale
{
private static int MAX_SIZE=10;
//使用数组来缓存已有的实例
private static CacheImmutale[] cache
=new CacheImmutale[MAX_SIZE];
//记录缓存实例在缓存中的位置,cache[pos-1]是最新缓存的实例
private static int pos=0;
private final String name;
private CacheImmutale(String name)
{
this.name=name;
}
public String getName()
{
return name;
}
public static CacheImmutale valueOf(String name)
{
//遍历已缓存的对象,
for (int i=0 ; i < MAX_SIZE; i++)
{
//如果已有相同实例,则直接返回该缓存的实例
if (cache[i] !=null
&& cache[i].getName().equals(name))
{
return cache[i];
}
}
//如果缓存池已满
if (pos==MAX_SIZE)
{
//把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池的最开始位置
cache[0]=new CacheImmutale(name);
//把pos设为1
pos=1;
}
else
{
//把新创建的对象缓存起来,pos加1
cache[pos++]=new CacheImmutale(name);
}
return cache[pos - 1];
}
public boolean equals(Object obj)
{
if(this==obj)
{
return true;
}
if (obj !=null && obj.getClass()==CacheImmutale.class)
{
CacheImmutale ci=(CacheImmutale)obj;
return name.equals(ci.getName());
}
return false;
}
public int hashCode()
{
return name.hashCode();
}
}
public class CacheImmutaleTest
{
public static void main(String[] args)
{
CacheImmutale c1=CacheImmutale.valueOf("hello");
CacheImmutale c2=CacheImmutale.valueOf("hello");
//下面代码将输出true
System.out.println(c1==c2);
}
}
上面CacheImmutale类使用一个数组来缓存该类的对象,这个数组长度为MAX_SIZE,即该类共可以缓存MAX_SIZE个CacheImmutale对象。当缓存池已满时,缓存池采用“先进先出”规则来决定哪个对象将被移出缓存池。
CacheImmutale类能控制系统生成CacheImmutale对象的个数,需要程序使用该类的valueOf方法来得到其对象,而且程序使用private修饰符隐藏该类的构造器,因此程序只能通过该类提供的valueOf方法来获取实例。
例如Java提供的java.lang.Integer类,它就采用了与CacheImmutale类相同的处理策略,如果采用new构造器来创建Integer对象,则每次返回全新的Integer对象;如果采用valueOf方法来创建Integer对象,则会缓存该方法创建的对象。下面程序示范了Integer类构造器和valueOf方法存在的差异:
public class IntegerCacheTest
{
public static void main(String[] args)
{
//生成新的Integer对象
Integer in1=new Integer(6);
//生成新的Integer对象,并缓存该对象
Integer in2=Integer.valueOf(6);
// 直接从缓存中取出Ineger对象
Integer in3=Integer.valueOf(6);
// 输出false
System.out.println(in1==in2);
//输出true
System.out.println(in2==in3);
//由于Integer只缓存-128~127之间的值
//因此200对应的Integer对象没有被缓存
Integer in4=Integer.valueOf(200);
Integer in5=Integer.valueOf(200);
System.out.println(in4==in5);
// 输出false
}
}
运行上面程序,即可发现两次通过Integer.valueOf(6);方法生成的Integer对象是同一个对象。但由于Integer只缓存-128~127之间的Integer对象,因此两次通过Integer.valueOf(200);方法生成的Integer对象不是同一个对象。
java实例练习
使用选择排序法对数组进行排序
选择排序(Select Sort)是一种简单直观的排序算法。本实例将演示如何使用选择排序法对一维数组进行排序。
1.
新建项目SelectSort,并在其中创建一个SelectSort.java文件。在排序方法中使用选择排序法对随机数进行排序,并将排序后的结果显示到文本域控件中:
public class SelectSort2{
// 排序
public static int[] orderby(int[] nums, String str){
// 从大到小排
if(str.equalsIgnoreCase("desc")){
for(int i=0;i<nums.length;i++){
for(int j = i+1;j<nums.length;j++){
if(nums[i] < nums[j]){
int tem = nums[i];
nums[i] = nums[j];
nums[j] = tem;
}
}
}
}
// 从小到大排
else if(str.equalsIgnoreCase("esc")){
for(int i=0; i<nums.length; i++){
for(int j = i+1; j<nums.length; j++){
if(nums[i]>nums[j]){
int tem = nums[i];
nums[i] = nums[j];
nums[j] = tem;
}
}
}
}
return nums;
}
public static void main(String[] args){
int[] nums = orderby(new int[]{1,5,2,4,9,36,5,7,8,3}, "desc");
for(int n =0 ; n<nums.length; n++){
System.out.print(nums[n] + " ");
}
}
}
本实例应用的主要技术就是选择排序算法。选择排序算法的基本思想如下:
每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。