20145312 《Java程序设计》第十周学习总结 20145312 《Java程序设计》第十周学习总结

学习笔记

Chapter 17反射与类加载器

17.1 运用反射

.class文档反应了类基本信息,因而从Class等API取得类信息的方式就称为反射。

17.1.1 Class与.class文档
  1. java.lang.Class的实例代表Java应用程序运行时加载的.class文档。
  2. 可以通过Object的getClass()方法,或者是通过.class常量取得每个对象接口。
  3. 在取得Class对象后,就可以操作Class对象的公开方法,取得基本信息。
代码如下:
package Reflection;

import static java.lang.System.out;
public class ClassInfo {
    public static void main(String[] args){
        Class clz=String.class;
        out.println("类名称:"+clz.getName());
        out.println("是否为接口:"+clz.isInterface());
        out.println("是否为基本类型:"+clz.isPrimitive());
        out.println("是否为数组对象:"+clz.isArray());
        out.println("父类名称:"+clz.getSuperclass());
    }
}
结果如下:
类名称:java.lang.String
是否为接口:false
是否为基本类型:false
是否为数组对象:false
父类名称:class java.lang.Object
  1. 使用类声明参考名称并不会加载.class文档,可设计测试类来验证。
  2. Some类定义了static区块,默认首次加载.class文档时会执行静态区块。通过文本模式下显示类型,了解何时加载.class文档.
代码如下:
package Reflection;

import static java.lang.System.out;
public class SomeDemo {
    public static void main(String[] args){
        Some s;
        out.println("声明Some参考名称");
        s=new Some();
        out.println("生成Some实例");
    }
}
结果如下:
声明Some参考名称
载入Some.class文档
生成Some实例
17.1.2 使用Class.forName()

使用Class.forName()方法实现动态加载类,可用字符串指定类名称来获得类相关信息。

代码如下:
package Reflection;

import static java.lang.System.out;
public class InfoAbout {
    public static void main(String[] args){
        try{
            Class clz=Class.forName(args[0]);
            out.println("类名称:"+clz.getName());
            out.println("是否为接口:"+clz.isInterface());
            out.println("是否为基本类型:"+clz.isPrimitive());
            out.println("是否为数组对象:"+clz.isArray());
            out.println("父类名称:"+clz.getSuperclass());
        }catch (ArrayIndexOutOfBoundsException e){
            out.println("没有指定类名称");
        }catch (ClassNotFoundException e){
            out.println("找不到指定的类"+args[0]);
        }
    }
}

或者可以使用forName()第二个版本,将initialize设定为false,这样会在建立类实例时执行static区块。

代码如下:
package Reflection;

import static java.lang.System.out;
class Some2{
    static {
        System.out.println("[执行静态区块]");
    }
}
public class SomeDemo2 {
    public static void main(String[] args) throws ClassNotFoundException{
        Class clz=Class.forName("Reflection.Some2",false,SomeDemo2.class.getClassLoader());
        out.println("已载入Some2.class");
        Some s;
        out.println("声明Some参考名称");
        s=new Some();
        out.println("生成Some实例");
    }
}
结果如下:
已载入Some2.class
声明Some参考名称
[执行静态区块]
生成Some实例
17.1.3 从Class获得信息

取得Class对象后,就可以取得.class文档中记载的信息:

包对应类型是:java.lang.Package
构造函数对应类型是:java.lang.reflect.Constructor
方法成员对应类型是:java.lang.reflect.Method
数据成员对应类型是:java.lang.reflect.Field

例如取得指定String类的包名称:

Package p=String.class.getPackage();
System.out.println(p.getName());     //显示java.lang
17.1.4 从Class建立对象

如果不知道类名称,就利用Class.forName()动态加载.class文档,取得Class对象后,利用newInstance()方法建立实例,如:

Class clz=Class.forName(args[0]);
Object obj=clz.newInstance();

想采用影片链接库来播放动画,利用接口定义出影片链接库该有的功能。

代码如下:
package Reflection;

import java.util.Scanner;
public class MediaMaster {
    public static void main(String[] args) throws ClassNotFoundException,InstantiationException,IllegalAccessException{
        String playerImp1=System.getProperty("Reflection.playerImp1");
        Player player=(Player) Class.forName(playerImp1).newInstance();
        System.out.println("输入想播放的影片:");
        player.play(new Scanner(System.in).nextLine());
    }
}
package Reflection;

public class ConsolePlayer implements Player {
    @Override
    public void play(String video){
        System.out.println("正在播放"+video);
    }
}

