《Java编程思想》读书笔记

目录

    • 1 对象导论
    • 2 一切都是对象
    • 5 初始化和清理
    • 6 访问权限控制
    • 7. 复用类
    • 8. 多态
    • 9. 接口
    • 10. 内部类
    • 11. 持有对象
    • 12. 通过异常处理错误
    • 13. 字符串
    • 14. 类型信息
    • 15. 泛型
    • 16. 数组
    • 20. 注解

1 对象导论

  1. 面向对象语言的 5 个基本特性:万物皆为对象;程序是对象的集合,它们通过发送消息来告知彼此所做的;每个对象都有自己的由其他对象所构成的存储;每个都拥有其类型;某一特定类型的所有对象都可以接收同样的消息。简言之,对象具有状态、行为和标识。
  2. 每个对象都提供服务,就是说在开发或理解程序设计时,将对象想象成“服务提供者”。这有助于提高对象的内聚性。注意:不要将过多的功能都塞在一个对象中。
  3. 在设计新类时,应该首先考虑组合,因为它更加简单灵活;如果优先使用继承,可能导致难以使用并过分复杂的设计。
  4. 前期绑定、后期绑定、动态绑定。
  5. 在 Java 中,所有的类最终都继承自单一的基类,即 Object,这就是 Java 的单根继承结构。单根继承结构的好处:保证所有对象都具有某些功能,使垃圾回收器的实现变得容易得多。
  6. Java 采用动态内存分配方式。

2 一切都是对象

  1. Java 存储数据的 5 个地方:
    - 寄存器:这是最快的存储区,它位于处理器内部;但是寄存器的数量有限,所以寄存器根据需求进行分配。你不能直接控制,也不能在程序中感觉到寄存器存在的任何迹象。
    - 堆栈:位于 RAM 中,但通过堆栈指针可以从处理器那里获得直接支持。堆栈指针若向下移动,则分配新的内存;若向上移动,则释放那些内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时,Java系统必须知道存储在堆栈内所有项的生命周期,以便上下移动堆栈指针。这一约束限制了程序的灵活性。对象引用存在于堆栈中。
    - 堆:一种通用的内存池(也位于 RAM 区),用于存放所有的 Java 对象。堆不同于堆栈的好处是:编译器不需要知道存储的数据在堆里存活多长时间。因此,在堆里分配存储有很大的灵活性。但是,用堆进行存储分配和清理可能比用堆栈进行存储分配需要更长的时间。
    - 常量存储:常量值通常直接放在程序代码内部。
    - 非 RAM 存储:即使程序终止,它们仍然可以保持自己的状态。
  2. 基本类型
基本类型 大小 最小值 最大值 包装器类型
boolean —— —— —— Boolean
char 16-bit Unicode o Unicode 2^16 Character
byte 8-bit -128 +127 Byte
short 16-bit -2^15 +2^15-1 Short
int 32-bit -2^31 +2^31-1 Integer
long 64-bit -2^63 +2^63-1 Long
float 32-bit IEEE754 IEEE754 Float
double 64-bit IEEE754 IEEE754 Double
void —— —— —— Void
  1. static 关键字:static 作用于字段时,对每个类来说都只有一份存储空间,但对于 static 作用于某个方法,差别却没那么大。static 方法的一个重要用法就是在不创建任何对象的前提下可以调用它。

5 初始化和清理

  1. 用构造器确保初始化:java 在用户操作对象之前自动调用相应的构造器,从而保证初始化的进行。注意,调用构造器是编译器的责任。在 java 中,“初始化”和“创建”是捆绑在一起,不可分离的。
  2. 方法重载适用于构造器和其他方法。
  3. this 关键字:编译器暗自把”所操作对象的引用”作为第一个参数传递给方法,如 peel()。假设要在方法的内部获取对当前对象的引用,是没有标识符可用的,因为这个引用是由编译器“偷偷”传入的。但是,为此有个专门的关键字:this。this 关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。this 关键字可以用于将当前对象传递给其他方法。
  4. static 关键字:static 方法就是没有 this 的方法。要是在代码里出现了大量的 static 方法,就该重新考虑自己的设计了。
  5. 初始化顺序:在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍会在任何方法(包括构造器)被调用前得到初始化。
  6. 静态数据的初始化:static 关键字不能应用于局部变量,因此只能作用于域。静态初始化只有在必要时刻才会进行。如果不创建静态成员所在类的对象,也不通过类名.的方式调用静态成员,那么静态成员就永远不会初始化。静态对象不会再次被初始化。
  7. 初始化的顺序是先静态对象,再非静态对象。
  8. 对象的创建过程
    a. 当首次创建类型为 Dog 的对象时(构造器可以看作是静态方法),或者 Dog 类的静态方法/静态域首次被访问时,Java 解释器必须查找类路径,已定位 Dog.class 文件;
    b. 然后载入 Dog.class,这将会创建一个 Class 对象,有关静态初始化的所有动作都会执行;
    c. 当用 new Dog() 创建对象的时候,首先在堆上为 Dog 对象分配足够的存储空间;
    d. 这块存储空间会被清零,这就自动地将 Dog 对象中的所有基本数据类型设置为默认值,而引用则被设置为 null;
    e. 执行所有出现于字段定义处的初始化动作;
    f. 执行构造器。
  9. 实例化初始化子句是在构造器之前执行的。
  10. 数组中的基本数据类型会自动初始化成空值。

6 访问权限控制

