2013年5月27日星期一

【温故而知新】读Java编程规范

前几日被一位初学Java的朋友问到几个Java基础的问题,粗心之下发现我也解错了。
这时候猛地一想回过头读读《Java编程规范》(作者Gosling)。
链接在:http://pan.baidu.com/share/link?shareid=523214&uk=3962180550

这是一部非常好的关于Java学习的教材,我会选择其中可能会有疏漏的地方写下来,也作为一个个人的读书笔记。下面是checklist。

  • Unicode 完成
  • 七种变量
class Point {

    static int numPoints; // numPoints  is a class variable

    int x, y; // x  and  y  are instance variables

    int[] w = new int[10]; // w[0]  is an array component

    int setX(int x) { // x  is a method parameter

        int oldx = this.x; // oldx  is a local variable

        this.x = x;

        return oldx;

    }

}
 这里因为加了static,所以numPoints是类变量;而x,y是需要实例化的,所以是实例变量。
  • 关于final
final指的是变量的引用不改变,而不是引用指向的object不改变,这样导致的结果是引用被继承后,指向的依然是同一个object。这也是为什么被final修饰的引用,在声明时就要赋值;如果声明时不赋值,那么之后就没法赋值了。中文教科书中写final的意思是继承时不可重新赋值,其实只是个表象而已,根本原因在此。

用final修饰的class是不能有subclass的。

用volatile修饰的变量不能同时用final修饰。
  • Gneric Class
泛型是可以传入到inner class的
  • Inner class和Inner interface
Inner class可以是静态的也可以不是,但Inner interface默认是静态的。
Any local variable, formal method para meter or exception handler parameter used but not declared in an inner class must be declared final. Any local vari-able, used but not declared in an inner class must be definitely assignedbefore the body of the inner class.

class Outer {
    int i = 100;
    static void classMethod() {
        final int l = 200;
        class LocalInStaticContext{
            int k = i; // compile-time error
            int m = l; // ok
        }
    }
    void foo() {
        class Local {
            // a local class
            int j = i;
        }
    }
}
Inner class只能访问用final修饰的变量,而且因为是final修饰,便必须要在声名时赋值。
class WithDeepNesting{
    boolean toBe;
    WithDeepNesting(boolean b) {
        toBe = b;
    }
    class Nested {
        boolean theQuestion;
        class DeeplyNested {
            DeeplyNested(){
                theQuestion = toBe || !toBe;
            }
        }
    }
}
Here, every instance of WithDeepNesting.Nested.DeeplyNested has an enclosing instance of class WithDeepNesting.Nested (its immediately enclos-ing instance) and an enclosing instance of class WithDeepNesting (its 2nd lexi-cally enclosing instance).

意即,对于内部类,每次实例化都会带着一个外部类的实例,层层包裹。
  • 接口
一个类可以实现两个具有相同签名方法的接口,但不能实现两个名字一样但返回类型不同方法。比如下面的代码是可以通过编译的:
interface Fish {
    int getNumberOfScales();
}
interface Piano {
    int getNumberOfScales();
}
class Tuna implements Fish, Piano {
    //  You can tune a piano, but can you tuna fish?
    int getNumberOfScales() {
        return 91;
    }
} 
但如下的代码却不能通过编译:
interface Fish {
    int getNumberOfScales();
}
interface StringBass {
    double getNumberOfScales();
}
class Bass implements Fish, StringBass {
    //  This declaration cannot be correct, no matter what type is used.
    public ??? getNumberOfScales() {
        return 91;
    }
} 
 
下面看一个因为package不同而在重写方法中编译不过的例子:
 
package points;
public class Point {
    int x, y;
    public void move(int dx, int dy) {
        x += dx; y += dy;
    }
} 
package points;
public class Point3d extends Point {
    int z;
    public void move(int dx, int dy, int dz) {
        x += dx; y += dy; z += dz;
    }
}
import points.Point3d;
class Point4d extends Point3d {
    int w;
    public void move(int dx, int dy, int dz, int dw) {
        x += dx; y += dy; z += dz; w += dw; // compile-time errors
    }
} 
这时候就会出错,因为Point4d不在package中。更好的做法是:
import points.Point3d;
class Point4d extends Point3d {
    int w;
    public void move(int dx, int dy, int dz, int dw) {
        super.move(dx, dy, dz); w += dw;
    }
} 
从这里也可以看出,默认修饰的权限范围是package level
即,package也是权限控制的一种方式。且子类继承后的权限应该小于等于父类。
  • Volatile
