基本功2-对象与内存管理

江枫思渺然 提交于 2019-11-28 13:26:29

下面这篇文章是李刚老师的突破程序员基本功16课的读书心得

对象与内存管理

​ Java中的变量分为成员变量和局部变量:

​ 局部变量的生存周期分短暂,被存储在栈内存中

​ 局部变量分为以下几种:

  • 方法的形式参数:随着方法的结束而消失
  • 方法内的局部变量:随着方法的结束而消失
  • 代码块中的局部变量:初始化完成后开始生效,随着代码块的结束而消失

1、实例变量和类变量

​ 实例变量:定义成员变量之前没有使用static修饰的变量,也叫做非静态变量

​ 类变量:定义成员变量时使用static修饰的变量,也称为静态变量

​ 对于上面的静态:也就是static关键字的作用:就是将实例变量变成类变量,static只能修饰在类里定义的成员部分,如果使用了static修饰,这些成员属于类本身,如果没有使用static修饰,这些成员属于类的实例。

​ 实例变量和类变量的区别:在下面简单说明:


  1. 内存空间不同
  • 对于实例变量,每创建一个该类的实例,JVM就会分配一块内存空间

  • 对于类变量,只需要一块内存空间即可。


  1. 初始化时机不同
  • 对于实例变量,每次创建Java对象都会为实例变量分配内存空间,并对实例变量执行初始化。程序可以在3个地方对实例变量进行初始化:

    • 定义实例变量是指定初始化值
    • 非静态初始化代码块中指定初始化值
    • 构造器中对实例变量指定初始化值

    以下例子是实例变量的代码例子

    public class Dog {
        //定义name、age两个实例变量
        String name;
        int age;
    	//定义构造器初始化name、age两个实例变量
        public Dog(String name, int age) {
            this.name = name;
            this.age = age;
        }
        
    
        //定义时指定初始化值
        String color = "white";
        //非静态代码块
        {
        	 color = "yellow";
        }
        
        @Override
        public String toString() {
            return "Dog{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", color='" + color + '\'' +
                    '}';
        }
    }
    public class demo1 {
        public static void main(String[] args) {
            Dog d1 = new Dog("big-dog", 4);
            System.out.println(d1);
            Dog d2 = new Dog("small-dog", 2);
            System.out.println(d2);
        }
    }
    /*
    

程序结果:
Dog{name=‘big-dog’, age=4, color=‘yellow’}
Dog{name=‘small-dog’, age=2, color=‘yellow’}
*/




<font color= #ff0000>
上面的三种方式有先后顺序:
​	非静态代码块和定义时指定初始化值的初始化顺序根据代码的顺序,前面定义相同的变量的初始化值会被后边的覆盖掉。最后构造器执行,如果初始化值不同,以构造器为准。(代码不再赘述,可以自己调试试试)。
</font>

* 对于类变量:只有在程序初始化该类时为该类的类变量分配内存空间,执行初始化。可以在2个地方对类变量执行初始化。

* 定义类变量是指定初始化值
* 静态初始化块中对类变量指定初始化值

代码例子如下:

```java
public class StaticInitTest {
    static int num = 2; //定义类变量时,直接指定初始化值
    static {
        String name = "Java";
    }

    static String name = "JavaScript";

    public static void main(String[] args) {
        System.out.println("num = " + StaticInitTest.num);
        System.out.println("name = "+StaticInitTest.name);
    }
}
/*
程序结果:
num = 2
name = JavaScript
*/

2、父类构造器

​ 当创建任何Java对象时,程序总会先依次调用每个父类非静态初始化块、父类构造器(总是从Object开始)执行初始化,最后调用本类的非静态初始化块、构造器执行初始化

2.1、隐式调用和显式调用

​ 当调用某个类的构造器来创建Java对象时,系统总会先调用父类的费静态初始化块进行初始化。这个调用是隐式调用,而且这个父类的静态初始化块总是会被执行。接着会调用父类的一个或多个构造器执行初始化,这个调用既可以通过super显式调用, 也可以隐式调用。当所有父类的非静态初始化块、构造器依次执行完毕后,系统会调用本类的非静态初始化块、构造器来执行初始化,最后返回本类的实例。

​ 下面是代码例子演示:

class Creature { //生物类
    {
        System.out.println("Creature的非静态初始化块");
    }
    //定义两个构造器
    public Creature() {
        System.out.println("Creature无参构造");

    }

    public Creature(String name) {
        //使用this调用另外的一个重载、无参数的构造器
        this();
        System.out.println("Creature带有name参数的构造器,name参数:" + name);

    }
}
class Animal extends Creature { //动物类
    {
        System.out.println("Animal的非静态代码块");
    }
    public Animal(String name)
    {
        super(name);
        System.out.println("Animal带一个参数的构造器,name参数:"+name);
    }
    public Animal(String name,int age)
    {
        //使用this调用另外一个重载的构造器
        this(name);
        System.out.println("Animal带2个参数的构造器,age:"+age);

    }
}
class Wolf extends Animal { //狼类
    {
        System.out.println("wolf的非静态代码块");

    }
    public Wolf() {
        //显示调用父类有2个无参数的构造器
        super("灰太狼",3);
        System.out.println("Wolf无参的构造器");

    }
    public Wolf (double weight){
        //使用this调用另外一个重载的构造器
        this();
        System.out.println("Wolf的带weight参数的构造器,weight:"+weight);
    }
}
public class InitTest {
    public static void main(String[] args) {
        new Wolf(5.6);
    }
}	

/*
运行结果:
Creature的非静态初始化块
Creature无参构造
Creature带有name参数的构造器,name参数:灰太狼
Animal的非静态代码块
Animal带一个参数的构造器,name参数:灰太狼
Animal带2个参数的构造器,age:3
wolf的非静态代码块
Wolf无参的构造器
Wolf的带weiht参数的构造器,weight:5.6
 */

​ 只要程序创建Java对象,系统总是先调用最顶层的父类的初始化操作。包括初始化块和构造器,然后依次向下调用所有父类的初始化操作,最终执行本类的初始化操作后返回本类的实例,至于调用父类的那个构造器执行初始化,分为以下情况:

  • 子类构造器执行体的第一行代码使用了super显式调用父类构造器,系统将根据super调用里传入的实参列表来调确定调用父类的哪个构造器。

  • 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表来确定本类的另一个构造器。

  • 子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器

    super调用用于显示调用父类的构造器,this调用用于显示调用本类中的其他的构造器,super调用和this调用都只能在构造器中使用,且super调用和this调用都必须要作为构造器的第一行代码,因此super调用和this调用只能使用一个,且最多只能调用一次

2.2、访问子类对象是实例变量

​ 子类的方法可以访问父类的实例变量,这是因为子类继承了父类就会获得父类的成员变量和方法,但是父类的方法不能访问子类的实例变量,因为父类根本不知道被子类继承。

​ 但是,在极端的情况下,可能出现父类访问子类变量的情况。

class Base
{
    //定义一个i的实例变量
    private int i;
    public Base() {
        i = 2;
        System.out.println(this.i);
        this.display();  //这里的this代表谁?
        System.out.println(this.getClass());//this的实际类型是Derived
       // this.sub();
        //因为这里的this是编译类型是Base,所以不能调用sub()方法
    }
    public void display() {
        System.out.println(i);
    }
}
//继承Base的Derived子类
class Derived extends Base {
    private int i = 22;
    //构造器,将实例变量i初始化为222
    public Derived() {
        i = 222;
    }
    public void display() {
        System.out.println(i);
    }
}
public class Test {
    public static void main(String[] args) {
        //创建Derived的构造器来创建实例
        new Derived();
    }
}
/*
运行结果
2
0
class top.pepeduck.demo2.Derived

 */

​ 当变量的编译是类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定。但是通过该变量调用它引用的对象的实例方法是,该本方法的行为由它实际说引用的对象来决定。因此,当程序访问this.i时,将会访问Base类中定义的i实例变量。也就是2,但是执行this.diaplay()代码时,则时间表现出Derived对象的行为,也就是输出Derived对象的i的实例变量,即为0。

Java中的对象是由构造器创建的吗?错。实际情况是,构造器只负责为Java对象实例变量执行初始化,在执行构造器之前,该对象所占用的内存已经分配,这些值默认是空值。在上面的代码中系统为Derived对象分配了2块内存,分别存储Derived对象的两个i实例变量,其中一个属于Base类定义的i实例变量,一个属于Derived类的实例变量。

2.3调用被子类重写的方法

​ 在访问权限允许的情况下,子类可以调用父类的方法,这是因为子类继承父类会获得父类定义的成员变量和方法;但父类不能调用子类的方法,因为父类根本无从知道他讲被那个子类继承,

​ 但是有一种特殊的情况,当子类方法重写了父类方法之后,父类边上看只是属于自己的。被子类重写的方法,但随着背景的改变,将会变成父类实际调用了子类方法。

class Animal {
    private String desc;

    public Animal() {
        this.desc = getDesc();                      //2
    }

    public String getDesc() {
        return "Animal";
    }

    public String toString() {
        return desc;
    }
}

 class Wolf extends Animal {
     private String name;
     private double weight;

     public Wolf(String name, double weight) {
         this.name = name;                          //3
         this.weight = weight;
     }

     //重写父类的getDesc()方法
     @Override
     public String getDesc() {
         return "Wolf[name= " + name + ",weight= " + weight + "]";
     }
}
public class Test {
    public static void main(String[] args) {
        System.out.println(new Wolf("灰太狼",33.3));      //1
    }
}

/*
运行结果:
Wolf[name= 灰太狼,weight= 0.0]
*/

​ 平常输出的是Wolf[name = 灰太狼,weight=33.3]

​ 主要的关键在于上面代码2处调用的是父类定义的getDesc()方法,但实际运行中,变成了调用被子类重写的getDesc()方法,也就是说在初始化Wolf对象之前,会隐式执行父类的无参构造方法也就是说程序执行3之前先执行2处。而执行2的时候Wolf的name和weight的默认值分别为null和0.0。为了避免上面的结果。也就是避免Animal类的构造器中执行被子类重写过的方法,可将Animal类重写成下面的形式

class Animal {


    public String getDesc() {
        return "Animal";
    }

    public String toString() {
        return getDesc();
    }
}

 class Wolf extends Animal {
     private String name;
     private double weight;

     public Wolf(String name, double weight) {
         this.name = name;
         this.weight = weight;
     }

     //重写父类的getDesc()方法
     @Override
     public String getDesc() {
         return "Wolf[name= " + name + ",weight= " + weight + "]";
     }
}
public class Test {
    public static void main(String[] args) {
        System.out.println(new Wolf("灰太狼",33.3));
    }
}

​ 通过改写的类不再提供构造器(系统会提供一个无参数的构造器),程序改由toString()方法来调用被重写的getDesc()方法。这样就保证了this.name=name.this.weight=weight;在getDesc()方法之前被执行,从而使得getDesc()方法得到了Wolf对象的name和height实例变量的值。

如果父类的构造器调用了被子类重写的方法,且通过这个子类构造器来创建了子类的对象,调用了父类的构造器。就会导致子类的重写方法在子类构造器的所有代码之前执行,从而导致子类的重写方法访问不到子类实例变量值的情况。

3、父子实例的内存控制

​ 继承是面向对象的3大特征之一,也是Java语言的重要特性,而父、子继承关系则是Java编程中需要注意的地方。

3.1、继承成员变量和继承成员方法的区别

​ 当子类继承父类是,子类会获得父类中定义的成员变量和方法。当访问权限允许的情况下,子类可以直接访问父类中定义的变量和方法

class Base{
    int count = 2;
    public void display() {
        System.out.println(this.count);
    }
}
class Derived extends Base{
    int count =20;

    @Override
    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);
        b.display();
        
        Derived d = new Derived();
        System.out.println(d.count);
        d.display();
        
        Base bd = new Derived();            //1
        System.out.println(bd.count);
        bd.display();
        
        Base d2b = d;
        System.out.println(d2b.count);          //2
    }
}
/*
运行结果:
2
2
20
20
2
20
2
 */

​ 程序在1处声明了一个Base变量bd,并将一个Derived对象赋给该变量。此时系统将会自动进行向上转型来保证程序的正确。问题是当程序通过bd来访问count实例变量是应该是多少?

​ 直接通过bd来访问count实例变量,输出的将是Base(声明时类型)对象的count实例变量的值,如果通过bd来调用display()方法,该方法将表现出Derived(运行时类型)对象的行为方式。

​ 程序在2处直接将d变量的赋值给d2b变量,只时d2b是Base类型的。这意味着d2b和d两个变量指向了同一个对象,访问d.count输出的为20,访问d2b.count输出的是2。两个指向同一个对象的变量,分别访问他们的实例变量输出不同的值,这表明在d2b、d变量所指向的Java对象中包含了两块内存,分别存放值为2的count实例变量和值为20的count实例变量。

​ 但是不管是d变量还是db变量、d2b变量,只要他们实际执行Derived对象,不管声明他们时用的是什么类型,当通过这些变量调用方法时,方法的行为总是表现出他们实际类型的行为;但如果通过这些变量来访问他们所指向对象的实例变量,这些实例变量的值总是表现出声明这些变量所用类型的行为。由此可见,Java继承在处理成员变量和方法时是有区别的。

​ 总结成句话:通过方法访问的是(实际)子类的值,通过变量访问的是(声明)父类的的值。

3.2、内存中子类实例

​ 创建子类对象时系统内存中并不存在父类对象,程序内存中只有子类对象,只是这个子类对象不仅爆出弄了子类中定义的所有实例变量,还保存了他所有父类定义的实例变量。

​ super关键字的作用?

class Fruit {
    String color = "未确定颜色";
    //定义一个方法,该方法返回调用该方法的实例
    public Fruit getThis() {
        return this;
    }

    public void info() {
        System.out.println("Fruit 方法");
    }
}
public class Apple extends Fruit{
    @Override
    public void info() {
        System.out.println("Apple方法");
    }
    //通过super来调用父类的方法
    public void AccessSuperInfo() {
        super.info();
    }
    //尝试放回super关键字代表的内容
    public Fruit getSuper() {
        return super.getThis();
    }

    String color = "红色";

    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();
    }
}
/*
运行结果:
a和f所引用的对象是否相同:true
访问a所引用对象的color实例变量:红色
访问f所引用对象的color实例变量:未确定颜色
Apple方法
Apple方法
Fruit 方法
 */

​ 上面程序中Fruit类中定义了getThis()方法,该方法直接返回调用该方法的对象,接着Apple类的粗体字代码又定义了一个getSuper()方法, 该方法返回super.getThis()。主程序先创建了一个Apple对象a,然后调用Apple对象的getSuper()方法返回一个Fruit对象f。接着,程序分别判断a和f之间的关系,并通过a、f来访问color实例变量,调用info()方法。

​ 但是实际上可以看出Apple对象的getSuper()方法所返回的实际上是该Apple对象本身,只是他的声明类型是Fruit,因此通过f变量访问color实例变量时,该实例变量的值由Fruit类决定,但通过f变量调用info()方法,该方法的行为由f变量实际所引用的Java对象决定,因此看到的输出结果为Apple方法。

​ 通过上面分析:super关键本身并没有引用任何对象,它甚至不能被当成一个真正的引用变量使用。原因有以下:

  • ​ 子类方法不能直接使用return super;但使用return this。返回调用该方法的对象是允许的。

  • 程序不允许直接把super当成变量使用,例如,试图判断super和a变量是否引用同一个java对象,不能使用super==a,这条语句会引起编译错误。

    对父、子对象在内存中存储的准确结论是:当程序常见一个子类对象时,系统不仅会为该类中定义的实例变量分拨诶内存,也会为其父类中定义的所用实例变量分配内存,及时子类中定义了和父类同名的实例变量。也即是。当系统创建一个Java对象时候,如果该Java类有两个父类(一个直接父类A,一个间接父类B),假设A类中定义了2个实例变量,B类中定义了3个实例变量。当前类中定义了2个实例变量,那么这个Java对象就会保存了2+3+2个实例变量。

    ​ 如果再子类中定义了与父类中已有重名的变量,那么子类中定义的变量会隐藏父类中定义的变量,不是覆盖,即系统为创建子类对象时,依然会为父类中定义的、被隐藏的变量分配内存空间。

    ​ 为了在子类方法方法中访问父类中定义的、被隐藏的实例变量,或者为了在子类方法中调用父类中定义的被覆盖的方法,可以通过super。作为限定来修饰这些实例变量和实例方法。

    ​ 因为子类中定义与父类中同名的实例变量并不完全覆盖父类中定义的实例变量,它只是简单地隐藏了父类中的实例变量,所以会出现下面的特殊情况。

        class Parent{
            public String tag = "小明";
        }
    
    class Derived extends Parent {
        //定义一个私有的tag实例变量来隐藏父类的tag实例变量
        private String tag = "小小明";
        }
    
    
    public class HideTest {
        public static void main(String[] args) {
            Derived d = new Derived();
            //程序不能访问d的私有成员变量,下面这行语句会引起编译错误
            //System.out.println(d.tag);
            //将d向上转型
            System.out.println(((Parent)d).tag);   //这里输出小明
        }
    }
    
    3.3、父、子类的类变量

    ​ 类变量与上面的实例变量类似,不同的是,类变量属于类本身,而实例变量属于Java对象;类变量在初始化阶段完成初始化,而实例变量这在对象初始化阶段完成初始化。

    ​ 由于类变量本质上属于类的本身,因此通常不会涉及父、子实例变量那么复杂,但由于Java允许通过对象来访问类变量,因此也可以使用super来限定访问父类中定义的类变量

    class StaticBase{
        //定义一个count类变量
        static int count = 20;
    }
    
    public class StaticSub extends StaticBase {
        //子类再定义一个count类变量
        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();
        }
    }
    /*
    结果:
    访问本类的count类变量200
    访问父类的count类变量20
    访问父类的count类变量20
     */
    
    

    ​ 从上面的代码可以看出,如果想要访问父类中定义的count类变量,用两种方法:

    • 直接使用父类的类名作为主调来访问count类变量
    • 使用super作为限定来访问count类变量

4、final修饰符

  • final可以修饰变量,被final修饰的变量被赋初始值之后,不能对该变量进行重新赋值
  • final可以修饰方法,被final修饰的方法不能被重写
  • final可以修饰类,被final修饰的类不能派生子类
4.1、final修饰的变量

​ 被final修饰的实例变量必须显示指定初始化值,而且只能在如下3个位置指定初始值

  1. 定义final实例变量是指定初始值

  2. 在非静态初始化块中为final实例变量指定初始化值

  3. 在构造器中为final实例变量指定初始化值

​ 对于普通实例变量,Java程序可以对它执行默认的初始化,也就是将实例变量的值值得顶为默认的初始值0或null,但是对于final实例变量,必须由程序员指定初始化值。

public class FinalInstanceVeriableTest {
    //定义final实例变量是赋初始化值
    final int var1 = "小明".length();
    final int var2;
    final int var3;
    // 在初始化块中为var2赋值
    {
        var2 = "小小明".length();
    }
    //在构造器中为var3赋值
    public FinalInstanceVeriableTest() {
        this.var3 = "小小小明".length();
    }

    public static void main(String[] args) {
        FinalInstanceVeriableTest fiv = new FinalInstanceVeriableTest();
        System.out.println(fiv.var1);
        System.out.println(fiv.var2);
        System.out.println(fiv.var3);
    }
}
/*
程序结果:
2
3
4
 */

​ 上面的代码经过编译器的处理,3种方式都会被抽取到构造器中进行赋初始值。

​ 对于final类变量,同样必须要显示指定初始化值,且final类变量只能在2个地方指定初始值:

  • 定义final类变量是指定初始值。

  • 在静态初始化块中为final类变量指定初始值

    
    public class FinalClassVaribaleTest {
        final static int var1 = "小红".length();
        final static int var2;
        //在静态代码块中为var2赋值
        static {
            var2 = "小小红".length();
        }
    
        public static void main(String[] args) {
            System.out.println(FinalClassVaribaleTest.var1);
            System.out.println(FinalClassVaribaleTest.var2);
        }
    }
    /*
    运行结果:
    2
    3
     */
    

    ​ 上面的代码经过编译器处理后会被抽取到静态代码块中赋初始值,而在本质上final类变量只能在静态初始化块中被赋初始值。

    ​ final修饰局部变量同样要进行显示赋初始值,一旦赋值不可改变

    ​ 通过上面可以发现:被final修饰的变量一旦进行赋值了就不能改变。

    ​ 除此之外,final还有一个功能,代码如下:

    class Price{
        //类成员是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) {
            //通过Price的 INSTANCE 访问currentPrice实例变量
            System.out.println(Price.INSTANCE.currentPrice);
            //显示创建Price实例
            Price p = new Price(2.8);
            //通过显示创建的Price实例访问currentPrice实例变量
            System.out.println(p.currentPrice);
        }
    }
    /*
    运行结果:
    -2.8
    17.2
     */
    
    
    class Price{
        //类成员是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) {
            //通过Price的 INSTANCE 访问currentPrice实例变量
            System.out.println(Price.INSTANCE.currentPrice);
            //显示创建Price实例
            Price p = new Price(2.8);
            //通过显示创建的Price实例访问currentPrice实例变量
            System.out.println(p.currentPrice);
        }
    }
    /*
    运行结果:
    17.2
    17.2
     */
    

    ​ 上面的两段代码的区别分别是类变量有没有final

    ​ 通过发现,使用final修饰类变量时,如果定义该final类变量时指定了初始值,而且该初始值可以在编译是被确定下来,这个final变量不再是一个变量,系统会将其当成一个宏变量爱出行。系统将不会在静态初始化块中对该类变量赋初始值,而将是在类定义中直接使用该初始值代替该final变量。

4.2、执行宏替换的变量

​ 对于一个final变量,不管它是类变量。实例变量,还是局部变量,只要定义该变量时使用了final修饰符修饰,并在定义该final类变量时指定了初始值,而且该初始值可以在编译时就确定下来,那么这个final变量本质上已经不是变量,而是相当于一 个直接量。

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

​ 如果被赋的表达式只是基本的算术运算表达式或字符串连接运算,没有范文普通变量,调用方法,Java编译器同样会将这种final变量当成“宏变量”处理。

public class FinalTest {
    public static void main(String[] args) {
        //下面定义了4个final“宏变量”
        final int a = 5 + 2;
        final double b = 1.2 / 3;
        final String str="小明" + "好帅";
        final String name = "小明" + 99.0;
        //下面的name2变量因为调用了方法,所以无法在编译时就被确定下来
        final String name2 = "小明" + String.valueOf(99.0);
        System.out.println(name == "小明99.0");
        System.out.println(name2 == "小明99.0");
    }
}
/*
运行结果:
true
false
 */

​ 上面的程序中表面上看name和name2没有太大的区别,但是name2将数值99.0转换为字符串,但是由于该变量需要调用String类的方法,因此编译器无法在编译的时候就确定name2的值。也就不会被当成“宏变量”处理。可能这样的解释还不太明确,下面还有一个例子

public class FinalStringTest {
    public static void main(String[] args) {
        String s1 = "小明好帅";
        String s2 = "小明" + "好帅";
        System.out.println(s1 == s2);
        //定义两个字符串直接量
        String str1 = "小明";
        String str2 = "好帅";
        String s3 = str1 + str2;
        System.out.println(s1 == s3);
    }
}
/*
运行结果:
true
false
 */

​ 上面程序中s1是一个普通字符串直接量,s2是字符串直接量进行拼接,由于编译器在编译阶段就可以确定s2的值为"小明好帅"。所以呢系统会让s2直接指向字符串缓冲只给你的“小明好帅”字符串。

​ 对于s3,它的值由srt1和str2进行连接运算得到的,由于s1和s2是普通的变量,编译器不会执行“宏替换”,因此编译的时候无法确定s3的值,不会让s3执行字符串池中的“小明好帅”。

​ 如果想让s1 ==s3为true,只要编译器可对str1和str2这两个变量执行宏替换就可以了。上面的代码可修改成下面的形式。

public class FinalStringTest {
    public static void main(String[] args) {
        String s1 = "小明好帅";
        String s2 = "小明" + "好帅";
        System.out.println(s1 == s2);
        //定义两个字符串直接量
        final String str1 = "小明";
        final String str2 = "好帅";
        String s3 = str1 + str2;
        System.out.println(s1 == s3);
    }
}
/*
运行结果:
true
true
 */

​ 对于实例变量而言,除了可以在定义该变量的时候赋初始值之外,还可以在非静态初始化块、构造器中对它进行赋初始值,而且3个地方的效果是一样的,但是对于final实例变量而言,只有定义在该变量时候,自动初始化值才能有“宏变量”的效果,在非静态初始化块中、构造器中为final实例变量指定初始化值不会有宏变量的效果。

4.3、final修饰的方法不能重写
4.4、内部类中的局部变量

​ 如果程序需要在匿名内部类中使用局部变量,那么这个局部变量必须使用final修饰符修饰

import java.util.Arrays;
interface IntArrayProductor{
    //接口里定义的product方法用于封装"处理行为"
    int product();
}
public class CommandTest {
    //定义一个方法生成指定长度的数组,但是每个数组元素有cmd负责产生
    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;
        //生成数组,具体生成方法取决于IntArrayProductor接口的匿名实现类
        int[] result = ct.process(new IntArrayProductor() {
            @Override
            public int product() {
                return (int) Math.round(Math.random() * seed);
            }
        }, 10);
        System.out.println(Arrays.toString(result));
    }
}
/*
运行结果:
[5, 4, 4, 2, 0, 2, 2, 1, 5, 1]
 */

​ 上面的程序中CommandTest类中定义了一个process()方法。该方法上陈指定长度的数组,当调用该方法是该方法需要接受一个IntArrayProducto 对象,该对象的product方法赋值缠上每个数组元素。上面的匿名内部类实现了IntArrayProductor接口,该匿名内部类中实现的Product()方法中访问了局部变量seed,因此,这个局部变量必须要使用final修饰,否则编译期间会提示“从内部类中访问局部变量,需要被声明称为最终的类型”。当然,普通内部类(这里的内部类指定是局部内部类,静态内部类是不能访问方法体中的变量的)要访问的局部变量也应该要用final修饰。

​ Java要求所有被访问的类访问的局部变量都是使用final修饰也是有其原因的;对于普通局部变量而言,他的作用于就是停留在该方法内,当方法执行结束,该局部变量与会随着消失,但是内部类则可能缠上隐式的闭包,闭包将使局部变量脱离他所在的方法继续存在。

​ 下面的程序在局部变量内脱离它所在方法继续存在的例子。

public class ClosureTest {
    public static void main(String[] args) {
		//定义一个局部变量
        final String str = "java";
        //在内部类中访问局部变量str
        new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    //此处将一直访问到str局部变量
                    System.out.println(str + " " + i);
                    //暂停1秒
                    try {
                        Thread.sleep(1000);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

​ 上面的代码。正常情况下,当程序执行完start代码后,main方法的声明周期就结束了。str变量的作用域也会随着消失。然是新线程里的run方法没有执行完,匿名内部类的实例的声明就一直存在着,就要一直访问str这个局部变量。

​ 由于内部类可能扩大局部变量的作用域。如果被内部类访问的局部变量没有使用final修饰,可能说该变量的是就可能改变,就会引起极大的问题,因此java的编译器就会要求所用被内部类访问的局部变量要使用final修饰符修饰。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!