开始:2019年6月11日21:54:32,结束:2019年6月16日15:22:37

  1. 访问权限修饰词的作用:供类库开发人员向客户端程序员指明哪些是可用的,哪些是不可用的。
  2. 访问权限控制的等级,从最大权限到最小权限依次为:public、protected、包访问权限(没有关键词)和private。访问权限修饰词会因类是存在于一个相同的包,还是存在于一个单独的包而受到影响。
  3. 编译单元:也称为转译单元,就是一个 Java 源代码文件。每个编译单元都必须有一个后缀名 .java,而在编译单元内则可以有一个 public 类,该类的名称必须与文件的名称相同(包括大小写,但不包括文件的后缀名.java)。每个编译单元只能有一个 public 类,否则编译器不能接受。如果在编译单元之中还有额外的类的话,那么在包之外是无法看见这些类的,这是因为它们不是 public 类,而且它们主要用来为 public 类提供支持。
  4. 手动设置临时 CLASSPATH 及查看 CLASSPATH:
    G:\AndroidWorkspaces\Think4JavaExamples\app\src\main\java>set CLASSPATH=C:\DOC\JavaT
    G:\AndroidWorkspaces\Think4JavaExamples\app\src\main\java>echo %CLASSPATH%
    C:\DOC\JavaT
    
  5. Java解释器的运行过程:首先找出环境变量CLASSPATH,CLASSPATH包含一个或多个目录,用作查找.class文件的根目录;从根目录开始,解释器获取包的名称并将每个句点替换成反斜杠,这样就从CLASSPATH根中产生一个路径名称;得到的路径会与CLASSPATH中的各个不同的项相连接,解释器就在这些目录中查找与要创建的类名称相关的.class文件。
  6. Java 没有 C 的条件编译功能,这是因为 Java 本身是跨平台的。
  7. 获取对某成员的访问权的方法:使该成员成为 public;把其他类放在与该类同一个包内;继承该类;提供 getter\setter 方法。
  8. public: 接口访问权限,可以用于类、域、方法;
  9. private:你无法访问,可以用于类、域、方法(用于方法的例子:控制如何创建对象,从而把构造方法私有化),不可以用于作为编译单元的类,可以用于内部类;如果想阻止对某个类的继承,也可以私有化构造来实现。
  10. protected:继承访问权限 ,可以修饰类、域、方法,不可以用于作为编译单元的类,但可以用于内部类。
  11. 将 public 成员置于开头,后面依次是 protected、包访问权限和 private 成员,这样可以使程序读起来容易些。
  12. 类既不可以是 private,也不可以是 protected。对于类的访问权限,仅有两个选择:包访问权限或public。
  13. 控制对成员的访问权限的两个原因:不让用户碰触那些他们不该碰触的部分,第二点,也是最重要的原因,可以让类库设计者在不影响客户端程序员的情况下,更改类的内部工作方式。

7. 复用类

开始:2019年6月16日15:23:28,结束:2019年7月13日16:44:09

  1. 使用组合时,初始化引用的位置:在定义对象的地方;在类的构造器中;惰性初始化(在正要使用这些对象之前);使用实例初始化。
  2. 继承是所有OOP语言和Java语言不可缺少的组成部分。当创建一个类时,总是在继承。
  3. 即使一个类只具有包访问权限,其 public main() 仍然是可用的。
  4. 初始化基类:基类在导出类构造器可以访问它之前,就已经完成初始化。基类构造器总是会被调用,并且是在导出类构造器之前调用的。
  5. java 中没有C++中析构函数的概念,析构函数是一种在对象被销毁时可以被自动调用的函数。因此,想要某个类清理一些东西,必须显式地编写一个特殊方法来做这件事。应当注意,清理的顺序同生成的顺序相反。
  6. 组合和继承:组合和继承都允许在新的类中放置子对象,组合是显示地这样做,而继承是隐式地做;组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形;“is-a”(是一个)的关系是用继承来表达的,而“has-a”(有一个)的关系是用组合来表达的。
  7. protected关键字:就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它却是可以访问的。
  8. 为什么称为向上转型?传统的类继承图的绘制是将根置于页面的顶端,然后逐渐向下。由导出类转型为基类,在继承图上是向上移动的,因此一般称为向上转型。
  9. 为什么说向上转型总是安全的?因为向上转型是从一个较专用类型向较通用类型转换的。
  10. 在组合和继承之间做出选择: 问问自己是否需要从新类向基类进行向上转型。如果必须向上转型,那么继承是必要的;但如果不需要,则应该好好考虑自己是否需要继承。
  11. final 数据:一个永不改变的编译器常量;一个在运行时被初始化的值,而你不希望它被改变。第一种情况,必须是基本数据类型。对于基本数据类型,final 使数值恒定不变,而对于对象引用,final使引用恒定不变。
  12. 空白final:指的是被声明为final但又未给定初值的域。必须在域的定义处或者每个构造器中用表达式对final进行赋值。空白final是第二种情况。
  13. final方法:在继承中使方法行为保持不变,并且不会被覆盖。可以有效地关闭动态绑定。类中所有的private方法都隐式地指定为final的。

8. 多态

