您的位置:首页 > 编程语言 > Java开发

Java基础加强_Eclipse、枚举、反射、注解、泛型、类加载器、动态代理

2015-03-12 12:36 1181 查看

第1单元:Eclipse开发工具

eclipse及IDE开发工具介绍

MyEclipse是eclipse的一个插件,使得eclipse可以开发JavaEE项目

javaw.exe是启动java的图形界面所用的虚拟机

Java EE: Java Enterprise Edition

IDE: Integrated Development Environment

WorkSpace:工作间

工程管理与快捷键设置

一个工作间workspace的设置应用于它包含的所有project

设置整个工作间编译器版本和运行时环境的版本

window-->Preferences-->Java-->compiler或者installed JREs

切换workspace:File-->Switch Workspace,自命名一个workspace,没有则自动创建并打开一个工作间

perspective:透视图

每个java文件都要有一个包名,包名是公司的网址倒着写(去哪个公司面试包名就写哪个公司)

想要更改工程名,右键工程名选重构refactor-->rename。如果别的地方调用了这个类,类名也会跟着改,这就是重构的好处

为当前工作间下所有的工程设置快捷键

window-->preferences-->general-->Keys,输入content assist,选择unbind command解除原绑定,然后再设置新的快捷键,注意不要和其他快捷键冲突

查看工程所在目录:安装OperExplorer插件,点击Open Explorer图标即可

eclipse快捷键

提示快捷键:比如设置content assist:alt + / ,输入syso再按该快捷键就可以输出打印语句。注意:一般自动补全快捷键:word completion的快捷键默认也是alt+/,所以此时需要对其进行修改

Ctrl+z/y:取消/还原上次操作

Alt+↓: 移动当前行

Ctrl+Alt+↓: 复制当前行到下一行(复制增加)

Ctrl+Alt+↑ :复制当前行到上一行(复制增加)

Ctrl+I :正确缩进代码

Ctrl+/ :单行注释

Ctrl+Shift+/ :多行注释

Ctrl+T:查看类或者接口的实现类

Alt+Shift+r :选中所有同一变量,方便进行批量修改

Alt+Shift+S/右键选Source-->Generate Constructor using Fields:Eclipse自动根据定义好的字段生成构造函数

直接复制源代码在某包下,自动生成相应文件!

视图管理与程序调试

一个透视图perspective就是若干个小窗口view的集合

如果关闭了一个view就点window-->show view来打开

调试一个变量debug步骤

在需要debug的代码前双击产生端点
点右键单击debug as进如debug透视图
鼠标选中一个变量,右键选watch就可以看到它的值
选择step over先前走一步观察变量的变化

配置eclipse的编译与运行环境

如何设置单个工程的Javac和Java?
选择工程右键选properties-->compiler设置javac
右键选择run as 中的configurations来设置java运行环境
高版本的JRE能否运行低版本的javac编译的程序?可以
低版本的JRE能否运行高版本的javac编译的程序?不可以,会出现bad version number in .class file错误

在eclipse中配置java模板代码

选取一段代码,右键选择surround with 选取需要生成的模板代码
自己配置需要的模板:
preferences-->java-->Editor下拉-->templates中new一个自定义模板,需要在模板中定义cursor和line_selection

在eclipse中导入已有的工程

首先,将工程文件拷贝至workspace目录下
然后,File-->Import-->Genereal-->Existing Projects into Workspace/File System选择该工程的根目录确认即可
如果导入工程的JRE和自己eclipse的JRE不一样(比如JRE的安装目录以及JRE所含的jar包不一样),右键工程名-->build path-->configure build path-->libraries下把该工程原JRE库删除,再增加自己的JRE库
add jars:增加单独的jar包(当jar包存在于工程内但未导入)
add external jars:要导入不在工程内的jar包
add library:增加一个库,该库中有许多jar包
可以自定义一个库,选择User Library,命名一个新的库,然后再在库里增加jar包

第2单元:java5的一些简单新特性

java5的静态导入与编译器语法设置

import可以导入某个类或者某个包中的所有类
import static导入一个类中的某个静态方法或者所有静态方法
import static java.lang.Math.max;//注意不要写掉static
...
System.out.println(max(3,6));//省略调用类名

如果编译器的jdk版本低于1.5会报错!

Java5的可变参数与OverLoad与Override相关面试题

一个方法接受的参数个数不固定时,就使用可变参数

可变参数VariableParameter的特点:

只能出现在参数列表的最后
三个点...位于变量类型和变量名中间,前后有无空格都可以
调用可变参数的方法时,编译器为该可变参数隐含创建一个数组,该方法体中以数组的形式访问可变参数

public static int add(int x,int... args){
int sum = x;
/*for(int i=0;i<args.length;i++){
sum += args[i];
}*/
//for循环增强
for(int arg : args){
sum += arg;
}
return sum;
}


面试题:overload vs override
两个方法的参数列表完全一样但是返回值不一样,不是overload,overload只看同名函数的参数列表,参数列表不同的函数才能实现重载
父类方法私有,子类的一个方法与之一模一样,不是override

Java5的增强for循环

语法:for(变量类型 变量名 : 集合变量名) { ... }
使用见上面代码
学会阅读java的语言规范langspec-3.0

Java5的基本数据自动拆装箱及享元设计模式

自动装箱:Integer num1 = 12;
自动拆箱:num1+12;

Integer num1 = 13;
Integer num2 = 13;
num1和num2为同一个对象

Integer num1 = 137;
Integer num2 = 137;
num1和num2不是同一对象,与字符串一样

手动包装同自动装箱一样!
Integer num3 = Integer.valueOf(213)

原因:为了节省内存空间,因为小的数使用频率高,大数适用频率低,所以对于-128~127之间的数字,一旦被包装成一个Integer对象之后就会被缓存起来,当下一个又被装成Integer对象时,先看缓存池中有木有,有就直接拿来用。

以上的设计是一种设计模式:享元模式 flyweight

当一个对象经常使用,大量重复,就将其设计成享元模式,把相同的属性用对象包装,称为内部状态,不同的变成外部属性,作为方法的参数传入,比如字母i,字符char为相同属性,位置为不同的变量,i.display(x,y)

第3单元:Java5的枚举

枚举的作用

枚举就是要让某个类型的变量的取值只能为若干个固定值中的一个,否则,编译器就会报错。
枚举可以让编译器在编译时就可以控制源程序中填写的非法值,普通变量的方式在开发阶段无法实现这一目标。

用普通类模拟枚举的实现原理

定义一个WeekDay的类来模拟枚举功能:

私有的构造方法
每个元素分别用一个公有的静态成员变量来表示
可以有若干公有方法或者抽象方法,例如要提供nextDay()方法必须是抽象的

public abstract class WeekDay1 {
private WeekDay1(){}
public final static WeekDay1 SUN = new WeekDay1(){
public WeekDay1 nextDay() {
return MON;
}

};
public final static WeekDay1 MON = new WeekDay1(){
public WeekDay1 nextDay() {
return SUN;
}

};

public abstract WeekDay1 nextDay(); //采用抽象方法定义nextDay就将大量的if else语句转移成了一个个独立的类,这里在对象实例化时用匿名内部类来定义子类对象,public可以让子类实现该方法

/*	public WeekDay nextDay(){
if(this == SUN){
return  MON;
}else{
return SUN;
}
}
*/

public String toString(){
return this==SUN?"SUN":"MON";
}
}


枚举的基本应用

枚举是一种特殊的类,不用关键字class标识,而是通过关键字enum来标识一个枚举类,每一个枚举元素都是该类的一个实例对象,例如可以调用WeekDay.SUN.getClass().getName()和WeekDay.class.getName();

枚举类的方法

非静态方法:name,toString,ordinal

静态方法:values,valueOf

public static void main(String[] args) {
WeekDay weekDay2 = WeekDay.FRI;//访问枚举类中的元素
System.out.println(weekDay2);
System.out.println(weekDay2.name());//结果为FRI
System.out.println(weekDay2.ordinal());	//返回weekDay2的排序,此例为5,ordinal意思是序数
System.out.println(WeekDay.valueOf("SUN").toString());//将SUN字符串包装成一个WeekDay类型的枚举对象,等同于WeekDay.SUN
System.out.println(WeekDay.values().length); //values返回一个枚举类型数组
}