对于以下两个方法:
class Test {
    static int i = 0, j = 0;
    static void one() {
        i++; j++;
    }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
} 
假如有两个线程,一个在不停地调用one,另一个在不停地调用two。那么two就有可能看到i和j不一致的情况。(有可能i比j大,或j比i大)
一种解决方法是给这两个方法加上同步:
class Test {
    static int i = 0, j = 0;
    static synchronized void one() {
        i++; j++;
    }
    static synchronized void two() {
        System.out.println("i=" + i + " j=" + j);
    }
} 
但是这种方法阻止了两个线程同步进行。另一种方法是加上volatile:
class Test {
    static volatile int i = 0, j = 0;
    static void one() {
        i++; j++;
    }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
} 

  • volatile适用的两个场合:修改值的操作是原子的且不依赖原来的值,读的操作大于写的操作
  • 在类变量(static类型)的初始化过程中,不能使用this和super关键字: 因为没有instance

class Test {
    Test() {
        k = 2;
    }
    int j = 1;
    int i = j;
    int k;
} 
上面这样是可以的,尽管Test的初始化中的k是在后面声明的

class Z {
    static int peek() {
        return j;
    }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
} 
以上代码会输出0,是因为当调用peek方法的时候,会返回j的值,可是这个时候j还没有被初始化,所以
class UseBeforeDeclaration {
    static {
        x = 100; // ok - assignment
        int y = x + 1; //  error - read before declaration
        int v = x = 3; //  ok - x at left hand side of assignment
        int z = UseBeforeDeclaration.x * 2;
        //  ok - not accessed via simple name
        Object o = new Object(){
            void foo(){
                x++;
            }
            // ok - occurs in a different class
            {
                x++;
            }
            // ok - occurs in a different class
        }
        ;
    }
    {
        j = 200; // ok - assignment
        j = j + 1; // error - right hand side  reads before declaration
        int k = j = j + 1;
        int n = j = 300; // ok - j at left hand side of assignment
        int h = j++; //  error - read before declaration
        int l = this.j * 3; //  ok - not accessed via simple name
        Object o = new Object(){
            void foo(){
                j++;
            }
            // ok - occurs in a different class
            {
                j = j + 1;
            }
            // ok - occurs in a different class
        }
        ;
    }
    int w = x = 3; //  ok - x at left hand side of assignment
    int p = x; // ok - instance initializers may access static fields
    static int u = (new Object(){
        int bar(){
            return x;
        }
    }
    ).bar();
    //  ok - occurs in a different class
    static int x;
    int m = j = 4; //  ok - j at left hand side of assignment
    int o = (new Object(){
        int bar(){
            return j;
        }
    }
    ).bar();
    //  ok - occurs in a different class
    int j;
} 
这段说明了初始化变量的用法
  • 隐藏变量(包括类变量和实例变量)
class Point {
    int x = 2;
}
class Test extends Point {
    double x = 4.7;
    void printBoth() {
        System.out.println(x + " " + super.x);
    }
    public static void main(String[] args) {
        Test sample = new Test();
        sample.printBoth();
        System.out.println(sample.x + " " +
        ((Point)sample).x);
    }
} 
在上面的代码中,Test继承了Point,并且隐藏了Point中的x变量。于是对于每一个Test实例,都有两个x,一个是int类型,一个是double类型。但是在Test中的x都是自己的double类型,只有显式地用super.x才能指明是Point中的int类型的x。而当强转成了父类Point后,x就成了point中int类型的x。如果把Test中的成员变量的声明删掉,那所有的x就只有Point中的x了。

  • 继承的时候可以从父类或者接口(或者父类的接口)继承相同名字的变量,但是在本类的方法中要显式地指出用的是哪个变量。
interface Frob {
    float v = 2.0f;
}
class SuperTest {
    int v = 3;
}
class Test extends SuperTest implements Frob {
    public static void main(String[] args) {
        new Test().printV();
    }
    void printV() {
        System.out.println(v);
    }
} 
上面的代码会出现问题,因为printV方法不知道v是哪个v。修改成下面的代码,使用SuperTest.v或者Frob.v就可以指明是哪个v。
interface Frob {
    float v = 2.0f;
}
class SuperTest {
    int v = 3;
}
class Test extends SuperTest implements Frob {
    public static void main(String[] args) {
        new Test().printV();
    }
    void printV() {
        System.out.println((super.v + Frob.v)/2);
    }
} 
  • 下面一个例子说明:即使使用继承的具有歧义的同名变量是具有相同的类型和相同的值,编译还是会出错。

