Java学习笔记--Thinking in Java(精华部分提取)
Chapter1 对象导论
(1)编程即抽象
(2)每个对象都有接口
(3)每个对象都提供服务
设计过程中,应遵循"高内聚,低耦合"原则
(4)被隐藏的具体实现
类设计过程,应通过封装的手段,将实现细节隐藏(private),而将接口暴露(public)
(5)复用具体实现
复用:同一个实现,应尽可能保证在不同的场景下可以直接使用
复用的手段包括:组合(composition)、继承(aggregation)..
所谓组合,即has-a;所谓继承,即is-a
(6)继承
基类(父类),通过继承的方式,得到导出类(子类)。子类成为父类的泛化。
子类可以复制父类所有行为,也可以重写父类的行为。
理想的继承应为is-a,但实际上通常是is-like-a
(7)伴随多态的可互换对象
面向接口编程,在需要改动时(更换接口对象),将修改开销降到最小。
实现相同接口的类(继承自相同父类的子类),由于多态的作用,对于相同的接口,表现出各自不同的行为。
多态的实现方法,即后期绑定。在编译期间,编译器无法确定接口方法调用的哪个具体实现。
将子类转化为父类对象(实现转化为接口)的行为,称为向上转型(upcast);反之,称为向下转型。
(8)单根继承结构
java中,所有对象(基本类型不算)具有相同的基类(Object),且所有子类都只能继承自唯一一个父类。
(9)容器
jdk中提供了很多不同类型的容器,应对不同场景的使用。
(10)对象的创建及生命期
不同于c++,Java中申请的对象不需要手动释放,而由GC释放。常见算法包括:引用计数、根节点可及。
由于GC,Coder可将精力放在更顶层的设计上。
(11)异常处理:处理错误
程序中意外情况,都以异常的形式抛出,供处理。
(12)并发编程
多线程/多进程可以提升CPU的利用率,也解决了多个任务需要同时运行的需求,但是也带来了难度和问题。
隐患主要在于共享资源的访问及修改,而锁,成为解决并发编程的一大利器。
(13)Java与Internet
网络即socket。万维网将遍布世界的不同主机连接起来,提供通信。常见的通信架构:C/S,B/S。
Chapter2 一切都是对象
(1)用引用操作对象
所有的对象都被jvm放在堆中,程序中操作的是对对象的引用。
(2)必须有你创建所有对象
jvm内存中存储数据的位置包括:
a.寄存器,在处理器内部,速度快,但是数量有限
b.堆栈,方法执行过程中存储局部变量的地点。方法执行结束后,堆栈中的数据自动回收。地址从高向低生长。
c.堆,存储对象的内存区域,由GC进行对象的回收管理。地址从低向高生长。
d.常量存储,常量通常直接存放在代码内部。
e.非RAM存储。以上几条皆在RAM中,为运行过程中的数据存储,易失。ROM可持久存储。
基本类型:
java中,每个基本类型都有一个包装类与之对应。例如,Boolean类包装了boolean基本类型,Character类包装了char基本类型...
除了基本类型,Java还提供了特殊类型BigInteger/BigDecimal存储高精度数字
数组:
java中的数组提供了范围检查,对于越界问题在运行时进行了检查。在创建数组过程,java自动将数组全部置零初始化。
(3)永远不需要销毁对象
GC对于已经不再使用的对象,自行回收,不需要编程主动释放。
(4)类的成员
类由字段的方法组成,基本成员中的基本类型会被java自动置为默认值(0,false),对象被置为null
(6)构建一个Java程序
命名:程序通过包区分不同的命名空间,import关键字可导入包。
(7)java程序
javac用于编译java程序,java用于运行java程序(前提是配置好JDK)
(8)注释和嵌入式文档
java代码中通过特殊的语法编辑注释,可通过工具javadoc将注释生成文档或HTML。
例如标签: @see/@author/@param等
(9)编码风格
通常使用驼峰风格
Chapter3 操作符
(1)int与Integer
同样表示整形数字,前者为基本类型,后者为类(引用类型)。使用存在区别,在方法内部申请二者,前者存于堆中,后者于栈。
例如,Integer i1=5; Integer i2=5; (i1==i2)结果为false,因为二者虽然数值相同,但是属于不同对象。
而int j1=5; int j2=5.则有(j1==j2)为true
(2)指数的表示
数学中,e表示自然对数的基数(约为2.718)。而在设计FORTRAN语言时,设计师自然的决定用e代表10的幂次,于是设计C/C++/java时沿用下来。
例如,float f4 = 2e-20f; 代表2*(10的-20次方)
(3)移位操作符
包括有符号的移位符<<和>>(符号扩展,右移时,若为正数,高位补0;若为负数,高位补1)
以及无符号的移位符<<<和>>>(0扩展,右移时,高位补0)
(4)类型转换
包括窄化转换和扩展转换,前者指新的类型相比原先容纳更少的数据,后者则转换后容纳更多。
例如,short转化为int为扩展转换,反之为窄化转换。
(5)整形数据类型的数值运算
a.若类型比int小(如short、char、byte),会先转换为int在做运算。
b.不同数据类型做运算,最大的数据类型决定了结果值的类型。
Chapter4 控制执行流程
(1)for语句的使用
for(A;B;C){;}
A/C中皆可有多个语句,用逗号分隔。例如,
for(int i=1,j=i+10; i<5; i++, j=i*2)
{
System.out.println("i="+i+",j="+j);
}
(2)swith-case语句
选择因子类型必须为整数或枚举类型
Chapter5 初始化与清理
(1)类可包含多个构造方法Constructor,默认为无参构造器。带参构造器中可调用其他构造器(最多一个)
(2)清理:终结处理与垃圾回收
finalize函数在垃圾回收之前调用,用于处理一些无法被GC释放的资源(例如socket、stream、内存相关资源等)。
在finalize之后,若依然满足垃圾回收条件,则在下次垃圾回收时,将对象的内存资源释放掉。
(3)垃圾回收工作方式
(3.1)引用计数,是一种简单但是很慢的垃圾回收计数。每个对象都有一个引用计数,当有引用连接至对象时,引用计数加1;当引用离开作用域或置为null时,引用减1.
虽然管理引用计数开销不大,但是此过程在程序生命周期中持续发生。垃圾回收期会在所有对象上扫描,当引用数为0时释放。
此垃圾回收机制存在问题:循环引用时,无法正常进行垃圾回收。而定位该问题开销极大,因此尚未被任何java虚拟机采用。
(3.2)常用垃圾搜寻及处理方式
(3.2.1)垃圾搜寻
在更快的模式中,基于思想:对任何活的对象,一定能最终追溯到其活在堆栈或静态存储区的引用。如果从堆栈或者静态存储区开始,遍历所有引用,就能找到所有活的对象。
(3.2.2)垃圾处理
Java虚拟机将采用一种自适应的垃圾回收技术。至于如何处理找到的存活对象,取决于jvm实现。
方式一:
有一种方法叫"停止-复制(stop-and-copy)",即暂停程序,将当前存活对象从当前堆复制到另外一个堆,未被复制的就是垃圾。复制到新堆之后,对象时连续紧凑挨着的。搬移之后,所有对象的引用将被修正。
此种方法,称为“复制式回收器”,效率会降低:首先,需要两个堆,需要多一倍的空间;其次,在于复制,若程序进入稳定器不产生垃圾,来回复制则十分浪费。
方式二:
为避免以上情况,一些jvm会进行检查:若未产生新垃圾,则转换工作模式(自适应)为"标记-清扫(mark-and-sweep)"。在垃圾比较少时,此种方式就很快了。
(3.2.3)jvm中,内存以"块"进行划分,每个块有相应的代数(generation count)。若有块,在停止-复制模式下,垃圾回收器可以将对象往废弃的块里拷贝对象。若块被引用,则其代数会增加。
垃圾回收器将对上次回收动作之后新分配块进行整理。这对处理大量短命临时对象很有帮助。jvm会进行监视,若对象都很稳定,则会切换至mark-and-sweep模式;若内存碎片过多,则会切换至"stop-and-copy",这就是自适应技术。
称为"自适应的、分代的、停止复制、标记清扫"式垃圾回收器。
(4)成员初始化
初始化成员变量时,注意不可向前引用为初始化的变量。例如,下面是不对的:
class A
{
int i = a; //此时a尚未初始化
int a = 2;
}
类的初始化顺序如下:
在首次调用类的静态方法、静态成员、主动加载类 或 执行构造类的实例时,其内部的静态成员被初始化(有且只有一次):
先按照从上到下的顺序初始化静态变量,然后执行静态块;
在实例化某个类时,其内部普通全局变量被初始化:
先按照从上到下顺序初始化全局变量,然后执行构造方法。
(5)Java中的数组
不同于C中的数组,Java中的数组可以在运行时检查到越界行为。因为在Java数组设计时,为其固定分配了一个固有成员:length。
运行时,java自动检查length与访问index的大小关系,判断是否出现越界,若是,则抛出异常。
Java中的数组,在构造之后,会被自动初始化为空值。
若想打印数组,可以执行Arrays.toString(a);
(6)可变参数列表
所谓可变参数列表,语法如下:
void f(String... strArray){;}
其本质是一个数组,可用for(String arg : strArray); 的方式访问其内容。
(7)方法重载下的访问选择
若存在方法重载(多个方法名称一致,参数不同),优先按照数据类型完全一致的匹配调用;
若不存在一致的,java会利用自动包装机制寻找相近的方法调用;
Chapter6 访问权限控制
(1)包
包(package),将不同生产商的代码分离开来。通过import,可将包导入。
若要访问jar文件,需要将其完整路径放在CLASSPATH中。执行import时,将从CLASSPATH下查找相应class文件。
包的名称应与路径相对应,例如:com.google.vector 应在com/google/vector.class
通过import,可实现类似C语言中的条件编译功能。
(2)访问权限修饰词
相同路径下未指明包名的类,默认在相同的默认包中;
class未加任何权限修饰词,默认为包访问权限(同包可访问);
(3)类的访问权限
权限的设定有以下限制:
a.类中的方法访问权限 <= 类的访问权限。
b.每个编译单元(文件)都只能有一个public类。
c.public类的名称必须与编译单元的文件名相匹配。
Chapter7 复用类
(1)继承
通过继承父类得到的子类,可以复用父类所有的public/protected接口。
a.继承的本质:
子类实例化得到的对象中,其内部隐藏了一个基类的子对象(包装在内部)。
b.子类的初始化
子类在执行构造方法时,若子类内部未主动调用父类的构造方法,则java会使其自动调用父类的默认构造方法。
从而可以保证,任何时候,基类对象都要先于子类对象创建。
此时,父类和子类的初始化顺序为:
父类静态全局变量-->父类静态块-->父类普通全局变量-->父类构造方法-->
子类静态全局变量-->子类静态块-->子类普通全局变量-->子类构造方法
(2)代理
代理是对象之间除了组合与继承之外的第三种关系。
代理:将A类的对象放在B类中,且让B对外暴露于A相同的接口,则B即A的代理。
通过代理,可实现一些额外功能的封装:例如远程调用的代理(RPC),大型对象的创建(网络图片的加载代理),访问代理(控制模块的访问权限)...
(3)继承下的清理工作
每个类可实现一个方法dispose(),其内部释放一些必要的外部资源(Socket/FileStream/Handle/other os resources...)。
与类的构建不同,dispose()清理的顺序与构造顺序完全相反:
因为后初始化的成员(B)可能对先初始化的成员(A)有依赖,若先释放A,可能会对B产生影响。因此,为了避免这种影响,要先释放后初始化的成员B。即释放与初始化顺序相反。
(4)Protected关键字
Java中,protected修饰的成员,为子类和包内可访问。
(5)final关键字
final可以修饰字段、方法以及类,详细如下:
a.final修饰字段,字段具有值不可修改的特性。
若在字段属于基本类型,且在定义处直接赋固定值,那么属于编译时常量,在运行时不可修改。
若在定义时为给出赋值,那么在运行时第一次赋值之后,不可再次赋值,属运行时常量。
需要注意的是,若final字段为对象,那么字段本身不可指向其他对象,但对象内部字段修改不受限制。
例如 final Animal animal = new Cat();则animal变量不可赋值成其他对象,但是animal对象的字段可以修改(eg. animal.name = "Tom",是没问题的)
b.final修饰方法,那么方法在子类中不可重写。
一方面,限制了继承下的方法重写;
另一方面,可建议编译器将方法转换为内嵌方法,提升方法运行效率。
c.final修饰类
final修饰类时,那么类不可再被继承。
Chapter8 多态
(1)方法绑定
将一个方法调用和一个方法主体关联起来被称为绑定。
若在程序执行前进行绑定,称为前期绑定;为了解决向上转型之后的方法调用问题,解决办法就是后期绑定,即在运行时根据对象的类型进行绑定。
Java中,除了static和private方法之外,其他方法都是后期绑定。
缺陷:全局变量与static方法不支持多态,仅非private非静态方法支持多态。
若在构造方法中调用其他方法,会有多态效果,可能会有意外(子类尚未初始化,调用方法效果存在意外)。
(2)向上转型与向下转型
向上转型时,类型安全,但是可能会丢失子类的行为;
向下转型时,可能会产生类型转换异常;
Chapter9 接口
(1)抽象类与抽象方法
抽象类,是包含抽象方法的类,用abstract修饰。
而抽象方法,只是一个抽象的方法定义,无具体实现。
抽象类不可被实例化,子类若非抽象类,需实现抽象父类中的所有抽象方法。
需要注明的是,抽象类除了包含抽象方法和不能实例化,其他的特性和普通类无异(可以有属性定义,普通方法实现...)。
(2)接口
interface关键字定义接口,产生一个完全抽象的类,没有提供任何具体实现。定义的方法默认public。
接口中可以包含域,但是默认都是static final。
同一个类,可以实现多个不同的接口,但不能继承自多个类。
(3)通过继承来扩展接口
虽然子类不可继承自多个父类,但是接口可以继承自多个父接口,以实现接口的扩展。
(4)接口的意义
接口是对类功能的抽象,可以对类的不同功能进行分别设计。同时,面向接口编程可以降低模块间的耦合度,实现模块间的弱耦合,提升代码的可扩展性。
Chapter10 内部类
(1)内部类与外部类的关系
内部类(InnerClass)是定义在一个类内部的类(EnclosedClass), 创建内部类时,需要确保有对应外部类示例与之关联。
eg.
class Outer { Inner inner = new Inner(); class Inner { } }
在Outer内部,可以随意创建Inner实例。而在外部调用,需按如下语法:
Outer outerInstance = new Outer();
Outer.Inner inner = onterInstance.new Inner();
即,内部类实例化依赖外部类对象,这种语法确保每个内部类Inner实例都一定有外部类Outer实例与之关联。
而内部类中,可以直接获取到与之关联的外部类实例:
class Outer { Inner inner = new Inner(); class Inner { Outer outer = Outer.this; } }
即Inner中通过Outer.this,即可获取到与之关联的外部类实例。
内部类中,不能包含static成员与数据。
(2)内部类与外部类之间的访问权限
内部类中,可以访问到外部类的成员(因为内部类在外部类内部)。
依据内部类种类,内部类可以获取到外部类中的不同内容:
a. 普通内部类
class Outer { public int a; protected int b; private int c; int d; class Inner { void print() { System.out.println(a); System.out.println(b); System.out.println(c); System.out.println(d); } } }
普通内部类中,可以访问到Outer中所有全局变量。
b.局部内部类
class Outer { public int a; protected int b; private int c; int d; void check() { int f=0; class Inner { void print() { System.out.println(a); System.out.println(b); System.out.println(c); System.out.println(d); System.out.println(f); } } } }
局部内部类中,除了可以访问到Outer的全局变量,同时还可访问所在局部方法中相应的变量。
c.匿名内部类
class Tmp { void tmp(); } class Outer { void check() { return new Tmp() { void tmp(){...} }; } }
匿名内部类对外部类的成员访问权限,类似以上两种。
(3)嵌套类
内部类定义中加上static,即是嵌套类。
嵌套类与外部类之间无实例关联关系,可以在外部直接: Nested nest = new Outer.Nested();
嵌套类可专门用来写测试方法:
class Outer { void check(){;} static class Nested { public static void main(String[] args) { Outer outer = new Outer(); outer.check(); } } }
嵌套类的编译class文件专门用Outer$Nested.class标识。
---持续更新---