可以将枚举定义为内部类,此时:public enum WeekDay{SUN,MON,TUE,WED,THI,FRI,SAT//此后无语句,不需要加分号}}




实现带有构造方法的枚举

元素列表必须放在所有语句之前,包括构造方法,此时元素列表最后要有分号
public enum WeekDay{
SUN(1),MON(),TUE,WED,THI,FRI,SAT;//在枚举元素后面跟上括号,表示创建该元素对应的实例对象时使用哪个构造方法,默认情况(即没有括号)是空参数的构造函数
private WeekDay(){System.out.println("first");}
private WeekDay(int day){System.out.println("second");}
}

一旦枚举中的元素被访问,所有的元素的构造函数都会被调用

public static void main(String[] args) {
WeekDay day2 = WeekDay.TUE;
}
运行结果



实现带有抽象方法的枚举

外部类可以有两个修饰符,一个默认的,一个public
内部类可以有四个访问修饰符
实现一个交通灯的枚举类(最复杂的枚举)
public enum TrafficLamp{
//定义枚举的元素列表(在所有语句之前),并且调用父类的有参构造方法
RED(30){
public  TrafficLamp nextLamp(){
return GREEN;
}
},
GREEN(45){
public  TrafficLamp nextLamp(){
return YELLOW;
}
},
YELLOW(5){
public  TrafficLamp nextLamp(){
return RED;
}
};
public abstract TrafficLamp nextLamp();//在父类中定义一个nextLamp的抽象方法,通过上面的匿名内部类来定义该方法的实现,public让子类可以实现该方法
private int time; //每种灯都有一个时间
private TrafficLamp(int time){this.time = time;}//时间通过构造方法传入,构造方法为private,代表只能在本类中使用
}


说明:new Date(300){}; 该写法是允许的,表示子类的构造方法内部会调用父类的构造方法,并且调用的是父类的有参的构造方法,而不需要在子类中写明该调用,参见上面交通灯例子

枚举只有一个元素时,可以作为单例设计模式的一种实现

第4单元:Java1.2的反射

反射的基础_Class类

Java程序中的各个Java类属于同一类事物,描述这类事物的Java类名就是Class
要注意与小写的java关键字区别
Class类描述的类的名字,类的访问属性,类所属的包名,字段名称的列表,方法名称的列表等等
Class类不能直接new对象,因为没有构造方法,它的各个实例对象分别对应各个类在内存中的字节码,例如Person类的字节码,ArrayList类的字节码等等
一个类被类加载器加载到内存中,占用一片存储空间,这个空间里面的内容就是类的字节码

如何得到各个字节码对应的实例对象

类名.class,例如,Class cls1 = Date.class;
对象.getClass(),例如,new Date().getClass();
Class.forName(完整的包名+类名),例如,Class.forName("java.lang.String"); 做反射时主要使用该方法,因为在写源程序的时候还不知道类名,需要作为参数传递。该方法会抛出异常ClassNotFoundException

面试题:forName的作用是返回字节码,返回的方式有两种:1,字节码已经被JVM加载过,直接返回。2,JVM里还没有该字节码,则用类加载器加载,把加载进来的字节码缓存在虚拟机里面,再返回

九个预定义Class实例对象

8个基本数据类型+1个void关键字
参看Class的isPrimitive()方法的帮助
类名.isPrimitive()判断该类是否为原始类型
int.class == Integer.TYPE的结果为true,因为TYPE字段代表被包装的基本数据类型的字节码
数组类型的class实例对象判断使用的是Class 的isArray()
String str1 = "abc";
Class cls1 = str1.getClass();
Class cls2 = String.class;
Class cls3 = Class.forName("java.lang.String");
System.out.println(cls1 == cls2);//true,内存中只会有一份字节码文件
System.out.println(cls1 == cls3);//true

System.out.println(cls1.isPrimitive());//fasle
System.out.println(int.class.isPrimitive());//true
System.out.println(int.class == Integer.class);//false
System.out.println(int.class == Integer.TYPE);//true,TYPE字段代表被包装的基本数据类型的字节码
System.out.println(int[].class.isPrimitive()); //false
System.out.println(int[].class.isArray());//true
总之,只要是在源程序中出现的类型,都有各自的class实例对象,例如int[],void

理解反射的概念

反射就是将java类中的各种成分映射成相应的java类
例如,一个java类用一个Class类的对象来表示,一个类中的组成部分:成员变量,方法,构造方法,包等等信息也用一个个的java类来表示。
表示java类的Class类显然要提供一系列的方法来获取其中的变量,方法,构造方法,修饰符,包等信息,这些信息就是用相应的类的实例对象来表示,它们是Field,Method,Constructor,Package等等
一个类中每个成员都可以用相应的反射API类的一个实例对象来表示,通过调用Class实例对象的方法可以得到这些实例对象并加以应用。

构造方法的反射

Constructor类的对象代表某个类中的一个构造方法

得到某个类所有的构造方法

Constructor [ ] constructors = Class.forName("java.lang.String").getConstructors();

得到某一个构造方法

Constructor constructor = Class.forName("java.lang.String").getConstructor(StringBuffer.class);//传入的是Class对象,通过参数列表来确定要得到那个构造方法,注意获得构造方法时要用到的类型

创建实例对象

一般方式:new String(new StringBuffer("abc"));
反射方式:String str = (String)constructor.newInstance(new StringBuffer("abc"));//因为编译时编译器不知道该构造方法对象constructor是属于哪个类的,只有运行时程序才知道,所以写程序时要加上类型强转(String)告诉编译器,注意调用创建实例对象的newInstance方法时要用到上面相同类型的实例对象

Class的newInstance()方法

例子:String obj = (String)Class.forName("java.lang.String").newInstance();
该方法内部先得到默认的不带参数的构造方法,然后用该构造方法创建实例对象。省略了获取构造方法的步骤
该方法内部的具体代码是怎样写得呢?用到了缓存机制来保存默认构造方法的实例对象,所以反射会导致程序性能下降

成员变量的反射

Field类的对象代表某个类中的一个成员变量
Eclipse自动生成构造函数源代码:定义好字段,Alt+Shift+S/右键选Source-->Generate Constructor using Fields
public class ReflectPoint {
public int y;
private int x;
int z;
public ReflectPoint(int x, int y,int z) {
super();
this.x = x;
this.y = y;
this.z = z;
}
}


通过反射进行操作:

ReflectPoint pt1 = new ReflectPoint(3,5,2);
//被public修饰的变量,通过getField方法获取
Field fieldY = pt1.getClass().getField("y");//fieldY的值是多少?是5,错!fieldY不是对象身上的变量的值,而是类上定义的变量"y",要用它去取某个对象上对应的值
//在pt1对象身上拿到对应的变量的值,结果为3
System.out.println(fieldY.get(pt1));
//对于只要是在类中声明过的成员变量(包括public),都可以通过getDeclaredField方法获取
//被private修饰的变量,只能通过getDeclaredField方法获取(让该字段可见,但是还不能获取该字段的值)
Field fieldX = pt1.getClass().getDeclaredField("x");
//然后通过setAccessible将该变量的值设置为可以访问,也叫暴力反射(让该字段的值可以拿到)
fieldX.setAccessible(true);
//打印结果为5
System.out.println(fieldX.get(pt1));
//无访问修饰符的变量,只能通过getDeclaredField方法获取
Field fieldZ = rp.getClass().getDeclaredField("z");
//此时该变量的值即可访问,结果为2
System.out.println(fieldZ.get(rp));



成员变量反射的综合案例

将任意一个对象中的所有String类型的成员变量所对应的字符串内容中的“b”改成“a”
private static void changeStringValue(Object obj) throws Exception {
Field[] fields = obj.getClass().getDeclaredFields();
for(Field field : fields){
//if(field.getType().equals(String.class)){
if(field.getType() == String.class){ //字节码,也就是Class类的对象,用等号比较地址值,因为内存中都只有一份
String oldValue = (String)field.get(obj);
String newValue = oldValue.replace('b', 'a');
field.set(obj, newValue);//最后还要给对象中的成员变量赋值
}
}

}

成员方法的反射

Method类的对象代表某个类中的一个成员方法

得到类中的某一个方法

Method charAt = Class.forName("java.lang.String").getMethod("charAt", int.class); //int.class表示的是参数类型,通过此列表来明确多个重载方法中的一个

调用方法

通常方式:str.charAt(1);
反射方式:charAt.invoke(str,1); //str为调用该方法的对象,1为传入该方法的参数,invoke方法参考画圆的方法circle.draw(),画圆的参数是圆的私有属性,所以画圆的方法是属于圆的(专家模式)

如果传递给Method对象的invoke方法的第一个参数为null,说明该Method对象对应的是一个静态的方法

jdk1.4和jdk1.5的invoke方法的区别

jdk1.5: public Object invoke(Object obj, Object...args)
jdk1.4: public Object invoke(Object obj, Object [ ] args), 即按照jdk1.4的语法,当需要将一个数组作为参数传递给invoke方法时,数组中的每个元素会分别对应被调用方法中的一个参数。jdk1.5兼容jdk1.4,因此调用charAt方法的代码也可以用jdk1.4改写为charAt.invoke(str, new Object[]{1})的形式

对接收数组参数的成员方法进行反射

点击类名,按F2可以显示完整的包名.类名。再右键选择run as-->run configurations-->Arguments可以给main函数传递想要的字符串参数
目标:

写一个程序,这个程序能够根据用户提供的类名,去执行该类中的main方法 String startingClassName = args[0];//用户传入的参数是一个完整的class类名 Method mainMethod = Class.forName(startingClassName).getMethod("main", String[].class);

问题:

启动java程序的main方法的参数是一个字符串数组,即public static void main(String[] args),通过反射方式来调用这个main方法时如何为invoke方法传递参数呢?按jdk1.5的语法,整个数组是一个参数,而按jdk1.4的语法,数组中的每个元素对应一个参数。当把一个字符串数组作为参数传递给invoke方法时,javac为了让1.5的语法兼容1.4的语法而按照1.4的语法处理,即把数组打散成为若干个单独的参数。所以再给invoke方法传递参数时,不能使用代码mainMethod.invoke(null,
new String[]{"xxx"}),javac只把它当做jdk1.4的语法进行理解,因此会出现参数类型不匹配的问题。

解决办法:

mainMethod.invoke(null, new Object[] {new String[]{"xxx"}}),这样每一个数组也是一个Object对象,只拆一次包,得到一个字符串数组
mainMethod.invoke(null, (Object)new String[]{"xxx"});,编译器会做特殊处理,编译时不把参数当做数组看待,也就不会把数组打散成为若干个参数了。

数组的反射

数组与Object的关系及其反射类型

具有相同维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象
代表数组的Class实例对象的getSuperclass()方法返回的父类为Object对应的Class
基本类型的一维数组可以被当做Object类型使用,不能当做Object [ ]类型使用,因为int为基本数据类型,不是Object类型。非基本类型的一维数组,既可以当做Object类型,也可以当做Object [ ]类型
Arrays.asList(T...a)方法处理int[]和String[]时的差异,处理int[]时,会把整个数组当做list集合的一个元素,因为基本类型的一维数组被当成Object类型使用,处理String[]时,会把数组中的每个元素转化成list集合的每一个元素

int [ ] a1 = new int[3];

Object obj1= a1;//编译通过
Object [ ] obj2 = a1; //会编译出错

asList与toArray

asList(T...a)方法,Arrays工具类的方法,参数a表示支持列表的数组(int[]就不支持)。该方法同toArray()一起,充当了基于数组的 API 与基于 collection 的 API 之间的桥梁。返回一个受指定数组支持的固定大小的列表

toArray()方法,被Collection对象调用,返回包含此 collection 中所有元素的数组。此方法充当了基于数组的 API 与基于 collection 的 API 之间的桥梁。

数组反射的应用

java.lang.reflect.Array类用于完成对数组的反射操作
以下方法打印一个传入的对象,如果该对象是数组,则打印出元素
private static void printInfor(Object obj) {
Class clazz = obj.getClass();
if(clazz.isArray()){
int length = Array.getLength(obj);//这里的Array是java.lang.reflect.Array
for(int i=0;i<length;i++)
System.out.println(Array.get(obj, i));
}else{
System.out.println(obj);
}

}

怎么得到数组中的元素类型?通过数组名.getClass()得到

ArrayList_HashSet的比较及Hashcode分析

只有存储集合是哈希算法的,hashCode方法才有价值
不覆写hashCode,则根据对象内存的地址值计算哈希值,即使两个对象内容相同,但是因为哈希值不同所以都会存进HashSet
覆写hashCode方法就会根据对象的字段值计算哈希值,两个相同内容的对象就会有一样的hash值,在HashSet中这两个对象只能存一个
当一个对象存储进HashSet集合中以后,就不能修改那个对象中参与计算哈希值的字段了,因为在删除该对象时会因此在HashSet中找不到该对象,防止内存泄露(对象不需要了,但是却一直占用内存空间)
注意:在类中对其父类Object类的equals方法进行覆盖时,注意传入参数类型一定是equals(Object obj),而不是该类的类型,否则就成函数的重载了

框架的概念及用反射技术开发框架的原理

反射的作用-->实现框架功能

框架与工具类区别

我做房子卖给用户住,由用户自己安装门窗和空调,我做的房子就是框架,用户需要使用我的框架,把门窗插入进我提供的框架中。
框架与工具类有区别,工具类被用户的类调用,而框架则是调用用户提供的类。

框架要解决的核心问题

因为在写框架的程序时无法知道要被调用的类名(因为类由以后的用户定义),所以,在程序中无法直接new某个类的实例对象了,而要用反射的方式来做

综合案例

首先直接用new语句创建ArrayList和HashSet实例对象

然后采用配置文件加反射的方式创建ArrayList和HashSet的实例对象
创建配置文件:创建一个File,命名为config.properties,写入className = java.util.ArrayList,注意都不要加引号
public static void main(String[] args) throws Exception{
//首先加载该配置文件
InputStream ips = new FileInputStream("config.properties");//这里使用多态,尽量面向父类或接口编程,但是实际项目中不会使用相对路径设置配置文件
//创建Properties对象,它等效于HashMap(有键值对),可以在初始化时从文件里把键值对加载进来,也可以把内存里的键值对存到硬盘去
Properties props = new Properties();
props.load(ips);//加载流资源
ips.close();//流资源一加载完就关闭流,否则会造成因为系统资源没有释放的内存泄露,这里该对象还存在,会由JVM的垃圾回收器管理
String className = props.getProperty("className");
Collection collections = (Collection)Class.forName(className).newInstance();//调用不带参数的构造方法

//Collection collections = new HashSet();
ReflectPoint pt1 = new ReflectPoint(3,3);
ReflectPoint pt2 = new ReflectPoint(5,5);
ReflectPoint pt3 = new ReflectPoint(3,3);

collections.add(pt1);
collections.add(pt2);
collections.add(pt3);
collections.add(pt1);

System.out.println(collections.size());
}


用类加载器的方式管理资源和配置文件

通常不会把源程序.java文件直接给用户,而是把.class文件打包成jar包给用户,而且在eclipse下源程序目录下的java文件一保存会自动在classpath目录下生成一份.class文件,并且对于源程序目录下的非java文件,比如配置文件,会直接拷贝一份到classpath目录下

配置文件的路径设置

在命令行下的文件的相对路径一定是相对当前工作目录的路径的,比如c:\Documents and Settings\IBM>java MyClass xx.file,xx.file既不是相对于java.exe文件,也不是相对于MyClass.class文件,而是在IBM目录下,也就是在当前工作目录下(c:\Documents and Settings\IBM)去运算的。
只要classpath指向了MyClass.class文件,在任意目录下都可以运行MyClass.class文件。所以在实习项目中,配置文件的路径不使用相对路径,因为不确定相对路径的目录,要用绝对路径。
绝对路径的盘符不是硬编码写入的(因为可能该盘符不存在),而是通过某种方式get而来。

得到资源文件路径的三种方式

JavaWeb的API提供了一个方法getRealPath(),可以得到整个Web项目在硬盘上的具体目录。比如对于“金山词霸”这个项目,配置文件存放在该项目的内部,首先通过getRealPath方法不管用户安装在哪个目录下,可以得到总目录(整个金山词霸项目)在硬盘上的绝对位置,然后拼接上配置文件的内部地址,即金山词霸某个文件的目录,就可以得到该配置文件真实的完整位置(金山词霸/内部地址)。不管是读取还是写入配置文件都可以,注意一定要记住使用完整的路径,该文件一定要存放在程序内部。但完整的路径不是硬编码,而是运算出来的。
最常用(但不能替代上面的方式):通过类加载器加载配置文件。原理:它可以将运行的类的.class文件加载到内存里面,因此也可以将classpath下的资源文件加载进来,比如:InputStream ips= ReflectTest2.class.getClassLoader().getResourceAsStream("cn/itcast/day1/config.properties");//会在classpath指定的目录下(即cn/itcast/day1/)逐一地查找要加载的文件(注意最前面不要加斜杠),如果只写config.properties就会默认在classpath指定的根目录下查找。但是这种方法对于配置文件只能读,不能写,因为没有OutputStream。对于类加载器的方式,配置文件都应该放在classpath目录下。Eclipse操作技巧:要将文件进行移动,可以直接进行拖拽操作。
简化的方法:类直接加载(内部都是调用的ClassLoader):InputStream ips = ReflectTest2.class.getResourceAsStream("config.properties");// 只有文件名。配置文件与class文件在同一个包中。最前面没有斜杠,表示相对路径。
InputStream ips = ReflectTest2.class.getResourceAsStream("resources/config.properties");//存放配置文件的resource包是class文件所在包的子包。最前面没有斜杠,表示是相对路径。

InputStream ips = ReflectTest2.class.getResourceAsStream("/cn/itcast/day1/resources/config.properties");//最前面加了斜杠,表示与class文件的当前目录无关,应从classpath根目录开始写上完整的路径cn/itcast/day1/resources/

总结:如果资源文件与class文件的包有关系,就用相对路径,如果与该包没关系,就用绝对路径。

由内省引出JavaBean的讲解

内省:IntroSpector,字面意为对内部进行检查,主要为了对JavaBean进行操作
JavaBean是一种特殊的Java类,主要用于传递数据信息,这种Java类的方法主要用于访问私有字段,且方法名符合某种命名规则,比如 int getAge()和void setAge(int age)以小写的get和set开头
如果要在两个模块之间传递多个信息,可以将这些信息封装到一个JavaBean中,这种JavaBean的实例对象通常称之为值对象(Value Object,简称VO)。这些信息在类中用私有字段来存储,如果读取或设置这些字段的值,则需要通过一些相应的方法来访问。JavaBean的属性是根据其中的setter和getter方法来确认的,而不是根据其中的成员变量
JavaBean可以当做普通类操作,普通类不一定能当JavaBean操作,除非有set和get方法
去掉set和get以后,剩下的名称就是该JavaBean的属性名,比如getAge-->age getCPU-->CPU。原则:如果第二个字母是小写,就把第一个字母小写
总之,一个类被当做JavaBean使用时,JavaBean的属性是根据方法名推断出来的,根本看不到java类内部的成员变量。

对JavaBean的简单内省操作PropertyDescriptor

在JavaEE开发中,经常要使用JavaBean
JDK提供了对JavaBean进行操作的一系列API,这套API就称为内省。用内省这套API操作JavaBean比用普通类的方式更方便。
Eclipse技巧:

自动生成setter和getter方法:右键-->source-->Generate setters and getters
将选中的代码重构抽取为一个方法:右键-->refractor-->extract method

演示将一个类当做JavaBean并利用内省API:PropertyDescriptor 来读取JavaBean的属性,然后再设置JavaBean的属性
public static void main(String[] args)throws Exception {
ReflectPoint rp = new ReflectPoint(1,3);
//用户提供的属性值,如果按照一般类利用反射方式,应将用户提供的属性值进行如下转换:"x"-->"X"-->"getX"得到方法名,再对类中的方法进行获取
String propertyName = "x";
//当做JavaBean处理利用内省API中的PropertyDescriptor
//两个参数,一个是需要获得的JavaBean的属性名称,二是该JavaBean的类,最好采用obj.getClass方式,更通用
PropertyDescriptor pd = new PropertyDescriptor(propertyName,rp.getClass());
//获取读取方法
Method methodGetX = pd.getReadMethod();
//利用反射调用该方法返回属性值,注意retVal的写法(return value的简写)
Object retVal = methodGetX.invoke(rp);
System.out.println(retVal);
//获取写入方法
Method methodSetX = pd.getWriteMethod();
//利用反射调用该方法写入新值
methodSetX.invoke(rp, 99);
System.out.println(rp.getX());
}

对JavaBean的复杂内省操作BeanInfo

采用BeanInfo获取所有属性方式来查找和设置某个ReflectPoint对象的x属性。

在程序中把一个类当做JavaBean来看,就是调用IntroSpector.getBeanInfo方法,得到的BeanInfo对象封装了把这个类当做JavaBean看的结果信息

//利用refactor将上面的部分代码提取成一个getProperty方法
private static Object getProperty(Object pt1, String propertyName)
throws IntrospectionException, IllegalAccessException,
InvocationTargetException {
//利用PropertyDescriptor获取,简单
/*PropertyDescriptor pd = new PropertyDescriptor(propertyName,pt1.getClass());
Method methodGetX = pd.getReadMethod();
Object retVal = methodGetX.invoke(pt1);*/
//利用BeanInfo获取,需要迭代,复杂
BeanInfo beanInfo =  Introspector.getBeanInfo(pt1.getClass());
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
Object retVal = null;
for(PropertyDescriptor pd : pds){
if(pd.getName().equals(propertyName))
{
Method methodGetX = pd.getReadMethod();
retVal = methodGetX.invoke(pt1);
break;
}
}
return retVal;
}


使用BeanUtils工具包操作JavaBean

BeanUtils是开源Apache提供的工具类,下载后解压会有三个jar包,其中common-beanutils包包含了其他两个子jar包,拷贝它就行了
先用Eclipse导入BeanUtils的jar包,等程序运行出错后再引入logging包

导入jar包的两种方式

第一种直接从外部导入至工程,右键点工程名,选择build path-->Configure Build Path-->Libraries-->Add External JARs。但是将工程拷贝给他人时会找不到jar包
第二种在本工程目录下新建一个普通的lib文件夹(用于放所有的库),在Eclipse中把需要导入的jar包复制粘贴到该目录下,虽然此时该jar包在工程内部,但是还没有加入到build path中,然后右键该jar包,build path-->add to build path,当该jar包前面变为小奶瓶时就完成了导入,这样项目拷贝给他人时该jar包也一同拷过去了
此例中的BeanUtils包用到了其他的jar包,日志开发包logging,所以也需要导入该包

getProperty与setProperty方法

在上面内省例子的基础上,用BeanUtils类先get原来设置好的属性,再将其set为一个新值,get属性时,接受一个字符串,返回的结果为字符串,set属性时可以接受任意类型的对象,通常使用字符串
注意要操作的JavaBean必须是public修饰的
BeanUtils.getProperty(rp, "x")//获得javabean rp 的x的值

BeanUtils.setProperty(rp, "x", "90");
BeanUtils.setBirthday(rp, "birthday.time", "111"); //属性的级联操作,此时ReflectPoint内有一个Date类型的birthday属性,time为Date的一个属性(把Date当javabean看),可以通过BeanUtils来操作它
BeanUtils.getBirthday(rp, "birthday.time");

BeanUtils工具包的好处

自动完成类型转换(字符串直接转换成相应类型),支持属性的级联操作

BeanUtils工具包的其他方法

将一个JavaBean的属性复制到另一个JavaBean:copyProperties(Object dest, Object orig)
将JavaBean转成Map:describe(Obejct bean)
将Map转成JavaBean:populate(Object bean, Map properties)

用BeanUtils直接操作Map对象

Java 7 的新特性
Map map = {name:"adf", age:23};
BeanUtils.setProperty(map, "name", "ljljl");//用BeanUtils直接操作Map对象

PropertyUtils工具包

用PropertyUtils类先get原来设置好的属性,再将其set为一个新值,get属性时返回的结果为该属性本来的类型,set属性时只接受该属性本来的类型。

想自动类型转换就用BeanUtils,如果转换出错或者不需要转换就用PropertyUtils

第5单元:Java5的注解

注解:Annotation

了解和入门注解的应用

注解用于向java编译器传递信息
注解相当于一种标记,在程序中加了注解就相当于为程序打上了某种标记。javac编译器,开发工具和其他程序可以用反射来了解你的类及各种元素上有无何种标记,根据相应标记去做相应的事。
标记可以加在包、类、字段、方法、方法的参数以及局部变量上。
一种注解就是一个类,使用一个注解就相当于创建了一个注解的实例对象

压缩警告注解

System.runFinalizersOnExit(true);
该方法已过时,在Eclipse中会有删除线
在命令行中的执行:



如果API升级了,被调用的某个方法被编译器提示过时(deprecation),就可以在调用该方法的方法前面加一个"压缩警告"注解@SuppressWarnings("Deprecation")告诉编译器忽略“过时”。
Deprecation:过时
压缩警告是jdk自己内部的一个注解

过时注解

用过时注解标记过时方法,提示新的开发人员不再调用该方法,而不是直接删除过时的方法,这样就允许其他调用过方法的程序还能正常运行
使用:在方法前加上@Deprecated

覆盖注解

在类中覆盖父类的方法时,在该方法前加上@Override注解,如果写成了重载,编译器会提示报错

注解的定义与反射调用

注解的定义

注解就相当于你的源程序中要调用的一个类,要在源程序中应用某个注解,得先准备好这个注解类,就像你要调用某个类,得先开发好这个类

创建一个注解

格式:public @interface XXXAnnotation { }
Eclipse右键直接new一个Annotation

元注解

注解上的注解,例如:
@Retention(RetentionPolicy.RUNTIME)
public @interface A { }

@Retention

retention:保留
该元注解有三种取值:RetentionPolicy.SOURCE、RetentionPolicy.CLASS、RetentionPolicy.RUNTIME分别对应java源文件-->class文件-->内存中的字节码
javac把源文件编译成class文件时可能去掉注解
类加载器将class文件加载进内存中也有可能去掉注解
所以在设计注解时需要加上@Retention来标注该注解的生命周期
默认值是在CLASS阶段
@Override和@SuppressWarnings的生命周期都在SOURCE阶段
@Deprecated的生命周期在RUNTIME阶段,因为会根据调用该方法的类的字节码查看该类是否过时

@Target

用于说明注解可以应用在哪个成分上
属性值:ElementType.METHOD、ElementType.TYPE(TYPE的意思包括了class,interface,enum)、FIELD、LOCAL VARIABLE、PAKAGE、PARAMETER、CONSTRUCTOR、ANNOTATION_TYPE
默认值为任何元素,设置Target等于ElementType.METHOD,原来加在类上的注解就报错了,改为用数组方式设置@Target({ElementType.METHOD,ElementType.TYPE})就可以了
注意导入import java.lang.annotation.*;

注解的反射调用



main方法必须放在一个类中,但是不属于该类。
//首先判断注解是否应用在该类上
if(B.class.isAnnotationPresent(A.class))
A a = (A)B.class.getAnnotation(A.class);//得到注解的实例对象,但是编译时不知道是哪一个具体的注解,运行时才知道,所以要强转
此时需要在注解类A上加元注解Retention并设置值为RetentionPolicy.RUNTIME,让该注解A保留在运行时内存的字节码中。否则运行程序时,判断注解会返回false

为注解增加各种属性

什么是注解的属性?

一个注解相当于一个胸牌,如果你胸前贴了胸牌,就是某个学校的学生,如果还想区分出是哪个班的学生,这时可以为胸牌增加一个属性进行区分。
加了属性的标记效果为:@MyAnnotation(color = "red")

定义和应用基本类型的属性

在注解类中增加String color();//注解中对属性的定义很像接口中的方法,定义了属性之后,注解后必须对属性进行设置,否则编译错误。
@MyAnnotation(color = "red");

value属性的特殊应用

对于注解中只有一个名为value的属性需要设置,这时可以不写属性名,只写属性值。@MyAnnotation("red");

属性的默认值

可以为属性指定缺省属性,例如 String color() default "blue";

为注解增加高级属性

数组类型的属性

int[] arrayAttr() default {1,2,3};

@MyAnnotation(arrayAttr = {2,3,4})

如果数组属性中只有一个元素,这时候属性值部分可以省略大括号,例arrayAttr = 1,@Target(ElementType.METHOD)--属性的名称为value

int [ ] 因为不是Object [ ],只当做一个Object,所以不能用asList转为list

枚举类型的属性

EnumTest.TrafficLamp lamp() ;//该枚举为EnumTest中的内部类

@MyAnnotation(lamp=EnumTest.TrafficLamp.GREEN)

注解类型的属性

定义一个注解属性:MetaAnnotation annotationAttr() default @MetaAnnotation("xxxx");
注解中的属性值还是一个注解:@MyAnnotation(annotationAttr=@MetaAnnotation(“yyy”) )

可以认为上面这个@MyAnnotation是MyAnnotaion类的一个实例对象,同样的道理,可以认为上面这个@MetaAnnotation是MetaAnnotation类的一个实例对象,调用代码如下:

MetaAnnotation ma = myAnnotation.annotationAttr();

System.out.println(ma.value));