指定-DReflection.PlayerImp1=Reflection.ConsolePlayer。

执行结果如下:
输入想播放的影片:Harry Potter
正在播放:Harry Potter
17.1.5 操作对象方法与成员
  1. 使用 invoke()方法来动态调用指定的方法。
代码如下:
package Reflection;

public class Student {
    private String name;
    private Integer score;
    public Student(){}
    public Student(String name,Integer score){
        this.name=name;
        this.score=score;
    }
    public void setName(String name){
        this.name=name;
    }
    public String getName(){
        return name;
    }
    public void setScore(Integer score){
        this.score=score;
    }
    public Integer getScore(){
        return score;
    }
}
17.1.6 动态代理
静态代理

在静态代理实现中,代理对象与被代理对象必须实现统一接口,在代理对象中可以实现日志服务。如下可定义一个Hello接口:

package Reflection;

public interface Hello {
    void hello(String name);
}
如果有个HelloSpeaker类操作了Hello接口:
package Reflection;

public class HelloSpeaker implements Hello{
    public void hello(String name){
        System.out.printf("哈啰,%s%n",name);
    }
}

在HelloSpeaker类中没有日志程序代码,日志程序代码会放在代理对象中,代理对象同样也要实现Hello接口。

代码如下:
package Reflection;

import java.util.logging.*;
import java.util.logging.Logger;
public class HelloProxy implements Hello {
    private Hello helloObj;

    public HelloProxy(Hello helloObj){
        this.helloObj=helloObj;
    }
    public void hello(String name){
        log("hello方法开始...");//日志服务
        helloObj.hello(name);    //执行商业规则
        log("hello方法结束...");//日志服务
    }
    private void log(String msg){
       // Logger logger=Logger.getLogger(HelloProxy.class.getName()).log(Level.INFO,msg);
    }
}

静态代理必须为个别接口操作个别代理类,多的代理接口定义多个代理对象,操作和维护代理对象会有不少的负担。

动态代理
  1. 反射API中提供动态代理相关类,该机制可使一个处理者代理多个接口的操作对象。
  2. 处理者类必须操作java.lang.reflect.InvocationHandler接口。
代码如下:
 package Reflection;

import java.lang.reflect.*;
import java.util.logging.*;
public class LoggingHandler implements InvocationHandler {
    private Object target;

    private Object bind(Object target){
        this.target=target;
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                this
        );
    }
    public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
        Object result=null;
        try{
            log(String.format("%s()呼叫开始...",method.getName()));
            result=method.invoke(target,args);
            log(String.format("%s()呼叫结束...",method.getName()));
        }catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e){
            log(e.toString());
        }
        return result;
    }
    private void log(String message){
        Logger.getLogger(LoggingHandler.class.getName()).log(Level.INFO,message);
    }
}

接下来可使用LoggingHandler的blind()方法来绑定被代理对象:

package Reflection;

public class ProxyDemo {
    public static void main(String[] args){
        LoggingHandler loggingHandler=new LoggingHandler();
        Hello helloProxy=(Hello) loggingHandler.blind(new HelloSpeaker());
        helloProxy.hello("Justin");
    }
}

17.2了解类加载器

17.2.1 类加载器级层架构
  1. 加载器的分类及作用:
Bootstrap Loader:产生Exrtended Loader和System Loader
Exrtended Loader:父加载器为Bootstrap Loader
System Loader:父加载器为Exrtended Loader
  1. Bootstrap Loader通常由C撰写而成。若是Oracle的JDK,Bootstrap Loader会搜索系统参数sun.boot.class.path中指定位置的类,默认是JRE目录的classes中的.class文档,或lib目录中.jar文档里的类。

  2. Exrtended Loader由Java撰写而成,会搜索系统参数java.ext.dirs中指定位置的类,默认是JRE目录libextclasses中的.class文档。

  3. System Loader由Java撰写而成,会搜索系统参数java.class.path中指定位置的类,也就是CLASSPATH路径,默认是当前工作路径下的.class文档。

  4. 加载类是会以Bootstrap Loader->Exrtended Loader->System Loader的顺序寻找类,如果所有类加载器都找不到指定类,就会抛出java.lang.NoClassDefFoundError

  5. 对null调用getParent()方法会抛出NullPointedException异常。

  6. ClassLoader可以使用loadClass()方法加载类,使用localClass方法加载类时,默认不会执行静态区块,真正使用类建立实例时才会执行静态区块。

  7. 关系代码如下:

package Reflection;

