Thinking in Java笔记

本文深入探讨了Java中数据存储的五个关键区域:寄存器、堆栈、堆、常量存储和非RAM存储,详细解释了它们的工作原理、特点及用途。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 存储到什么地方(P22)
            有五个不同的地方可以存储数据:

                寄存器
                    这是最快的存储区,因为它位于处理器内部。但是数量极其有限,所以寄存器根据需求进行分配。你不能直接控制,也不会在程序中感觉到寄存器存在的任何迹象。
                
                堆栈
                    位于通用RAM(随机访问存储器)中,通过堆栈指针可以从处理器那里获得直接支持。堆栈指针若向下移动,则分配新的内存;若向上移动,则释放那些内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时,Java系统必须知道存储在堆栈内所有项的确切生命周期,以便上下移动指针。这一约束限制了程序的灵活性,所以虽然某些Java数据存储于堆栈中——特别是对象引用,但是Java对象并不存储于其中。
                
                
                    一种通用的内存池(也位于RAM区),用于存放所有的Java对象。堆不同于栈的好处是:编译器不需要知道存储的数据在堆里存活多长时间。代价是进行存储分配和清理可能比堆栈需要更多的时间(如果确实可以在Java中像在C++中一样在栈中创建对象)。
                
                常量存储
                    常量值通常直接存放在程序代码内部。有时,在嵌入式系统中,常量本身会和其他部分分隔离开,所以在这种情况下,可以选择将其存放在ROM(只读存储器)中。这种存储区的一个例子是字符串池。所有字面常量字符串和具有字符串值的常量表达式都自动是内存限定的,并且会置于特殊的静态存储区中。
            
                非RAM存储
                    数据完全存活于程序之外,不受程序控制,程序没有运行时也可以存在。其中两个基本的例子是流对象持久化对象。在流对象中,对象转成字节流,通常被发送给另一台机器。在“持久化对象”中,对象被存放于磁盘上。

  • 基本类型(P23)
            对于这些类型,Java采取与C和C++相同的方法。也就是说,不用new来创建变量,而是创建一个并非是引用的“自动”变量。这个变量直接存储“值”,并置于堆栈中,因此更加高效。
            Java要确定每种基本类型所占存储空间的大小。它们的大小并不像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是Java程序比用其他大多数语言编写的程序更具可移植性的原因之一。
            

基本类型

大小

最小值

最大值

包装器类型

boolean

-

-

-

Boolean

char

16-bit

Unicode 0

Unicode 216-1

Character

byte

8 bits

-128

+127

Byte

short

16 bits

-215

+215-1

Short

int

32 bits

-231

+231-1

Integer

long

64 bits

-263

+263-1

Long

float

32 bits

IEEE754

IEEE754

Float

double

64 bits

IEEE754

IEEE754

Double

void

-

-

-