注解的详细语法可以通过看java语言规范了解,即看java的language specification。

第6单元:java5的泛型

入门泛型的基本应用

Jdk 1.5以前的集合类中存在的问题

ArrayList collection = new ArrayList();
collection.add(1);
collection.add(1L);
collection.add("abc");
int i = (Integer) collection.get(1);//编译要强制类型转换且运行时出错!
Jdk 1.5的集合类希望你在定义集合时,明确表示你要向集合中装哪种类型的数据,无法加入指定类型以外的数据

ArrayList<Integer> collection2 = new ArrayList<Integer>();
collection2.add(1);
/*collection2.add(1L);
collection2.add(“abc”);*///这两行代码编译时就报告了语法错误
int i2 = collection2.get(0);//不需要再进行类型转换

不仅使用集合时用到了泛型,使用反射时也用到泛型
Class<String> clazzString = String.class;
Constructor<String> con = clazzString.getConstructor(StringBuffer.class);
String s1 = con.newInstance(new StringBuffer("abc"));//不需要再进行String的类型转换了
System.out.println(s1.charAt(2));


没有使用泛型时,只要是对象,不管是什么类型的对象,都可以存储进同一个集合中。使用泛型集合,可以将一个集合中的元素限定为一个特定类型,集合中只能存储同一个类型的对象,这样更安全;并且当从集合获取一个对象时,编译器也可以知道这个对象的类型,不需要对对象进行强制类型转换,这样更方便。

