《疯狂java讲义》学习(15):final修饰符

版权声明:本文为博主原创文章,如若转载请注明出处 https://blog.csdn.net/tonydz0523/article/details/86508523

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] + " ");
        }
    }
}

本实例应用的主要技术就是选择排序算法。选择排序算法的基本思想如下:
每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。

猜你喜欢

转载自blog.csdn.net/tonydz0523/article/details/86508523