java 基础概念

第一章:对象与内存控制
Static
    对于static 关键字而言,从饲义上来看,它是"静态"的意思。但从Java 程序的角度来看,static 的作用就是将实例成员变为类成员。static 只能修饰在类里定义的成员部分 。包括成员变量、方法、内部类、初始化块、内部枚举类。如果没有使用static修饰这些类里的成员,这里成员属于该类的实例;如果使用了static修饰,这些成员就属于类本身. 从这个意义上看, static 只能修饰类里的成员,不能修饰外部类,不能修饰局部交量、局局部内部类。

实例变量的初始化时机
 
    
    对于实例变量而言,它属于Java对象本身,每次程序创建Java对象时都需要为实例变量分配内存空间,并执行初始化。从程序运行的角度来看,每次创建Java对象都会为实例变量分配内存空间,并对实例变量执行初始化.从语法角度来看,程序可以在3 个地方对实例变量执行初始化:
    • 定义实例变量时指定初始位;
    • 非静态初始化决中对实例变量指定初始值;
    • 构造器中对实例变量指定初始值。
    其中第1、2种方式(定义时指定的初始值和非静态初始化块中指定的初始值)比第3种方式(构造器中指定初始值)更早执行,但第1、2种方式的执行顺序与它们在源程序中的排列顺序相同。
    定义实例变量时指定的初始值、初始化块中为实例变量指定初始值的语句的地位是平等的,当经过编译器处理后,它们都将被提取到构造器中.

类变量的初始化时机
    实例变量属于Java 类本身,只有当程序初始化该ava 类时才会为该类的类变量分配内存空间,并执行初始化。
    从程序运行的角度来看,JVM 对一个Java 类只初始化一次,因此Java 程序每运行一次,系统只为类变量分配一次内存空间,执行次初始化。
    从语法角度来看,程序可以在2 个地方对类变量执行初始化:
        • 定义类变量时指定初始位;
        • 静态初始化块中对类变量指定初始值
    这两种方式的执行顺序与它们在源程序中排列顺序相同。
    先为所有类变量分配内存空间,再按源代码中的排列顺序执行静态初始化块中所指定的初始值和定义类变量时所指定的初始值。

构造函数的本质
    构造器只是负责对ava 对象实例变量执行初始化(也就是赋初始值) ,在执行构造器代码之前,该对象所占的内存已经被分配下来,这些内存里值都默认是空值一一对于基本类型的变量,默认的空值就是0 或false; 对于引用类型的变量,默认的空值就是null。

构造器的调用顺序
    当调用某个类的构造器来创建Java 对象时,系统总会先调用父类的非静态初始化块进行初始化。这个调用是隐式执行的, 而且父类的静态初始化块总是会被执行。接着会调用父类的一个或多个构造器执行初始化, 这个调用既可以是通过super进行显式调用, 也可以是隐式调用。当所有父类的非静态初始化块、构造器依次调用完成后,系统调用本类的非静态初始化块、构造器执行初始化,最后返回本类的实例。
    只要在程序创建Java 对象,系统总是先调用最顶层父类的初始化操作,包括初始化块和构造器,然后依次向下调用所有父类的初始化操作,最终执行本类的初始化操作返回本类的实例.至于调用父类的哪个构造器执行初始化,则分为如下几种情况:
    • 子类构造器执行体的第一行代码使用super显式调用父类构造器.系统将根据super 调用里传入的实参列在来确定调用父类的哪个构造器;
    • 子类构造器执行体的第一行代码使用this 显式调用本类中重载的构造器,系统将根据this调用里传入的实参列在来确定本类的另一个构造器(执行本类中另一个构造器时即进入第一种情况);
    • 子类构造器执行提中既没有super 调用,也没有this 调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器.

This和Super
    当this在构造器中时,this代表正在初始化的Java对象。
    super调用用于显式调用父类的构造器,this调用用于显式调用本类中另一个重载的构造器.super调用和this调用都只能在构造器中使用,而且super调用和this调用都必须作为构造器的第一行代。因此构造器中的super调用和this调用最多只能使用其中之一,而且最多只能调用一次。

变量与方法
    当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定。但通过该变量调用它引用的对象的实例方法时,该方法行为将由它实际所引用的对象来决定。
    当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定。但通过该变量调用它引用的对象的实例方法时,该方法行为将由它实际所引用的对象来决定。
    不管声明它们时用什么类型,当通过这些变量调用方法时,方法的行为总是表现出它们实际类型的行为,但如果通过这些变量访问它们所指对象的实例变量,这些实例变量的值总是表现出声明这些变所用类型的行为。

调用被子类重写的方法
    在访问权限允许的情况下,子类可以调用父类方法,这是因为子类继承父类会获得父类定义的成员变量和方法,但父类不能调用子类的方法,因为父类根本无从知道它将被哪个子类继承,它的子类将会增加怎样的方法。但有一种特殊情况,当子类方法重写了父类方法之后,父类表面上只是调用属于自己的、被子类重写的方法,但随着执行context 的改变,将会变成父类实际调用子类的方法。
    如果父类构造器,调用了被子类重写的方法,而且通过子类构造器来创建子类对象,调用(不论是显式还是隐式)了这个父类构造器,就会导致,子类重写的方法在子类构造器的所有代码执行之前被执行,从而导致子类重写的方法访问不到子类的实例变量的情形,如下面的代码:       
[java] view plain copy
  1. class Animal  
  2. {  
  3.     //desc实例变量保存对象toString方法的返回值  
  4.     private String desc;  
  5.     public Animal()  
  6.     {  
  7.         //调用getDesc()方法初始化desc实例变量  
  8.         this.desc = getDesc();                            //④  
  9.     }  
  10.     public String getDesc()  
  11.     {  
  12.         return "Animal";  
  13.     }  
  14.     public String toString()  
  15.     {  
  16.         return desc;  
  17.     }  
  18. }  
  19. public class Wolf extends Animal  
  20. {  
  21.     //定义name、weight两个实例变量  
  22.     private String name;  
  23.     private double weight;  
  24.     public Wolf(String name , double weight)  
  25.     {  
  26.         //为name、weight两个实例变量赋值                      //①  
  27.         this.name = name;  
  28.         this.weight = weight;  
  29.     }  
  30.     //重写父类的getDesc()方法  
  31.     @Override  
  32.     public String getDesc()                                        //②  
  33.     {  
  34.         return "Wolf[name=" + name + " , weight="  
  35.             + weight + "]";  
  36.     }  
  37.     public static void main(String[] args)  
  38.     {  
  39.         System.out.println(new Wolf("灰太郎" , 32.3));         //③  
  40.     }  
  41. }  
       上面的代码,编译后运行结果如下:
[plain] view plain copy
  1. Wolf[name=null , weight=0.0]  
    程序执行入口为③,然后跳转到①执行,但是执行①之前,程序会隐式的调用④,此时子类的name和weight为空值,执行④的代码时,由于是this调用,程序会调用子类的getDesc方法,因此返回的desc值为Wolf[name=null , weight=0.0],因此,最后输出上面的结果。
    
继承成员变量和继承方法的区别
    如果在子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中.对于实例变量则不存在这样的现象,即使子类中定义了与父类完全同名的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量.
