对象与内存控制

要点:
        实例变量属于java对象
        类变量属于类本身
        实例变量的初始化细节
        类变量的初始化细节
        子类构造器调用父类构造器
        避免在构造器钟访问子类的实例变量
        避免在构造器钟调用被子类重写的方法
        java继承对成员变量和方法的区别
        父、子实例的实例变量的内存分配机制
        父、子类的类变量的内存分配
        final修饰符的作用
        系统对哪些final变量执行“宏替换”
        final方法注意点
        使用final修饰被匿名、局部内部类访问的局部变量

 

java的内存管理分为两个方面:
    内存分配和内存回收。
    这里的内存分配特指创建java对象时JVM为该对象在堆内存中所分配的内存空间。
    内存回收指的是当java对象失去引用,变成垃圾时,JVM的垃圾回收机制自动清理该对象,并回收该对象所占用的内存。

 

2.1实例变量和类变量:
    java程序的变量大体可分为成员变量和局部变量。其中局部变量可以分为如下3类:
        形参:在方法签名中定义的局部变量,由方法调用者负责为其复制,随方法的结束而消亡
        方法内的局部变量:在方法内定义的局部变量,必须在方法内对齐进行显式初始化。这种类型的局部变量从初始化完成后开始生效,随方法的结束而消亡
        代码块内的局部变量:在代码块内定义的局部变量,必须在代码块内对其进行显式初始化。这种类型的局部变量从初始化完成后开始生效,随着代码块的结束而消亡。
        
    局部变量的作用时间很短,它们都被存储在方法的栈内存中
    类体内定义的变量被称为成员变量(Field)。如果定义该成员变量时没有使用static修饰,该成员变量又被称为非静态变量或者实例变量,如果使用了static修饰,则该成员变量又可被称为静态变量或类变量
    
    对于static关键字而言,从词义上来看,它是“静态”的意思。但是从java程序的角度来看,static的作用就是将势力成员变成类成员。static只能修饰在类里定义的成员部分,包括成员变量、方法、内部类、初始化块、内部枚举类。如果没有使用static,这里成员属于该类的实例;如果使用了static修饰,这些成员就属于类本身。从这个意义看来,static只能修饰类里的成员,不能修饰外部类、不能修饰局部变量、局部内部类
    
    表面上看,java类里定义成员变量时没有先后顺序,但实际上java要求定义成员变量时必须采用合法的前向引用。
    public class ErrorDef{
        //下面代码将提示:非法向前引用
        int num1 = num2 +2;
        int num2 = 20;
    }
    类似的,两个类变量也不允许采用这种“非法前向引用”
    public class ErrorEef2{
        //下面代码将提示:非法前向引用
        static int num1 = num2 +2;
        static int num2 = 20;
    }
    但是如果一个实施例变量,一个是类变量,则实例变量总是可以引用类变量
    public class RightDef{
        //下面的代码将完全正常
        int num1 = num2 +2;
        static int num2 = 20;
    }
    前面情况的原因是实例变量的初始化时机与顺序有关,类变量和成员变量的初始化时机也不太用。
        2.1.1实例变量和类变量的属性
            static修饰的成员变量时类变量,属于类本身;没有static修饰的成员变量时实例变量,属于该类的实例。在同一个JVM内,每个类只对应一个Class对象,但是每个类可以创建多个java对象。
            一个类的类变量只需要一块内存空间,对于实例变量,该类每创建一次实例,就需要为实例变量分配一块内存

class Person{
                String name;
                int age;
                static int eyeNum;
                public void info(){
                    System.out.println("我的名字是: " + name + ",我的年龄是:" + age );
                }
            }
            public class FieldTest {
                public static void main(String[] args){
                    //类变量属于该类本身,只要该类初始化完成,程序即可使用此变量
                    Person.eyeNum = 2;
                    System.out.println("Person的eyeNum属性:" + Person.eyeNum);

                    Person p = new Person();
                    p.name = "猪八戒";
                    p.age = 300;
                    System.out.println("通过p变量访问eyeNum类变量:" + p.eyeNum);
                    p.info();

                    p.eyeNum = 3;

                    System.out.println("Person的eyeNum属性:" + Person.eyeNum);
                    System.out.println("通过p变量访问eyeNum类变量:" + p.eyeNum);

                }
            }