开始:2019年7月13日16:44:09,结束:2019年7月14日14:48:12

  1. 方法调用绑定:将一个方法调用同一个方法主体关联起来被称为绑定。若在程序执行前进行绑定,叫做前期绑定(C只有一种方法调用,就是前期绑定)。若在运行时根据对象的类型进行绑定,叫做动态绑定运行时绑定。Java中除了static方法和final方法之外,其他所有的方法都是后期绑定。
  2. 域访问操作由编译器解析,不是多态的。多态不适用于域的例子:
    class Super {
    public int field = 0;
    public int getField() {
        return field;
    }
    }
    class Sub extends Super {
    public int field = 1;
    public int getField() {
        return field;
    }
    public int getSuperField() {
        return super.field;
    }
    }
    public class FieldAccess {
    public static void main(String[] args) {
        Super sup = new Sub();
        System.out.println("sup.field = " + sup.field +
                ", sup.getField() = " + sup.getField()); // 0,1
        Sub sub = new Sub();
        System.out.println("sub.field = " +
                sub.field + ", sub.getField() = " +
                sub.getField() +
                ", sub.getSuperField() = " +
                sub.getSuperField()); //1,1,0
    }
    }
    
  3. 静态方法不具有多态性。
  4. 构造器内部的多态方法的行为例子,理解初始化顺序是理解这个例子的关键:
    class Glyph {
    	void draw() {
        	System.out.println("Glyph.draw()");
    	}
    	Glyph() {
        	System.out.println("Glyph before draw()");
        	draw();
        	System.out.println("Glyph after draw()");
    	}
    }
    
    class RoundGlyph extends Glyph {
     	private int radius = 1;
    
    	RoundGlyph(int radius) {
        	this.radius = radius;
        	System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
    	}
    	void draw() {
        	System.out.println("RoundGlyph.draw(), radius = " + radius);
    	}
    }
    public class PolyConstructors {
    	public static void main(String[] args) {
        	new RoundGlyph(5);
    	}
    }
    /*
    输出结果:
    Glyph before draw()
    RoundGlyph.draw(), radius = 0
    Glyph after draw()
    RoundGlyph.RoundGlyph(), radius = 5
    */
    ```
    
  5. 编写构造器时的一条有效的准则:用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法。在构造器中唯一可以安全调用的方法是基类中的final方法。
  6. 用继承进行设计:用继承表达行为间的差异,并用字段表达状态上的变化。
  7. 运行时类型识别(RTTI):Runtime type information,在运行期间对类型进行检查的行为。

9. 接口

开始:2019年7月15日11:40:33,结束:2019年7月21日11:49:27

  1. 策略设计模式:能够根据所传递参数对象的不同而具有不同行为。
  2. 适配器设计模式:接受一个已有的接口,产生需要的接口。
  3. 使用接口的核心原因:为了能够向上转型为多个接口。第二个原因,与使用抽象类基类相同:防止客户端程序员创建该类的对象。
  4. 嵌套接口:对于嵌套在类中的接口来说,可以使用各种权限修饰符;对于嵌套在其他接口中的接口来说,public 修饰符是多余的,因为所有的接口元素都必须是 public 的,不可以在里面定义 private 修饰的接口。
    interface X {
    	interface G {
        	void f();
    	}
    	// public 是多于的
    	public interface H {
        	void f();
    	}
    
    	void g();
    	// private 是不合法的
    	private interface I {
    	}
    }
    
  5. 工厂设计模式
  6. 类与接口如何选择:恰当的原则应该是优先选择类而不是接口。从类开始,如果接口的必需性变得非常明确,那么就进行重构。接口是一种重要的工具,但是它们很容易被滥用。

10. 内部类

开始:2019年7月23日21:25:04,结束:2019年7月29日08:12:17

  1. 内部类的作用:隐藏和组织代码的方式;能访问其外围对象的所有成员,而不需要任何特殊条件。
  2. 内部类自动拥有对其外围类所有成员的访问权。这是如何做到的呢?当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。
  3. 创建内部类对象,必须提供外部类对象。使用外部类对象来创建内部类对象。这就是.new语法。
    public class DotNew {
    	public class Inner {}
    
    public static void main(String[] args) {
        DotNew dotNew = new DotNew();
        Inner inner = dotNew.new Inner();
    }
    

}
```

  1. 嵌套类:静态内部类,不需要对外部类对象的引用。因为内部类是static声明的,所以内部类对象与其外部类对象之间没有联系。嵌套类意味着:要创建嵌套类的对象,并不需要其外围类的对象;不能从嵌套类的对象中访问非静态的外围类对象。
  2. private 内部类:可以完全阻止依赖于任何类型的编码,并且完全隐藏了实现的细节。
  3. 外部类可以访问内部类的 private 元素,但是要经由内部类对象来实现。
    public class Outer {
    class Inner {
        private int value = 11;
        private void f() {
            System.out.println("Inner.f()");
        }
    }
    
    Inner inner() {
        return new Inner();
    }
    
    public static void main(String[] args) {
        Outer outer = new Outer();
        Inner inner = outer.inner();
        System.out.println(inner.value);
        inner.f();
    }
    }
    
    1. 在匿名内部类中不可能有命名的构造器,但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,下面是例子:
abstract class Base {
    public Base(int i) {
        System.out.println("Base constructor, i = " + i);
    }

    public abstract void f();
}
public class AnonymousConstructor {
    public static Base getBase(int i) { // 这个 i 不用是 final 的,因为它没有在匿名内部类内部被直接使用
        return new Base(i) {
            {
                System.out.println("Inside instance initializer" + this);
            }
            @Override
            public void f() {
                System.out.println("In Anonymous f()");
            }
            
        };
    }