Void


            Java提供了两个用于高精度计算的类:BigIntegerBigDecimal。两个类包含的方法,提供的操作与对基本类型所能执行的操作相似。也就是说,能作用于intfloat的操作,也同样能作用于BigInteger或BigDecimal。只不过必须以方法调用方式取代运算符方式来实现。
            BigInteger支持任意精度整数
            BigDecimal支持任何精度定点数

  • 名字可见性(P28)
            怎样才能区分两个名字以防止两者相互冲突呢?
            C++通过几个关键字引入了名字空间的概念。            
            Java采用了另外一种方法。Java设计者希望程序员反过来使用自己的Internet域名,例如作者的域名是MindView.net,所以作者开发的各种工具类库被命名为net.mindview.utility.foibles。
            
  • 文档注释和标签(P32)
            所有javadoc命令都只能在“/**”注释中出现,和通常一样,注释结束于“*/”。
            注意,javadoc只能为public和protected成员进行文档注释。

  • for语句(P66)
            for(initialization; Boolean-expression; step)
                statement
            initializationstep可以用逗号操作符,例如:
            for (int i = 1, int j = i + 10; i < 5; i++, j = i * 2) {}

  • goto(P70)
            尽管goto仍是Java中的一个保留字,但在语言中并未使用它。然而,Java也能完成一些类似于跳转的操作,这与breakcontinue这两个关键词有关。
            它们可以使用相同的机制:标签,实现类似于goto的功能。
            在Java中,标签起作用的唯一地方刚好是在迭代语句之前
            breakcontinue通常只中断当前循环,但若随同标签一起使用,它们就会中断循环,直到标签所在的地方
            
            例如:
            label1:
            outer-iteration {
                inner-iteration {
                    //...
                    break;    //(1)
                    //...
                    continue;    //(2)
                    //...
                    continue label1;    //(3)
                    //...
                    break label1;    //(4)
                }
            }
            (1),break中断内部迭代,回到外部迭代;
            (2),continue使执行点回内部迭代的起始处;
            (3),continue同时中断内部迭代以及外部迭代,直接转到label1处;随后,它实际上是继续迭代过程,但却从外部迭代开始;
            (4),break也会中断所有迭代,并回到label1处,但并不重新进入迭代。也就是说,它实际是完全终止了两个迭代

  • 一个方法如何知道是被哪个对象调用的(P84)
            例如有一个类Banana,其中有一个方法peel(),如下:
                class Banana { void peel(int i) { /* ... */ } }
        
                public class BananaPeel {
                    public static void main(String[] args) {   
                        Banana a = new Banana(), b = new Banana();
                        a.peel(1);
                        b.peel(2);
                    }
                }
            方法不像域(成员变量)是属于每个对象的,方法是属于一个的,它表示的是一个类能做的行为,而表示的是类中一个对象的状态
            那么方法是如何知道当前是谁调用的呢?
            编译器做了一些幕后工作,它暗自把“所操作对象的引用”作为第一个参数传递给peel()。所以例中两个方法的调用就变成了这样:
                Banana.peel(a, 1);
                Banana.peel(b, 1);
            这是内部表现形式,我们不能这样写,这样也无法通过编译。

  • 清理:终结处理和垃圾回收(P87)
            Java允许在类中定义一个名为finalize()的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
            
            在C++中,对象一定会被销毁(如果程序中没有缺陷的话);而Java里的对象并非总是被垃圾回收。或者换句话说:
                对象可能不被垃圾回收
                垃圾回收并不等于“析构”

            垃圾回收器的思想:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。由此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有“活”的对象
            在这种方式下,Java虚拟机将采用一种自适应的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的Java虚拟机实现。

            有一种做法名为停止——复制(stop-and-copy):先暂停程序的运行(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单、直接地分配新空间了。
            对于这种所谓的“复制式回收器”而言,效率会降低,这有两个原因。首先,得有两个堆,然后得在这两个分离的堆之间来回捣腾,从而得维护比实际需要多一倍的空间。
            第二个问题在于复制。程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。为了避免这种情形,一些Java虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种工作模式(这就是“自适应”)。
            这种模式称为标记——清扫(mark-and-sweep)。对一般用途而言,“标记——清扫”方式速度相当慢。“标记——清扫”所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。完成后,剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就得重新整理剩下的对象。

  • 类的初始化(P94)
            初始化的顺序是先静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“非静态”对象。
            先完成静态变量的初始化,然后完成变量的初始化。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。

            总结一下对象的创建过程,假设有个名为Dog的类:
                即使没有显示地使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时(构造器可以看成是静态方法),或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
                然后载入Dog.class,有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载的时候进行一次
                当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。
                这块存储空间会被清零,这就自动地将Dog对象中的所有基本类型数据都设置成了默认值,而引用则被设置成了null。
                执行所有出现于字段定义处的初始化动作
                执行构造器

            显示的静态初始化
                public class Spoon {
                    static int i;
                    static {
                        i = 47;
                    }
                }
            与其他静态初始化动作一样,这段代码仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于那个类的静态数据成员时。

            非静态实例初始化
                public class Spoon {
                    A a1;
                    A a2;

                    {
                        a1 = new A();
                        a2 = new A();
                    }
                }
            实例初始化是在构造器之前执行的。

  • 可变参数列表(P102)
            class A{}

            public class VarArgs {
                static void printArray(Object[] args) {
                for (Object obj : args) {
                    System.out.print(obj + " ");
                }
                System.out.println();
            }

            static void printVarArray(Object... args) {
                for (Object obj : args) {
                    System.out.print(obj + " ");
                }
                System.out.println();
            }

            public static void main(String[] args) {
                System.out.println("print array");
                printArray(new Object[] {new Integer(47), new Float(3.14), new Double(11.11)});
                printArray(new Object[] {"one", "two", "three"});
                printArray(new Object[] {new A(), new A(), new A()});
//                printArray(new Integer(31));

                System.out.println("\n print var array");
                printArray(new Object[] {new Integer(47), new Float(3.14), new Double(11.11)});
                printArray(new Object[] {"one", "two", "three"});
                printArray(new Object[] {new A(), new A(), new A()});
                printVarArray(new Integer(31));
            }
            
            可以看到在printArray(Object[] args)方法中,传入的参数必须是一个数组;而在printVarArray(Object... args)方法中,args会被看做数组处理传入的参数并不一定要是数组,只要类型相符即可。
            我们应该总是只在重载方法的一个版本上使用可变参数列表,或者压根就不使用它。

  • 方法调用绑定(P150)
            将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。例如,C只有一种方法调用,就是前期绑定。
            后期绑定的含义就是在运行时根据对象的类型进行绑定。后期绑定也叫做动态绑定运行时绑定编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体。

            Java中除了static方法final方法private方法属于final方法)之外其他所有的方法都是后期绑定
            
            将一个方法声明为final可以防止其他人覆盖该方法。更重要的一点或许是:这样做可以有效地“关闭”动态绑定。这样,编译器就可以为final方法调用生成更有效的代码。然而,大多数情况下,这样做对程序的整体性能不会有什么改观。所以,最好根据设计来决定是否使用final,而不是出于试图提高整体性能的目的来使用final。

  • 接口(P172)
            接口也可以包含,但是这些域隐式地是staticfinal的。

  • 策略设计模式(P175)
            创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略设计模式。这类方法包含所要执行的算法中固定不变的部分,而“策略”包含变化的部分
            策略就是传递进去的参数对象,它包含要执行的代码。

  • 适配器设计模式(P177)
            无法修改想要使用的类。可以使用适配器模式适配器中的代码将接受你所拥有的接口,并产生需要的接口

  • 关于实现接口的方法(P177)
            返回类型可以不相同,只要是子类就行。例子更能说明问题:
                public class A{}

                public class B extends A {}

                public class C extends B {}

                public interface AA {
                    public A n(Object m);   
                }

                以下三个类都是合法的:
                public class BA implements AA {
                    public B n(Object m) {
                        return null;
                    }
                }

                public class CA implements AA {
                    public n(Object m) {
                        return null;
                    }
                }
                
                public class OA implements AA {
                    public A n(Object m) {
                        return null;
                    }
                }

  • 接口的所有成员(P194)
            接口所有成员自动被设置为public的。

  • 内部类(P195)
            可以在一个方法里面或者在任意的作用域内定义内部类。这么做有两个理由:
                你实现了某个接口,于是可以创建并返回对其的引用;
                你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。

            如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的。这种称为局部类

            匿名内部类正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。
            
            如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static。这通常称为嵌套类

  • List(P256)
            ArrayList:它长于随机访问元素,但是在List的中间插入移除元素时较慢
            LinkedList:它通过代价较低的List中间进行的插入删除操作,提供了优化顺序访问。LinkedList在随机访问方面相对比较,但是它的特性集较ArrayList更

  • 迭代器(P259)
            迭代器是一个对象,它的工作遍历并选择序列中的对象,而客户端程序员不必知道或关心序列底层的结构。此外,迭代器通常被称为轻量级对象创建它的代价小
            Iterator能够将遍历序列的操作序列底层的结构分离。迭代器统一了对容器的访问方式
            ListIterator是一个更加强大的Iterator的子类型,它只能用于各种List类的访问。尽管Iterator只能向前移动,但是ListIterator可以双向移动。它还可以产生相对于迭代器在列表中指向的当前位置的前一个后一个元素的索引,并且可以使用set()方法替换它访问过的最后一个元素。

  • 异常说明(P256)
            从RuntimeException继承的异常,它们可以在没有异常说明的情况下被抛出

  • finally用来做什么(P265)
            当要把除内存之外的资源恢复到它们的初始状态时,就要用到finally子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以使外部世界的某个开关。

  • 不可变String
            String对象是不可变的。String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象,以包含修改后的字符串内容。而最初的String对象则丝毫未动。
            如果程序中有多个String对象,都包含相同的字符串序列,那么这些String对象都映射到同一块内存区域。

  • 格式化正则表达式详见P291和P295
        
  • Class对象(P314)
            类型信息在运行时是如何表示的。这项工作是由称为Class对象特殊对象完成的,它包含了与类有关的信息。事实上,Class对象就是用来创建类的所有的”常规“对象的。
            每个类都有一个Class对象。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。为了生成这个类的对象,运行这个程序的Java虚拟机(JVM)将使用被称为”类加载器“的子系统。
            记住是每个类只有一个Class对象,而不是每个实例都有一个Class对象
            
            获得一个类的Class对象有好几种方法,假设有类A位于包B中,并且有类A的一个实例a,那么,我们有以下几种方法获取Class对象(我不知道完整不):
                Class c = Class.forName("B.A");
                
                Class c = A.class;
                
                Class c = a.getClass();
            其中,第二种方法中的A.class叫做类字面常量
            类字面常量不仅可以应用于普通的类,也可以应用于接口数组以及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段TYPE。TYPE字段是一个应用,指向对象的基本数据类型的Class对象,如下所示:
            boolean.class            =              Boolean.TYPE
            char.class                   =            Character.TYPE
            byte.class                   =                     Byte.TYPE
            short.class                  =                   Short.TYPE
            int.class                      =                Integer.TYPE
            long.class                   =                    Long.TYPE
            float.class                   =                  FLOAT.TYPE
            double.class               =               DOUBLE.TYPE 
            void.class                    =                    VOID.TYPE

            Class的newInstance()方法是实现”虚拟构造器“的一种途径,虚拟构造器允许你声明:”我不知道你的确切类型,但是无论如何要正确地创建你自己。“另外,使用newInstance()来创建的类,必须带有默认的构造器

  • 使用类的三步(P319)
            加载:
                这是由类加载器执行的。该步骤将查找字节码,并从这些字节码中创建一个Class对象。
            链接:
                在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必须的话,将解析这个类创建的对其他类的所有引用。
            初始化
                如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
            
            当使用”.class“来创建对Class对象的引用时,不会自动地初始化该Class对象。
            例子:
                class Initable {
                    static final int staticFinal = 47;
                    static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
                    static { 
                        System.out.println("Initializing Initable");
                    }
                }

                public class ClassInitialization {
                    public static Random rand = new Random(47);
                    public static void main(String[] args) throws Exception {
                        Class initable = Initable.class;
                        System.out.println("After creating Initable ref");
                        
                        System.out.println(Initable.staticFinal);
                        
                        System.out.println(Initable.staticFinal2);
                    }
                }
            结果:
                After creating Initable ref
                47
                Initializing Initable
                258
            
            初始化有效地实现了尽可能的”惰性“。仅使用.class语法来获得对类的引用不会引发初始化。但是,Class.forName()会引发初始化
            如果一个static final值是”编译期常量“,那么这个值不需要对类进行初始化就可以被读取,像staticFinal一样。而staticFinal2就不是一个编译期常量。
            
  • instanceof与Class的等价性(P334)
            instanceof保持了类型的概念,它指的是“你是这个类吗,或者你是这个类的派生类吗?”而如果用==比较实际的Class对象,就没有考虑继承——它或者是这个确切的类型,或者不是。

  • 反射(P347)
            通过使用反射,我们可以到达并调用所有方法,甚至是private方法!
            如果知道方法名,你就可以在其Method对象上调用setAccessible(true)
            四个例子,以前从未了解过的特性!!!!
            
            public interface A {
                void f();
            }
            
            例子1:包访问权限控制
            class C implements A {
                public void f() {
                    System.out.println("public C.f()");
                }

                public void g() {
                    System.out.println("public C.g()");
                }

                void u() {
                    System.out.println("package C.u()");
                }

                protected void v() {
                    System.out.println("protect C.v()");
                }

                private void w() {
                    System.out.println("private C.w()");
                }
            }

            public class HiddenC {
                public static A makeA() {
                    return new C();
                }
            }

            import java.lang.reflect.Method;
            public class HiddenImplementation {
                public static void main(String[] args) throws Exception {
                    A a = HiddenC.makeA();
                    a.f();
                    System.out.println(a.getClass().getName());
                    callHiddenMethod(a, "g");
                    callHiddenMethod(a, "u");
                    callHiddenMethod(a, "v");
                    callHiddenMethod(a, "w");
                }

                static void callHiddenMethod(Object a, String methodName) throws Exception {
                    Method g = a.getClass().getDeclaredMethod(methodName);
                    g.setAccessible(true);
                    g.invoke(a);
                }
            }
            输出结果:
                public C.f()
                typeinfo.C
                public C.g()
                package C.u()
                protect C.v()
                private C.w()

            例子2:私有内部类
            class InnerA {
                private static class C implements A {

                    @Override
                    public void f() {
                        System.out.println("public C.f()");
                    }

                    public void g() {
                        System.out.println("public C.g()");
                    }

                    void u() {
                        System.out.println("package C.u()");
                    }

                    protected void v() {
                        System.out.println("protected C.v()");
                    }

                    private void w() {
                        System.out.println("private C.w()");
                    }
                }

                public static A makeA() {
                    return new C();
                }
            }

            public class InnerImplementation {
                public static void main(String[] args) throws Exception {
                    A a = InnerA.makeA();
                    a.f();
                    System.out.println(a.getClass().getName());

                    HiddenImplementation.callHiddenMethod(a, "g");
                    HiddenImplementation.callHiddenMethod(a, "u");
                    HiddenImplementation.callHiddenMethod(a, "v");
                    HiddenImplementation.callHiddenMethod(a, "w");
                }
            }
            输出结果:
                public C.f()
                typeinfo.InnerA$C
                public C.g()
                package C.u()
                protected C.v()
                private C.w()
            
            例子3:匿名类
            class AnonymousA {
                public static A makeA() {
                    return new A() {
                        @Override
                        public void f() {
                            System.out.println("public C.f()");
                        }

                        public void g() {
                            System.out.println("public C.g()");
                        }

                        void u() {
                            System.out.println("package C.u()");
                        }

                        protected void v() {
                            System.out.println("protected C.v()");
                        }

                        private void w() {
                            System.out.println("private C.w()");
                        }
                    };
                }
            }

            public class AnonymousImplementation {
                public static void main(String[] args) throws Exception {
                    A a = AnonymousA.makeA();
                    a.f();
                    System.out.println(a.getClass().getName());

                    HiddenImplementation.callHiddenMethod(a, "g");
                    HiddenImplementation.callHiddenMethod(a, "u");
                    HiddenImplementation.callHiddenMethod(a, "v");
                    HiddenImplementation.callHiddenMethod(a, "w");
                }
            }
            输出结果:
                public C.f()
                typeinfo.AnonymousA$1
                public C.g()
                package C.u()
                protected C.v()
                private C.w()
            
            例子4:获取private域,并修改(final修饰符的域无法修改)
            class WithPrivateFinalField {
                private int i = 1;
                private final String s = "I'm totally safe";
                private String s2 = "Am I safe?";

                public String toString() {
                    return "i = " + i + ", " + s + ", " + s2;
                }
            }

            public class ModifyingPrivateFields {
                public static void main(String[] args) throws Exception {
                    WithPrivateFinalField pf = new WithPrivateFinalField();
                    System.out.println(pf);

                    Field f = pf.getClass().getDeclaredField("i");
                    f.setAccessible(true);
                    System.out.println("f.getInt(pf): " + f.getInt(pf));

                    f.setInt(pf, 47);
                    System.out.println(pf);

                    f = pf.getClass().getDeclaredField("s");
                    f.setAccessible(true);
                    System.out.println("f.get(pf): " + f.get(pf));

                    f.set(pf, "No, you're not!");
                    System.out.println(pf);

                    f = pf.getClass().getDeclaredField("s2");
                    f.setAccessible(true);
                    System.out.println("f.get(pf): " + f.get(pf));

                    f.set(pf, "No, you're not!");
                    System.out.println(pf);
                }
            }
            输出结果:
                i = 1, I'm totally safe, Am I safe?
                f.getInt(pf): 1
                i = 47, I'm totally safe, Am I safe?
                f.get(pf): I'm totally safe
                i = 47, I'm totally safe, Am I safe?
                f.get(pf): Am I safe?
                i = 47, I'm totally safe, No, you're not!

  • 泛型(P352)
            泛型实现了参数化类型的概念。
            “泛型”这个术语的意思是:“适用于许多许多的类型”。

            有许多原因促成了泛型的出现,而最引人注目的一个原因,就是为了创造容器类。容器类算得上是最具重用性的类库之一。

            显示的类型说明
                要显示地指明类型,必须在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。如果是在定义该方法的类的内部,必须在点操作符之前使用this关键字,如果是使用static方法,必须在点操作符之前加上类名。
            例如:
                static void f(Mpa<Person, List<Pet>> petPeople) {};
                f(New.<Person, List<Pet>>map());
                f(this.<Person, List<Pet>>map());
                f(A.<Person, List<Pet>>map());
            
            补充:
                ? extends A
                ? super A

  • Arrays.fill(P442)
            使用这个方法可以填充整个数组,但是它使用的是同一个值,如果是对象,那么它使用的就是同一个引用的拷贝。也就是说,对数组中一个对象进行修改,其他对象也会相应改变。

  • HashMap、HashSet等的查询问题与hashCode()方法(P489)
            当我们使用自己的类作为HashMap的键(Key)时,如果我们没有重载hashCode()equals()方法,我们将查询不到正确的结果
            这是因为,自己的类会默认继承基类Object的hashCode()方法生成散列码(hash code),而它默认是使用对象的地址计算散列码,因此实际含义相同的两个实例会产生不同的散列码。
            编写恰当的hashCode()方法后,我们仍然需要覆盖equals()方法。
            这是因为,不同的实例有可能会产生相同的散列码,因此当两者散列码相同时,程序还会调用equals()方法比较两个实例是否真正相同(这一点在Head First Java的附录中有详细说明)。默认基类Object的equals()方法也只是比较对象的地址,因此,如果要使用自己的类作为HashMap等的键,必须同时重载hashCode()和equals()方法

            正确的equals()方法必须满足下列5个条件:
            1.自反性。对任意x,x.equals(x)一定返回true。
            2.对称性。对任意x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。
            3.传递性。对任意x、y、z,如果有x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true。
            4.一致性。对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致,要么一直是true,要么一直是false。
            5.对任何不是null的x,x.equals(null)一定返回false。

  • HashMap的性能因子(P511)
            容量:表中的桶位数。
            初始容量:表在创建时所拥有的桶位数。HashMap和HashSet都具有允许你指定初始容量的构造器。
            尺寸:表中当前存储的项数。
            负载因子:尺寸/容量。空表的负载因子是0,而半满表的是0.5,依此类推。
            
            HashMap使用的默认负载因子是0.75,只有当表达到四分之三满时,才进行再散列。

  • InputStream、OutputStream、Reader、Writer(P534)
            具体参见书本534页到538页的几个表格。

            设计ReaderWriter继承层次结构主要是为了国际化老的I/O流继承层次结构仅支持8位字节流,不能很好地处理16位的Unicode字符。由于Unicode用于字符国际化(Java本身的char也是16位的Unicode),所以添加Reader和Writer继承层次结构就是为了在所有的I/O操作中都支持Unicode。
            几乎所有原始的Java I/O流类都有相应的Reader和Writer类来提供天然的Unicode操作。然而在某些场合,面向字节的InputStream和OutputStream才是正确的解决方案;特别是,java.util.zip类库就是面向字节的而不是面向字符的。因此,最明智的做法是尽量尝试使用Reader和Writer,一旦程序代码无法成功编译,我们就会发现自己不得不使用面向字节的类库。


  • 标准I/O(P548)
            Java提供了System.in、System.out和System.err。其中System.out和System.err已经被包装成了PrintStream,但System.in却是一个没有被包装过的未经加工的InputStream。这意味着我们可以立即使用System.out和System.err,但是在读取System.in之前必须对其进行包装。


  • 新I/O:提高读写速度,对于大数据十分必要!(P551)

            关键词:FileChannel、FileInputStream、FileOutputStream、RandomAccessFile。

            以前从未了解过的特性!!!!


  • ByteBuffer:缓冲区(P560)
            补充一点:
                从MappedByteBuffer(内存映射)到ByteBuffer的一个方法:
                    假设有MappedByteBuffer变量mbb,ByteBuffer变量buff。
                    首先要创建缓冲区,需要指定起点和终点:
                        mbb.limit(end);                    //设置终点
                        mbb.position(start);            //定位起点
                    然后用slice()函数,把这一部分切割为可以修改的缓冲区并交给buff:
                        buff = mbb.slice();
                    
  • 内存映射文件(P563)
            内存映射文件允许我们创建和修改哪些因为太大而不能放入内存的文件。有了内存映射文件,我们就可以假定整个文件都放在内存中,而且可以完全把它当作非常大的数组来访问。
            
            举例说明:
                public class LargeMappedFiles {
                    static int length = 0 * 8FFFFFF; // 128MB
                    
                    public static void main(String[] args) throws Exception {
                        MappedByteBuffer out = new RandomAccessFile("test.dat", "rw").getChannel().map(FileChannel.MapMode.READ_WRITE, 0, length);
                        for (int i = 0; i < length; i++)
                            out.put((byte) 'x');
                        System.out.print("Finished writing");
                        for (int i = length / 2; i < length / 2 + 6; i++)
                            System.out.println((char) out.get(i));
                    }
                }
            MappedByteBuffer是一种特殊类型的直接缓冲器,我们必须制定映射文件的初始位置和映射区域的长度,这意味着我们可以映射某个大文件的较小的部分。
            
            补充:用ByteBuffer和MappedByteBuffer真的快很多很多。具体性能检测在P564

  • 文件加锁(P566)
            JDK 1.4引入了文件加锁机制,它允许我们同步访问某个作为共享资源的文件。不过,竞争同一个文件的两个线程可能在不同的Java虚拟机上;或者一个是Java线程,另一个是操作系统中其他的本地线程。文件锁对其他的操作系统进程是可见的,因为Java的文件加锁直接映射到了本地操作系统的加锁工具
            举例说明:
                public class FileLocking {
                    public static void main(String[] args) throws Exception {
                        FileOutputStream fos = new FileOutputStream("file.txt");
                        FileLock fl = fos.getChannel.tryLock();
                        if (fl != null) {
                            System.out.println("Locked File");
                            TimeUnit.MILLISECONDS.sleep(100);
                            fl.release();
                            System.out.println("Released Lock");
                        }
                        fos.close();
                    }
                }
            通过对FileChannel调用tryLock()lock(),就可以获得整个文件的FileLock。
            注意,SocketChannelDatagramChannelServerSocketChannel不需要加锁,因为它们是从单进程实体继承而来;我们通常不在两个进程之间共享网络socket。
            
            tryLock()非阻塞式的,它设法获取锁,但是如果不能获取(当其他一些进程已经持有相同的锁,并且不共享时),它将直接从方法调用返回。(参考:Computer's Tips)
            lock()则是阻塞式的,它要阻塞线程直至锁可以获得,或调用lock()的线程中断,或调用lock的通道关闭。
            
            使用FileLock.release()可以释放锁。

            补充,也可以用带参数的方法文件的一部分上锁:
                tryLock(long position, long size, boolean shared)
            或者
                lock(long position, long size, boolean shared)
            其中,加锁的区域由size和position决定。第三个参数指定是否是共享锁
            但是,具有固定尺寸的锁不随文件尺寸的变化而变化。如果你获得了某一区域(从position到position+size)上的锁,当文件增大超出position+size时,那么在position+size之外的部分不会被锁定。无参数的加锁方法会对整个文件进行加锁,甚至文件变大后也是如此。

  • 压缩(P568)
            Java I/O类库中的类支持读写压缩格式的数据流。你可以用它们对其他的I/O类进行封装,以提供压缩功能
            这些类不是从Reader和Writer类派生而来的,而是属于InputStream和OutputStream继承层次结构的一部分。这么做是因为压缩类库是按字节方式而不是字符方式处理的。不过有时我们可能会被迫要混合使用两种类型的数据流(注意我们可以使用InputStreamReaderOutputStreamWriter在两种类型间方便地进行转换)。
            

压缩类

功能

CheckedInputStream

GetCheckSum()为任何InputStream产生校验和(不仅是解压缩)

CheckedOutputStream

GetCheckSum()为任何OutputStream产生校验和(不仅是压缩)

DeflaterOutputStream

压缩类的基类

ZipOutputStream

一个DeflaterOutputStream,用于将数据压缩成Zip文件格式

GZIPOutputStream

一个DeflaterOutputStream,用于将数据压缩成GZIP文件格式

InflaterInputStream

解压缩类的基类

ZipInputStream

一个InflaterInputStream,用于解压缩Zip文件格式的数据

GZIPInputStream

一个InflaterInputStream,用于解压缩GZIP文件格式的数据

                单个文件压缩与解压缩:
                    BufferedReader in = new BufferedReader(new FileReader(filename));
                    BufferedOutputStream out = new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream("test.gz")));
                    System.out.println("Writing file");
                    int c;
                    while ((c = in.read()) != -1) {
                        out.write(c);
                    }
                    out.close();
                    in.close();
                就把文件filename中的内容读出来并压缩写到了test.gz中。

                BufferedReader in2 = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream("test.gz"))));    //由于使用的是BufferedReader,而GZIPInputStream是一个字节流,所以需要使用InputStreamReader字节流字符流之间进行转换
                String s;
                while ((s = in2.readLine()) != null) {
                    System.out.println(s);
                }
                in2.close();
                就把压缩文件test.gz中的内容解压并输出到了屏幕。
                在本例中也可以用ZipInputStream和ZipOutputStream替代GZIPInputStream和GZIPOutputStream。

                支持Zip格式的Java库更加全面。利用该库可以方便地保存多个文件,它甚至有一个独立的类,使得读取Zip文件更加方便。
                可以用Checksum类计算和校验文件的校验和。一共有两种Checksum类型:Adler32快一些)和CRC32慢一些,但更准确)。
                利用Zip进行多文件保存:
                    FileOutputStream f = new FileOutputStream("test.zip");
                    CheckedOutputStream csum = new CheckedOutputStream(f, new Adler32());
                    ZipOutputStream zos = new ZipOutputStream(csum);
                    BufferedOutputStream out = new BufferedOutputStream(zos);
                    zos.setComment("A test of Java Zipping");

                    for (String filename : filenames) {
                        System.out.println("Writing file " + filename);
                        BufferedReader in = new BufferedReader(new FileReader(filename));
                        zos.putNextEntry(new ZipEntry(filename));
                        int c;
                        while ((c = in.read()) != -1) {
                            out.write(c);
                        }
                        in.close();
                        out.flush();
                    }
                    out.close();
                程序逐行分析:
                    “test.zip”是压缩之后的文件名称,
                    使用Adler32方式计算校验和,
                    压缩输出流ZipOutputStream连接Checksum输出流CheckedOutputStream
                    包装到BufferedOutputStream,
                    设置注释评论,
                    对每一个文件,
                        进行包装输出,值得一提的是:
                            对于每一个要加入压缩档案的文件,都必须调用putNextEntry(),并将其传递给一个ZipEntry对象。
                            ZipEntry对象包含了一个功能很广泛的接口,允许你获取和设置Zip文件内该特定项上所有可利用的数据:名字、压缩的和未压缩的文件大小、日期、CRC校验和、额外字段数据、注释、压缩方法以及它是否是一个目录入口等等。

                解压缩:
                    System.out.println("Reading file");
                    FileInputStream fi = new FileInputStream("test.zip");
                    CheckedInputStream csumi = new CheckedInputStream(fi, new Adler32());
                    ZipInputStream in2 = new ZipInputStream(csumi);
                    BufferedInputStream bis = new BufferedInputStream(in2);
                    ZipEntry ze;
                    while ((ze = in2.getNextEntry()) != null) {
                        System.out.println("Reading file " + ze);
                        int x;
                        while ((x = bis.read()) != -1) {
                            System.out.write(x);
                        }
                    }
                程序分析:
                    其实就是一个与压缩相反的过程。得注意的是,要用ZipEntry来将压缩文档中的文件一个一个还原。
            
                解压缩的另外一种方法:
                    ZipFile zf = new ZipFile("test.zip");
                    Enumeration e = zf.entries();
                    while (e.hasMoreElements()) {
                        ZipEntry ze2 = (ZipEntry)e.nextElement();
                        System.out.println("Reading File: " + ze2);
                        // ... and extract the data as before
                    }
                程序分析:
                    用ZipFile读取文件,可以方便地获得ZipEntry的集合(一个枚举类型的变量)。

  • Java档案文件——JAR(P570)
            命令格式:
                jar [options] destination [manifest] inputfile(s)
            其中options只是一个字母集合(不必输入任何“-”(Linux需要)或其他任何标识符)。以下这些选项字符在Unix和Linux系统中tar文件中也具有相同的意义。具体意义如下:

c

创建一个新的或者空的压缩文档

t

列出目录表

x

解压所有文件

x file

解压该文件

f

如果没有这个选项,jar假设所有的输入都来自于标准输入;或者在创建一个文件时,输出对象假设为标准输出

m

表示第一个参数将是用户自建的清单文件的名字

v

产生详细输出,描述jar所做的工作

O

只存储文件,不压缩文件(用来创建一个可放在类路径中的JAR文件)

M

不自动创建文件清单

            举例:
                jar cf myJarFile.jar *.class
                创建了一个名为mJarFile.jar的JAR文件,该文件包含了当前目录中的所有类文件,i及自动产生的清单文件。
                
                jar cmf myJarFile.jar myManifestFile.mf *.class
                与前例相似,但添加了一个名为myManifestFile.mf的用户自建清单文件。
                
                jar tf myJarFile.jar
                产生myJarFile.jar内所有文件的一个目录表。
                
                jar tvf myJarFile.jar
                与上例相似,只是会提供myJarFile.jar中的文件的更纤细的信息。
                
                jar cvf myApp.jar audio classes image
                假定audio、classes和image是子目录,下面的命令将所有子目录合并到文件myApp.jar中,其中也包括了“v”标识,当jar程序运行时,该标志可以提供更详细的信息。
                
  • 对象序列化(P572)
            Java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。
            
            对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能够追踪对象内包含的所有引用,并保存那些对象;接着又能对对象内包含的每个这样的引用进行追踪;依此类推。这种情况有时被称为“对象网”,单个对象可与之建立连接,而且它还包含了对象的引用数组以及成员对象。

            注意在对一个Serializable对象进行还原的过程中,没有调用任何构造器,包括默认的构造器。整个对象都是通过从InputStream中取得数据恢复过来的。

            Externalizable接口
                Externalizable接口继承了Serializable接口,同时增添了两个方法:writeExternal()readExternal()。这两个方法会在序列化反序列化还原的过程中被自动调用,以便执行一些特殊操作。
                实现了Externalizable接口的类,在反序列化还原的过程中会调用默认构造函数所以如果该类有显示的构造函数,必须写出一个public的默认构造函数才行,不然会产生Exception。

                Externalizable的工作流程(我个人的推测):
                    序列化:
                        序列化的过程中会先调用writeExternal()方法,然后再把类以及实例的信息写入;
                    解序列化:
                        首先调用实例的构造函数,然后调用readExternal()方法。
                    
            Externalizable不同于Serializable的地方在于,它不会保存字段的信息,需要在writeExternal()中手段选择保留(写入)哪些信息,在readExternal()中(实例已经调用了构造函数,构造好了)读入字段信息。
            Serializable是通过关键字transient来控制哪些信息是瞬时的(不保存)。
            另外,Serializable没有调用构造函数,Externalizable需要调用默认的构造函数(必须是public才行)。

            考虑问题:如果我们将两个对象——它们都具有指向第三个对象的引用——进行序列化,会发生什么情况?当我们从它们的序列化状态恢复这两个对象时,第三个对象只会出现一次吗?如果将这两个序列化成独立的文件,然后再代码的不同部分对它们进行反序列化还原,又会怎样呢?
            只要将任何对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,并且没有任何意外重复复制出的对象。否则就会有重复复制的对象(本来应该是在同一个内存地址的,而现在两个内容相同的对象处在不同的内存地址)。
            如果我们想保存系统状态,最安全的做法是将其作为“原子”操作进行序列化。如果我们序列化了某些东西,再去做其他一些工作,再来序列化更多的东西,如此等等,那么将无法安全地保存系统状态。取而代之的是,将构成系统状态的所有对象置入单一容器内,并在一个操作中将该容器直接写出。然后同样只需要一次方法调用,既可以将其恢复。

            注意
                static数据根本没有被序列化,只会初始化。
                因此假如想序列化static值,必须自己动手去实现

  • Sleep(P579)
            TimeUnit.SECONDS.sleep(1);

  • XML(P586)

  • 静态import(P591)
            import static ***,叫做静态导入,之后可以不用类名访问类的静态方法
            例如:
                import static java.lang.System.*;
                
                public class A {
                    public static void main(String[] args) {
                        out.println("Hello World");
                    }
                }
            对于枚举(enum)而言,就可以直接访问enum实例的标识符。
            例如:
                public enum Spiciness {
                    NOT, MILD, MEDIUM, HOT, FLAMING
                }

                import static enumerated.Spiciness.*;                
                public class Burrito {
                    public Burrito(Spiciness degree) {}
                    public static void main(String[] args) {
                        System.out.println(new Burrito(NOT));
                    }
                }

  • 枚举Enum(P592)
            枚举可以当做是普通的类,enum后面跟的是枚举类型,之后是实例定义
            enum不能继承,可以添加方法。如果打算定义自己的方法,那么必须在enum实例序列的最后添加一个分号
            enum中的构造器与普通的类没有区别,因为除了有少许限制之外,enum就是一个普通的类。

            public enum OzWitch {
                WEST("Miss Gulch, aka the Wicked Witch of the West"),
                NORTH("Glinda, the Good Witch of the North"),
                EAST("Wicked Witch of the East, wearer of the Ruby Slippers, crushed by Dorothy's house"),
                SOUTH("Good by inference, but missing");

                private String description;

                private OzWitch(String description) {
                    this.description = description;
                }

                public String getDescription() {
                    return description;
                }

                public static void main(String[] args) {
                    for (OzWitch witch : OzWitch.values()) {
                        System.out.println(witch + ": " + witch.getDescription());
                    }
                }
        }
        注意:
            构造的实例必须和构造函数的定义一致!
            
  • 包装枚举的方法(P597)
                对于enum而言,实现接口是使其子类化的唯一办法
                public interface Food {
                    enum Appetizer implements Food {
                        SALAD, SOUP, SPRING_ROLLS
                    }

                    enum MainCourse implements Food {
                        LASAGNE, BURRITO, PAD_THAI, LENTILS, HUMMOUS, VINDALOO
                    }

                    enum Dessert implements Food {
                        TIRAMISU, GELATO, BLACK_FOREST_CAKE, FRUIT, CREME_CARAMEL
                    }

                    enum Coffee implements Food {
                        BLACK_COFFEE, DECAF_COFFEE, ESPRESSO, LATTE, CAPPUCCINO, TEA, HERB_TEA;
                    }
                }

  • EnumSetEnumMap(P601)
            针对枚举类型的Set和Map。

            EnumSet顾名思义,它是以一个枚举类型为基础的集合。由于不能从enum中删除或添加元素,因此开发了EnumSet来进行辅助。
            EnumSet以一个具体的enum类型为基础,在这个Set中所有的元素都只能是该Enum类型中的实例EnumSet的设计充分考虑到了速度因素,因为它必须与非常高效的bit标志相竞争(其操作与HashSet相比,非常地快)。就其内部而言,它(可能)是将一个long值作为比特向量。使用EnumSet的优点是,它在说明一个二进制位是否存在时,具有更好的表达能力,并且无需担心性能
            EnumSet的基础是long,一个long值有64位,而一个enum实例只需一位bit表示其是否存在。当实例超过64个的时候,作者猜测,EnumSet会在必要的时候增加一个long
            举例:
            public enum AlarmPoints {
                STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3, OFFICE4, BATHROOM, UTILITY, KITCHEN
            }

            public class EnumSets {
                public static void main(String[] args) {
                    EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class);        //以AlarmPoints枚举类型为基础建立一个空的EnumSet
                    points.add(BATHROOM);
                    System.out.println(points);
                    points.addAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
                    System.out.println(points);
                    points = EnumSet.allOf(AlarmPoints.class);
                    points.removeAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
                    System.out.println(points);
                    points.removeAll(EnumSet.range(OFFICE1, OFFICE4));
                    System.out.println(points);
                    points = EnumSet.complementOf(points);
                    System.out.println(points);
                }
            }

            EnumMap是一种特殊的Map,它要求其中的键(key)必须来自一个enum。由于enum本身的限制,所以EnumMap在内部可由数组实现。
            与EnumSet一样,enum实例定义时的次序决定了其在EnumMap中的顺序
            enum中的每个实例作为一个键,总是存在的。但是,如果没有为这个键调用put()方法来存入相应地值的话,其对应值就是null。

  • enum每个实例不同的行为(P603)
            Java的enum有一个非常有趣的特性,即它允许程序员为enum实例编写方法,从而为每个enum实例赋予各自不同的行为。要实现常量相关的方法,你需要为enum定义一个或多个abstract方法,然后为每个enum实例实现该抽象方法
            例如:
            public enum ConstantSpecificMethod {
                DATE_TIME {
                    String getInfo() {
                        return DateFormat.getDateInstance().format(new Date());
                    }
                },
                CLASSPATH {
                    String getInfo() {
                        return System.getenv("CLASSPATH");
                    }
                },
                VERSION {
                    String getInfo() {
                        return System.getProperty("java.version");
                    }
                };

                abstract String getInfo();

                public static void main(String[] args) {
                    for (ConstantSpecificMethod csm : values()) {
                        System.out.println(csm.getInfo());
                    }
                }
            }

            通过相应地enum实例,我们可以调用其上的方法。这通常也称为表驱动的代码(table-driven code)。
            除了实现abstract方法以外,每个实例也可以覆盖enum中的方法。

  • 职责链(Chain of Responsibility)设计模式
            在这个设计模式中,程序员以多种不同的方式来解决一个问题,然后将它们链接再一起。当一个请求到来时,它遍历这个链,直到链中的某个解决方案能够处理该请求。

  • 多路分发(P613)
            标题解释,以及问题说明:
                我们可能会声明Number.plus(Number)、Number.multiple(Number)等等,其中Number是各种数字对象的超类。然而,当你调用方法a.plus(b)时,你并不知道a或b的确切类型,那你如何能让它们正确地交互呢?
                Java中只支持单路分发。也就是说,如果要执行的操作包含了不止一个类型未知的对象时,那么Java的动态绑定机制只能处理其中一个的类型。这就无法解决我们上面提到的问题。所以你必须自己来判定其他的类型,从而实现自己的动态绑定行为。
                解决上面问题的办法就是多路分发(在a.plus(b)这个例子中,只有两个分发,一般称之为两路分发)。多态只能发生在方法调用时,所以,如果你想使用两路分发,那么就必须有两个方法调用:第一个方法调用决定第一个未知类型,第二个方法调用决定第二个未知的类型。要利用多路分发,程序员必须为每一个类型提供一个实际的方法调用,如果你要处理两个不同的类型体系,就需要为每个类型体系执行一个方法调用。

                以上这两段是书中原话,不是特别好理解。结合例子更好说明,例如上面说的a.plus(b),Java动态绑定机制能够正确处理a的类型(多态),但是进入a的plus方法之后,我们并不知道b是一个什么确切的数字类型。这就是第一段话所表达的意思。
                第二段话的意思是说,这时候我们只能在a中的plus方法中(这时候a的类型已经确定)调用b的方法,如此一来,就可以利用Java动态绑定(多态)确定b的类型。

                程序举例参考(P613)

  • 自限定的类型(P404)
            在Java泛型中,有一个好像是经常性出现的惯用法,它相当令人费解:
            class SelfBounded<T extends SelfBounded<T>> 
                        
            自限定所做的,就是要求在继承关系中,像下面这样使用这个类:
            class A extends SelfBounded<A> {}
            这会强制要求将正在定义的类当做参数传递给基类。
            以下也是允许的:
                class B extends SelfBounded<A> {}
            这样不允许:
                class C {}
                class D extends SelfBounded<C> {}
            自限定可以保证类型参数和正在被定义的类相同。

            古怪的循环泛型(CRG)。“古怪的循环”是指类相当古怪地出现在它自己的基类中这一事实。            
            实例:
                public class BasicHolder<T> {
                    T element;
                    void set(T arg) { element = arg; }
                    T get() { return element; }
                    void f() {
                        System.out.println(element.getClass().getSimpleName());
                    }
                }
            
                class Subtype extends BasicHolder<Subtype> {}
                
                public class CRGWithBasicHolder {
                    public static void main(String[] args) {
                        Subtype st1 = new Subtype(), st2 = new Subtype();
                        st1.set(st2);
                        Subtype st3 = st1.get();
                        st1.f();
                    }
                }
                /* output:
                Subtpye
                */
            
            注意,新类Subtype接受的参数和返回的值具有Subtype类型而不仅仅是基类BasicHolder类型这就是CRG的本质基类用导出类替代其参数。这意味着泛型基类变成了一种其所有导出类的公共功能的模板,但是这些功能对于其所有参数和返回值将使用导出类型。也就是说,在所产生的类中将使用确切类型而不是基本类型

  • 注解(P620),从未使用过的,好好看看
            注解使得我们能够以将由编译器来测试和验证的格式,存储有关程序的额外信息。注解的优点还包括:更加干净易读的代码以及编译器类型检查等。
            
            Java SE5内置了三种注解,定义在java.lang中:
            @Override,表示当前的方法定义将覆盖超类中的方法。如果拼写错误或者方法签名对不上,编译器会发出错误提示。
            @Deprecated,如果程序员使用了注解为它的元素,编译器会发出警告信息。
            @SuppressWarnings,关闭不当的编译器警告信息。

  • 定义注解(P621)
            定义注解时,会需要一些元注解(meta-annotation),如@Target@Retention@Target用来定义你的注解将应用于什么地方(例如是一个方法或者一个域)。@Retention用来定义该注解在哪一个级别可用,在源代码中(SOURCE)、类文件中(CLASS)或者运行时(RUNTIME)。
            例如:
            @Target(ElementType.METHOD)
            @Retention(RetentionPolicy.RUNTIME)
            public @interface MyAnnotation {}

            在注解中,一般都会包含一些元素表示某些值。当分析处理注解时,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,唯一的区别是你可以为其指定默认值
            没有元素的注解称为标记注解(marker annotation),例如上例中的@Test。
            @Target(ElementType.METHOD)
            @Retention(RetentionPolicy.RUNTIME)
            public @interface UseCase {
                public int id();
                public String description() default "no description";
            }
            注意,iddescription类似方法定义。description元素有一个default值,如果在注解某个方法时没有给出description的值,则该注解的处理器就会使用此元素的默认值。
            
  • 元注解(meta-annotation)(P622)
            Java目前只内置了三种标准注解,以及四种元注解
            元注解专职负责注解其他的注解
            