View Code

大部分时候会把类和对象严格地区分开来,但是从另一个角度看,类也是对象,虽有的类都是Class的实例。每个类初始化完成之后,系统都会为该类创建一个对应的Class实例,程序可以通过反射获取某个类所对应的Class实例。
            一旦Person类初始化完成,程序即可通过Person类访问eyeNum类变量,除此之外java还允许通过Person类的任意实例来访问eyeNum类变量。虽然java允许通过Person对象来访问Person类的eyeNum类变量,但由于Person对象本身并没有eyeNum类变量,因此程序通过Person对象来访问eyeNum类变量时,底层依然会转换为通过Person访问eyeNum的类变量。

 

2.1.2实例变量的初始化时机
            对于实例变量而言,它属于java对象本身,每次程序创建java对象时都需要为实例变量分配内存空间,并执行初始化。
            从程序运行的角度来看,每次创建java对象都会为实例变量分配内存空间,并对实例变量执行初始化。
            从语法角度来看,程序可以在3个地方对实例变量执行初始化:
                1、定义实例变量时指定初始值
                2、非静态初始化代码块中对实例变量指定初始值
                3、构造器中对实例变量指定初始值
            其中第1、2种方式比第三种方式更早执行,但1、2种方式的执行顺序与他们在源程序中的排列顺序相同

class Cat{
                String name;
                int age;
                public Cat(String name, int age){
                    System.out.println("执行构造器");
                    this.name  = name;
                    this.age = age;
                }
                {
                    System.out.println("执行非静态初始化块");
                    weight = 2.0;
                }
                double weight = 2.3;
                public String toString(){
                    return "Cat[name=" + name + ",age=" + age + ",weight=" + weight + "]";
                }
            }
            public class InitTest {
                public static void main(String[] args){
                    Cat cat = new Cat("kitty", 2);
                    System.out.println(cat);
                    Cat c2 = new Cat("Jerfield", 3);
                    System.out.println(c2);
                }
            }
            
View Code

    初始化块中指定初始值与定义weight时指定初始值,都属于对该实例变量执行的初始化操作,它们的执行顺序与他们在源程序中的排列顺序相同。
            定义实例变量时指定的初始值、初始化块中为实例变量指定初始值的语句的地位是平等的,当经过编译器处理后,它们都将会被提取到构造器中。
            double weight = 2.3;
            实际上会被分为两步执行:
                1、double weight; 创建java对象时系统根据该语句为该对象分配内存
                2、weight = 2.3; 这条语句将会被提取到java类的构造器中执行
                
                这个的话可以将生成的Class文件反编译看下。或者javap看编译器的处理。

 

2.1.3 类变量的初始化时机
            实例变量属于java类本身,只有当程序初始化该Java类时才会为该类的类变量分配内存空间,并执行初始化。
            从程序运行角度看,每个JVM对一个java类只初始化一次,因此Java程序每运行一次,系统只为类变量分配一次内存空间,执行一次初始化。
            从语法角度来看,程序可以在2个地方对类变量执行初始化:
                1、定义类变量时指定初始值
                2、静态初始化块中对类变量指定初始值
            这两种方式的执行顺序和它们在源代码中的排列顺序相同。我感觉Class类应该也有类似隐式构造这种概念

  

class Price{
                final static Price INSTANCE = new Price(2.8);
                static double initPrice = 20;
                double currentPrice;
                public Price(double discount){
                    currentPrice = initPrice - discount;
                }
            }
            public class PriceTest {
                public static void main(String[] args){
                    System.out.println(Price.INSTANCE.currentPrice);//首先时机,声明Price中的变量,默认值与类型有关,然后按顺序初始化类变量,先INSTANCE后initPrice,所以new Price时initPrice为0,返回的是-2.8
                    Price p = new Price(2.8);
                    System.out.println(p.currentPrice);//这个时候new 了一个,走构造20 -2.8=17.2
                }
            }
View Code

 