因为继承成员变量和继承方法之间将在这样的差别,所以对于一个引用类型的变量而言,当通过该变量访问它所引用的对象的实例变量时,该实例变量的值取决于声明该变量时类型, 当通过该变量来调用它所引用的对象的方法时,该方法行为取决于它所实际引用的对象的类型。
    因此,当定义一个子类对象的时候,系统内存中并不存在其直接父类或者间接父类的对象,内存中只有一个子类对象,只是,这个子类中不仅保存了其直接父类中定义的所有实例变量,还保留了其间接父类中定义的所有实例变量。
    super 关键字本身并没有引用任何对象,它甚至不能被当成一个真正的引用变量使用。主要有如下两个原因:
        • 子类方法不能直接使用return super; 但使用return this;返回调用该方法的对象是允许的;
        • 程序不允许直接把super当成变量使用,例如,试图判断super和a变量是否引用同一个Java对象:super = a; 这条语句将引起编译错误.
    至此,对父、子对象在内存中存储有了准确的结论: 当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即使子类定义了与父类中同名实例变量。也就是说,当系统创建一个Java 对象时候,如果该Java 类有两个父类(一个直接父类A ,一个间接父类B)时,假设A类中定义了2 个实例变量,B类中定义了3个实例变量,当前类中定义了2 个实例变量,那这个Java对象将会保存2+3+2个实例变量。
    如果在子类里定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量。注意不是完全覆盖,因此系统为创建子类对象时,依然会为父类中定义的、被隐藏的变量分配内存空间。
    为了在子类方法中访问父类中定义的、被隐藏的实例变量,或者为了在子类方法中调用父类中定义的、被覆盖(Override) 的方法,可以通过super作为限定来修饰这些实例变量和实例方法.

父子类的类变量
    父、子类的类变量与实例变量基本类似.不同的是,类变量属于类本身,而实例变量则属于Java 对象,类变量在类初始化阶段完成初始化,而实例变量则在对象初始化阶段完成初始化,子类的类变量会隐藏父类的类变量。

final修饰变量
    被final 修饰的实例变量必须显式指定初始值,而且只能在如下3 个位置指定初始值.    
     • 定义final实例变量时指定初始值;
     • 在非静态初始化块中为何final实例变量指定初始值。
     • 在构造器中为final实例变量指定初始值.
    对于普通实例变量,Java程序可以对它执行默认的初始化,也就是将实例变量的值指定为默认的初始值0或null,但对于final 实例变量,则必须由程序员显式指定初始值。
    不过,经过编译器的处理,这三种方式都会被抽取到构造器中赋初始值。
    可以使用javap -c选项来查看编译后的代码结构。
    final实例变量必须显式地被赋初始值,而且本质上final实例变量只能在构造器中被赋初始值。当然,就程序员编程来说,还可在定义final 实例变量时指定初始值,也可以初始化块中为final实例变量指定初始值,但它们本质上是一样的。除此之外,final实例变量将不能被再次赋值。
    对于final类变量而言,同样必须显式指定初始值,而且final类变量只能在2个地方指定初始值:
        • 定义final类变量时指定初始值;
        • 在静态初始化块中为final类变量指定初始值。
    需要指出的是,经过编译器的处理,这2种方式都会被抽取到静态初始化块中赋初始值.
    final类变量必须显式地被赋初始值,而且本质上final实例变量只能在静态初始化块中被赋初始值.当然,就程序员编程来说,还可在定义final 类变量时指定初始值,也可以静态初始化块中为final类变量指定初始值,但它们本质上是一样的。除此之外,final类变量将不能被再次赋值.
    fina1修饰局部变量的情形则比较简单一Java本来就要求局部变量必须被显式地赋初始值,final修饰的局部变量一样需要被显式地赋初始值。与普通初始变量不同的是: final修饰的局部变量被赋初始值之后,以后再也不能对final 局部变量重新赋值.
    被fina1修饰的变量一旦被赋初始值,final变量的值以后将不会被改变。

final变量的“宏替换”效果
    对一个final 变量,不管它是类变量、实例变量,还是局部变量,只要定义该变量时使用了final修饰符修饰,并在定义该final变量时指定了初始值,而且该初始值可以在编译时就被确定下来,那么这个final 变量本质上已经不再是变量,而是相当于一个直接量。编译器会把程序中所有用到该变量的地方值接替换成该变量的值。
    对于实例变量而言,除了可以在定义该变量时赋初始值之外,还可以在非静态初始化块、构造器中对它赋初始值,而且在这3 个地方指定初始值的效果基本一样。但对于final 实例变量而言,只有在定义该变量时指定初始值才会有"宏变量"的效果, 在非静态初始化块、构造器中为final实例变量指定初始值则不会有这种效果。

final修饰方法
    如果父类中某个方法使用了final修饰符进行修饰,那么这个方法将不可能被它的子类访问到,因此这个方法也不可能被它的子类重写。从这个意义上来说,private和final同时修饰某个方法没有太大意义,但是被Java语法允许的。比如,下面的代码将一起编译错误:
[plain] view plain copy
  1. class base  
  2. {  
  3.     final void info()  
  4.     {  
  5.         System.out.println("base info()");  
  6.     }  
  7. }  
  8.   
  9. class derived extends base  
  10. {  
  11.     void info()  
  12.     {  
  13.         System.out.println("derived info()");  
  14.     }  
  15. }  
  16.   
  17. public  class test  
  18. {  
  19.     public static void main(String [] args)  
  20.     {  
  21.         System.out.println("main");  
  22.     }  
  23. }<span style="font-family:宋体;">  
  24. </span><pre code_snippet_id="330040" snippet_file_name="blog_20140506_4_5470651"></pre>  
  25. <pre></pre>  
  26. <pre></pre>  
  27. <pre></pre>  
  28. <pre></pre>  
  29. <pre></pre>  


[plain] view plain copy
  1. D:java>javac test.java  
  2. test.java:11: 错误: derived中的info()无法覆盖base中的info()  
  3. void info()  
  4. ^  
  5. 被覆盖的方法为final  
  6. 1 个错误  
  7.   
  8. D:java>   
局部内部类中对的局部变量

    如果程序需要在内部类(不论何种内部类)中使用内部类外的局部变量,那么这个局部变量必须使用final修饰符修饰。此处所说的内部类指的是局部内部类,因为只有局部内部类(包括匿名内部类)才可访问局部变量,普通静态内部类、非静态内部类不可能访问方法体内的局部变量.
    Java要求所有被内部类访问的局部变量都使用final修饰也是有其原因的:对于普通局部变量而言,它的作用域就是停留在该方法内,当方法执行结束,该局部变量也随之消失,但内部类则可能产生隐式的"闭包(Closure)" ,闭包将使得局部变量脱离它所在的方法继续存在。比如下面的代码:
[java] view plain copy
  1. public class ClosureTest   
  2. {  
  3.     public static void main(String[] args)  
  4.     {  
  5.         //定义一个局部变量  
  6.         final String str = "Java";  
  7.         //在内部类里访问局部变量str  
  8.         new Thread(new Runnable()  
  9.         {  
  10.             public void run()  
  11.             {  
  12.                 for (int i = 0; i < 100 ; i++ )  
  13.                 {  
  14.                     //此处将一直可以访问到str局部变量  
  15.                     System.out.println(str + " " + i);  
  16.                     //暂停0.1秒  
  17.                     try  
  18.                     {  
  19.                         Thread.sleep(100);  
  20.                     }  
  21.                     catch (Exception ex)  
  22.                     {  
  23.                         ex.printStackTrace();  
  24.                     }  
  25.                 }  
  26.             }  
  27.         }).start();  
  28.         //执行到此处,main方法结束  
  29.     }  
  30. }  
    由于内部类可能扩大局部变量的作用域.如果再加上这个被内部类访问的局部变量没有使用final 修饰,也就是说该变量的值可以随意改变,那将引起极大的混乱,因此Java编译器要求所有被内部类访问的局部变量必须使用final修饰符修饰.

@Override
    Java编程中有一个比较有用的工具注释:@Override. 被该注释修饰的方法必须重写父类方法.为了避免在编程过程中出现手误,每当希望某个方法重写父类方法时,总应该该方法添加@Override 注释. 如果被@Override 修饰的方法没有重写父类的方法,编译器会在编译该程序时提示编译错误.

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

第二章:集合
集合存储的类型
    虽然集合号称存储的是Java对象,但实际上并不会真正将Java 对象放入Set 集合中,而只是在Set集合中保留这些对象的引用而言.也就是说,Java集合实际上是多个引用变量所组成的集合,这些引用变量指向实际的Java 对象.就像引用类型的数组一样,当java把对象放入数组时,并不是真正将java对象放入数组中,而是把对象的引用放入数组中,数组中的每个元素,都是一个引用变量。