@Target
表示该注解可以用于什么地方。可能的ElementType参数包括:
CONSTRUCTOR: 构造器的声明
FIELD: 域声明(包括enum实例)
LOCAL_VARIABLE: 局部变量声明
METHOD: 方法声明
PACKAGE: 包声明
PARAMETER: 参数声明
TYPE: 类、接口(包括注解类型)或enum声明
@Retention
表示需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括:
SOURCE: 注解将被编译器丢弃
CLASS: 注解在class文件中可用,但会被VM丢弃
RUNTIME: VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息
@Documented将此注解包含在Javadoc中
@Inherited允许子类继承父类中的注解

  • JUnit测试

  • 并发——更快的执行(P651)
            并发通常是提高运行在单处理器上的程序的性能
            在单处理器上运行的并发程序开销确实应该比该程序的所有部分都顺序执行的开销大,因为其中增加了所谓上下文切换的代价(从一个任务切换到另一个任务)。
            但是,存在阻塞。如果程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务或线程阻塞了。如果没有并发,则整个程序都将停止下来,直至外部条件发生变化。
            事实上,从性能的角度看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。

            在单处理器系统中的性能提高的常见示例是事件驱动的编程。

  • 并发编程的简单机制(P654-P658)
  • 定义任务
                        要想定义任务,只需实现Runnable接口并编写run()方法。这个方法并无特殊之处——它不会产生任何内在的线程能力。要实现线程行为,必须显式地将一个任务附着到线程上。
                        run()方法的内容就是新线程要做的事情。
                        run()方法类似于主线程里面的main()方法。

  • 执行任务
                        定义了任务之后,我们需要创建一个新线程并把任务交给它。由新线程来调用run()方法。
  • Thread类
                                将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。再调用Thread对象的start()方法为该线程执行必需的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务。
                                例如:
                                    class MyJob implements Runnable { ...... }

                                    myRunnable = new MyJob();
                                    Thread t = new Thread(myRunnable);
                                    t.start();
                                补充
                                    Thread对象t有方法run(),myRunnable也有方法run()。如果直接调用t的run()方法将不会创建一个新线程,而是在当前线程调用myRunnable的run()方法。
  • Executor(以前没用过的,Head First Java中也没有提及的)
                                Java SE5的java.util.concurrent包中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。
                                Executor在客户端和任务之间提供了一个间接层,与你直接执行任务不同,这个中介对象接管了一切,它将执行任务。
                                Executor允许你管理异步任务的执行,而无须显式地管理线程的生命周期。
                                Executor在Java SE5/6中是启动任务的优选方法
            
                                ExecutorService(具有服务生命周期的Executor)知道如何构建恰当的上下文来执行Runnable对象。注意,ExecutorService对象是使用静态类Executors中的方法创建的,这个方法可以确定其Executor类型。
                                一共有三种Executor类型:
                                    CachedThreadPool
                                        通常会创建与所需数量相同的线程。
                                    FixedThreadPool
                                        使用了有限的线程集来执行所提交的任务。
                                        预先执行代价高昂的线程分配,因而也就可以限制线程的数量。
                                    SingleThreadExecutor
                                        线程数量为1的FixedThreadPool
                                        如果有多个任务,那么这些任务将排队。先进先出队列。
                                注意,在任何线程池中(包括CachedThreadPool和FixedThreadPool),现有线程在可能得情况下,都会被复用,就像String一样。

                                例如:
                                    ExecutorService exec = Executors.newCachedThreadPool();
                                    for (int i = 0; i < 5; i++)
                                        exec.execute(new MyJob());
                                    exec.shutdown();
                                通过execute()方法把任务添加到Executor中,然后由Executor对象为其创建新线程。
                                对shutdown()方法的调用可以防止新任务被提交给这个Executor对象。

                                其他两中Executor类型举例:
                                    ExecutorService exec = Executors.newFixedThreadPool(5);
                                    for (int i = 0; i < 5; i++)
                                        exec.execute(new MyJob());
                                    exec.shutdown();
                                    ExecutorService exec = Executors.newSingleThreadExecutor();
                                    for (int i = 0; i < 5; i++)
                                        exec.execute(new MyJob());
                                    exec.shutdown();

  • 从并发任务中产生返回值(P658)
            Runnable是执行工作的独立任务,但是它不返回值。如果需要返回一个值,那么可以实现Callable接口而不是Runnable接口。
            在Java SE5中引入的Callable是一种具有类型参数的泛型它的类型参数表示的是从方法call()(而不是run())中返回的值,并且必须使用ExecutorService对象的submit()方法调用它
            
            class TaskWithResult implements Callable<String> {
                private int id;

                public TaskWithResult(int id) {
                    this.id = id;
                }

                @Override
                public String call() throws Exception {
                    return "result of TaskWithResult " + id;
                }
            }

            public class CallableDemo {
                public static void main(String[] args) {
                    ExecutorService exec = Executors.newCachedThreadPool();
                    ArrayList<Future<String>> results = new ArrayList<Future<String>>();
                    for (int i = 0; i < 10; i++) {
                        results.add(exec.submit(new TaskWithResult(i)));
                    }
                    for (Future<String> fs : results) {
                        try {
                            System.out.println(fs.get());
                        } catch (InterruptedException e) {
                            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                        } catch (ExecutionException e) {
                            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                        } finally {
                            exec.shutdown();
                        }
                    }
                }
            }
            
            submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化。你可以用isDone()方法来查询Future是否已经完成。
            也可以检查直接调用get()来获取结果,get()将阻塞,直至结果准备就绪。

  • 休眠(P659)
            不再使用老的Thread.sleep(100),而是用TimeUnit.MILLISECONDS.sleep(100)
            TimeUnit可以指定延迟的时间单位,包括微秒、毫秒、秒等等。
                
  • 后台线程(P662)
            所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分
            当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。
    
            必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
            由后台线程创建的任何线程都被自动设置成后台线程。

            代码示例:
                直接Thread的形式:
                    public class SimpleDaemons implements Runnable {
                        @Override
                        public void run() {
                            try {
                                while (true) {
                                    TimeUnit.MILLISECONDS.sleep(100);
                                    System.out.println(Thread.currentThread() + " " + this);
                                }
                            } catch (InterruptedException e) {
                                System.err.println("sleep() interrupted");
                            }
                        }

                        public static void main(String[] args) {
                            for (int i = 0; i < 10; i++) {
                                Thread daemon = new Thread(new SimpleDaemons());
                                daemon.setDaemon(true);
                                daemon.start();
                            }
                            System.out.println("All daemons started");
                            try {
                                TimeUnit.MILLISECONDS.sleep(100);
                            } catch (InterruptedException e) {
                                System.err.println("sleep() interrupted");
                            }
                        }
                    }

                Executor形式
                    class DaemonThreadFactory implements ThreadFactory {

                        @Override
                        public Thread newThread(Runnable r) {
                            Thread t = new Thread(r);
                            t.setDaemon(true);
                            return t;
                        }
                    }

                    public class DaemonFromFactory implements Runnable {

                        @Override
                        public void run() {
                            try {
                                while (true) {
                                    TimeUnit.MILLISECONDS.sleep(100);
                                    System.out.println(Thread.currentThread() + " " + this);
                                }
                            } catch (InterruptedException e) {
                                System.err.println(e);
                            }
                        }

                        public static void main(String[] args) {
                            ExecutorService exec = Executors.newFixedThreadPool(10, new DaemonThreadFactory());
                            for (int i = 0; i < 10; i++) {
                                exec.execute(new DaemonFromFactory());
                            }
                            System.out.println("All daemons started");
                            try {
                                TimeUnit.MILLISECONDS.sleep(100);
                            } catch (InterruptedException e) {
                                System.err.println(e);
                        }
                    }
                }
            在Executor形式中,需要用一个ThreadFactory对任务进行包装。
            ThreadFactory接口中只有一个方法需要覆盖newThread(Runnable r),这个方法接收一个任务,然后为任务分配一个线程,设置好线程的细节之后,返回这个线程。
            在创建ExecutorService对象的时候,我们需要指定使用自己的ThreadFactory。

  • Join方法(P669)
            线程a可以在自己的生命周期中调用线程b的join()方法,其效果是a将等待一段时间直到b结束才继续执行。
            a也可以在调用join()时带上一个超时参数(单位可以是微秒,或者毫秒和纳秒),这样如果b过了这个指定时间还没有结束的话,a也会继续执行。

  • 捕获异常(P672)
            由于线程的本质特性,使得你不能捕获从线程中逃逸的异常。
            Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允许你在每个Thread对象上都附着一个异常处理器
            Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用

            例如:
                class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

                    @Override
                    public void uncaughtException(Thread t, Throwable e) {
                        System.out.println("caught " + e);
                    }
                }
            这就是一个新的UncaughtExceptionHandler。

            定义好了自己的UncaughtExceptionHandler之后,需要为必要的线程手动添加,可以通过实现ThreadFactory来完成:
                class HandlerThreadFactory implements ThreadFactory {

                    @Override
                    public Thread newThread(Runnable r) {
                        System.out.println(this + " creating new Thread");
                        Thread t = new Thread(r);
                        System.out.println("created " + t);
                        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
                        System.out.println("eh = " + t.getUncaughtExceptionHandler());
                        return t;
                    }
                }

            如果你知道将要在代码中处处使用相同的异常处理器,那么更简单的方式是在修改Thread类中的静态域:
                    Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

  • 解决共享资源竞争(P676)
            基本上所有的并发模式解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。
            通常这是通过在代码前面加上一条锁语句来实现的,这就使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种互相排斥的效果,所以这种机制常常称为互斥量(mutex)

            所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意synchronized方法的时候,此对象被加锁,这时该对象上的其他synchronized方法都不能被其他人访问,只有等到前一个方法调用完毕并释放了锁之后才能被调用。
            一个任务可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个synchronized方法,就会发生这种情况。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁(即锁被完全释放),其计数变为0。在任务第一次给对象加锁的时候,计数变为1。每当这个相同的任务在这个对象上获得锁时,计数都会递增。显然,只有首先获得了锁的任务才能允许继续获取多个锁。每当任务离开一个synchronized方法,计数递减,当计数为零的时候,锁被完全释放,此时别的任务就可以使用此资源。
            针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static方法可以在类的范围内防止对static数据的并发访问。

            Brian(Brian Goetz,《Java Concurrency in Practice》的作者之一)的同步规则
                如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。

  • 显示的Lock(P678)
            除了在方法前加上synchronized标识之外,Java SE5提供了显式的互斥机制。
            Lock对象必须被显式地创建、锁定和释放。
            它与内建的锁形式相比,代码缺乏优雅性。但是,对于解决某些类型的问题来说,它更加灵活。

            例如:
                private Lock lock = new ReentrantLock();

                @Override
                public int next() {
                    lock.lock();
                    try {
                        ++currentEvenValue;
                        ++currentEvenValue;
                        return currentEvenValue;
                    } finally {
                        lock.unlock();
                    }
                }
            unlock()必须放置在finally子句中,因为return必须在try子句中,避免unlock()不会过早发生,从而将数据暴露给了第二个任务。

  • 原子性与易变性(Atomic & Volatile,P680-684)
            原子操作是不能被线程调度机制中断的操作。

            原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。
                对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作内存。

            但是当你定义long或double变量时,如果使用volatile关键字,就会获得(简单的复制与返回操作的)原子性。

            在多处理器系统,可视性问题远比原子性问题多得多。
                一个任务做出的修改,即使是原子性的,对其他任务也可能是不可视的。
                例如,修改只是暂时性地存储在本地处理器的缓存(cache)中,因此不同的任务对应用的状态有不同的视图。
    
            volatile关键字还确保了应用中的可视性。
                如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作都可以看到这个修改。即使使用了本地缓存,情况也确实如此,volatile域会立即被写入到主存中,而读取操作就会发生在主存中。

            在非volatile域上的原子操作不会刷新到主存中去,因此其他读取该域的任务也不必看到这个新值。如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置成volatile的。

            当一个域的值依赖于它之前的值时(例如递增一个计数器),volatile就无法工作了。如果某个域的值受到其他域的值得限制,那么volatile也无法工作。

            对域中的值做赋值和返回操作通常都是原子性的。

            Java递增操作不是原子性的。

  • 原子类(P684)
            Java SE5引入了诸如AtomicIntegerAtomicLongAtomicReference等特殊的原子性变量类。

  • 临界区(Critical Section, P685)
            synchronized(someObject) {
                ...
            }
            这样就构成了一个临界区,锁加在了对象someObject上
            
            性能上优于加锁在方法上!!但一定要确保安全!!!!

  • 线程状态(P694)
    • 新建(new)
    • 就绪(Runnable)
    • 阻塞(Blocked)
    • 死亡(Dead)
            你可以通过调用sleep()使任务进入阻塞状态,在这种情况下,任务在指定的时间内不会运行。
            你可以通过调用wait()使线程挂起。直到线程得到了notify()或notifyAll()消息(或者在Java SE5的java.util.concurrent类库中等价的signal()或signalAll()消息),线程才会进入就绪状态。

  • 中断(P695)
            Thread类包含interrupt()方法,因此你可以终止被阻塞的任务,这个方法将设置线程的中断状态。

            中断发生的唯一时刻是在任务要进入到阻塞操作中,或者已经在阻塞操作内部时。
            因此,如果你调用interrupt()以停止某个任务,那么在run()循环碰巧没有产生任何阻塞调用的情况下,你的任务将需要第二种方式来退出。
            代码举例:
                while (!Thread.interrupted()) {
                    interrupted();
                    TimeUnit.MILLISECONDS.sleep(100);
                }
            interrupted()执行之后,任务被标记为中断状态,但是仍然在继续执行。
            遇到sleep()后,任务要进行阻塞调用,此时抛出InterruptedException(),可以退出。
            如果没有sleep()调用,就会检查中断状态标志。

            而在Executor方式中,如果你在Executor上调用shutdownNow(),那么它将发送一个interrupt()调用给它启动的所有线程
            如果你想关闭的只是单一的线程,那么需要使用submit()而不是execute()来启动任务,这样就会返回一个Future<?>对象,你可以在其上调用cancel()cancel()是一种中断由Executor启动的单个线程的方式。

  • 线程之间的协作(P702)
            wait()与notifyAll()
                调用sleep()的时候锁并没有被释放,调用yield()也属于这种情况。另一方面,当一个任务在方法里遇到了对wait()的调用的时候,线程的执行被挂起,对象上的锁被释放。
                
                只能在同步控制方法同步控制块中调用wait()、notify()和notifyAll()。

                注意wait()是发生在对象上的。例如,线程a中的任务调用了对象b的wait(),那么此时线程a将在对象b上处于wait()状态,并释放对象b的锁。直到其他线程在对象b上调用notify()/notifyAll()方法。
    
            Lock和Condition
                除了默认的wait()和notifyAll()之外,可以用显示的Lock和Condition对象。由Lock对象的newCondition()方法可以创建出Condition对象。
                你可以通过在Condition对象上调用await()来挂起一个任务,signal()来唤醒一个任务,或者调用signalAll()来唤醒所有在这个Condition对象上被挂起的任务。

            同步队列
                同步队列任何时刻都只允许一个任务向其中插入或移除元素。
                LinkedBlockingQueue,无大小限制;
                ArrayBlockingQueue,固定大小;
                SynchronousQueue,大小为1。

            管道,PipedWriter/PipedReader
                PipedWriter类允许任务向管道写PipedReader类允许不同任务从同一个管道中读取(?经过我的测试,不同任务不能同时连接同一个输出管道)
                管道基本上就是一个阻塞队列,存在于多个引入BlockingQueue之前的Java版本中。

  • 死锁(P718)
            Edsger Dijkstra提出的哲学家就餐问题。
            
            发生死锁的四个条件(P721)
                互斥条件:
                    任务使用的资源中至少有一个是不能共享的。
                至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。
                资源不能被任务抢占。
                必须有循环等待,一个任务等待其他任务所持有的资源,后者又在等待另一个任务所持有的资源。

  • 用于并发编程的工具类(P722)
            CountDownLatch
                你可以向CountDownLatch对象设置一个初始计数值,任何在这个对象上调用await()的方法都将阻塞,直至这个计数值为0。其他任务在结束其工作时,可以在该对象上调用countDown()来减小这个计数值。

                CountDownLatch的典型用法是将一个程序分为n个互相独立的可解决任务,并创建值为n的CountDownLatch。当每个任务完成时,都会在这个锁存器上调用countDown()。等待问题被解决的任务在这个锁存器上调用await(),将它们自己拦住,直至锁存器技术结束。

            CyclicBarrier
                非常像CountDownLatch,只是CountDownLatch是只触发一次的事件,而CyclicBarrier可以多次重用。

            DelayQueuePriorityBlockingQueueScheduledExecutorSemaphore、Exchanger

  • GUI(P771)
            有关GUI的代码在本书中提倡作为一个任务交由SwingUtilities启动。
            例如:
                main() {
                    SwingUtilities.invokeLater(myTask());
                }
                
                myTask implements Runnable

  • 工具提示
            setToolTipText()方法

  • 绘图(P803)
            一个典型的做法是从JPanel继承,覆盖方法paintComponent()

  • JNLP与Java Web Start(P812)
            JNLP
                Java网络发布协议(Java Network Launch Protocol)描述的是一个协议,而不是实现;所以要使用JNLP,必须要有一个实现。

            Java Web Start
                Java Web Start是由Sun免费提供的官方参考实现,并且作为Java SE5的一部分而发布的。

            其余部分可参考《Head First Java》中介绍

  • Swing与并发(P816)
            一个Swing事件分发线程,它始终在那里,通过从事件队列中拉出每个事件并依次执行它们,来处理所有的Swing事件。
            因此,如果在GUI中有一个长期的任务,我们需要额外再开一个线程去处理它。

  • JavaBeans(P823)

  • SWT(P844)
            Swing采用的方式是将所有的UI组件逐个像素地构建,以便提供所有想要的组件,无论底层操作系统是否拥有这些组件。
            SWT采用了中间路线,如果操作系统提供本地组件,那么就使用这些本地组件,如果不提供就合成这些在组件。

            SWT与等价的Swing程序相比,性能上有明显的提高,而且其编程模型要更简单一些。

            Display管理SWT和底层操作系统之间的连接,它是操作系统和SWT之间的桥的一部分。

            Shell顶层主窗口,所有其他组件都构建于其中。为了显示窗口以及这样的应用程序,你必须在Shell上调用open()。
            简单示例:
                Display display = new Display();
                Shell shell = new Shell(display);
                shell.setText("Hello SWT");
                shell.open();
                while (!shell.isDisposed()) {
                    if (!display.readAndDispatch()){
                        display.sleep();
                    }
                }
                display.dispose();
    
            SWT经常要求你显式地释放资源,因为这些通常都是来自底层操作系统的资源,如果不释放可能会被耗尽。
            
            在SWT中,所有部件必须有一个具有泛化类型Composite的父对象(其实,就是继承了Composite类型的子类型的对象,例如Shell就是一个子类型),并且必须在构造器中将这个父对象作为第一个参数提供。第二个参数是标志,可以是各种各样的标志,接受任意数量的样式指示信息
            简单示例:
                Display display = new Display();
                Shell shell = new Shell(display);
                shell.setText("Display Properties");
                shell.setLayout(new FillLayout());
                Text text = new Text(shell, SWT.WRAP | SWT.V_SCROLL);
                StringWriter props = new StringWriter();
                System.getProperties().list(new PrintWriter(props));
                text.setText(props.toString());
                shell.open();
                while (!shell.isDisposed()) {
                    if (!display.readAndDispatch()) {
                        display.sleep();
                    }
                }
                display.dispose();
        在Text构造器中可以清晰地看到这一点,其中第二个标志,是按位或在一起的。标志的具体含义是:将文本包装起来,如果需要的话会自动添加一个垂直滚动条。        
        在你使用一个构件之前一定要详细查看文档,了解其可接受的标志。

        widgetSelected()和widgetDefaultSelected()
            这两个侦听器类中,widgetSelected和widgetDefaultSelected这两个方法处理的事件类型不同。widgetSelected处理用户用鼠标选择窗口小部件的事件,例如,点击一个按钮。widgetDefaultSelected处理当按钮具有焦点时,用户按〔空格〕键或〔回车〕键所产生的事件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值