2.2父类构造器
        当创建任何java对象时,程序总是会依次调用每个父类静态代码块、父类构造器执行初始化,最后才调用本类的非静态初始化块、构造器执行初始化
        2.2.1隐式调用和显式调用
            当调用某个类的构造器来创建java对象时,系统总会先调用父类的非晶态初始化块进行初始化。这个调用时隐式执行的,而且父类的静态初始化块总是会被执行。接着调用父类的一个或多个构造器执行初始化,这个调用既可以是通过super进行显式调用,也可以是隐式调用。

  

class Creature{
                {
                    System.out.println("Creature的非静态初始化块");
                }
                public Creature(){
                    System.out.println("Creature无参数的构造器");
                }
                public Creature(String name){
                    this();
                    System.out.println("Creature带有name参数的构造器,name参数:"  +name);
                }
            }
            class Animal extends Creature{
                {
                    System.out.println("Animal的非静态初始化块");
                }
                public Animal(){}
                public Animal(String name){
                    super(name);
                    System.out.println("Animal带一个参数的构造器,name参数:" + name);
                }
                public Animal(String name, int age){
                    this(name);
                    System.out.println("Animal带两个参数的构造器,其中age:" + age);
                }
            }
            class Wolf extends Animal{
                {
                    System.out.println("Wolf的非静态初始化块");
                }
                public Wolf(){
                    super("灰太狼",3);
                    System.out.println("Wolf无参数的构造器");
                }
                public Wolf(double weight){
                    this();
                    System.out.println("Wolf的带weight参数的构造器,weight参数:" + weight);
                }
            }
            public class InitTest02 {
                public static void main(String[] args){
                    new Wolf(5.6);
                }
            }
View Code

   在程序创建java对象,系统总是先调用最顶层父类的初始化操作,包括初始化块和构造器,然后依次向下调用所有父类的初始化操作,最终执行本类的初始化操作返回本类的实例。至于调用父类那个构造器执行初始化,则分为如下几种情况:
                1、子类构造器执行体的第一行代码使用super显式调用父类构造器,系统将根据super调用里传入的实参列表来确定调用父类的哪个构造器
                2、子类构造器执行体的第一行代码使用this显示调用本类中重载的构造器,系统根据this调用里传入的实参列表来确定本类另一个构造器
                3、子类构造器执行中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器
            注意:
                super调用用于显式调用父类的构造器,this调用用于显式调用另一个重载的构造器,super和this调用都只能在构造器中使用,而且super调用和this调用都必须作为构造器的第一行代码。

  

  2.2.2访问子类对象的实例变量

            当创建任何java对象时,程序总是会依次调用每个父类静态代码块、父类构造器执行初始化,最后才调用本类的非静态初始化块、构造器执行初始化
        2.2.1隐式调用和显式调用
            当调用某个类的构造器来创建java对象时,系统总会先调用父类的非晶态初始化块进行初始化。这个调用时隐式执行的,而且父类的静态初始化块总是会被执行。接着调用父类的一个或多个构造器执行初始化,这个调用既可以是通过super进行显式调用,也可以是隐式调用。
          

 class Creature{
                {
                    System.out.println("Creature的非静态初始化块");
                }
                public Creature(){
                    System.out.println("Creature无参数的构造器");
                }
                public Creature(String name){
                    this();
                    System.out.println("Creature带有name参数的构造器,name参数:"  +name);
                }
            }
            class Animal extends Creature{
                {
                    System.out.println("Animal的非静态初始化块");
                }
                public Animal(){}
                public Animal(String name){
                    super(name);
                    System.out.println("Animal带一个参数的构造器,name参数:" + name);
                }
                public Animal(String name, int age){
                    this(name);
                    System.out.println("Animal带两个参数的构造器,其中age:" + age);
                }
            }
            class Wolf extends Animal{
                {
                    System.out.println("Wolf的非静态初始化块");
                }
                public Wolf(){
                    super("灰太狼",3);
                    System.out.println("Wolf无参数的构造器");
                }
                public Wolf(double weight){
                    this();
                    System.out.println("Wolf的带weight参数的构造器,weight参数:" + weight);
                }
            }
            public class InitTest02 {
                public static void main(String[] args){
                    new Wolf(5.6);
                }
            }