在JDK 1.5中,你还可以按原来的方式将各种不同类型的数据装到一个集合中,但编译器会报告unchecked警告(黄色灯泡)。可以用suppress warning注解让其不要报告


泛型的内部原理及更深应用

泛型是提供给javac编译器使用的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,编译器编译完带类型说明的集合时会去除掉“类型”信息,使程序运行效率不受影响,因此,对于参数化的泛型类型(比如ArrayList<Integer>),getClass()方法的返回值和原始类型(比如ArrayList)完全一样:

//用下面的代码查看getClass()方法返回的结果已经去掉了“类型”信息。
System.out.println(new ArrayList<Integer>().getClass().getName());
System.out.println(new ArrayList<String>().getClass().getName());


由于编译生成的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中加入其它类型的数据,例如,用反射得到集合,再调用其add方法即可。

ArrayList<String> al1 = new ArrayList<String>();
ArrayList<Integer> al2 = new ArrayList<Integer>();
System.out.println(al1.getClass()==al2.getClass());//结果为true,证明了泛型的去类型化
//al2.add("abc");编译会报错,下面用反射跳过编译器添加字符串
Method m = al2.getClass().getMethod("add",Object.class);
m.invoke(al2, "abc");
System.out.println(al2.get(0));

ArrayList<E>类定义和ArrayList<Integer>类引用中涉及如下术语:

