>>>>封装<<<<
被隐藏(也即封装)的部分通常代表对象内部脆弱的部分,它们很容易被程序员所毁坏,因此将实现隐藏起来可以减少程序的bug。
隐藏是通过访问控制修饰符(public、protected、包访问、private)来实现的。
访问控制的第一个存在原因就是让调用者无法直接触及他们不应该触及的部分,但从另一方面来看,其实这不失为一项服务,因为他们可以很容易地看出哪些东西对他们来说很重要,而哪些东西可能不关心;访问控制的第二个存在原因就是允许库设计者可以改变类的内部的工作方式而不用担心会影响到调用者。
>>>>继承<<<<
代码复用:复用是面向对象程序设计所提供最了不起的优点之一。
最简单的代码复用就是直接调用类的方法,此外,我们还可以将该类置于某个新类中,使它成为新类的属性成员。新的类也可由任意数量、任意类型的其他对象以任意可以实现新的类中想要功能的方式所组成,这种使用现有的类合成新的类方式称为组合复用。
组合复用带来了极大的灵活性,使得它能在运行时动态的修改其实现行为,但继承并不具备这样的灵活性,因为继承是在编译时期就已经确定其行为,在运行时期是不能修改的。
继承两种实现方式,第一种方式非常直接:直接在子类中添加新的方法,即扩展父类接口。第二种方式就是子类覆写父类方法,但不新增父类没有接口。
“is-a是一个”与“is-like-a像是一个”。继承时,我们使用第一种还是第二种方式呢?这可能引起争论:继承应该只覆盖基类的方法,而并不添加在基类中没有的新方法吗?如果这样做,就意味着子类与基类是完全相同的类型,因为它们具有完全相同的接口,结果可以用一个子类对象来完全替代一个基类对象,这可被认为是纯粹替代,通常称之为替代原则,这也是一种理想的方式,我们经常称这种情况下的子类与基类的关系是“is-a是一个”;有时必须在子类型中添加新的接口,这样也就扩展了接口,这个新的类型仍可以替代基类,但是这种替代并不完美,因为基类无法访问新添加的方法,这种情况下我们可以描述为“is-like-a像是一个”关系。
>>>>多态<<<<
一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定,而向对象程序设计语言使用了后期绑定的概念。
方法的调用就是编译器将产生对一个具体函数名字的调用,前期绑定是在运行时将这个调用解析到将要被执行的代码的绝对地址。然而在OOP中,程序直到运行时才能够确定代码的地址,为了执行后期绑定,Java编译器使用了一小段特殊代码来替代绝对地址调用,这段代码使用对象中存储的信息来计算方法体的地址,根据这段特殊代码,每个对象都可以具有不同的行为表现。
在某些语言中,必须明确地声明某个方法具备后期绑定属性所带来的灵活性,如C++使用virtual关键字来实现,在默认情况下,C++不是动态绑定的,而在Java中,动态绑定是默认行为,不需要添加额外的关键字来实现多态。
>>> Java语言支持四种类型<<<
接口(interface)、类(class)、数组(array)和基本类型(primitive)。前三种类型通常被称为引用类型(reference type),类实例和数组是对象(object),而基本类型的值则不是对象。类的成员(member)由它的域(field)、方法(method)、成员类(member class)和成员接口(member interface)组成。方法签名(signature)由它的名称和所有参数类型组成;签名不包括它的返回类型。
>>>>运行Java<<<<
用javac命令编译一个打包的类时,如果没有加参数"-d"时,则编译出的类不会放在包中(即相应的文件夹中),是没有包路径的,除非用参数"-d"指定类存放的位置,–d 指示的是编译后的class文件放在哪个目录下,并且会自动创建包名文件夹。
比如现有如下类:
package a.b;
class A{}
javac A.java 时会在当前工作目录下产生一个A.class文件,不会创建包目录结构。
javac –d . A.java ,则会在当前工作目录产生 a/b/A.class 目录与文件。
所以使用javac编译时要想产生相应的类包名上当结构,则需要带上“–d .”这样的参数。
用java命令运行一个类时,如果该类是存放在包中的,则运行时一定要带上包名,并且在环境变量要有该包存放的路径。
java -classpath . a.A
注,如果用java命令运行时,没有配置classpath环境变量,则这里的classpath不能缺少。
>>>类与类之间的关系<<<
类和类、类和接口、接口和接口之间有如下几种关系:泛化关系、实现关系、关联关系(聚合、合成)、依赖关系。
l 泛化:表示类与类之间的继承关系,使用extends关键字来表示。在图形上使用虚线三角形箭头表示。
l 实现:表示类与接口之间的实现关系,使用implements关键字来表示。在图形上使用实线三角形箭头表示。
l 关联:类与类之间的联接。关联可以是双向的,也可以是单向的,双向的关联可以有两个箭头或都没有箭头。单向的关联有一个箭头,表示关联的方向。在Java里,关联关系是使用实例变量实现的。在每一个关联的端点,还可以有一个基数,表时这一端的类可以有几个实例。常见的基数有:0..1(零个或者一个实例)、0..*或者*(没限制,可以是零)、1(只有一个实例)、1..*(至少有一个实例)。一个关联关系可以进一步确定为聚合与合成关系。在图形上使用实线的箭头表示。
l 聚合:是关联关系的一种,是强的关联关系,聚合是整体和个体之间的关系。关联与聚合仅仅从Java语法是上是分辨不出的,需要考察所涉及的类之间的逻辑关系。如果不确定是聚合关系,可以将之设置为关联关系。图形是在实线的箭头的尾部多一个空心菱形。
l 合成:是关联关系的一种,是比聚合关系强的关系。它要求普通的聚合关系中代表整体的对象负责代表部分的对象的生命周期。整体消亡,则部分与消亡。图形是在实线的箭头的尾部多一个黑心菱形。
l 依赖:类与类之间的连接,依赖总是单向的。表示一个类依赖于另一个类的定义。一般而言,依赖关系在Java里体现为局部变量、方法的参数、以及对静态方法的调用。但如果对方出现在实例变量中,那关系就超过了依赖关系,而成了关联关系了。在图形上使用虚线的箭头表示。
>>>>对象存放位置与生命周期<<<<
C++创建的对象可以存放在栈、静态存储区与堆(heap)中,放在栈中的对象用完后不需手动释放,会自动销毁,但放在堆中的对象需手动释放,栈中的对象所需空间与生命周期都是确定的,堆中的对象内存分配是动态的,在运行时才知道需要多少内存以及生命周期,如果说在堆上创建对象,编译器就会对它的生命周期一无所知,C++就需要以编程的方式来确定何时销毁对象,这可能因不正确处理而导致内存泄漏,而Java则提供了自动垃圾回收机制。
Java对象的创建采用了动态内存分配策略,即创建的堆都是放在堆中的。
>>>>数据内存分配<<<<
寄存器——位于处理器内部,这是最快的存储区,大小极其有限,一般不能直接控制,但C和C++允许你向编译器建议寄存器分配方式。
堆栈——位于通用RAM(随机访问存储器)中,堆栈指针向下移动,则分配新的内存;若向上移动,则释放内存。这是一种快速有效的分配方法,速度仅次于寄存器。创建程序时,Java系统必须知道存储在堆栈内所有项的确切生命周期,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些Java数据存储于堆栈中(如对象引用),但是Java对象并不存储于其中。
堆——一种通用的内存池(也位于RAM区),用于存放所有的Java对象,堆不同于堆栈的好处是,编译器不需要知道存储的数据在堆里存活多长时间。因此,在堆栈分配存储有很大的灵活性。当然,这种灵活性导致了分配需要更多的时间,时间效率上不如堆栈。
常量存储:常量值通常直接存放到程序代码内部,这样做是安全的,因为它们永远不会被改变。
非RAM存储:数据可完全存活于程序之外,在没有运行机制时也可以存在,如持久化对象的存放。
JVM有两类存储区:常量缓冲池和方法区。常量缓冲池用于存储类名称、方法和字段名称以及串常量。方法区则用于存储Java方法的字节码。
Java字节码的执行有两种方式:
1.即时编译方式:解释器先将字节码编译成机器码,然后再执行该机器码。
2.解释执行方式:解释器通过每次解释并执行一小段代码来完成Java字节码程 序的所有操作。
通常采用的是第二种方法。由于JVM规格描述具有足够的灵活性,这使得将字节码翻译为机器代码的工作具有较高的效率。对于那些对运行速度要求较高的应用程序,解释器可将Java字节码即时编译为机器码,从而很好地保证了Java代码的可移植性和高性能。
转载C/C++内存分配:
堆和栈的区别
一、预备知识—程序的内存分配
一个由C/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
二、例子程序
这是一个前辈写的,非常详细
//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; 栈
char s[] = "abc"; 栈
char *p2; 栈
char *p3 = "123456"; "123456"在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
上面分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); "123456"放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方,即指向同一个字符串。
}
二、堆和栈的理论知识
2.1申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。
2.2
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
2.3申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
2.4申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
2.5堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
2.6存取效率的比较
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。比如:
#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。
>>>>基本类型<<<<
void属于基本类型,但只能用来修饰方法,不能用来修饰变量。
只要两个操作数中有一个是double类型的,另一个将会被转换成double类型,并且结果也是double类型;
否则,只要两个操作数中有一个是float类型的,另一个将会被转换成float类型,并且结果也是float类型;
否则,只要两个操作数中有一个是long类型的,另一个将会被转换成long类型,并且结果也是long类型;
否则,两个操作数(包括byte、short、int、char)都将会被转换成int类型,并且结果也是int类型。
Java提供了两个用于高精度计算类:BigInteger和BigDecimal,大体属于“包装器类”范畴,但都没有对应的基本类型。
BigInteger支持任意精度的整数,可表示任何大小的整数值。
BigDecimal支持任意精度的定点数,例如,可以用它进行精确的货币计算。
>>>>引用与对象生存周期<<<<
{
String s = new String("a string");
}
引用s在作用域终点就消失了,然而,s指向的String对象继续占据内存,最后由垃圾回收器回收。
>>>>方法签名<<<<
方法名和参数列表(合起来被称为“方法签名”)唯一地标识出某个方法。
>>>>static修饰字段与方法的区别<<<<
一个static字段对每个对象来说都只有一份空间,而非static字段则是对每个对象都有一个存储空间,但是如果static作用于某个方法时,差别却没有那么大,static方法的一个重要的用法就是在不创建任何对象的前提下就可以调用它,这一点对定义main()方法很重要,该方法是运行时程序的一个入口。
char c = 0xffff;//最大字符串
byte b = -0x80;//最小字节型 或byte b = (byte)0x80; 因为0x80形式为正,即第一位不是符号位而是数字位,所以超过byte范围,但-0x80却明确说明第一位是符号位,即为负,没有超过byte范围类型
byte b = 0x7f;//最大字节型
如果将比较小的类型传递给Integer.toBinaryString()方法,则该类型将自动被提升为int再执行。
如果对char、byte或short类型的数值进行移位处理,那么在移位进行之前,它们会被转换为int类型,并且得到的结果也是一个int类型的值;如果为int与long,则结果类型不变。
“移位”可与“等号”(<<=、>>=、>>>=)组合使用,此时,操作符左边的值会移动由右边的值指定的位数,再将得到的结果赋给左边的变量,但在进行“无符号”右移结合赋值操作时,可能会遇到一个问题:如果对byte或short值进行这样的位移运算时,得到的可能不是正确的结果,它们会先转换成int类型,再进行右移操作,然后被截断,赋值给原来的类型,在这种情况下可能得到-1的结果。
long l = -1;
System.out.println(Long.toBinaryString(l));// 64个1
l >>>= 10;
System.out.println(Long.toBinaryString(l));// 54个1
int i = -1;
System.out.println(Integer.toBinaryString(i));// 32个1
i >>>= 10;
System.out.println(Integer.toBinaryString(i));// 22个1
byte b = -1;
// 字节型-1提升到整型-1,所以结果为32个1
System.out.println(Integer.toBinaryString(b));// 32个1
// b先提升到int 即在原来8个1前再补24个1,然后无符号右移,再截最后8位并赋值给b
b >>>= 10;
// b会先提升为int,再运算
System.out.println(Integer.toBinaryString(b));// 32个1
b = -1;
// 不使用 >>>= 时,结果为正确,虽然在执行前b提升为int,但计算后没有截断,直接输出整型结果
System.out.println(Integer.toBinaryString(b >>> 10));// 22个1
将float或double转型为整型值时,总是对该数字执行截尾((int) 0.7结果为0),如果想要得到舍入的结果,就需要使用java.lang.Math中的round()方法(Math.round(0.7)结果为1)。
一般来说,如果在程序里使用了“直接常量”,编译器可以准确地知道要生成什么样的类型,但有时候却是不明确的,此时可以在常量后附加上一些特定类型字符来明确。
/*
* 计算表达式中如果都整型数常量,则计算过程中还是整型,这有可能引起计算
* 溢出问题,但编译与运行都不会报错,对于此种情况,在表达式中的某个常量
* 后加上L即可使表达式计算过程中以long型来计算
*/
long l = 2147483647 * 2;// -2
l = 2147483647L * 2;// 4294967294
// 编译运行都不会错
int i = 2147483647 * 2;// -2
// !! 以下编译时就出错
// short s1 = 32767 * 2;
// byte b1 = 127 * 2;
// short s2 = 16384 * 2;
// byte b2 = 64 * 2;
// short s3 = 2147483647 * 3;//乘以奇数就不行,偶数就可以?
// byte b3 = 2147483647 * 3;
// 但下面可编译运行
short s = 2147483647 * 2;// -2
byte b = 2147483647 * 2;// -2
Foreach循环可用于数组,以及实现了java.util.Iterator接口的对象。
public interface Iterable<T> {
Iterator<T> iterator();
}
如果在返回void的方法中没有return语句,那么在该方法的结尾处会有一个隐式的return,因此在方法中并非总是必须要有一个return语句。但是,如果一个方法声明它将返回void之外的其他东西,那么必须确保每一条代码路径都将返回一个值。
int i;
for (i = 0; i <= 5; i++) {
if (i == 2) {
break;//退出时i不会再递增,但continue会
}
}
System.out.println("i=" + i);//2
for(;;)与while(ture)等效。
尽管goto仍是Java中的一个保留字,但在语言中并未使用它,Java没有goto。但可使用带标签的 continue或break来完成类似的跳转操作。
带标签与不带标签的continue、break用于迭代语句时规则:
一般的continue会退回到最内层循环的开头,并继续执行循环。
带标签的continue会到标签的位置,并重新进入紧接在那个标签后面的循环。
一般的break会中断并跳出当前循环。
带标签的break会中断并跳出标签所指的循环。
在Java里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中break或continue到外层循环外。
标签只能紧跟在循环语句前(注:如果中间还有其他语句,则continue与break语句编译出错,但如果标签不应用到continue与break中,则不会有问题)
int i = 0, j = 0;
outer:
// !! 注,标签的下面不能写任何其他非迭代语句
for (; i < 5; i++) { // 死循环
inner:
// !! 注,标签的下面不能写任何其他非迭代语句
for (; j < 10; j++) {
System.out.println(("i=" + i + " j = " + j));
if (j == 2) {
System.out.println("continue");
continue;// 回到内层循环起始处继续执行内层循环,j会自动递增
}
if (j == 3) {
System.out.println("break");
// 为了下次循环不再走该分支,则要使用i递增1,因为break后j不会自动递增
j++;
break;// 跳出内层循环,回到外层循环起始处继续执行外层循环
}
if (j == 7) {
System.out.println("continue outer");
// 由于带标签的continue跳到了外层循环起始处,所以j不会自动递增,但为了
// 下一次不再走该分支,所以要手动递增1
j++;
continue outer;
}
if (j == 8) {
System.out.println("break outer");
break outer;// 当j为8时,退出内外层循环,实质上执行最后打印语句
}
for (int k = 0; k < 5; k++) {
if (k == 3) {
System.out.println("continue inner");
// j为0、1、4、5、6时分别会执行一遍
continue inner;
}
}
}
}
// 由于 break outer 跳出,所以i不会递增,最后还是2
System.out.println(("i = " + i));
switch(integer-selector){
case integer-value1: statement;break;
case integer-value2: statement;break;
case integer-value3: statement;break;
//…
default: statement;break;
}
switch语句是可用的选择数据类型有 int、char、enum。
case均以一个break结尾,这样可使执行流程跳转至switch主体的末尾。这是构建switch语句的一种传统方式,但break是可选的。若省略break,会继续执行后面的case语句,直到遇到一个break为止。注意最后的default语句没有break,因为执行流程已到了break的跳转目的地。当然,如果考虑到编程风格方面的原因,完全可以在default语句的末尾放置一个break,尽管它并没有任何实际的用处。
重载:方法名相同,参数列表不同(参数类型、个数、顺序)。
以基本类型参数重载方法时,会优先调用该类型的参数的方式,如果没有找到相同类型时,则提升最近的参数类型。
this(type x)用来在构造函数中调用其他构造函数,也只能用于构造函数中,并且只能用在第一行。
如果你的对象(并非使用new)获得了一块“特殊”的内存区域中,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。
finalize():该方法并不是用来释放由Java程序本身分配的空间,因为它有可能不被调用,由Java程序本身分配的空间会由JVM在适当的时机释放,有可能不调用,分配的空间直到程序运行完时一次性还给操作系统,那其作用是什么呢?由于在分配内存时可能采用了类似C语言中的做法,而非Java中的通常做法。这种情况主要发生在使用“本地方法”的情况下,在非Java代码中,也许会调用C的malloc()函数系列来分配存储空间,而且除非调用了free()函数,否则存储空间将得不到释放,从而造成内存泄露。当然free()是C与C++中的函数,所以需要在finalize()中用本地方法来调用它。
C++中在堆栈中创建的对象相当于局部对象(Java是不可以在栈上创建对象的),会在方法调用完毕时自动销毁对象。但如果是采用的new方式创建的对象(类似Java),那么程序员调用C++的delete操作符时(Java中没有该命令),就会调用相应的析构函数。如果没有调用delete,那么就不会调用析构函数,这样就会引起内存泄露。
>>>>可变参数<<<<
如果有多个参数,则可变参数只能放在最后,否则编译不能通过。
可变参数接收到所有参数时,实质上是一个数组,所以可变参数的所有参数都是同一类型的。
即然是可变的,当然我们也可不输出任何参数。
public static void main(String[] args) {
print("you input:", 1, 2, 3);
//与下面等效,即可变参数也可使用数组进行传递
print("you input:", new int[] { 1, 2, 3 });
}
private static void print(String msg, int... is) {
System.out.print(msg + " ");
for (int i : is) {
System.out.print(i + " ");
}
}
可变参数重载时,有时有问题,如下:
static void f(float i, Character... args) {}
static void f(Character... args) {}
因为在只传一个参数时可能引起模糊,所以这样是不允许的,如果你给这两个方法都添加一个非可变参数,就可以解决问题:
static void f(float i, Character... args) {}
static void f(char c, Character... args) {}
继 承 与 访 问 |
|
public |
protected |
缺省 |
private |
同包同类 |
Y |
Y |
Y |
Y |
|
同包子类 |
Y |
Y |
Y |
N |
|
同包不子类 |
Y |
Y |
Y |
N |
|
不同包子类 |
Y |
Y |
N |
N |
|
不同包不子类 |
Y |
N |
N |
N |
要学会把变动的代码与保持不变的代码区分开来。
如果有必要,你尽可能将一切方法都定为private。
非public类在其它包中是访问不到的。
所有默认包的类都是属于同一个包,尽管它们在不同的文件夹下面。
private,只允许本类所有对象可访问,其他任何类都不能访问,哪怕是它的子类或同一包中的类都不行。
如果一类继承自不同包中的类,则该子类不能继承与访问父类中的“包访问权限或默认访问权限”的属性与方法,只能继承与访问protected或public的属性与方法。
设计一个类时一般按照public、protected、默认、private的顺序来定义属性与方法,这样让人最先注意到公开的,当然,我们一般把接口与实现分离更好,这样用户只关心他所需要的接口,而对实现不必理会。
控制对成员的访问权限有两个原因:第一是为了使用户不必关心那此不必关心的private部分,而只关心类提供了哪些服务接口,即public修饰的部分。第二就是让类库设计者可以更改类的内部工作方式,而不必担心这样会客户程序产生重大的影响。
>>>关于protected修饰符<<<
定义规则前,我这里约定有三个类,一个是Base类,一个是Base类的子类Sub类,一个是Sub类的子类SubSub类,另一个是Other类且与Base、Sub、SubSub没有继承关系,并假设Base中有protected方法与属性,都叫YYY吧。
先看看protected规则:首先要搞清楚什么叫访问?这里在讲到的访问是有二种的:
一、就是在类中通过“XXX x = new XXX(); x.YYY;”的形式来访问(不妨叫此种形式为“外部访问”吧,此种访问形式除了可以应用到自己与子类中外,还可以应用在其他类中访问,其中XXX表示定义的类型,这里可为Base与Sub、SubSub,YYY为方法或属性);
二、就是this.YYY的形式来访问(不妨叫此种形式为“内部访问”吧,不过这种访问形式只能应用在在自己的类或是子类中)。
protected方法与属性可访问的地方有三个:
1. 在自己的类Base中:上面的“XXX x = new XXX(); x.YYY;”与“this.YYY”两种访问形式都可以访问的到自己定义的portected方法或属性;
2. 二是子类Sub、SubSub中,这要分三种访问方式:
a. 在Sub、SubSub 中的“this.YYY”内部访问形式:在此种方式形式下,不管是否重写或重新定义过父类Base中protected方法与属性,子类Sub、SubSub一定可以访问的。
b. 在Sub、SubSub 中“Base x = new XXX (); x.YYY;”外部访问形式:此种形式就不一定的能访问的到了,这要看父类Base与子类Sub、SubSub是否在同一包(注意,此时与是否重写或重新定义过这些protedted方法与属性没有关系);
c. 在SubSub 中“Sub x = new XXX (); x.YYY;” 外部访问形式:此种访问形式能否访问关键看Sub是否重写或重新定义过Base的属性与方法:
i. 如果重写或重新定义过,则看Sub与SubSub是否在同包中
ii. 如果没有,则看Base与SubSub是否在同包中
3. 在其他类Other中:此时只支持外部访问形式,不过到底是要求Other与Base同包还是要求Other与Sub同包,则要依你访问方式而定了:
a. 如果是通过父类引用“Base x = new XXX (); x.YYY;”形式来访问的,则要求Other与Base同包;
b. 如果是通过子类引用“Sub x = new Sub (); x.YYY;”形式来访问的,情况又会比较复杂了,此时关键是看子类Sub是否重写或重新定义过父类Base中的protected方法与属性:
i. 如果重写或重新定义过了,则要求Other与Sub同包即可;
ii. 如果没有重写或重新定义过了,则要求Other与Base同包即可;
规则总结肯定有遗漏的地方,不过我只想到这些,希望大家一起看看。看文字比较绕,下面来应用上面的规则看看这些实例也许会好理解一些:
package pk1.a;
public class Base {
protected int i = 1;
protected void protect() {
System.out.println("Base::protect");
}
}
package pk1.a;
import pk1.b.Sub;
public class SubSub extends Sub {
void g() {
Sub s = new SubSub();
//!! s.protect();//规则2.c.i
System.out.println(s.i);//规则2.c.ii
}
}
package pk1.b;
import pk1.a.Base;
public class Sub extends Base {
private void prt() {}
protected void protect() {
System.out.println("Base::protect");
}
void f() {
//规则2.a
this.protect();
this.i = 2;
//规则2.b
Base a2 = new Sub();
//!! a2.protect();
//!! System.out.println(a2.i);
//规则1
Sub b = new Sub();
b.protect();
b.i = 1;
b.prt();
}
}
package pk1.b;
public class SubSub extends Sub {
void g() {
Sub s = new SubSub();
s.protect();//规则2.c.i
//!! System.out.println(s.i);//规则2.c.ii
}
}
package pk1.c;
import pk1.a.Base;
import pk1.b.Sub;
public class SubSub extends Sub {
void g() {
this.protect();//规则2.a
//规则2.b
Base b = new SubSub();
//!! b.protect();
//!! System.out.println(b.i);
//规则2.b
Sub s = new SubSub();
//!! s.protect();
//!! System.out.println(s.i);
}
}
package pk2.a;
public class Base {
protected int i = 1;
protected void protect() {
System.out.println("Base::protect");
}
}
package pk2.a;
import pk2.b.Sub;
public class Other {
void g() {
//规则3.a
Base b = new Sub();
b.protect();
System.out.println(b.i);
//规则3.b.ii
Sub s = new Sub();
s.protect();
System.out.println(s.i);
}
}
package pk2.b;
import pk2.a.Base;
public class Other {
void g() {
//规则3.a
Base b = new Sub();
//!! b.protect();
//!! System.out.println(b.i);
//规则3.b.ii
Sub s = new Sub();
//!! s.protect();
//!! System.out.println(s.i);
}
}
package pk2.b;
import pk2.a.Base;
public class Sub extends Base {}
package pk3.a;
import pk3.b.Sub;
public class Base {
protected int i = 1;
protected void protect() {
System.out.println("Base::protect");
}
static protected int i_s = 1;
static protected void protect_s() {
System.out.println("Static:Base::protect");
}
void f() {
//!! Sub.i_s = 2; //规则3.b.i
Sub.protect_s();//规则3.b.ii
}
}
package pk3.a;
import pk3.b.Sub;
public class Other {
void g() {
Sub s = new Sub();
//!! s.protect();//规则3.b.i
System.out.println(s.i);//规则3.b.ii
}
void f() {
//!! Sub.i_s = 2; //规则3.b.i
Sub.protect_s();//规则3.b.ii
Base.i_s = 2;//规则3.a
Base.protect_s();//规则3.a
}
}
package pk3.b;
import pk3.a.Base;
public class Other {
void f() {
Sub.i_s = 2;//规则3.b.i
//!! Sub.protect1();//规则3.b.ii
//!! Base.i1 = 2;//规则3.a
//!! Base.protect1();//规则3.a
}
}
package pk3.b;
import pk3.a.Base;
public class Sub extends Base {
protected void protect() {
System.out.println("Base::protect");
}
static protected int i_s = 2;
void f() {
/*
* 在子类中可能通过子类类型或父类类型来来访问父类中protected静态
* 成员,而不管子类与父类是否在同一包中,或是子类重新定义了这些成员
*
* 注,在父类或子类中访问时后面的规则不再适用
*/
System.out.println(Sub.i_s);//2
Sub.protect_s();
System.out.println(Base.i_s);//1
Base.protect_s();
}
}
最后,通过上面的规我们可以很好的解释Object.clone()类似的问题:Object.clone()访问修饰符为protected,如果某个类没有重写此方法,则Object中的clone()方法除被自己与子类能调用方法外,其他不管与这个类在同一包还是不同包都是不可见的,因为未重写,还是属于Object中的方法,又Object在java.lang包中,与我们定义的包又不在java.lang包中,所以不能访问到(这也与你在在程序里定义了Object o = new Object();你还是不能在当前类中调用o.clone();一样)。所以如果要能被不同包中的非子类克隆,则需重写Object.clone()并设置访问权限为public(如果重写后还是protected,则还是只能被同一包访问)。
package a;
public class A {
protected void f() {}
void g() {}
private void p(A a) {}
private void m(A a) {
p(a);
}
public static void main(String[] args) {
A a = new A();
a.m(a);
}
}
package b;
import a.A;
public class B extends A {
protected void f() {}
private void p() {}
public void call1(B b) {
b.f();// 此种访问方式属于访问同类中方式,是没有问题的
f();// 属于访问自已的方法
// !! g();// 父类中的包访问权限方法不能被子类继承访问
b.p();//在同一类中是可以访问私有方法的,访问控制是针对整个类来说的,而不是某个对象
}
public void call12(A a) {
/*
* 即使是f()是父类中的protected方法,并且子类还重写过了 ,但
* 是这种以父类类型实例访问父类中的protected方式属于不同包非
* 子 类访问方式,所以不能访问,即使访问的代码在子类中!
*/
// !! a.f();
A a1 = new B();
// 此种访问与上面是一样也不能编译
// !! a1.f();
}
public static void main(String[] args) {
B b = new B();
b.f();
A a = new B();
// 与上面 call2方法中的调用一样,也是不能访问的
// !! a.f();
}
}
代码复用可分为组合复用与继承复用。
属性成员初始化的4个时机:定义时、构造器中、块中、使用时。
main方法所在的类不一定要是public的(但方法一定要定义成public,如果不是public方法,则启动时会报找不到main方法)为包访问也是可以的,照样可以用作程序入口。另外,main方法也可以由另一个main方法来调用,这也是允许的。
继承并不只是复制基类的接口,当创建了一个子类的对象时,该对象包含了一个基类的子对象,这个子对象与你用基类直接创建的对象是一样的,二者区别在于,后者来自于外部,而基类的子对象被包装在子类对象内部。
当我们亲自处理清理时,要加以小心,因为,一旦涉及垃圾回收,能够信赖的事就不会很多了,垃圾回收器可能永远也无法被调用,即使用被调用,它可能以任何它想要的顺序来回收对象。最好的办法是除了内存以外,不能依赖垃圾回收器去做任何事,如果需要进行清理,最好编写你自己的清理方法,但不要使用finalize(),最好是在finally块调用自己的清理方法。
组合和继承都允许在新的类中放置子对象,组合是显示地这样做,而继承则是隐藏地做。
组合技术通常用于想在新类中使用现有类的功能而非它的接口这种情形。即,在新类中嵌入某个对象,让其实现所需要的功能,但新类的用户看到的只是为新类所定义的接口,而非所嵌入对象的接口,一般需在新类中嵌入一个现有类的private对象。
向上转型是从一个较专用类型向较通用类型转换,所以总是很安全的。向上转型的过程中,类接口唯一可能发生的事情是丢失方法,而不能访问它们。
到底是该用组合还是用继承,一个最清晰的判断办法就是问一问自己是否需要从新类向基类进行向上转型。如果必须向上转换,则继承是必要的,但如果不需要,则应当好好考虑自己是否需要继承。
使用static与final域只占据一段不能改变的存储空间。
final数据在编译时可能还不能确定其值,比如值是通过某个方法计算才能得到的,需在运行时才能确定。
对于编译期常量这种情况,编译器可以将该常量值代入任何可能用到它的计算式中,也就是说,可以在编译时执行计算式,这可减轻一些运行时的负担。在Java中,这类常量必须是基本数据类型。
在Java早期实现中,如果将一个方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。当编译器发现一个final方法调用命令时,它会跳过插入程序代码这种正常方式而执行方法调用机制,并且以方法体中的实际代码的副本来替代方法调用,这将消除方法调用的开销。当然,如果方法本身很大,这种效率提升不是很明显,反而可能下降。在最近的Java版本中,虚拟机可以探测到这些情况,并优化去掉这些效率反而降低的额外的内嵌调用,因此不再需要使用final方法来进行优化了,事实上,这种做法正在逐渐受到劝阻,在使用JavaSE5/6时,应该让编译器和虚拟机去处理效率问题,只有在想要明确禁止覆盖时,才将方法设置为final。
类中所有的private方法都隐式地指定为final,可以对private方法添加final修饰词,但这并不能给该方法增加任何额外的意义。
final类中定义的属性成员可以是final的,也可以不是的。但是,所有final类的中方法默认都是final的,因此在final类中的方法前添加final修饰符没有什么意义。
有关final、finally、finalize区别请参考XXX
多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来,多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序。
前期绑定在编译时期(如C),后期绑定是在运行时期(即多态,C++与Java),多态得程序的后期扩展。
Java中除了static方法和final方法(private方法属于final方法),其他所有的方法都是后期绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定——它会自动发生。
final方法会关闭动态绑定。
类的构造方法隐式的为static,它们实际上是static方法。
如果父类构造函数抛出异常,子类构造函数一定要抛出,不能被捕获。
覆写/重写父类方法时,子类方法异常处理有以下几种:
import java.io.IOException;
public class Parent {
public void overwrite(int i) throws IOException {}
public void overwrite() throws NullPointerException {}
}
import java.io.ObjectStreamException;
class SubSub extends Parent {
/*
* 父类抛出捕获型异常,子类却抛出运行时异常,这是可以,因为
* 抛出运行时就相当于没有抛出任何异常
*/
public void overwrite(int i) throws RuntimeException {}
/*
* 如果父类抛出的是非捕获型异常,则子类可以抛出任意非捕获型异
* 常,没有扩大异常范围这一问题
*/
public void overwrite() throws RuntimeException {}
}
class Sub2 extends Parent {
//也可以不抛出任何异常
public void overwrite(int i) {}
/*
* 如果父类抛出的是非捕获异常,子类也可以不用抛出,这与父类为捕
* 获型异常是一样的
*/
public void overwrite() {}
}
class Sub3 extends Parent {
//如果抛出的是捕获型异常,则只能是IOException的子类
//!!public void overwrite(int i) throws ObjectStreamException{}
/*
* 如果父类抛出的是非捕获异常,子类就不能抛出任何捕获型异常,因
* 为这样会扩大异常的范围
*/
//!! public void overwrite() throws IOException {}
}
组合更加灵活,因为它可以动态选择类型,运行时可以动态的修改具体的行为,而继承在编译时就知道确切类型。“用继承表达行为间的差异,并用字段(组合)表达状态上的变化。”下面实例中,两者都用到了:通过继承得到了两个不同的类,用于表达act()方法的差异;而Stage通过运用组合使用自己的状态发生变化。
//演员
class Actor {
public void act() {}
}
//继承(is-a关系):HappyActor(喜剧演员)确实是一种Actor(演员)
class HappyActor extends Actor {
public void act() { System.out.println("HappyActor"); }
}
//继承(is-a关系):HappyActor(悲剧演员)确实是一种Actor(演员)
class SadActor extends Actor {
public void act() { System.out.println("SadActor"); }
}
//舞台
class Stage {
private Actor actor = new HappyActor();//组合(has-a关系):舞台上有演员
public void change() { actor = new SadActor(); }
public void performPlay() { actor.act(); }
}
public class Transmogrify {
public static void main(String[] args) {
Stage stage = new Stage();
stage.performPlay();
stage.change();//运行过程中改变具体性为
stage.performPlay();
}
} /* Output:
HappyActor
SadActor
*///:~
private方法不能被覆写,即使子类覆写也不会起作用,根据调用它的引用类型来调用相应的方法,实质上private方法就是final方法,所以在编译期就已确定调用哪个方法。
public class PrivateOverride {
private void f() { System.out.println("private f()"); }
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride {
public void f() { System.out.println("public f()"); }
} /* Output:
private f()
*///:~
只有非静态的方法才有可能构造多态,属性成员(即使是public)与静态方法不会造成多态,即使用子类重写了这些属性成员与静态方法,因为这此是在编译期进行解析的,而不是在运行时间确定的。因此在调用属性成员与静态方法时只与调用它的引用类型相关。
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(); // Upcast
System.out.println("sup.field = " + sup.field + ", sup.getField() = "
+ sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = " + sub.field + ", sub.getField() = "
+ sub.getField() + ", sub.getSuperField() = "
+ sub.getSuperField());
}
} /*
* Output:
* sup.field = 0, sup.getField() = 1
* sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
*/// :~
上面实例中,为Super.field和Sub.field分配了不同的存储空间。这样,Sub实际上包含两个称为field的属性成员:它自己的和它从Super类中得到的。因此在子类中要得到父类中的同名属性成员时,则要使用super.field来获取。
class StaticSuper {
public static String staticGet() {
return "Base staticGet()";
}
public String dynamicGet() {
return "Base dynamicGet()";
}
}
class StaticSub extends StaticSuper {
public static String staticGet() {
return "Derived staticGet()";
}
public String dynamicGet() {
return "Derived dynamicGet()";
}
}
public class StaticPolymorphism {
public static void main(String[] args) {
StaticSuper sup = new StaticSub(); // Upcast
System.out.println(sup.staticGet());
System.out.println(sup.dynamicGet());
}
} /* Output:
Base staticGet()
Derived dynamicGet()
*///:~
如果自己处理清理动作,则销毁的顺序应该和初始化顺序相反。对于字段,则意味着与声明的顺序相反(因为字段的初始化是按照声明的顺序进行的)。对于基类,应该首先对其子类进行清理,然后才是基类,这是因为子类的清理可能会调用基类中的某些方法,所以需要使用基类中的构件仍起作用而不应过早地销毁它们,这与C++中的析构函数形式是一样的。
在父类的构造函数中调用多态方法时,可能会引发问题:
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 r) {
radius = r;
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);
}
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*///:~
在构造函数内唯一能够安全调用的那些方法是基类中的final方法(当然也适用于private方法,它们自动属于final方法)。
子类覆盖父类方法时,子类的返回类型可以是父类返回类型的某个子类型,但返过来不行。
注,没有父子关系时返回类型要一样,如果允许子类返回类型是父类返回类型的子类型,则要求在J2SE5或以上版本
class Grain {}
class Wheat extends Grain {}
class Mill {
Grain p() { return new Grain(); }
Wheat f() { return new Wheat(); }
float g() { return 0;}
}
class WheatMill extends Mill {
Wheat p() { return new Wheat(); }
//子类覆盖父类方法时返回类型不能比子类宽
//!! Grain f() { return new Grain(); }
//虽然int可以隐式的转换成float,但int不是float的子类,所以编译不能通过
//!! int g() { return 0;}
}
向上转型是安全的,因为基类不会具有大于子类的接口,因此,通过基类接口发送的消息保证都能被接;由于向下转型是不安全的,所以在Java语言中,所有向下转型都会得到检查!所以即使我们只是进行一次普通宾加括弧形式的类型转换,但在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型,如果不是,运行时就会抛出一个ClassCastException的类型转换异常,这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RTTI),这种我们也可以通过编程的方式来保证,在后面反射章节我们会看到。
一个类同时继承与实现一个类与接口时,extends要在implements前。
接口中的方法前可以加abstract关键字,也可以不加,这都将是一种抽象方法。
接口中可以什么都没有,这时是一个标志性接口,如Serializable接口。
接口其实是一种很特殊的抽象类,是一种纯的抽象类。
接口中方法前不能用static关键字,所以接口中没有静态的方法。
abtract关键词不能与static,private同时使用。
接口与抽象类不能用final来修饰。
接口中的有成员都是public,即使没有明确指出。
抽象类中抽象方法前默认访问修饰符为包访问限制,而不是public,这不象接口抽象方法前一定是public。
接口中不能有具体的方法,接口中的数据成员默认为public static final,数据成员如果写上访问控制符,只能是public,接口中的方法默认为public abstract,但可以省略。
接口中的数据成员一定要初始化,因为他是final型,但也只能在定义时就初始化,不能在静态区中赋值,因为在接口中是不能定义静态与非静态块的。
>>>>完全解耦<<<<
只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。如果你想要将这个方法应用于不在此继承结构中的某个类,那么你就会发现在不修改当然程序应用的情况下很难做到功能动态的扩展。但接口可以在很大程度上具有灵活性,因此,它使得我们可以编写可复用性更好的代码。
下面是程序的第一个版本:
刚开始时没有考虑到后期会有不同Processor处理器扩展,对当然应用来说,它已经是完美的了,程序基本上已做到了可扩展,我们可以随时添加一种其它字符处理器。
//处理器类
class Processor {
public String name() {
return getClass().getSimpleName();
}
Object process(Object input) {
return input;
}
}
//大写处理器
class Upcase extends Processor {
String process(Object input) {
return ((String) input).toUpperCase();
}
}
//小写处理器
class Downcase extends Processor {
String process(Object input) {
return ((String) input).toLowerCase();
}
}
//应用
public class Apply {
public static void process(Processor p, Object s) {
System.out.println("Using Processor " + p.name());
System.out.println(p.process(s));
}
public static String s = "Disagreement with beliefs is by definition incorrect";
public static void main(String[] args) {
process(new Upcase(), s);
process(new Downcase(), s);
}
}/* Output:
Using Processor Upcase
DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT
Using Processor Downcase
disagreement with beliefs is by definition incorrect
*///:~
上面例子中,Apply.process()方法可以接受任何类型的Processor,并将其应用到一个Object对象上,然后输出结果,在本例中,创建一个能够根据所传递参数对象的不同而具有不同行为的方法被称为策略模式。这一类方法包含所要执行的算法是不变的,在这里不变的为Apply.process方法中的算法,而“策略”包含了变化的部分。策略就是传递进去的参数对象,它包含了动态执行的代码,在这里Processor对象就暗一个策略类,所有Processor类型的对象都能应用到Apply.process中。
下面我们来看看另一种与Processor功能相似的过滤器Filter(滤波器):
//波形
public class Waveform {
private static long counter;
private final long id = counter++;
public String toString() { return "Waveform " + id; }
}
//滤波器,与Processor接口相同
public class Filter {
public String name() {
return getClass().getSimpleName();
}
public Waveform process(Waveform input) { return input; }
}
//低通滤波器
public class LowPass extends Filter {
double cutoff;
public LowPass(double cutoff) { this.cutoff = cutoff; }
public Waveform process(Waveform input) {
return input; // 什么都不做,直接返回
}
}
//高通滤波器
public class HighPass extends Filter {
double cutoff;
public HighPass(double cutoff) { this.cutoff = cutoff; }
public Waveform process(Waveform input) { return input; }
}
上面是另一种处理器,与第一个版本“字符处理器”用途相当,都是用来过滤的,这就意味着我们能否重用Apply.process()代码呢?第一个版本的Processor肯定是不行的,因为Filter并非继承自Processor(假设它来自于其它厂商),因为Filter类的创建者压根不清楚你想要将它用作Processor,因此你不能将Filter用于Apply.process()方法。这主要原因是Apply.process()与Processor之间的耦合过紧,已经超出了所需要的程度,这就无法复用Apply.process()的代码。
但是,如果Processor是一个接口,那么这些限制就会变得宽松,就可以重用Apply.process()方法算法。
下面是Processor和Apply的修改版本:
//处理器接口,只要是处理器,都会遵循此接口,那么就可以重用Apply.process
public interface Processor {
String name();
Object process(Object input);
}
public class Apply {
public static void process(Processor p, Object s) {
System.out.println("Using Processor " + p.name());
System.out.println(p.process(s));
}
}
下面是重写“字符处理器”后的应用:
//字符处理器,实现处理接口
public abstract class StringProcessor implements Processor{
public String name() {
return getClass().getSimpleName();
}
//处理接口
public abstract String process(Object input);
public static String s ="If she weighs the same as a duck, she's made of wood";
public static void main(String[] args) {
Apply.process(new Upcase(), s);
Apply.process(new Downcase(), s);
}
}
//字符大写处理器
class Upcase extends StringProcessor {
public String process(Object input) {
return ((String)input).toUpperCase();
}
}
//字符小写处理器
class Downcase extends StringProcessor {
public String process(Object input) {
return ((String)input).toLowerCase();
}
}/* Output:
Using Processor Upcase
IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD
Using Processor Downcase
if she weighs the same as a duck, she's made of wood
*///:~
下面来将Filter应用到Apply.process()方法中。由于在前面Filter已经开发完毕,假如我们现在不能修改Filter的源码,在这种情况下,我们可以使用适配器设计模式:
//适配器,适配成Processor接口
class FilterAdapter implements Processor {
Filter filter;//对象适配器
public FilterAdapter(Filter filter) {
this.filter = filter;
}
public String name() { return filter.name(); }
public Waveform process(Object input) {
return filter.process((Waveform)input);
}
}
//重用Apply.process算法
public class FilterProcessor {
public static void main(String[] args) {
Waveform w = new Waveform();
Apply.process(new FilterAdapter(new LowPass(1.0)), w);
w = new Waveform();
Apply.process(new FilterAdapter(new HighPass(2.0)), w);
}
} /* Output:
Using Processor LowPass
Waveform 0
Using Processor HighPass
Waveform 1
*///:~
在这种使用适配器的方式中,FilterAdapter的构造器接受接口Filter,然后返回你所需要的Processor接口对象,另外,在FilterAdapter类中用到了代理。
将接口从具体实现中解耦使得接口可以应用于多种不同的具体实现,因此代码也就更具可复用性了。
>>>>Java中的多重继承<<<<
有时我们需要“一个X是一个A和一个B以及一个C”,在C++中,组合多个类的接口的行为被称为“多重继承”,这可能会出现隐藏的问题,因为每个类都有一个具体实现,但在Java中,你只能继承一个类,因些,组合多个接口,C++中的问题是不会在Java发生的。
如果一个类继承了一个类,同时实现了某个接口,父类中有一个方法与接口中的方法签名是一样,则这个类默认就已实现了这个接口,无需再实现接口中的方法,这就是默认适配器模式。
我们应该如何选择接口还是抽象类?如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类,一般接口灵活性高。
接口(interface)可以继承(extends)多个接口,但只能继承一个类。
组合多个接口时名字冲突:
interface I1 { void f(); }
interface I2 { int f(int i); }
interface I3 { int f(); }
class C { public int f() { return 1; } }
class C2 implements I1, I2 {
public void f() {}
public int f(int i) { return 1; } // 重载
}
class C3 extends C implements I2 {
public int f(int i) { return 1; } // 重载
}
class C4 extends C implements I3 {
// 重写C中的方法,实现I3接口中的方法,这里可以省去该方法
public int f() { return 1; } //重写
}
// 仅根据返回类型不同是不行的
//! class C5 extends C implements I1 {}
//! interface I4 extends I1, I3 {}
在打算组合的不同接口中使用相同的方法名通常会造成代码可读性混乱,请尽量避免这种情况。
>>>>嵌套接口<<<<
class A {
interface B {//包访问权限接口
void f();
}
public class BImp implements B {
public void f() {}
}
private class BImp2 implements B {
public void f() {}
}
public interface C {//公有访问权限接口
void f();
}
class CImp implements C {
public void f() {}
}
private class CImp2 implements C {
public void f() {}
}
private interface D {//私有接口
void f();
}
private class DImp implements D {
public void f() {}
}
public class DImp2 implements D {
public void f() {}
}
//虽然方法是公有的,但返回类型却是私有的,所以
//该方法只能被有权的对象调用它,如:receiveD
public D getD() { return new DImp2(); }
private D dRef;
public void receiveD(D d) {
dRef = d;
dRef.f();
}
}
interface E {
// 接口中的内部接口默认就是 "public":
interface G {
void f();
}
// 多余的 "public":
public interface H {
void f();
}
void g();
// 接口中的内部接口只能是public的
//! private interface I {}
//! protected interface I {}
}
public class NestingInterfaces {
public class BImp implements A.B {
public void f() {}
}
class CImp implements A.C {
public void f() {}
}
// 不能实现私有接口,即private接口不能在定义它的类外实现
//! class DImp implements A.D {
//! public void f() {}
//! }
//实现某个接口时,不一定要实现其嵌套接口
class EImp implements E {
public void g() {}
}
class EGImp implements E.G {
public void f() {}
}
class EImp2 implements E {
public void g() {}
class EG implements E.G {
public void f() {}
}
}
public static void main(String[] args) {
A a = new A();
// 不能访问 A.D:
//! A.D ad = a.getD();
// 能这样,虽然返回的是私有类型,但外界没有访问
a.getD();
//虽然a.getD()返回的是私有类型A.D,但这里
//强转成公有类型A.DImp2,这是没有问题
A.DImp2 di1 = (A.DImp2) a.getD();
// 即使A.D是public,但不能将父类型赋值给子类型,除非强转
//! A.DImp2 di2 = a.getD();
// 因为a.getD()返回的是私有类型A.D,所以不能访问
//! a.getD().f();
//这样也是可以的
((A.DImp2)a.getD()).f();
//将a.getD()返回的私有类型A.D对象交给有权使用它的对象
A a2 = new A();
a2.receiveD(a.getD());
}
}
内部类对象可以直接访问外围对象的所有成员(包括私有的),而不需要任何特殊条件,就像调用自己的方法与属性成员一样。但外围类不能直接访问内部类中的方法,除非使用内部类的实例来访问(也能访问私有的)。
内部类自动拥有对其外围类所有成员的访问权。这是如何做到的呢?当某个外围类的对象创建了一个内部类对象时,此内部对象会有一个指向外围类对象的引用,然后在你访问此外围类的成员时,就是用那个引用来选择外围类的成员,编译器会帮你处理所有的细节。注,这只限于非静态的内部类。
构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。
静态的内部类也叫嵌套类。
匿名内部类与普通的内部类继承相比是有些受限的,虽然匿名内部类既可以继承类,也可以实现接口,但是不能两者兼备,而且如果实现接口,也只能实现一个接口。
非静态的内部类中不能定义static成员,但final staitc成员是可以的。因为一个成员类实例必然与一个外部类实例关联,这个static定义完全可以移到其外部类中去。
内部(类中或接口中)接口一定static的。
接口中的内部类一定是public与static,但不一定是final(与数据成员不同),因为省略final时可以被继承
非静态的内部类里不能定义接口,因为内部接口(嵌套接口)默认为static,是无法改变的,所以内部类里的接口只能定义在静态的内部类里面。
方法与块里定义的内部类只能是非静态的,不能加static,所以局部内部类里只能定义非静态的成员。局部内部类也能直接访问外部内所有成员。
静态的内部类可以定义非静态与静态的东西。
不能在静态内部类中直接(实例化外部类再访问是可以的)访问外部类中的非静态成员与方法。而非静态内部类是可以访问外部类的静态成员。
在方法与作用域内都可定义内部类。如果一个内部类在if条件语句中定义,则不管条件是否成立都会编译成类,但这个类的使用域只限于该if语句中,if外面不能访问。
设计模式总是将变化的事物与保持不变的事物分离开,比如模板方法模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。
内部类不能被重写:父类中的内部类不能被子类中的同名内部类重写。
为什么加上final后的局部变量就可以在内部类中使用了?因为加上final后,编译器是这样处理内部类的:如果这个外部局部变量是常量,则在内部类代码中直接用这个常量;如果是类的实例,则编译器将产生一个内部类的构造参数,将这个final变量传到内部类里,这样即使外部局部变量无效了,还可以使用,所以调用的实际是自己的属性而不是外部类方法的参数或局部变量。这样理解就很容易得出为什么要用final了,因为两者从外表看起来是同一个东西,实际上却不是这样,如果内部类改掉了这些参数的值也不可能影响到原参数,然而这样却失去了参数的一致性,因为从编程人员的角度来看他们是同一个东西,如果编程人员在程序设计的时候在内部类中改掉参数的值,但是外部调用的时候又发现值其实没有被改掉,这就让人非常的难以理解和接受,为了避免这种尴尬的问题存在,所以编译器设计人员把内部类能够使用的参数设定为必须是final来规避这种莫名其妙错误的存在。
一个类对外提供一个公共接口的实现(接口与实现完全分离,而且隐藏了实现)是内部类的典型应用,以JDK Collection类库为例,每种Collection的实现类必须提供一个与其对应的Iterator实现,以便客户端能以统一的方式(Iterator接口)遍历任一Collection实例。每种Collection类的Iterator实现就被定义为该Collection类的私有的内部类。
内部类作用:
1、内部类方法可以访问该类所在的作用域中的数据,包括私有数据。
2、内部类可以对同一个包中的其他类隐藏起来。
3、当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。
4、实现多继承。
>>>内部类机制<<<
如果有如下内部类:
public class Outer {
private boolean b;
private int i;
//内部类
private class Inner {
private boolean b;
Inner(boolean b) {
this.b = b;
}
public void print() {
System.out.println(Outer.this.b & this.b);
}
}
public void f(final String str) {
class Inner {//局部内部类
public void print() {
System.out.println(Outer.this.i);
}
}
}
}
我们来使用Reflection反射外部及内部类,看看内部类是怎样访问外部类成员的:
D:\work\Test\bin>java Reflection Outer
class Outer
{
//构造器
public Outer();
//字段
private boolean b;
private int i;
//方法
static boolean access$0(Outer);// 内部类Outer$Inner通过该方法访问外部类b成员,这样就可以访问一个私有成员了
static int access$1(Outer); // 局部类Outer$1Inner通过该方法访问外部类i成员
public void f(java.lang.String);
}
class Outer$Inner
{
//构造器
Outer$Inner(Outer, boolean);// 在编译期会自动传入外部类实例
//字段
private boolean b;
final Outer this$0;//指向外部类实例
//方法
public void print();
}
class Outer$1Inner
{
//构造器
Outer$1Inner(Outer, java.lang.String);//第二个参数是引用的final类型的局部变量,也是通过构造器传入的
//字段
final Outer this$0;
private final java.lang.String val$str;//存储引用的局部final类型变量
//方法
public void print();
}
非静态内部类创建方式:
OuterClassName.InnerClassName inner = new OuterClassName().new InnerClassName();
静态内部类创建方式:
OuterClassName.InnerClassName inner = new OuterClassName.InnerClassName();
继承内部类语法规则:
class WithInner {
class Inner {}
}
public class InheritInner extends WithInner.Inner {
//! InheritInner() {} // 不能编译
/*
* 这里的super指InheritInner类的父类WithInner.Inner
* 的默认构造函数,而不是WithInner的父类构造函数,这
* 种特殊的语法只在继承一个非静态内部类时才用到,表示
* 继承非静态内部类时,外围对象一定要存在,并且只能在
* 第一行调用,而且一定要调用一下。为什么不能直接使用
* super()或不直接写出呢?最主要原因就是每个非静态的内部类
* 都会与特有的一个外围类实例对应,这个外围类实例是运行时传到内部类里去的,所以在内部类里可以直接使用那个对象(比如Outer.this),但这里是在外部内外,使用时还是需要存在外围类实例对象,所以这里就显示的通过构造器传递进来,并且在外围对象上显示的调用一下内部类的构造器,这样就确保了在继承至一个类部类的情况下,外围对象一类会存在这样一个约束。
*/
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
静态内部类里可以使用this(而不像静态方法或块里是不能使用this的),此时的this指向静态内部类,而不是外部类,下面为LinkedList类里的内部类,代表一个节点的实现:
private static class Entry {
Object element;
Entry next;
Entry previous;
Entry(Object element, Entry next, Entry previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
在非静态的内部类里访问外围类相同属性成员时,需在this前加上外围类型(采用Outer.this.XX 来访问,其中Outer为外围类的类型),一般在访问自己成员时,可以省略this前自身的类型,但默认应该是有的:
public class Outer {
private int i = 1;
public void f(){
System.out.println("f Outer.this.i=" + Outer.this.i);//1
}
private /*static*/class Inner {
private int i = 2;
public void p() {
System.out.println("p this.i=" + this.i);//2
System.out.println("p Inner.this.i=" + Inner.this.i);//2
//!!注,如果是静态的内部类时,下面语句不能编译通过,因为静态的内部类没与外部类实例关联
System.out.println("p Outer.this.i=" + Outer.this.i);//1
}
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.f();
Outer.Inner inner = outer.new Inner();
inner.p();
}
}
匿名内部类(方法或块中的内部类一样)使用外部类作用域内的局部变量需定义成final,但这个变量需在匿名内部类中直接使用,如果是作为匿名构造器的参数时不需要:
public class Wrapping {
private int i;
public Wrapping() {}
public Wrapping(int x) { i = x; }
public int value() { return i; }
}
public class Parcel {
// 可以直接内部类或匿名的内部类访问,不需要定义成final
private int ii = 1;
/*
* 这里的参数 x 不需要定义成final,因为它只是
* 作为构建匿名对象时传递的一个参数,而没有直接
* 在匿名内部类中使用
*/
public Wrapping wrapping1(int x) {
// 调用匿名内部类的基类的带参构造函数
return new Wrapping(x) { // 传递参数
public int value() {
//调用外部内的域成员时可直接调用,但加this时就需在
//this前加外部类名,因为该内部类没有定义ii
return super.value() * 47 * Parcel.this.ii;
}
};
}
/*
* 注,这里的参数 x 一定要定义成final,因为
* 它被匿名内部类直接使用了
*/
public Wrapping wrapping2( final int x ) {
final int y = 1;
return new Wrapping() {
//不管是在定义时还是方法中使用都需要定义成final
private int i=y;
public int value() {
return i * x * 47;
}
};
}
public static void main(String[] args) {
Wrapping w = new Parcel().wrapping1(10);
w = new Parcel().wrapping2(10);
}
}
匿名类中不可能有命名的构造器,因为它根本没有名字。但通过块初始化,就能够达到为匿名内部类创建一个构造器的效果,当然它受到了限制——你不能重载块方法,这不像普通内部类的构造函数:
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) {
return new Base(i) {
{ //初始化块
System.out.println("Inside instance initializer");
}
public void f() {
System.out.println("In anonymous f()");
}
};
}
public static void main(String[] args) {
Base base = getBase(47);
base.f();
}
} /* Output:
Base constructor, i = 47
Inside instance initializer
In anonymous f()
*///:~
实现接口的匿名内部类:
public class Outer {
public Comparable getComp() {
return new Comparable() {// Comparable为比较器接口
//实现接口
public int compareTo(Object o) {
return 0;
}
};
}
但要注意,匿名内部类实现一个接口时,构造时不能带参数:
interface InnerI {}
public InnerI getII() {
// 匿名内部内实现一个接口时,构造器不能带参数,
// 因为接口根本就没有构造器,更没有带参的构造器
// !!return new InnerI(int i) {};
}
}
>>>java实现多重继承<<<
java不支持多继承。即没有extends Class1,Class2的语句形式。这里的多继承是指继承类的属性和行为,并且是编译时就决定的静态行为。
广义的继承是指除了组合之外的的第二种代码复用方法,只要满足“像一个”或者“像是一个”、“里面有一个”条件都可以看做继承。
java的非静态内部类可以使用外部类的所有成员方法和变量。这给继承多个类的同名成员并共享带来可能。同时非匿名内部类可以继承一个父类和实现多个接口,因此外部类想要多继承的类可以分别由内部类继承,并进行Override或者直接复用。然后外部类通过创建内部类的对象来使用该内部对象的方法和成员,从而达到复用的目的,这样外部内就具有多个父类的所有特征。
这里的多继承可以说是外部类继承自一个内部类对象,而不是类,内部类 is in a 外部类,外部内的所有行为都是通过内部类对象动态获得的。
下面是采用组合多个内部类的方式模拟多继承的实例(基于对象层面,即组合多个内部类对象):
//手机
abstract class Mobile {
public abstract void call();
}
// MP3播放器
abstract class Mp3Palyer {
public abstract void play();
}
// 智能手机
class SmartPhone {
private Mobile mb = new SmartMobile();
private Mp3Palyer mp3 = new PhoneMp3();
public Mobile getMobile() {
return mb;
}
public Mp3Palyer getMp3() {
return mp3;
}
public class SmartMobile extends Mobile {
@Override // 不同的智能机有的call方式
public void call() {
System.out.println("Call phone!");
}
}
public class PhoneMp3 extends Mp3Palyer {
@Override // 不同的智能机有的play方式
public void play() {
System.out.println("Play music!");
}
}
}
public class MutiImpTest1 {
static void call(Mobile m) {
m.call();
}
static void play(Mp3Palyer p) {
p.play();
}
public static void main(String[] args) {
SmartPhone sp = new SmartPhone();
call(sp.getMobile());// 智能手机具有手机功能
play(sp.getMp3());// 又具有Mp3的功能
}
}
下面采用继承与匿名类的方式模拟(一个是类层面的,一个是对象层面,即外部类首先继承一个类,然后通过引用内部类对象来继承另外一个类):
//手机
abstract class Mobile {
public abstract void call();
}
// MP3播放器
abstract class Mp3Palyer {
public abstract void play();
}
// 智能手机,继承自Mobile
class SmartMobile extends Mobile {
@Override // 不同的智能机有的call方式
public void call() {
System.out.println("Call phone!");
}
// 智能手机也有播放音乐的功能
public Mp3Palyer getMp3() {
return new Mp3Palyer() {
@Override // 不同的智能机有的play方式
public void play() {
System.out.println("Play music!");
}
};
}
}
public class MutiImpTest2 {
static void call(Mobile m) {
m.call();
}
static void play(Mp3Palyer p) {
p.play();
}
public static void main(String[] args) {
// 现在智能手机即是手机类型,又具有Mp3的功能
SmartMobile sp = new SmartMobile();
call(sp);// 智能手机即是手机类型
play(sp.getMp3());// 又具有Mp3的功能
}
}
上面的多继承是站在外部类的角度来看的,即它们是通过外部类引用内部类来达到多态与复用的目的。反过来,内部类继承了一个类,同时拥有了外部类的所有成员方法和属性,我们是否可以认为内部类集成了两个类呢?——一个是类层面的,一个是对象层面的(因为非静态内部类使用前一定有外部类的对象来创建它,它持有外部类某个对象的引用)。如果外部类还继承了其他类呢?内部类还是可以访问其他类的方法与属性。现在从内部类的角度来模拟多继承:
//智能机抽象类
abstract class SmartMobile {
public abstract void call();
public void play() {
System.out.println("Play music!");
}
}
// 手机
class Mobile {
/*
* 该方法是私有的,与SmartMobile类中方法同名,所以
* Mobile不能直接继承自SmartMobile,因为重写时不能
* 缩小访问权限,所以只能使用一个内部类来重写。
*/
private void call() {
System.out.println("Call phone!");
}
public SmartMobile getSmartMobileImp() {
// 智能机的实现,好比多继承(继承外部类与SmartMobile)
return new SmartMobile() {
// 调用“继承”自外部类的相应方法来实现call
public void call() {
// 回调外部类真真的实现
Mobile.this.call();
}
};
}
}
public class MutiImpTest3 {
static void call(SmartMobile m) {
m.call();
}
static void play(SmartMobile p) {
p.play();
}
public static void main(String[] args) {
// 智能机即是手机也是Mp3播放器
SmartMobile sp = new Mobile().getSmartMobileImp();
call(sp);// 智能机即是手机
play(sp);// 也是是Mp3播放器
}
}
另外,从上面程序可看出内部类的另一作用:如果你想继承一个类或实现一个接口,但是这个接口或类中的一个方法和你构想的这个类中的一个方法的名称,参数相同,但访问权限缩小了,所以你不能直接继承与实现它,你应该怎么办?这时候,你可以建一个内部类继承这个类或实现这个接口(当然你可以修改访问权限是可以的)。由于内部类对外部类的所有内容都是可访问的,内部类可以通过调用外部类的这个方法来重写那个类或接口。上面的Mobile类中的call方法就是这种情况,所以你不能直接让Mobile去继承SmartMobile,固只能采用内部类来达到重写的目的。
>>>>闭包与回调<<<<
动态语言的闭包是一个永恒的话题。闭包在编码过程的方便和快捷使得动态语言的拥护者对它津津乐道,而静态语言特别是Java语言的扇子们会拿出匿名内部类来说Java语言也有类似的功能。
JavaScript 中闭包的产生是由于 JavaScript 中允许内部 function,也就
是在一个 function 内部声明的 function 。内部 function 可以访问外部 function 中的局部变量、传入的参数和其它内部 function 。当内部 function 可以在包含它的外部 function 之外被引用时,就形成了一个闭包。这个时候,即便外部 function 已经执行完成,该内部 function 仍然可以被执行,并且其中所用到的外部 function 的局部变量、传入的参数等仍然保留外部 function 执行结束
时的值。下面是一个例子:
function Outer(){
var i=0;
function Inner(){
alert(++i);
}
return Inner;
}
var inner = Outer();
inner();
因为函数Outer外的变量inner引用了函数Outer内的函数Inner,就是说:当函数Outer的内部函数Inner被函数Outer外的一个变量inner引用的时候,就创建了一个闭包。
闭包有什么作用:简而言之,闭包的作用就是在Outer执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回Outer所占用的资源,因为Outer的内部函数Inner的执行需要依赖Outer中的变量。
闭包是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出Java中内部类是面向对象的闭包,因为它不仅包含创建内部类的作用域的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括private成员。
C++有指针函数,可以实现回调。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。Java中没有指针,回调是通过匿名类来实现的。
以下是TIJ中的闭包与回调实例:
//具有自增能力的接口,也是回调接口
interface Incrementable {
void increment();
}
// 回调框架
class Caller {
// 回调接口对象引用
private Incrementable callbackReference;
// 动态的传进回调接口实现
Caller(Incrementable cbh) { callbackReference = cbh; }
void go() {
callbackReference.increment();//回调
}
}
//自增接口简单实现:
class Callee1 implements Incrementable {
privateinti = 0;
publicvoid increment() {
i++;
System.out.println(i);
}
}
//另一个具有类似功能的自增具体类,但功能不是Incrementable所要现实的功能
class MyIncrement {
//与类Callee1中的increment方法具有相同的签名
publicvoid increment() { System.out.println("Other operation"); }
staticvoid f(MyIncrement mi) { mi.increment(); }
}
// 如果你想实现Incrementable接口而功能又不是MyIncrement的功能实现,
// 即我们又想要MyIncrement中的increment功能实现,同时又要想实现Incrementable接口,
// 此时我们只能使用一个类内部类来实现Incrementable接口
class Callee2 extends MyIncrement {
privateinti = 0;
// 重写了父类的功能
publicvoid increment() {
super.increment();//同时又保留了父类的功能
// 以下是Callee2具体类自己的实现
i++;
System.out.println(i);
}
// 内部类,与外部类形成一个闭包,因为内部类有一个指向外围类的引用,且在内部类作
// 用域内可以调用外围类对象一切成员
privateclass Closure implements Incrementable {//实现Incrementable接口
publicvoid increment() {
// 指定调用外部类的increment方法,否则会发生递归调用
Callee2.this.increment();//闭包,可以访问其作用域以外的外围类对象信息
}
}
// 提供回调对象引用
Incrementable getCallbackReference() {
returnnew Closure();
}
}
// 调用
publicclass Callbacks {
publicstaticvoid main(String[] args) {
Callee1 c1 = new Callee1();//简单实现者
Callee2 c2 = new Callee2();//MyIncrement子类,但同时实现了Incrementable功能
MyIncrement.f(c2);
Caller caller1 = new Caller(c1);//动态传进回调对象
Caller caller2 = new Caller(c2.getCallbackReference());//动态传进回调对象
caller1.go();//开始执行回调
caller1.go();
caller2.go();
caller2.go();
}
} /* Output:
Other operation
1
1
2
Other operation
2
Other operation
3
*/
以下是孙妹妹在《JAVA面向对象编程》写的回调,基本上是模仿上面那个例子,但觉得她对回调理解的有问题,因为根本不像上面有回调框架类Caller存在,唉,国人写的书啊,这里那能看的出是回调?我咋就没有悟出来呢?我看到的只是一种闭包。请看她所列举的例子:
在以下Adjustable接口和Base类中都定义了adjust()方法,这两个方法的参数签名相同,但是有着不同的功能。
interface Adjustable{//个人认为这是回调接口
//功能:调节温度
public void adjust(int temperature);
}
class Base{
private int speed;
//功能:调节速度
public void adjst(int spped){
this.speed = speed;
}
}
如果有一个Sub类同时具有调节温度和调节速度的功能,那么Sub类需要继承Base类,并且实现Adjustable接口,但是以下代码并不能满足这一需求:
class Sub extends Base implements Adjustable{
private int temperature;
public void adjust(int temperature) {
this.temperature = temperature;
}
}
以上Sub类实现了Adjustable接口中的adjust()方法,并且把Base类中的adjust()方法覆盖了,这意味着Sub类仅仅有调节温度的功能,但失去了调节速度的功能。或以使用内部类来解决这一问题:
class Sub extends Base {
private int temperature;
// 为了防止覆盖父类的adjst,所以取了不同的名字
private void adjustTemperature(int temperature) {
this.temperature = temperature;
}
// 实现回调接口
private class Closure implements Adjustable {
public void adjust(int temperature) {
// 这里是回调?
adjustTemperature(temperature);
}
}
// 提供回调对象引用
public Adjustable getCallBackReference() {
return new Closure();
}
public static void main(String[] args) {
Sub sub = new Sub();
// 具有调节速度的功能
sub.adjst(1);
//又具有调节温度的功能,这里就是回调?
sub.getCallBackReference().adjust(2);
}
}
上面使Sub类既不覆盖Base类的adujst()方法,又实现了Adjustable接口的adjust()方法。
客户类先调用sub实例的getCallBackReference()方法,获得内部类的Closure实例,然后再调用Closure实例的adjust()方法,该方法又调用Sub实例的adjustTemperature()方法。这种调用过程称为回调(这就叫回调?不理解)。
回调实质上是指一个类尽管实际上实现了某种功能,但是没有直接提供相应的接口,客户类可以通过这个类的内部类的接口来获得这种功能。而这个内部类本身并没有提供真正的实现,仅仅调用外部类的实现。可见,回调充分发挥了内部类所具有的访问外部类的实现细节的优势。
以下回调来源于网络:
回调的基本原理跟好莱坞原则一样,Don't call me,I'll call you.
编程上来说,一般使用一个库或类时,是你主动调用人家的API,这个叫Call,有的时候这样不能满足需要,需要你注册(注入)你自己的程序(比如一个对象),然后让人家在合适的时候来调用你,这叫Callback。设计模式中的Observer就是例子:所有的观察者都需要向自己关心的主题Observable注册,然后主题在适当时机(主题类对象的属性发生变化时)通知所有订阅它的观察者并更新,其中观察者都实现了一个统一的Observer接口中的Update方法。
/**下面应用中ICallBack接口与Printer类好比是别人提供的API,*/
public interface ICallBack {//回调接口
public void print();
}
public class Printer {
ICallBack ic;
void setCallBack(ICallBack ic) {
this.ic = ic;
}
/*供外界调用,即自己提供一个接口ICallBack,由外界PrintHandler去实现,再在适当时机回头调用外界所提供的实现print方法。
我没有实现接口,但是我取得了一个实现接口的对象,而这个对象是外界类调用我的方法setCallBack()时所赋给我的,因此我可以在业务需要的地方来调用外界所提供的实现print方法
*/
void execute() {
//固定算法do some thing…
ic.print(); //抽取变化的部分,由外界去实现
//固定算法 do some thing…
}
}
/**下面是外界应用*/
public class PrintHandler {
public static void main(String[] args) {
Printer printer = new Printer();
/*注意下面的这项代码片段,它给printer对象传递了一个实现ICallBack接口的匿名类,这样Printer类的对象就取得了一个实现回调接口的类,因此Printer可以在任何时候调用接口中的方法*/
printer.setCallBack(new ICallBack() {
/* print 方法在PrintHandler类中实现,但不在PrintHandler 类对象中调用,而是在Printer类对象中调用,这就是回调*/
public void print() {
System.out.println("This is a callback");
}
});
// 这句话可以设置成当满足某条件时再执行
printer.execute();
}
}
观察者模式也符合这一种理解:请参见XXXXXXXXXXXXX
内部类生成的class文件名规则:
public class A {//A.class
class B {//A$B.class
class C {}//A$B$C.class
}
{
class B {}//A$1B.class
}
B f() {
class D {}//A$1D.class
return new B() {};//A$1.class
}
B g() {
class E {//A$1E.class
B h() {
return new B() {};//A$1E$1.class
}
}
return new B() {};//A$2.class
}
static class F{}//A$F.class
public static void main(String[] args) {
A a = new A();
System.out.println(a.f().getClass().getName());
System.out.println(a.g().getClass().getName());
}
}
>>>再论工厂模式<<<
接口与工厂模式
接口典型的应用就是多继承,而工厂方法设计模式能生成实现同一接口的对象,这与我们直接在使用的地方new某个实现对象是不同的,我们通过工厂对象上调用是业务实现对象创建方法,而该工厂对象将生成接口的某个业务实现的对象,理念上,我们的代码将完全与接口分离,这使得我们可以透明地将某个实现替换为另一个实现。下面的实例展示了工厂方法的结构:
// 业务接口
interface Service {
void method1();
void method2();
}
// 业务工厂
interface ServiceFactory {
Service getService();
}
// 业务1的实现
class Implementation1 implements Service {
Implementation1() {} // 包访问
public void method1() {System.out.println("Implementation1 method1");}
public void method2() {System.out.println("Implementation1 method2");}
}
// 业务1的工厂
class Implementation1Factory implements ServiceFactory {
public Service getService() {
return new Implementation1();
}
}
//业务2的实现
class Implementation2 implements Service {
Implementation2() {} // 包访问
public void method1() {System.out.println("Implementation2 method1");}
public void method2() {System.out.println("Implementation2 method2");}
}
//业务2的工厂
class Implementation2Factory implements ServiceFactory {
public Service getService() {
return new Implementation2();
}
}
//工厂应用
public class Factories {
public static void serviceConsumer(ServiceFactory fact) {
// 通过不同的工厂获得不同的业务实现
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(new Implementation1Factory());
// 业务实现可以透明的改变:
serviceConsumer(new Implementation2Factory());
}
} /* Output:
Implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2
*/
如果不使用工厂方法,你的代码就必须在使用处指定将要创建的Service的确切类型,以便调用合适的构造器。为什么需要这种额外的间接性呢?一个常见的原因就是想要创建框架,业务实例的生产过程对客户是透明的,而且还可以在工厂方法返回实例前对业务对象进行额外处理。
内部类与工厂模式
看看再使用匿名内部类来修改上面的程序:
interface Service {
void method1();
void method2();
}
interface ServiceFactory {
Service getService();
}
class Implementation1 implements Service {
private Implementation1() {}// 私有的,只能通过工厂方法来返回
public void method1() {System.out.println("Implementation1 method1");}
public void method2() {System.out.println("Implementation1 method2");}
public static ServiceFactory factory = // 静态成员,只需一个工厂即可
new ServiceFactory() {// 使用匿名类来实现工厂接口
public Service getService() {
// 但需多个业务对象,每次调用工厂方法都会获得一个业务实现对象
return new Implementation1();
}
};
}
class Implementation2 implements Service {
private Implementation2() {}// 私有的,只能通过工厂方法来返回
public void method1() {System.out.println("Implementation2 method1");}
public void method2() {System.out.println("Implementation2 method2");}
public static ServiceFactory factory =
new ServiceFactory() {
public Service getService() {
return new Implementation2();
}
};
}
public class Factories {
public static void serviceConsumer(ServiceFactory fact) {
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(Implementation1.factory);
serviceConsumer(Implementation2.factory);
}
}
修改后Implementation1与Implementation2的构造器都可以是private的,并且没有任何必要去创建具有具体名字的工厂类,另外,你经常只需要一个工厂对象即可,因此在本例中它被创建为Service实现的一个static域,这样更具有实际意义。修改后的程序是多么的完美!
请参考十七章
使用异常的好处:一是使用程序更加的健壮。二是它往往能够降低错误处理的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它,而如果使用异常,那就不必在方法调用处进行检查,因为异常机制保证能够捕获这个错误,并且,只需在一个地方处理错误,即所谓的异常处理程序块中。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离,总之,与以前的错误处理方法相比,异常机制使用代码的阅读、编写和调试工作更加高效。
当抛出异常后,有几件事会随之发生。首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使用程序能要么换一种方式运行,要么继续运行下去。
通常把错误信息输出到e.printStackTrace(System.out)要比直接e.printStackTrace()要好,因为System.out也许会重定向,而e.printStackTrace()默认则是将信息输出到操作系统标准错误流,所以一般我们使用e.printStackTrace(System.out)打印异常信息。如果把结果送到System.err,它就会把会随System.out一起被重定向,这样更容易被用户注意。
使用程序包的客户端程序员可能仅仅只是查看一下抛出的异常类型,其他的就不管了(大多数Java库里的异常都是这么用的),所以对异常所添加的其他功能也许根本用不上,名称代表发生的问题,并且异常的名称应该可以望文知意。
可以声明方法将抛出异常,实际上却不抛出,这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。
Exception的方法
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("My Exception");
} catch (Exception e) {
System.out.println("getMessage():" + e.getMessage());
System.out.println("getLocalizedMessage():" + e.getLocalizedMessage());
System.out.println("toString():" + e);
System.out.println("printStackTrace():");
e.printStackTrace(System.out);
}
}
}
/*
getMessage():My Exception
getLocalizedMessage():My Exception
toString():java.lang.Exception: My Exception
printStackTrace():
java.lang.Exception: My Exception
at excep.ExceptionMethods.main(ExceptionMethods.java:10)
*/
使用JDK日志器记录日志
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;
class MyException extends Exception {
String errKey;//错误键
public MyException setErrKey(String errKey) {
this.errKey = errKey;
return this;
}
//重写父类方法,输出详细信息
public String getMessage() {
return "errKey = " + errKey + " " + super.getMessage();
}
}
public class LoggingExceptions {
// JDK日志记录器
private static Logger logger = Logger.getLogger("LoggingExceptions");
static void logException(Exception e) {// 记录异常日志
// 字符串缓存
StringWriter trace = new StringWriter();
// 将日志输出到缓存
e.printStackTrace(new PrintWriter(trace));
// 输出异常日志
logger.severe(trace.toString());
}
public static void main(String[] args) {
try {
throw new MyException().setErrKey("100");
} catch (MyException e) {
logException(e);
}
}
}
/*
2010-2-9 11:17:38 excep.LoggingExceptions logException
严重: excep.MyException: errKey = 100 null
at excep.LoggingExceptions.main(LoggingExceptions.java:37)
*/
printStackTrace()方法所提供的信息可以通过getStackTrace()方法来直接访问,这个方法返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一桢。元素0是栈顶元素,并且是调用序列中的最后一个方法调用。数组中的最后一个元素即栈底是调用序列中的第一个方法调用。
public class WhoCalled {
static void f() {
// Generate an exception to fill in the stack trace
try {
throw new Exception();
} catch (Exception e) {
for (StackTraceElement ste : e.getStackTrace())
System.out.println("getClassName: " + ste.getClassName()
+ " getFileName: " + ste.getFileName()
+ " getLineNumber: " + ste.getLineNumber()
+ " getMethodName: " + ste.getMethodName()
+ " isNativeMethod: " + ste.isNativeMethod());
}
}
static void g() {
f();
}
static void h() {
g();
}
public static void main(String[] args) {
f();
System.out.println("--------------------------------");
g();
System.out.println("--------------------------------");
h();
}
}
/*
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 8 getMethodName: f isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 28 getMethodName: main isNativeMethod: false
--------------------------------
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 8 getMethodName: f isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 20 getMethodName: g isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 30 getMethodName: main isNativeMethod: false
--------------------------------
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 8 getMethodName: f isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 20 getMethodName: g isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 24 getMethodName: h isNativeMethod: false
getClassName: WhoCalled getFileName: WhoCalled.java getLineNumber: 32 getMethodName: main isNativeMethod: false
*/
如果只是把当前异常对象重新抛出,那么printStackTrace()方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用fillInStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的:
public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void g() throws Exception {
try {
f();
} catch(Exception e) {
System.out.println("Inside g(),e.printStackTrace()");
e.printStackTrace(System.out);
throw e;//再次抛出
}
}
public static void h() throws Exception {
try {
f();
} catch(Exception e) {
System.out.println("Inside h(),e.printStackTrace()");
e.printStackTrace(System.out);
//重新抛出,这一行将成为异常的新发生行了,好比在这里重新包装new后抛出:throw new Exception();
throw (Exception)e.fillInStackTrace();
}
}
public static void main(String[] args) {
try {
g();
} catch(Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
try {
h();
} catch(Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
} /* Output:
originating the exception in f()
Inside g(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:4)
at Rethrowing.g(Rethrowing.java:8)
at Rethrowing.main(Rethrowing.java:27)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:4)
at Rethrowing.g(Rethrowing.java:8)
at Rethrowing.main(Rethrowing.java:27)
originating the exception in f()
Inside h(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:4)
at Rethrowing.h(Rethrowing.java:17)
at Rethrowing.main(Rethrowing.java:33)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.h(Rethrowing.java:22)
at Rethrowing.main(Rethrowing.java:33)
*///:~
常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这个被称为异常链。在JDK1.4以前,程序员必须自己编写代码来保存原始中异常的信息。现在所有Throwable的子类在构造器中都可能接受一个cause对象作为参数(Throwable(Throwable cause))。这个cause就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并了新的异常,也能通过这个异常链追踪到异常最初发生的位置。并可以使用initCause(Throwable cause)来重样设置异常根原因,但此方法至多可以调用一次,如果抛出的异常是通过 Throwable(Throwable) 或 Throwable(String,Throwable) 创建的,则该异常对象的此方法甚至一次也不能调用。
public class ExcpTest {
public static void nullExc() {
throw new NullPointerException();
}
public static void call1() throws Exception {
try {
nullExc();
} catch (Exception e) {
throw new Exception(e);
}
}
public static void call2() throws Exception {
try {
call1();
} catch (Exception e) {
//该行运行时会抛异常,因为e已经设置过 cause 了
throw (Exception) e.initCause(new ArithmeticException());
}
}
public static void call3() throws Exception {
try {
nullExc();
} catch (Exception e) {
//该行运行没问题,因为还没有给 e 设置 cause
throw (Exception) e.initCause(new ArithmeticException());
}
}
public static void main(String[] args) {
try {
call1();
} catch (Exception e) {
e.printStackTrace();
}
try {
call2();
} catch (Exception e) {
e.printStackTrace();
}
try {
call3();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
java.lang.Exception: java.lang.NullPointerException
at ExcpTest.call1(ExcpTest.java:11)
at ExcpTest.main(ExcpTest.java:37)
Caused by: java.lang.NullPointerException
at ExcpTest.nullExc(ExcpTest.java:4)
at ExcpTest.call1(ExcpTest.java:9)
... 1 more
java.lang.IllegalStateException: Can't overwrite cause
at java.lang.Throwable.initCause(Unknown Source)
at ExcpTest.call2(ExcpTest.java:21)
at ExcpTest.main(ExcpTest.java:42)
java.lang.NullPointerException
at ExcpTest.nullExc(ExcpTest.java:4)
at ExcpTest.call3(ExcpTest.java:27)
at ExcpTest.main(ExcpTest.java:47)
Caused by: java.lang.ArithmeticException
at ExcpTest.call3(ExcpTest.java:30)
... 1 more
*/
RuntimeException:属于运行时异常(即非捕获性异常),它包括继承自它的所有子类会自动被Java虚拟机抛出,所以不向在方法声明时抛出异常说明,所以下面的代码也将是多余的:
if(t == null){
throw new NullPointerException();
}
如果不捕获运行时异常,则异常会穿过(抛出)所有的调用路径直达main()方法,而不会被捕获,并在程序退出前将自动调用异常的printStackTrace()方法。
受检查异常是程序可以处理的异常。如果抛出异常的方法本身不能处理它,那么方法调用者应该去处理它,从而使程序运行,不至于终止程序。
运行时异常表示无法让程序恢复运行的异常,导致这种异常的原因通常是由于执行了错误操作,一旦出现了错误操作,建议终止程序,因此Java编译器不检查这种异常。如果出现运行时异常,则表示你的程序代码本身的问题,而不是由程序外界引起的,比如读文件时文件不存在,这不是由程序本身引起的,所以文件不存在的抛出的是检查行异常。
RuntimeException代表的是编程错误:
1、无法预料的错误,比如从你控制范围之外传递进来的null引用。
2、作为程序员,应该在代码中进行检查错误。(比如对于ArrayIndexOutOfBoundsException,就得注意一下数组的大小了。)在一个地方发生的异常,常常会在另一个地方导致错误。
运行时异常是应该尽量避免的(也是完全可能避免的,既然是可以避免的,所以运行时异常不需捕获),在程序调试阶段,遇到这种异常时,正确的做法是程序的设计和实现方式,修改程序中的错误,从而避免这种异常。捕获运行时异常并且使程序恢复运行并不是明智的办法,这主要有两方面的原因:
1、这种异常一旦发生,损失严重。
2、即使程序恢复运行,也可能会导致程序的业务逻辑错乱,甚至导致更严重的异常,或都得到错误的运行结果。
Error类及其子类表示程序本身无法修复的错误,它和运行时异常的相同之处是:Java编译器都会检查它们,当程序运行时出现它们时都会终止程序。
如果有必要,一般将try放循环里,这样就避免了当Java中的异常出现时我们回到异常抛出地点再次执行的问题,这样可以建立了一个“程序继续执行之前必须要达到”的条件。
避免过于庞大的try代码块,因为try代码块越庞大,出现异常的地方就越多,要发生异常的原因就越困难。
不要使用catch(Exception ex)子句来捕获所有异常,理由如下:
1、对不同的异常通常有不现的处理方式,不同的错误使用同样的处理方式是不现实的。
2、会捕获本应该抛出的运行异常,掩盖程序中的错误。
什么情况下才用到finally?当要把除内存之外的资源恢复到它们的初始状态时,就要用到finally子名,这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部对象某个状态的恢复。
Finllay块的异常丢失
public class ExceptionSilencer {
public static void f() {
try {
throw new RuntimeException();
} finally {
// 从这里返回时,异常将丢失,不會再向外拋出了
return;
}
}
public static void main(String[] args) {
f();//得不到打印的信息
}
}
如果一个类继承了某个类同时又实现了某个接口,他们有同样的接口方法,但都抛出了不同的捕获性异常,则该子类实现与重写该方法时,则方法声明处不能抛出任何捕获性异常了。
如果调用的父类构造器抛出捕获性异常,则子类相应的构造器也只能抛出,不能在构造器里进行捕获。
构造器抛出异常时正确的清理方式:
比如在构造器中打开了一个文件,清理动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得以清理,而不能直接在构造器里的finally块上关闭,因为finally块是不管是否有异常都会关闭,而构造器执行成功能外界需要这个文件流。但如果在文件成功打开后才抛出异常,则需要关闭文件,并向外界抛出异常信息:
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class InputFile {
private BufferedReader in;
// 抛出异常的构造器
public InputFile(String fname) throws Exception {
try {
// 在构造器中打开一个文件流
in = new BufferedReader(new FileReader(fname));
} catch (FileNotFoundException e) {
System.out.println("Could not open " + fname);
// 如果是文件没有找到,则不需要关闭流,只需重新抛出
throw e;
} catch (Exception e) {
// 如果是其他异常,则需要关闭流,因為文件已打开
try {
in.close();
} catch (IOException e2) {
System.out.println("in.close() unsuccessful");
}
throw e; // 再重新抛出
} finally {
// 这里不能关闭流!!!
}
}
public String getLine() {
String s;
try {
s = in.readLine();
} catch (IOException e) {
throw new RuntimeException("readLine() failed");
}
return s;
}
// 在文件成功打开后,j由外界使用完后调用
public void dispose() {
try {
in.close();
System.out.println("dispose() successful");
} catch (IOException e2) {
throw new RuntimeException("in.close() failed");
}
}
}
class Cleanup {
public static void main(String[] args) {
// 该try是对构造器异常的捕获,如果出现了异常则不需关闭,
// 因为构造器内部已处理
try {
InputFile in = new InputFile("InputFile.java");
// 如果运行到这里说明文件已正常打开,所以后面需关闭
try {
String s;
int i = 1;
while ((s = in.getLine()) != null)
; // 读文件...
} catch (Exception e) {
System.out.println("Caught Exception in main");
e.printStackTrace(System.out);
} finally {
// 不管读取是否正常,用完后一定要关闭
in.dispose();
}
} catch (Exception e) {
System.out.println("InputFile construction failed");
}
}
}
异常处理的一个重要的原则是“只有在你知道如何处理的情况下才捕获异常”。实际上,异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相分离。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中。这样以来,主干代码就不会与错误处理逻辑混在一起,也更容易理解和维护。
“被检查的异常”可能使问题变得复杂,因为它们强制你在可能还没有准备好处理错误的时候被迫加上cacth子名,即使我们不知道如何处理的情况下,这就导致了异常的隐藏:
try{
//… throw …
}catch(Exception e){//什么都不做}
把“被检查的异常”转换为“不检查的异常”:当在一个普通方法里调用别的方法时,要考虑到“我不知道该怎样处理这个异常,但是也不能把它‘吞’了,或者只打印一些无用的消息”。JDK1.4的异常链提供了一种新的思路来解决这个问题,可以直接把“被检查的异常”包装进RuntimeException里面:
try{
//… throw 检查异常…
}catch(IDontKnowWhatToDoWithThisCheckedException e){
Throw new RuntimeException(e);
}
如果想把“被检查的异常”这种功能“屏蔽”掉的话,上面是一个好的办法。不用“吞”掉异常,也不必把它放到方法的异常声明里面,而异常链还能保证你不会丢失任何原始异常的信息。你还可以在“知道如何处理的地方”来处理它,也可以其他上层catch里通过 throw e.getCause(); 再次抛出原始的“被检查的异常”:
public class Test {
static void f() {
try {
throw new Exception();
} catch (Exception e) {
//将检测性异常转换成非检测性异常后继续抛出
throw new RuntimeException(e);
}
}
static void g() {
//不用捕获,因为检测异常转换了运行异常
f();
}
static void h() throws Exception {
try {
g();
} catch (Exception e) {
e.printStackTrace();
System.out.println("-----");
//可以将检测异常继续做为检测异常抛出
throw new Exception(e.getCause());
}
}
public static void main(String[] args) {
try {
h();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
java.lang.RuntimeException: java.lang.Exception
at Test.f(Test.java:7)
at Test.g(Test.java:13)
at Test.h(Test.java:18)
at Test.main(Test.java:29)
Caused by: java.lang.Exception
at Test.f(Test.java:4)
... 3 more
-----
java.lang.Exception: java.lang.Exception
at Test.h(Test.java:23)
at Test.main(Test.java:29)
Caused by: java.lang.Exception
at Test.f(Test.java:4)
at Test.g(Test.java:13)
at Test.h(Test.java:18)
... 1 more
*/
字符串是不可变的:是final类固不能继承它;也不能通过指向它的引用来修改它的内容。
StringBuilder是Java SE5引用的,在这之前用的是StringBuffer。后者是线程安全的,因此开销也会大些,所以在Java SE5/6中,字符串操作应该还会更快一点。
在JDK1.5中:String s = "a" + "b" + "c"; 在编译时,编译器会自动引入java.lang.StringBuilder类,并使用StringBuilder.append方法来连接。虽然我们在源码中并没有使用StringBuilder类,但是编译器自动地使用了它,因为它更高效。
虽然在JDK1.5或以上版本中使用“+”连接字符串时为避免产生过多的字符串对象,编译器会自加使用StringBuilder类来优化,但是如果连接操作在循环里,编译器会为每次循环都创建一个StringBuilder对象,所以在循环里一般我们不要直接使用“+”连接字符串,而是自己在循环外显示的创建一个StringBuilder对象,用它来构造最终的结果。但是在使用StringBuilder类时也要注意,不要这样使用:StringBuilder.append(a + ":" + c); ,如果这样,那编译器就会掉入陷井,从而为你另外创建一个StringBuilder对象处理括号内的字符串连接操作。
如果重写了父类的toString方法(一般是Object的toString),当需要打印对象的内存地址时,应该调用super.toString()方法,而不是直接打印this,否则会发生StackOverflowError异常。
Java SE5的PrintStream与PrintWriter对象都引入了format()方法,那我们就要可以使用System.out.format()格式化输出了。format()方法模仿自C语言的printf(),如果你比较怀旧的话,也可以使用printf(),它还是调用format()来实现的,只不过换了个名而已:
public class SimpleFormat {
public static void main(String[] args) {
int x = 5;
double y = 5.332542;
// 以前我们是这样打印的:
System.out.println("Row 1: [" + x + " " + y + "]");
// 现在我们是这样打印的:
System.out.format("Row 1: [%d %f]\n", x, y);
// 或者是
System.out.printf("Row 1: [%d %f]\n", x, y);
}
} /* Output:
Row 1: [5 5.332542]
Row 1: [5 5.332542]
Row 1: [5 5.332542]
*///:~
在Java中,所有新的格式化功能(PrintStream与PrintWriter的format方法以及String的静态方法format)都是由java.util.Formatter类来处理的,创建时需要指定输出到哪。
Formatter的对齐格式化输出抽象语法:
%[argument_index$][flags][width][.precision]conversion
可选的 argument_index是一个十进制整数,用于表明参数在参数列表中的位置。第一个参数由 "1$" 引用,第二个参数由 "2$" 引用,依此类推。
Width用来控制一个域的最小尺寸,如果输出的参数值不够宽,则添加空格来确保一个域至少达到某个宽度,在默认的情况下,数据是右对齐的,但我们可以使用“-” flags标志来改变对齐方向。
与width相对的是precision,它用来指明输出的最大尺寸。Width可以应用于各种数据类型时其行为方式都是一样,但precision不一样,并不是所有类型都能应用precision,而且,应用于不同类型的数据转换时,precision的意义也不同:将precision应用String时,它表示打印String时输出字符的最大个数;而在将precision应用于浮点数时,它表示小数部分要显示出来的位数(默认是6位小数位),如果小数位过多则舍入,太少则在尾部补零。由于整数没有小数部分,固不能应用于整形数据类型,否则抛异常:
import java.util.Formatter;
public class Receipt {
private double total = 0;
// 这里输出到控制台,我们也可以格式化后输出到文件、StringBuilder、CharBuffer
private Formatter f = new Formatter(System.out);
public void printTitle() {
// %-15s表示输出一个最小宽度为15的且左对齐的字符,2$表示左起输出参数位置
f.format("%2$-15s %1$5s %3$10s\n", "Qty", "Item", "Price");
f.format("%-15s %5s %10s\n", "----", "---", "-----");
}
public void print(String name, int qty, double price) {
// %-15.15s在%-15s基础上最多能输出15个字符
f.format("%-15.15s %5d %10.2f\n", name, qty, price);
total += price;
}
public void printTotal() {
// %10.2f表示输出一个最小宽度为10,右对齐小数点后两位的浮点数
f.format("%-15s %5s %10.2f = %s\n", "Tax", "", total * 0.06, total + " * 0.06");
f.format("%-15s %5s %10s\n", "", "", "-----");
f.format("%-15s %5s %10.2f = %s\n", "Total", "", total * 1.06, total + " * 1.06");
}
public static void main(String[] args) {
Receipt receipt = new Receipt();
receipt.printTitle();
receipt.print("Jack's Magic Beans", 4, 4.254);
receipt.print("Princess Peas", 3, 5.1);
receipt.print("Three Bears Porridge", 1, 14.285);
receipt.printTotal();
}
}
/*
* Output:
Item Qty Price
---- --- -----
Jack's Magic Be 4 4.25
Princess Peas 3 5.10
Three Bears Por 1 14.29
Tax 1.42 = 23.639 * 0.06
-----
Total 25.06 = 23.639 * 1.06
*/
Fomatter常用类型转换字符
%d :整数型(十进制)
%c :Unicode字符
%b :Boolean值
%s :String
%f :浮点数(十进制)
%e :浮点数(科学计数)
%x :整数(十六进制)
%h :散列码(十六进制)
%% :字符“%”
“%b”对于boolean基本类型及Boolean,其转换结果为对应的true或false,但是,对其他类型的参数,只要该参数不为null,那转换的结果就永远都是true,即使是数字0,转换结果依然为true,而这不像其他语言(如C)为false。上面列举的是常用的格式字符,其他可以在JDK文档中的Formatter类部分找到。
String.format():String.format()是一个Static方法,当我们只需要使用format()方法一次时很方便,其实在String.format()内部,它也是创建一个Formatter对象,格式化后传进的字符串后返回新的字符串。下面是一个十六进制工具:
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class Hex {
/**
* 以十六进制格式输出数据
* @param counts 每行多少个
* @param data 要格式化的数据
* @return 格式化后的数据
*/
public static String format(int counts, byte[] data) {
StringBuilder result = new StringBuilder();
int n = 0;
int rows = 0;
for (byte b : data) {
if (n % counts == 0) {
rows++;
result.append(String.format("%05d: ", rows));
}
result.append(String.format("%02X ", b));
n++;
if (n % counts == 0) {
result.deleteCharAt(result.length() - 1);
result.append("\n");
}
}
result.append("\n");
return result.toString();
}
/**
* 读取二进制文件
* @param bFile
* @return
* @throws IOException
*/
public static byte[] read(File bFile) throws IOException {
BufferedInputStream bf = new BufferedInputStream(new FileInputStream(
bFile));
try {
byte[] data = new byte[bf.available()];
bf.read(data);
return data;
} finally {
bf.close();
}
}
public static void main(String[] args) throws IOException {
System.out.println(format(16, read(new File("src/Hex.class")
.getAbsoluteFile())));
}
}
/*
* Output:
00001: CA FE BA BE 00 00 00 31 00 7F 07 00 02 01 00 03
00002: 48 65 78 07 00 04 01 00 10 6A 61 76 61 2F 6C 61
00003: 6E 67 2F 4F 62 6A 65 63 74 01 00 06 3C 69 6E 69
...
*/
判断一个字符串是否匹配指定的模式,最简单的是使用String对象的matches方法,需传递正则式参数,实质上是调用Pattern.matches(regex, string)来实现的。
String的split()方法也使用到了正则式,该方法的重载版本允许你限制字符串分割次数split(String regex,int limit) :limit 参数控制模式应用的次数,因此影响结果数组的长度,默认就是0。如果该限制 n大于 0,则模式将被最多应用 n - 1 次,数组的长度将不会大于 n,而且数组的最后项将包含超出最后匹配的定界符的所有输入。如果 n为非正,则模式将被应用尽可能多的次数,而且数组可以是任意长度。如果 n为零,则模式将被应用尽可能多的次数,数组可有任何长度,并且结尾空字符串将被丢弃。例如,字符串 "boo:and:foo" 使用这些参数可生成下列结果:
Regex Limit 结果
: 2 { "boo", "and:foo" }
: 5 { "boo", "and", "foo" }
: -2 { "boo", "and", "foo" }
o 5 { "b", "", ":and:f", "", "" }
o -2 { "b", "", ":and:f", "", "" }
o 0 { "b", "", ":and:f" }
CharBuffer、String、StringBuffer、StringBuilder都实现了CharSequence接口,大多数的正则表达式操作都接受CharSequence类型的参数。
如果使用功能强大的正则表达式对象,我们使用静态的Patter.compile()方法来编译正则表达式即可,它会生成一个Patter对象。接下来将想要检测的字符串传Patter对象的matcher()方法,会生成一个Matcher对象,该对象有很多的功能可用。
另外,Patter类还提供了静态的方法:static boolean matches(String regex, CharSequence input)
Matcher对象的groupCount方法返回该匹配器的模式中的分组数目,但第0组不包括在内。
String的split()方法实质上是调用Pattern对象的split方法:Pattern.compile(regex).split(string, limit)来实现的。String的replace方法则是通过调用Matcher对象的replace方法来实现的。
Matcher对象的appendReplacement(StringBuffer sbuf,String replacement)执行渐进式的替换,而不是像replaceFirst()和replaceAll()那样只替换第一个匹配或全部匹配。这是一个非常重要的方法,它允许你调用其他方法来生成或处理replacemanet(replaceFirst()和replaceAll()则只能使用一个固定的字符串),使你能够以编程的方式将目标分割成组,从而具备列强大的替换功能,appendTail(StringBuffer sbuf),在执行了一次或多次appendReplacement之后,调用此方法可以将输入字符串余下的部分复制到sbuf中。
Matcher对象的reset()、reset(CharSequence input)将Matcher对象重新设置到当前字符序列的起始位置,可以重用Pattern与Matcher对象。
Java SE5新增了Scanner类,它可以大大减轻扫描输入的工作。可以通过useDelimiter(String pattern)设置next操作的定界符。还可通过hasNext(String pattern)判断是否还存在指定正则式的串,如果有,则可通过String next(String pattern)来读取。除此之后,它还有很多的读取各种不同基本类型的数据的nextXX方法。
在运行时识别对象和类的信息有两种方式:一种是“传统的”RTTI(如“(Circle)”),它假设我们在编译时已经知道了所有的类型,但易引起ClassCastException异常,不过我们可以通过 instanceof 先检测具体类型;另一种是“反射”机制(使用类型的Class对象),它允许我们在运行时查询Class对象的信息。
Class对象相关方法:
getName():回此 Class对象所表示的实体(类、接口、数组类、基本类型或 void)名称。如果此类对象表示的是非数组类型的引用类型,则返回该类的二进制名称,包括包名;如果此类对象表示一个基本类型或 void,则返回的名字是一个与该基本类型或void 所对应的 Java 语言关键字相同的串;如果此类对象表示一个数组类,该数组嵌套深度的一个或多个 '[' 字符加元素类型名。元素类型名的编码如下:
元素类型编码
boolean Z
byte B
char C
类或接口 Lclassname;
double D
float F
int I
long J
short S
如:
String.class.getName():java.lang.String
byte.class.getName():byte
(new Object[3]).getClass().getName():[Ljava.lang.Object;
(new int[3][4][5][6][7][8][9]).getClass().getName():[[[[[[[I
getSimpleName():产生不包括包名的类名。
getInterfaces():返回所有实现的接口的Class对象数组。
getSuperclass():返回直接基类。如果此 Class 表示 Object 类、接口、基本类型或 void,则返回 null。如果此对象表示一个数组类,则返回表示该 Object 类的 Class 对象。
newInstance():创建此Class对象所表示的类的一个新实例。如同用一个带有一个空参数列表的 new
表达式实例化该类。使用该方法创建对象实例时,必须要有默认构造函数。
获取某个类的Class对象:
l Class.forName(String className)。
l 通过对象的getClass()方法。
l 直接通过类的静态属性class,如Test.class,这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且不需要调用forName()方法,所以更高效。
另外,对于基本数据类型的包装器类,还有一个标准字段TYPE,该字段是一个引用,指向对象的基本数据类型的Class对象。但还是建议使用“.class”的形式,以保持与普通类型的一致性。
>>>Class.forName、Object.class、classLoader.loadClass异同<<<
Class.forName与“.class”区别在于:前者会初始化Class对象(如静态数据成员的初始化与静态块的执行),而后者不会。“.class”只是去加载类,不会链接(即不会给静态域分配空间);ClassLoader类的loadClass()方法只是加载一个类,并不会分配内存空间,更不会导致类的初始化:
public class ClassLoadTest {
// 测试时请调整虚拟机的堆大小:-Xms32M -Xmx1024M
public static void main(String[] args) {
// 编译时直接将2替换,不加导致类的加载与初始化
System.out.println("Bean.y=" + Bean.y);
sleep("->调用类的静态字面常量不会导致类的加载");
/*
* 不会初始化静态块与静态成员,只是加载到内存,也没进行
* 链接操作(静态成员分配空间),更没有初始化类(静态成
* 员初始化与静态块的执行)
*/
Class cl = Bean.class;
sleep("->Bean.class只会加载类,不会分配内存空间与初始化类");
ClassLoader cld = ClassLoader.getSystemClassLoader();
try {
// 也只是加载到内存,没有进行空间的分配与初始化类
cld.loadClass("Bean");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
sleep("->ClassLoader的loadClass()方法也只会加载类,不会" +
"分配内存空间与初始化类");
try {
// 加载与初始化类,并可看到内存猛增
cl = Class.forName("Bean");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
sleep("->Class.forName()会加载类,且分配内存空间与初始化类");
}
private static void sleep(String msg) {
System.out.println(msg);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Bean {
static {
System.out.println("static block");
}
public static final int y = 2;
public static final int i = f(1);
public static int j = f(2);
// 测试 A.class是否进行了链接操作
public static long[] l = h(21474836);
static {
System.out.println("i=" + i);
System.out.println("j=" + j);
}
static int f(int i) {
System.out.println("static f(" + i + ")");
return i;
}
static long[] h(int len) {
long[] l = new long[len];
for (int i = 0; i < len; i++) {
l[i] = i;
}
return l;
}
}
使用一个类前需做的三个准备步骤:
1、加载:这是由类加载器执行的。该步骤将查找字节码文件并读取到内存,并从这些字节码中创建一个Class对象。
2、链接:验证被加载类的正确性(验证)、为类的静态变量分配内存空间,并将其初始化为默认值(准备)、把类中的符号引用转换为直接引用(解析),如将方法的调用解析成直接方法在方法区的内存地址调用。
3、类的初始化(初始化Class对象):初始化静态成员和执行静态初始化块。
调用类一个 static final(编译期常量,注一定要是在定义时就初始化了,否则还是会初始化静态块与成员的)成员时,类的Calss对象不会执行初始化操作,也就是说“编译期常量”在类Class对象还没有初始化就可以引用了。
如果一个static域不是final的,那么在对它访问时,总是要求在它被读取前,要先对类进行链接(为这个域分配存储空间)和初始化(初始化该存储空间)操作。
类的初始化时机发生在类首次主动使用时,类主动使用发生在以下时机:
1、 创建类的实例。创建实例途经:使用new(构造器隐式也是静态的)、反射、克隆、反序列化
2、 调用类的静态方法。
3、 访问某个类或接口的静态变量(注意,不能是静态的字面常量域,静态的字面常量在编译时就确定,使用之前不会先加载类,更不会初始化类)。
4、 调用Class.forName()加载类。
5、 初始化一个类的子类,会导致父类初始化。
6、 Java虚拟机启动时被标明为启动类的类,例如: “java Test”命令,Test类就是启动类,Java虚拟机会先初始化它。
Java程序对类的使用方式可分为两种:主动使用与被动使用。
所有的Java