import static java.lang.System.out;
public class ClassLoaderHierarchy {
    public static void main(String[] args){
        Some some=new Some();
        Class clz=some.getClass();
        ClassLoader loader=clz.getClassLoader();
        out.println(loader);
        out.println(loader.getParent());
        out.println(loader.getParent().getParent());
    }
}
结果如下:
载入Some.class文档
sun.misc.Launcher$AppClassLoader@1b84c92
sun.misc.Launcher$ExtClassLoader@140e19d
null
17.2.2 建立ClassLoader实例
  1. 使用URLClassLoader来产生新的类加载器,需要java.net.URL作为其参数来指定类加载的搜索路径。使用URLClassLoader的loadClass()方法加载指定类时,会先委托父加载器代为搜索。
  2. 由同一类加载器载入的.class文档,只会有一个Class实例。如果同一.class文档由两个不同的类加载器载入,则会有两份不同的Class实例。
代码如下:
package Reflection;

import static java.lang.System.out;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassLoaderDemo {
    public static void main(String[] args){
        try {
            String path=args[0];
            String clzName=args[1];

            Class clz1=loadClassFrom(path,clzName);
            out.println(clz1);
            Class clz2=loadClassFrom(path,clzName);
            out.println(clz2);
            out.printf("clz1与clz2为%s实例",clz1==clz2 ? "相同":"不同");
        }catch (ArrayIndexOutOfBoundsException e){
            out.println("没有指定类加载路径与名称");
        }catch (MalformedURLException e){
            out.println("加载路径错误");
        }catch (ClassNotFoundException e){
            out.println("找不到指定的类");
        }
    }
    private static Class loadClassFrom(String path,String clzName)throws ClassNotFoundException,MalformedURLException{
        ClassLoader loader=new URLClassLoader(new URL[] {new URL(path)});
        return loader.loadClass(clzName);
    }
}

path可以输入不在System Loader以上层级类加载器搜索路径的其他路径。

Chapter 18 自定义泛型、枚举与注释

18.1 自定义泛型

泛型定义:
仅定义在方法上的泛型语法 
用来限制泛型可用类型的extends与super关键字
?类型通配字符的使用
18.1.1 使用extends与?
  1. 若extends之后指定了类与接口,想再指定其他接口,可以使用&连接。
代码如下:
package Generics;

import java.util.Arrays;
public class Sort {
    public static <T extends Comparable<T>> T[] sorted(T[] array){
        T[] arr=Arrays.copyOf(array,array.length);
        sort(arr,0,arr.length-1);
        return arr;
    }
    private static void sort(Object[] array,int left,int right){
        if(left<right){
            int q=partition(array,left,right);
            sort(array,left,q-1);
            sort(array,q+1,right);
        }
    }
    private static int partition(Object[] array,int left,int right){
        int i=left-1;
        for(int j=left;j<right;j++){
            if(((Comparable) array[j]).compareTo(array[right])<=0){
                i++;
                swap(array,i,j);
            }
        }
        swap(array,i+1,right);
        return i+1;
    }
    private static void swap(Object[] array,int i,int j){
        Object t=array[i];
        array[i]=array[j];
        array[j]=t;
    }
}
package Generics;

public class SortDemo {
    public static void main(String[] args){
        String[] strs={"3","2","5","1"};
        for(String s:Sort.sorted(strs)){
            System.out.println(s);
        }
    }
}

结果如下:

1
2
3
5
  1. 如果B是A的子类,而Node< B>可视为一种Node< A>,则称Node具有共变性或有弹性的。Java泛型不具有共变性,可以使用类型通配字符?与extends来声明变量,使其达到类似的共变性。
  2. 若声明?不搭配extends,则默认为? extends Object。
    Node<?> node = null;//相当于Node<? extends Object>
  3. java的泛型语法在执行时期实际上只会知道是Object类型,由于无法在执行时期获得类型信息,编译程序只能就编译时期看到的类型来做检查,因而造成以上谈及的限制。
18.1.2 使用super与?
  1. 如果B是A的子类,而Node< A>可视为一种Node< B>,则称为Node具有逆变性。Java泛型不具有逆变性,可以使用类型通配字符?与super来声明,使其达到类似的逆变性的效果。
  2. 若泛型类或接口不具共变性或逆变性,则称为不可变的或严谨的。

18.2 自定义枚举