第三章:java内存管理
new
    程序员需要通过关键字new创建Java对象,即可视作为Java对象申请内存空间,JVM会在堆内存中为每个对象分配空间,当一个Java 对象失去引用时,JVM的垃圾回收机制会自动消除它们,并回收它们所占用的内存空间。

对象引用
    对于JVM 的垃圾回收机制来说,是否回收一个对象的标准在于:是否还有引用变量引用该对象?只要有引用变量引用该对象,垃圾回收机制就不会回收它.也就是说,当Java对象被创建出来之后,垃圾回收机制会实时地监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等.当垃圾回收机制实时地监控到某个对象不再被引用变量所引用时,立即回收机制就会回收它所占用的空间。    基本上,可以把JVM 内存中对象引用理解成一种有向图,把引用变量、对象都当成为有向图的顶点,将寻|用关系当成固的有向边,有向边总是从引用端指向被引用的Java对象。因为Java所有对象都是由一条一条线程创建出来的,因此可把线程对象当成有向图的起始顶点.    对于单线程程序而言,整个程序只有一条mam线程,那么该图就是以main进程为顶点的有向图.在这个有向图中,main顶点可达的对象都处于可达状态,垃圾回收机制不会回收它们,如果某个对象在这个有向图中处于不可达状态,那么就认为这个对象不再被引用,接下来垃圾回收机制就会主动回收它了。

对象状态
    当一个对象在堆内存中运行时,根据它在对应有向图中的状态,可以把它所处的状态分成如下3种:
    • 可达状态:当一个对象被创建后,有一个以上的引用交量引用它.在有向图中可从起始顶点导航带该对象,那它就处于可达状态,程序可通过引用变量来调用该对象的属性和方法.
    • 可恢复状态:如果程序中某个对象不再有任何引用交量引用它,它将先进入可恢复状态,此时从有向图的起始顶点不能导航到该对象. 在这个状态下,系统的垃圾回收机制准备回收该对象所占用的内存. 在回收该对象之前,系统会调用可恢复状态的对象的finalize 方法进行资源清理,如采系统在调用finalize方法重新让一个以上引用变量引用该对象,则这个对象会再次变为可达状态;否则,该对象将进入不可达状态.
    • 不可达状态:当对象的所有关联都被切断,且系统调用所有对象的finalize方法依然没有使该对象变成可达状态,那这个对象将永久性地失去引用,最后变成不可达状态. 只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源.
    一个对象可以被一个方法局部变量所引用,也可以被其他类的类变量引用,或者被其他对象的实例变量性引用。当某个对象被其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态,当某个对象被其他对象的实例变量引用时,只有当引用该对象的对象被销毁或变成不可达状态后,该对象才会进入不可达状态.

对象引用
    JDK 1.2 开始,Java在java.1ang.ref包下提供了3个类:SoftReference、PhantomReference和WeakReference.它们分别代表了系统对对象的3 种引用方式:软引用、虚引用和弱引用。Java语言对对象的引用有如下4种:
    • 强引用
    这是Java 程序中最常见的引用方式,程序创建一个对象,并把这个对象戴给一个引用变量,这个引用变量就是强引用。Java 程序可通过强引用采访问实际的对象,当一个对象被一个或一个以上的强引用变量所引用时,它处于可达状态,它不可能被系统垃圾回收机制回收。强引用是Java编程中广泛使用的引用类型,被强引用所引用的Java 对象绝不会被垃圾回收机制回收,即使系统内存非常紧张,即使有些Java 对象以后永远都不会被用到,JVM也不会回收被强引用所引用的Java对象.
    • 软应用
    软引用需要通过SoftReference类来实现,当一个对象只具有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象,当系统内存空间不足时,系统将会回收它。软引用通常用于对内存敏感的程序中,软引用是强引用很好的替代。当系统内存空间充足时.软引用与强引用没有太大的区别,当系统内存空间不足时,被软引用所引用的Java对象可以被垃圾回收机制回收,从而避免系统内存不足的异常。当程序需要大量创建某个类的新对象可以充分使用软引用来解决内存紧张的难题。
    • 弱引用
    弱引用与软引用有点相似,区别在于弱引用所引用对象的生存期更短。弱引用通过WeakReference 类实现,弱引用和软引用很像,但弱引用的引用级别更低.对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存.当然.并不是说当一个对象只有弱引用时,它就会立即被回收,正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。
    弱引用具有很大的不确定性,因为每次垃圾回收机制执行时都会回收弱引用所引用的对象,而垃极回收机制的运行又不受程序员的控制,因此程序获取弱引用所引用的Java 对象时必须小心空指针异常:通过弱引用所获取的Java对象可能是null。
    由于垃圾回收的不确定性,当程序希望从弱引用中取出被引用对象时,可能这个被引用对象已经被释放了。如果程序需要使用那个被引用的对象,则必须重新创建该对象。
与WeakReference 功能类似的还有WeakHashMap. 其实程序很少会考虑直接使用单个的WeakReference来引用某个Java 对象,因此这种时候系统内存往往不会特别紧张。当程序有大量的Java 对象需要使用弱引用来引用时,可以考虑使用WeakHashMap 来保存它们。
    • 虚引用
    软引用和弱引用可以单独使用,但虚引用不能单独使用,单独使用虚引用没有太大的意义。虚引用的主要作用就是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用所引用对象是否即将被回收。引用队列由java.1ang.ref. ReferenceQueue类表示,它用于保存被回收后对象的引用。当把软引用、弱引用和引用队列联合使用时,系统回收被引用的对象之后,将会把被回收对象对应的引用添加到关联的引用队列中。与软引用和弱引用不同的是,虚引用在对象被释放之前,将把它对应的虚引用添加到它的关联的引用队列中,这使得可以在对象被回收之前采取行动。
    虚引用通过PhantomReference 类实现,它完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用,那它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue) 联合使用。

内存泄露
    但如果程序中有一些Java 对象,它们处于可达状态,但程序以后永远都不会再访问它们,那它们所占用的内存空间也不会被回收,它们所占用的空间也会产生内存泄漏。

垃圾回收机制
    垃圾回收机制主要完成下面两件事情:
        • 跟踪并监控每个Java 对象,当采个对象处于不可达状态时,回收该对象所占用的内存;
        • 清理内存分配、回收过程中产生的内存碎片.

垃圾回收算法
    垃圾回收机制不可能实时检测到每个Java 对象的状态,因此当一个对象失去引用后,它也不会被立即回收,只有等垃圾回收运行时才会被回收。对于一个垃圾回收器的设计算法来说,大致有如下可供选择的设计:
    • 串行回收(Serial)和并行回收(Parallel):串行回收就是不管系统有多少个CPU,始终只用一个CPU 来执行垃圾回收操作;而并行回收就是把整个回收工作拆分成多部分,每个部分由一个CPU负责,从而让多个CPU 并行回收.并行回收的执行效率很高,但复杂度增加,另外也有其他一些副作用,比如内存碎片会增加.
    • 并发执行(Concurrent)和应用程序停止(Stop-the-world): Stop-the-world的垃圾回收方式在执行主圾回收的同时会导致应用程序的暂停.并发执行的垃圾回收虽然不会导效应用程序的暂停,但由于并发执行垃圾回收需要解决和应用程序的执行冲突(应用程序可能会在垃圾回收的过称中修改对象),因此并发执行垃圾回收的系统开销比Stop-the-world 更高,而且执行时也需要更多的堆内存.
    • 压缩(Compacting)和不压缩(Non-compacting)和复制(Copying ):为了减少内存碎片,支持压缩的垃圾回收器会把所有的活对象搬迁到一起,然后将之前占用的内存全部回收. 不压缩式的垃圾回收只是回收内存,这样回收回来的内存不可能是连续的,因此将会有较多的内存碎片.较之压缩式的垃圾回收,不压缩式的回收内存速度快,但分配内存时就会慢,而且无法解决内存碎片的问题.复制式的垃圾回收会将所有可达对象复制到另一块相同的内存中,这种方式的优点是垃圾回收过程不会产生内存碎片,但缺点也很明显,需要复制数据和额外的内存.