View Code


            在程序创建java对象,系统总是先调用最顶层父类的初始化操作,包括初始化块和构造器,然后依次向下调用所有父类的初始化操作,最终执行本类的初始化操作返回本类的实例。至于调用父类那个构造器执行初始化,则分为如下几种情况:
                1、子类构造器执行体的第一行代码使用super显式调用父类构造器,系统将根据super调用里传入的实参列表来确定调用父类的哪个构造器
                2、子类构造器执行体的第一行代码使用this显示调用本类中重载的构造器,系统根据this调用里传入的实参列表来确定本类另一个构造器
                3、子类构造器执行中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器
            注意:
                super调用用于显式调用父类的构造器,this调用用于显式调用另一个重载的构造器,super和this调用都只能在构造器中使用,而且super调用和this调用都必须作为构造器的第一行代码。
                
            子类的方法可以访问父类的实例变量,这是因为子类继承父类就会获得父类的成员变量和方法,但父类的方法不能访问子类的实例变量,因为父类根本无从得知它将被哪个子类继承,它的子类将会增加怎样的成员变量。
            但是在极端的情况下,可能出现父类访问子类变量的情况。
          

 class Base{
                private int i = 2;
                public Base(){
                    this.display();
                }
                public void display(){
                    System.out.println(i);
                }
            }
            class Derived extends Base{
                private int i = 22;
                public Derived(){
                    i = 222;
                }
                public void display(){
                    System.out.println(i);
                }
            }
            public class Test01 {
                public static void main(String[] args){
                    new Derived(); //1
                    /**
                     * 得到的输出结果是0
                     * 匪夷所思吗?
                     * 当程序在1的位置的时候,这个Derived对象其实是有两个i实例变量的。
                     *
                     * java对象是由构造器构建的吗?实际情况是构造器只对java对象实例变量进行初始化,也就是赋初始值。在执行构造器代码之前,该变量所占的内存已被分配下来,这些内存里面默认是空值。
                     * 当new操作的时候,系统先为Derived对象分配内存空间,这时需要为Derived的实例变量分配两块内存,分别用于存放Derived的两个i实例变量,一个是它定义的,一个是父类定义的。此时的i的值都是0
                     * 在程序执行Derived的构造之前会先执行父类构造,编译后,父类的构造如下格式:
                     *  i = 2; //将赋值提取到构造
                     *  this.display();
                     *  
                     */
                }
            }
View Code


            上面情况的原因我还是有点混,记死几点吧:
                当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定。但通过该变量调用它所引用的对象的实例方法时,该方法的行为将由它实际所引用的对象来决定。
                this的编译类型是它所在的类Base,运行时类型是实际调用的类也就是Derived
    2.2.3调用被子类重写的方法
        在访问权限允许的情况下,子类可以调用父类方法,这是因为子类继承父类会获得父类定义的成员变量和方法;但是父类不能调用子类的方法,因为父类根本无从得知它将被哪个子类继承,它的子类将会增加怎么样的方法。
        但是,有特殊情况:          

 class Animal{
                private String desc;
                public Animal(){
                    this.desc = getDesc();//这里隐式为 this.desc = this.getDesc();所以getDesc代表的是实际对象的getDesc方法所以是被重写了的那个getDesc方法,因为子类还没走构造,所以都是初始值。
                }
                public String getDesc(){
                    return "Animal";
                }
                public String toString(){
                    return desc;
                }
            }
            public class Wolf extends Animal{
                private String name;
                private double weight;
                public Wolf(String name, double weight){
                    this.name = name;
                    this.weight = weight;
                }
                public String getDesc(){
                    return "Wolf[name=]" + name +" ,weight=" + weight + "]";
                }
                public static void main(String[] args){
                    System.out.println(new Wolf("灰太狼", 32.3));
                }
            }