18.2.1 了解java.lang.Enum类
  1. 直接撰写程序继承Enum类会被编译程序拒绝。
  2. Enum是个抽象类,无法直接实例化,它操作了Comparable接口。Action的构造函数被声明为private,因此只能在Action类中调用。
  3. 在JDK1.4之前撰写的API,仍是使用interface定义常数作为枚举值。
  4. Enum的equals()方法与hashCode()方法基本上继承了Object的行为,但被标示为final。由于标示为final,所以定义枚举是,不能重新操作equals()与hashCode(),这是因为枚举成员,在JVM中智慧存在单一实例,Object定义的equals()与hashCode()作为对象相等性比较是适当的定义。
18.2.2 enum高级运用
  1. values()方法,将内部维护Action枚举实例的数据复制后返回。如果想要知道有哪些枚举成员,就可以使用这个方法。
  2. Enum类可以自行定义构造函数,但不得为公开构造函数,也不可以在构造函数中调用super()。
  3. 在enum中调用构造函数比较特别,直接在枚举成员后加上括号,就可以指定构造函数需要的自变量。
  4. 在static区块中,编译程序仍自行维护name与ordinal的值,接着才是调用自定义构造函数时传入的value值。
  5. 特定值类本体语法:在枚举成员后,直接加上{}操作Command的execute()方法,这代表着每个枚举实例都会有不同的execute()曹组欧,在职责分配上,比switch的方式清楚许多。特定值类本体语法不仅在操作接口时可以使用,,也可以运用在重新定义父类方法。

18.3 关于注释

18.3.1 常用标准注释
  1. @Override
    就是标准注释,被注释的方法必须是父类或接口中已定义的方法,请编译程序协助是否真的为重新定义方法。
  2. @Deprecated
    如果某个方法原先存在与API中,后来不建议再使用,可以在该方法上注释。若有用户后续想调用或重新定义这个方法,编译程序会提出警告。对于支持泛型的API,建议明确指定泛型真正类型,如果没有指定,编译程序会提出警告。
  3. @SuppressWarnings
    指定抑制unchecked的警告产生:@SuppressWarnings(value={"unchecked"})
  4. @SafeVarargs
    表明开发人员确定避免了heap pollution问题。heap pollution问题就是编译程序无法检查执行时期的类型错误,无法具体确认自变量类型。
  5. @FunctionalInterface
    让编译程序可协助检查interface是否可做为lambda的目标类型
18.3.2 自定义注释类型
  1. 标示注释:就是注释名称本身就是信息,对编译程序或应用程序来说,主要是检查是否有注释出现,并作出对应的动作。
    2.相关规则:
(1)如果注释名称本身无法提供足够信息,设置单值注释 
(2)注释属性也可以用数组形式指定。 
(3)在定义注释属性时,如果属性名称为value,则可以省略属性名称,直接指定值。 
(4)对成员设定默认值,使用default关键字即可。 
(5)要设定数组默认值,可以在default之后加上{},必要时{}中可放置元素值。
  1. 定义注释时,可使用java.lang.annotation.Target限定时可指定java.lang.annotation.ElementType的枚举值。
  2. 在制作JavaDoc文件时,默认不会将注释数据加入文件中,如果想要将注释数据加入文件,可以使用java.lang.annotation.Documented
  3. 默认父类设定的注释,不会被继承至子类,在定义注释时,设定java.lang.annotation.Inherited注释,就可以让注释被子类继承。
18.3.3 JDK8标注增强功能
  1. ElementType的枚举成员是用来限定哪个声明位置可以进行标注。在JDK8中,增加了两个枚举成员TYPE _PARAMETERTYPE _USE
  2. ElementType.TYPE _ USE可用于标注在各式类型,一个标注如果被设定为ElementType.TYPE_USE,只要是类型名称,都可以进行标注。
  3. @Repeatable
    可以让你在同一个位置重复相同标注
  4. @Filters
    作为收集重复标注信息的容器,而每个@Filters储存各自指定的字符串值。
18.3.4 执行时期读取注释信息
  1. 自定义注释,默认会将注释信息存储于.class文档,可被编译程序或位码分析工具读取,但执行时期无法读取注释信息,在执行时期读取注释信息,可以使用java.lang.annotation.Retention搭配java.lang.annotation.RetentionPolicy枚举指定。
  2. RetentionPolicy为RUNTIME的时机,在于让注释在执行时期提供应用程序信息,可使用java.lang.reflect.AnnotatedElement接口操作对象取得注释信息。
  3. JDK 8中新增了getDeclaredAnnotation()getDeclaredAnnotationsByType()getAnnotationsByType()三个方法。
    getDeclaredAnnotation()可以让你取回指定的标注,在指定@Repeatable的标注时,会寻找收集重复标注的容器。
    getDeclaredAnnotationsByType()getAnnotationsByType()就不会处理@Repeatable的标记。