重新开始学习javase_类再生(类的合成和继承)

一、合成
在新类里简单地创建原有类的对象。我们把这种方法叫作“合成”

为进行合成,我们只需在新类里简单地置入对象句柄即可。举个例子来说,假定需要在一个对象里容纳几个 String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。例:

class Demo1_4 {
    Demo4_4 d3 = new Demo4_4();
}
class Demo4_4 {
    public Demo4_4() {
        System.out.println("Demo4_4");
    }
}

编译器并不只是为每个句柄创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望句柄得到初始化,可在下面这些地方进行:
(1) 在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。
(2) 在那个类的构建器中。
(3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。

class Demo4_4 {
    //在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。
    private String s=new String("wangyang");
}

class Demo2_4 {
    //在那个类的构建器中。
    private String s;
    public Demo2_4() {
        s=new String("wangyang");
    }
}

class Demo3_4 {
    //紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。
    private String s;
    public void doTest(){
        s=new String("wangyang");
        System.out.println(s);
        
    }
}

二、继承

第二种方法则显得稍微有些技巧。它创建一个新类,将其作为现有类的一个“类型”。我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种魔术般的行为叫作“继承”

  1. 继承的语法
    java中,其实只要你创建了一个类,其实这个类都是继承了一个根类(object类)

    需要继承的时候,我们会说:“这个新类和那个旧类差不多。”为了在代码里表面这一观念,需要给出类名。但在类主体的起始花括号之前,需要放置一个关键字extends,在后面跟随“基础类”的名字。若采取这种做法,就可自动获得基础类的所有数据成员以及方法。

    class Cleanser {
    private String s = new String("Cleanser");
    public void append(String a) { s += a; }
    public void dilute() { append(" dilute()"); }
    public void apply() { append(" apply()"); }
    public void scrub() { append(" scrub()"); }
    public void print() { System.out.println(s); }
    public static void main(String[] args) {
    Cleanser x = new Cleanser();
    x.dilute(); x.apply(); x.scrub();
    x.print();
    }
    }
    public class Detergent extends Cleanser {
    // Change a method:
    public void scrub() {
    append(" Detergent.scrub()");
    super.scrub(); // Call base-class version
    }
    // Add methods to the interface:
    public void foam() { append(" foam()"); }
    // Test the new class:
    public static void main(String[] args) {
    Detergent x = new Detergent();
    x.dilute();
    x.apply();
    x.scrub();
    x.foam();
    x.print();
    System.out.println("Testing base class:");
    Cleanser.main(args);
    }
    } ///:~

    需要着重强调的是Cleanser 中的所有类都是public属性。请记住,倘若省略所有访问指示符,则成员默认为“友好的”。这样一来,就只允许对包成员进行访问。在这个包内,任何人都可使用那些没有访问指示符的方法。例如,Detergent 将不会遇到任何麻烦。然而,假设来自另外某个包的类准备继承Cleanser,它就只能访问那些public 成员。所以在计划继承的时候,一个比较好的规则是将所有字段都设为private,并将所有方法都设为public(protected 成员也允许衍生出来的类访问它;以后还会深入探讨这一问题)。当然,在一些特殊的场合,我们仍然必须作出一些调整,但这并不是一个好的做法。注意Cleanser 在它的接口中含有一系列方法:append(),dilute(),apply(),scrub()以及print()。由于Detergent 是从Cleanser 衍生出来的(通过 extends关键字),所以它会自动获得接口内的所有这些方法——即使我们在 Detergent 里并未看到对它们的明确定义。这样一来,就可将继承想象成“对接口的重复利用”或者“接口的再生”(以后的实施细节可以自由设置,但那并非我们强调的重点)。
    正如在scrub()里看到的那样,可以获得在基础类里定义的一个方法,并对其进行修改。在这种情况下,我们通常想在新版本里调用来自基础类的方法。但在 scrub()里,不可只是简单地发出对scrub()的调用。那样便造成了递归调用,我们不愿看到这一情况。为解决这个问题,Java 提供了一个 super 关键字,它引用当前类已从中继承的一个“超类”(Superclass)。所以表达式super.scrub()调用的是方法 scrub()的基础类版本。

    进行继承时,我们并不限于只能使用基础类的方法。亦可在衍生出来的类里加入自己的新方法。这时采取的做法与在普通类里添加其他任何方法是完全一样的:只需简单地定义它即可。extends关键字提醒我们准备将新方法加入基础类的接口里,对其进行“扩展”。foam()便是这种做法的一个产物。在Detergent.main()里,我们可看到对于Detergent 对象,可调用Cleanser 以及Detergent 内所有可用的方法(如foam())。

  2.  初始化基础类(其实就是子类实例化肯定会父类也会实例化,有父才有子---就是调用子类的构造器的时候,也会调用父类的构造器,如果父类中没有无参构造器,需要用super(**),显示调用)

    由于这儿涉及到两个类——基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生类的对象里了。

    当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构建器中执行初始化,通过调用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构建器中,Java 会自动插入对基础类构建器的调用。下面这个例子向大家展示了对这种三级继承的应用:

    //: Cartoon.java
    // Constructor calls during inheritance
    class Art {
    Art() {
    System.out.println("Art constructor");
    }
    }
    class Drawing extends Art {
    Drawing() {
    System.out.println("Drawing constructor");
    }
    }
    public class Cartoon extends Drawing {
    Cartoon() {
    System.out.println("Cartoon constructor");
    }
    public static void main(String[] args) {
    Cartoon x = new Cartoon();
    }
    } ///:~
    Art constructor
    Drawing constructor
    Cartoon constructo
    View Code
    •   子类可以用不同的入参来重载父类中的方法

      @Test
          public void test() {
              Demo2_5 d = new Demo2_5();
              d.prin(1);
              d.prin("S");
              d.prin('S');
              /*
               * int i String i char i
               */
          }
      
      
      
      class Demo1_5 {
          public void prin(int i) {
              System.out.println("int i");
          }
      
          public void prin(char i) {
              System.out.println("char i");
          }
      }
      
      class Demo2_5 extends Demo1_5 {
          public void prin(String i) {
              System.out.println("String i");
          }
      }
    • 上面是无参构造器(下面是有参的构造器,使用super显示的指示)

      //: Chess.java
      // Inheritance, constructors and arguments
      class Game {
      Game(int i) {
      System.out.println("Game constructor");
      }
      }
      class BoardGame extends Game {
      BoardGame(int i) {
      super(i);
      System.out.println("BoardGame constructor");
      }
      }
      public class Chess extends BoardGame {
      Chess() {
      super(11);
      System.out.println("Chess constructor");
      }
      public static void main(String[] args) {
      Chess x = new Chess();
      }
      } ///:~




三、再谈protected

还记得我们的四个java权限访问修辞符码?

  • public是整个工程都可以访问
  • protected:如果一个子类不在父类同一个包中,而父类中的方法或属性是用protected修辞的,那么子类虽然不在同一个包中但也可以访问

四,深入继承关系(转:http://blog.csdn.net/smithdoudou88/article/details/12756187)

 
继承的基本概念:
(1)Java不支持多继承,也就是说子类至多只能有一个父类。
(2)子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法。
(3)子类中定义的成员变量和父类中定义的成员变量相同时,则父类中的成员变量不能被继承。
(4)子类中定义的成员方法,并且这个方法的名字返回类型,以及参数个数和类型与父类的某个成员方法完全相同,则父类的成员方法不能被继承。
 
分析以上程序示例,主要疑惑点是“子类继承父类的成员变量,父类对象是否会实例化?私有成员变量是否会被继承?被继承的成员变量在哪里分配空间?”
1:虚拟机加载ExtendsDemo类,提取类型信息到方法区。
2:通过保存在方法区的字节码,虚拟机开始执行main方法,main方法入栈。
3:执行main方法的第一条指令,new Student(); 这句话就是给Student实例对象分配堆空间。因为Student继承Person父类,所以,虚拟机首先加载Person类到方法区,并在堆中为父类成员变量在子类空间中初始化。然后加载Student类到方法区,为Student类的成员变量分配空间并初始化默认值。将Student类的实例对象地址赋值给引用变量s。
4:接下来两条语句为成员变量赋值,由于name跟age是从父类继承而来,会被保存在子类父对象中(见图中堆中在子类实例对象中为父类成员变量分配了空间并保存了父类的引用,并没有实例化父类。),所以就根据引用变量s持有的引用找到堆中的对象(子类对象),然后给name跟age赋值。
4:调用say()方法,通过引用变量s持有的引用找到堆中的实例对象,通过实例对象持有的本类在方法区的引用,找到本类的类型信息,定位到say()方法。say()方法入栈。开始执行say()方法中的字节码。
5:say()方法执行完毕,say方法出栈,程序回到main方法,main方法执行完毕出栈,主线程消亡,虚拟机实例消亡,程序结束。
 
总结:相同的方法会被重写,变量没有重写之说,如果子类声明了跟父类一样的变量,那意味着子类将有两个相同名称的变量。一个存放在子类实例对象中,一个存放在父类子对象中。父类的private变量,也会被继承并且初始化在子类父对象中,只不过对外不可见。
 
super关键字在java中的作用是使被屏蔽的成员变量或者成员方法变为可见,或者说用来引用被屏蔽的成员变量或成员方法,super只是记录在对象内部的父类特征(属性和方法)的一个引用。啥叫被屏蔽的成员变量或成员方法?就是被子类重写了的方法和定义了跟父类相同的成员变量,由于不能被继承,所以就称作被屏蔽。
说到这里,上面提出的疑惑也就解开了。
 
 
五,上塑造型,就是向上转型,也就是子类当父类用(这个时候如果子类特有的属性或方法是不能用的)
 
六、深入final关键字
  1. final 数据

    许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面:
    (1) 编译期常数,它永远不会改变
    (2) 在运行期初始化的一个值,我们不希望它发生变化
    对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。也就是说,计算可在编译期间提前执行,从而节省运行时的一些开销。在 Java 中,这些形式的常数必须属于基本数据类型(Primitives),而且要用 final关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个值。

    无论static还是 final字段,都只能存储一个数据,而且不得改变。若随同对象句柄使用final,而不是基本数据类型,它的含义就稍微让人有点儿迷糊了。对于基本数据类型,final 会将值变成一个常数;但对于对象句柄,final 会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有“常数”效果)。这一限制也适用于数组,它也属于对象。

    下面是演示 final字段用法的一个例子:

    package t1;
    
    class Value {
        int i = 1;
    }
    
    public class t6 {
        // Can be compile-time constants
        final int i1 = 9;
        static final int I2 = 99;
        // Typical public constant:
        public static final int I3 = 39;
        // Cannot be compile-time constants:
        final int i4 = (int) (Math.random() * 20);
        static final int i5 = (int) (Math.random() * 20);
        Value v1 = new Value();
        final Value v2 = new Value();
        static final Value v3 = new Value();
        // ! final Value v4; // Pre-Java 1.1 Error:
        // no initializer
        // Arrays:
        final int[] a = { 1, 2, 3, 4, 5, 6 };
    
        public void print(String id) {
            System.out.println(id + ": " + "i4 = " + i4 + ", i5 = " + i5);
        }
    
        public static void main(String[] args) {
            t6 fd1 = new t6();
            // ! fd1.i1++; // Error: can't change value
            fd1.v2.i++; // Object isn't constant!
            fd1.v1 = new Value(); // OK -- not final
            for (int i = 0; i < fd1.a.length; i++)
                fd1.a[i]++; // Object isn't constant!
            // ! fd1.v2 = new Value(); // Error: Can't
            // ! fd1.v3 = new Value(); // change handle
            // ! fd1.a = new int[3];
            fd1.print("fd1");
            System.out.println("Creating new FinalData");
    
            t6 fd2 = new t6();
            fd1.print("fd1");
            fd2.print("fd2");
        }
    } /// :~

    由于i1和 I2都是具有 final属性的基本数据类型,并含有编译期的值,所以它们除了能作为编译期的常数使用外,在任何导入方式中也不会出现任何不同。I3是我们体验此类常数定义时更典型的一种方式:public表示它们可在包外使用;Static 强调它们只有一个;而 final 表明它是一个常数。注意对于含有固定初始化值(即编译期常数)的 fianl static基本数据类型,它们的名字根据规则要全部采用大写。也要注意i5 在编译期间是未知的,所以它没有大写。
    不能由于某样东西的属性是final,就认定它的值能在编译时期知道。i4 和i5 向大家证明了这一点。它们在运行期间使用随机生成的数字。例子的这一部分也向大家揭示出将final 值设为static 和非static 之间的差异。只有当值在运行期间初始化的前提下,这种差异才会揭示出来。因为编译期间的值被编译器认为是相同的。这种差异可从输出结果中看出:

    fd1: i4 = 15, i5 = 9
    Creating new FinalData
    fd1: i4 = 15, i5 = 9
    fd2: i4 = 10, i5 = 9

    注意对于fd1和 fd2来说,i4的值是唯一的,但 i5的值不会由于创建了另一个t6对象而发生改变。那是因为它的属性是static,而且在载入时初始化,而非每创建一个对象时初始化。从v1 到v4 的变量向我们揭示出final 句柄的含义。正如大家在main()中看到的那样,并不能认为由于v2属于final,所以就不能再改变它的值。然而,我们确实不能再将v2绑定到一个新对象,因为它的属性是final。这便是final 对于一个句柄的确切含义。我们会发现同样的含义亦适用于数组,后者只不过是另一种类型的句柄而已。将句柄变成final 看起来似乎不如将基本数据类型变成 final那么有用。


    • 既然final修辞的是不能改变的,那是不是意思着fianl int i;这种空白的final就没有意义了呢?
      class Demo3_5 {
          final int i;
      
          public Demo3_5() {
              i = 1;// 在构造函数中是可以对空白的final初始化的
          }
          // public void test2(){
          // i=1;
          // }
      }
    • finl修辞入参的时候
      这个时候只能对该入参进行读操作不能写操作(对象的话,句柄只能指向唯一的对象)
      class Demo4_5{
          public void test1(final int i){
              System.out.println(i);//ok
              //i=i+1;错误
          }
      }
  2.   final修辞方法

    之所以要使用final 方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。

    采用final 方法的第二个理由是程序执行的效率。将一个方法设成 final 后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final 方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。Java 编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final 方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。

    类内所有private方法都自动成为final。由于我们不能访问一个 private方法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。可为一个 private方法添加final 指示符,但却不能为那个方法提供任何额外的含义


  3.    final 修辞类

    如果说整个类都是final(在它的定义前冠以 final关键字),就表明自己不希望从这个类继承,或者不允许其他任何人采取这种操作。换言之,出于这样或那样的原因,我们的类肯定不需要进行任何改变;或者出于安全方面的理由,我们不希望进行子类化(子类处理)。除此以外,我们或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。

    注意数据成员既可以是 final,也可以不是,取决于我们具体选择。应用于final 的规则同样适用于数据成员,无论类是否被定义成final。将类定义成 final后,结果只是禁止进行继承——没有更多的限制。然
    而,由于它禁止了继承,所以一个 final类中的所有方法都默认为 final。因为此时再也无法覆盖它们。所以与我们将一个方法明确声明为final 一样,编译器此时有相同的效率选择。可为final 类内的一个方法添加final 指示符,但这样做没有任何意义


  4. 深入理解java final内存模型(转:http://www.infoq.com/cn/articles/java-memory-model-6/)

    对于final域,编译器和处理器要遵守两个重排序规则:

    1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
    2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

    下面,我们通过一些示例性的代码来分别说明这两个规则:

    public class FinalExample {
        int i;                            //普通变量
        final int j;                      //final变量
        static FinalExample obj;
    
        public void FinalExample () {     //构造函数
            i = 1;                        //写普通域
            j = 2;                        //写final域
        }
    
        public static void writer () {    //写线程A执行
            obj = new FinalExample ();
        }
    
        public static void reader () {       //读线程B执行
            FinalExample object = obj;       //读对象引用
            int a = object.i;                //读普通域
            int b = object.j;                //读final域
        }
    }
    

    这里假设一个线程A执行writer ()方法,随后另一个线程B执行reader ()方法。下面我们通过这两个线程的交互来说明这两个规则。

     

    写final域的重排序规则

    写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:

    • JMM禁止编译器把final域的写重排序到构造函数之外。
    • 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

    现在让我们分析writer ()方法。writer ()方法只包含一行代码:finalExample = new FinalExample ()。这行代码包含两个步骤:

    1. 构造一个FinalExample类型的对象;
    2. 把这个对象的引用赋值给引用变量obj。

    假设线程B读对象引用与读对象的成员域之间没有重排序(马上会说明为什么需要这个假设),下图是一种可能的执行时序:

     

    在上图中,写普通域的操作被编译器重排序到了构造函数之外,读线程B错误的读取了普通变量i初始化之前的值。而写final域的操作,被写final域的重排序规则“限定”在了构造函数之内,读线程B正确的读取了final变量初始化之后的值。

    写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程B“看到”对象引用obj时,很可能obj对象还没有构造完成(对普通域i的写操作被重排序到构造函数外,此时初始值2还没有写入普通域i)。

    读final域的重排序规则

    读final域的重排序规则如下:

    • 在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。

    初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如alpha处理器),这个规则就是专门用来针对这种处理器。

    reader()方法包含三个操作:

    1. 初次读引用变量obj;
    2. 初次读引用变量obj指向对象的普通域j。
    3. 初次读引用变量obj指向对象的final域i。

    现在我们假设写线程A没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序:

    在上图中,读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时,该域还没有被写线程A写入,这是一个错误的读取操作。而读final域的重排序规则会把读对象final域的操作“限定”在读对象引用之后,此时该final域已经被A线程初始化过了,这是一个正确的读取操作。

    读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final域一定已经被A线程初始化过了。

    如果final域是引用类型

    上面我们看到的final域是基础数据类型,下面让我们看看如果final域是引用类型,将会有什么效果?

    请看下列示例代码:

    public class FinalReferenceExample {
    final int[] intArray;                     //final是引用类型
    static FinalReferenceExample obj;
    
    public FinalReferenceExample () {        //构造函数
        intArray = new int[1];              //1
        intArray[0] = 1;                   //2
    }
    
    public static void writerOne () {          //写线程A执行
        obj = new FinalReferenceExample ();  //3
    }
    
    public static void writerTwo () {          //写线程B执行
        obj.intArray[0] = 2;                 //4
    }
    
    public static void reader () {              //读线程C执行
        if (obj != null) {                    //5
            int temp1 = obj.intArray[0];       //6
        }
    }
    }
    

    这里final域为一个引用类型,它引用一个int型的数组对象。对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:

    1. 在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

    对上面的示例程序,我们假设首先线程A执行writerOne()方法,执行完后线程B执行writerTwo()方法,执行完后线程C执行reader ()方法。下面是一种可能的线程执行时序:

    在上图中,1是对final域的写入,2是对这个final域引用的对象的成员域的写入,3是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的1不能和3重排序外,2和3也不能重排序。

    JMM可以确保读线程C至少能看到写线程A在构造函数中对final引用对象的成员域的写入。即C至少能看到数组下标0的值为1。而写线程B对数组元素的写入,读线程C可能看的到,也可能看不到。JMM不保证线程B的写入对读线程C可见,因为写线程B和读线程C之间存在数据竞争,此时的执行结果不可预知。

    如果想要确保读线程C看到写线程B对数组元素的写入,写线程B和读线程C之间需要使用同步原语(lock或volatile)来确保内存可见性。

    为什么final引用不能从构造函数内“逸出”

    前面我们提到过,写final域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了。其实要得到这个效果,还需要一个保证:在构造函数内部,不能让这个被构造对象的引用为其他线程可见,也就是对象引用不能在构造函数中“逸出”。为了说明问题,让我们来看下面示例代码:

    public class FinalReferenceEscapeExample {
    final int i;
    static FinalReferenceEscapeExample obj;
    
    public FinalReferenceEscapeExample () {
        i = 1;                              //1写final域
        obj = this;                          //2 this引用在此“逸出”
    }
    
    public static void writer() {
        new FinalReferenceEscapeExample ();
    }
    
    public static void reader {
        if (obj != null) {                     //3
            int temp = obj.i;                 //4
        }
    }
    }
    

    假设一个线程A执行writer()方法,另一个线程B执行reader()方法。这里的操作2使得对象还未完成构造前就为线程B可见。即使这里的操作2是构造函数的最后一步,且即使在程序中操作2排在操作1后面,执行read()方法的线程仍然可能无法看到final域被初始化后的值,因为这里的操作1和操作2之间可能被重排序。实际的执行时序可能如下图所示:

    从上图我们可以看出:在构造函数返回前,被构造对象的引用不能为其他线程可见,因为此时的final域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到final域正确初始化之后的值。

    final语义在处理器中的实现

    现在我们以x86处理器为例,说明final语义在处理器中的具体实现。

    上面我们提到,写final域的重排序规则会要求译编器在final域的写之后,构造函数return之前,插入一个StoreStore障屏。读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。

    由于x86处理器不会对写-写操作做重排序,所以在x86处理器中,写final域需要的StoreStore障屏会被省略掉。同样,由于x86处理器不会对存在间接依赖关系的操作做重排序,所以在x86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说在x86处理器中,final域的读/写不会插入任何内存屏障!

    JSR-133为什么要增强final的语义

    在旧的Java内存模型中 ,最严重的一个缺陷就是线程可能看到final域的值会改变。比如,一个线程当前看到一个整形final域的值为0(还未初始化之前的默认值),过一段时间之后这个线程再去读这个final域的值时,却发现值变为了1(被某个线程初始化之后的值)。最常见的例子就是在旧的Java内存模型中,String的值可能会改变(参考文献2中有一个具体的例子,感兴趣的读者可以自行参考,这里就不赘述了)。

    为了修补这个漏洞,JSR-133专家组增强了final的语义。通过为final域增加写和读重排序规则,可以为java程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指lock和volatile的使用),就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值。

  5. 继承的经典题:
    package t1;
    
    import org.junit.Test;
    
    public class t8 {
    
        @Test
        public void test() {
            A a1 = new A();
            A a2 = new B();
            B b = new B();
            C c = new C();
            D d = new D();
            System.out.println(a1.show(b));// A and A
            System.out.println(a1.show(c));// A and A
            System.out.println(a1.show(d));// A and D
            System.out.println(a2.show(b));// B and B?A
            System.out.println(a2.show(c));// B and B?A
            System.out.println(a2.show(d));// A and D
            System.out.println(b.show(b));// B and B
            System.out.println(b.show(c));// B and B
            System.out.println(b.show(d));// A and D
            /*
             * a1.show(b));Class A 中没有show(B obj),B转向B的父类A,执行A show(A obj)--->return
             * "A and A"
             * 
             * a1.show(c));Class A 中没有show(C obj),C转向C的父类B,Class A 中没有show(B
             * obj),再转向父类A,执行A show(A obj)--->return "A and A"
             * 
             * a1.show(d));Class A 中有show(D obj)执行A show(D obj)--->return "A and D"
             * 
             * 这个比较特殊:A a2 = new B();父类声明,子类实例,你应该把a2当作子类重写完后的父类看,注意只有父类的方法。
             * 
             * a2.show(b));Class A 中没有show(B obj),B转向B的父类A,执行A show(A obj),A的show
             * 方法被重写,执行B show(A obj)--->return "B and A"
             * 
             * a2.show(c));Class A 中没有show(C obj),C转向C的父类B,Class A 中没有show(B
             * obj),B转向父类A,执行A show(A obj),A的show 方法被重写,执行B show(A obj)--->return
             * "B and A"
             * 
             * a2.show(d));Class A 中有show(D obj)执行A show(D obj)--->return "A and D"
             * b.show(b)); Class B 中有show(B obj)--->return "B and B"
             * 
             * b.show(c)); Class B 中没有show(C obj),C转向C的父类B,执行B show(B obj)--->return
             * "B and B"
             * 
             * b.show(d)); Class B 中有继承了Class A 的show(D obj),执行A show(D
             * obj)--->return "A and D"
             */
        }
    
    }
    
    class A {
        public String show(D obj) {
            return ("A and D");
        }
    
        public String show(A obj) {
            return ("A and A");
        }
    }
    
    class B extends A {
        public String show(B obj) {
            return ("B and B");
        }
    
        public String show(A obj) {
            return ("B and A");
        }
    
    }
    
    class C extends B {
    
    }
    
    class D extends B {
    
    }













原文地址:https://www.cnblogs.com/wangyang108/p/5785252.html