黑马程序员——Java高新技术(4)

来源:互联网 发布:深圳cnc编程培训 编辑:程序博客网 时间:2024/06/06 08:53

---------------------- android培训java培训、期待与您交流! ----------------------

关键字:泛型,类加载器,代理,动态代理


泛型:JDK1.5新特性

 1、泛型入门:泛型目的在于定义安全的泛型类,之前的Object解决了泛型类的部分需求。泛型的出现解决了安全问题。

 没有泛型以前,容器Collection的add方法可以加入任何对象。将对象装入容器后,对象将失去其本来的类型,都是Object,取出的时候要得到元素的实际类型需要进行强转,一不小心就会强转错误,程序员必须细心,记住取回的对象时什么类型,才能正确的从容器中取回对象并加以实现。使用泛型能很好的解决这一问题。使用泛型所定义的类,在声明和配制对象时可以使用尖括号一并指定泛型类类型持有者T的真正类型,使用泛型后,不需要进行类型转换。

  使用泛型设计的类,声明及配制对象时不指定类型,会默认使用Object类型,同时编译器会给出警告。

 容器类使用泛型设计,一个容器中只能存储指定的数据类型的对象,这样更安全。

 2、泛型的内部原理及有应用

  泛型是提供给编译器使用的,可以限定集合中存放对象的类型,让编译器挡住不符合类型要求的对象。编译器编译带泛型配置的集合时会除掉类型信息。使程序运行的效率不受影响。由于参数化的泛型类型,getClass()的方法返回值类型和原始类型完全一样,由于编译生成的字节码会去掉泛型类型的信息,只要能够跳过编译器,就可以往某个泛型集合中加入其他的类型数据。例如,用反射得到集合,再调用其add方法

 

ArrayList<Integer> arrayList = new ArrayList<Integer>();  arrayList.add(1);    ArrayList<String> arrl2 = new ArrayList<String>();    //打印两个不同类型的容器的class是不是同一个  System.out.println(arrayList.getClass() == arrl2.getClass());  //绕过编译器用反射机制调用add方法向Integer类型的容器中加入String类型的元素  arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, "abc");    System.out.println(arrayList.get(0));

 

结果将打印abc。
(1)ArrayList<E>类定义和ArrayList<Integer>类应用中涉及如下术语:
整个称为ArrayList<E>泛型类型。ArrayList<E>中的E称为类型变量或类型参数。   
整个ArrayList<Integer>称为参数化的类型。 ArrayList<Integer>中的Integer称为类型参数的实例或实际类型参数
ArrayList<Integer>中的<>念着typeof     ArrayList称为原始类型
(2)参数化类型与原始类型的兼容性:
 参数化类型可以引用一个原始类型的对象。编译器报告警告。
 原始类型可以应用一个参数化类型的对象。编译器报告警告。
(3)参数化类型不考虑类型参数的继承关系。
Vector<String> v = new Vector<Object>//错误
Vector<Object> v = new Vector<String>//也是错误的
  在创建数组实例时,数组的元素不能使用参数化类型
  Vector<Integer> vecList[] = new Vector<Integer>[10];3、泛型中通配符的扩展应用
  使用通配符可以应用其他各种参数化的类型。通配符定义变量主要作引用。可以调用与参数化无关的方法,不能调用与参数化有关的方法。
  通配符的扩展:
  (1)限定通配符的上边界:?extends E 表示参数化的类型为E或E的之类
   (2)限定通配符的下边界: ?super E  表示参数化类型为E或E的父类
4、自定义泛型方法
  (1)用于放置泛型的参数化类型的尖括号放在方法的其他所有的修饰符之后好方法返回类型之前。也就是紧邻返回值之前。参数化类型一般用单个大写字母表示。
  (2)只有引用类型才作为方法的实际参数。

     (3) 除了在应用泛型时可以使用extends限定符,在定义泛型时也可以使用extnds限定符,例如:

        Class.getAnnotation方法的定义。并且可以使用&来指定多个边界如<V extends Serializable & cloneable> void method(){}

    (4)普通方法构造方法和静态方法中都可以使用泛型。编译器也不允许创建类型变量的数组。

    (5)也可以用类型变量表示异常,称为参数化异常。可以用于方法的throws列表中,但是不能应用于ctch字句中。

    (6)在泛型中可以同时又多个类型参数,在定义它们额尖括号中用逗号分开

         例如: publc static <K,V>  V getValue(K key){return map.get(key)}