    public static void main(String[] args) {
        Base base = getBase(47);
        base.f();
    }
}
/*
Base constructor, i = 47
Inside instance initializerinnerclasses.AnonymousConstructor$1@1540e19d
In Anonymous f()
*/
  1. 匿名内部类与正规的继承相比的限制:匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。
  2. 嵌套类与普通的内部类的另外一个区别:普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static字段,也不能包含嵌套类。但是嵌套类可以包含这些东西。
  3. 接口内部可以放置嵌套类(不加 static 关键字,默认就是 public 和 static 的),好处是这个类可以被这个接口的所有不同实现所公用。
  4. 一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外围类的所有成员。
    class MNA {
    	private void f() {
        	System.out.println("MNA.f()");
    	}
    
    class A {
        private void g() {
            System.out.println("A.g()");
        }
    
        public class B {
            void h() {
                System.out.println("B.h()");
                g();
                f();
            }
        }
    }
    
    }
    public class MutiNestingAccess {
    	public static void main(String[] args) {
        	MNA mna = new MNA();
        	MNA.A mnaa = mna.new A();
        	MNA.A.B mnaab = mnaa.new B();
        	mnaab.h();
    	}
    }
    /* 打印结果:
    B.h()
    A.g()
    MNA.f()
    */
    
  5. 内部类允许继承多个非接口类型(即类或抽象类),也就是说如果拥有的是抽象的类或具体的类,而非接口,那就只能使用内部类才能实现多重继承。
    class D { }
    
    	abstract class E { }
    
    	class Z extends D {
    		E makeE() {
        	return new E() {};
    	}
    }
    public class MultiImplementation {
    	static void takesD(D d) {
    	}
    
    	static void takesE(E e) {
    	}
    
    	public static void main(String[] args) {
        	Z z = new Z();
        	takesD(z);
        	takesE(z.makeE());
    	}
    }
    
  6. 闭包(closure):是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。内部类是面向对象的闭包。
  7. 迭代器模式
  8. 命令模式
  9. 内部类的继承,必须在构造器中使用 enclosingClassReference.super(); 语法
    /**
    * 内部类的继承
    * @author wzc
    * @date 2019/7/28
    */
    class WithInner {
    	class Inner {
    
    	}
    }
    public class InheritInner extends WithInner.Inner{
    //    InheritInner(){}
     	InheritInner(WithInner wi) {
        	wi.super();
    	}
    
     	public static void main(String[] args) {
        	WithInner wi = new WithInner();
        	InheritInner ii = new InheritInner(wi);
    	}
    }
    
  10. 内部类可以被覆盖吗?当继承自某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内。
    class Egg {
    	private Yolk y;
     	protected class Yolk {
        	public Yolk() {
            	System.out.println("Egg.Yolk");
        	}
    	}
    	public Egg() {
        	System.out.println("New Egg()");
        	y = new Yolk();
    	}
    }
    
    public class BigEgg extends Egg {
    	public class Yolk {
        	public Yolk() {
            	System.out.println("BigEgg.Yolk()");
        	}
     	}
    	public static void main(String[] args) {
        	new BigEgg();
    	}
    }
    /*
    输出结果:
    New Egg()
    Egg.Yolk
    */
    
  11. 局部内部类前面不可有权限说明符,这是因为它不是外围类的一部分,但是它可以访问当前代码块内的常量,以及此外围类的所有成员。

11. 持有对象