interface Color {
    int RED=0, GREEN=1, BLUE=2;
}
interface TrafficLight {
    int RED=0, YELLOW=1, GREEN=2;
}
class Test implements Color, TrafficLight {
    public static void main(String[] args) {
        System.out.println(GREEN); // compile-time error
        System.out.println(RED); // compile-time error
    }
} 
上面的代码中RED变量就是例子。

  • It is a compile-time error for a private method to be declared abstract .
  • It is a compile-time error for a static method to be declared abstract .
  • It is a compile-time error for a final method to be declared abstract .
  • 子类可以重写父类的方法并改成abstract,但是这样的话,子类的子类就不能调用super.abstractMethod(...)了,因为它的父类的这个方法是abstract的。总之在通过super调用的方法不能是abstract的
  • 静态方法中不能调用this,super,以及泛型类型
  • 如果一个class被final修饰,那么相当于它里面的所有方法都加上了final
  • final在处理过程中会被inline处理
  • 被native修饰的方法不能被abstract修饰
  • 方法中如果有throw就可以不用return
  • 实例方法不能重写静态方法, 但隐藏静态变量是可以的.『静态方法与静态变量的不同之处』
  • 同样,静态方法不能隐藏实例方法『但是静态变量可以隐藏实例变量』
  • 如果父类的方法被重写,可以在子类通过super来调用它
  • strictfp对于重写没有影响,也就是non-strictfp的可以重写strictfp的,strictfp也可以重写non-strictfp的
  • 如果重写的方法返回的类型R1不是被重写的方法的返回类型R2的子类型,则警告。『重写后类型变成子类型』
  • 如果被重写的方法是protected,那么重写后应该是protected或者public;如果被重写的方法是public,那么重写后就是public;如果是private,则不可重写
package p1;
public class Outer {
    protected class Inner{
    }
}
package p2;
class SonOfOuter extends p1.Outer {
    void foo() {
        new Inner(); // compile-time access error
    }
} 
  • 上面这个例子会有编译错误,虽然Inner是protected的,意味着SonOfOuter可以访问Inner类,可以继承它,但是:不意味着Inner的构造方法能够被SonOfOuter访问。

  • 控制构造函数的访问权限可以控制构造的范围,甚至防止构造,例如:
class ClassOnly {
    private ClassOnly() {
    }
    static String just = "only the lonely";
} 
以上的代码中,ClassOnly就永远不会给构造。

  • 显式地初始化一个枚举类型,会导致编译错误
  • 枚举类型的clone方法是被final修饰的,这就意味着枚举常量永远不会被clone
  • 序列化过程中的一些特殊过程,保证了反序列化的时候枚举类型永远不会被复制。即使使用反射也是被禁止的。
  • 枚举类型不能被abstract修饰
  • 枚举类型无须定义为final,它被隐式地定义为了final,除非:枚举常量中有class
  • 嵌套的枚举类型被隐式地修饰为了static,显式地把嵌套的枚举类型修饰为static是允许的
enum Color {
    RED, GREEN, BLUE;
    static final Map <String,Color> colorMap =
    new HashMap<String,Color>();
    Color() {
        colorMap.put(toString(), this);
    }
} 
  • 以上代码在会出错,因为Color的构造函数在执行的时候,colorMap这个HashMap还没有被构造起来,会抛出NullPointerException。

  • EnumSet类中有很多静态方法,用来处理枚举类型
  • 枚举类型中可以给常量成员添加一些“行为”,如以下的代码:
import java.util.*;
public enum Operation {
    PLUS {
        double eval(double x, double y) {
            return x + y;
        }
    }
    ,
    MINUS {
        double eval(double x, double y) {
            return x - y;
        }
    }
    ,
    TIMES {
        double eval(double x, double y) {
            return x * y;
        }
    }
    ,
    DIVIDED_BY {
        double eval(double x, double y) {
            return x / y;
        }
    }
    ;
    // Perform the arithmetic operation represented by this constant
    // abstract double eval(double x, double y);
    public static void main(String args[]) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        for (Operation op : Operation.values())
        System.out.println(x + " " + op + " " + y + " = " +
        op.eval(x, y));
    }
} 
上面的代码展示了“加减乘除”的四个操作的四种行为,有点switch-case的味道。

  • 所有的接口被隐式地定义为public
  • 接口定义的成员相当于被public static修饰
  • 接口属于类变量,所以this和super也是不允许出现的
  • Annotation类型不可以是泛型
  • Annotation类型不能继承,默认是继承自annotation.Annotation
方法不能有任何参数
方法也不能有任何类型参数
方法的声明也不能出现throws句型
class Point {
    int x, y;
}
class ColoredPoint extends Point {
    int color;
}
class Test {
    public static void main(String[] args) {
        ColoredPoint[] cpa = new ColoredPoint[10];
        Point[] pa = cpa;
        System.out.println(pa[1] == null);
        try {
            pa[0] = new Point();
        }
        catch (ArrayStoreException e) {
            System.out.println(e);
        }
    }
} 
  • 上面的代码会抛出java.lang.ArrayStoreException异常,是因为pa引用指向的实际上是一个ColoredPoint的数组,这样的话在每次存储的时候java都会检查存储的是不是ColoredPoint类型。

  • 初始化class会要求先初始化父class,但是初始化interface不会要求先初始化父class
  • finalize的过程与gc收集,强引用,弱引用,软引用,幽灵引用有关
  • finalize的过程不能保证顺序,有可能同时进行(循环引用的object)
  • 通过重写finalize方法可以使得上述的object按照顺序进行(从无序变有序)
  • 如果一个class有静态变量或者静态初始化过程,那么它就不应该被reload
  • 在一个object的构造方法完成以前,不要让其他线程有看到这个object的引用的机会。这样可以保证:一个object可以被多个线程正确地看到
  • 如果在构造方法中有对final变量的赋值,那么在构造方法结束以前,这个final变量对于其他线程都是不可见的
  • 通常final修饰的是不可修改的,但是出于历史原因,被final static修饰的System.in, System.out, System.err可以被修改。(通过System.setIn, System.setOut, System.setErr方法来修改)这些域会被编译器作特殊地处理:比如,对于这些域的读操作是“对同步免疫的”,锁或者是volatile读不需要关心从这些域中读到什么值,这些write-protected的域可以被看到发生了改变,那么它们就需要一个同步操作。而write-protected的意思就是只有protected作用域内的代码才能执行写操作。
  • 关于Word Tearing:
public class WordTearing extends Thread {
    static final int LENGTH = 8;
    static final int ITERS = 1000000;
    static byte[] counts = new byte[LENGTH];
    static Thread[] threads = new Thread[LENGTH];
    final int id;
    WordTearing(int i) {
        id = i;
    }
    public void run() {
        byte v = 0;
        for (int i = 0; i &lt; ITERS; i++) {
            byte v2 = counts[id];
            if (v != v2) {
                System.err.println("Word-Tearing found: " +
                "counts[" + id
                + "] = " + v2 + ", should be " + v);
                return;
            }
            v++;
            counts[id] = v;
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i &lt; LENGTH; ++i)
        (threads[i] = new WordTearing(i)).start();
    }
} 
以上的代码说明:数组中的任意成员都会被当成不同的值来处理,即使它们同时被不同的线程读写。代码中的System.err不会输出任何东西。

  • 对于double和long类型的非原子写操作:因为double和long是64个bit的,所以对于double和long的一个写操作由两次组成,每次各写32bit。这样的话就有可能造成多个线程之间读到的值的不一致。所以虚拟机尽量避免分割开64bit的数据。而且鼓励程序员尽量把共享的64bit数据声明为volatile(保证读一致性),或者在使用的时候进行同步操作。
  • 无论是Thread.sleep方法还是Thread.yield方法,都不含同步的语义。也就是说,在调用这两个方法之前,编译器不需要把寄存器中的写cache操作,给flush到共享内存中。以为这下面这段代码永远不会退出while循环:

while (!this.done)
Thread.sleep(1000); 
  • while循环在判断this.done的时候cache了这个变量,在这之后每次读取都会读到cache的值,即使其他线程修改了this.done。但是前提是,this.done是non-volatile的,比如final修饰的。

没有评论:

发表评论