整个称为ArrayList<E>泛型类型
ArrayList<E>中的E称为类型变量或类型参数
整个ArrayList<Integer>称为参数化的类型
ArrayList<Integer>中的Integer称为类型参数的实例或实际类型参数
ArrayList<Integer>中的<>念着typeof
ArrayList称为原始类型

参数化类型与原始类型的兼容性:

参数化类型可以引用一个原始类型的对象,编译报告警告,例如, Collection<String> c = new Vector();//可以,例如jdk1.4的一个方法返回一个原始类型的集合对象,在jdk1.5中要使用参数化类型接受该方法的返回值,为了兼容jdk1.4,编译器会允许。所以可不可以,不就是编译器一句话的事吗?
原始类型可以引用一个参数化类型的对象,编译报告警告,例如, Collection c = new Vector<String>();//例如jdk1.4的某个方法接受一个集合参数,该参数是原始类型的,在jdk1.5中调用该方法需要传递一个参数化类型的对象进去,兼容性考虑编译器会允许新的类型能传进去

参数化类型不考虑类型参数的继承关系:

Vector<String> v = new Vector<Object>(); //错误!///不写<Object>没错,写了就是明知故犯
Vector<Object> v = new Vector<String>(); //也错误!

泛型中的类型参数严格说明集合中装载的数据类型是什么和可以加入什么类型的数据,记住:Collection<String>和Collection<Object>是两个没有转换关系的参数化的类型。假设Vector<String> v = new Vector<Object>();可以的话,那么以后从v中取出的对象当作String用,而v实际指向的对象中可以加入任意的类型对象;假设Vector<Object> v = new Vector<String>();可以的话,那么以后可以向v中加入任意的类型对象,而v实际指向的集合中只能装String类型的对象。咱们80的跟70的谈事,可以不按80的规则走,但是跟80谈,咱们必须按80的套路来。

编译器不允许创建泛型变量的数组。即在创建数组实例时,数组的元素不能使用参数化的类型,例如,下面语句有错误:

Vector<Integer> vectorList[] = new Vector<Integer>[10];

思考题:下面的代码会报错误吗?(不会,编译时严格按照语法检查,不考虑运行时的类型)

Vector v1 = new Vector<String>();

Vector<Object> v = v1;


泛型中的?通配符及拓展

问题:

定义一个方法,该方法用于打印出任意参数化类型的集合中的所有数据,该方法如何定义呢?

错误方式:

public static void printCollection(Collection<Object> cols) {
for(Object obj:cols) {
System.out.println(obj);
}
cols.add("string");//没错
cols = new HashSet<Date>();//会报告错误!Cols<Object> 中的Object只是说明Cols<Object> 实例对象中的方法接受的参数是Object Cols<Object> 是一种具体类型,new HashSet<Date>也是一种具体类型,两者没有兼容性问题。
}


正确方式:

public static void printCollection(Collection<?> cols) {
for(Object obj:cols) {
System.out.println(obj);
}
//cols.add("string");//错误,因为它不知自己未来匹配就一定是String
cols.size();//没错,此方法与类型参数没有关系
cols = new HashSet<Date>();//可以,可以让?指向任何其他类型。Collection<?> a 可以与任意参数化的类型匹配,但到底匹配的是什么类型,只有以后才知道,所以,a=new ArrayList<Integer>和a=new ArrayList<String>都可以,但a.add(new Date())或a.add(“abc”)都不行,
}


总结:

使用?通配符可以引用其他各种参数化的类型,?通配符定义的变量主要用作引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法。
?代表不知道什么类型,E代表一个具体的类型

泛型限定:表示在某个范围内的任意类型

限定通配符的上边界:

正确:Vector<? extends Number> x = new Vector<Integer>();

错误:Vector<? extends Number> x = new Vector<String>();

限定通配符的下边界:

正确:Vector<? super Integer> x = new Vector<Number>();

错误:Vector<? super Integer> x = new Vector<Byte>();

提示:

限定通配符总是包括自己。

?只能用作引用,不能用它去给其他变量赋值:

Vector<? extends Number> y = new Vector<Integer>();

Vector<Number> x = y;

上面的代码错误,不能把一个不确定的类型赋值给一个具体的类型。只能通过强制类型转换方式来赋值。

泛型集合的综合应用案例

能写出下面的代码即代表掌握了Java的泛型集合类:

HashMap<String,Integer> hm = new HashMap<String,Integer>();
hm.put("zxx",19);
hm.put("lis",18);