开始:2019年8月1日11:05:16,结束:2019年8月18日12:05:21

  1. 向上转型可以像作用于其他类型一样作用于泛型。

    class GrannySmith extends Apple {}
    
    class Gala extends Apple {}
    
    class Fuji extends Apple {}
    
    class Braeburn extends Apple {}
    
    public class GenericsAndUpcasting {
    	public static void main(String[] args) {
            ArrayList<Apple> apples = new ArrayList<Apple>();
            apples.add(new GrannySmith());
        	apples.add(new Gala());
        	apples.add(new Fuji());
        	apples.add(new Braeburn());
        	for (Apple c : apples) {
            	System.out.println(c);
        	}
    	}
    }
    /**
    * 打印结果:
    * holding.GrannySmith@1540e19d
    * holding.Gala@677327b6
    * holding.Fuji@14ae5a5
    * holding.Braeburn@7f31245a
    */
    
  2. 所有的 Collection 都可以用 foreach 语法遍历。因为 Collection 包含了这个接口:

    Iterator<E> iterator();
    
  3. Arrays.asList() 的输出,是一个 List,但是它的底层表示的是数组,因此不能调整尺寸。

    List<Integer> list = Arrays.asList(16, 17, 18, 19, 20);
        list.set(1, 99); // 可以修改
    //        list.add(21); // 运行报错:UnsupportedOperationException,底层数组不支持增删
    //        list.remove(16);// 运行报错:UnsupportedOperationException,底层数组不支持增删
    
  4. 显式类型参数说明

    class Snow {}
    class Powder extends Snow {}
    class Light extends Powder {}
    class Heavy extends Powder {}
    class Crusty extends Snow {}
    class Slush extends Snow {}
    
    public class AsListInterface {
    	public static void main(String[] args) {
        	List<Snow> snow1 = Arrays.asList(
                	new Crusty(), new Slush(), new Powder()
        	);
        	// 编译错误:需要 List,但发现 List
    //        List snow2 = Arrays.asList(
    //                new Light(), new Heavy()
    //        );
        	List<Snow> snow3 = new ArrayList<>();
        	Collections.addAll(snow3, new Light(), new Heavy());
    
        	// 显式地类型参数说明
            List<Snow> snow4 = Arrays.<Snow>asList(new Light(), new Heavy());
       	 }
    }
    

    因为 Arrays.asList 是一个泛型函数,static 后面的 表示泛型变量。

        public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    	}
    
  5. 容器的打印:打印数组需要借助 Arrays.toString(),而打印容器无需任何帮助。这是因为已经重写了 toString() 方法。例如 List,Set 继承于 AbstractCollection 对 toString() 做了重写,Map 继承于 AbstractMap 对 toString() 做了重写。

  6. List 的特点:可以将元素维护在特定的序列中。List 接口在 Collection 接口上增加了与索引相关的操作,例如:

    boolean addAll(int index, Collection<? extends E> c);
    E get(int index);
    E set(int index, E element);
    void add(int index, E element);
    E remove(int index);
    int indexOf(Object o);
    int lastIndexOf(Object o);
    // 迭代器
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int index);
    List<E> subList(int fromIndex, int toIndex);
    

    有两种类型的 List:ArrayList长于随机访问元素,但在List的中间插入和移除元素时较慢;LinkedList在List中间插入和删除代价较低,但在随机访问方面较慢。LinkedList的特性集比ArrayList更大。

  7. 对于 Collection 接口的 boolean containsAll(Collection c) 方法来说,传入集合的元素顺序是不重要的,因为它在 AbstractCollection 中的实现是这样的:

    public boolean containsAll(Collection<?> c) {
    for (Object e : c)
        if (!contains(e))
            return false;
    return true;
    }
    
  8. List 的 List subList(int fromIndex, int toIndex); 接口:从较大的列表中创建出一个片段,这个产生的列表的幕后就是那个大的列表,因此,对返回的列表的修改都会反映到较大的列表中,反之亦然。查看这个方法在 ArrayList 中的实现:

    public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
    }
    

    可以看到返回的是一个 SubList 对象,它是 ArrayList 的内部类,继承于 AbstractList。

  9. List 的 boolean retainAll(Collection c),是继承自 Collection 接口的。这是一种有效的“交集”操作。

  10. Collection 的两个重载的 toArray 方法有什么区别:

    Object[] toArray();
    <T> T[] toArray(T[] a);
    

    第一个将任意的 Collection 转换成一个数组,返回的是 Object 数组;
    第二个可以传递目标类型的数据,那么就会返回目标类型的数组。需要注意的是参数列表的参数传入:如果传入的参数数组太小,存放不下集合中所有的元素,那么 toArray 方法将创建一个具有合适尺寸的数组。如果传入的参数数组太大,那么下表为[list.size()] 的数组元素将会被置为 null,其他数组元素保持原值。所以我们最好将参数组大小定义与集合元素个数一致。这一点阿里巴巴Java开发手册上有强制要求。

  11. 持有事物是容器的基本工作。

  12. Iterator 的真正威力:能够将遍历序列的操作与序列底层的结构分离。所以可以说,迭代器统一了对容器的访问。

  13. ListIterator:是 Iterator 的子类型,但比 Iterator 更强大,它只能用于各种 List 类的访问。Iterator 只能向前移动,但是 ListIterator 可以双向移动。它可以产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引,并且可以使用 set() 方法替换它访问过的最后一个元素。通过调用 listIterator() 方法产生一个指向 List 开始处的 ListIterator,并且还可以使用 listIterator(n) 方法创建一个一开始就指向列表索引为 n 的元素处的 ListIterator。需要注意的是若反向遍历,那么 n 应该指定为集合的size。

    public interface ListIterator<E> extends Iterator<E> {
    	// Query Operations
    
    	boolean hasNext(); // 继承而来
    
    	E next(); // 继承而来
    
    	boolean hasPrevious();
    
    	E previous();
    
    	int nextIndex();
    
    	int previousIndex();
    
    
    	// Modification Operations
    
    	void remove(); // 继承而来
    
    	void set(E e);
    
    	void add(E e);
    }
    
  14. 创建一个空的 LinkedList,通过使用 ListIterator,将若干个 Integer 出入这个 List 中,插入时,总是将它们插入到 List 的中间。

    public class Ex14 {
    
    	private static void addMiddle(LinkedList<Integer> l, Integer[] ia) {
        	for (Integer i : ia) {
            	ListIterator<Integer> it = l.listIterator(l.size() / 2);
            	it.add(i);
            	System.out.println(l);
        	}
    	}
    
    	public static void main(String[] args) {
        	LinkedList<Integer> li = new LinkedList<>();
        	Integer[] x = {0, 1, 2, 3, 4, 5, 6, 7};
        	Ex14.addMiddle(li, x);
    	}
    }
    
  15. 使用 LinkedList,可以产生更好的 Stack。

    public class Stack<T> {
    	private LinkedList<T> storage = new LinkedList<>();
    
    	// 入栈
    	public void push(T t) {
        	storage.addFirst(t);
    	}
    	// 获取栈顶元素
    	public T peek() {
        	return storage.getFirst();
    	}
    	// 移除并返回栈顶元素
    	public T pop() {
        	return storage.removeFirst();
    	}
    	// 判断栈是否为空
    	public boolean empty() {
        	return storage.isEmpty();
    	}
    
    	@Override
    	public String toString() {
        	return storage.toString();
    	}
    }
    
  16. Set 具有与 Collection 完全一样的接口,因此没有任何额外的功能。实际上 Set 就是 Collection,只是行为不同。(这是继承与多态思想的简单应用:表现不同的行为。)

  17. Queue:队列,是一种典型的先进先出(FIFO)的容器。它的特点是:从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。

  18. PriorityQueue:优先级队列,声明下一个弹出元素时最需要的元素。

  19. Foreach:任何实现了 Iterable 接口的类,都可以将它用于 foreach 语句中。JavaSE5引入了 Iterable 接口,该接口包含一个能够产生 Iterator 的 iterator 方法,并且 Iterable 接口被 foreach 用来在序列中移动。

  20. 数组并不是一个 Iterable,但是 foreach 可以用于数组。

  21. Arrays.asList():这个方法产生的 List 被直接打乱,会修改底层的数组。这个方法产生的 List 不支持增删。如果不想底层数组改变,就应该去创建一个副本的集合。

    public class ModifyingArraysAsList {
    	public static void main(String[] args) {
        	Random random = new Random(47);
        	Integer[] integers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        	List<Integer> list = new ArrayList<>(Arrays.asList(integers));
        	System.out.println("Before shuffling: " + list);
        	Collections.shuffle(list, random);
        	System.out.println("After shuffling: " + list);
        	System.out.println("array: " + Arrays.toString(integers));
    
        	List<Integer> list2 = Arrays.asList(integers);
        	System.out.println("Before shuffling: " + list2);
        	Collections.shuffle(list2, random);
        	System.out.println("After shuffling: " + list2);
        	System.out.println("array: " + Arrays.toString(integers));
    //        list2.remove(0); // 不支持移除
    //        System.out.println("remove: " + list2);
    //        list2.add(5); // 不支持添加
    	}
    }
    /*
    Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    After shuffling: [4, 6, 3, 1, 8, 7, 2, 5, 10, 9]
    array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    Before shuffling: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    After shuffling: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]
    array: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]
    */
    
  22. 数组一旦生成,它的容量就不能改变了。而 Collection,Map 却可以改变 size。

  23. 各种 Queue 及栈的行为,由 LinkedList 提供支持。