压缩(Compacting)和不压缩(Non-compacting)和复制(Copying )
    • 复制:将堆内分成两个相同空间,从根(类似于前面介绍的有向阁的起始定点)开始访问每一个关联的可达对象. 将空间A的可达对象全部复制到空间B. 然后一次性回收整个空间A.
    对于复制算法而言,因为只需访问所有的可达对象,将所有可达对象复制走之后就回收整个空间,完全不用理会那些不可达的对象,所以遍历空间的成本较小,但需要巨大的复制成本和较多的内存。
    • 标记清除(mark-sweep):也就是不压缩的回收方式.垃圾回收器先从根开始访问所有可达对象,将它们标记为可达状态,然后再遍历一次整个内存区域,把所有没有标记为可达的对象进行回收处理.
    标记清除(mark-sweep)无需进行大规模的复制操作,而且内存利用率高.但这种算法需要两次遍历堆内存空间,遍历的成本较大,因此造成应用程序暂停的时间随堆空间大小线性增大。而且垃圾回收回来的内存往往是不连续的,因此整理后堆内存里的碎片很多.
    • 标记压缩(mark-sweep-compact):这是压缩方式,这种方式充分利用上述两种算法的优点,垃圾回收器先从根开始访问所有可达对象,将它们标记为可达状态. 接下来垃圾回收器会将这些活动对象搬迁在一起,这个过程也被称为内存压缩,然后垃圾回收机制再次回收那些不可达对象所占用的内存空间,这样就避免了回收产生的内存碎片.

堆内存的分代回收
    现行的垃圾回收器用分代的方式来采用不同的回收设计.分代的基本思路是根据对象生存时间的长短,把堆内存分成3 个代:
    • Young (年轻代);
    • Old (老年代);
    • Permanent (永久代).
    垃圾回收器会根据不同代的特点采用不同的回收算法,从而充分利用各种回收算法的优点。分代回收的一个依据就是对象生存时间的长短,然后再根据不同代采取不同的垃圾回收策略。采用这种"分代回收"的策略基于如下两点事实:
       • 绝大对数的对象不会被长时间引用,这些对象在其Young 期间就会被回收;
       • 很老的对象(生存时间很长)和很新的对象(生存时间很短)之间很少存在相互引用的情况.
    • Young代
    对于Young 代的对象而言,大部分对象都会很快就进入不可达状态,只有少量的对象能熬到垃圾回收执行时,而垃圾回收器只需要保留Young 代中处于可达状态的对象,对Young代采用复制算法只需遍历那些处于可达状态的对象,而且这些对象的数量较少,可复制成本也不大,因此大部份垃圾回收器对Young代都采用复制算法,以充分发挥复制算法的优点。
    Young代由l个Eden区和2个Survivor区构成。绝大多数对象先分配到Eden区中(有一些大的对象可能会直接被分配到Old代中),Survivor区中的对象都至少在Young代中经历过一次垃圾回收,所以这些对象在被转移到Old代之前会先保留在Survivor空间中。同一时间2个Survivor空间中有一个用来保存对象,而另一个是空的,用来在下次垃圾回收时保存Young代中的对象。每次复制就是将Eden和第l个Survior的可达对象复制到第2 个Survivor区,然后清空Eden与第1个Survior区。Eden和Survivor的比例通过一XX:SurvivorRatio附加选项来设定,默认为32。如果Survivor太大会产生浪费,太小则会使一些Young代的对象提前进入Old代。
    • Old代
    如果Young代中对象经过数次垃圾回收依然还没有被回收掉,即这个对象经过足够长的时间还处于可达状态,垃圾回收机制就会将这个对象转移到01d代。Old代的大部分对象都是"久经考验"的"老人"了,因此它们没那么容易死。而且随着时间的流逝. Old代的对象会越来越多,因此Old 代的空间要比Young代空间更大。出于这两点考虑,Old代的垃圾回收具有如下两个特征:
    • Old代总圾回收的执行频率无需太高,因为很少有对象会死掉;
    • 每次对Old代执行垃圾回收需要是长的时间来完成.
    基于以上考虑,垃圾回收器通常会使用标记压缩算法。这种算法可以避免复制Old代的大量又如象,而且由于Old 代的对象不会很快死亡,回收过程不会大量地产生内存碎片,因此相对比较划算。
    • Permanent代
    Permanent代主要用于装载Class、方法等信息,默认为64M,垃圾回收机制通常不会回收Permanent 代中的对象。对于那些需要加载很多类的服务器程序,往往需要加大Permanent代内存,否则可能因为内存不足而导致程序终止。
    当Young代的内存将要用完的时候,垃圾回收机制会对Young代进行垃圾回收,垃圾回收机制会采用较高的频率对Young 代进行扫描和回收。因为这种回收的系统开销比较小,因此也被称为次要回收(minor collection)。当Old 代的内存将要用完时,垃圾回收机制会进行全回收,也就是对Young代和Old 代都要进行回收,此时回收成本就大得多了,因此也称为主要回收(major collection)。通常来说,Young代的内存会先被回收,而且会使用专门的回收算法(复制算法)来回收Young代的内存;对于Old 代的回收频率则要低得多,因此也会采用专门的回收算法。如果需要进行内存压缩,每个代都独立地进行压缩。
    下面两个选项用于设置Java 虚拟机内存大小的一些参数:
        • -Xmx:设直Java虚拟机堆内存的最大容量,如java -Xmx256m XxxClass.
        • -Xns:设直Java虚拟机堆内存的初始容量,如java -Xms128m XxxClass.
        • -XX:MinHeapFreeRatio = 40:设置Java堆内存最小的空闲百分比,默认值为40,如java-XX:MinHeapFreeRatio = 40 XxxClass.
        • -XX:MaxHeapFreeRatio = 70:设直Java堆内存最大的空闲百分比,默认值为70,如java-XX:MaxHeapFreeRatio = 70 XxxClass.
        • -XX:NewRatio = 2:设直Young/Old 内存的比例,如java -XX:NewRatio = I XxxClass.
        • -XX:NewSize = size:设直Young代内存的默认容量,如java -XX:NewSize = 64mX xxClass.
        • -XX:SurvivorRatio = 8:设直Young代中Eden/Survivor的比例,如java -XX:SurvivorRatio = 8 XxxClass.
        • -XX:MaxNewSize = size:设直Young代内存的最大容量,如java -XX:MaxNewSize = 128m XxxClass.
        • -XX:PennSize = size:设直Pennanent代内存的默认容量,如java -XX: PennSize = 128m XxxC1ass.
        • -XX:M似PennSize= 64m:设直Permanent代内存的最大容量,如java -XX:MaxPennSize = 128m XxxC1ass.
    当设置Young代的内存超过了-Xmx设置的大小时,Young设置的内存大小将不会起作用,JVM会自动将Young代内存设置为与-Xmx设置的大小相等。