Set<Map.Entry<String,Integer>> mes= hm.entrySet();
for(Map.Entry<String,Integer> me : mes) {
System.out.println(me.getKey() + ":" + me.getValue());
对在jsp页面中也经常要对Set或Map集合进行迭代:

<c:forEach items=“${map}” var=“entry”>
${entry.key}:${entry.value}
</c:forEach>

自定义泛型方法及其应用

Java中的泛型类型(或者泛型)类似于 C++ 中的模板。但是这种相似性仅限于表面,Java 语言中的泛型基本上完全是在编译器中实现,用于编译器执行类型检查和类型推断,然后生成普通的非泛型的字节码,这种实现技术称为擦除(erasure)(编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除)。这是因为扩展虚拟机指令集来支持泛型被认为是无法接受的,这会为 Java 厂商升级其 JVM 造成难以逾越的障碍。所以,java的泛型采用了可以完全在编译器中实现的擦除方法。

例如,下面这两个方法,编译器会报告错误,它不认为是两个不同的参数类型,而认为是同一种参数类型,因为参数上的泛型在运行时已被擦除。

private static void applyGeneric(Vector<String> v){
}
private static void applyGeneric(Vector<Date> v){
}


如下函数的结构很相似,仅类型不同:

int add(int x,int y) {

return x+y;

}

float add(float x,float y) {

return x+y;

}

double add(double x,double y) {

return x+y;

}

C++用模板函数解决,只写一个通用的方法,它可以适应各种类型,示意代码如下:

template<class T>

T add(T x,T y) {

return (T) (x+y);

}

Java的泛型方法没有C++模板函数功能强大,java中的如下代码无法通过编译:

<T> T add(T x,T y) {
return (T) (x+y);
//return null;
}

声明泛型的类型
用于放置声明泛型的类型参数的尖括号应出现在方法的其他所有修饰符之后和在方法的返回类型之前,也就是紧邻返回值之前。按照惯例,类型参数通常用单个大写字母表示。

交换数组中的两个元素的位置的泛型方法语法定义如下:

static <E> void swap(E[] a, int i, int j) {
E t = a[i];
a[i] = a[j];
a[j] = t;
}//或用一个面试题讲:把一个数组中的元素的顺序颠倒一下


只有引用类型才能作为泛型方法的实际参数,对于add方法,使用基本类型的数据进行测试没有问题,这是因为自动装箱和拆箱了。但是对于swap(new int[3],3,5);语句会报告编译错误,这是因为编译器不会对new int[3]中的int自动拆箱和装箱了,因为new int[3]本身已经是对象了,你想要的有可能就是int数组呢?它装箱岂不弄巧成拙了。

除了在应用泛型时可以使用extends限定符,在定义泛型时也可以使用extends限定符,例如,Class.getAnnotation()方法的定义。并且可以用&来指定多个边界,如<V extends Serializable & cloneable> void method(){},V的类型必须是实现两个接口的类

普通方法、构造方法和静态方法中都可以使用泛型。

也可以用类型变量表示异常,称为参数化的异常,可以用于方法的throws列表中,但是不能用于catch子句中。

用下面的代码说明对异常如何采用泛型:

private static <T extends Exception> sayHello() throws T{
try{

}catch(Exception e){ //catch中必须捕获具体的异常对象
throw (T)e;
}
}


在泛型中可以同时有多个类型参数,在定义它们的尖括号中用逗号分,例如:

public static <K,V> V getValue(K key) { return map.get(key);}

自定义泛型方法的练习与类型推断总结

自定义泛型方法的练习

练习1:编写一个泛型方法,自动将Object类型的对象转换成其他类型。

public static <T> T autoConvertType(Object obj)
{
return (T)obj;
}


测试代码:

Object xxx = "abc";
String yyy = autoConvertType(xxx);//具体类型根据方法的返回值类型决定


泛型方法的另外一个常见应用就是调用者无需对返回值进行类型转换

练习2:定义一个方法,可以将任意类型的数组中的所有元素填充为相应类型的某个对象。

public static <T> void fillArray(T[] arr, T t){
for(int i=0; i<arr.length; i++)
arr[i] = t;
}


练习3:采用自定泛型方法的方式打印出任意参数化类型的集合中的所有内容。

public static <E> void printCollection(Collection<E> cols) {
for(E obj:cols) {
System.out.println(obj);
}//这时可以add相应的元素了:cols.add(e);
}
在这种情况下,前面的通配符方案要比范型方法更有效,当一个类型变量用来表达两个参数之间或者参数和返回值之间的关系时,即同一个类型变量在方法签名的两处被使用,或者类型变量在方法体代码中也被使用而不是仅在签名的时候使用,才需要使用范型方法。

练习4:定义一个方法,把任意参数类型的集合中的数据安全地复制到相应类型的数组中。

这个需要使用泛型方法进行定义,如果使用如下形式:

static void copy(Collection a, Object[] b);

否则有可能出现A类型的数据复制进B类型的数组中的情况。

使用泛型方法的定义形式为:

static <T> void copy(Collection<T> a,T[] b);//copy(new Vector<Date>(), new String[10])会出现问题,因为编译器的类型推断有传播性,Date和String不是同一类型

练习5:定义一个方法,把任意参数类型的一个数组中的数据安全地复制到相应类型的另一个数组中。

static <T> void copy(T[] a, T[] b);//copy(new Date[10], new String[10])没有问题,因为编译器进行类型推断时会取两者的公共类型,即Object[]

类型参数的类型推断

编译器判断范型方法的实际类型参数的过程称为类型推断,类型推断是相对于知觉推断的,其实现方法是一种非常复杂的过程。

根据调用泛型方法时实际传递的参数类型或返回值的类型来推断,具体规则如下:

1. 当某个类型变量只在整个参数列表中的所有参数和返回值中的一处被应用了,那么根据调用方法时该处的实际应用类型来确定,这很容易凭着感觉推断出来,即直接根据调用方法时传递的参数类型或返回值来决定泛型参数的类型,例如:

swap(new String[3],3,4) --> static <E> void swap(E[] a, int i, int j)

2. 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型都对应同一种类型来确定,这很容易凭着感觉推断出来,例如:

add(3,5) --> static <T> T add(T a, T b)

3. 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型,且没有使用返回值,这时候取多个参数中的最大交集类型,例如,下面语句实际对应的类型就是Number了,编译没问题,只是运行时出问题:

fill(new Integer[3],3.5f) --> static <T> void fill(T[] a, T v)

4. 当某个类型变量在整个参数列表中的所有参数和返回值中的多处被应用了,如果调用方法时这多处的实际应用类型对应到了不同的类型, 并且使用返回值,这时候优先考虑返回值的类型,例如,下面语句实际对应的类型就是Integer了,编译将报告错误,将变量x的类型改为float,对比eclipse报告的错误提示,接着再将变量x类型改为Number,则没有了错误:

int x =(3,3.5f) --> static <T> T add(T a, T b)
对于前面的add方法,下面这两条语句都可以运行:

System.out.println(add(2.0f,3.5));

System.out.println(add(1.5f,"abc"));

5. 参数类型的类型推断具有传递性,下面第一种情况推断实际参数类型为Object,编译没有问题,而第二种情况则根据参数化的Vector类实例将类型变量直接确定为String类型,编译将出现问题:

copy(new Integer[5],new String[5]) --> static <T> void copy(T[] a,T[] b);

copy(new Vector<String>(), new Integer[5]) --> static <T> void copy(Collection<T> a , T[] b);

自定义泛型类的应用

Dao:Data Access Object-->用于对某个实体对象的增删改查的操作,一般有5个方法,查有两个方法,按ID查找和按条件查找
CRUD:Create Retrieve Update Delete,就是增删改查(JavaEE)
一个项目中一般有很多实体类型和要编写很多Dao,不需要为每一种实体类型都编写一个Dao类,而是使用一个泛型类定义:

class GenericDAO<T> {
public  void save(T t) {

}

public void delete(int id) {

}

public  void update(T t) {

}

public T findById(int id)
{
return null;
}

public Set<T> findByConditions(String conditions) {
return null;
}
}


如果类的实例对象中的多处都要用到同一个泛型参数,即这些地方引用的泛型类型要保持同一个实际类型时,这时候就要采用泛型类型的方式进行定义,也就是类级别的泛型,语法格式如下:

public class GenericDao<T> {
private T field1;
public void save(T obj){}
public T getById(int id){}
}

类级别的泛型是根据引用该类名时指定的类型信息来参数化类型变量的,例如,如下两种方式都可以:

GenericDao<String> dao = null;
new genericDao<String>();

注意:

在对泛型类型进行参数化时,类型参数的实例必须是引用类型,不能是基本类型。

当一个变量被声明为泛型时,只能被实例变量、方法和内部类调用,而不能被静态变量和静态方法调用。因为静态成员是被所有参数化的类所共享的,所以静态成员不应该有类级别的类型参数。如果静态方法需要使用泛型,则在该方法上单独定义

问题:类中只有一个方法需要使用泛型,是使用类级别的泛型,还是使用方法级别的泛型?方法级别的泛型

通过反射获得泛型的实际类型参数

Vector<Date> v = new Vector<Date>();
现在要获取变量v的泛型的类型参数Date,通过变量v自身是无法得到泛型的类型参数的。可以把该变量交给一个方法去使用,通过该方法可以获得变量的泛型的类型参数。
public static void main(String[] args) throws Exception {
Method m =GenericTest.class.getMethod("applyVector", Vector.class);
Type[] types = m.getGenericParameterTypes();//得到泛型参数类型列表,是一个通用的类型Type。Type是Class的父类,下面还有ParameterizedType类
ParameterizedType pType = (ParameterizedType)types[0];//获取参数类型
System.out.println(pType.getRawType());//获取参数类型的原始类型,结果为class java.util.Vector
System.out.println(pType.getActualTypeArguments()[0]);//获取参数类型的实际类型参数,可能有多个(比如对于Map),所以返回的是一个数组。结果为:class java.util.Date
}
public static void applyVector(Vector<Date> v) {}

第7单元:类加载器的深入讲解与应用

类加载器及其委托机制的深入分析

类加载器概述

类加载器是用来加载Java类的。

Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类:BootStrap,ExtClassLoader,AppClassLoader

类加载器也是Java类,因为其他是java类的类加载器本身也要被类加载器加载,显然必须有第一个类加载器不是java类,这正是BootStrap,在Java虚拟机内核中。

String name = ClassLoaderTest.class.getClassLoader().getClass().getName();
System.out.println(name);//sun.misc.Launcher$AppClassLoader
System.out.println(System.class.getClassLoader());//null。因为System类由BootStrap类加载器加载


Java虚拟机中的所有类加载器采用具有父子关系的树形结构进行组织,在实例化每个类加加器对象时,需要为其指定一个父级类加载器对象或者默认采用系统类加载器为其父级类加载。

用下面的代码让查看类加载器的层次结构关系:

ClassLoader loader = ClassLoaderTest.class.getClassLoader();
//打印出当前的类加载器,及该类加载器的各级父类加载器
while(loader!=null){
System.out.println(loader.getClass().getName());
loader = loader.getParent();
}
System.out.println(loader);
//结果为:sun.misc.Launcher$AppClassLoader sun.misc.Launcher$ExtClassLoader null


类加载器之间的父子关系和管辖范围图



eclipse打JAR包操作:右键类名-->Export-->Java-->JAR file-->选择对应jre目录的lib

lib下rt.jar存放java系统提供的那些类,ext目录为扩展目录,存放的是扩展的jar包,可以把自己的jar包放在里面让类加载器加载。

用eclipse的打包工具将ClassLoaderTest输出到相应jdk目录的jre/lib/ext目录下的itcast.jar包,再在eclipse中运行这个类,运行结果显示为ExtClassLoadr。此时的环境状态是classpath目录有ClassLoaderTest.class,ext/itcast.jar包中也有ClassLoaderTest.class,此时会优先加载父类类加载器目录下的内容,也是类加载器的委托机制。

写自己的ClassLoader类时都要继承ClassLoader类,每个类加载器都有一个爸爸,即在实例化每个类加加器对象时,需要为其指定一个父级类加载器对象或者默认采用系统类加载器为其父级类加载,这样就把它挂到了Java虚拟机的类加载器的树状结构上去了,自己的类加载器会解密指定的特殊目录下的文件。

类加载器的委托机制

每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类装载器去加载类,这就是类加载器的委托模式。

当Java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?

首先当前线程的类加载器去加载线程中的第一个类。
如果类A中引用了类B,Java虚拟机将使用加载类A的类装载器来加载类B。
还可以直接调用ClassLoader对象的loadClass(String className)方法来指定某个类加载器去加载某个类。

每个类加载器加载类时,又先委托给其上级类加载器。

类装载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类装载器去进行真正的加载。当回退到最初的类装载器时,如果它自己也不能完成类的装载,那就应报告ClassNotFoundException异常。不是再去找发起者类加载器的儿子,因为没有getChild方法,即使有,那有多个儿子,找哪一个呢?这样不会出现多份字节码文件重复的现象,会优先利用父类中已有的字节码文件。
对着类加载器的层次结构图和委托加载原理,解释先前将ClassLoaderTest输出成jre/lib/ext目录下的itcast.jar包中后,运行结果为ExtClassLoader的原因。因为ExtClassLoader是AppClassLoader的父加载器,优先找。

面试题:能不能自己写个类叫java.lang.System?通常不可以,为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,也就是总是使用爸爸们能找到的类,这样总是使用java系统提供的System。不过可以写自己的类加载器去加载

把先前编写的类加入到jdk的rt.jar中,会有怎样的效果呢?不行!!!看来是不能随意将自己的class文件加入进rt.jar文件中的。

自定义类加载器的编写原理分析

自定义的类加载器的必须继承抽象类ClassLoader

loadClass方法与findClass方法:loadClass方法的功能是找父类加载器,该方法已经被ClassLoader类实现,findClass方法是自己实现的loadClass里面的局部细节,所以只需要覆盖findClass方法。findClass是protected修饰的,只给子类使用
为什么要覆盖findClass方法?是为了保留loadClass方法里的流程,因为如果覆盖了loadClass方法,那么这个流程就没有了。这是模板方法设计模式:总体的流程在父类中已经规定好了,但是一些细节父类无法确定,就把它们空出来留给子类实现

defineClass方法:返回class文件的字节码,已由ClassLoader类实现。

编写对class文件进行加密的工具类

有包名的类不能调用无包名的类
编写一个对文件内容进行简单加密的程序。

public class MyClassLoader{
public static void main(String[] args) throws Exception{
String srcPath = args[0];
String destDir = args[1];
FileInputStream fis = new FileInputStream(srcPath);
String destFileName = srcPath.substring(srcPath.lastIndexOf('\\')+1);
String destPath = destDir+"\\"+destFileName;
FileOutputStream fos = new FileOutputStream(destPath);
cypher(fis,fos);
fis.close();
fos.close();
}
//加密方法
public static void cypher(InputStream in, OutputStream out) throws Exception {
int num = 0;
while((num=in.read())!=-1)
out.write(num^0xff);//对每个字节与ff执行异或运算
}
}


下面这段代码可能遇到255的字节,当成byte就成了-1了

/*byte b = 0;
while((b = (byte)istream.read()) != -1)
{
ostream.write(b ^ 0xff);//对每个字节与ff执行异或运算
}*/

被加密的java类的代码:

public class ClassLoaderAttachment extends Date { //这继承Date是因为该文件被加密之后的class文件不能被编译器识别,必须用父类来接受
@Override
public String toString() {
return "this is the text from ClassLoaderAttachement";
}
}
加密过的class文件是无法运行的

编写和测试自己编写的解密类加载器

编写了一个自己的类装载器,可实现对加密过的类进行装载和解密:

public class MyClassLoader extends ClassLoader{
private String classDir;

public MyClassLoader(){

}

public MyClassLoader(String classDir){
this.classDir = classDir;
}

public static void main(String[] args) throws Exception{
String srcPath = args[0];
String destDir = args[1];
FileInputStream fis = new FileInputStream(srcPath);
String destFileName = srcPath.substring(srcPath.lastIndexOf('\\')+1);
String destPath = destDir+"\\"+destFileName;
FileOutputStream fos = new FileOutputStream(destPath);
cypher(fis,fos);
fis.close();
fos.close();
}

public static void cypher(InputStream in, OutputStream out) throws Exception {
int num = 0;
while((num=in.read())!=-1)
out.write(num^0xff);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {//子类不能抛出比父类更广泛的异常,所以对于io异常下面需要进行try catch操作
String classFileName = classDir+"\\"+name+".class";
try{
FileInputStream fis = new FileInputStream(classFileName);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
cypher(fis,bos);//解密加密过的class文件
fis.close();
byte[] bytes = bos.toByteArray();
return defineClass(bytes,0,bytes.length);

}catch(Exception e){
e.printStackTrace();
}
return super.findClass(name);//否则返回父类的该方法
}
}
编写一个程序调用类加载器加载类,:
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
Class clazz = new MyClassLoader("itcast").loadClass("ClassLoaderAttachment");
Date date = (Date)clazz.newInstance();//不能用被加密的类的类名定义引用变量,因为编译器无法识别这个被加密的类的class文件,这里只能用其父类Date接受
System.out.println(date.toString());
}
}