View Code

 

 父子类型的内存控制
        2.3.1继承成员变量和继承方法的区别
            好多书籍都会介绍:当子类继承父类时,子类会获得父类中定义的成员变量和方法。当访问权限允许的情况下,子类可以直接访问父类中定义的成员变量和方法。
            其实,java继承中对成员变量和方法的处理是不同的。
          

 class Base{
                int count = 2;
                public void display(){
                    System.out.println(this.count);
                }
            }
            class Derived extends Base{
                int count = 20;
                public void display(){
                    System.out.println(this.count);
                }
            }
            public class FieldAndMethodTest {
                public static void main(String[] args){
                    Base b = new Base();
                    System.out.println(b.count);//2
                    b.display();//2
                    Derived d = new Derived();
                    System.out.println(d.count);//20
                    Base bd = new Derived();
                    System.out.println(bd.count);//2
                    bd.display();//20
                    Base d2b = d;
                    System.out.println(d2b.count);//2
                }
            }
View Code


            通过对象.属性访问成员变量,得到的时对象声明类型的值
            对象.方法的话,调用的是实际对象的方法
            可见,java继承在处理成员变量和方法时是有区别的
           

class Animal{
                public String name;
                public void info(){
                    System.out.println(name);
                };
            }
            public class Wolf extends Animal{
                private double weight;
            }
View Code


            使用javap分析可以看到:
                Wolf继承Animal的时候,编译器会直接将Animal里的void info()方法转移到Wolf钟,如果Wolf也包含了void info()方法,编译器无法将Animal里的void info()方法转移到Wolf类中。这意味着编译器无法将Animal的void info()方法转移到Wolf类中
                对于实例变量则不存在这样的现象,即使子类中定义了与父类完全同名的实例变量,这个变量依然不可能覆盖父类中定义的实例变量。
                因为继承成员变量和继承方法之间存在这样的区别,所以对于一个引用类型的变量而言,当通过该变量来访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时类型;当通过该变量来调用它所引用的对象的方法时,该方法行为取决于它所实际引用的对象的类型
        2.3.2内存中子类实例
            前面我们发现,两个引用变量引用同一个对象,但是程序通过这两个引用变量访问同名实例变量时,输出不同的结果
            Derived d  = new Derived();
            System.out.println(d.count);
            Base d2b = d;
            System.out.println(d2b.count);
            可以发现,对象引用同一个堆地址,输出的一个是20一个是2,千米只能说过,当通过引用变量访问它所引用的实例变量时,该实例变量的值取决于声明该变量时所用的类型。
            Derived对象在内存中到底如何存储的呢?很显然它有两个不同的count实例变量,这意味着必须用两块内存保存它们。
          

 class Base{
                int count = 2;
            }
            class Mid extends Base{
                int count = 20;
            }

            public class Sub extends Mid{
                int count = 200;
                public static void main(String[] args){
                    Sub s = new Sub();
                    Mid s2m = s;
                    Base s2b = s;
                    System.out.println(s.count);
                    System.out.println(s2m.count);
                    System.out.println(s2b.count);
                    s.accessMid();
                }
                public void accessMid(){
                    System.out.println(super.count);
                }
            }
View Code


            此时的super代表什么?super代表父类的默认实例?这个说法含糊而笼统,如果super代表父类的默认实例,那么这个默认实例在哪里?
            两个内存图:
            

 


            如果图右时对的,也就是内存中保存3个java对象,三个对象各具有一个count实例变量,super正是引用sub对象关联的Mid对象,实际上图左是对的,只有一个sub对象,且sub对象持有3个count实例变量。
            注意:系统内存中并不存在Mid和Base两个对象,程序内存中只有一个Sub对象,只是这个Sub对象中不仅仅保存了SUb中定义的所有实例变量,还保存了它的所有父类所定义的全部实例变量。
            super的作用到底时什么?
         

  class Fruit{
                String color = "未确定颜色";
                public Fruit getThis(){
                    return this;
                }
                public void info(){
                    System.out.println("Fruit方法");
                }
            }
            public class Apple extends Fruit{
                String color = "红色";
                public void info(){
                    System.out.println("Apple方法");
                }
                public void AccessSuperInfo(){
                    super.info();
                }
                public Fruit getSuper(){
                    return super.getThis();
                }
                public static void main(String[] args){
                    Apple a = new Apple();
                    Fruit f = a.getSuper();
                    System.out.println("a和f所引用的对象是否相同:"  + (a == f));
                    System.out.println("访问a所引用对象的color实例变量:"  + a.color);
                    System.out.println("访问f所引用对象的color实例变量:" + f.color);
                    a.info();
                    f.info();
                    a.AccessSuperInfo();
                }
            }
