《深入理解Java7:核心技术与最佳实践》读书笔记(二)

这一篇为《深入理解Java7:核心技术与最佳实践》的第二篇读书笔记,主要记录的是Java语言的动态性。我们知道Java本身是一种静态类型的高级编程语言,即其源代码在编译时需要进行类型检查。我们这里所说的动态性不是指类型上的,而是使用方式上的动态。为了对Java语言的动态性有一个全面的了解,此处所涉及的内容不仅有Java7的新特性,还包括了之前版本就有的特性,如对脚本语言支持的API、反射API和动态代理等。

脚本语言支持API

  • JavaScript、JRuby和Groovy等许多流行脚本语言已经尅一在Java虚拟机上运行,他们Java语言的交互是通过Java提供的脚本语言API进行的,同时通过此API,Java程序和脚本之间可以进行双向的方法调用和数据传递。脚本语言API在Java标准API中是javax.script包,在Java 6引入。;

  • 一段脚本的执行需要由此脚本语言对应的脚本引擎来完成,而Java中已经定义了脚本引擎的注册和查找机制。一般地,可以通过以下方式获得脚本引擎:首先创建一个脚本引擎管理器javax.script.ScriptEngineManager对象,再通过管理器查找所需的脚本引擎。查找脚本管理引擎的方式有如下3种,以获取JavaScript引擎为例:

    • javax.script.ScriptEngineManager.getEngineByName("JavaScript"),通过引擎名称获取;
    • javax.script.ScriptEngineManager.getEngineByExtension("js"),通过文件扩展名获取;
    • javax.script.ScriptEngineManager.getEngineByMimeType("text/javascript"),通过MIME类型获取。
  • Java程序和脚本之间的数据双向传递是通过语言绑定对象实现的,所谓语言绑定对象就是一个简单的哈希表,数据为这个哈希表的条目,一个简单的名值对。下面的代码实现了Java程序和脚本语言的双向数据传递:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void useDefaultBinding() {
    ScriptEngine jsEngine = getJsEngine();
    jsEngine.put("name", "Jay Chou");
    try {
    jsEngine.eval("var message = 'Hello,'+name;");
    jsEngine.eval("println('jsEngine: '+message)");
    Object obj = jsEngine.get("message");
    System.out.println("Java lang: " + obj);
    } catch (ScriptException e) {
    e.printStackTrace();
    }
    }
  • 实现了javax.script.Invocable接口的脚本引擎可以调用脚本的顶层方法(使用InvocableinvokeFunction),也可以调用对象中的成员方法(使用InvocableinvokeMethod),下面的代码片段展示了Java程序和脚本之间双向方法调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // getGreeting是对象obj的成员方法,在Java程序中调用时需把这个对象作为参数传入
    public void invokeMethod() throws ScriptException, NoSuchMethodException{
    ScriptEngine jsEngine = getJsEngine();
    String scriptText = "var obj = {getGreeting : function(name){return 'Hello,' + name;}};";
    jsEngine.eval(scriptText);
    Invocable invocable = (Invocable)jsEngine;
    Object scope = jsEngine.get("obj");
    Object result = invocable.invokeMethod(scope, "getGreeting", "Jay Chou");
    System.out.println(result);
    }

    在有些脚本引擎中,可以在Java程序中定义接口,而在脚本中实现接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // getGreeting为Java定义的接口Greet中的一个方法,其实现由脚本来完成
    public void userInterface() throws ScriptException{
    ScriptEngine jsEngine = getJsEngine();
    String scriptText = "function getGreeting(name){ return 'Hello,' + name;}";
    jsEngine.eval(scriptText);
    Invocable invocable = (Invocable)jsEngine;
    Greet gteet = invocable.getInterface(Greet.class);
    System.out.println(gteet.getGreeting("Jay Chou"));
    }