此时如果用父类的加载器加载这个被加密的文件(就是把该文件放在父类加载器加载的文件夹下,相应的类文件名也要加上包名),就会报错了。
程序中可以除了使用ClassLoader.load方法之外,还可以使用设置线程的上下文类加载器或者系统类加载器,然后再使用Class.forName。

类加载器的一个高级问题的实验分析

Tomcat服务器内置了许多的类加载器,对于Web项目里面的Java类Servlet,就是被这些类加载器加载的
编写一个能打印出自己的类加载器名称和当前类加载器的父子结构关系链的MyServlet,正常发布后,看到打印结果为WebAppClassloader。

注意:Tomcat运行时的jdk版本应和eclipse相同,比如,如果新配置了eclipse的工作环境,比如曾经将eclispe运行环境设置为了eclipse自带的1.5,后来输出MyServlet的jar文件到了这个jdk1.5中,而tomcat运行时用的是jdk1.6,就会无法加载。

把MyServlet.class文件打jar包,放到ext目录中,重启tomcat,发现找不到HttpServlet的错误,因为MyServlet用到了HttpServlet。
把Tomcat目录下的servlet-api.jar也放到ext目录中,问题解决了,打印的结果是ExtclassLoader 。

父级类加载器加载的类无法引用只能被子类加载器加载的类,原理如下图:



sun公司的建议,发布到lib/ext/itcast.jar中后,如果没有删除web-inf/classes/目录下的MyServlet.class文件,不会出现问题。但是实际中还是出了问题,这说明tomcat似乎没有按照sun的建议做。根据邮件视频中的类加载器实际应用,可以确定WebAppClassLoader是自己先加载的,而不是ExtClassLoader加载的

第8单元:动态代理技术的深入讲解

分析代理类的作用与原理及AOP概念

代理的概念与作用

生活中的代理

武汉人从武汉的代理商手中买联想电脑和直接跑到北京传智播客旁边来找联想总部买电脑,你觉得最终的主体业务目标有什么区别吗?基本上一样吧,都解决了核心问题,但是,一点区别都没有吗?从代理商那里买真的一点好处都没有吗?
批发进货的成本和运输费的优势,比你自己直接到北京总部买的总成本要低

程序中的代理

要为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能,例如,异常处理、日志、计算方法的运行时间、事务管理、等等,你准备如何做?
编写一个与目标类具有相同接口的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法时加上系统功能的代码。 (参看代理架构图)
如果采用工厂模式和配置文件的方式进行管理,则不需要修改客户端程序,在配置文件中配置是使用目标类还是代理类(比如在config.prperties文件中:className = java.util.HashSet),这样以后很容易切换,譬如,想要日志功能时就配置代理类,否则配置目标类,这样,增加系统功能很容易,以后运行一段时间后,又想去掉系统功能也很容易。

代理架构图



接口类的应用是为了在程序中能够自由切换是使用代理类还是目标类。比如Collection c = new ArrayList(); Collection 就是一个接口类
画示意图时,先画一个客户端直接调用目标、然后加上代理和客户端改为调用代理。

AOP:面向方面编程

系统中存在交叉业务,一个交叉业务就是要切入到系统中的一个方面,如下所示:

安全 事务 日志

StudentService ------|----------|------------|-------------

CourseService ------|----------|------------|-------------

MiscService ------|----------|------------|-------------
安全,事务,日志等功能要贯穿到好多个模块中,所以,它们就是交叉业务

用具体的程序代码描述交叉业务:

method1 method2 method3

{ { {

------------------------------------------------------切面

.... .... ......

------------------------------------------------------切面

} } }

交叉业务的编程问题即为面向方面的编程(Aspect oriented program ,简称AOP),AOP的目标就是要使交叉业务模块化。可以采用将切面代码移动到原始方法的周围,这与直接在方法中编写切面代码的运行效果是一样的,如下所示:

------------------------------------------------------切面

func1 func2 func3

{ { {

.... .... ......

} } }

------------------------------------------------------切面

使用代理技术正好可以解决这种问题,代理是实现AOP功能的核心和关键技术。

重要原则:不要把供货商暴露给你的客户

动态代理技术

要为系统中的各种接口的类增加代理功能,那将需要太多的代理类,全部采用静态代理方式,将是一件非常麻烦的事情!写成百上千个代理类,太累!
动态代理有两种方式:

JVM可以在运行期动态生成出类的字节码,这种动态生成的类往往被用作代理类,即动态代理类。JVM生成的动态类必须实现一个或多个接口,所以,JVM生成的动态类只能用作具有相同接口的目标类的代理。
CGLIB库可以动态生成一个类的子类,一个类的子类也可以用作该类的代理,所以,如果要为一个没有实现接口的类生成动态代理类,那么可以使用CGLIB库。它是一个开源的第三方类库。

代理类的各个方法中通常除了要调用目标的相应方法和对外返回目标返回的结果外,还可以在代理方法中的如下四个位置加上系统功能代码:

在调用目标方法之前
在调用目标方法之后
在调用目标方法前后
在处理目标方法异常的catch块中

示例代码:
Class proxy{
void sayHello(){
……….
try{
target.sayHello();
}catch(Exception e){
………..
}
………….
}
}

创建动态类及查看其方法列表信息

创建实现了Collection接口的动态类和查看其名称,分析Proxy.getProxyClass方法的各个参数。

编码列出动态类中的所有构造方法和参数签名

编码列出动态类中的所有方法和参数签名

//使用Proxy的静态方法getProxyClass生成动态类,传递的ClassLoader参数通常与传递的接口参数的ClassLoader一致,并且该代理类由该类加载器定义
Class clazzProxy = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
System.out.println(clazzProxy.getName());
System.out.println("---begin constructors list---");
Constructor[] constructors = clazzProxy.getConstructors();
for(Constructor constructor:constructors){
//单线程用StringBuilder,性能高,多线程用StringBuffer
StringBuilder sBuilder = new StringBuilder(constructor.getName());
sBuilder.append('(');
Class[] clazzParams = constructor.getParameterTypes();
for(Class clazzParam:clazzParams){
sBuilder.append(clazzParam.getName()+',');
}
//删除最后一个逗号
if(clazzParams!=null&&clazzParams.length!=0)
sBuilder.deleteCharAt(sBuilder.length()-1);
sBuilder.append(')');
System.out.println(sBuilder);
}

System.out.println("---begin methods list---");
Method[] methods = clazzProxy.getMethods();
for(Method method:methods){
//单线程用StringBuilder,性能高,多线程用StringBuffer
StringBuilder sBuilder = new StringBuilder(method.getName());
sBuilder.append('(');
Class[] clazzParams = method.getParameterTypes();
for(Class clazzParam:clazzParams){
sBuilder.append(clazzParam.getName()+',');
}
//删除最后一个逗号
if(clazzParams!=null&&clazzParams.length!=0)
sBuilder.deleteCharAt(sBuilder.length()-1);
sBuilder.append(')');
System.out.println(sBuilder);
}
}

运行结果显示生成的动态类没有空参构造方法,只有一个构造方法:$Proxy0(java.lang.reflect.InvocationHandler)

创建动态类的实例对象及调用其方法

创建动态类的实例对象

用反射获得构造方法
编写一个最简单的InvocationHandler类
调用构造方法创建动态类的实例对象,并将编写的InvocationHandler类的实例对象传进去
打印创建的对象和调用对象的没有返回值的方法和getClass方法,演示调用其他有返回值的方法报告了异常。

System.out.println("---begin create instance object---");
//Object obj = clazzProxy.newInstance();
//clazzProxy.getConstructor().newInstance();//这里不能调用不带参数的构造方法,因为生成的动态类没有空参构造方法
//用反射获得构造方法
Constructor constructor = clazzProxy.getConstructor(InvocationHandler.class);
//InvocationHandler是一个接口,只能new它的实现类,所以自定义一个简单的InvocationHandler类
class MyInvocationHandler1 implements InvocationHandler {
@Override
public Object invoke(Object arg0, Method arg1, Object[] arg2)
throws Throwable {
return null;
}
}
//调用构造方法创建动态类的实例对象,并将编写的InvocationHandler类的实例对象传进去
Collection proxy1 = (Collection)constructor.newInstance(new MyInvocationHandler1());
//打印该对象,结果为null,表示该对象的toString方法返回为null,其实proxy1不为null,因为没有报异常
System.out.println(proxy1);
//调用对象的没有返回值的方法和getClass方法,演示调用其他有返回值的方法报告了异常。
proxy1.clear();//没有返回值,没有报告异常,因为invoke方法返回null,
System.out.println(proxy1.getClass().getName());//结果为$Proxy0
//proxy1.add("abc");//返回boolean,调用有基本类型返回值的方法会抛出异常,因为此时invoke方法内部返回为null,给了add方法
//proxy1.size();//返回int,也报告了异常,因为此时invoke方法内部返回为null