View Code


            程序中,Fruit类的粗体代码定义了一个getThis()方法,该方法直接返回调用该方法对象;接着Apple类的粗体子代码又定义了一个getSuper()方法,该方法返回super.getThis()。程序试图通过这种方式达到一个效果:当一个Apple对象调用getSuper()方法时,该方法返回该Apple对象所关联的父类对象。
            结果:
            getSuper()返回的时Apple对象本身,只是声明类型时Fruit。所以f.count得到的时Apple的count,f.info是Fruit的方法调用。
            通过上面分析看出:super关键字本身并没有引用任何对象,它甚至不能被当成一个真正的引用变量使用,主要有如下两个原因:
                1、子类方法不能直接使用return super;但使用return this;返回调用该方法的对象是允许的
                2、程序不允许直接把super当成变量使用,例如试图判断super和a变量是否引用同一个java对象-- super == a; 但这条语句将引起编译错误
            对于父、子对象在内存中存储有了准确的结论:当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即子类定义了与父类中同名实例变量。
            如果在子类中定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。注意是隐藏而不是覆盖。
            为了在子类方法中访问父类定义的、被隐藏的实例变量,或者为了在子类方法中调用父类中定义的、被覆盖的方法,可以通过super.作为限定六修饰这些实例变量和实例方法
        2.3.3 父、子类的类变量
            父、子类的类变量基本与此类似
          

 class StaticBase{
                static int count = 20;
            }
            public class StaticSub extends StaticBase{
                static int count  = 200;
                public void info(){
                    System.out.println("访问本类的count类变量:"  + count);
                    System.out.println("访问父类的count类变量:" + StaticBase.count);
                    System.out.println("访问父类的count类变量:" + super.count);
                }
                public static void main(String[] args){
                    StaticSub sb = new StaticSub();
                    sb.info();
                }
            }
View Code


            如果需要访问父类中定义的count类变量,程序有两种方式:
                直接使用父类的类名作为主调来访问count类变量
                使用super.作为限定来访问count类变量

 2.4 final修饰符
        简单的几个口诀:
            final可以修饰变量,被final修饰的变量被赋初始值之后,不能对它重新赋值
            final可以修饰方法,被final修饰的方法不能被重写
            final可以修饰类,被final修饰的类不能派生类
        口诀是不够的。
        
        2.4.1 final修饰的变量
            被final修饰的实例变量必须显式指定初始值,而且只能在如下3个位置指定初始值
                定义final实例变量时指定初始值
                非静态初始化块中为final实例变量指定初始值
                构造器中为final实例变量指定初始值
                     

public class FinalInstanceVariableTest {
                public int var1 = "疯狂java讲义".length();
                final int var2;
                final int var3;
                {
                    var2 = "轻量级Jave EE企业应用实战".length();
                }
                public FinalInstanceVariableTest(){
                    var3 = "疯狂XML讲义".length();
                }
                public static void main(String[] args){
                    FinalInstanceVariableTest fiv = new FinalInstanceVariableTest();
                    System.out.println(fiv.var1);
                    System.out.println(fiv.var2);
                    System.out.println(fiv.var3);
                }
            }
View Code

            上面例子可以知道:final实例变量必须显式被赋初始值,而且本质上final实例变量只能在构造器中被赋初始值。除此之外final实例变量将不能被再次赋值   
            
            对于final类变量而言,同样必须显式指定初始值,而且final类变量只能在2个地方指定初始值:
                定义final类变量时指定初始值
                在静态初始化块中为final类变量指定初始值
              

 public class FinalClassVariableTest {
                    final static int var1 = "疯狂java讲义".length();
                    final static int var2;
                    static {
                        var2 = "轻量级Java EE企业应用实战".length();
                    }
                    public static void main(String[] args){
                        System.out.println(FinalClassVariableTest.var1);
                        System.out.println(FinalClassVariableTest.var2);
                    }
                }
            