常用的垃圾回收器
     1、串行回收器(Serial Collector)
    串行回收器通过运行Java 程序时使用-XX: +UseSerialGC 附加选项启用。串行回收器对Young代和Old代的回收都是串行的(只使用一个CPU) ,而且垃圾回收执行期间会使得应用程序产生暂停。具体策略为,Yound代采用串行复制的算法. Old代采用串行标记压缩算法。
    串行回收器对Young代的回收流程:
    系统将Eden中的活动对象直接复制到初始为空的To Survior区中,如果有些对象占用空间特别大,垃圾回收器会直按降其复制到01d代中。对于From Survior 区中的活动对象(该对象至少经历过一次垃圾回收),到底是复制到To Survior区中,还是复制到Old代中,则取决这个对象的生存时间:如果这个对象的生存时间较长,它将被复制到01d 代中;否则,将被复制到To Survior 区中。完成复制之后,Eden和Form Survior区中剩下的对象都是不可达对象,系统直接回Eden区和From Survior 区的所有内存,而原来空的To Survior区则保存了活动的对象。在下一次回收时,原本的From Survior区将变为To Survior区,原本的To Survior区将变为From Survior区。
    串行回收器对Old代的回收流程:
    串行回收器对Old代的回收采用串行、标记压缩算法(mark-sweep-compact),这个算法有3个阶段: mark(标识可达对象)、sweep(清除) 、compact (压缩)。在mark阶段,回收器会识别出哪些对象仍然是可达的,在sweep阶段将会回收不可达对象所占用的内存。在compact阶段回收器执行sliding compaction(滑动压缩),把活动对象往Old代的前端启动,而在尾部保留一块连续的空间,以便下次为新对象分配内存空间。
     2、并行回收器
    并行回收器通过运行Java程序时使用-XX:+UseParallelGC附加选项启用,它可以充分利用计算机的多个CPU来提高来垃圾回收吞吐量。对于并行回收器而言,只有多CPU并行的机器才能发挥其优势。
    并行回收器对Young代的回收流程:
    并行回收器对于Young代采用与串行回收器基本相似的回收算法,只是增加了多CPU并行的能力,即同时启动多线程并行来执行垃圾回收。线程数默认为CPU个数,当计算机CPU很多时,可用-XX:ParallelGCThreads = size来减少并行线程的数目。
    并行回收器对Old代的回收流程:
    并行回收器对于Old代采用与串行回收器完全相同的回收算法,不管计算机有几个CPU,并行回收器依然采用单线程、标记整理的方式进行回收。
     3、并行压缩回收器(Parallel Compacting Collector)
    并行压缩回收器是在J2SE 5.0 update 6开始引人的,它和并行回收器最大的不同是对Old代的回收使用了不同的算法, 并行压缩回收器最终会取代并行回收器。并行压缩回收器通过运行Java程序时使用-XX: + UseParallelOldGC附加选项启用,通过-XX :ParallelGCThreads = size来设置并行线程的数目。
    并行压缩回收器对Young代的回收流程:
    并行压缩回收器对于Young代采用与并行回收器完全相同的回收算法。
    并行压缩回收器对Young代的回收流程:
    并行压缩回收器的改变主要体现在对Old代的回收上。系统首先将Old代划分成几个固定大小的区域。在mark 阶段,多个垃极回收线程会并行标记Old代中的可达对象。当某个对象被标记为可达对象时,还会更新该对象所在区域的大小以及该对象的位置信息。接下来是summary阶段。Summary阶段操作直接操作Old代的区域,而不是单个的对象。由于每次垃圾回收的压缩都会在Old 代的左边部分存储大量可达对象,对这样的高密度可达对象的区域进行压缩往往很不划算。所以summary 阶段会从最左边的区域开始检验每个区域的密度,当检测到某个区域中能回收的空间达到了某个数值的时候(也就是可达对象的密度较小时).垃圾回收器会判定该区域以及该区域右边的所有区域都应该进行回收,而该区域左边的区域都会被会被标识为密集区域,垃圾回收器既不会把新对象移动到这些密集区域中去,也不会对这些密集区域进行压缩。该区域和其右边的所有区域都会被进行压缩并回收空间。summary阶段目前还是串行操作,虽然并行是可以实现的,但重要性不如对mark和压缩阶段的并行重要。最后是compact阶段。回收器利用summary 阶段生成的数据识别出有哪些区域是需要装填的,多个垃圾回收线程可以并行地将数据复制到这些区域中。经过这个过程后,Old代的一端会密集地存在大量活动对象,另一踹则存在大块的空闲块。
     4、并发标识-清理(Concurrent-Mark-Sweep)回收器(CMS)
    并发标识-清理回收器通过运行Java程序时使用-XX: + UseConcMarkSweepGC附加选项启用。
    CMS对Young代的回收流程:
    CMS回收器对Young代的回收方式和并行回收器的回收方式完全相同。由于对Young的回收依然采用复制回收算法,因此垃圾回收时依然会导致程序暂停,除非依靠多CPU并行来提高垃圾回收的速度。
    常来说,建议适当加大Young代的内存。如果Young代内存够大就不用频繁地进行垃圾回收,而且增加垃圾回收的时间间隔后可以让更多的Young 代对象自己死掉,从而避免复制。但将Young代内存设得过大也有一个坏处:当垃圾回收器回收Young代内存时,复制成本会显著上升(复制算法必须等Young满了之后才开回收).所以回收时会让系统暂停时间显著加长。
   CMS对Old代的回收流程:
    CMS对Old代的回收多数是并发操作,而不是并行操作。垃圾回收开始的时候需要一个短暂的暂停,称之为初始标识(initial mark) 。这个阶段仅仅标识出那些被直接引用的可达对象。接下来进入了并发标识阶段(concurrent marking phase) .垃圾回收器会依据在初始标识中发现的可达对象来寻找其他可达对象。由于在并发标识阶段应用程序也会同时在运行,无法保证所有的可达对象都被标识出来,因此应用程序会再次很短地暂停一下,多线程并行地重新标记之前可能因为并发而漏掉的对象,这个阶段也被称为再标记(remark)阶段。
    完成了再标记以后,所有的可达对象都已经被标识出来了,接下来就可以运行并发清理操作了。CMS回收器的最大改进在于对Old代的回收,它只需2次短暂的暂停,而其他过程都是与应用程序并发执行的,因此对实时性要求较高的程序更合适。
    对于串行、标记压缩的回收器而言,它可以等到Old代满了之后再开始回收,反正垃圾回收总会让应用程序暂停。但CMS 回收器要与应用程序并发运行,如果Old满了才开始回收,那应用程序将无内存可用,所以系统默认在Old 代68%满的时候就开始回收。如果系统内存设得比较大,而且程序分配内存速度不是特别快时,可以通过-XX: CMSlnitiatingOccupancyFraction = ratio 适当增大这个比例。而且CMS不会进行内存压缩,也就是说不可达对象占用的内存被回收以后,垃圾回收器不会移动可这对象占用的内存。
    由于Old代的可用空间不是连续的,因此CMS垃圾回收器必须保存一份可用空间的列表。当需要分配对象的时候, 垃圾回收器就要通过这份列表找到能容纳新对象的空间,这样就会使得分配内存时的效率下降,从而影响了Young代回收过程中将Young代对象移到Old代的效率。
    对于CMS 回收器而言,当垃圾回收器执行并发标识时,应用程序在运行的同时也在分配对象,因此Old 代也同时在增长。而且,虽然可达对象在标识阶段会被识别出来,但有些在标识阶段成为垃圾的对象并不能立即被回收,只有等到下次垃圾回收时才能被回收。因此CMS回收器较之前面的几种回收器需要更大的堆内存.
   CMS对Permanent代的回收流程:
    对于Permanent代内存,CMS可通过运行Java程序时使用-XX:+CMSClassUnloadingEnabled,-XX:+CMSPermGenSweepingEnabled,附加选项强制回收Permanent代内存.