12. 通过异常处理错误

开始:2019年8月20日21:49:33,结束:2019年9月1日15:06:57

  1. 异常处理程序:可以节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。
  2. 被检查异常:在编译时被强制检查的异常。
  3. printStackTrace() 方法:可以打印 Throwable 和 Throwable 的调用栈轨迹。它默认是把错误发送给标准错误流
  4. 栈轨迹:最先打印出来的是栈顶元素,这里是 Throwable 被创建和抛出之处,最后打印出来的元素是调用序列中的第一个方法调用。
  5. 异常链:在捕获一个异常后抛出另一个异常,并且把原始异常的信息保留下来。具体做法是使用带 cause 参数的构造,把原始异常作为 cause 传进去。但只有 Error、Exception 以及 RuntimeException 提供了带 cause 参数的构造器。其他异常应该使用 initCause 方法。
  6. 不受检查异常:RuntimeException 及其子类的异常,也叫运行时异常,它们会自动被java虚拟机抛出。这种异常属于错误。这些异常要是受检的话,代码就太混乱了。比如:ArithmeticException 可能由除以 0 导致,ClassCastException 是由类转换错误导致的。
  7. finally 子句的作用:把除内存之外的资源恢复到它们的初始状态,如已经打开的文件或网络连接关闭掉,在屏幕上画的图形清理掉。
  8. 异常丢失的例子:
    public class ExceptionSilencer {
    	public static void main(String[] args) {
        	try {
            	throw new RuntimeException();
        	} finally {
            	return;
        	}
    	}
    }
    
  9. 异常的限制:对于构造器而言,派生类构造器必须包含基类构造器的异常说明(当然不能捕获基类构造器抛出的异常),可以添加新的异常。对于普通的覆写方法而言,覆盖后的方法不能抛出更大的异常,这是为了保证对象的可替换性。需要特别说明的一点:异常说明本身并不属于方法类型的一部分(方法类型是由方法的名字和参数的类型组成的),因此不能基于异常说明来重载方法。一个在基类方法的异常说明中的异常,未必会出现在派生类方法的异常说明里。这是与继承的规则明显不同的,因为在继承中,基类的方法必须出现在派生类里。也就是说,在继承和覆盖的过程中,某个特定方法的“异常说明的接口”不是变大了而是变小了,这与类接口在继承时的情形恰恰相反。

13. 字符串

  1. String 对象是不可变的;
    public class Immutable {
    	public static String upcase(String s) {
        	System.out.println("s hashCode="+s.hashCode());
        	return s.toUpperCase();
    	}
    
    	public static void main(String[] args) {
        	String q = "howdy";
        	System.out.println("q hashCode="+q.hashCode());
        	System.out.println(q);
        	String qq = upcase(q);
        	System.out.println(qq);
        	System.out.println(q);
    	}
    }
    /*
    打印结果:
    q hashCode=99470565
    howdy
    s hashCode=99470565
    HOWDY
    howdy
    */
    
  2. “+”与StringBuilder:“+”对于 String 操作是重载过的操作符,当调用“+”操作符在 String 对象时,编译器会创建一个 StringBuilder 对象,依次调用 append 方法,最终构造一个 String 对象,这是编译器做的优化。但是,注意在循环中的情况:
    public class WhitherStringBuilder {
    	public String implicit(String[] fields) {
        	String result = "";
        	for (int i = 0; i < fields.length; i++) {
        		// 这里会生成多个 StringBuilder 对象:每循环依次就会创建一个。
           	 	result += fields[i];
        	}
        	return result;
    	}
    
    	public String explicit(String[] fields) {
        	StringBuilder result = new StringBuilder();
        	for (int i = 0; i < fields.length; i++) {
            	result.append(fields[i]);
        	}
        	return result.toString();
    	}
    }
    
    可以使用 javap -c WhitherStringBuilder 反编译查看,其中 -c 表示将生成JVM字节码。
  3. StringBuilder 是 Java SE5 引入的,它是线程不安全的;而 StringBuffer 是 Java SE1 就有的,它是线程安全的。

