Thinking in Java
第一章 对象导论
- 构造新类型时应首先考虑用组合方式而不是继承方式,继承方式需保证导出类与基类具有相同的类型,即导出类是基类的一种,但反过来并不成立。
- 向下转型是不安全的,若转型错误则会在运行时抛出异常
第二章 对象、引用、存储、注释
- Java系统必须知道存储在堆栈内所有项的确切生命周期
- 对象引用一般存储在堆栈中,而对象存储在堆中
- 编译器并不知道堆中的项的生命周期,这样分配存储更灵活,但相比堆栈中的分配和清理会更耗时
- new创建小对象效率不高,所以Java将基本类型的变量存储在堆栈中(非对象,不用new),而对普通对象则将对象存储在堆中,其引用存储在堆栈中
- 基本类型的包装器类使得可以在堆中创建一个非基本对象,用来表示对应的基本类型(含义一致,形式不一致,可自动转换)
- BigInteger支持任意长度的整数,BigDecimal支持任何精度的定点数,其计算必须以方法调用的方式进行
- 创建数组时实际是创建了一个引用数组,引用不可以为null值,因此必须进行初始化,且通过运行时的下标检查保证了安全性
- java中不允许在小作用域中将一个大作用域内的变量覆盖的做法(C/C++中可以)
- 引用存活在作用域内,因此编译器知道其确切生命周期,而对象存在于堆内,由垃圾回收器监视所有对象,辨别出那些不再会被引用的对象,并释放其内存,因此解决了内存泄露问题
- 类的基本类型成员会自动初始化,但局部基本类型变量不会
- static对象只会存储一个,多new对象只会复制其引用
- javadoc是用于提取注释输出HTML的工具(内置于JDK),一般使用/**+*/编写注释文档,只有public和protect成员可以进行文档注释(类、域、方法前注释)
- 文档标签有两种,前置于注释行的@+标签(不包括前面的*号)和{@+}形式的行内文档标签
第三章 操作符
- 基本类型的操作是复制数据,而对对象的操作是复制其引用,故存在“别名现象”,用a=b为对象赋值后就绑定到了同一对象上
- ==和!=操作的是对象的引用,因此要对比两个对象的值应用equals(),并且要在类中覆盖equals方法进行比较
- java中不可将非boolean值当作boolean值用,//!注释会被自动移除
第四章 控制执行流程
- 使用continue label或break label可以跳过或中止label下最外层迭代,带标签的continue会到达标签的位置,并重新进入紧接在那个标签后面的循环;带标签的break会中断并跳出标签所指的循环
- foreach语句总是会复制数组(基本类型及其自动包装类型)中的元素,因此不可以在foreach语句中修改数组中的元素,只有访问权;若是对象数组,则会复制引用,拥有修改权
第五章 初始化与清理
- 若在为对象分配内存时调用的是C或C++代码,则需要在finalize函数中调用free函数释放内存
- Java虚拟机在未面临内存耗尽的情形下,不会浪费时间去执行垃圾回收的工作
- System.gc()方法用于强制执行垃圾回收动作,在其回收前会调用所有对象的finalize方法,故可以在finalize方法中对终结条件进行验证,在不符合验证时打印错误以方便程序员找出错误
- 类成员的初始化是强制进行的,故无论是定义时初始化或是构造器初始化之前,类成员都有了其默认值
- 变量定义的顺序决定了初始化的顺序,但静态对象会优先进行初始化,只有当用到对象时才创建静态成员,入口main函数所在类的静态成员及其父类的静态成员即使没用到对象也会初始化
- 实例初始化或静态初始化(在方法外用{}包起来的初始化语句)可以保证无论调用哪个显式构造器,实例初始化都会优先在构造器内的前部进行
- 基本类型的数组为普通数组,而对象数组实质为对象的引用数组,故Book[] a=new Book[5]不会创建Book对象,只会创建空引用null
- 创建数组的三种形式,int[] a=new int[5];int[] a={1,2,3,4,5};int[] a=new int[]{1,2,3,4,5}
- 使用可变参数列表class…作为定义形参,可以接收可变数目(包括0)的class对象或class数组,应只在重载方法的一个版本中使用一种class可变参数列表,否则会编译出错
- 初始化顺序:父类静态成员,子类静态成员,父类实例成员初始化,父类构造器,子类实例成员初始化,子类构造器
第六章 访问权限控制
- 访问修饰符包括private、public、protected和默认(无关键字,包内访问权限)
- 内部类可以看做是外部类的一个成员,因此可以有private、public、protect三种访问权限
- package用于将多个类包在一个群组中,将单一的全局名字空间分隔开,避免名称冲突问题
- 要使用外部包内的类,需要先将该包所在路径添加到CLASSPATH环境变量中,才能正确使用import语句,也可以将该包导入到工程目录下
- 未指定包的java文件将被编译器看作是隶属于所在目录的默认包中,而默认包中文件同样享有包访问权限
- 父类中包访问权限的成员会被继承到所有子类中,但若子类在另一个包中,则无法使用该成员,而protected成员则既提供包访问权限,又提供子类跨包间的访问权限;而类中包访问权限的域或方法只能在包内可见,包外的类即使继承自该类,也无法用super.调用到包访问权限的域或方法
- 子类会自动获得父类中所有的域和方法,但也无法访问父类中的private成员,而只能通过父类中public或protected方法来间接访问private成员
- 通过将构造器都设为private可以阻止外部程序直接创建该类对象,此时构造器就被隐藏起来了;可以再定义一个public static的方法来调用隐藏起来的构造器,这样可以在每次创建该类对象时多做一些额外工作,例如记录一共创建了多少个对象;另外一种创建方法是单例设计模式,在类中定义并创建一个private static的该类对象本身,并定义一个public static access方法返回这个对象的引用,就可以保证始终只能创建该类唯一一个对象,而且只能通过access方法调用
- 包访问权限的类中若有public static的成员,该成员仍可以被包外的程序调用,尽管包外程序并不能生成该类的对象
第七章 复用类
- 每个类中都允许有且仅有一个public static void main方法,哪个类被命令行调用,程序就将哪个类的main方法作为入口
- 如果父类中只有一个默认的无参数构造器,那么子类构造器会自动先调用父类的构造器,不用显式地调用;但若父类中存在多个构造器或有带参数的构造器,则子类构造器不会自动调用,需要用super显式地调用,且必须是构造器中的第一句语句
- @Override注解可以标注一个方法为覆盖父类而非重载,当不小心写为重载时会出现编译错误提示
- final类型的基本类型数据是固定值的数据,而final修饰的非基本类型对象则是固定引用的数据(final修饰的参数也一样),该引用所指向的对象的具体值仍可以改变,数组也是对象,故final数组也只是引用固定而非数值固定
- Static final类型的数据不仅是固定数值的,而且在多个对象中也是固定的,其值在第一次初始化时确定
- final域必须在定义时初始化或在类的构造器中初始化
- 方法的参数列表中final修饰的参数值无法被修改
- final修饰方法禁止子类中覆盖,而private修饰方法实际上都隐式地指定了为final的(子类中无法调用父类中private的域或方法,实际上private修饰方法非常少见,private方法只能被类内部的其它方法调用)
- final修饰的类不允许继承
第八章 多态
- Java中除了static和final(private方法属于final方法)之外,其它所有方法都是后期(运行时,动态地)绑定的,即具有多态性质
- 非private方法才能被覆盖,如果尝试覆盖private方法编译器不会报错,但并没有覆盖,这时导出类中相当于存在了两个重名的方法,定义对象时类型为基类就优先调用基类的方法,定义为导出类型则优先调用导出类中新增加的方法
- 域访问操作将由编译器完成,因此不存在多态,若向上转型则访问域默认访问基类域,但通过get方法返回域还是会返回导出类域
- 静态方法是与类相关联的,因此也不具有多态性质
- 构造器实际上是隐式地static方法,不具有多态性质
- 在需要进行手动清理时,清理顺序应与初始化顺序相反,并且对于共享对象,在清理之前要先确认已无其它对象共享此被清理对象
- 尽量不要在构造器中就调用所构造对象的方法,其成员未初始化完整,并且调用方法具有多态性,可能导致结果错误
- 初始化顺序:将分配给对象的存储空间都初始化为零(引用为null),调用基类构造器,按照声明顺序初始化成员,调用导出类的构造器
- java具有协变返回类型,即导出类中覆盖基类方法,新的方法的返回类型可以是旧的返回类型的导出类型
- 将对象向上转型后,该对象就无法再调用导出类中新增的方法,但若通过向下转型后该对象就又变回导出类对象了,就可以调用导出类中新增的方法了,向下转型并不一定是安全的,故java会在运行时自动检查,称作运行时类型识别(RTTI)
第九章 接口
- 包含抽象方法的类被限定为抽象类,继承者可以选择具体化称为子类或者不具体化保持抽象类
- 接口可以是public的或默认包访问权限的,接口内的域都是默认static和final的
- 接口中的方法默认是public的
- 当一个算法有多种策略实现时,可以采用策略设计模式,Processor作为算法策略基类,有多种实现方法继承自Processor,调用process(Processor p,Object s)实现不同策略下的算法;当采用基类+继承耦合过紧时,可以采用Processor接口,这样Processor和process中的代码可以被更多地复用
- 当所用到的类存在于库中而无法被修改时,可以采用适配器设计模式,适配器接受你所拥有的接口Filter,然后生成具有你所需要用到的Processor接口的对象;将接口从具体实现中解耦使得接口可以应用于多种不同的具体实现,因此代码也就更具可复用性
- 一个类可以实现多个接口,实现多重继承,也可以向上转型为任一接口(使用接口的核心原因),因为每一个接口都是一个独立类型
- 接口之间可以使用extend继承,且可以多重继承
- 一个类实现多个接口时,若多个接口中有相同名字的方法,则会进行方法重载,若方法无法重载(只有返回类型不同),则会编译出错
- 接口中的域都自动是final和static的,其不属于接口的一部分,不会因为类实现接口而被继承
- 嵌套在另一个接口中的接口被自动定义为public的,实现外部接口时不一定要实现其中嵌套的接口,private接口不能在定义它的类之外被实现
- 接口是实现多继承的途径,而生成遵循接口对象的典型方式就是工厂方法设计模式。
原始方式:
方法以接口为参数,void action(Service s){...}
调用的时候if(xxx)action(new SImp1());else if(xxx)action(new SImp2());else if....
每个调用地方都需要直接提供实现类对象。如果创建对象需要额外计算一些初值,代码就会很冗余。
工厂方式:
Class Service1Factory implement Factory{public void getService(){return new SImp1();}}
void action(ServiceFactory fac){Service s = fac.getService();....}
如此一来对象只在工厂中进行创建,相应的初值计算也只需要在factory里实现即可,使得使用与实现更大程度解耦,程序更直观。
接口Factory使得在有新的产品推出时,不需要修改原有代码,只要新增加一个ServiceFactory继承自Factory即可,保持了拓展开放,修改关闭的开闭原则,此为工厂方法设计模式,一个抽象工厂接口+一个抽象产品接口,一个具体工厂只生产一个具体产品。
另外还有一种抽象工厂设计模式,相比原来的区别在于存在了多个抽象产品接口,一个具体工厂可以生产多个具体产品。
- 优先选择类而不是接口
第十章 内部类
- 内部类隐藏在外部类中,因此无法在外部类之外直接创建一个内部类对象,通常在外部类中定义一个getInnerObject方法返回对象,也可以通过outer.new Inner()创建内部类对象
- 迭代器设计模式,定义end方法判断是否到达末尾,current方法返回当前序列的对象,next方法移动到下一个对象
- 内部类可以直接调用外部类的所有成员,而外部类在调用内部类的成员时需要先创建内部类对象(同样可以调用所有成员)
- 内部类不管存在何处,在编译时都会像普通类一样编译,其修饰符可以为private/protected/public,与外部类的成员具有类似的权限特性
- 一个匿名内部类形式为
new 父类构造器(参数列表)|实现接口() {…};,匿名内部类中无构造器
- 若在其类内部需要使用到外部定义的对象,编译器会要求其参数引用是final的,因为匿名内部类会将传入的参数复制一份使用,而如果在内部类中对参数进行修改,实际只修改了复制的那份,实际值不受影响,为了避免误会故将参数固定为final的,使得在内部类中无法修改参数
- 匿名内部类既可以拓展类,也可以实现接口,但是不能两者兼备,而且实现接口也只能是单个接口
- 10.7 嵌套类
- static的内部类为嵌套类,无法从嵌套类对象中访问非静态的外部类对象
- 普通内部类不能有static数据或字段,但嵌套类可以有
- 接口中也可以定义内部类(也可以叫嵌套类),这种内部类不需要先创建外部类的对象就可以直接创建
Interface1.Inner a =new Interface1.Inner();
- 内部类编译时生成Outer$Inner.class文件
- 10.8 为什么需要内部类
- 接口和内部类共同实现了java中多重继承的功能
- 继承自内部类的类,其构造器必须传入一个外部类,并在构造器内调用outer.super();
- 内部类类似外部类的一个成员数据,无法被覆盖
第11章 持有对象
- Arrays.asList方法接受一组元素并将之转化为数组,该数组可以作为ArrayList构造器参数,Arrays.asList方法会将第一个元素的类型作为该数组类型,所以该组元素必须是相同类型的对象,asList生成序列的底层实现为数组,故无法更改长度
- Java容器可以分为两类Collection和Map,Collection为一个独立元素的序列,Map为一组成对的键值对对象,也称为关联数组
- Set容器中相同值的元素只会保存一次
- List和Queue和Set属于Collection,Map通过put(key,value)增加新的键值对,因此其key也是唯一存在的
- ArrayList的copy是引用的复制,对copy进行操作会反映到原始列表中
- 迭代器统一了对容器的访问方式
- 不能使用基本类型的容器,例如int的容器,要转化为Integer的容器,自动包装
- Queue是窄化的LinkedList
- 使用Array.asList(a)方法返回一个List对象,此对象会使用底层数组a作为其物理实现,因此若修改此List,会影响到数组a
- 生成Iterator是将队列与消费队列的方法连接在一起的耦合度最小的方式,并且与实现Collection相比,它在序列类上施加的约束也少得多
- foreach语句使用了Iterabal.itrator()方法,因此任何实现了Iterabal接口的类都可以用于foreach语句
![]()
第12章 通过异常处理错误
- Throw 抛出异常,catch捕捉异常,在一个可能产生异常的方法外要么处理异常(catch并处理),要么声明throws某种类型的异常(留给后面调用的程序员处理,但要告知他会产生异常)
- 在获得对当前异常对象的引用后,可以重新将异常抛出,抛出后同一个try块后续的catch子句将被忽略,异常抛给上一级环境
- RuntimeException不需要进行声明和检查,抛出时会自动调用printStackTrace方法
1:异常的根类是Throwable
2:用try-catch捕获并处理异常。注意catch(exception)语句的设置:异常处理匹配遵循就近原则。异常处理系统会按照catch子句的代码顺序找到“最相近”的处理程序进行处理,而找到后就认为该异常以得到处理,不会再往下查找。所以我们应该按照从小到大的顺序捕捉,异常一旦被其中一环捕获了就不会再向下传递,在最后一个catch才用catch(Exception)捕捉所有类型的异常。
3:异常信息栈轨迹:printStackTrace()打印“从方法调用处直到异常抛出处”的方法调用序列。
4:抛出异常:throw 会把异常抛给上一级环境中处理,此时后面的catch语句将被忽略。所以,一般我们把抛出异常的catch子句放在最后。
5:Throwable子类中,Error、Exception、RuntimeException提供了带参数的构造器,可以自定义异常提示信息。
6:异常分两种:编译(静态)异常和运行时异常。静态异常在编码阶段就要进行捕获处理,而运行时异常是不可预料的,它没有被捕获的话就会一直往上传递直到main(),然后程序打印异常信息并退出。
7:finally:无论try-catch语句发生了什么,finally子句都能运行。一般用来进行清理工作,比如关闭try子句中打开的文件连接、数据库连接等。注意:即使在try语句中进行了return,finally语句也会执行!


浙公网安备 33010602011771号