5、类型参数的类型推断

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

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

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

   (2)当某个类型变量在整个参数列表中的所有参数和返回值的多处被应用了。如果调用方法时这多处的实际应用类型都是同一种类型。这很容易推断。

   (3)当某个类型变量在整个参数列表中的所有参数和返回值的多处被应用了。如果调用方法时这多处的实际应用类型对应到了不同的类型,并没有返回值。这时编译器取多个参数中最小交集类型,如:staic<T> void fill(T[] a, T t)  fill(new Intger[],3.5)编译没问题,运行时出错

  (4)当某个类型变量在整个参数列表中的所有参数和返回值的多处被应用了。如果调用方法时这多处的实际应用类型对应到了不同的类型,并且使用返回值,这时候优先考虑返回值的类型,例如,static<T> T add(T a, T b),此时只要返回值类型是 参数列表中两个值得父类就可以了。类型变量取值为两个参数的父类的最小范围。

 (5)参数类型推断具有传递性。当类型变量在整个参数列表中的对象类型,而不是应用在<>中时,两个参数列表的T的实际类型不同编译器不会报错,运行时报错。如果用在<>中,且方法调用时即用到了T对象也用到了<T>则编译器会报错

    copy(Integer[],String[])---->Static <T> void copy(T[] a,T[] b)编译器不报错。运行时报错

   copy(Collection<String>  , new Integer[])----->static <T> void copy(Collection<T> c, T[] b)
6、自定义泛型类

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

public class GenericDao<T>{

       private T field1;

       public void save(T t){}。。。。

}

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

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

3、注意

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

   当一个变量被声明为泛型时,只能被实例化变量和方法调用。还有内嵌类型,而不能被静态变量和静态方法调用。因为静态成员是被所有参数化的类所共享的,所以静态成员不应有类级别的类型参数。

三、类加载器

1、java中类的加载

Java在真正需要使用一个类时才会加载,而不是在程序启动时就加载所有的类。

Class的信息是在编译时期就被加入到.class文件中,这是Java支持运行时期类型类型辨识的一种方式:在编译时期,编译器会先检查对应的.class文件,而运行时期JVM在使用某个类时,会先检查对应的Class对象是否已经加载。如果没有加载则寻找对应的.class文件并载入。一个类在JVM中只会有一个Class实例,每个类的实例都会记得自己是由哪个Class实例所生成。

Class类中有静态方法forName()可以实现动态加载类,forName有两个版本。一个是参数(类名称),另一个参数列表(类名称,加载类时是否运行静态区块,指定类加载器)

2、类加载器:java类的加载是由类加载器来完成的。当在命令行下执行java XXX指令后,java运行程序会尝试找到JRE目录,然后寻找jvm.dll(默认在bin\client目录中),启动JVM,并进行初始化动作,调用Bootstrap Loader,Bootstrap Loader加载Extended Loader并设置Extended Loader的parent为Bootstrap Loader,Bootstrap Loader会加载System Loader,并将System Loader的parent设置为Extended Loader。

     Bootstrap Loader通常由C++编写的二进制代码。Extended Loader由java编写而成,实际上对应于sun.misc.Launcher$ExtClassLoader,System Loader由java编写而成,对应于sun.misc.Launcher$AppClassLoader

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


System.out.println(ClassLoaderTest.class.getClassLoader().getClass()    .getName());//sun.misc.Launcher$AppClassLoader类加载器    System.out.println(System.class.getClassLoader());//返回null,说明是Bootstrap Loader

ClassLoader loader = ClassLoaderTest.class.getClassLoader();    while(loader != null)  {   System.out.println(loader.getClass().getName());   loader = loader.getParent();  }

 

打印结果为

sun.misc.Launcher$AppClassLoadernullsun.misc.Launcher$AppClassLoadersun.misc.Launcher$ExtClassLoader

从以上代码的结果知道,类加载器的父子关系。