运行时动态特性之反射

  • 反射API的重要使用场景之一就是要调用的方法或要操作的域的名称按照某种规律变化的时候;

  • 对于嵌套类构造方法的获取,需要区分静态和非静态。静态的操作与一般的无异,而非静态的嵌套类对象由于持有一个指向包含它的外部类对象的引用,所以在获取其构造方法时,参数列表第一个参数类型必须为外部类的Class对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class ConstructorUsage {
    static class StaticNestedClass {
    public StaticNestedClass(String name) {
    //do something
    }
    }
    class NestedClass {
    public NestedClass(int age) {
    //do something
    }
    }
    public void useNestedClassConstructor() throws Exception {
    Constructor<StaticNestedClass> sncc =
    StaticNestedClass.class.getDeclaredConstructor(String.class);
    sncc.newInstance("Jay Chou");
    //非静态嵌套类的构造方法获取
    Constructor<NestedClass> nc =
    NestedClass.class.getDeclaredConstructor(ConstructorUsage.class, int.class);
    NestedClass ncIns = nc.newInstance(this, 5);
    }
    }
  • 反射API可以获取到类中公开的静态域和实例域,私有的无法获取也无法操作。体现在代码中,使用静态域时不需要提供具体的实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // count和name分别为类FieldContainer中声明的静态域和实例域
    public void useField() throws Exception{
    Field fieldCount = FieldContainer.class.getDeclaredFeild("count");
    fieldCount.set(null, 3);
    Field fieldName = FieldContainer.class.getDeclaredFeild("name");
    FieldContainer fieldContainer = new FieldContainer();
    fieldName.set(fieldContainer, "Jay Chou");
    }
  • 反射API不仅可以获取类中的公共方法,也可以获得私有的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public void useMethod(){
    MethodContainer mc = new MethodContainer();
    Method publicMethod = MethodContainer.class.getDeclaredMethod("publicMethod");
    publicMethod.invoke(mc);
    Method privateMethod = MethodContainer.class.getDeclaredMethod("privateMethod");
    // 设置访问权限,使得私有方法也可以被访问到
    privateMethod.setAccessible(true);
    privateMethod.invoke(mc);
    }

    此例中,通过反射API绕过了Java中默认的访问控制权限,ConstructorFieldMethod都继承自java.lang.reflect.AccessibleObject,其中的方法setAccessible可以用来设置是否绕开默认权限检查;

  • 最佳实践:在使用invoke调用方法时,如果方法本身抛出异常,invoke方法会抛出InvocationTargetException,当捕获了这个异常即通过方法getCause来获得真正的异常信息,方便调试;值得一提的是,Java 7为所有与反射操作有关的异常添加了一个新的父类java.lang.ReflaciveOperationException,在处理与反射有关的异常时可以直接捕获这个异常,而在Java 7之前需要分别捕获。

运行时动态特性之动态代理

  • 动态代理只支持对接口的代理,所以使用动态代理需要两个元素即可: 第一个是被代理的接口,第二个是处理接口方法调用的java.lang.reflect.InvocationHandler接口。举个栗子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class UseProxy {
    // 处理接口方法调用的invocationHandler
    static class PrintInvocationHandler implements InvocationHandler{
    private Object targetObj;
    public PrintInvocationHandler(Object targetObj) {
    this.targetObj = targetObj;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
    System.out.println(">>>>> 调用方法" + method.getName());
    Object result = method.invoke(targetObj, args);
    return "++++++ " + result + " ++++++";
    }
    }
    public static void main(String[] args) {
    // 原始的方法调用接收者对象,方便在invocationHandler中执行原始调用
    DoSomeServiceImpl doSomeServiceImpl = new DoSomeServiceImpl();
    /**
    * 代理类实现了指定的接口,并通过invocationHandler对目标类中的接口实现方法进行了增强
    */
    DoSomeService proxyInstance =
    (DoSomeService)Proxy.newProxyInstance(DoSomeServiceImpl.class.getClassLoader(),
    new Class[]{DoSomeService.class}, new PrintInvocationHandler(doSomeServiceImpl));
    String res = proxyInstance.doSomeThing("Python");
    /**
    * >>>>> 调用方法doSomeThing
    * ++++++ Python, Hello World! ++++++
    */
    System.out.println(res);
    }
    }
  • 如果被代理的接口不是公开的,那么被代理的接口需要和实现动态代理的代码在同一个包中;

  • 最佳实践:当代理类实现多个接口时,在创建代理类时接口排列顺序尤为重要。即对于相同的一组接口,不同的接口排列顺序所产生的代理类也是不同的。相同排列的接口产生的代理类只会被创建一次,并且创建一次会被缓存起来。如果多个接口中都存在声明类型相同的方法,那么排列顺序中最先出现的接口方法会被选择。

对动态语言的支持

  • Java 7中的动态语言支持主要是在虚拟机规范的层次上修改的,如新增字节码指令invokedynamic。但是在 代码和API层次上也新增了标准库APIjava.lang.invoke包;

  • 方法句柄是Java中对方法、构造方法和域的一种可执行应用,通过方法句柄可以动态指定方法的接收者 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import java.lang.invoke.MethodHandle;
    import java.lang.invoke.MethodType;
    import static java.lang.invoke.MethodHandles.lookup;
    public class MethodHandleTest {
    static class ClassA {
    public void println(String s) {
    System.out.println(s);
    }
    }
    public static void main(String[] args) throws Throwable {
    Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
    // 无论obj最终是哪个实现类,下面这句都能正确调用到println方法。
    getPrintlnMH(obj).invokeExact("Hello Java!");
    }
    private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
    // MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数)。
    MethodType mt = MethodType.methodType(void.class, String.class);
    // lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。
    /**
    * 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,
    * 代表该方法的接收者,也即是this指向的对象,这个参数以前是放在参数列表中进行传递, 现在提供了bindTo()方法来完成这件事情。
    */
    return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
    }
    }

    方法getPrintlnMH()中实际上是模拟了invokevirtual指令的执行过程,只不过它的分派逻辑并非固化在Class文件的字节码上的,而是通过一个具体方法来实现。而这个方法本身的返回值(MethodHandle对象),可以视为对最终调用方法的一个“引用”。


参考资料