内存管理技巧
     尽量使用直接量
    当需要使用字符串,还有Byte 、Short 、Integer 、Long 、Float 、Double、Boolean 、Character包装类的实例时,程序不应该采用new 的方式来创建对象,而应该直接采用直接量来创建它们例如,程序需要"hello"字符串,应该采用如下代码:
    String str = "hello";
    上面这种方式会创建一个"hello" 字符串,而且JVM的字符串缓存池还会缓存这个字符事。但如果程序使用如下代码:
    String str = new String("hello");
    此时程序同样创建了一个缓存在字符串缓存池中的"hello"字符串。除此之外str 所引用的String对象底层还包含一个char[]数组,这个char[]数组里依次存放了h 、e、l 、l 、o 等字符。
     不要使用String类型的字符串进行连接操作
    String、StringBuilder、StringBuffer都可代表字符串,其中String代表字符序列不可变的字符串,而StringBuidler和StringBuffer 都代表字符序列可变的字符串.如果程序使用多个String对象进行字符串连接运算.在运行时将生成大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降。
     尽早释放无用对象的引用
     尽量少使用静态变量
    从理论上来说, Java对象何时被回收由垃圾回收机制决定,对程序员来说是不确定的。由于垃圾回收机制判断一个对象是否是垃圾的唯一标准就是该对象是否有引用变量引用它,因此推荐尽早释放对象的引用。但最坏的情况是某个对象被static变量所引用,那么垃圾回收机制通常是不会回收这个对象所占的内存。比如下面的代码:
[java] view plain copy
  1. class Person  
  2. {  
  3.     static Object obj = new Object();  
  4. }  
   对于上面的Object对象而言,JVM会将程序中的Person类的信息存入Permanent代中,也即Person类、obj引用变量都将存储在Permanent代,只要obj变量还引用到它,它就不会被垃圾回收机制所回收。obj变量是Person类的静态变量,因此它的生命周期与Person 类同步。在Person类不被卸载的情况下,Person类对应的Class 对象会常驻内存,直到程序运行结束。因此obj所引用的Object对象一旦被创建,也会常驻内存,直到程序运行结束。
     不要在经常调用的方法、循环中创建Java对象
    经常调用的方法和循环有一个共同特征,这些代码段会被多次重复调用,虽然这些变量都是代码块的局部变量,当循环执行结束时这些局部变量都会失效,因此,循环会导致系统需要不断地为这些对象分配内存空间,执行初始化操作,而这些对象的生存时间并不长,接下来系统又需要回收它们所占的内存空间,在这种不断的分配、回收操作中,程序的性能受到巨大的影响.
     缓存经常使用的对象
    如果有些对象需要被经常使用,可以考虑把这些对象用缓存池保存起来,这样当下次需要时,就可直接拿出这些对象来用。典型的缓存就是数据连接池,数据连接池里缓存了大量数据库连接,每次程序需要访问数据库时都可直接取出数据库连接。除此之外,如果系统中还有一些常用的基础信息,也考虑对它们进行缓存。实现缰存时缓存通常有两种方式:
    • 使用HashMap进行缓存;
    • 直接使用一些开源的缓存项目。
    如果直接使用HashMap进行缓存,程序员需要手动控制HashMap容器里key-value对不至于太多,因为当key-va1ue太多时将导致HashMap 占用过大的内存,从而导致系统性能下降。除了使用HashMap 进行缓存之外,还可使用一些开源的缓存项目来解决这个问题。这些缓存项目都会主动分配一个一定大小的缓存容器,再按照一定算法来淘汰容器中不需要继续缓存的对象。这样一方面可以通过缓存己用过的对象来提高系统的运行效率,另一方面又可以控制缓存容器的无限制扩大,从而减少系统的内存占用。对于这种开源的缓存实现有很多选择,如OSCache 、Ehcache 等,它们大都实现了FIFO 、LRU等常见的缓存算法。
    缓存设计本身就是一种以空间换时间的策略,而往往申请的缓存还比较大,因此,如何以最小的缓存代价来存放最多的对象,这就是是缓存设计的关键。
     尽量不要使用finalize方法
    在一个对象失去引用之后,垃圾回收器准备回收该对象之前,垃圾回收机制会先调用该对象的finalize()方法来执行资源清理。但最好别使用finalize()方法来进行资源清理,因为,根据JVM的垃圾回收算法的原理可知,垃圾回收机制的工作量已经够大了,尤其是回收Young代内存时,大都会引起应用程序暂停,使得用户难以忍受。在垃圾回收器本身已经严重制约应用程序性能的情况下,如果再选择使用finalize()方法进行资源清理,无疑会进一步增加垃圾回收器的负担,导致程序运行效率更差。
     考虑使用SoftReference
    当程序需要创建长度很大的数组时,可以考虑使用SoftReference来包装数组元素,而不是直接让将数组元素来引用对象,当内存足够时,它的功能等同于普通引用;当内存不够时,它会牺牲自己,释放软引用所引用的对象。使用软引用来引用对象时不要忘记了软引用的不确定性。程序通过软引用所获取的对象有可能为null,因为当系统内存紧张时,SoftReference 所引用的Java对象将被释放,因此应用程序取出SoftReference所引用的Java之后,应该显式判断该对象是否为null ,当该对象为null时,应重建该对象。

第四章:表达式
JVM对象的创建
    Java 程序中创建对象的常规方式有如下4 种.
        • 通过new 调用构造器创建Java 对象.
        • 通过Class 对象的newInstanceO方法调用构造器创建Java 对象.
        • 通过Java 的反序列化机制从10 流中恢复Java 对象.
        • 通过Java 对象提供的clone()方法复制一个新的Java 对象.
    除此之外, 对于字符亭以及Byte 、Short、lnt、Long、Character、Float、Double 和Boolean这些基本类型的包装类.Java 还允许以直接量赋值的方式来创建Java对象,也可通过简单的算法表达式、连接运算来创建Java对象。

表达式的类型提升
    Java语言中的自动提升规则如下。
        • 所有byte 型、short 型和char 型将被提升到int型.
        • 整个算术农达式的数据类型自动提升到与表达式中最高等级操作数同样的类型.
    Java语言几乎允许所有的双目运算符和等于(=)一起结合成符合贼值运算符,如+= 、-= 、位、/= 、%= 、<<=、>>= 、>>> = 、&等双目运算符。根据Java 的语言规范,复合贼值运算符包含了一个隐式的类型转换,也就是说,下面两条语句并不等价:
a = a + 5;
a += 5 ;
    实际上,a += 5 等价于a = (a 的类型)(a + 5), 这就是复合赋值运算符中包含的隐式类型转换.
    对于复合赋值运算符而言,语句
     El op= E2 (其中op可以是+、-、*、 /、%、<<、>>、>>>、&、^、|等双目运算符)
    并不等价于如下简单的语句:
     El = E1 op E2
    而是等价于如下语句:
     El = (E1 的类型) ( El op E2 )
    也就是说,复合赋值运算符会自动地将它计算的结果值强制类型转换为其左侧变量的类型。如果结果的类型与该变量的类型相同, 那么这个转型不会造成任何影响。如果结果值的类型比该变量的类型要大,那么复合赋值运算符将会执行一次强制类型转换,这个强制类型转换将有可能导致高位"截断"。
    但如果把+当成字符串连接运算符使用,则+=运算符左边的变量只能是String 类型,而不可
能是Sting的父类型(如Object或CharSequence等)。

泛型
    • 当程序把一个原始类型的变量赋给一个带泛型信息的变量时,总是可以通过编译,但只是会提示一些警告信息;
    • 当程序试图访问带泛型声明的集合的集合元素时,编译总是把集合元素当成泛型类型处理,它并不关心集合里集合元素实际类型;
    • 当程序试图访问带泛型声明的集合的集合元素时. JVM会遍历每个集合元素,并自动执行强制转型,如果集合元素的实际类型与集合所带的泛型信息不匹配,运行时将引发ClassCastException.

泛型类型擦除
    当把一个具有泛型信息的对象赋给另一个没有泛型信息的变盘时,所有在尖括号之间的类型信息都将被擦除。当把一个带泛型信息的Java 对象赋给不带泛型信息的变量时,Java程序会发生擦除,这种擦除不仅会擦除使用该Java 类时传入的类型实参, 而且会擦除所有的泛型信息,也就是擦除所有尖括号里的信息。

泛型数组
    JDK虽然支持泛型,但不允许创建泛型数组。

