准备:
java编程思想电子版
别人整理的思维导图
前言
适用范围:Java SE5/6 版本。
Java的设计目标是:为程序员减少复杂性,缩短代码的开发时间,跨平台复用。
学习方法:一模式或一节点就进入一练习,思维与实践并行,现学现卖。
每当我认为我已经理解了并发编程时,又会有新的奇山峻岭等待这我去征服。——作者都花了好几个月写并发这一篇章并发出这样的感慨,我们又有什么理由妄自菲薄呢。
绪论
学习语言时:需要在头脑中创建一个模型,以加强对这种语言的深入理解;如果遇到了疑问,就将它反馈到头脑的模型中并推断出答案。
一、对象导论
1 知识
人们所能够解决的问题的复杂性直接取决于抽象的类型和质量。类型即指所抽象的是什么,也可以说用的是什么类型的语言。Java,C ,汇编,Python等。其中想C或者汇编要基于计算机的结构来求解问题,面向过程;而Java 等面向对象的语言是基于问题来求解,面向对象。
面向对象的5个基本特性:
- 万物皆对象。抽象
- 程序是对象的集合,它们通过发送消息来告知彼此所要做的。对象之间的方法调用
- 每个对象都有自己的由其他对象所构成的存储。封装
- 每个对象都拥有其类型。class,继承
- 某个特定类型的所有对象都可以接受同样的消息。多态
类实际上就是一个数据类型,程序员根据需求,通过添加新的数据类型(class)来扩展编程语言,而不需要像面向过程语言那样只能使用现有的用来表示机器中的存储单元的数据类型。
类创造出来的对象就是服务提供者(好处:有助于提高对象的内聚性,或者说通过设计模式的六大原则来设计对象提供服务),通过将问题的解决分解成对象集合的方式去调用(现有类库)和设计创建对象。
访问控制的原因:
- 让调用者无法触及他们不应该触及的部分,且通过控制符可以让调用者很容易地区分哪些东西对他们很重要(public),哪些是可以忽略的(private)。
- 允许类或库设计者可以改变类内部的工作方式而不用担心会影响到调用方。
子类通过添加新方法(is like a 关系 )和覆盖父类方法(is a 关系)来改变与父类的差异。(但要满足里式替换原则)
单(跟)继承的好处:
- 确保所有对象都属于同一个基本类型。
- 保证所有对象都具备某些功能。
- 极大简化参数的传递。(如参数可以直接放Object对象类型?)
- 使垃圾回收器的实现变得容易得多。
使用不同的容器选择点是:
- 不同容器提供了不同类型的接口和外部行为。
- 不同的容器对于某些操作具有不同的效率。(例如:ArrayList查找快,增删慢(相对于LinkedList);LinkedList查找慢,增删快)
最后,最好要对其他语言(如python)有个清晰的认识,java语言是否符合项目的设计及未来的发展需要。
2 疑问
什么是CGI?
CGI(Common Gateway Interface)公共网关接口,是外部扩展应用程序与 Web 服务器交互的一个标准接口。其实就是我们通常说的接口。
3 思想总结
面向对象语言JAVA的特性是:抽象、封装、继承、多态。使用面向对象语言可以让我们更好的面向问题来设计解决程序,也更容易读懂和维护该程序的代码,相对于面向过程语言来说。
二、一切皆对象
1 知识
Java通过操作引用来操纵对象,就像我们用遥控器来操纵电视机一样。
5个不同的地方存储数据
- 寄存器。最快,因为它在处理器内部。
- 堆栈。速度仅次于寄存器,位于RAM,必须知道确切的生命周期来移动堆栈指针分配和释放内存。基本数据类型和对象引用存储在堆栈中。
- 堆。通用内存池也位于RAM。堆不同于堆栈的好处是不需要知道存储数据或对象的生命周期,因此分配和释放内存需要更多时间。用于存放所有的Java对象。
- 常量存储。存放在程序代码中或者只读的ROM(只读存储器),因为不变所有是安全的。
- 非RAM存储。一般是流对象和持久化对象。在程序不运行时也可以存在。
基本数据类型如下:
基本类型的值存储在堆栈中,每种类型所占存储空间大小不变,因此更具可移植性。
BigInteger(支持任意精度的整数) 和 BigDecimal (支持任意精度的定点数,货币计算)属于高精度数据类型,以方法调用代替运算符实现运算,因此运行速度较慢,是以速度换取精度的数据类型。
PS: 定点和浮点的区别
类的成员变量与局部变量是否需要初始化的区别:
类的成员变量如果是基本数据类型,即使没有初始化值,java也会确保它获得一个对应类型的默认值,防止程序错误。而java不会对局部变量进行默认初始化,如果局部变量没有初始化赋值,则会编译报错说变量没有初始化。
类的成员变量默认初始化值如下:
PS:构建程序时通过反写域名作为包名来避免名字冲突。(com.xixi)
2 疑问
什么是处理器?什么是寄存器?处理器内部结构是什么样的?为什么寄存器存储最快?
这是计算机组成原理的知识,需要补补。
堆栈通过上下移动堆栈指针来分配和释放内存,那么多线程的时候是如何分配内存的呢?
栈是线程私有的,因此多线程就有各自的栈,互不干扰。
3 思想总结
万物皆对象!
三、操作符
1 知识
别名现象是因为java操作中操作的是对对象的引用,所以会出现别名现象。注意方法参数传递是基本数据类型是值传递,对象类型是”引用传递“(如果替换整个对象没事,但如果修改对象内的属性值的话,原有对象会发生变化)。
整数乘除会直接去掉结果的小数位,不是四舍五入。
a++ :先生成值,再执行++运算。
++a : 先执行++运算,再生成值。
类对象比较时 : == 或者 != 比较的是对象的引用(即地址),而equals()比较的是内容。
普通的对象用equals()比较的还是引用,和 == 一样,要想比较内容就得重写equals()方法。
基本数据类型比较直接用 == 或者 !=
java指数的写法
//编译器通常会将指数作为双精度(double)处理
double e = 1E-43d;// = 10 -43次方
double f = 1.6E43d;// = 1.6 * 10 43次
2 疑问
Random(47)的使用?随机数怎么有效地创造及生成随机数的注意事项。
A:Random(47)使用见下:
@Test
public void randomTest(){
//如果不传参数生成随机数对象Random,则java会把当前时间作为随机数生成器的种子seed,则每一次执行
//都会产生不同的输出。而如果传入固定值的seed,则每一次输出都是可预见性的相同的值,每一次nextInt //的返回值按次序都是相同的,固定seed方便测试验证。
Random r = new Random(47);
//每次调用nextInt方法传入的参数如100表示所产生的随机数的上限,下限为0,如果不想得到0的结果可以 +1
System.out.println( r.nextInt(100)+1);//随机数小于上限且不等于
System.out.println( r.nextInt(100)+1);
System.out.println( r.nextInt(100)+1);
//Random r1 = new Random();
}
如何重写equals()方法比较内容?
单精度和双精度的区别?
8进制和16进制的写法?
@Test
public void otherTest(){
int c = 0x2f;//16进制 零x
System.out.println(Integer.toBinaryString(c));
int d = 0177;
System.out.println(Integer.toBinaryString(d));
//int e = 01987; //前面如果加了零,表示这个数是8进制的数,每位最大值不能超过7
}
java 按位操作符 & | ^ ~ 和移位操作符<< >> >>>的使用?在算法中用的是否普遍?
3 思想总结
一些基本的操作符使用。
四、控制执行流程
1 知识
无穷循环的形式
for(;;)
//或者
while(true){
//里面没有结束条件break;
}
禁止使用标签跳跃,防止滥用,程序混乱。
switch 新特性:与 enum 枚举或 String 一起使用。
吸血鬼数字解法
参考答案
@Test
public void xixueguiTest(){
int num =0;
for (int i=10;i<100;i++){
for (int j=i+1;j<100;j++){
int target=i*j;
if (target<1000||target>9999){
continue;
}
num++;
int[] targetNum = { target / 1000, target / 100 % 10, target / 10 % 100 % 10, target%10 };
int[] strNum = { i % 10, i / 10, j % 10, j / 10 };
Arrays.sort(targetNum);
Arrays.sort(strNum);
if (Arrays.equals(targetNum,strNum)){
System.out.println(target + " = " + i + " * " + j);
}
}
}
System.out.println(num);
}
2 疑问
3 思想总结
一些普通的流程控制,如 while , for , break , continue , switch
五、初始化与清理
1 知识
初始化
创建对象时通过自动调用构造器来确保初始化。
类和方法的命名:名字起的好可以使系统易于理解和修改。
方法重载或构造器重载相当于人类语言的冗余性——可以从具体的语句中推断出含义。重载的规则由参数类型,个数,顺序的不同来确定,一般不推荐顺序不同。注意,返回值的不同不能用于重载,因为有时候调用有返回值的方法并不必须要返回值,这样编译器无法区分是调用哪个。
基本数据类型的重载思想是能从一个较小类型如int自动提升至一个较大类型如double,如果要把较大类型如double转为较小类型如long则必须强转。这部分我觉得除非不得已,绝对不进行这种自动提升重载方法,不便于理解。
this关键字的使用场合:
- 只有当需要明确指出对当前对象的引用时,才需要使用this关键字。如需要返回对当前对象的引用时,
- return this.
- 将当前对象传递给其他方法时(作为参数)。
- 一个类中有多个构造器,构造器之间调用另一构造器使用this。this(a,b),this(a)。注意构造器调用必须置于第一行,因此构造器调用其他构造器一次只能调用一个,要调用多个就要构造器间嵌套调用。注意:只能构造器调用构造器,构造器禁止被其他方法调用。
- 通过构造器给类成员变量赋值,如 this.a = a;
静态初始化只有在必要时刻(类第一次加载.class文件时:一般是类对象的第一次创建或第一次直接用类访问静态数据时)才会进行。之后无论创建多少对象,静态数据都只占用一份存储区域。
PS:构造器实际上也是static静态方法。
对象的创建过程:
- java解释器查找类路径,定位如Monkey.class 文件。
- 载入Monkey.class,执行所以静态初始化动作。
- 在堆上为Monkey对象分配足够的存储空间。
- 存储空间清零,Monkey对象的所以类成员变量置为默认值,如0,false,null。
- 执行所有类成员变量的初始化动作。
- 执行构造器。
用代码块来初始化类成员变量的实例与静态成员变量的初始化差不多,代码块来初始化类成员变量的实例也在构造器之前执行,区别在于静态成员变量的初始化只有一次,而代码块来初始化非静态类成员变量在每次创建对象时都会执行。
数组初始化
编译器不允许指定数组的大小,数组的创建是在运行时刻进行的。数组的长度一旦确定则不可变。创建数组为非基本数据类型时,该数组为引用数组,数组元素存储的是引用对象的引用(地址)。
注意:通过花括号{}初始化列表时最后一个逗号是可有可无的。
@Test
public void arrayTest(){
//数组内元素的值会根据数据类型自动初始化为空值,如int为 0.
int[] a = new int[5];
Integer[] b = {1,2,new Integer(3)};
//注意构建格式 new String[]{};
String[] maomao = new String[]{"mao","mao","hong"};
}
focus: 数组使用参考
清理
垃圾回收注意事项:
- 对象可能不被垃圾回收。
- 垃圾回收并不等于”析构“(c++)。
- 垃圾回收只与内存有关。
fanalize()方法用于释放为本地方法(一种在java中调用非java代码的方式)分配的内存。
GC前,会调用finalize()方法,所以可以重写finalize()方法来验证终结条件。
System.gc():强制GC(不一定触发)
自适应垃圾回收技术思想依据:对于任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。
类的初始化顺序:
成员变量定义的先后顺序决定了变量初始化的顺序。而类成员变量初始化是最早的,然后是构造器,再然后是方法。
可变参数列表
应用场合:参数个数和类型未知时使用。
可变参数列表本质上还是数组,当我们指定参数时,编译器实际上会为什么去填充数组,所以我们可以用foreach迭代遍历。可变参数列表也可以接受其对应类型的数组当做可变参数列表。
可变参数列表不依赖于自动包装机制,实际使用的是基本数据类型,且可变参数列表可以使用自动包装机制装箱拆箱。
推荐:在使用可变参数列表时,如果有重载方法,则应该只使用一个版本类型(如下)的重载方法,而不是参数类型个数不同等重载方法。
private void printArray(int a,Object... obj){
for (Object o : obj) {
System.out.print(o);
}
System.out.println();
}
private void printArray(float a,Object... obj){
for (Object o : obj) {
System.out.print(o);
}
System.out.println();
}
枚举enum
常量命名:大写字母及下划线。
enum与switch是绝佳的组合,我们可以将enum用作另外一种创建数据类型的方式。
2 疑问
在创建一个类时,在定义时就被初始化了的String域域通过构造器初始化的String域两种方式的差异?
A: 差别在于strA一开始被初始化为"strA",而strB先被初始化为null,然后再被初始化为"strB" .
class Test{
private String strA = "strA";
private String strB;
Test(){
strB = "strB";
}
}
代码块来初始化非静态类成员变量的实例如何支撑匿名内部类的初始化?
java枚举的使用?有什么特别实用的使用技巧么?
3 思想总结
初步理解java的初始化和自动清理过程,注意数组的初始化使用,可变参数列表适用于参数个数和类型未知的场合,枚举enum用于自定义数据类型,并最好能与switch配合使用最佳。
六、访问权限控制
1 知识
访问权限控制的前景交代是:迭代,重构,需求变更等为了增加可读性、理解性和可维护性。
访问权限控制修饰词:把变动的事物与保持不变的事物区分开,是对具体实现的隐藏,即封装。(package和import)
一个类只有一个public class类,其他类都不算public 的,它们主要是为主public类提供支持的。
用导入限定的命名空间位置描述来提供一个管理名字空间的机制。
java包命名规则是全部使用小写字母连接。
有冲突名字情况下,则必须返回到指定全面的方式。
将构造器私有(private),则不能通过构造器来创建对象,而必须调用专门的如getInstance()方法来创建对象。且因为构造器是私有的,他阻止对此类的继承。
protected 修饰符处理的是继承的概念(包含包访问权限)。
类只能定义包访问权限和public权限,如果是默认的包访问权限,则该类不能被非同一个包下的类创建访问。
java比c的名字空间控制好在:
- package 定义每个类的包路径
- 包命名模式(域名反写)
- 关键字import
2 疑问
如何把类库空间置于包中?或者说设计项目时如何分配类的位置和创建包分类。
这个一般有个默认的分类。可以参考这个分类。
java如何实现条件编译?如何定义程序为调试版和发布版?现在这样定义还有价值么>
private在多线程环境下的重要性?
被private修饰的私有方法不会有线程安全问题。
3 思想总结
访问权限控制的原因有二:
- 隔离不属于客户端所需接口的部分实现封装,也使客户端能更清楚地理解哪些对他们是重要的和哪些是可以忽略的。
- 让方法或接口提供者可以安全地更改和迭代类内部的工作方式和结构。
权限是约定俗成的东西,权限的修饰符可以更好地方便于程序的理解迭代和维护。
七、复用类
1 知识
复用类的方式有二:组合与继承。
初始化类成员变量的方式有四:
- 在定义对象的地方。这样则总能在被构造器调用之前被初始化。
- 在类构造器中。
- 在使用这些对象之前,称为惰性初始化(一般是使用前判空,为空则初始化该对象)。
- 使用实例初始化。(代码块等)
继承自动得到父类中所有的域和方法,所有一般在设计继承时会将所有的成员变量(数据成员)指定为private,所有的方法指定为public。
子类调用父类方法用super关键字(如:super.action() )。
当创建一个子类对象时,该对象包含了一个父类的子对象。因此当子类创建对象时,会先去创建父类子对象,及父类对象,然后再初始化子类的成员变量,最后调用子类的构造器。(p130练习5)PS:创建父类时会调用父类构造器【注意:每次创建子类时都会去调用父类构造器创建一个父类的子对象,即每次创建子对象都会调用父类构造器创建对象,不是想static常量那样只创建一次的】,如果是默认构造器则可以不写,默认调用父类的无参构造器,如果是只有带参数的构造器,则必须在子类构造器第一行显式调用父类构造器【super(3) 】。
组合与继承
组合和继承都允许在新的类中放置子对象(成员变量),组合是显示地这么做,而继承是隐式的这么做。一般组合中的成员变量会修饰为private,为了安全而隐藏具体实现。
组合与继承的选择在于两种之间的关系是"is -a"还是 "has-a"关系。选择继承时要判断自己需不需要从新类向父类进行向上转型,需要才可以用继承。
向上转型和向下转型用于有继承结构的对象,方便一视同仁地操作对象。
final关键字
final 修饰一般表示这是无法改变的。
不可改变的理由:设计或效率。
final数据
一个既是static又是final的域只占据一段不能改变的存储空间。
对于基本类型,final使数值恒定不变,而对于对象引用(数组也是),final使引用恒定不变,但对象自身却可以被修改。因此,使引用成为final没有使基本类型成为final的用处大。
空白final:指被声明为final但又未给定初值的域。可以通过构造器初始化final域来对一个类的final域初始化值,使之可以根据对象的不同而有所不同,且又保持恒定不变的特性。
final参数:无法在方法中更改参数引用锁指向的对象或基本参数的值(PS:参数引用的对象的值还是可以更改,太鸡肋,没用)。可以读参数,无法修改参数。主要用来向匿名内部类传递数据。
final方法
使用的原因:一是锁定方法,防止被继承覆盖重写修改该方法的含义。二是效率。(在虚拟机是HotSpot已经没必要了)
类中所有private方法都隐式的指定为是final的。但因为方法是private的,所有只在该类中私有使用,不影响其他继承类也继续叫这个方法名。
final类
被final修饰的类不可被继承。final类的域是可变的,和其他的一样,除非被final修饰;而final类的所有方法都无法覆盖重写,因为final禁止继承,所以的方法都隐式指定为final的。final类的方法就无需加final修饰了。
设计类时,除非明确不想被覆盖,否则不应给类或方法加final修饰,既没有效率提高可能,又可能会妨碍其他人通过继承来复用这个类。
继承与初始化(p146案例)
一开始会先加载父类的static静态变量初始化,
然后再加载子类的static静态变量初始化,
再然后是初始化父类的成员变量为默认值,
然后是初始化子类的成员变量为默认值,
然后是调用父类的构造器,
再然后调用子类的构造器。
//典型常量定义方式:
/**
*public 公开的 ; static 静态的,只有一份 ; final 常量,不可变的
*/
public static final int VALUE_TWO = 3;
//final修饰的数据只能在运行时才能确认他的值,编译时是不能确认他的值的,从可以把随机值赋值给final域得知
static final int INT_A = rand.nextInt(20);
其他:
toString()方法会在编译器打印一个String而传递的是一个对象时自动调用该对象的toString()方法,因此如果想要使要打印日志的对象具备这样的行为时只有编写一个toString()方法,否则打印的是对象的地址类型组合信息。
每个类都可以创建一个main()方法,方便单元测试,且无需删除。即使一个程序中含有多个类,也只有命令行锁调用的哪个类的main()方法会被调用,及我们run的那个类方法。
ArrayList 代替了 Vector;
HashMap 代替了 HashTable
2 疑问
final参数主要用来向匿名内部类传递数据,对于引用参数的内部的值是可以改变的,加final有什么意义么
所以要看场合添加,不是为了防止内重写或者确定是不可变的对象特别是基本类型值时,没有必要添加。
3 思想总结
多用组合,少用继承。
设计类时要遵循单一职责,继承是要遵循里式替换原则。
final关键字主要用在设计上想得到不可变元素、方法和类上时。
设计系统时应考虑程序开发是增量过程,如图人类的学习;要有扩展性,像进化的生命体而不是想设计摩天大楼一样快速见效?
八、多态
1 知识
多态的作用:消除类型之间的耦合关系。
绑定:将一个方法调用同一个方法主体关联起来被称作绑定。
前期绑定:在程序执行前进行绑定,如面向过程语言C。
后期绑定:在运行时根据对象的类型进行绑定,如JAVA。
Java除了static、final(private也属于final)方法之外,都是后期绑定。所以当我们声明一个方法为final时,意思是关闭动态绑定。
多态的"缺陷"
- 父类中只有非private 方法才能被覆盖。否则其实在子类中名字相同的只是个全新的方法,并没有覆盖。
- 成员变量在多态时访问的是父类的成员变量值(如果成员变量相同的时候,这时候子类包含这两个名字相同的成员变量值,一个是父类的,通过super.field调用,一个是子类的,this.field),方法访问的是子类的复写方法(如果有覆盖重写的话)
- static静态方法是类方法,与对象无关,不具有多态性
在实际工作中,我们一般不会这样,一般会把成员变量都修饰为private(再通过get/set方法提供访问),且对于父类和子类的成员变量命名也不会相同,避免引起混淆。
//多态
Shape s = new Cycle();
//普通创建对象
Cycle c = new Cycle();
多态和构造器
构造器的特殊任务:检查对象是否被正确地构造。
复杂对象调用构造器的顺序:
- 在对象创建之前,将分配给对象的存储空间初始化为二进制零。
- 调用父类构造器,这个步骤会递归传递到Objcet对象。
- 按照声明顺序调用成员变量的初始化方法。
- 调用子类构造器的主体。
注意:每一个类的初始化都遵循如果第一次调用的话,会有static成员变量先初始化,然后是类的成员变量的初始化,再然后是构造器的调用。这一调用顺序在父类或者成员对象的调用中都适用。当然要注意的是如果类的static成员变量已经不是第一次初始化则不会再调用了。
PS:如果父类在初始化的构造器中调用覆盖的方法,则根据多态调用的其实是子类覆盖的方法,只是由于子类还未初始化,其中如果有成员变量的话则值为0.
编写构造器准则:
用尽可能简单的方法使对象进入正常状态;如果可以的话,构造器避免调用其他方法。构造器唯一可以安全调用的是final修饰的方法(包括private)。其他的会多态到子类上去。
协变返回类型允许我们写方法时返回更具体的对象类型。比如不是Shape而是Cycle.
通过继承表达行为间的差异,并用成员变量(相同的接口,不同的类型赋予不同的子类)表达状态上的变化。
注意:
多态时创建的对象调用只能是父类有的方法,因为多态时引用是向上转型的,子类的扩展方法会“丢失”,如果要使用子类的扩展方法则要向下转型。如果向下转型不成功(不是该类型或其父类)则会报ClassCastException(类转型异常)。
2 疑问
不同的类型修饰构造器有什么区别,一般private是不想让对象被创建,用于单例,那public、protected、和默认的使用有什么讲究么?
这个注意就是看作用的范围域了,在使用时尽可能地缩小范围,而不是一开始就使用public修饰,除非是对外提供的方法。
static可以修饰构造器么?
A:构造器就是默认的static了,所以不允许。confirmed.
不同访问权限的修饰符修饰的static成员变量、类成员变量、构造器初始化的先后顺序是按照声明顺序来的么?
如果是private static 修饰的静态类,只会在明确调用到它时才会初始化,参考单例的静态内部类写法,利用了静态内部类延迟初始化的特性。
3 思想总结
多态让程序针对一体性类型的对象统一处理有了可能,让程序的开发更加迅速,代码编写更加人性化处理,也使得扩展和更加容易。
但是也要注意多态的缺陷,那就是多态针对的是对象的公共行为,对象的静态方法和对象成员变量及私有行为(private、final)都是不能多态的。
还有,因为多态增加了对象的组织复杂和庞大,所以使用的原则是多用组合,少用继承。
九、接口
1 知识
接口和内部类(特别是匿名内部类)为我们提供了一种将接口与实现分离的更加结构化的方法。
抽象类:
它是普通的类与接口之间的一种中庸之道。特别是在不能使用纯接口的时候。抽象类适用于重构,这样我们可以将公共方法沿着继承层次结构向上移动。(PS:只有类名上修饰了abstract则不管有没有抽象方法,该类都是一个抽象类,不能被创造出对象。当然更可以全部是abstract,这种其实就是接口了。)
接口:
接口可以包含成员变量。其成员变量都是static和final的(使接口成为便捷的用来创建常量组的工具,不过如果是enum枚举类型常量的话最好还是用enum,直观好看),即静态常量;接口内所有的方法和成员变量都是public的,无论是否写public修饰。
如果接口定义时不加public修饰符(接口或者类里面的接口定义),则该接口只有包访问权限,只能在同一个包内使用。引申出接口可以嵌套在类或其他接口中。
Java通过多接口实现多重继承,其他具体实现类或者抽象类都只能单继承。
在接口和抽象类选择中尽量选择接口来设计。(作者的建议是前期如果没有必要可以直接选择设计类而不是接口,看中需要性)
在继承和实现的接口中方法名一样时,一起按照重写和重载的规则来,相同则只要有一个实现就行(或者重写),不同的则看方法签名不同实现重载。(如果只是返回值类型不同则无法重载编译器会报错无法实现)
接口配合策略模式和适配器模式使得程序更加地灵活。
2 疑问
接口中的类呢?也是常量么?
接口中可以放置嵌套类(内部类),并且是自动public
和static
的。
3 思想总结
面向接口编程才能解耦,依赖倒置。
十、内部类
1 知识
一个类的定义在另一个类的定义内部,就是内部类。
内部类的使用可以很方便地隐藏实现细节。
注意:注意是要在另一个类的内部,如果是在外面就是一个普通的类,当然该类不能是public的,因为一个类文件只能有一个public类型的类;如果是内部类的话,则可以是public的。
内部类可以直接访问其外部类的方法和成员变量,包括private等所有的方法。(这是因为内部类对象在创建时会秘密捕获一个指向外部类的引用。)
内部类还是一个完整的类,跟其他类一模一样,有类的访问权限限制等。区别在于
一:内部类依赖于外部类,因此。内部类对其外部类是完全透明可见的,包括其private的私有成员变量,外部类也能访问。
二:要注意内部的定义的作用域,超出作用域则内部类不可用。
在方法和作用域内的内部类
内部类可以定义在任何地方,包括方法的参数,方法内部(不能用private修饰),方法的作用域内(局部内部类)。
匿名类不可能有构造器。(想要用的话可以用父类的带参构造器,父类不能是接口才行,必须是普通类或者抽象类)
匿名内部类可以有字段,方法,还能够对其字段执行初始化操作。
匿名内部类使用外部定义的对象或值时,编译器要求其参数引用是final类型的。如果是通过匿名内部类的父类构造器传递参数进来的话,则不需要是final类型的,因为这个参数并不会被匿名内部类直接使用。所以加final修饰是为了保证直接使用时该内部类的成员变量不可变?
匿名内部类与正规的继承区别:
匿名内部类既可以扩展类,也可以实现接口,但不能两者兼备。如是是实现接口,也只能实现一个接口。
嵌套类
把内部类声明为static类型的我们称为嵌套类,嵌套类不需要内部类对象与其外部类对象之间有联系。
- 创建嵌套类对象,不需要外部类的对象。
- 不能从嵌套类的对象中访问非静态的外部类对象。(因为嵌套类没有保存外部类对象的引用,不需要依赖外部类)
嵌套类与内部类的区别
普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static域,也不能包含嵌套类。但是嵌套类可以有static数据和static域,也能包含嵌套类。
嵌套类是内部类的一种,其编译后的class文件还是在外部类文件中。
在接口中写的内部类因为是接口里面的,自动是public和static的,所以接口中的内部类是嵌套类。我们可以在接口中放置内部类(嵌套类)代码。可以用于创建某些公共代码,使得他们可以被某个接口的所以不同实现所共用。
内部类无论嵌套了多少层的内部类,它都能透明地访问所以它嵌入的外部类的所有成员(即使是外部类的private成员、方法)。
为什么需要内部类
内部类实现一个接口与外部类实现这个接口的区别:
外部类实现一个接口不是总能享受到接口带来的方便,有时需要用到接口的实现。(比如外部类已经有同名的方法实现了,无法重复覆盖写出想要的覆盖方法)
这时候由内部类实现接口的优势:每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
内部类使得多重继承的解决方案变得完整:内部类允许继承多个非接口类型。(类或抽象类,因为每个内部类都可以去继承不同的类,这样这个外部类就有了多种继承的能力)
实现2个接口时我们可以选择使用单一类(多实现),或者使用内部类来实现。而如果实现的是2个抽象类或者具体的类,而不是接口,就只能使用内部类才能实现多重继承。
内部类使用前提:如果不需要解决多重继承的问题,那么自然还是用别的编程方式。
使用内部类可以获得的特性:
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外部类对象的信息相互独立。
- 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。(实现不同的行为效果,有点策略模式的意思)
- 创建内部类对象的时刻并不依赖于外部类对象的创建。(通过static方法调用或者创建的是嵌套类?)
- 内部类并没有令人迷糊的"is -a"关系,他就是一个独立的实体。
主要用来响应事件的系统被称为事件驱动系统。
注意这个处理事件结合的写法,把要迭代处理的事件用一个新的集合包装,再处理完毕之后对原有的处理事件集合进行移除事件(在此期间可能会原事件集合还可能会进行添加待处理事件,所以这样处理逻辑是极好的),这样就不会影响到迭代循环的长度。
通过内部类,使变化的事务与不变的事务相互分离(模板方法)。内部类允许:
- 控制框架的完整实现是有的那个的类创建的,从而使得实现的细节被封装起来。内部类用来表示解决问题所必需的各种不同的action()。
- 内部类能够很容易地访问外部类的任意成员,所以可以避免这种实现变得笨拙。
内部类覆盖
如果两个外围类都有相同名字的内部类,而这两个外围类是继承关系的话,这时候这两个内部类是完全独立的两个实体,各自在自己的命名空间内,没有继承关系。而当这两个内部类一个明确继承另外一个的时候,则他们便有继承关系。和普通类继承没什么不同。
局部内部类
局部内部类指在代码块里面的内部类,典型如方法体的里面。局部内部类不能有访问说明符(public权限访问符),其他的和内部类一样。局部内部类与匿名内部类的使用区别:
- 我们需要不止一个该内部类的对象。
- 我们需要一个已命名的构造器,或者需要重载构造器。匿名内部类做不到,因为匿名内部类只能用于实例初始化。
如果不是这些需要,还是直接用匿名内部类好了。
其他知识点补充:
访问说明符:指的是访问权限符,如public,private。
回调:
参考
如下图所示, 回调是一种双向的调用方式, 其实而言, 回调也有同步和异步之分, 讲解中是同步回调, 第二个例子使用的是异步回调 。
回调的思想是:
- 类A的a()方法调用类B的b()方法
- 类B的b()方法执行完毕主动调用类A的callback()方法
通俗而言: 就是A类中调用B类中的某个方法C, 然后B类中反过来调用A类中的方法D, D这个方法就叫回调方法。
//创建内部类对象
Outer outer = new Outer;
Outer.Inner inner = outer.getInner();
//内部类的基本使用如下
public class Outter {
public class Inner{
public void getShow(){
//两种调用方法,第一种比较明显能展示该方法是外部类的方法
Outter.this.show();
show();
}
public Outter getOuter(){
//内部类中,this表示该内部类,Outter.this 表示内部类对应的外部类的引用对象
return Outter.this;
}
}
//静态内部类则不需要创建外部类对象,直接用类调用方式
public static class StaticInner{
public static void staticShow(){
System.out.println("staticShow");
}
}
public void show(){
System.out.println("outer");
}
public Inner getInner(){
return new Inner();
}
public static void main(String[] args) {
Outter outter =new Outter();
//内部类的创建可以如下:使用外部类的引用.new语法创建内部类对象
Outter.Inner inner = outter.new Inner();
//也可以通过外部类方法来创建一个
Inner in = outter.getInner();
in.getShow();
inner.getShow();
inner.getOuter().show();
Outter.StaticInner.staticShow();
}
}
//=====================================================
//有参匿名内部类使用方法 前提是得有普通类或者抽象类(带有参数构造器)实现。
public abstract class Fishing {
int i =0;
public Fishing() {
}
public Fishing(int i) {
this.i = i;
}
public void fish(){
System.out.println("fish"+i);
}
}
public class Human {
public Fishing getFishing(int a){
return new Fishing(a){
private String name = "maomao";
@Override
public void fish() {
super.fish();
}
};
};
}
public static void main(String[] args) {
Human human = new Human();
Fishing fishing = human.getFishing(10);
fishing.fish();
}
}
//=====================================================
//p199 用匿名内部类实现工厂方法
public class Platform {
public static void ride( CycleFacrory facrory){
facrory.getCycle().getName();
}
public static void main(String[] args) {
ride(Bicycle.facrory);
ride(Unicycle.facrory);
ride(Tricycle.facrory);
}
}
public class Bicycle implements Cycle {
//静态单例工厂创建
public static CycleFacrory facrory = new CycleFacrory() {
@Override
public Cycle getCycle() {
return new Bicycle();
}
};
@Override
public void getName() {
System.out.println("Bicycle");
}
}
//工厂接口
public interface CycleFacrory {
public Cycle getCycle();
}
//产品接口
public interface Cycle {
public void getName();
}
2 疑问
静态类里的方法没有加static修饰还是静态方法么?
A:不是,待详细解答。
为什么方法内部的内部类不能用private修饰?
匿名内部类使用外部定义的对象或值时,编译器要求其参数引用是final类型的,为什么呢?
静态类里的类(static修饰的class)和方法都没有static修饰,属于静态方法和静态常量么?
嵌套类的作用?
普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static域,也不能包含嵌套类。但是嵌套类可以有static数据和static域,也能包含嵌套类。为什么通的内部类不能有static数据和static域?
创建内部类对象的时刻并不依赖于外部类对象的创建。为什么不需要?什么时候不需要?(通过static方法调用或者创建的是嵌套类?)
闭包?闭包的作用?
回调?回头网上结合研究下
enclossingClassReference.super(); ?继承内部类时为什么要用这个?
P245 练习26里如何生成一个带参数的构造器?目前能实现的只能是带继承的内部类的外部类的引用参数,而不能创建内部类自带的参数,因为这样无法继承了。待处理?
3 思想总结
内部类的使用主要用于配合弥补接口无法达到的多重继承的时候使用,还有用于封装实现,达到解耦。
其实如非必要,在设计阶段尽量规避使用内部类实现,大部分情况下单继承及接口实现便能满足需求了。
十一、持有对象
1 知识
基本的容器使用:List、Set、Queue、Map。
数组使用的局限:数组具有固定的尺寸的局限性,使用起来不够灵活。
集合容器通过使用泛型,就可以在编译期防止将错误类型的对象放置到容器中。
如果不需要使用每个元素的索引(对索引有操作),可以使用foreach来迭代数据。
使用多态来创建容器,但是,当想要使用具体容器类的额外的功能时,就要直接创建具体类。如下:
List list = new ArrayList();
//List没有peek()方法
LinkedList linkedList = new LinkedList();
linkedList.peek();
//Arrays.asList();底层表示还是数组,因此不能调整尺寸,不能使用add()或delete(),但可以修改set()
List list1 = Arrays.asList();
//通过显示类型参数说明,告诉编译器对于Arrays.asList();产生的List类型,这样才能编译成功。
List list2 = Arrays.asList();
//数组容器的打印 必须使用方法
Arrays.toString()
//其他容器不需要,直接toString()即可(即直接传入对象就可以调用默认toString()打印容器)。
//Collection打印出来的内容用方括号括住,逗号分隔。[1,2,3,4]
//Map打印出来的内容用大括号括住,逗号分隔,键和值由等号联系。{rat=1,tiger=2,monkey=3,drogon=4}
ArrayList 和 LinkedList 都是List类型,它们都按照被插入的顺序保存元素。
HashSet、 TreeSet 、 LinkedHashSet 都是Set类型,Set的保存的值都是唯一的,存储元素方式的区别:
HashSet: 通过散列值来存储元素,使用HashSet可以最快地获取元素。
TreeSet: 按照比较结果升序来保存对象,如果存储顺序很重要用TreeSet 。
LinkedHashSet : 对存储顺序,按照被添加的顺序保存对象。
Map: 也称关联数组,像一个简单的数据库。
HashMap:提供最快的查询技术,不是按照明显的顺序保存元素。
TreeMap:按照比较结果升序来保存键。
LinkedHashMap:按照被添加的顺序保存对象,同时保留了HasMap的查询速度。
List
ArrayList :优势在随机访问元素,但是在List的中间插入和移除元素时较慢。
LinkedList :优势在与在List的中间插入和移除元素时代价较低,并提供了优化的顺序访问。缺点是在随机访问方面相对比较慢。
P256 ListFeatures.class 演示了ArrayList 的主要常用操作方法。
ArrayList的indexOf()、remove()方法针对的是对象的引用操作及对象(比较基于equals()方法),这样就不会因为相同的对象也能被remove。
ArrayList通过containsAll()方法来判断包含关系,并不会因为排序和顺序问题而不同,比较的只是元素的包含关系。
retainAll()是取交集的操作。
isEmpty()判断容器集合是否为空
clear()清除集合操作。
Pet[] pet = list.toArray(new Pet[0]);//返回一个具有合适尺寸的数组。
迭代器
//迭代器模式,不关心具体的容器集合类型
Iterator iterator = list.iterator();
//ListIterator只能用于各种List类的访问。可以进行双向移动,往前往后,且可以增删改查。
ListIterator listIterator = list.listIterator();
LinkedList
LinkedList 还添加了可以使其用作栈、队列或双端队列的方法。有些方法作用相同而名字不同,是为了在特定用法的上下文环境中更加适用(特别是在Queue)如getFirst()和element()与peek().三个都是获取第一个元素的方法,前面两个如果List为空则抛出异常,而第三个为空时返回null。
还有其他一些也是一样的。
TreeSet: 按照比较结果升序来保存对象,我们还可以对TreeSet传入我们想要的排序特性。
//不区分大小写字母排序
SortedSet set = new TreeSet(String.CASE_INSENSITIVE_ORDER);
Stack
LinkedList具有能够直接实现栈的所以功能的方法,因此可以直接将LinkedList作为栈使用。
类名之后的
Java.util.Stack 设计欠佳。
public class Stack {
private LinkedList storage = new LinkedList<>();
public void push(T v){
storage.addFirst(v);
}
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();
}
}
Set
Set不保存重复的元素。
Set最常被使用的是测试归属性,经常要询问某个对象是否在某个Set中,因此查询是Set中最重要的操作。而,使用HashSet可以最快地获取元素。
//使用contains()方法测试归属性
boolean contains = set1.contains("a");
Map
将对象映射到其他对象的能力是一种解决编程问题的杀手锏。
Map可以返回它的键Set(因为键都是唯一的),它的值Collection(因为它的值可以是相同的,所以不是set),或者它的键值对的Set(是个EntrySet)。
@Test
public void mapTest1() {
Map map = new HashMap();
map.put(Integer.valueOf(1),1);
map.put(Integer.valueOf(2),1);
boolean containsKey = map.containsKey(1);
System.out.println("containsKey="+containsKey);
boolean containsValue = map.containsValue(1);
System.out.println("containsValue="+containsValue);
Set keySet = map.keySet();
System.out.println("keySet="+keySet);
Collection values = map.values();
System.out.println("values="+values);
Set> entrySet = map.entrySet();
System.out.println("entrySet="+entrySet);
}
Queue
队列是一个典型的先进先出(FIFO)容器。
队列在并发编程中特别重要,因为它可以安全地将对象从一个任务传输给另一个任务。
LinkedList 可以作为Queue的一种实现,因为它实现了Queue接口。
queue.offer()方法将一个元素插入到队尾。
peek()和element()在不移除的情况下返回队头。peek()在队列为空是返回null,element()则抛异常。
poll() 和 remove() 移除并返回队头。poll()在队列为空是返回null,remove()则抛异常。
PriorityQueue
优先级队列的下一个弹出元素是最需要的元素(具有最高的优先级)。
PriorityQueue 确保当我们调用peek()、poll() 和 remove()时,获取的元素是队列中优先级最高的元素。
如果需要,可以通过提供自己的Comparator来修改这个顺序。
@Test
public void queueTest() {
Queue queue = new LinkedList();
String[] split = "i am iron man".split(" ");
for (int i = 0; i < split.length; i++) {
queue.offer(split[i]);
}
while (queue.peek() != null) {
//System.out.print(JSON.toJSONString(queue.remove()));
System.out.print(queue.remove()+" ");
}
}
@Test
public void queueTest() {
Queue queue = new LinkedList();
String[] split = "i am iron man".split(" ");
for (int i = 0; i < split.length; i++) {
queue.offer(split[i]);
}
while (queue.peek() != null) {
//System.out.print(JSON.toJSONString(queue.remove()));
System.out.print(queue.remove()+" ");
}
java.util.List list = Arrays.asList(19, 2, 3, 5, 7, 10);
PriorityQueue priorityQueue = new PriorityQueue(list);
//注意,必须用方法打印出来,如果只是用JSON.toJSONString(priorityQueue)是不能得到效果的,因为存储并不固定,而是通过获取元素时比较来确定优先级的
while (priorityQueue.peek()!= null){
System.out.print(priorityQueue.remove()+" ");
}
System.out.println();
//System.out.println(JSON.toJSONString(priorityQueue));
PriorityQueue priorityQueue1 = new PriorityQueue(list.size(),Collections.reverseOrder());
priorityQueue1.addAll(list);
while (priorityQueue1.peek()!= null){
System.out.print(priorityQueue1.remove()+" ");
}
// System.out.println(JSON.toJSONString(priorityQueue1));
}
我们可以通过参数化Collection来表示容器之间的共性或者实现iterator()方法来实现迭代器功能。C++没有Collection而是通过迭代器来表示容器的共性。Java都有。
foreach:
只要我们创建的类实现了Iterable接口,就能用foreach语法糖。Iterable接口包含一个能够产生Iterator的Iterator()方法。
foreach语句可以用于数组或其他实现了Iterable接口的类,但注意:数组不是一个Iterable。
其他知识:
@Suppress Warnings (unchecked):表示只有有关“不受检查的异常”的警告信息应该被抑制。这样就不会有黄色警告了。
默认的toString()方法将打印类名@散列码(例:Apple@11b86e7 。是hashCode()方法生成的无符号16进制数)。
容器结构图:
2 疑问
既然LinkedHashMap按照被添加的顺序保存对象,同时保留了HasMap的查询速度。为什么我们不用LinkedHashMap替换HashMap?其实还是有影响的?
LinkedHashMap效率肯定没有HashMap高,毕竟还要维护一个插入的先后顺序,如果不需要关心插入顺序,那用HashMap就好了,时间复杂度是O(1).
ArrayList 和 LinkedList 只是在List中插入数据快慢有区别?在默认add方法插入有性能区别么?LinkedList 缺点是在随机访问方面相对比较慢,那么迭代器顺序访问区别还很大么?
要回答这个问题需要了解ArrayList 和LinkedList 底层的数据结构。ArrayList 底层是数组实现的,因此它要随机访问一个值只要根据数组下标即可,而LinkedList底层是链表,则需要从头到尾遍历一遍去寻找才行。相对于的,链表结构增删特别是在任何位置增删都非常快,只要更换头尾节点的引用即可,而数组则在对中间位置进行插入时则会导致其他元素要进行位置腾挪。
ArrayList
和 Stack 这些后面尖括号里面的值是有区别的么?还是只是一个类型代称?
这只是泛型的一个描述,没有具体意思。
SortedSet set = new TreeSet(); SortedSet 不是 TreeSet的父类或者接口,为什么也能多态呢?
A: 是有接口继承关系的。可以在idea中右击类选择diagrams-show diagrams.查看类继承结构。
Set对于其填充的值只能add一种类型么?那为什么ArrayList可以是Objcet类型的?set不行?
public class TreeSet extends AbstractSet{
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
不是这么理解的,
Map.Entry
?map.entrySet();返回的这个entry,entrySet是个什么类型的数据结构?本质是其实就是个Map么?
Map.Entry
static class Node implements Map.Entry {}
3 思想总结
数组一旦生成,其容量或者说长度是不能改变的,这是数组使用的局限性。
Collection保存单一的元素,而Map保存相关联的键值对。
如果需要进行大量的随机访问,则使用ArrayList,如果经常从表中间插入或删除元素,则使用LinkedList。
Queue 或Stack(栈,不是java.util里面的那个),由LinkedList 提供支持。
新程序不应该使用过时的Vector、Hashtable、Stack。
总的其实只有四种容器:Map、List、Set、Queue
十二、通过异常处理错误
1、如何编写正确的异常处理程序
2、如何自定义异常。
1 知识
使用异常的好处(对编程语言来说加入异常的好处):
- 降低处理错误代码的复杂度,否则处理错误代码繁多且臃肿,影响正常核心逻辑的理解和处理。
- 只需要在一个地方处理错误(异常处理程序,catch块)。
- 将正常代码和问题处理代码相分离。
异常最重要的方面之一就是如果发生问题,它将不允许程序沿着其正常的路径继续走下去。C或 C++不行,尤其是C没有任何办法强制程序在出现问题时停止在某条路径上运行下去。
我们抛出异常时总是用new在堆上创建异常对象。异常一般会有默认构造器或者接受字符串作为参数的构造器,以便把相关信息放入异常对象的构造器;或者两种都有。
只有匹配的catch子句才能得到执行,执行后变跳出异常处理程序块,有finally则执行,否则结束。
异常处理的两种模型:
- 终止模型。将异常抛出不处理。
- 恢复模型。修正错误,重新尝试调用出问题的方法。
创建自定义异常
要自定义异常类,必须从已有的异常类继承.对异常来说,最重要的部分就是类名,异常的名称应该望文生义。如果需要自定义异常描述信息则添加有参构造器即可。
异常说明
即在方法后加上声明可能会抛出的异常,告知此方法的调用者。
public void exceptionTest() throws TestExecption {}
如果方法没有throws 异常说明,则说明此方法不会抛出任何异常(也有可能被try catch了)。
PS: 任何继承了RuntimeException的异常(包含它自己,这些称为不受检查异常,属于错误,将被自动捕获),是可以在没有异常说明的情况下被抛出的,因为这时候是运行时异常。
在定义设计抽象基类和接口时可以预留声明方法将抛出异常,方便子类和接口实现类可以抛出预先声明的异常。
我们可以在catch处理时通过捕获Exception基类来捕获所以类型的异常,Throwable 也可以。所以如果要分批catch处理,那么最好将Exception放在末尾,防止它抢先匹配捕获异常。
//会重新装填异常信息,如果调用fillInStackTrace,那么会把调用的那一行变成异常的新发生地。
//fillInStackTrace()方法在thow 异常时的Throwable构造器里面调用封装异常信息
fillInStackTrace();
异常链
异常链:在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来。
两种方式实现吧其他类型的异常链接起来:
- 继承Error、Exception、RuntimeException(主要是后面两个),实现带cause参数的构造器。
- 调用initCause()方法连接异常。
@Test
public void exceptionTest() throws Exception {
//throw new TestExecption();
try {
//throw new TestExecption("i am god");
//throw new TestExecption(new NullPointerException());
throw (Exception) new TestExecption().initCause(new IndexOutOfBoundsException());
} catch (Exception testExecption) {
//默认输出标准错误流
testExecption.printStackTrace();
//可以指定输出流——标准流
testExecption.printStackTrace(System.out);
}
}
Error用来表示编译时和系统错误,一般程序员不关心;Exception是可以被抛出的基本类型,是程序员应该关心的。
只能在代码中忽略RuntimeException及其子类的异常(因为RuntimeException是种错误,无法执行下去了),其他类型异常的处理都是由编译器强制实施的。
如果一个方法中有多个return,包括finally块也有,则最后返回的是finally块里的return.
使用try+finally则就是异常也正常执行,这种情况下可以丢失异常。
通过强制子类遵守父类方法的异常说明对象的可替换性就得到了保证。子类不能抛出大于父类异常声明的类型,最大的是Throwable。
父类抛出异常,这时子类方法可以不抛出任何异常。因为不影响已有的程序。
不要在构造器中打开资源什么的,否则构造器就算能执行finally或者catch关闭也不好。
通用的清理资源规则是:在创建需要关闭资源,清理对象之后,立即进入一个try-finally语句块。
异常匹配
抛出异常的时候,异常处理程序会按照代码书写顺序找出“最近”的处理程序。匹配处理之后将不再继续查找。catch会捕获其异常类及所有从它派生的异常。
当我们还不知道怎么handle 异常的时候把异常catch了,导致后面异常被吃了,这种是有问题的。
所有模型都是错误的,但有些是能用的。
异常处理的可选方式(重要)
被检查异常的优缺点:
优点是一次说明能增加开发人员的效率,并提高代码的质量,对小项目,小程序友好。
缺点是对于大项目来说,过多的异常类型声明及检查导致项目无法管理,开发效率下降,也不能很好的提高代码质量。
总的来说,Java的”被检测异常“带来的麻烦比好处要多。
原因是被检查异常强迫程序员在不知道该采取什么措施的时候提供异常处理程序,这是不现实的。(亚信的代码就是这样,方法里一堆的异常声明;优车就好很多,控制得很好。)
异常机制及强静态类型检查必要的原因是:
- 不在于编译器是否会强制程序员去处理错误,而是要有一致的、使用异常来报告错误的模型。
- 不在于什么时候进行检查,而是一定要有类型检查。必须强制程序使用正确的类型,置于这种强制是在编译器还是运行时并不重要。
减少编译时施加的约束能显著提高程序员的编程效率。反射和泛型就是用来补偿静态类型检查所带来的过多限制。
好的程序设计语言能帮助程序员写出好程序,但无论哪种语言都避免不了程序员用它写出坏程序。
对于被检查异常的处理方式:
- 把异常传递给控制台。就不需要写try-catch处理了。
- 把”被检查异常“ 转换为 ”不被检查异常“。方法有2:
- 即把”被检查异常“包装进RuntimeException,这样方法也不用异常声明(因为RuntimeException是不被检查异常,不需要声明或者处理)。
- 创建自己的RuntimeException子类,这样抛出的异常也是不受检查的,也不需要异常声明或者try-catch处理。
@Test
public void exceptionTest() {//不需要声明异常 throws Execption
try {
throw new TestExecption();
} catch (TestExecption e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
异常使用指南:
在恰当的基本处理问题。(即在知道该如何处理异常的情况下才捕获异常。)
解决问题并且重新调用产生异常的方法。(很少这样处理)
进行少许修补,然后绕过异常发生的地方继续执行。(一般是异常不影响流程或者可以忍受,直接忽略异常往下执行。)
用别的数据进行计算,以代替方法预计会返回的值。(也很少用到)
把当前运行环境下能做的事情进来做完,然后把相同的异常重新抛到更高层。(跟第三点差不多,这个也有用到)
把当前运行环境下能做的事情进来做完,然后把不同的异常重新抛到更高层。
终止程序。
进行简化。(如果异常模式使得问题变得太复杂,则相对恼人)
让类库和程序更安全。
2 疑问
什么叫析构函数?好像是指垃圾回收?
析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作。
为什么要用finally来关闭资源,关闭文件呢?不关闭会怎么样?
不关闭就造成文件描述符无法释放,属于一种系统文件的浪费
不关闭可能造成对文件的写入丢失,写入有可能存在缓存区,没有关闭并且没有主动flush到具体的文件上,则可能造成丢失。
如果该文件被文件锁独占,那么就会造成其他线程无法操作该文件。
Too many open files错误,操作系统针对一个进程分配的文件描述符表是有限大小的,因此打开不释放可能造成该表溢出。
try+finally 的使用场景?是对异常忽略的时候用?
- 对异常的捕获处理
- 加锁解锁操作
- 对文件或流的打开和关闭
构造器也可以抛异常?为什么要抛异常?相当于一个方法是么?
可以抛异常,构造器本质上也是一个方法,是用于对对象的构造的初始化。
3 思想总结
java异常处理机制将正常的逻辑与异常部分分开,让我们可以更专注地分别处理这两个问题。
异常处理的报告功能是异常的精髓所在,即错误日志打印或沉淀等。
一致的错误报告系统意味着我们再也不必对所写的每一段代码都质问自己是否有错误被遗漏。