1——面向对象和JVM基础
1.java中的4种访问制权限:
(1).public:最大访问控制权限,对所有的类都可见。
(2).protect:同一包可见,不在同一个包的所有子类也可见。
(3).default:包访问权限,即同一个包中的类可以可见。默认不显式指定访问控制权限时就是default包访问控制权限。
(4).private:最严格的访问控制权限,仅该类本身可见,对外一切类都不可以访问(反射机制可以访问)。
2.面向对象编程中两种对象组合方式——is-a 和 has-a:
(1).is-a组合:一个类继承具有相似功能的另一个类,根据需要在所继承的类基础上进行扩展。
优点:具有共同属性和方法的类可以将共享信息抽象到父类中,增强代码复用性,同时也是多态的基础。
缺点:子类中扩展的部分对父类不可见,另外如果共性比较少的时候使用继承会增加冗余代码。
(2).has-a组合:has-a组合是在一个类中引用另一个类作为其成员变量。
优点:可扩展性和灵活性高。在对象组合关系中应优先考虑has-a组合关系。
缺点:具有共性的类之间看不到派生关系。
3.多态:
在面向对象编程中,子类中拥有和父类相同方法签名的方法称为子类方法覆盖父类方法,当调用子类方法的某个操作时,不必明确知道子类的具体类型,只需要将子类类型看作是父类的引用调用其操作方法,在运行时,JVM会根据引用对象的具体子类类型而调用应该的方法,这就是多态。
多态的基础是java面向对象编程的晚绑定机制。编程中有如下两种绑定机制:
(1).早绑定:一般在非面向对象编程语言中使用,在程序编译时即计算出具体调用方法体的内存地址。
(2).晚绑定:面向对象编程语言中经常使用,在程序编译时无法计算出具体调用方法体的内存地址,只进行方法参数类型和返回值类型的校验,在运行时才能确定具体要调用方法体的内存地址。
4.java单继承的优点:
相比于C++的多继承,java只支持类的单继承,java中的所有类的共同基类是Object类,Object类java类树的唯一根节点,这种单继承有以下好处:
(1).单继承可以确保所有的对象拥有某种共同的特性,这样对于JVM虚拟机对所有的类进行系统级的操作将提供方便,所有的java对象可以方便地在内存堆栈中创建,传递参数也变的更加方便简单。
(2).java的单继承使得实现垃圾回收器功能更加容易,因为可以确保JVM知道所有对象的类型信息。
5.选择容器对象两个原则:
(1).容器所能提供不同的类型的接口和外部行为是否能够满足需求。
(2).不同容器针对不同的操作效率不同。
6.类型转换:
Java中有两种常见的类型转换:向上类型转换(upcast)和向下类型转换(downcast):
(1).向上类型转换(upcast):
向上类型转换是将子类对象强制类型转换为父类类型,经典用法是面向对象的多态特性。向上类型转换时,子类对象的特性将不可见,只有子类从父类继承的特性仍然保持可见,向上类型转换时编译器会自动检查是否类型兼容,通常是安全的。
(2).向下类型转换:
向下类型转换是将父类类型强制转换为子类类型,转换过后父类中不可见的子类特性又恢复可见性,向下类型转换时,编译器无法自动检测是否类型兼容,往往会产生类型转换错误的运行时异常,通常不安全。
7.java中5个存放数据的地方:
(1).寄存器(Registers):位于CPU内部,是速度最快的存储区,但是数量和容量有限。在java中不能直接操作寄存器。
(2).栈(Stack):栈位于通用随机访问存储器 (General random-access memory,RAM,内存) 中,通过处理器的栈指针访问,栈指针从栈顶向栈底分配内存,从栈底向栈顶释放内存。栈是仅次于寄存器的速度第二快的存储器,在java程序中,一般的8种 基本类型数据和对象的引用通常存放在栈内存中,不通过new关键字的字符串对象也是存放在栈的字符串池中。栈的优势是,存取速度比堆要快,仅次于寄存器, 栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
(3).堆(Heap):也是位于通用随机访问存储器 (General random-access memory,RAM,内存) 中的共享内存池。Java的堆是一个运行时数据区,类的对象从中分配空间,凡是通过new关键字创建的对象都存放在堆内存中,它们不需要程序代码来显式的 释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器 会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
(4).常量存储器(Constant storage):java中的常量是存放在系统内嵌的只读存储器中(read-only memory,ROM)的。
(5).非随机存储器(Non-RAM storage):对于流对象和持久化对象,通常存放在程序外的存储器,如硬盘。
8.javadoc只处理public和protected访问控制权限的文档注释,private和default权限的稳定注释将被忽略。
9.java中赋值运算:
基本类型赋值是直接复制值,赋值操作后,相互不影响。
引用类型赋值是复制引用值,相当于给对象取一个别名,赋值之后两个引用指向同一个引用对象,相互之间有影响。
在Java中,向方法传递引用类型参数会改变参数的值,不让参数受到影响的解决方法:在方法内首先先将引用克隆一份,然后操作克隆的对象。
10.移位运算:
左移运算符<<:将比特位左移指定位数,右边部分补0,左移一位相当于乘2。
右移运算符>>:将比特位右移指定位数,如果是正数,左边第一位(符号位)补0,其余位补0,如果是负数,左边第一位补1,其余位补0。右移一位相当于除2。
无符号右移运算符>>>:将比特位右移指定位数,不论是正数或者负数,左边移除位统统补0。
11.java中,比int类型小的原始类型(char、byte、short)进行数学运算或者位运算时,数据类型首先转换成int类型,然后进行相应的运算。
12.方法重载(overloading):方法同名,参数列表不同称为方法重载,注意方法的返回值类型不同不能作为方法重载。
13.java中的析构函数:
Java中没有像C/C++的析构函数,用来销毁不用的对象是否内存空间,只有以下三个方法用于通知垃圾回收器回收对象。
(1).finalize( )只是通知JVM的垃圾收集器当前的对象不再使用可以被回收了,但是垃圾回收器根据内存使用状况来决定是否回收。
finalize()最有用的地方是在JNI调用本地方法时(C/C++方法),调用本地方法的析构函数消耗对象释放函数。
(2). System.gc()是强制析构,显式通知垃圾回收器释放内存,但是垃圾回收器也不一定会立即执行,垃圾回收器根据当前内存使用状况和对象的生命周期自行决定是否回收。
(3).RunTime.getRunTime().gc()和System.gc()类似。
注意:这三个函数都不能保证垃圾回收器立即执行,推荐不要频繁使用。
14.垃圾回收器原理:
(1).引用计数(ReferenceCounting)垃圾回收算法:
一种简单但是速度较慢的垃圾回收算法,每个对象拥有一个引用计数器(Reference Counter),当每次引用附加到这个对象时,对象的引用计数器加1。当每次引用超出作用范围或者被设置为null时,对象的引用计数器减1。垃圾回收 器遍历整个对象列表,当发现一个对象的引用计数器为0时,将该对象移出内存释放。
引用计数算法的缺点是,当对象环状相互引用时,对象的引用计数器总不为0,要想回收这些对象需要额外的处理。
引用计数算法只是用来解释垃圾回收器的工作原理,没有JVM使用它实现垃圾回收器。
引用计数的改进算法:
任何存活的对象必须被在静态存储区或者栈(Stack)中的引用所引用,因此当遍历全部静态存储区或栈中的引用时,即可以确定所有存活的对象。每当 遍历一个引用时,检查该引用所指向的对象,同时检查该对象上的所有引用,没有引用指向的对象和相互自引用的对象将被垃圾回收器回收。
(2).暂停复制(stop-and-copy)算法:
垃圾回收器的收集机制基于:任何一个存活的对象必须要被一个存储在栈或者静态存储区的引用所引用。
暂停复制的算法是:程序在运行过程中首先暂停执行,把每个存活的对象从一个堆复制到另一个堆中,已经不再被使用的对象被回收而不再复制。
暂停复制算法有两个问题:
a.必须要同时维护分离的两个堆,需要程序运行所需两倍的内存空间。JVM的解决办法是在内存块中分配堆空间,复制时简单地从一个内存块复制到另一个内存块。
b.第二个问题是复制过程的本身处理,当程序运行稳定以后,只会产生很少的垃圾对象需要回收,如果垃圾回收器还是频繁地复制存活对象是非常低性能的。JVM的解决方法是使用一种新的垃圾回收算法——标记清除(mark-and-sweep)。
一般来说标记清除算法在正常的使用场景中速度比较慢,但是当程序只产生很少的垃圾对象需要回收时,该算法就非常的高效。
(3).标记清除(mark-and-sweep)算法:
和暂停复制的逻辑类似,标记清除算法从栈和静态存储区开始追踪所有引用寻找存活的对象,当每次找到一个存活的对象时,对象被设置一个标记并且不被回收,当标记过程完成后,清除不用的死对象,释放内存空间。
标记清除算法不需要复制对象,所有的标记和清除工作在一个内存堆中完成。
注意:SUN的文档中说JVM的垃圾回收器是一个后台运行的低优先级进程,但是在早期版本的JVM中并不是这样实现的,当内存不够用时,垃圾回收器先暂停程序运行,然后进行垃圾回收。
(4).分代复制(generation-copy)算法:
一种对暂停复制算法的改进,JVM分配内存是按块分配的,当创建一个大对象时,需要占用一块内存空间,严格的暂停复制算法在释放老内存堆之前要求把每个存活的对象从源堆拷贝到新堆,这样做非常的消耗内存。
通过内存堆,垃圾回收器可以将对象拷贝到回收对象的内存堆中,每个内存块拥有一个世代计数(generation count)用于标记对象是否存活。每个内存块通过对象被引用获得世代计数,一般情况下只有当最老的内存块被回收时才会创建新的内存块,这主要用于处理大 量的短存活周期临时对象回收问题。一次完整的清理过程中,内存块中的大对象不会被复制,只是根据引用重新获得世代计数。
JVM监控垃圾回收器的效率,当发现所有的对象都是长时间存活时,JVM将垃圾回收器的收集算法调整为标记清除,当内存堆变得零散碎片时,JVM又重新将垃圾回收器的算法切换会暂停复制,这就是JVM的自适应分代暂停复制标记清除垃圾回收算法的思想。
15.java即时编译技术(JIT):
Java的JIT是just-in-timecomplier技术,JIT技术是java代码部分地或全部转换成本地机器码程序,不再需要JVM解释,执行速度更快。
当一个”.class”的类文件被找到时,类文件的字节码被调入内存中,这时JIT编译器编译字节码代码。
JIT有两个不足:
(1).JIT编译转换需要花费一些时间,这些时间贯穿于程序的整个生命周期。
(2).JIT增加了可执行代码的size,相比于压缩的字节码,JIT代码扩展了代码的size,这有可能引起内存分页,进而降低程序执行速度。
对JIT不足的一种改进技术是延迟评估(lazy evaluation):其基本原理是字节码并不立即进行JIT编译除非必要,在最近的JDK中采用了一种类似延迟JIT的HotSpot方法对每次执行的代码进行优化,代码执行次数越多,速度越快。
16.非内部类的访问控制权限只能是默认的包访问权限或者是public的,不能是protected和private的。内部类的访问控制权限可以是protected和private。
17.Java中的高精度数值类型:
BigInteger和BigDecimal是java中的高精度数值类型,由于它们是用于包装java的基本数据类型,因此这两个高精度数值类型没有对应的原始类型。
(1).BigInteger支持任意精度的整数,即使用BigInteger可以表示任意长度的整数值而在运算中不会因为范围溢出丢失信息。
(2).BigDecimal支持任意精度的固定位数浮点数,可以用来精确计算货币等数值。
普通的float和double型浮点数因为受到小数点位数限制,在运算时不能准确比较,只能以误差范围确定是否相等,而BigDecimal就可以支持固定位数的浮点数并进行精确计算。
18.Java只处理public和protected访问控制权限成员的文档注释,private和默认的包访问控制权限成员的文档注释将被忽略。
19.Java中赋值运算:
Java中赋值运算是把赋值运算符”=”右边的值简称右值拷贝到赋值运算符左边的变量,如a=b,即把b代表的变量或常量值复制给变量a,切记a只能是变量,不能说常量值。
(1).原始类型赋值运算:
Java中8种原始数据类型赋值运算是将赋值运算符右边的值拷贝到赋值运算符左边的变量中。
原始类型赋值运算后,无论改变赋值运算符那一边的值,都不会影响赋值运算符另一边的值。
(2).引用类型的赋值运算:
Java中除了8中原始数据类型外,所有的数据类型都是对象类型,对象类型的赋值运算是操作引用,如a=b,把b引用赋值给a引用,即原本b引用指向的对象现在由a和b引用同时指向。
引用赋值运算符又叫别名运算符,即它相当于给引用对象取了一个别名,其实引用的还是同一个对象。
引用类型的赋值运算,如果赋值运算符任意一边的引用改变了被引用对象的值,赋值运算符另一边的引用也会受影响,因为两个引用指向的是同一个被引用的对象。
2——对象初始化和面向对象特性
1.java类的初始化顺序:
(1).在一个类中,初始化顺序由变量在类中的声明定义顺序决定,成员变量(非set方法和构造方法的初始化)的初始化发生在方法调用之前,包括构造方法。
(2).静态变量在整个存储区只保留一份拷贝,本地变量不能使用静态关键字,基本类型的静态变量不需要初始化,它会根据类型获得初始化值,引用类型的静态变量默认初始化为null。
静态变量的初始化发送在需要使用的时候,一旦被初始化之后,静态变量就不会再初始化。
(3).静态初始化块和静态变量类似的执行也在构造方法之前,并且仅执行一次。
(4).动态初始化块(与静态初始化块类似,只是没有static关键字,即放在一对大括号中的代码块)在静态初始化块初始化结束后执行,动态初始化块每次创建新对象都会初始化一次。
(5).构造方法执行时,先执行父类的构造方法,后执行子类的构造方法。
(6).本地变量初始化最晚,在方法中初始化。
综述,类的初始化顺序依次为:
a.父类的静态变量/静态初始化块;
b.子类类的静态变量/静态初始化块;
c.父类的动态初始化块、非构造方法和set方法的成员变量初始化
d.子类的动态初始化块、非构造方法和set方法的成员变量初始化
e.父类的构造方法。
f.子类的构造方法。
g.父类本地变量。
h.子类的本地变量。
2.数组初始化:
Java中数组初始化有以下3中方式:
(1).数组声明时直接初始化,如:
int[] a = {1,2,3};
(2).动态数组初始化,如:
int[] a = new int[]{1,2,3};
注意:动态数组初始化时,不能在new()操作符中指定数组的大小,即int a = new int[3]{1,2,3}的写法是错误的,数组的大小由初始化数组元素个数决定。
(3).固定长度数组初始化,如:
int[] a = new int[3];
a[1] = 0;
a[2] = 1;
a[3] = 2;
注意:固定长度大小的数组初始化时不能大于所声明的数组长度,没有声明的数组元素使用其默认值,如int默认为0,对象类型的值为引用,默认为null.
3.java代码重用4中方式:
java面向对象编程中提供了如下4中代码重用的方式:
(1).组合:
面向对象编程中最常用的代码复用方式,具体的方式是在一个对象中将另一个对象引用最为成员变量,其最大的优点是既实现松散耦合,有可能提高代码复用率。
(2).继承:
面向对象编程中常用的提高代码复用率的方法之一,适用于子类和父类是同一种抽象类型,具有共同的属性情况。
使用继承,子类可以复用父类除private私有房屋控制权限以为的所有属性和方法,编译器将父类封装为子类对象内部的一个对象。
需要注意的是:调用子类初始化构造方法时,编译器会确保首先调用父类的构造方法初始化父类,然后才初始化子类,如果父类中没有默认的构造方法,即需要显式传入参数的构造方法时,子类必须通过super关键字显式传入参数调用父类的构造方法。
(3).委派:
Java中不支持委派方式的代码复用,但是开发人员可以使用委派机制实现代码的重用。
委派是指,java对象的所有方法其实都是委派调用另一个类的方法实现,但是当前类又不是所委派类的类型,因此使用继承不太合适,解决方式和组合类似,将被委派类作为委派类的成员变量,委派类的方法直接调用被委派类对象应用的方法。
如:
[java] view plaincopy
1. //委派类
2. public Class A{
3. //被委派类
4. private B b = new Class B();
5. public void method1(){
6. b.method1();
7. }
8. public void method2(){
9. b.method2();
10. }
11. ……
12. }
(4).联合使用组合和继承方式:
因为java中不允许多继承,如果某种情况下,一个java类需要使用多个其他类功能,且该类和其中某个类具有很多共同属性,即可以看作同一类,则可以使当前类继承具体共同属性的类,同时将其他类作为成员变量组合引用。
4.组合和继承的区别:
组合和继承都可以复用代码,很多java程序员,甚至是架构师都分不清楚什么情况下该使用组合,什么情况下应该使用继承,具体的区别如下:
(1).组合:
组合通常是在一个类中想使用另一个类已有的特性,但是却不想使用其接口。
使用组合可以可以将一个类作为当前类的内嵌对象,这样在当前类中就可以显式地使用内嵌类已经实现的功能,与此同时又不会影响当前类的调用接口。
(2).继承:
继承是隐式地使用被继承类的功能,相当于提供了一个新版本的父类实现。
使用继承,子类不但可以复用父类的功能,同时还复用了父类的接口,子类和父类的对外调用接口相同的情况下适合使用继承。
使用继承时,很多情况下需要向上类型转换,即将子类看作其父类。在编程时到底选用组合方式还是继承方式,一个简单的判断依据是:是否需要向上类型转换,如果需要就使用继承,如果不需要,则选择组合。
5.final方法:
Java中使用final类型的方法有以下两种原因:
(1).设计原因:
final类型的方法不允许其子类修改方法,即不允许子类覆盖父类的final方法。
(2).效率原因:
在早期的java实现中,如果方法被声明为final,编译器将final方法调用编译为内联调用。
正常的方法调用是:如果方法调用时,将当前的方法上下文保持到栈中,调用被调用的方法,然后在将调用上下文出栈恢复调用现场。
内联调用是:如果方法调用时,将被调用方法体拷贝到当前调用的地方合并成一个方法体,这样就避免因需要保存方法调用线程而进行的进栈和出栈操作,可以提高效率。
新版的使用hotspot技术的java虚拟机可以探测方法调用情况而做效率优化,final方法不再作为提高效率的手段,唯一的作用是确保方法不被子类覆盖。
注意:任何private的方法都是隐式的final类型,同final方法类似,private方法不能被子类所覆盖,但是private比final更严格,基类的private方法对子类不可见,private方法不再是接口的一部分。
6.多态性:
面向对象编程中的多态和继承往往是一起发挥作用的,使用继承,所有的子类和父类使用相同的对外接口,而多态的基础是晚绑定或动态绑定或运行时绑定, 即对象引用使用基类类型,在编译时编译器无法确切知道到底调用哪一个具体类,只有在运行时,java虚拟机才通过类型检查确定调用对象的具体类型。
Java中默认对象引用全部是晚绑定,只有static和final类型的引用时早绑定或编译时绑定。
多态的优势是:程序的可扩张性好,无论添加多少个子类,基类的接口都不用改变,只需要在子类对应方法中提供具体实现即可,也就是所谓的将程序变化的部分和程序保持不变的部分分离。
注意:只有正常的方法可以使用多态,字段和静态方法没有多态机制。
构造方法也不支持多态机制,构造方法是隐式的static声明。
3——内部类
1.java中,可以将一个类的定义放在另一个类的内部,这种叫做内部类。
内部类允许编程人员将逻辑上相关的类组织在一起,并且控制内部类对其他类的可见性。
2.在外部类的非静态方法中创建内部类的对象语法:
外部类类名.内部类类名 对象名 = 外部类对象.new 内部类类名();
如:
[java] view plaincopy
1. public class Outter{
2. class inner{
3. }
4. }
5. Outter out = new Outter();
6. Outter.Inner inner = out.new Inner();
注意:非静态的内部类必须要有外部类对象之后才能创建,因为外部类对象持有内部类的引用,如果内部类是静态的,则不需要外部类对象引用内部类对象。
3.内部类对外部类对象的引用:
外部类中所有的元素对内部类都是可见的,内部类持有对外部类对象引用的语法:外部类名称.this。如:
[java] view plaincopy
1. public class Outter{
2. void f(){
3. System.out.println(“Outter f() method”);
4. }
5. class Inner{
6. public Outter outer(){
7. return Outter.this;
8. }
9. }
10. public Inner inner(){
11. return new Inner();
12. }
13. }
4.方法内部类:
除了最常见的最为成员变量的内部类以外,内部类还可以定义在方法中,如:
[java] view plaincopy
1. public class Outter{
2. public Inner getInner(){
3. class Inner{
4. public void f(){
5. System.out.println(“Method inner class”);
6. }
7. }
8. return new Inner();
9. }
10. public void fout(){
11. Inner inner = getInner();
12. inner.f();
13. }
14. }
5.匿名内部类:
Java中匿名内部类的应用十分广泛,所谓的匿名内部类就是指所创建的内部类没有类名称,也就是不知道内部类的具体类型,如:
[java] view plaincopy
1. public class Outter{
2. public Inner getInner(){
3. return new Inner(){
4. private String name = “inner”;
5. public String getName(){
6. return name;
7. }
8. };
9. }
10. }
上面的return newInner{……};就是一个匿名内部类,这个匿名内部类继承了Inner类,即其基类是Inner,但是其具体类型不清楚,该匿名内部类等效于下面的内部类写法:
[java] view plaincopy
1. public class Outter{
2. class MyInner implement Inner{
3. private String name = “Inner class”;
4. public String getName(){
5. return name;
6. }
7. }
8. public Inner getInner(){
9. return new MyInner();
10. }
11. }
注意:匿名内部类是实现了new关键字之后的接口或继承了类,只是没有具体的类名称。
6.内名内部类传递final参数:
如果在创建匿名内部类,需要外部类传递参数时,参数必须是final类型的,否则,编译时会报错。如:
[java] view plaincopy
1. public class Outter{
2. public Inner getInner(final String name){
3. return new Inner(){
4. public name getName(){
5. return name;
6. }
7. };
8. }
9. }
注意:如果外部类传递的参数在内部类中使用,则必须是final类型,如果没有在内部类中使用(如,仅在基类中使用),则可以不用是final类型的。
7.静态内部类:
静态内部类又称为嵌套类,静态内部类和普通内部类的区别:
(1).对于非静态的内部类来说,内部类和外部类必须保持对象引用,内部类可以访问外部类的任何元素.
(2).静态内部类不需要和外部类保持对象引用,静态内部类只能访问外部类的静态元素。
8.为什么要使用内部类:
使用内部类主要有以下两个原因:
(1).解决java中类不能多继承的问题:
Java中的继承是单继承,在某些情况下接口的多继承可以解决大部分类似C++中多继承的问题,但是如果一个类需要继承两个父类而不是接口,在java中是没法实现这种功能的,内部类可以帮助我们部分解决这种问题,如:
[java] view plaincopy
1. abrstact class A{
2. abstract public void f();
3. }
4. abstract class B{
5. abstract public void g();
6. }
7. public class D extends A{
8. public void f(){}
9. public B makeB(){
10. return new B(){
11. public void g(){};
12. }
13. }
14. }
这样既继承了A类,有在makeB方法中使用匿名内部类继承了B类。
(2).闭包方法问题:
在面向对象中有个术语叫闭包,所谓闭包是指一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。Java中不支持闭包,但是 java的内部类可以看作是对闭包的一种解决方案,因为外部类的所有元素对于普通的内部类来说都是可见的,并且内部类还包含一个指向外部类的对象引用,因 此在作用域内,内部类有权操作外部类的所有成员包括private成员。如:
[java] view plaincopy
1. <pre name="code" class="java">interface Incrementable {
2. void increment();
3. }
4.
5. class Callee1 implements Incrementable {
6. private int i=0;
7. public void increment() {
8. i++;
9. System.out.println(i);
10. }
11. }
12.
13. class MyIncrement {
14. void increment() {
15. System.out.println("other increment");
16. }
17. static void f(MyIncrement mi) {
18. mi.increment();
19. }
20. }
21.
22. class Callee2 extends MyIncrement {
23. private int i=0;
24. private void incr() {
25. i++;
26. System.out.println(i);
27. }
28. //闭包内部类
29. private class Closure implements Incrementable {
30. public void increment() {
31. incr();
32. }
33. }
34. //回调函数
35. Incrementable getCallbackReference() {
36. //新建内部类
37. return new Closure();
38. }
39. }
40.
41. class Caller {
42. private Incrementable callbackRefference;
43. Caller(Incrementable cbh) {
44. callbackRefference = cbh;
45. }
46. void Go() {
47. //调用increment()方法
48. callbackRefference.increment();
49. }
50. }
51.
52. public class Callbacks {
53. public static void main(String [] args) {
54. Callee1 c1=new Callee1();
55. Callee2 c2=new Callee2();
56. MyIncrement.f(c2);
57. Caller caller1 =new Caller(c1);
58. //将内部类中的Closure赋给Caller
59. Caller caller2=new Caller(c2.getCallbackReference());
60. caller1.go();
61. caller1.go();
62. caller2.go();
63. caller2.go();
64. }
65. }
66.
67.
68. 输出:
69. other increment
70. 1
71. 2
72. 1
73. 2
74. Callee2 通过使用内名内部类提供了一个访问自身increment方法的钩子,通过getCallbackReference()方法获取内部类对象的引用,进而通 过内部类的increment方法调用外部类的回调方法,这就是java中用于实现回调方法的闭包。
75.
4——集合容器
1.集合中添加另一个集合的方法:
(1).Collection.addAll(被添加的Collection对象)方法:
如:list1.addAll(list2);
(2).Collections.addAll(添加到的目标Collection对象,可变参数的集合或者对象)方法:
如:Collections.addAll(list1, new Object1(), new Object2()…);
Collectionns.addAll(list1, list2…);
注意:Collections是java集合容器的工具类,相比于(1),使用Collections的(2)更灵活。
2.Java集合中常用的交集、并集和差集操作:
并集:collection对象1.addAll(collection对象2);
交集:collection对象1. retainAll(collection对象2);
差集:collection对象1. removeAll(collection对象2);
注意:上述的集合操作时,集合元素的equals方法会影响操作结果。
3.将其他类型集合转换为List:
Arrays.asList(非List类型的集合对象/可变参数的对象);方法可以将传递的参数转变为List集合。如:Arrays.asList(new Object1(),new Object2(),…);
Arrays和Collections类似,是Array数组类型集合的工具类。
注意:Arrays.asList()方法转换后的List对象是一个size不能改变的对象,如果对该对象做增加或者删除元素操作时,将会报不支持的操作异常。
4.List集合:
List集合主要有两种具体的集合容器:ArrayList和LinkedList。
(1).ArrayList:底层实现是数组,提供了根据数组下标快速随机访问的能力,但是增加和删除元素时因为需要引动数组的元素,因此比较慢。
(2).LinkedList:底层实现是链表,链表访问元素时必须从链表头至链表尾挨个查找,因此只能顺序访问,速度比随机访问要慢。但是增加和删除元素时,只需要修改链表的指针而不需要移动元素,因此速度比较快。
5.LinkedList:
LinkedList除了实现了基本的List接口以外,还提供了一些特定的方法,使得LinkedList可以方便地实现Stack、Queue以及双端Queue的功能。
LinkedList提供的非List接口方法:
(1).getFirst():获取并且不移除LinkedList集合中第一个元素。如果集合为空,抛出NoSuchElementException异常。
(2).element():获取并且不移除LinkedList集合中第一个元素。如果集合为空,抛出NoSuchElementException异常。
(3).peek():获取并且不移除LinkedList集合中第一个元素。如果集合为空,则返回null。
(4).removeFirst():获取并且移除LinkedList集合中第一个元素。如果集合为空,抛出NoSuchElementException异常。
(5).remove():获取并且移除LinkedList集合中第一个元素。如果集合为空,抛出NoSuchElementException异常。
(6).poll():获取并且移除LinkedList集合中第一个元素。如果集合为空,则返回null。
(7).addFirst():向LinkedList集合的头部插入一个元素。
(8).add():向LinkedList集合的尾部插入一个元素。
(9).offer():向LinkedList集合的尾部插入一个元素。
(10).removeLast():获取并且移除LinkedList集合中最后一个元素。如果集合为空,抛出NoSuchElementException异常。
6.Iterator:
Iterator迭代器在java集合容器中应用比较广泛,对于List类型的集合,可以通过下标索引值获取到指定的元素,而对于Set类型的集合,因为Set是没有索引的,因此只能通过迭代器来遍历。
Iterator迭代器是一个顺序选择和遍历集合元素的对象,使用者不需要关心其底层的数据结构和实现方式。Java中的Iterator迭代器是单向的。
Iterator的常用方法如下:
(1).collection对象.iterator()方法:将集合对象转换为Iterator迭代器。
(2).iterator对象.hasNext()方法:判断迭代器中是否还有元素。
(3).iterator对象.next()方法:获取迭代器中下一个元素。
(4).iterator对象.remove()方法:删除迭代器中当前元素。
注意:使用迭代器的好处是,当数据结构从List变为Set之后,迭代集合的相关代码一点都不用改变。
7.ListIterator:
ListIterator是Iterator的子类,它只能有List类型的集合产生,ListIterator是一个双向的迭代器,即它可以向前和向后双向遍历集合。ListIterator的常用方法如下:
(1).list类型对象.listIterator():将List类型的集合转换为ListIterator迭代器。
(2).list类型对象.listIterator(int n):将List类型的集合转换为ListIterator迭代器,同时指定迭代器的起始元素为第n个元素。
(3).listIterator对象.hasNext():判断迭代器中是否还有下一个元素。
(4).listIterator对象.next():获取迭代器中的下一个元素。
(5).listIterator对象.hasPrevious():判断迭代器中是否还有前一个元素。
(6).listIterator对象.previous():获取迭代器中的前一个元素。
(7).listIterator对象.set(元素对象):将当前迭代到的元素设置为另一个值。
8.Map遍历3中方法:
Map<String, Object>map = new HashMap<String, Object>();
map.put(“test1”, object1);
……
map.put(“testn” , objectn);
(1).Map的values()方法可以获取Map值的集合:
[java] view plaincopy
1. Iterator it = map.values().iterator();
2. while(it.hasNext()){
3. Object obj = it.next();
4. }
(2).Map的keySet方法可以获取Map键的Set集合:
[java] view plaincopy
1. Set<String> keys = map.keySet();
2. for(Iterator it = key.iterator(); it.hasNext(); ){
3. String key = it.next();
4. Object obj = map.get(key);
5. }
(3).通过使用Entry来得到Map的key和value:
[java] view plaincopy
1. Set<Map.Entry<String, Object>> entrySet = map.entrySet();
2. for(Iterator <Map.Entry<String, Object>> it = entrySet.iterator(); it.hasNext(); ){
3. Map.Entry<String, Object> entry = it.next();
4. String key = entry.getKey();
5. Object value = entry.getValue();
6. }
9.Collection和Iterator:
Collection是java中除了Map以外的集合容器的通用接口,如果想自定义一种集合容器类型的类,可以选择实现Collection接口或者继承Collection的子类。
实现Collection接口或者继承Collection子类的时候,必须实现Collection接口的所有方法,而Iterator为定义自 定义集合容器类型提供了另一种方便,Iterator是一种轻量级的接口,只需要实现hasNext(),next()方法即可,remove()方法是 可选方法。
注意:Foreach循环支持所有实现了Iterable接口的集合容器(Collection接口的父接口是Iterable),Map集合没有实现Iterable接口,因此不支持Foreach循环。
10.java集合容器框架图:
5——正则表达式量词匹配
Java正则表达式有3中量词匹配模式:
1.贪婪量词:
先看整个字符串是否匹配,如果没有发现匹配,则去掉最后字符串中的最后一个字符,并再次尝试,如果还是没有发现匹配,那么,再次去掉最后一个字符串的最后一个字符,整个匹配过程会一直重复直到发现一个匹配或者字符串不剩任何字符。简单量词都是贪婪量词。
贪婪量词匹配时,首先将整个字符串作为匹配的对象,然后逐步从后向前移除不匹配的字符,尽可能找到最多的匹配。
2.惰性量词:
先看字符串中的第一个字符是否匹配,如果单独一个字符不够,则读入下一个字符,组成两个字符的字符串,如果还没有发现匹配,惰性量词继续从字符串中添加字符直到发现一个匹配或者整个字符串全部检查完都不匹配。惰性量词和贪婪量词工作方式恰好相反。
惰性量词匹配时,只匹配第一个字符,然后依次添加字符,尽可能找到最少匹配。
3.支配量词:
只尝试匹配整个字符串,如果整个字符串不能产生匹配,则不进行进一步尝试。
支配量词目前只有java中支持,支持量词是贪婪量词第一次匹配不成功时,阻止正则表达式继续匹配,使得正则表达式效率更高。
贪婪量词 惰性量词 支配量词 描述
X? X?? X?+ X出现0次或者1次
X* X*? X*+ X出现0次或者多次
X+ X+? X++ X出现1次或者多次
X{n} X{n}? X{n}+ X只出现n次
X{n,} X{n,}? X{n,}+ X至少出现n次
X{n,m} X{n,m}? X{n,m}+ X至少出现n次,至多不超过m次
6——Java动态代理
代理是一种常用的程序设计模式,如同网络代理一样,代理是介于调用者和真正调用目标对象之间的中间对象,代理在调用真正目标对象时提供一些额外或者不同的操作,真正的对目标对象的操作还是通过代理调用目标对象来完成。
简单的代理例子如下:
[java] view plaincopy
1. //接口
2. interface Interface{
3. void doSomething();
4. void somethingElse(String arg);
5. }
6. //目标对象
7. class RealObject implement Interface{
8. public void doSomething(){
9. System.out.println(“RealObject doSomething”);
10. }
11. public void somethingElse(String arg){
12. System.out.println(“RealObject somethingElse ” + arg);
13. }
14. }
15. //简单代理对象
16. class SimpleProxy implements Interface(
17. private Interface proxied;
18. public SimpleProxy(Interface proxied){
19. this.proxied = proxied;
20. }
21. public void doSomething(){
22. System.out.println(“SimpleProxy doSomething”);
23. proxied.doSomething();
24. }
25. public void somethingElse(String arg){
26. System.out.println(“SimpleProxy somethingElse ” + arg);
27. proxied.somethingElse(arg);
28. }
29. )
30. Class SimpleProxyDemo{
31. public static void consumer(Interface iface){
32. iface.doSomething();
33. iface.somethingElse(“TestProxy”);
34. }
35. public static void main(String[] args){
36. //不是用代理
37. cosumer(new RealObject());
38. //使用代理
39. cosumer(new SimpleProxy(new RealObject()));
40. }
41. }
输出结果为:
RealObject doSomething
RealObjectsomethingElse TestProxy
SimpleProxy doSomething
RealObject doSomething
SimpleProxy somethingElse TestProxy
RealObject somethingElse TestProxy
上面例子可以看出代理SimpleProxy在调用目标对象目标方法之前做了一些额外的操作。
Java中的代理是针对接口的动态代理,当然java也可以使用第三方的CGLIB实现针对类的代理,但是JDK中只支持针对接口的动态代理,我们只分析JDK的动态代理。
JDK动态代理的要素:
(1).实现了InvocationHandler的代理处理类,实现其invoke方法,该方法是代理调用目标对象方法以及提供额外操作的方法。
(2).使用Proxy.newProxyInstance(类加载器, 代理接口列表,InvocationHandler对象);方法创建实现了指定接口的动态代理。
JDK的代理例子如下:
[java] view plaincopy
1. //接口
2. interface Interface{
3. void doSomething();
4. void somethingElse(String arg);
5. }
6. //目标对象
7. class RealObject implement Interface{
8. public void doSomething(){
9. System.out.println(“RealObject doSomething”);
10. }
11. public void somethingElse(String arg){
12. System.out.println(“RealObject somethingElse ” + arg);
13. }
14. }
15. //代理处理类
16. class DynamicProxyHandler implements InvocationHandler{
17. provate Object proxied;
18. public DynamicProxyHandler(Object proxied){
19. this.proxied = proxied;
20. }
21. //动态代理调用目标对象的方法
22. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
23. System.out.println(“Dynamic proxy invoke”);
24. return method.invoke(proxied, args);
25. }
26. }
27. class SimpleDynamicProxy{
28. public static void consumer(Interface iface){
29. iface.doSomething();
30. iface.somethingElse(“DynamicProxy”);
31. }
32. public static void main(String[] args){
33. RealObject real = new RealObject();
34. //不是用代理
35. consumer(real);
36. //创建动态代理
37. Interface proxy = (Interface) Proxy.newProxyInstance(
38. Interface.class.getClassLoader(),
39. new Class[]{Interface.class},
40. new DynamicProxyHandler(real)
41. );
42. cosumer(proxy);
43. }
44. }
输出结果为:
RealObject doSomething
RealObject somethingElse DynamicProxy
Dynamic proxy invoke
RealObject doSomething
Dynamic proxyinvoke
RealObject somethingElse DynamicProxy
7——泛型编程基础
一般的类和方法都是针对特定数据类型的,当写一个对多种数据类型都适用的类和方法时就需要使用泛型编程,java的泛型编程类似于C++中的模板, 即一种参数化类型的编程方法,具体地说就是将和数据类型相关的信息抽象出来,主要提供通用的实现和逻辑,和数据类型相关的信息由使用时参数决定。
1.泛型类/接口:
(1).泛型接口:
如一个提供产生指定类的接口:
[java] view plaincopy
1. public interface Gernerator<T>{
2. T next() ;
3. }
4. public class A implement Generator<A>{
5. A next(){
6. return new A();
7. }
8. }
(2).泛型类:
一个使用泛型实现的栈数据结构如下:
[java] view plaincopy
1. public class LinkedListStack<T>{
2. //节点内部类
3. private static class Node<U>{
4. U item;
5. Node<U> next;
6. Node(){
7. item = null;
8. next = null;
9. }
10. Node(U item, Node<U> next){
11. this.item = item;
12. this.next = next;
13. }
14. Boolean end(){
15. return item == null && next == null;
16. }
17. }
18. private Node<T> top = new Node<T>();
19. public void push<T>(T item){
20. top = new Node<T>(item, top);
21. }
22. public T pop(){
23. T result = top.item;
24. if(!top.end()){
25. top = top.next();
26. }
27. return result;
28. }
29. }
使用这个使用泛型实现的栈,可以操作各种数据类型。
2.泛型方法:
例如:
[java] view plaincopy
1. public class GenericMethods{
2. public <T> void f(T x){
3. System.out.println(x.getClass().getName()) ;
4. }
5. public static void main(String[] args){
6. GenericMethods gm = new GenericMethods();
7. gm.f(“”);
8. gm.f(1);
9. gm.f(1.0);
10. ……
11. }
12. }
输出结果为:
java.lang.String
java.lang.Integer
java.lang.Double
3.泛型集合:
Java中泛型集合使用的非常广泛,在Java5以前,java中没有引入泛型机制,使用java集合容器时经常遇到如下两个问题:
a. java容器默认存放Object类型对象,如果一个容器中即存放有A类型对象,又存放有B类型对象,如果用户将A对象和B对象类型弄混淆,则容易产生转换错误,会发生类型转换异常。
b. 如果用户不知道集合容器中元素的数据类型,同样也可能会产生类型转换异常。
鉴于上述的问题,java5中引入了泛型机制,在定义集合容器对象时显式指定其元素的数据类型,在使用集合容器时,编译器会检查数据类型是否和容器指定的数据类型相符合,如果不符合在无法编译通过,从编译器层面强制保证数据类型安全。
(1).java常用集合容器泛型使用方法:
如:
[java] view plaincopy
1. public class New{
2. public static <K, V> Map<K, V> map(){
3. return new HashMap<K, V>();
4. }
5. public static <T> List<T> list(){
6. return new ArrayList<T>() ;
7. }
8. public static <T> LinkedList<T> lList(){
9. return new LinkedList<T>();
10. }
11. public static <T> Set<T> set(){
12. return new HashSet<T>();
13. }
14. public static <T> Queue<T> queue(){
15. return new LinkedList<T>() ;
16. }
17. ;public static void main(String[] args){
18. Map<String, List<String>> sls = New.map();
19. List<String> ls = New.list();
20. LinkedList<String> lls = New.lList();
21. Set<String> ss = New.set();
22. Queue<String> qs = New.queue();
23. }
24. }
(2).Java中的Set集合是数学上逻辑意义的集合,使用泛型可以很方便地对任何类型的Set集合进行数学运算,代码如下:
[java] view plaincopy
1. public class Sets{
2. //并集
3. public static <T> Set<T> union(Set<T> a, Set<T> b){
4. Set<T> result = new HashSet<T>(a);
5. result.addAll(b);
6. return result;
7. }
8. //交集
9. public static <T> Set<T> intersection(Set<T> a, Set<T> b){
10. Set<T> result = new HashSet<T>(a);
11. result.retainAll(b);
12. return result;
13. }
14. //差集
15. public static <T> Set<T> difference(Set<T> a, Set<T> b){
16. Set<T> result = new HashSet<T>(a);
17. result.removeAll(b);
18. return Result;
19. }
20. //补集
21. public static <T> Set<T> complement(Set<T> a, Set<T> b){
22. return difference(union(a, b), intersection(a, b));
23. }
24. }
8——泛型编程高级
1.泛型边界:
Java泛型编程时,编译器忽略泛型参数的具体类型,认为使用泛型的类、方法对Object都适用,这在泛型编程中称为类型信息檫除。
例如:
[java] view plaincopy
1. class GenericType{
2. public static void main(String[] args){
3. System.out.println(new ArrayList<String>().getClass());
4. System.out.println(new ArrayList<Integer>().getClass());
5. }
6. }
输出结果为:
java.util.ArrayList
java.util.ArrayList
泛型忽略了集合容器中具体的类型,这就是类型檫除。
但是如果某些泛型的类/方法只想针对某种特定类型获取相关子类应用,这时就必须使用泛型边界来为泛型参数指定限制条件。
例如:
[java] view plaincopy
1. interface HasColor{
2. java.awt.Color getColor();
3. }
4. class Colored<T extends HasColor>{
5. T item;
6. Colored(T item){
7. this.item = item;
8. }
9. java.awt.Color color(){
10. //调用HasColor接口实现类的getColor()方法
11. return item.getColor();
12. }
13. }
14. class Dimension{
15. public int x, y, z;
16. }
17. Class ColoredDimension<T extends Dimension & HasColor>{
18. T item;
19. ColoredDimension(T item){
20. this.item = item;
21. }
22. T getItem(){
23. return item;
24. }
25. java.awt.Color color(){
26. //调用HasColor实现类中的getColor()方法
27. return item.getColor();
28. }
29. //获取Dimension类中定义的x,y,z成员变量
30. int getX(){
31. return item.x;
32. }
33. int getY(){
34. return item.y;
35. }
36. int getZ(){
37. return item.z;
38. }
39. }
40. interface Weight{
41. int weight();
42. }
43. class Solid<T extends Dimension & HasColor & Weight>{
44. T item;
45. Solide(T item){
46. this.item = item;
47. }
48. T getItem(){
49. return item;
50. }
51. java.awt.Color color(){
52. //调用HasColor实现类中的getColor()方法
53. return item.getColor();
54. }
55. //获取Dimension类中定义的x,y,z成员变量
56. int getX(){
57. return item.x;
58. }
59. int getY(){
60. return item.y;
61. }
62. int getZ(){
63. return item.z;
64. }
65. int weight(){
66. //调用Weight接口实现类的weight()方法
67. return item.weight();
68. }
69. }
70. class Bounded extends Dimension implements HasColor, Weight{
71. public java.awt.Color getColor{
72. return null;
73. }
74. public int weight(){
75. return 0;
76. }
77. }
78. public class BasicBounds{
79. public static void main(String[] args){
80. Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
81. solid.color();
82. solid.getX();
83. solid.getY();
84. solid.getZ();
85. solid.weight();
86. }
87. }
Java泛型编程中使用extends关键字指定泛型参数类型的上边界(后面还会讲到使用super关键字指定泛型的下边界),即泛型只能适用于extends关键字后面类或接口的子类。
Java泛型编程的边界可以是多个,使用如<T extends A & B & C>语法来声明,其中只能有一个是类,并且只能是extends后面的第一个为类,其他的均只能为接口(和类/接口中的extends意义不同)。
使用了泛型边界之后,泛型对象就可以使用边界对象中公共的成员变量和方法。
2.泛型通配符:
泛型初始化过程中,一旦给定了参数类型之后,参数类型就会被限制,无法随着复制的类型而动态改变,如:
[java] view plaincopy
1. class Fruit{
2. }
3. class Apple extends Fruit{
4. }
5. class Jonathan extends Apple{
6. }
7. class Orange extends Fruit{
8. }
9. 如果使用数组:
10. public class ConvariantArrays{
11. Fruit fruit = new Apple[10];
12. Fruit[0] = new Apple();
13. Fruit[1] = new Jonathan();
14. try{
15. fruit[0] = new Fruit();
16. }catch(Exception e){
17. System.out.println(e);
18. }
19. try{
20. fruit[0] = new Orange();
21. }catch(Exception e){
22. System.out.println(e);
23. }
24. }
编译时没有任何错误,运行时会报如下异常:
java.lang.ArrayStoreException:Fruit
java.lang.ArrayStoreException:Orange
为了使得泛型在编译时就可以进行参数类型检查,我们推荐使用java的集合容器类,如下:
[java] view plaincopy
1. public class NonConvariantGenerics{
2. List<Fruit> flist = new ArrayList<Apple>();
3. }
很不幸的是,这段代码会报编译错误:incompatible types,不兼容的参数类型,集合认为虽然Apple继承自Fruit,但是List的Fruit和List的Apple是不相同的,因为泛型参数在声 明时给定之后就被限制了,无法随着具体的初始化实例而动态改变,为解决这个问题,泛型引入了通配符”?”。
对于这个问题的解决,使用通配符如下:
[java] view plaincopy
1. public class NonConvariantGenerics{
2. List<? extends Fruit> flist = new ArrayList<Apple>();
3. }
泛型通配符”?”的意思是任何特定继承Fruit的类,java编译器在编译时会根据具体的类型实例化。
另外,一个比较经典泛型通配符的例子如下:
public class SampleClass < T extendsS> {…}
假如A,B,C,…Z这26个class都实现了S接口。我们使用时需要使用到这26个class类型的泛型参数。那实例化的时候怎么办呢?依次写下
SampleClass<A> a = new SampleClass();
SampleClass<B> a = new SampleClass();
…
SampleClass<Z> a = new SampleClass();
这显然很冗余,还不如使用Object而不使用泛型,使用通配符非常方便:
SampleClass<? Extends S> sc = newSampleClass();
3.泛型下边界:
在1中大概了解了泛型上边界,使用extends关键字指定泛型实例化参数只能是指定类的子类,在泛型中还可以指定参数的下边界,是一super关键字可以指定泛型实例化时的参数只能是指定类的父类。
例如:
[java] view plaincopy
1. class Fruit{
2. }
3. class Apple extends Fruit{
4. }
5. class Jonathan extends Apple{
6. }
7. class Orange extends Fruit{
8. }
9. public superTypeWildcards{
10. public static void writeTo(List<? super Apple> apples){
11. apples.add(new Apple());
12. apples.add(new Jonathan());
13. }
14. }
通过? Super限制了List元素只能是Apple的父类。
泛型下边界还可以使用<?super T>,但是注意不能使用<Tsuper A>,即super之前的只能是泛型通配符,如:
[java] view plaincopy
1. public class GenericWriting{
2. static List<Apple> apples = new ArrayList<Apple>();
3. static List<Fruit> fruits = new ArrayList<Fruit>();
4. static <T> void writeExact(List<T> list, T item){
5. list.add(item);
6. }
7. static <T> void writeWithWildcards(List<? super T> list, T item){
8. list.add(item);
9. }
10. static void f1(){
11. writeExact(apples, new Apple());
12. }
13. static void f2(){
14. writeWithWildcards(apples, new Apple());
15. writeWithWildcards(fruits, new Apple());
16. }
17. public static void main(String[] args){
18. f1();
19. f2();
20. }
21. }
4.无边界的通配符:
泛型的通配符也可以不指定边界,没有边界的通配符意思是不确定参数的类型,编译时泛型檫除类型信息,认为是Object类型。如:
[java] view plaincopy
1. public class UnboundedWildcard{
2. static List list1;
3. static List<?> list2;
4. static List<? extends Object> list3;
5. static void assign1(List list){
6. list1 = list;
7. list2 = list;
8. //list3 = list; //有未检查转换警告
9. }
10. static void assign2(List<?> list){
11. list1 = list;
12. list2 = list;
13. list3 = list;
14. }
15. static void assign3(List<? extends Object> list){
16. list1 = list;
17. list2 = list;
18. list3 = list;
19. }
20. public static void main(String[] args){
21. assign1(new ArrayList());
22. assign2(new ArrayList());
23. //assign3(new ArrayList()); //有未检查转换警告
24. assign1(new ArrayList<String>());
25. assign2(new ArrayList<String>());
26. assign3(new ArrayList<String>());
27. List<?> wildList = new ArrayList();
28. assign1(wildList);
29. assign2(wildList);
30. assign3(wildList);
31. }
32. }
List和List<?>的区别是:List是一个原始类型的List,它可以存放任何Object类型的对象,不需要编译时类型检 查。List<?>等价于List<Object>,它不是一个原始类型的List,它存放一些特定类型,只是暂时还不确定是什 么类型,需要编译时类型检查。因此List的效率要比List<?>高。
5.实现泛型接口注意事项:
由于泛型在编译过程中檫除了参数类型信息,所以一个类不能实现以泛型参数区别的多个接口,如:
[java] view plaincopy
1. interface Payable<T>{
2. }
3. class Employee implements Payable<Employee>{
4. }
5. class Hourly extends Employee implements Payable<Hourly>{
6. }
类Hourly无法编译,因为由于泛型类型檫除,Payable<Employee>和Payable<Hourly>在编译时是同一个类型Payable,因此无法同时实现一个接口两次。
6.泛型方法重载注意事项:
由于泛型在编译时将参数类型檫除,因此以参数类型来进行方法重载在泛型中要特别注意,如:
[java] view plaincopy
1. public class GenericMethod<W,T>{
2. void f(List<T> v) {
3. }
4. void f(List<W> v){
5. }
6. }
无法通过编译,因为泛型檫除类型信息,上面两个方法的参数都被看作为Object类型,使用参数类型已经无法区别上面两个方法,因此无法重载。
7.泛型中的自绑定:
通常情况下,一个类无法直接继承一个泛型参数,但是你可以通过继承一个声明泛型参数的类,这就是java泛型编程中的自绑定,如:
[java] view plaincopy
1. class SelfBounded<T extends SelfBounded<T>>{
2. T element;
3. SelfBounded<T> set(T arg){
4. Element = arg;
5. return this;
6. }
7. T get(){
8. return element;
9. }
10. }
11. class A extends SelfBounded<A>{
12. }
13. class B extends SelfBounded<A>{
14. }
15. class C extends SelfBounded<C>{
16. C setAndGet(C arg){
17. set(arg);
18. return get();
19. }
20. }
21. public class SelfBounding{
22. public static void main(String[] args){
23. A a = new A();
24. a.set(new A());
25. a = a.set(new A()).get();
26. a = a.get();
27. C c = new C();
28. C = c.setAndGet(new C());
29. }
30. }
泛型的自绑定约束目的是用于强制继承关系,即使用泛型参数的类的基类是相同的,强制所有人使用相同的方式使用参数基类。
9——集合容器高级
1.Arrays.asList()方法产生的List是一个固定长度的数组,只支持不改变长度的操作,任何试图改变其底层数据结构长度的操作(如,增加,删除等操作)都会抛出UnsupportedOperationException异常。
为了使Arrays.asList()方法产生的List集合长度可变,可以将其作为集合容器的构造方法参数,如:
Set set = new HashSet(Arrays.asList(newint[]{1,23}));
或者将其作为Collections.addAll()方法的参数,如:
Collections.addAll(Arrays.asList(new int[]{1,23}));
2.SortedSet是一个对其元素进行排序了的Set,SortedSet接口有以下方法:
(1).Comparator comparator():
返回此Set中元素进行排序的比较器,如果该方法返回null,则默认使用自然排序。
(2).Object first():
返回Set中第一个(最低)元素。
(3).Object last():
返回Set中最后一个(最高)元素。
(4).SortedSet subset(fromElement, toElement):
返回此Set中从fromElement(包括)到toElement(不包括)的子Set。
(5).SortedSet headset(toElement):
返回此Set中元素严格小于toElement的子Set。
(6).SortedSet tailSet(fromElement):
返回此Set中元素大于等于fromElement的子Set。
3.SortedMap是一个根据Key排序的Map,SortedMap接口有以下方法:
(1).Comparator comparator():
返回此Map中key进行排序的比较器,如果返回的是null,则该Map的key使用自然排序。
(2).T firstKey():
返回此Map中第一个(最低)key。
(3).T lastKey();
返回此Map中最后一个(最高)key。
(4).SortedMap subMap(fromKey, toKey):
返回此Map中key从fromKey(包括)到toKey(不包括)的子Map。
(5).SortedMap headMap(toKey):
返回此Map中key严格小于toKey的子Map。
(6).SortedMap tailMap(fromKey):
返回此Map中key大于等于fromKey的Map。
4.HashMap/HashSet等Hash算法集合重写equals方法和hashCode方法:
HashSet,HashMap以及它们的子类等这些使用Hash算法的集合存放对象时,如果元素不是java中的8种基本类型(即元素都是对象类型),则必须重写对象的equals和hashCode方法,否则会产生一些错误,例如:
[java] view plaincopy
1. Class A{
2. int i;
3. public A(int i){
4. this.i = i;
5. }
6. Public static void main(String args[]){
7. Map map = new HashMap();
8. map.put(new A(1), “First”);
9. map.put(new A(2), “Second”);
10. A a = new A(1);
11. boolean b = map.containsKey(a);
12. System.out.println(b);
13. }
14. }
输出的结果是:false。
Map中有Key为A(1)对象,但是却没有找到,这是因为HashMap使用Hash算法根据对象的hashCode值来查找给定的对象,如果没 有重写hashCode和equals方法,则对象默认使用Object的hashCode方法,Object默认hashCode方法使用对象的内存地 址作为hashCode值。
为了使Hash算法集合存放对象类型数据符合用户的期望,必须重写对象的hashCode和equals方法,其中hashCode方法用于Hash算法的查找,equals方法用于对象比较,Hash算法中还要用到equals方法如下:
因为Hash算法所使用的hashCode可能会产生碰撞(不相等对象的hashCode值相同),当Hash算法产生碰撞时,就需要再次 Hash(即再次通过其他方法计算hashCode值),所以一个对象使用Hash算法时,其对应的HashCode值可能不止一个,而是一组,当产生碰 撞时就选择另一个hashCode值。当hashCode值产生碰撞时,还必须使用equals方法方法对象是否相等。
注意:由于Hash算法有可能会产生碰撞(不相等的对象hashCode值相同),所以hashCode和equals方法有如下关系:
(1).equals方法相等的对象,hashCode方法值一定相同。
(2).hashCode方法相同的对象,equals不一定相等。
5.创建只读集合容器:
List,Set和Map类型的集合容器都可以通过下面的方法创建为只读,即只可以访问,不能添加,删除和修改。
[java] view plaincopy
1. static Collection<String> data = new ArrayList<String>();
2. data.add(“test”);
3. static Map<String, String> m = new HashMap<String, String>();
4. m.put(“key”, “value”);
(1).只读集合:
[java] view plaincopy
1. Collection<String> c = Collections.unmodifiableCollection(new ArrayList<String>(data));
2. System.out.println(c); //可以访问
3. //c.add(“test2”);只读,不可添加
(2).只读List:
[java] view plaincopy
1. List<String> list = Collections.unmodifiableList(new ArrayList<String>(data));
2. System.out.println(list.get(0)); //可以访问
3. //list.remove(0);只读,不可删除
(3).只读Set:
[java] view plaincopy
1. Set<String> set = Collections.unmodifiableSet(new HashSet<String>(data));
2. System.out.println(set.Iterator().next()) //可以访问
3. //set.add(“test”);只读,不可添加
(4).只读Map:
[java] view plaincopy
1. Map<String, String> map = Collections.unmodifiableMap(new HashMap<String, String>(m));
2. System.out.println(map.get(“key”)); //可以访问
3. //map.put(“key2”, “value2”);只读,不可添加
只读集合容器会在编译时检查操作,如果对只读集合容器进行增删等操作时,将会抛出UnSupportedOperationException异常。
只读集合容器类似于将集合对象访问控制修饰符设置为private,不同之处在于,其他类可以访问,只是不能修改。
6.线程同步集合容器:
Java集合容器中,Vector,HashTable等比较古老的集合容器是线程安全的,即处理了多线程同步问题。
而Java2之后对Vector和HashTable的替代类ArrayList,HashSet,HashMap等一些常用的集合容器都是非线程安全的,即没有进行多线程同步处理。
Java中可以通过以下方法方便地将非线程安全的集合容器进行多线程同步:
(1).线程同步集合:
Collection<String> c= Collections.synchronizedCollection(newArrayList<String>());
(2).线程同步List:
List<String> c= Collections.synchronizedList(newArrayList<String>());
(3).线程同步Set:
Set<String> c= Collections.synchronizedSet(newHashSet<String>());
(4).线程同步Map:
Map<String> c= Collections.synchronizedMap(newHashMap<String, String>());
7.对象的强引用、软引用、弱引用和虚引用:
JDK1.2以前版本中,只存在一种引用——正常引用,即对象强引用,如果一个对象被一个引用变量所指向,则该对象是可触及(reached)的状态,JVM垃圾回收器不会回收它,弱一个对象不被任何引用变量指向,则该对象就处于不可触及的状态,垃圾回收器就会回收它。
从JDK1.2版本之后,为了更灵活的控制对象生命周期,引入了四种引用:强引用(java.lang.ref.Reference)、软引用 (java.lang.ref.SoftReference)、弱引用(java.lang.ref.WeakReference)和虚引用 (java.lang.ref.PhantomReference):
(1). 强引用(java.lang.ref.Reference):
即Java程序中普遍使用的正常对象引用,存放在内存中得对象引用栈中,如果一个对象被强引用,则说明程序还在使用它,垃圾回收器不会回收,当内存不足时,JVM抛出内存溢出异常使程序异常终止。
(2). 软引用(java.lang.ref.SoftReference):
如果一个对象只具有软引用,则内存空间足够,垃圾回收器也不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,JVM就会把这个软引用加入到与之关联的引用队列中。
(3). 弱引用(java.lang.ref.WeakReference):
弱引用用来实现内存中对象的标准映射,即为了节约内存,对象的实例可以在一个程序内多处使用。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程 中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的后台线程,因此不一定会很快发 现那些只具有弱引用的对象。
弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
(4). 虚引用(java.lang.ref.PhantomReference):
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);
JVM中,对象的引用往往都是很复杂的,各个对象之间相互引用形成一个内存引用树,java中某个对象是否可触及,有以下两条判断原则:
a.单条引用路径可及性判断:在这条路径中,最弱的一个引用决定对象的可及性。
b.多条引用路径可及性判断:几条路径中,最强的一条的引用决定对象的可及性。
软引用,弱引用,虚引用例子如下:
[java] view plaincopy
1. package com.test.reference;
2. //测试对象
3. class VeryBig {
4. private static final int SIZE = 10000;
5. private long[] la = new long[SIZE];
6. private String ident;
7.
8. public VeryBig(String id) {
9. ident = id;
10. }
11.
12. public String toString() {
13. return ident;
14. }
15.
16. protected void finalize() {
17. System.out.println("Finalizing " + ident);
18. }
19. }
20.
21. package com.test.reference;
22.
23. import java.lang.ref.PhantomReference;
24. import java.lang.ref.Reference;
25. import java.lang.ref.ReferenceQueue;
26. import java.lang.ref.SoftReference;
27. import java.lang.ref.WeakReference;
28. import java.util.LinkedList;
29.
30. public class TestReferences {
31. //引用队列
32. private static ReferenceQueue<VeryBig> rq = new ReferenceQueue<VeryBig>();
33. //检查引用队列是否为空
34. public static void checkQueue() {
35. //强引用,轮询引用队列,看是否有可以的对象引用
36. Reference<? extends VeryBig> inq = rq.poll();
37. if (inq != null)
38. //如果有可以使用的对象引用,则打印出该引用指向的对象
39. System.out.println("In queue: " + inq.get());
40. }
41.
42. public static void main(String[] args) {
43. int size = 2;
44. //创建存放VeryBig对象的软引用集合
45. LinkedList<SoftReference<VeryBig>> sa = new LinkedList<SoftReference<VeryBig>>();
46. for (int i = 0; i < size; i++) {
47. //将对象和软引用添加到引用队列中
48. sa.add(new SoftReference<VeryBig>(new VeryBig("Soft " + i), rq));
49. System.out.println("Just created: " + sa.getLast());
50. checkQueue();
51. }
52. //创建存放VeryBig对象的弱引用集合
53. LinkedList<WeakReference<VeryBig>> wa = new LinkedList<WeakReference<VeryBig>>();
54. for (int i = 0; i < size; i++) {
55. //将对象和弱引用添加到引用队列中
56. wa.add(new WeakReference<VeryBig>(new VeryBig("Weak " + i), rq));
57. System.out.println("Just created: " + wa.getLast());
58. checkQueue();
59. }
60. SoftReference<VeryBig> s = new SoftReference<VeryBig>(new VeryBig(
61. "Soft"));
62. WeakReference<VeryBig> w = new WeakReference<VeryBig>(new VeryBig(
63. "Weak"));
64. //垃圾回收器回收,在回收之前调用对象的finalize()方法
65. System.gc();
66. //创建存放VeryBig对象的虚引用集合
67. LinkedList<PhantomReference<VeryBig>> pa = new LinkedList<PhantomReference<VeryBig>>();
68. for (int i = 0; i < size; i++) {
69. //将对象和虚引用添加到引用队列中
70. pa.add(new PhantomReference<VeryBig>(new VeryBig("Phantom " + i),
71. rq));
72. System.out.println("Just created: " + pa.getLast());
73. checkQueue();
74. }
75. }
76. }
输出结果为:
Just created:[email protected]
Just created:[email protected]
Just created:[email protected]
Just created:[email protected]
In queue: null
Finalizing Weak 0
Finalizing Weak
Finalizing Weak 1
Just created:[email protected]
In queue: null
Just created:[email protected]
注意:由于System.gc()只是通知JVM虚拟机可以进行垃圾回收器可以进行垃圾回收了,但是垃圾回收器具体什么时候允许说不清楚,所以这个输出结果只是个参考,每次运行的结果Finalize方法的执行顺序不太一样。
从程序可以看出,尽管对象被引用,垃圾回收器还是回收了被引用对象,引用队列总是创建一个包含null对象的引用。
8.WeakHashMap:
WeakHashMap专门用于存放弱引用,WeakHashMap很容易实现弱引用对象标准映射功能。
在WeakHashMap中,只存储一份对象的实例及其值,当程序需要对象实例值时,WeakHashMap从现有的映射中找出已存在的对象值映射。
由于弱引用节约内存的技术,WeakHashMap允许垃圾回收器自动清除器存放的key和value。WeakHashMap自动将其中存放的key和value包装为弱引用,当key不再被使用时,垃圾回收器自动回收该key和value。
10——文件和目录常用操作
1.文件目录的List操作:
Java中,File类其实代表文件的路径,它既可以代表一个特定文件的文件,也可以代表一个包含多个文件的目录名称。如果File代表目录,可以使用List列出目录中文件。
[java] view plaincopy
1. import java.util.regex.*;
2. import java.io.*;
3. import java.util.*;
4. public class DirList{
5. public static void main(String[] args){
6. //当前目录
7. File path = new File(“.”);
8. String[] list;
9. //如果没有指定参数,则将目录中文件全部列出
10. if(args.length == 0){
11. list = path.list();
12. }
13. //指定了参数,则根据指定文件名过滤符合条件的文件
14. else{
15. list = path.list(new DirFilter(args[0]));
16. }
17. Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
18. for(String dirItem : list){
19. System.out.println(dirItem);
20. }
21. }
22. }
23. class DirFilter implements FilenameFilter{
24. private Pattern pattern;
25. public DirFilter(String regex){
26. //将输入的命令行参数编译为正则表达式的模式串
27. pattern = Pattern.compile(regex);
28. }
29. //File的List方法回调方法
30. public boolean accept(File dir, String name){
31. //使用正则表达式匹配给定目录下的文件名
32. return pattern.matcher(name).matches();
33. }
34. }
命令行输入参数:“*\.java”
输出结果如下:
DirFilter.java
DirList.java
2.java中除了File类可以表示文件的路径外,还可以表示目录的路径,可以通过File的isDirectory判断File对象是一个文件还是一个目录。
如下的例子通过local()方法列出给定目录中符合条件的文件/目录名称,walk()方法遍历给定的目录:
[java] view plaincopy
1. import java.util.regex.*;
2. import java.io.*;
3. import java.util.*;
4.
5. public final class Directory{
6. //列出目录中符合条件的文件名
7. public static File[] local(File dir, final String regex){
8. return dir.listFiles(new FilenameFilter(){
9. private Pattern pattern = Pattern.compile(regex);
10. public Boolean accept(File dir, String name){
11. return pattern.matcher(new File(name).getName()).matches();
12. }
13. });
14. }
15. //重载列出目录下符合条件的文件名方法
16. public static File[] local(String path, final String regex){
17. return local(new File(path), regex);
18. }
19. //代表文件树信息的静态内部类
20. public static class TreeInfo implements Iterable<File>{
21. public List<File> files = new ArrayList<File>();
22. public List<File> dirs = new ArrayList<File>();
23. //默认的迭代器方法,跌倒文件树元素对象
24. public Iterator<File> iterator(){
25. return files.iterator();
26. }
27. void addAll(TreeInfo other){
28. files.addAll(other.files);
29. dirs.addAll(other.dirs);
30. }
31. public String toString(){
32. return “dirs: ” + dirs + “\n\nfiles: ” + files;
33. }
34. }
35. //从指定的文件/目录开始遍历符合条件的文件
36. public static TreeInfo walk(String start, String regex){
37. return recurseDirs(new File(start), regex);
38. }
39. //重载遍历文件/目录方法
40. \public static TreeInfo walk(File start, String regex){
41. return recurseDirs(start, regex);
42. }
43. //默认的指定文件/目录查找任何文件名的文件
44. public static TreeInfo walk(File start){
45. return recurseDirs(start, “.*”);
46. }
47. //重载默认的查找任何文件的方法
48. public static TreeInfo walk(String start){
49. return recurseDirs(new File(start), “.*”);
50. }
51. //从指定的文件/目录开始遍历,查找符合条件的文件名
52. static TreeInfo recurseDirs(File startDir, String regex){
53. TreeInfo result = new TreeInfo();
54. for(File item : startDir.listFiles()){
55. //如果遍历的文件是目录
56. if(item.isDirectory()){
57. result.dirs.add(item);
58. //迭代子目录
59. result.addAll(recurseDirs(item, regex));
60. }
61. //如果遍历的的文件是普通文件
62. else{
63. if(item.getName().matches(regex)){
64. result.files.add(item);
65. }
66. }
67. }
68. return result;
69. }
70. }
3.文件和目录的其他操作:
文件和目录除了常规的查找和遍历操作意外,还有很多其他的操作,例如:创建、删除、判断文件/目录是否已存在,获取文件.目录的绝对路径,已经文件/目录的权限等等,下面的小例子就展示文件/目录的这些操作:
[java] view plaincopy
1. import java.io.*;
2. public class MakeDirectories{
3. //获取文件/目录的基本信息
4. private static void fileData(File f){
5. System.out.println(
6. “Absolute path: ” + f.getAbsolutePath() +
7. “\n Can read: ” + f.canRead() +
8. “\n Can write: ” + f.canWrite() +
9. “\n getName: ” + f.getName() +
10. “\n getParent: ” + f.getParent() +
11. “\n getPath: ” + f.getPath() +
12. “\n length: ” + f.length() +
13. “\n lastModified: ” + f.lastModifed());
14. if(f.isFile()){
15. System.out.println(f.getName() + “ is a file”);
16. }
17. else if(f.isDirectory()){
18. System.out.println(f.getName() + “ is a directory”);
19. }
20. }
21. public static void main(String[] args){
22. File old = new File(“oldFile”);
23. File new = new File(“newFile”);
24. old.renameTo(new);
25. fileData(old);
26. fileData(new);
27. File d = new File(“/test”);
28. if(d.exists()){
29. System.out.println(“Deleting …” + d);
30. d.delete();
31. }
32. else {
33. System.out.prinln(“Creating…” + d);
34. d.mkdirs();
35. }
36. }
37. }
11——Java I/O
Java中使用流来处理程序的输入和输出操作,流是一个抽象的概念,封装了程序数据于输入输出设备交换的底层细节。JavaIO中又将流分为字节流和字符流,字节流主要用于处理诸如图像,音频视频等二进制格式数据,而字符流主要用于处理文本字符等类型的输入输出。
1.字节输入流InputStream
输入流InputStream负责从各种数据/文件源产生输入,输入源包括:数组,字符串,文件,管道,一系列其他类型的流,以及网络连接产生的流等等。
常用字节输入流的主要类型:
(1).ByteArrayInputStream字节数组输入流:
主要功能:允许内存缓存作为输入流。
ByteArrayInputStream包含一个内部缓冲区,该缓冲区包含从流中读取的字节。内部计数器跟踪read()方法要提供的下一个字节。
注意:关闭ByteArrayInputStream无效,该类中的方法在关闭此流之后仍可被调用,而不会产生任何的IOException。
(2).FileInputStream文件输入流:
主要功能:从文件系统中的某个文件获得输入字节,用于读取诸如图像数据子类的原始字节流。若要读取字符流,请使用FileReader。
(3).PipedInputStream管道输入流:
主要功能:和管道输出流一起构成一个输入输出的管道,是管道的数据输入端。
管道输入流应该连接到管道输出流,管道输入流提供要写入管道输出流的所有数据字节。通常,这些数据有某个线程从PipedInputStream对象中读取,并有其他线程将其写入到相应的PipedOutputStream对象中。
注意:不建议PipedInputStream和PipedOutputStream对象使用单线程,因为这样可能思索线程。管道输入流包含一个缓 冲区,可以在缓冲区限定范围内将读操作和写操作分离开,如果先连接管道输出流提供数据字节的线程不再存在,则认为该管道已损坏。
(4).SequenceInputStream顺序输入流:
重要功能:将两个或多个输入流对象转换为一个单个输入流对象。
SequenceInputStream表示其他输入流的逻辑串联关系,它从输入流的有序集合开始,并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,以此类推,直到到达包含的最后一个输入流的文件末尾为止。
(5).FilterInputStream过滤输入流:
主要功能:包含其他一些输入流,将这些被包含的输入流用作其基本数据源,它可以直接传输数据或者提供一些额外的功能。
常用的FilterInputStream是DataInputStream数据输入流,主要用于允许程序以与机器无关的方式从底层输入流中读取java基本数据类型。其常用的方法有readInt(),readBoolean(),readChar()等等。
2.字节输出流OutputStream:
和字节输入流相对应,字节输出流负责字节类型数据想目标文件或设备的输出。常见的字节输出流如下:
(1).ByteArrayOutputStream字节数组输出流:
主要功能:在内存中创建一个缓冲区,将接收到的数据放入该内存缓冲区中。
ByteArrayOutputStream实现了一个输出流,其中的数据被写入一个byte数组中。缓冲区会随着数据的不断写入而自动增长,可使用toByteArray()和toString()方法获取数据。
注意:和ByteArrayInputStream类似,关闭ByteArrayOutputStream也是无效的,此类中的方法在关闭此流后仍可被调用,而不会产生任何IOException。
(2).FileOutputStream文件输出流:
主要功能:将数据写入到指定文件中。
文件输出流是用于将数据写入File或FIleDescriptor的输出流,用于写入诸如图像数据之类的原始字节流,若要写入字符流,请使用FileWriter。
(3).PipedOutputStream管道输出流:
主要功能:连接管道输入流用来创建通信管道,管道输出流是管道数据输出端。
(4).FilterOutputStream过滤输出流:
主要功能:用于将已存在的输出流作为其基本数据接收器,可以直接传输数据或提供一些额外的处理。
常用的FIlterOutputStream是DataOutputStream数据输出流,它允许程序以适当的方式将java基本数据类型写入输 出流中。其常用方法有writeInt(intV),writeChar(int v),writeByte(String s)等等。
3.字符流:
Java中得字节流只能针对字节类型数据,即支持处理8位的数据类型,由于java中的是Unicode码,即两个字节代表一个字符,于是在JDK1.1之后提供了字符流Reader和Writer。
字符流相关常用类如下:
(1).Reader:
用于读取字符串流的抽象类,子类必须实现的方法只有reader(char[],int, int)和close()。
(2).InputStreamReader:
是将字节输入流转换为字符输入流的转换器,它使用指定的字符集读取字节并将其解码为字符。即:字节——>字符。
它使用的字符集可以有名称指定或显式给定,也可以使用平台默认的字符集。
(3).Writer:
用于写入字符流的抽象类,子类必须实现的方法只有write(char[],int, int)和close()。
(4).OutputStreamWriter:
是将字符输出流转换为字节输出流的转换器,它使用指定的字符集将要写入流的字符编码成字节。即:字符——>字节。
4.综合使用java IO各种流:
Java IO中的各种流,很少单独使用,经常结合起来综合使用,既可以满足特定需求,又搞效。例子如下:
(1).使用缓冲流读取文件:
[java] view plaincopy
1. import java.io.*;
2.
3. public class BufferedInputFile{
4. public static String read(String filename) throws IOException{
5. //缓冲字符输入流
6. BufferedReader in = new BufferedReader(new FileReader(filename));
7. String s;
8. StringBuilder sb = new StringBuilder();
9. //每次读取文件中的一行
10. While((s = in.readLine()) != null){
11. sb.append(s + “\n”);
12. }
13. in.close();
14. return sb.toString();
15. }
16. public static void main(String[] args) throws IOException{
17. System.out.println(read(“BufferedInputFile.java”));
18. }
19. }
(2).读取内存中的字符串:
[java] view plaincopy
1. import java.io.*;
2.
3. public class MemoryInput{
4. public static void main(String[] args) throws IOException{
5. //将字符串包装为字符输入流
6. StringReader in = new StringReader(
7. BufferedInputFile.read(“BufferedInputFile.java”));
8. int c;
9. //读取字符输入流中的字符
10. while((c == in.read()) != -1){
11. System.out.println((char)c);
12. }
13. }
14. }
(3).数据输入/输出流:
[java] view plaincopy
1. import java.io.*;
2.
3. public class DataInputOutput{
4. public static void main(String[] args) thows IOException{
5. DataOutputStream out = new DataOutputStream(new BufferedOutputStream(
6. new FileOutputStream(“Data.txt”)));
7. out.writeDouble(3.14159);
8. out.writeUTF(“That was pi”);
9. out.writeDouble(1.41413);
10. out.writeUTF(“Square root of 2”);
11. out.close();
12. DataInputStream in = new DataInputStream(new BufferedInputStream(
13. new FileOutputStream(“Data.txt”)));
14. System.out.println(in.readDouble());
15. System.out.println(in.readUTF());
16. System.out.println(in.readDouble());
17. System.out.println(in.readUTF());
18. }
19. }
输出结果:
3.14159
That was pi
1.41413
Square root of 2
(4).文本文件输出流:
[java] view plaincopy
1. import java.io.*;
2.
3. public class TextFileOutput{
4. //输出文件名
5. static String file = “BasicFileOutput.out”;
6. public static void main(String[] args) throws IOException{
7. //将字符串先包装成字符串输入流,然后将字符串输入流再包装成缓冲字符输入流
8. BufferedReader in = new BufferedReader(new StringReader
9. (BufferedInputFile.read(“TextFileOutput.java”)));
10. //字符输出流
11. PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
12. int lineCount = 1;
13. String s;
14. While((s = in.readLine()) != null){
15. out.println(lineCount++ + “: ” + s);
16. }
17. out.close();
18. }
19. }
(5).二进制文件读写:
[java] view plaincopy
1. import java.io.*;
2.
3. public class BinaryFileOutput{
4. //输出文件名
5. static String file = “BinaryFileOutput.out”;
6. public static void main(String[] args) throws IOException{
7. //将字符串先包装成字符串输入流,然后将字符串输入流再包装成缓冲字符输入流
8. BufferedInputStream in = new BufferedInputStream(
9. new FileInputStream(“TestFile.png”)));
10. //字符输出流
11. BufferedOutputStream out = new BufferedOutputStream (
12. new FileOutputStream(file));
13. byte[] buf = new byte[1024];
14. int n;
15. While((n = in.read(buf)) > 0){
16. out.write(buf, 0, n);
17. }
18. out.close();
19. }
20. }
12——Java new I/O(一)
为了提高Java I/O的速度和效率,从JDK1.4开始引入了java.nio.*包,即java new I/O(NIO)。
事实上,为了利用java nio的速度和效率优势,原来的java I/O包中相关的类已经使用java nio重新实现,因此在编程中即使没有显式地使用java nio的代码,使用传统java I/O还是利用了nio的速度和效率优势。Java nio的速度提高主要在:文件I/O和网络I/O两个方面。
Java nio的速度提升主要因为使用了类似于操作系统本身I/O的数据结构:I/O通道(Channel)和缓冲区。
1.Channel通道:
通道表示实体,如硬件设备、文件、网络套接字或可以执行的一个或多个不同的I/O操作(如读取或写入)的程序组件的开发的链接,用于I/O操作的链接。
通道可处于打开或关闭状态。创建通道时它处于打开状态,一旦将其关闭,则保持关闭状态。一旦关闭了某个通道,试图对其调用I/O操作都会抛出 ClosedChannelException异常,通过调用通道的isOpen()方法可以探测通道是否处于打开状态。一般情况下通道对于多线程的访问 是安全的。
2.ByteBuffer字节缓冲区:
字节缓冲区是nio中唯一直接和通道channel打交道的缓冲区。字节缓冲区可以存放原始字节,也可以存放java的8中基本类型数据,但是不能存放引 用类型数据,String也是不能存放的。正是由于这种底层的存放类型似的字节缓冲区可以更加高效和绝大部分操作系统的I/O进行映射。
字节缓冲区通过allocation()方法创建,此方法为该缓冲区的内容分配空间,或者通过wrapping方法将现有的字节数组包装到缓冲区中来创建。
字节缓冲区的常用操作:
(1).读写单个字节的绝对和相对get和put方法:
a. 绝对方法:
get(int index):读取指定索引处的字节。
put(int index, byte b):将字节写入指定索引处。
b.相对方法:
get():读取此缓冲区当前位置的字节,然后该位置递增。
put(byte b):将字节写入此缓冲区的当前位置,然后该位置递增。
(2).相对批量get方法:
ByteBuffer get(byte[] dst):将此缓冲区的字节传输到给定的目标数组中。
(3).相对批量put方法:
ByteBuffer put(byte[] src):将给定的源byte数组的所有内容传输到此缓冲区中。
(4).读写其他基本类型值:
getChar(), putChar(char value), getChare(int index), putChar(int index, char value).
getInt(), putInt(int value), getInt(int index), putInt(int index, int value)等等读写基本类型值得相对和绝对方法。
注意:基本类型值得相对和绝对读写方法,根据java基本类型数据底层字节数进行缓冲区移动。
(5).创建视图缓冲区:
为了访问同类二进制数据,允许将字节缓冲区视为包含它们基本类型值的缓冲区,视图缓冲区只是其内容受该字节缓冲区支持的另一种缓冲区。字节缓冲区和视图缓冲区内容更改是相互可见的。这两种缓冲区的位置、限制和标记值都是独立的。创建方法如下:
asCharBuffer(), asDoubleBuffer(), asFloatBuffer(), asIntBuffer(), asLongBuffer(), asReadOnlyBuffer(), asShortBuffer()。
与具体类型的get和put方法系列相比,视图缓冲区优势如下:
a视图缓冲区不是根据字节进行索引,而是根据其特定类型的值得大小进行索引。
b.视图缓冲区提供了相对批量get和put方法,这些方法可以在缓冲区和数组或相同类型的其他缓冲区直接传输连续序列的值。
c.视图缓冲区可能更搞效,因为当且仅当其支持的字节缓冲区为直接缓冲区时它才是直接缓冲区。
(6).缓冲区其他操作:
a.ByteBuffer compact():压缩缓冲区,从缓冲区写入数据之后调用,以防写入不完整。
b.ByteBuffer duplicate():创建共享此缓冲区内容的新的字节缓冲区,新缓冲区中的内容为此缓冲区的内容,此缓冲区和新缓冲区内容更改是相互可见的。
c.ByteBuffer slice():创建新的字节缓冲区,其内容是此缓冲区内容的共享子序列。
3.直接与非直接缓冲区:
字节缓冲区要么是直接的,要么是非直接的。
如果字节缓冲区是直接的,则JVM会尽最大努力直接在此缓冲区上执行本机的I/O操作,即在每次调用底层操作系统的一个本机I/O之前(或之后),JVM 都会尽量避免将缓冲区的内容复制到中间缓冲区中(或尽量避免从中间缓冲区复制内容)。直接字节缓冲区可以通过调用allocateDirect()方法来 创建,此方法返回的缓冲区进行分配和取消分配所需的成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此它对应用程序的内存 需求量造成的影响可能并不明显,所以建议将直接缓冲区主要分配给那些容易受底层操作系统的本机I/O操作影响的大型的、持久的缓冲区
可以通过调用isDirect()来判断字节缓冲区是直接的还是非直接的。
4.文件通道:
Java I/O的FileInputStream,FileOutputStream和RandomAccessFile可以产生文件通道,例子如下:
[java] view plaincopy
1. import java.nio.*;
2. import java.nio.channels.*;
3. import java.io.*;
4.
5. public class FileChannel{
6. //分配字节缓冲区时指定其大小
7. private static final int BSIZE = 1024;
8. public static void main(String[] args) throw Exception{
9. //获取FileOutputStram的文件通道
10. FileChannel fc = new FileOutputStream(“data.txt”).getChannel();
11. //向字节缓冲区中写入字节数组
12. fc.write(ByteBuffer.wrap(“Some text”.getBytes()));
13. fc.close();
14. //以读写方式获取随机访问文件的文件通道
15. fc = new RandomAccessFile(“data.txt”, “rw”).getChannel();
16. //定位到字节缓冲区当前内容之后
17. fc.position(fc.size());
18. //向字节缓冲区中追加内容
19. fc.write(ByteBuffer.wrap(“Some more”.getBytes()));
20. fc.close();
21. //获取FileInputStream的文件通道
22. fc = new FileInputStream(“data.txt”).getChannel();
23. //分配字节缓冲区
24. ByteBuffer buff = ByteBuffer.allocate(BSIZE);
25. //将字节数组从文件通道读入到字节缓冲区中
26. fc.read(buff);
27. //放置缓冲区,为缓冲区写出或相对获取做准备
28. buff.flip();
29. //判断缓冲区是是否还有元素
30. while(buff.hasRemaining()){
31. //获取字节缓冲区字节的相对方法
32. System.out.println((char)buff.get());
33. }
34. }
35. }
输出结果:
Some text Some more
传统java I/O中FileInputStream, FileOutputStream和RandomAccessFile三个类可以产生文件通道。
注意:Java new I/O的目标是提高I/O速度,快速移动大批量的数据,因此,字节缓冲区的大小非常重要,例子中的1K大小不一定是合适的,应用程序需要在生产环境中测试确定合适的缓冲区大小。
5.文件复制:
文件通道和字节缓冲区不但可以实现数据从文件读入和读出,还可以实现文件的复制,例子如下:
[java] view plaincopy
1. import java.nio.*;
2. import java.nio.channels
import java.util.*;
import java.lang.ref.*;
class Key {
String id;
public Key(String id) {
this.id = id;
}
public String toString() {
return id;
}
public int hashCode() {
return id.hashCode();
}
public boolean equals(Object r) {
return (r instanceof Key) && id.equals(((Key) r).id);
}
public void finalize() {
System.out.println("Finalizing Key " + id);
}
}
class Value {
String id;
public Value(String id) {
this.id = id;
}
public String toString() {
return id;
}
public void finalize() {
System.out.println("Finalizing Value " + id);
}
}
public class MapCache {
public static void main(String[] args) throws Exception {
int size = 1000;
// 或者从命令行获得size的大小
if (args.length > 0)
size = Integer.parseInt(args[0]);
Key[] keys = new Key[size]; // 存放键对象的强引用
WeakHashMap<Key, Value> whm = new WeakHashMap<Key, Value>();
for (int i = 0; i < size; i++) {
Key k = new Key(Integer.toString(i));
Value v = new Value(Integer.toString(i));
if (i % 3 == 0)
keys[i] = k; // 使Key对象持有强引用
whm.put(k, v); // 使Key对象持有弱引用
}
// 催促垃圾回收器工作
System.gc();
// 把CPU让给垃圾回收器线程
Thread.sleep(8000);
}
}
4. Java中的析构方法finalize
finalize()方法常称之为终止器
protected void finalize(){
// finalization code here
}
对象即将被销毁时,有时需要做一些善后工作。可以把这些操作写在finalize()方法里。
Java终止器却是在对象被销毁时调用。一旦垃圾收集器准备好释放无用对象占用的存储空间,它首先调用那些对象的finalize()方法,然后才真正 回收对象的内存。而被丢弃的对象何时被销毁,应用是无法获知的。大多数场合,被丢弃对象在应用终止后仍未销毁。到程序结束的时候,并非所有收尾模块都会得 到调用。
5. 应用能干预垃圾回收吗?
在应用代码里控制JVM的垃圾回收运作是不可能的事。
对垃圾回收有两个途径。第一个就是将指向某对象的所有引用变量全部移走。这就相当于向JVM发了一个消息:这个对象不要了。第二个是调用库方法 System.gc()。第一个是一个告知,而调用System.gc()也仅仅是一个请求。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个 垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。
希望JVM及时回收垃圾,是一种需求。其实,还有相反的一种需要:在某段时间内最好不要回收垃圾。要求运行速度最快的实时系统,特别是嵌入式系统,往往希望如此。
Java的垃圾回收机制是为所有Java应用进程服务的,而不是为某个特定的进程服务的。因此,任何一个进程都不能命令垃圾回收机制做什么、怎么做或做多少。
6. 垃圾回收算法
* 引用计数
该算法在java虚拟机没被使用过,主要是循环引用问题,因为计数并不记录谁指向他,无法发现这些交互自引用对象。
-- 怎么计数?
当引用连接到对象时,对象计数加1
当引用离开作用域或被置为null时减1
-- 怎么回收?
遍历对象列表,计数为0就释放
-- 有什么问题?
循环引用问题。
* 标记算法
标记算法的思想是从堆栈和静态存储区的对象开始,遍历所有引用,标记活得对象。
对于标记后有两种处理方式:
(1) 停止-复制
-- 所谓停止,就是停止在运行的程序,进行垃圾回收
-- 所谓复制,就是将活得对象复制到另外一个堆上,以使内存更紧凑
-- 优点在于,当大块内存释放时,有利于整个内存的重分配
-- 有什么问题?
一、停止,干扰程序的正常运行,二,复制,明显耗费大量时间,三,如果程序比较稳定,垃圾比较少,那么每次重新复制量是非常大的,非常不合算
-- 什么时候启动停止-复制?
内存数量较低时,具体多低我也不知道
(2) 清除 也称标记-清除算法
-- 也就是将标记为非活得对象释放,也必须暂停程序运行
-- 优点就是在程序比较稳定,垃圾比较少的时候,速度比较快
-- 有什么问题?
很显然停止程序运行是一个问题,只清除也会造成很对内存碎片。
-- 为什么这2个算法都要暂停程序运行?
这是因为,如果不暂停,刚才的标记会被运行的程序弄乱
java正则表达式学习总结,以及和javascript正则表达式的区别
用正则表达式处理字符串功能非常强大,下面总结一下java正则表达式的一些知识:
基本元字符
. 任何字符(与行结束符可能匹配也可能不匹配)
// 反斜杠
/t 间隔 ('/u0009')
/n 换行 ('/u000A')
/r 回车 ('/u000D')
/d 数字 等价于[0-9]
/D 非数字 等价于[^0-9]
/s 空白符号 [/t/n/x0B/f/r]
/S 非空白符号 [^/t/n/x0B/f/r]
/w 单独字符 [a-zA-Z_0-9]
/W 非单独字符 [^a-zA-Z_0-9]
/f 换页符
/e Escape
/b 一个单词的边界
/B 一个非单词的边界
/G 前一个匹配的结束
^为限制开头
$为限制结尾
加入特定限制条件[]
[a-z] 条件限制在小写a to z范围中一个字符
[A-Z] 条件限制在大写A to Z范围中一个字符
[a-zA-Z] 条件限制在小写a to z或大写A to Z范围中一个字符
[0-9] 条件限制在小写0 to 9范围中一个字符
[0-9a-z] 条件限制在小写0 to 9或a to z范围中一个字符
[0-9[a-z]] 条件限制在小写0 to 9或a to z范围中一个字符(交集)
[]中加入^后加再次限制条件[^]
[^a-z] 条件限制在非小写a to z范围中一个字符
[^A-Z] 条件限制在非大写A to Z范围中一个字符
[^a-zA-Z] 条件限制在非小写a to z或大写A to Z范围中一个字符
[^0-9] 条件限制在非小写0 to 9范围中一个字符
[^0-9a-z] 条件限制在非小写0 to 9或a to z范围中一个字符
[^0-9[a-z]] 条件限制在非小写0 to 9或a to z范围中一个字符(交集)
表示匹配次数的符号
{n,}:至少n次
“或”符号
可以使用“|”操作符。“|”操作符的基本意义就是“或”运算。要匹配“toon”,使用“t(a|e|i|o|oo)n”正则表达式。这里不能使用方扩号,因为方括号只允许匹配单个字符;这里必须使用圆括号“()”。圆括号还可以用来分组。
捕获组
>捕获组可以通过从左到右计算其开括号来编号。例如,在表达式 ((A)(B(C))) 中,存在四个这样的组:
1 | ((A)(B(C))) |
2 | /A |
3 | (B(C)) |
4 | (C) |
以 (?) 开头的组是纯的非捕获 组,它不捕获文本,也不针对组合计进行计数。
匹配模式
java正则表达式默认使用的是最长匹配模式,即:如果用ro*去匹配room,并将其替换为r,则结果为rm。
如果某些情况下想使用最短匹配,即:如果用ro?去匹配room,并将其替换为b,则结果为boom。
总结:默认使用最长匹配,但是结尾为"?"的模式使用的是最短匹配。
基本用法
Pattern pattern = Pattern.compile(正则表达式模式串);
Matcher matcher = pattern.matcher(要验证或处理的源字符串);
matcher.matches();如果匹配返回true,否则,false.
JavaScript正则表达式和java正则表达式的区别
javascript中的正则表达式和java的正则表达式基本上是相同的,区别在于分组引用和对象,方法
具体区别:
1).javascript正则表达式创建有两种方法:
a.显式创建:
var re = new RegExp("正则表达式模式串");
re.test(要校验或处理的源字符串);
b.隐式创建:
var re = /正则表达式模式串/;
要校验或处理的源字符串.match(re);
2).分组捕获对象引用方式不同
javascript也是使用"()"进行分组,但是捕获对象用RegExp对象的$1到$99来引用捕获对象。
附录:常用的javascript正则表达式,java的也类似
ip地址校验正则表达式(IPv4):
/^(/d{1,2}|1/d/d|2[0-4]/d|25[0-5])(/.(/d{1,2}|1/d/d|2[0-4]/d|25[0-5])){3}$/
Email校验正则表达式:
/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(/.[a-zA-Z0-9_-]+)+$/
格式为:2010-10-08类型的日期格式校验正则表达式:
/^/d{4}-(0?[1-9]|1[0-2])-(0?[1-9]|[1-2]/d|3[0-1])$/
格式为:23:11:34类型的时间格式校验正则表达式:
/^([0-1]/d|2[0-3]):[0-5]/d:[0-5]/d$/
防范JAVA内存泄漏解决方案
Java是如何管理内存
为了判断Java中是否有内存泄露,我们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,程序 员需要通过关键字new为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在Java中,内存的分配是由程序完成的,而内存的释放是有GC完成的,这种收支两条线 的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。因为,GC为了能够正确释放对象,GC必须 监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。
监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。
为了更好理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可 以作为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有 效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。
以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。
Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们 的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相 比,精度行低(很难处理循环引用的问题),但执行效率很高。
什么是Java中的内存泄露
下面,我们就可以描述什么是内存泄漏。在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有 向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存 泄漏,这些对象不会被GC所回收,然而它却占用内存。
在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。
通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编程的效率。
因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。
对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java 语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策 略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心 这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃 圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。
下面给出了一个简单的内存泄露的例子。在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个Vector中,如果我们仅仅释放引 用本身,那么Vector仍然引用该对象,所以这个对象对GC来说是不可回收的。因此,如果对象加入到Vector后,还必须从Vector中删除,最简 单的方法就是将Vector对象设置为null。
Vector v=new Vector(10); for (int i=1;i<100; i++) { Object o=new Object(); v.add(o); o=null; } |
//此时,所有的Object对象都没有被释放,因为变量v引用这些对象。
几种典型的内存泄漏
我们知道了在Java中确实会存在内存泄漏,那么就让我们看一看几种典型的泄漏,并找出他们发生的原因和解决方法。
(1) 全局集合
在大型应用程序中存在各种各样的全局数据仓库是很普遍的,比如一个JNDI-tree或者一个session table。在这些情况下,必须注意管理储存库的大小。必须有某种机制从储存库中移除不再需要的数据。
通常有很多不同的解决形式,其中最常用的是一种周期运行的清除作业。这个作业会验证仓库中的数据然后清除一切不需要的数据。
另一种管理储存库的方法是使用反向链接(referrer)计数。然后集合负责统计集合中每个入口的反向链接的数目。这要求反向链接告诉集合何时会退出入口。当反向链接数目为零时,该元素就可以从集合中移除了。
(2) 缓存
缓存一种用来快速查找已经执行过的操作结果的数据结构。因此,如果一个操作执行需要比较多的资源并会多次被使用,通常做法是把常用的输入数据的操作结果进 行缓存,以便在下次调用该操作时使用缓存的数据。缓存通常都是以动态方式实现的,如果缓存设置不正确而大量使用缓存的话则会出现内存溢出的后果,因此需要 将所使用的内存容量与检索数据的速度加以平衡。
常用的解决途径是使用java.lang.ref.SoftReference类坚持将对象放入缓存。这个方法可以保证当虚拟机用完内存或者需要更多堆的时候,可以释放这些对象的引用。
(3) 类装载器
Java类装载器的使用为内存泄漏提供了许多可乘之机。一般来说类装载器都具有复杂结构,因为类装载器不仅仅是只与"常规"对象引用有关,同时也和对象内 部的引用有关。比如数据变量,方法和各种类。这意味着只要存在对数据变量,方法,各种类和对象的类装载器,那么类装载器将驻留在JVM中。既然类装载器可 以同很多的类关联,同时也可以和静态数据变量关联,那么相当多的内存就可能发生泄漏。
如何检测内存泄漏
最后一个重要的问题,就是如何检测Java的内存泄漏。目前,我们通常使用一些工具来检查Java程序的内存泄漏问题。市场上已有几种专业检查 Java内存泄漏的工具,它们的基本工作原理大同小异,都是通过监测Java程序运行时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分 析、可视化。开发人员将根据这些信息判断程序是否有内存泄漏问题。这些工具包括Optimizeit Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。
下面,我们将简单介绍Optimizeit的基本功能和工作原理。
Optimizeit Profiler版本4.11支持Application,Applet,Servlet和Romote Application四类应用,并且可以支持大多数类型的JVM,包括SUN JDK系列,IBM的JDK系列,和Jbuilder的JVM等。并且,该软件是由Java编写,因此它支持多种操作系统。Optimizeit系列还包 括Thread Debugger和Code Coverage两个工具,分别用于监测运行时的线程状态和代码覆盖面。
当设置好所有的参数了,我们就可以在OptimizeIt环境下运行被测程序,在程序运行过程中,Optimizeit可以监视内存的使用曲线(如 下图),包括JVM申请的堆(heap)的大小,和实际使用的内存大小。另外,在运行过程中,我们可以随时暂停程序的运行,甚至强行调用GC,让GC进行 内存回收。通过内存使用曲线,我们可以整体了解程序使用内存的情况。这种监测对于长期运行的应用程序非常有必要,也很容易发现内存泄露。
在运行过程中,我们还可以从不同视角观查内存的使用情况,Optimizeit提供了四种方式:
· 堆视角。 这是一个全面的视角,我们可以了解堆中的所有的对象信息(数量和种类),并进行统计、排序,过滤。了解相关对象的变化情况。
· 方法视角。通过方法视角,我们可以得知每一种类的对象,都分配在哪些方法中,以及它们的数量。
· 对象视角。给定一个对象,通过对象视角,我们可以显示它的所有出引用和入引用对象,我们可以了解这个对象的所有引用关系。
· 引用图。 给定一个根,通过引用图,我们可以显示从该顶点出发的所有出引用。
在运行过程中,我们可以随时观察内存的使用情况,通过这种方式,我们可以很快找到那些长期不被释放,并且不再使用的对象。我们通过检查这些对象的生 存周期,确认其是否为内存泄露。在实践当中,寻找内存泄露是一件非常麻烦的事情,它需要程序员对整个程序的代码比较清楚,并且需要丰富的调试经验,但是这 个过程对于很多关键的Java程序都是十分重要的。
综上所述,Java也存在内存泄露问题,其原因主要是一些对象虽然不再被使用,但它们仍然被引用。为了解决这些问题,我们可以通过软件工具来检查内存泄露,检查的主要原理就是暴露出所有堆中的对象,让程序员寻找那些无用但仍被引用的对象。