View Code

            final修饰局部变量的情形则比较简单--java本来就要求局部变量必须被显式的赋初始值,final修饰的局部变量一样需要被显式地赋初始值。与普通初始变量不同的是:final修饰的局部变量一旦被赋初始值后,再也不能重新赋值。
        
            上面介绍,不难发现,final修饰符一个简单功能:被final修饰的变量一旦被赋予初始值,final变量的值以后将不能被改变。
            除此之外,final修饰符还有一个功能。
          

 class Price{
                final static Price INSTANCE = new Price(2.8);
                final static double initPrice = 20;
                double currentPrice;
                public Price(double discount){
                    currentPrice = initPrice - discount;
                }
            }
            public class PriceTest {
                public static void main(String[] args){
                    System.out.println(Price.INSTANCE.currentPrice);
                    Price p = new Price(2.8);
                    System.out.println(p.currentPrice);
                }
            }
View Code

            发现输出的都是17.2.进行javap观察,发现当使用final修饰类变量时,如果定义该final类变量时指定了初始值,而且该初始值在编译时就能被确定下来(简单的基本数据类型和string),系统将不会在静态初始化代码块中对该类变量赋初始值,而是在类定义中直接使用初始值代替final变量。final会将将其当成“宏变量”处理。也就是说所有出现该变量的地方,系统直接把它当成对应的值处理。对于上面的程序,由于使用final修饰了initPrice类变量,因此Price类的构造器中执行currentPrice = initPrice - discount;代码时,程序将会直接将initPrice替换成20.因此,执行该代码的效果相当于currentPrice = 20 -discount;        
        
        2.4.2 执行"宏替换"的变量
            对于一个final变量,不管它是类变量、实例变量还是局部变量,只要定义该变量时使用了final修饰符修饰,并且在定义该final类变量时指定了初始值(在代码块,构造指定等不算),而且该初始值可以在编译时就被确定下来,那么这个final变量本质已经不再是变量,而是相当于一个直接量
        2.4.3 final方法不能被重写
            当final修饰某个方法时,用于限制该方法不可被它的子类重写。
            学会使用@Override注解
        2.4.4内部类中的局部变量
            如果程序需要在匿名内部类中使用局部变量,那么这个局部变量必须使用final修饰符修饰。
         

  interface IntArrayProductor{
                int product();
            }
            public class CommandTest{
                public int[] process(IntArrayProductor cmd, int length){
                    int[] result = new int[length];
                    for(int i=0; i<length; i++){
                        result[i] = cmd.product();
                    }
                    return result;
                }

                public static void main(String[] args){
                    CommandTest ct = new CommandTest();
                    final int seed = 5;
                    int[] result = ct.process(new IntArrayProductor() {
                        @Override
                        public int product() {
                            return (int)Math.round(Math.random() * seed);
                        }
                    },6);
                }
            }
View Code


            为什么java要求内部类访问的局部变量必须使用final修饰?
                对于普通局部变量而言,它的作用域就是停留在该方法内,当方法执行结束,该局部变量也随之消失;但内部类则可能产生隐式的“闭包”,闭包将使得局部变量脱离它所在的方法继续存在。
             

  public class ClosureTest {
                public static void main(String[] args){
                    final String str = "Java";
                    new Thread(new Runnable(){
                        public void run(){
                            for(int i=0; i<100; i++){
                                System.out.println(str + "" + i);
                                try{
                                    Thread.sleep(100);
                                }catch(Exception ex){
                                    ex.printStackTrace();
                                }
                            }
                        }
                    }).start();//1
                }
            }
View Code

            上面的第一条代码定义了一个局部变量。正常情况下,当程序执行完1代码之后,main方法的声明周期就结束了,局部变量str的用户域也会随之结束。但是只要新县城里的run方法没有执行完,匿名内部类的实例的生命周期就没有结束,将一直可以访问str局部变量的值,这就是内部类会扩大局部变量作用域的实例。
            由于内部类可能扩大局部变量的作用域,如果再加上这个被内部类访问的局部变量没有使用final修饰,也就是变量的值可以随意改变,将引起极大的混乱,所以。

 

posted @ 2017-03-01 16:13  guodaxia  阅读(170)  评论(0)    收藏  举报