14. 类型信息

  1. Java 在运行时识别对象和类的信息的方式:一种是“传统”的 RTTI,它假定我们在编译时已经知道了所有的类型;另一种是“反射”机制,它允许我们在运行时发现和使用类的信息。
  2. 在 Java 中,所有的类型转换都是在运行时进行正确性检查的。这就是 RTTI 的含义:在运行时,识别一个对象的类型。
  3. Java 使用 Class 对象来执行其 RTTI。类是由类加载器动态加载的。类只有在需要时才会被加载,而非在程序运行之前全部加载。
  4. 如果要在运行时使用类型信息,就要首先获得对恰当的 Class 对象的引用。Class.forName() 是实现此功能的便捷途径。如果有对象,可以调用 getClass() 方法获取 Class 对象。
  5. 使用 Class.newInstance() 来创建的类,必须带有默认的构造器。
  6. 当使用 ".class"来创建对 Class 对象的引用时, 不会自动地初始化该 Class 对象; 但是, 使用 Class.forName() 方法来产生 Class 引用时, 就会立即初始化该 Class 对象.
  7. 为了使用类而做的准备工作包括三个步骤: 加载, 这是由类加载器执行的; 链接; 初始化.
  8. Class 类是一个泛型类, Class优于平凡的Class, 即便它们是等价的, 并且平凡的Class如你所见, 不会产生编译警告信息. Class的好处是它表示你并非是碰巧或者由于疏忽, 而使用了一个非具体的类引用, 你就是选择了非具体的版本.
  9. 为了创建一个 Class 引用, 它被限定为某种类型, 或该类型的子类型, 就需要将通配符?与 extends 关键字相结合, 来创建一个范围.
  10. 通过使用泛型语法, 可以让编译器强制执行额外的类型检查.
  11. RTTI 和反射之间的区别:对 RTTI 来说,编译器在编译时打开和检查 .class 文件;对于反射机制来说,.class 文件在编译时是不可获取的,所以是在运行时打开和检查 .class 文件。
  12. 动态代理:它可以动态地创建代理并动态地处理对所有代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策。
  13. YAGNI 原则:(You Aren’t going to Need It,你用不需要它),在你的设计草案的初稿中,应该努力使用最简单且可以正常工作的事物,直到程序的某个方面要求你添加额外的特性,而不是一开始就假设它是必须的。

15. 泛型

开始:2019年12月8日19:14:55 结束:2020年1月18日21:45:16

  1. 理解了边界所在,你才能成为程序高手。因为只有知道了某个技术不能做到什么,你才能更好地做到所能做的。
  2. 泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
  3. 元组(tuple):也叫数据传送对象或信使,将一组对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中的元素,但是不允许向其中存放新的对象。元组可以有任意长度,元组中的对象可以是任意不同的类型。注意,元组隐含地保持了其中元素的次序。Android 中的 android.util.Pair 类就是一个元组。
    public class Pair<F, S> {
    	// 注意这里是 public final 修饰的
    	public final F first;
    	public final S second;
    
    	public Pair(F first, S second) {
        	this.first = first;
        	this.second = second;
    	}
    
    	@Override
    	public boolean equals(Object o) {
        	if (!(o instanceof Pair)) {
            	return false;
        	}
        	Pair<?, ?> p = (Pair<?, ?>) o;
        	return Objects.equals(p.first, first) && Objects.equals(p.second, second);
    	}
    
    
    	@Override
    	public int hashCode() {
        	return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode());
    	}
    
    	@Override
    	public String toString() {
        	return "Pair{" + String.valueOf(first) + " " + String.valueOf(second) + "}";
    	}
    
    	public static <A, B> Pair <A, B> create(A a, B b) {
        	return new Pair<A, B>(a, b);
    	}
    }
    
  4. Java 泛型的一个局限性:基本类型无法作为类型参数。
  5. 泛型方法:泛型方法使得该方法能够独立于类而产生变化。记住一个基本的指导原则:无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更加清楚明白。另外,对于一个 static 的方法而言,无法访问泛型类的类型参数,所以,如果 static 方法需要使用泛型能力,就必须使其成为泛型方法。
  6. 类型参数推断(type argument inference):在使用泛型方法时,通常不必指明参数类型,因为编译器会为我们找出具体的类型。
  7. 在泛型方法中,可以显式地指明类型,做法就是在点操作符与方法名之间插入尖括号,然后把类型置于尖括号内。如果是在定义该方法的类的内部,必须在点操作符之前使用 this 关键字,如果是使用 static 的方法,必须在点操作符之前加上类名。
  8. 泛型擦除:
    查看这段代码的输出结果:
    public class ErasedTypeEquivalence {
    	public static void main(String[] args) {
        	Class c1 = new ArrayList<String>().getClass();
        	Class c2 = new ArrayList<Integer>().getClass();
        	System.out.println(c1 == c2);
    	}
    }
    
    输出结果:true
  9. Class.getTypeParameters() 按声明顺序返回 TypeVariable 对象的一个数组,这些对象表示用此 GenericDeclaration 对象所表示的常规声明来声明的类型变量。如果底层常规声明不声明类型变量,则返回长度为 0 的数组。但是,只是返回用作参数占位符的标识符。TypeVariable 是一个接口,在这里它的实现类是 TypeVariableImpl。
  10. 声明 T 必须具有类型 Bounds 或者从 Bounds 导出的类型。
  11. 泛型类型参数将擦除到它的第一个边界(它可能会有多个边界)。编译器实际上会把类型参数替换为它的擦除,T 擦除到了 Bounds,就好像在类的声明中用 Bounds 替换了 T 一样。
  12. Java 为什么会存在泛型擦除,而 C++ 却没有?擦除是 Java 的泛型实现的一种折中,因为泛型不是 Java 语言出现时就有的组成部分。如果泛型在 Java 1.0 中就已经是其一部分了,那么这个特性将不会使用擦除来实现—它将使用具体化,使类型参数保持为第一类实体,因此你就能够在类型参数上执行基于类型的语言操作和反射操作。擦除减少了泛型的泛化性。泛型在 Java 中仍然是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。
  13. 边界:边界使得我们可以在用于泛型的参数类型上设置限制条件,这样就可以按照自己的边界类型来调用方法。因为擦除移除了类型信息,所以,可以用无界泛型参数调用的方法只是那些可以用 Object 调用的方法。但是,如果能够将这个参数限制为某个类型的子集,那么就可以用这些类型子集来调用方法。为了执行这种限制,Java 泛型重用了 extends 关键字。多边界时,使用 & 来进行连接,并且类放在第一位,接口放在后面。并且,只可以有一个具体类的边界,可以有多个接口边界,这点和继承是一样的。
  14. 数组的特殊行为:可以向导出类型的数组赋予基类型的数组引用。
  15. 泛型容器:泛型没有内建的协变类型。
    public class NonCovariantGenerics {
    public static void main(String[] args) {
        // 把数组的实现
    //        Fruit[] fruit = new Apple[10];
        // 改为 泛型容器的实现,可以看到直接编译不通过了:类型不兼容:需要是List,但发现的却是ArrayList
        	// 理解为:不能把一个涉及 Apple 的泛型赋给一个涉及 Fruit 的泛型。
    //        List flist = new ArrayList();
    		// Apple 的 List 可以持有 Apple 类型及其子类型
        	List<Apple> apples = new ArrayList<>();
        	apples.add(new Apple());
        	apples.add(new Jonathan());
    		// Fruit 的 List 可以持有 Fruit 类型及其子类型
        	List<Fruit> fruits = new ArrayList<>();
        	fruits.add(new Fruit());
        	fruits.add(new Jonathan());
        	fruits.add(new Apple());
        	fruits.add(new Orange());
        	// Apple 的 List 在类型上不等价于 Fruit 的 List。
    	}
    }
    