BootStrap----->JRE/lib/rt.jar
ExtClassLoader----->JRE/lib/ext/*.jar
AppClassLoader------->ClassPath指定的所有jar或所在目录下的class文件。
3、类加载器的委托机制:
当java虚拟机要加载一个类时,到底派出哪个类加载器去加载?
(1)首先当前线程的类加载器去加载线程中的第一个类。
(2)如果类A中引入了类B,java虚拟机将使用加载类A的加载器来加载类B.
(3)还可以直接调用ClassLoader.loadClass(String name)方法指定某个类加载器去加载某个类。
每个类加载器加载类时,又先委托给上级类加载器。
 当所有的父类加载器没有加载到类,回到发起者类加载器,还加载不到,则抛出ClassNotFoundException,不是再去找发起者类加载器的儿子。因为没有getChild方法,即使有,那有很多个子类,找哪一个也不确定。
 知识补充:Thread中setContextClassLoader()为当前线程设置上下文类加载器,getContextClassLoader() 获取该线程的上下文类加载器 Thread.currentThread()获取当前线程。
 面试题:能不能自己写个类叫java.lang.System。类加载采用委托机制,这样可以保证让父类加载器优先,总是使用jre中的Sysem类,这样总是使用java系统提供的System,自己写的类没有任何意义。如果想要有意义,需要指定自己的类加载器。当要调用方法的时候还是采用就近原则,如果想用系统提供的System类的一些方法,如果自己写的System类中没有定义,编译器将报错。

4、自定义类加载器的编写原理

   (1)自定义的类加载器必须继承ClassLoader,loadClass(String name)方法与findClass方法,defineClass方法

            自定义的加载类使用的时候先调用父类加载器,如果父类加载器管辖的范围内没有找到要加载的类文件,则会执行findClass方法,findClass方法返回一个Class对象,返回的Class对象可以用defineClass方法,将某个类的字节码文件读取流写入到ByteArray输出流中,将字节码对象转换成Byte数组。这样可以写自己的加密类

5、类加载器的高级问题实验分析:

    JVM在编译某个类A的时候,会采用类加载的委托机制,调用父类加载器,如果父类加载器中有类A的class文件,则使用父类加载器加载,如果A中使用了其他类B,如果父类加载器的加载目录下找不到类B,则会出现错误。

6、代理类的作用及原理以及AOP概念

程序中的代理:

要为已知存在的多个具有相同接口的目标类的各个方法增加一些系统功能,例如异常处理、日志、计算方法的运行时间,事务管理等。

编写一个与目标类具有相同接口的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法时加上系统功能的代码。

如果采用工厂模式和配置文件的方式进行管理,则不需要修改客户端的程序。在配置文件中配置是使用目标类还是代理类,这样以后很容易切换。譬如,想要日志功能时就配置代理类,否则配置目标类,这样增加系统功能很容易。以后运行一段时间后,又想去掉系统功能很容易

7、动态代理技术与AOP

(1)系统中存在交叉业务,一个交叉业务就是要切入到系统中的一个方面。

          如:安全,事务,日志等要贯穿到好多个模块中,所以就是交叉业务

                               安全            事务           日志

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

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

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

     method1        method2       method3      

    {                     {                    {

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

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

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

     }                     }                    }

AOP交叉业务编程又为面向方面(切面)编程(Aspect Oriented Program)

目标是使交叉业务模块化,可以采用将切面代码移动到原始方法的周围,这与直接在方法中编写切面代码的运行那个效果一样。

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

   method1        method2       method3      

    {                     {                    {

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

     }                     }                    }

    -----------------------------------------------------切面
使用代理技术正好解决这种问题。

 

(2)要为系统中的各种接口和类增加代理功能,那将需要太多的代理类,全部用静态代理方式将是一件非常麻烦的事情,

JVM可以在运行期间动态生成出类的字节码,这种动态生成的类往往被用作代理类。即动态代理类。

JVM生成的动态类必须实现一个或多个接口。所以JVM生成的动态类只能用作具有相同接口的U币啊哦类的代理。

CGLIB库可以动态生成一个类的子类,一个类的子类也可以做该类的代理。所以如果要为一个没有实现接口的类生成动态代理类,那么可以使用CGLIB库。

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

  1)在调用目标方法之前

  2)在调用目标方法之后

  3)在调用目标方法前后

  4)在处理目标方法异常的Catch块中

8、创建动态类:

  用反射机制获取构造方法,编写一个最简单的InvocationHandler类。

  调用构造方法创建动态类的实例对象,并编写InvocationHandler类的实例对象传进去。打印创建对象和调用对象没有返回值的方法和getClass方法,演示调用其他又返回值得方法报告异常,也可以通过匿名内部类创建InvocationHandler实例对象。

代码如下:

 

//获取Collection对象的动态代理类

Class clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);

//创建实例对象(创建Collection的代理)  Constructor constructor  = clazzProxy1.getConstructor(InvocationHandler.class);

//参数通过创建匿名内部类

constructor.newInstance(new InvocationHandle()

{

//实现匿名类的方法

public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {     return null;    }

});

  总结:让JVM创建动态类及其实例对象,用Proxy.newProxyInstance(类加载器,实现那些接口,InvocationHandler对象)以下三个参数

    生成的类中有哪些方法,通过让其实现哪些接口的方式进行告知。

    产生的类字节码必须有一个关联的类加载器对象。

    生成的类中的方法的代码是怎样的也得由我们提供。把我们的代码写在一个约定好了接口对象的方法中。把对象传给它,它调用我的方法,相当于插入了我的代码。提供执行代码的对象就是那个InvocationHandler对象,它是创建动态类的实例对象的构造方法时传递进去的。在上面的InvocationHandler对象的invoke方法中加一点代码,就可以看到这些代码被调用运行了。

    代码如下

 

接口:定义要实现的方法:

public interface Advice { public void beforeMethod(Method method); public void afterMethod(Method method);}

定义一个实现生活中方法的类:

public class MyAdvice implements Advice { long endTime ; long beginTime ; @Override public void beforeMethod(Method method) {  // TODO Auto-generated method stub  System.out.println("上课");  beginTime = System.currentTimeMillis(); }

 @Override public void afterMethod(Method method) {  // TODO Auto-generated method stub    endTime = System.currentTimeMillis();  System.out.println(method.getName()+":"+(endTime - beginTime));  System.out.println("毕业"); }

}

public static Object getProxy(final Object target,final Advice advice)

{

Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(),

target.getClass().getInterfaces(),

new InvocationHandler(){

public Object invoke(Object proxy,Method method,Object[] args)

{

advice.before();

Object retVal = method.invoke(target,args);

advice.after();

return retVal;

}

});

return proxy;

}

 
动态生成的类,实现了Collection接口,实现什么接口由第二个参数指定,也可以是多个,生成的类有Collection接口中的所有方法和一个接受InvocatioHandler对象为参数的构造方法,构造方法接收一个InvocationHandler对象,说明动态生成的类中有一个InvocationHandler类型的成员变量。InvocationHandler是代理实例的调用处理程序 实现的接口。 
每个代理实例都具有一个关联的调用处理程序。对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序的 invoke方法。
 invoke(Object proxy, Method method, Object[] args) 在代理实例上处理方法调用并返回结果。
调用代理对象的方法会执行代理对象的成员属性InvocationHandler对象的invoke方法:
参数三个(调用的代理对象,调用对象的哪个方法,调用方法的参数)
 

//生成的Collection接口的方法的运行原理:

执行到动态类对象.方法的时候,就会调用传入的InvocationHandler对象的invoke方法

比如,生成动态类中的方法

int size()

{

return handler.invoke(this,this.getClass().getMthod("size"),null);

}

boolean add(Object o)

{

return hanler.invoke(this,this.getClass().getMthod("add"),o);

}

在接口中重复的方法详见API
9、实现类似Sprig的可配置的AOP框架
  有一个配置文中记录着是使用代理还是使用目标类
 1) 工厂类BeanFactory负责创建目标类或代理类的实例对象,并通过配置文件实现代理类和目标类的切换。其中getBean方法根据参数字符串返回一个相应的对象。参数字符串是配置在文件中对应的类名,而不是ProxyFactoryBean,则直接返回该类的实例,否则返回该类实例对象的getProxy方法返回的对象
  BanFactory的构造方法接受代表配置文件的输入流对象。
 2)ProxyFactoryBean充当封装成动态代理的工厂,需要为工厂提供两个属性,要代理的目标类,和通知Advice
 3)编写客户端应用,调用BeanFactory获取对象。编写通知接口的类,和实现通知接口的通知对象。将对象赋值给动态代理工厂。
   还要新建相关的配置文件,以便工厂类读取配置。
---------------------- android培训java培训、期待与您交流! ----------------------