线程创建
    从JDK 1.5 开始,Java提供了3 种方式来创建、启动多钱程:
        • 继承Thread类来创建线程类,重写run()方法作为线程执行体;
        • 实现Runnable接口来创建线程类,重写run()方法作为线程执行体;
        • 实现Callable接口来创建线程类,重写call()方法作为线程执行体.
    其中,第l种方式的效果最差,它有2点坏处:
        • 线程类继承了Thread 类,无法再继承其他父类;
        • 因为每条线程都是一个Thread子类的实例,因此多个线程之间共享数据比较麻烦.
    对于第2种和第3种方式,它们的本质是一样的,只是Ca11able接口里包含的call()方法既可以声明抛出异常,也可以拥有返回值。

线程启动
    启动线程应该使用start()方法,而不是run()方法.如果程序从未调用线程对象的start()方法来启动它,那么这个线程对象将一直处于"新建"状态,它永远也不会作为线程获得执行的机会,它只是一个普通的Java 对象。当程序调用线程对象的run()方法时,与调用普通Java 对象的普通方法并无任何区别,因此绝对不会启动一条新线程.调用一条线程start()方法后,该线程并不会立即进入运行状态,它将只是保持在就绪状态,当线程获得了CPU的控制权后,该线程就可以运行了。

线程同步
    Java提供了synchronized关键字用于修饰方法,使用synchronized修饰的方法被称为同步方法。当然,synchronized 关键字除了修饰方法之外,还可以修饰普通代码块,使用synchronized修饰的代码块被称为同步代码块.
    Java 语法规定,任何线程进入同步方法、同步代码块之前,必须先获取同步方法、同步代码块对应的同步监视器。
    对于同步代码块而言,程序必须显式为它指定同步监视器,对于同步非静态方法而言,该方法的同步监视器是this,即调用该方法的Java对象,对于静态的同步方法而言,该方法的同步监视器不是this,而是该类本身.

    分析一个程序不能仅仅停留在静态的代码上,而是应该从程序执行过程来把握程序的运行细节。不要认为所有放在静态初始化块巾的代码就一定是类初始化操作,静态初始化块中启动的新线程的run()方法代码只是新线程的线程执行体,并不是类初始化操作。类似地,不要认为所有放在非静态初始化块中的代码就一定是对象初始化操作,非静态初始化块中启动的新线程的run()方法代码只是新线程的线程执行体,并不是对象初始化操作。

第五章:流程控制
流程控制中的局部变量
    Java语言规定: for 、while 或do循环中的重复执行语句不能是一条单独的局部变量定义语句,如果程序需要使用循环来重复定义局部变量,这条局部夜量定义语句必须放在花括号内才有效。

foreach
    当使用foreach循环来迭代输出数组元素或集合元素时,系统将数组元素、集合元素的副本传给循环计数器,即foreach 循环中的循环计数器并不是数组元素、集合元素本身。
    由于foreach 循环中的循环计数器本身并不是数组元素、集合元素,它只是一个中间变量,临时保存了正在遍历的数组元素、集合元素,因此通常不要对循环变量进行赋值,虽然这种贼值在语法上是允许的,但没有太大的实际意义,而且极容易引起错误。

第六章:面向对象
instanceof运算符
    根据Java语言规范,使用instanceof运算符有一个限制: instanceof运算符前面操作数的编译时类型必须是如下3种情况:
        • 要么与后面的类相同;
        • 要么是后面类的父类;
        • 要么是后面类型的子类.
    如果前面操作数的编译时类型与后面的类型没有任何关系,程序将设法通过编译。因此,当使用instanceof 运算符的时候,应尽量从编译、运行两个阶段来考虑它: 如果instanceof运算符使用不当,程序编译时就会抛出异常,当使用instanceof 运算符通过编译后,才能考虑它的运算结果是true,还是false。
    instanceof运算符除了可以保证某个引用变量时特定类型的实例外,还可以保证该变量没有引用一个null,这样就可以将该引用变量转型为该类型,并调用该类型的方法,而不用担心会引发ClassCastException或者NullPointerException异常。

强制类型转换
    对于Java 的强制转型而言,也可以分为编译、运行两个阶段来分析它。
    • 在编译阶段,强制转型要求被转型交量的编译时类型必须是如下3 种情况之一:
        > 被转型变量的编译时类型与目标类型相同;
        > 被转型变量的编译时类型是目标类型父类;
        > 被转型变量的编译时类型是目标类型子类,这种情况下可以自动向上转型,无需强制转换.
    如果被转型变量的编译时类型与目标类型没有任何继承关系,编译器将提示编译错误。通过上面分析可以看出,强制转型的编译阶段只关心引用变量的编译时类型,至于该引用变量实际引用对象的类型,编译器并不关心,也没法关心。
    • 在运行阶段,被转型史量所引用对象的实际类型必须是目标类型的实例,或者是目标类型的子类、实现类的实例,否则在运行时将引发ClassCastException 异常.

构造器
    构造器是Java 每个类都会提供的一个"特殊方法"。构造器负责对Java 对象执行初始化操作,不管是定义实例变量时指定的初始值,还是在非静态初始化块中所做的操作,实际都会被提取到构造器中被执行。
    当为构造器声明添加任何返回值类型声明,或者添加void声明该构造器没有返回值时,编译器并不会提示这个构造器有错误,知识系统会把这个所谓的”构造器“当成普通方法处理。
    大部分Java 书籍、资料都笼统地说: 通过构造器来创建一个Java 对象。这样很容易给人一个感觉,构造器负责创建Java 对象.但实际上构造器并不会创建Java 对象,构造器只是负责执行初始化,在构造器执行之前, Java 对象所需要的内存空间, 应该说是由new 关键字申请出来的.
    以下面两种方式创建Java对象无需使用构造器。
        • 使用反序列化的方式恢复Java对象;
        • 使用clone方法复制Java对象.
序列化
    可能有读者对自己以前写的某些单例类感到害怕,以前那些通过把构造器私有来保证只产生一个实例的类真地不会产生多个实例吗,如果程序使用反序列化机制不是可以获取多个实例吗?没错,程序完全通过这种反序列化批制确实会破坏单例类的规则.当然,大部分时候也不会主动使用反序列化去破坏单例类的规则.如果真的想保证反序列化时也不会产生多个Java 实例,则应该为单例类提供readResolve()方法, 该方法保证反序列化时得到已有的Iava 实例.当JVM反序列化地恢复一个新对象时,系统会自动调用这个readResolve()方法返回指定好的对象,从而保证系统通过反序列化机制不会产生多个Java对象。

构造器的递归调用
    以下代码会导致构造器的递归调用:   
[java] view plain copy
  1. public class ClosureTest   
  2. {  
  3.     public static void main(String[] args)  
  4.     {  
  5.         //定义一个局部变量  
  6.         final String str = "Java";  
  7.         //在内部类里访问局部变量str  
  8.         new Thread(new Runnable()  
  9.         {  
  10.             public void run()  
  11.             {  
  12.                 for (int i = 0; i < 100 ; i++ )  
  13.                 {  
  14.                     //此处将一直可以访问到str局部变量  
  15.                     System.out.println(str + " " + i);  
  16.                     //暂停0.1秒  
  17.                     try  
  18.                     {  
  19.                         Thread.sleep(100);  
  20.                     }  
  21.                     catch (Exception ex)  
  22.                     {  
  23.                         ex.printStackTrace();  
  24.                     }  
  25.                 }  
  26.             }  
  27.         }).start();  
  28.         //执行到此处,main方法结束  
  29.     }  
  30. }  
    因此,无论如何不要导致构造器产生递归调用。也就是说,应该:
        • 尽量不要在定义实例变量时指定实例变量的值为当前类的实例;
        • 尽量不要初始化块中创建当前类的实例;
        • 尽量不要在构造器内调用本构造器创建Java对象.