16. 数组

开始:2020年1月19日22:05:17,结束:2020年2月19日23:41:56

  1. 数组与其他种类的容器之间的区别有三方面:效率、类型和保存基本类型的能力。数组的效率高于容器;数组可以持有某种具体类型,容器却需要泛型来保证;数组可以持有基本类型,而容器不能,只能持有对应的包装类型。
  2. 数组标识符是什么?只是一个引用,指向了在堆中创建的一个真实对象,这个数组对象用以保存指向其他对象的引用。
  3. 对象数组和基本类型数组的区别是什么?对象数组保存的是引用,基本类型数组直接保存基本类型的值。
  4. 打印多维数组的所有元素,使用 Arrays.deepToString(Object[] a)
  5. 粗糙数组:数组中构成矩阵的每个向量都可以具有任意的长度。
  6. 数组与泛型不能很好地结合,我们不能实例化具有参数化类型的数组。这是因为擦除会移除参数类型信息,而数组必须知道它们所持有的确切类型,以强制保证类型安全。但是,数组与泛型并非格格不入,我们可以参数化数组本身的类型。
  7. 数组与泛型结合的两种方法:参数化类和参数化方法。参数化方法要好于参数化类。
  8. 数组是协变类型的。
  9. Arrays.fill()方法: 使用单一的值来填充整个数组或者数组的某个区域。
  10. System.arraycopy() 不会执行自动包装和自动拆包,两个数组必须具有相同的确切类型。(p776)
  11. 优选容器而不是数组。

20. 注解

开始: 2020年04月13日12:51:44, 结束:

  1. Java SE5 内置的三种标准注解,是定义在 java.lang中的注解:@Override@Deprecated@SuprressWarnings

  2. 注解的定义看起来很像接口的定义。与其他任何Java接口一样,注解也将会编译成class文件

    @Target(ElementType.METHOD) 
    @Retention(RetentionPolicy.RUNTIME) 
    public @interface Test {}
    

    interface关键字前面有一个 @
    @Target 定义你的注解将应用于什么地方,是一个方法还是一个域;
    @Retention 用来定义该注解在哪一个级别可用,在源代码中(SOURCE)、在类文件中(CLASS)或者运行时(RUNTIME)。

    没有元素的注解称为标记注解(marker annotation),例如 @Test

  3. 注解的元素在使用时表现为名-值对的形式,并需要置于注解声明之后的括号内。

  4. Java 提供了四种注解,专门负责新注解的创建。这四种注解被称为元注解,因为它们专职负责注解其他的注解。

    @Target :表示该注解可以用于什么地方。可能的 ElementType 参数包括:
    CONSTRUCTOR:构造器的声明
    FIELD:域声明(包括 enum 实例)
    LOCAL_VARIABLE:局部变量声明
    METHOD:方法声明
    PACKAGE:包声明
    PARAMETER:参数声明
    TYPE:类、接口(包括注解类型)或 enum声明

    @Retention:表示需要在什么级别保存该注解信息。可选的 RetentionPolicy 参数包括:
    SOURCE:注解将被编译器丢弃;
    CLASS:注解在 class 文件中可用,但会被 VM 丢弃;
    RUNTIME:VM 将在运行期也保留注解,因此也可以通过反射机制读取注解的信息。
    @Documented:将此注解包含在 Javadoc 中。
    @Inherited:允许子类继承父类中的注解。

  5. ClassConstructorMethodField 等类都实现了 AnnotatedElement 接口。

  6. 注解元素可用的类型:
    所有基本类型(intfloatboolean 等)
    String
    Class
    enum
    Annotation
    以上类型的数组

    注意:如果使用了其他类型,那么编译器就会报错。也不允许使用任何包装类型。
    注解可以作为元素的类型,也就是说注解可以嵌套。

  7. 默认值限制
    元素不能有不确定的值:要么有默认值,要么在使用注解时提供元素的值。
    对于非基本类型的元素,不管是在源代码中声明时,还是在注解接口中定义默认值时,都不能以 null 作其值。

  8. 快捷方式:如果程序员的注解中定义了名为value的元素,并且在应用注解的时候,如果该元素是唯一需要赋值的一个元素,那么此时无需使用名-值对的这种语法,而只需要在括号内给出 value 元素所需的值即可。

  9. 编译器允许程序员对同一个目标使用多个注解。注意,使用多个注解的时候,同一个注解不能重复使用。

  10. 注解不支持继承:不能使用关键字 extends 来继承某个 @interface

  11. 注解处理工具apt:是sun公司为了帮助注解的处理过程而提供的工具。与 javac 一样,apt 被设计为操作 Java 源文件,而不是编译后的类。

你可能感兴趣的:(Java)