完成InvocationHandler对象的内部功能

将创建动态类的实例对象的代理改成匿名内部类的形式编写,锻炼匿名内部类的使用。

Collection proxy2 = (Collection)constructor.newInstance(new InvocationHandler(){
@Override
public Object invoke(Object arg0, Method arg1, Object[] arg2)
throws Throwable {
// TODO Auto-generated method stub
return null;
}

});
总结思考:让jvm创建动态类及其实例对象,需要给它提供哪些信息?

三个方面:

生成的类中有哪些方法,通过让其实现哪些接口的方式进行告知;
产生的类字节码必须有个一个关联的类加载器对象;
生成的类中的方法的代码是怎样的,也得由我们提供。把我们的代码写在一个约定好了接口对象的方法中,把对象传给它,它调用我的方法,即相当于插入了我的代码。提供执行代码的对象就是那个InvocationHandler对象,它是在创建动态类的实例对象的构造方法时传递进去的。在上面的InvocationHandler对象的invoke方法中加一点代码,就可以看到这些代码被调用运行了。

用Proxy.newProxyInstance方法直接把三步一步到位就创建出代理对象。该方法接受以上三个参数。

Collection proxy3 = (Collection)Proxy.newProxyInstance(
Collection.class.getClassLoader(),
//接口可能有多个,所以用数组,因为不是最后一个参数,所以这里不用可变数组
new Class[]{Collection.class},
new InvocationHandler(){
ArrayList target = new ArrayList();//指定代理的目标对象,这里是成员变量,所以添加几个元素,size就返回相应元素数。
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//指定代理的目标对象,这里是局部变量,每调用一次代理的add方法,就会访问InvocationHandler的invoke方法(见下面分析),并创建一个全新的ArrayList对象,所以size方法始终返回0
//ArrayList target = new ArrayList();
long startTime = System.currentTimeMillis();//加入的系统功能
Object retVal = method.invoke(target,args);//在目标对象上执行代理正在执行的那个方法,并将参数也传递给目标的方法,并把目标的方法返回的结果也返回给代理对象
long endTime = System.currentTimeMillis();
System.out.println(method.getName()+"的运行时间为:"+(endTime-startTime));
return retVal;
}
});
proxy3.add("hhh");//调用一次该方法就会打印“add的运行时间为:0”
System.out.println(proxy3.size());//调用一次该方法就会打印“size的运行时间为:0”

分析InvocationHandler对象的运行原理

动态生成的类实现了Collection接口(可以实现若干接口),生成的类有Collection接口中的所有方法和一个如下接受InvocationHandler参数的构造方法。

构造方法接受一个InvocationHandler对象,接受对象了要干什么用呢?该方法内部的代码会是怎样的呢?

$Proxy0 implements Collection
{
InvocationHandler handler;
public $Proxy0(InvocationHandler handler)
{
this.handler = handler;
}
}
实现Collection接口的动态类中的各个方法的代码又是怎样的呢?InvocationHandler接口中定义的invoke方法接受的三个参数又是什么意思?图解说明如下:



$Proxy0 implements Collection
{
InvocationHandler handler;
public $Proxy0(InvocationHandler handler)
{
this.handler = handler;
}
//生成的Collection接口中的方法的运行原理
int size()
{
return handler.invoke(this,this.getClass().getMethod("size"),null);
}
void clear(){
handler.invoke(this,this.getClass().getMethod("clear"),null);
}
boolean add(Object obj){
return handler.invoke(this,this.getClass().getMethod("add"),obj);
}
}

分析先前打印动态类的实例对象时,结果为什么会是null呢?

因为先前的invoke方法返回为null,proxy对象的toString方法就会返回null,所以打印为null。
调用有基本类型返回值的方法时为什么会出现NullPointerException异常?

因为invoke方法返回为null,与有基本类型返回值的方法不匹配,就会报告异常。

分析为什么动态类的实例对象的getClass()方法返回了正确结果呢?

调用代理对象的从Object类继承的hashCode, equals, 或toString这三个方法时,代理对象将调用请求委托转发给InvocationHandler对象,对于其他方法,则不转发调用请求。所以对于getClass方法会返回正确结果。

总结分析动态代理类的设计原理与结构

动态代理的工作原理图



先前的方式都是硬编码:目标类的实例对象以及系统功能代码都是直接写入invoke方法中,现在要将代理的目标类和系统功能都转移为外部对象

怎样将目标类传进去?

直接在InvocationHandler实现类中创建目标类的实例对象,可以看运行效果和加入日志代码,但没有实际意义。
为InvocationHandler实现类注入目标类的实例对象,不能采用匿名内部类的形式了。
让匿名的InvocationHandler实现类访问外面方法中的目标类实例对象的final类型的引用变量。(方法里面的内部类要访问临时局部变量,临时局部变量要加final)

将创建代理的过程改为一种更优雅的方式,eclipse重构出一个getProxy方法绑定接收目标同时返回代理对象,让调用者更懒惰,更方便,调用者甚至不用接触任何代理的API。

将系统功能代码模块化,即将切面代码也改为通过参数形式提供,怎样把要执行的系统功能代码以参数形式提供?

把要执行的代码装到一个对象的某个方法里,然后把这个对象作为参数传递,接收者只要调用这个对象的方法,即等于执行了外界提供的代码!
为绑定方法getProxy增加一个Advice参数。

编写可生成代理和通告的通用方法

将目标类实例对象用final修饰并放在重构的getProxy方法外部
final ArrayList target = new ArrayList();
Collection proxy3 = (Collection)getProxy(target,new MyAdvice());
重构一个getProxy方法绑定接受目标和系统功能Advice的对象,并返回代理对象
//一个更通用的getProxy方法
private static Object getProxy(final Object target, final Advice advice) {
Object proxy3 = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
//参数都通过target获取
target.getClass().getInterfaces(),
new InvocationHandler(){
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
advice.beforeMethod(method);
Object retVal = method.invoke(target,args);
advice.afterMethod(method);
return retVal;
}
});
return proxy3;
}


定义一个Advice接口,定义希望被实现的系统功能,可理解为一个“建议”,所以用advice

public interface Advice {
//这些方法还可以接受三个参数:target,args,method,这里为了简便省去
void beforeMethod(Method method);
void afterMethod(Method method);
}
Advice接口的实现类

public class MyAdvice implements Advice {
long startTime = 0;
public void afterMethod(Method method) {
System.out.println("the program ends");
long endTime = System.currentTimeMillis();
System.out.println(method.getName()+"的运行时间为:"+(endTime-startTime));
}

public void beforeMethod(Method method) {
System.out.println("the program starts");
startTime = System.currentTimeMillis();
}
}

实现类似spring的BeanFactory与可配置的AOP框架

工厂类BeanFactory负责创建目标类或代理类的实例对象,并通过配置文件实现切换。其getBean方法根据参数字符串返回一个相应的实例对象,如果参数字符串在配置文件中对应的类名不是ProxyFactoryBean,则直接返回该类的实例对象,否则,返回该类实例对象的getProxy方法返回的代理对象。

BeanFactory的构造方法接收代表配置文件的输入流对象,配置文件格式如下:

#xxx=java.util.ArrayList
xxx=javaenhance.aopframework.ProxyFactoryBean
xxx.target=java.util.ArrayList
xxx.advice=javaenhance.MyAdvice
#来注释配置文件中的内容
BeanFactory代码如下:

public class BeanFactory {
Properties props = new Properties();
//构造函数用于加载配置文件
public BeanFactory(InputStream ips){
try {
props.load(ips);
ips.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public Object getBean(String name){
String className = props.getProperty(name);
Object bean = null;//try catch块外部还要用到bean,所以将其抽出
try {
Class clazz = Class.forName(className);
bean = clazz.newInstance();//每个JavaBean都有一个不带参数的构造方法,这里调用该构造方法
} catch (Exception e) {
e.printStackTrace();
}
if(bean instanceof ProxyFactoryBean){
Object proxy = null;
ProxyFactoryBean proxyFactoryBean = (ProxyFactoryBean)bean;
try {
Advice advice = (Advice) Class.forName(props.getProperty(name+".advice")).newInstance();//参阅配置文件写法
Object target = Class.forName(props.getProperty(name+".target")).newInstance();
proxyFactoryBean.setAdvice(advice);
proxyFactoryBean.setTarget(target);
proxy = proxyFactoryBean.getProxy();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return proxy;
}
return bean;
}
}


ProxyFacotryBean充当封装生成动态代理的工厂,需要为工厂类提供哪些配置参数信息?目标、通知

ProxyFactoryBean代码如下:

public class ProxyFactoryBean {
private Advice advice;
private Object target;
public Advice getAdvice() {
return advice;
}
public void setAdvice(Advice advice) {
this.advice = advice;
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
public Object getProxy() {
Object proxy3 = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler(){
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
advice.beforeMethod(method);
Object retVal = method.invoke(target,args);
advice.afterMethod(method);
return retVal;
}
});
return proxy3;
}
}


编写客户端应用程序:

编写实现Advice接口的类和在配置文件中进行配置
MyAdvice实现了Advice接口(代码见上)

配置文件中的目标类和代理类可以进行自由切换

客户端应用AopFrameworkTest类,调用了BeanFactory获取对象:

public class AopFrameworkTest {
public static void main(String[] args) {
//加载配置文件
InputStream ips = AopFrameworkTest.class.getResourceAsStream("config.properties");
Object bean = new BeanFactory(ips).getBean("xxx");
System.out.println(bean.getClass().getName());
((Collection) bean).clear();
}
}
运行结果:
$Proxy0

the program starts

the program ends

clear的运行时间为:0
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