重载
    Java 的重载解析过程分成以下两个阶段。
         第一阶段JVM 将会选取所有可获得并匹配调用的方法或构造器,
         第二阶段决定到底妥调用哪个方法,此时JVM 会在第一阶段所选取的方法或构造器中再次选取最精确匹配的那一个.
    精确匹配的原则是:当实际调用时传人的实参同时满足多个方法时,如果某个方法的形参要求参数范围越小,那这个方法就越精确。

private方法重写
    对于使用private修饰符修饰的方法,只能在当前类中访问到该方法,子类无法访问父类中定义的private 方法.既然子类无法访问父类的private方法,当然也就无法重写该方法。如果子类中定义了一个与父类private 方法具有相同方法名、相同形参列表、相同返回值类型的方法,依然不是重写,只是子类中重新定义了一个新方法.也就是说,对于访问不到的方法,不能称为重写,只是定义了一个新的方法而已。

包方法
    方法不使用任何访问控制符修饰,表明它是包访问控制权限。它只能被与当前类处于同一个包中的其他类访问,其他包中的子类依然无法访问该方法。这意味着,只有与当前类处于同一个包中的其他类才能访问该方法。

内部类
    内部类也是Java提供的一个常用语法.内部类能提供更好的封装,而且它可以直接访问外部类的private成员,因此在一些特殊场合下更常用。
    非静态内部类必须寄生在外部类的实例中,没有外部类的对象, 就不可能产生非静态内部类的对象.因此,非静态内部类不可能有无参数的构造器,即使系统为非静态内部类提供一个默认的构造器,这个默认的构造器也需要一个外部类形参。虚拟机底层会将出this(代表当前默认的Outer 对象)作为实参传人Inner构造器中。
    由此可见,系统在编译阶段总会为非静态内部类的构造器增加了个参数,非静态内部类的构造器的第一个形参类型总是外部类,因此,调用非静态内部类的构造器时,必须传入一个外部类对象作为参数,否则程序将会引发运行时异常。
    对于非静态内部类而言,由于它本身就是一个非静态的上下文环境,因此非静态内部类不允许拥有静态成员.
    总之,由于非静态内部类必须寄生在外部类的实例之中,程序创建非静态内部类对象的实例,派生非静态内部类的子类时都必须特别小心,否则很容易将人陷阱.
    如果条件允许,推荐多使用静态内部类,而不是非静态内部类。对于静态内部类来说,外部类相当于它的一个包,因此静态内部类的用法就简单多了,限制也少多了,但另一方面,这也给静态内部类增加了一个限制:静态内部类不能访问外部类的非静态成员。

static
    static 是一个常见的修饰符,它只能用于修饰在类里定义的成员: Field、方法、内部类、初始化块、内部枚举类. static的作用就是把类里定义的成员变成静态成员,也就是所谓类成员.
    被static 关键字修饰的成员(Field、方法、内部类、初始化块、内部枚举类)属于类本身,而不是单个的Java 对象.具体到静态方法也是如此,静态方法属于类,而不是属于Java对象.

native方法
    native 方法通常需要借助C语言来完成,即需要使用C语言为Java方法提供实现,其实现步骤如下:
        用javah编译第一步生成的class文件,将产生一个.h文件.
        写一个.cpp文件实现native方法,其中需要包含第上面产生的h文件(.h文件中又包含了JOK带的jni.h文件).
        将第产生的.cpp文件编译成动态链接库文件.
        在Java中用System的loadLibrary()方法或Runtime的loadLibrary()方法产生的动态链接库文件,就可以在Java 程序中调用这个native方法了.
    因此,千万不要过度相信JDK所提供的方法。虽然Java语言本身是跨平台的,但Java的native方法还是要依赖于具体的平台,尤其是JDK 所提供的方法,更是包含了大量的native方法.使用这些方法时,要注意它们在不同平台上可能存在的差异.

第七章:异常捕获
垃圾回收
    垃圾回收机制属于Java 内存管理的一部分,它只是负责回收堆内存中分配出来的内存,至于程序中打开的物理资源, 垃圾回收机制是无能为力的。

finally
    System.exit(0)将停止当前线程和所有其他当场死亡的线程,finally块并不能让已经停止的线程继续执行.
    当System.exit(0)被调用时,虚拟机退出前要执行两项清理工作:
        • 执行系统中注册的所有关闭钩子;
        • 如果程序调用了System.runFinalizerOnExit(true); 那么JVM会对所有还未结束的对象调用Finalizer.
    因此,只要Java虚拟机不退出,不管try 块正常结束,还是遇到异常非正常退出,finally块总会获得执行的机会.
    当Java 程序执行try 块、catch块时遇到了retum 语句,return语句会导致该方法立即结束.系统执行完return语句之后,并不会立即结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有位finally块,方法终止,返回相应的返回值。如果有finally块,系统立即开始执行finally块,只有当finally块执行完成后,系统才会再次跳回来根据retum语句结束方法。如果finally块里使用了return语句来导致方法结束,则finally块已经结束了方法,系统将不会跳回去执行try 块、catch块里的任何代码.
    当程序执行try 块、catch 块时遇到throw语句时.throw语句会导致该方法立即结束,系统执行throw语句时并不会立即抛出异常,而是去寻找该异常处理流程中是否包含finally块.如果没有finally块,程序立即抛出异常,如果有finally块,系统立即开始执行finally 块,只有当finally块执行完成后,系统才会再次跳回来抛出异常。如果finally块里使用retun语句来结束方法,系统将不会跳回去执行try块、catch块去抛出异常。

catch
    每个try 块后可以有多个catch 块,不同的catch块针对不同异常类提供相应的异常处理方式.当发生不同意外情况时,系统会生成不同的异常对象,这样可保证Java 程序能根据该异常对象所属的异常类来决定使用哪个catch块处理该异常。通过在try块后提供多个catch块,可以无需在异常处理块中使用if、switch 判断异常
类型,但依然可以针对不同异常类型提供相应的处理逻辑,从而提供更细致、更有条理的异常处理逻辑.
    通常情况下,如果try块被执行一次,则try块后只有一个catch块会被执行,绝不可能有多个catch块被执行。除非在循环中使用了continue开始下一次循环,下一次循环又重新运行了try块,才可能导致多个catch块被执行.

异常捕捉
    捕捉父类异常的catch 块都应该排在捕捉子类异常的catch块之后(简称为,先处理小异常,再处理大异常),否则将出现编译错误。也就是说,语句的多个catch块应该先捕获子类异常(子类代表的范围较小).后捕获父类异常(父类代表的范围较大),否则编译器会提示编译错误。

Checked与Runtime
    RuntimeException类及其子类的实例被称为Runtime异常,不是RuntimeException类及其子类的异常实例则被称为Checked 异常,只要愿意,程序总可以使用catch(XxxException ex)来捕捉运行时异常。
    根据Java语言规范,如果一个catch子句试图捕获一个类型为XxxException 的checked异常,那么它对应的try子句必须可能抛出XxxException 或其子类的异常,否则编译器将提示该程序具有编译错误,但在所有Checked异常中,Exception是一个异类,无论try块是怎样的代码, catch(Exception ex)总是正确的。
    Runtime异常是一种非常灵活的异常,它无需显式声明抛出,只要程序有需要,即可以在任何有需要的地方使用try...catch块来捕捉Runtime异常.
    无论如何不要在finally块中递归调用可能引起异常的方法,因为这将导致该方法的异常不能被正常抛出,甚至StackOverflowError错误也不能中止程序,只能采用强行结束java.exe 进程的方式来中止程序的运行。
    对于Checked 异常的处理方式有两种:
        • 当前方法明确知道如何处理该异常,程序应该使用try...catch块来捕获该异常,然后在对应的catch块中修复该异常;
        • 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常.
    Java语言规定,子类重写父类方法时,不能声明抛出比父类方法类型更多、范围更大的异常.也就是说,子类重写父类方法时,子类方法只能声明抛出父类方法所声明抛出的异常的子类.
原文地址:https://www.cnblogs.com/yeemi/p/7470126.html