Java面向对象(Final修饰符)

107 阅读14分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情

4 final修饰符

final修饰变量时,表示该变量一旦获得了初始值就不可被改变,final既可以修饰成员变量(包括类变量和实例Z变量),也可以修饰局部变量、形参。 final修饰的变量不可被改变,一旦获得了初始值,该final变量的值就不能被重新赋值。

4.1 final成员变量

当执行静态初始化块时可以对类Field赋初始值;当执行普通初始化块、构造器时可对实例Field赋初始值。因此,成员变量的初始值可以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。

对于final修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、'\u0000'、false或null,这些成员变量也就完全失去了存在的意义。因此Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值。

        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成员变量(包括实例Field和类Field)必须由程序员显式初始化,系统不会对final成员进行隐式初始化。

4.2 final局部变量

系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用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;
            }
        }

4.3 final修饰基本类型变量和引用类型变量的区别

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

        class Person
        {
            private int age;
            public Person(){}
            //有参数构造器
            public Person(int age)
            {
                  this.age=age;
            }
            //省略age Field的setter和getter方法
            ...
        }
        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修饰的引用类型变量不能被重新赋值,但可以改变引用类型变量所引用对象的内容。

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

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

  1. 使用final修饰符修饰;
  2. 在定义该final变量时指定了初始值;
  3. 该初始值可以在编译时就被确定下来。
        public class FinalLocalTest
        {
            public static void main(String[] args)
            {
                  //定义一个普通局部变量
                  final int a=5;
                  System.out.println(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");
            }
        }

Java会使用常量池来管理曾经用过的字符串直接量,例如执行String a="java";语句之后,系统的字符串池中就会缓存一个字符串" java ";如果程序再次执行String b="java";,系统将会让b直接指向字符串池中的"java"字符串,因此a==b将会返回true。

        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修饰即可。

4.5 final方法

final修饰的方法不可被重写,如果出于某些原因,不希望子类重写父类的某个方法,则可以使用final修饰该方法。

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

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

    public class PrivateFinalMethodTest
    {
        private final void test(){}
    }
    class Sub extends PrivateFinalMethodTest
    {
        //下面的方法定义不会出现问题
        public void test(){}
    }

final修饰的方法仅仅是不能被重写,并不是不能被重载

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

4.6 final类

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

            public final class FinalClass {}
            //下面的类定义将出现编译错误
            class Sub extends FinalClass {}

4.7 不可变类

不可变(immutable)类的意思是创建该类的实例后,该实例的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方法也是根据字符序列计算得到的。

        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());
            }
        }

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

前面介绍final关键字时提到,当使用final修饰引用类型变量时,仅表示这个引用类型变量不可被重新赋值,但引用类型变量所指向的对象依然可改变。这就产生了一个问题:当创建不可变类时,如果它包含Field的类型是可变的,那么其对象的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对象的不可变性,必须保护好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());
            }
        }

当Person对象返回name Field时,它并没有直接把name Field返回,直接返回name Field的值也可能导致它所引用的Name对象被修改。

4.8 缓存实例的不可变类

不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。

本节将使用一个数组来作为缓存池,从而实现一个缓存实例的不可变类。

        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类的valueOf方法来生成对象时,系统是否重新生成新的对象,取决于图6.3中被灰色覆盖的数组内是否已经存在该对象。如果该数组中已经缓存了该类的对象,系统将不会重新生成对象。

缓存实例的不可变类示意图 在这里插入图片描述 CacheImmutale类能控制系统生成CacheImmutale对象的个数,需要程序使用该类的valueOf方法来得到其对象,而且程序使用private修饰符隐藏该类的构造器,因此程序只能通过该类提供的valueOf方法来获取实例。

提示: 是否需要隐藏CacheImmutale类的构造器完全取决于系统需求。盲目乱用缓存也可能导致系统性能下降,缓存的对象会占用系统内存,如果某个对象只使用一次,重复使用的概率不大,缓存该实例就弊大于利;反之,如果某个对象需要频繁地重复使用,缓存该实例就利大于弊。

例如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只缓存-128~127之间的Integer对象,因此两次通过Integer.valueOf(200);方法生成的Integer对象不是同一个对象。

参考文献:《 疯狂java讲义》 李刚